버퍼오버플로우라는 키워드를 검색하셔서 이 글을 보시고 계신다면, 버퍼오버플로우가 대충 무엇인지는 알고 계실거라 생각합니다.
아마도 이 허접한 글까지 검색해서 보게된 경우라면 마음에 와 닿을 정도로 이해를 하지 못하였기 때문이라고 생각되는데 맞나요? 저도 그랬거든요 -_-
그래서 기본적인 정의 같은건 생락을 하고, 버퍼오버플로우를 이해하는데 가장 필요한 것들이라 생각되는 것과 제가 너무 어렵게 느꼈던 부분에 대하여 최대한 쉽고 간단하게 정리를 해보고자 합니다.
오버플로우에는 힙 기반 오버플로우와 스택 기반 오버플로우 두 가지가 있습니다.
여기에서는 스택 기반 오버플로우에 대해서 다루도록 하겠습니다.
1. 스택의 특성을 알아야 한다는데...
스택의 특성에 대한 개념자체는 찹 쉽습니다. 좋은 문서들을 많이 읽어보셨을테니 이에 대해서는 생락을 하고 스택의 특성을 눈으로 확인해가면서 마음으로 받아들이는 기회를 가져 볼게요.
(1)프로그램 실행시 메모리의 구조
하나의 프로그램이 실행되면 프로그램 수행에 필요한 메모리가 할당 됩니다. 하나의 프로그램에 할당되는 메모리의 전체적인 모습은 다음과 같죠.
/--------------------/ ← 메모리의 높은 숫자의 주소
| |
| Stack |
| |
/------------------/
| |
| Heap |
| |
/--------------------/
| |
| Data |
| |
/--------------------/
| |
| Text |
| |
/--------------------/ ←메모리의 낮은 숫자의 주소
Heap영역이나 Data영역, Text영역에서 변수에 그 자리를 줄 때에는 "메모리의 낮은 주소"에서 부터 "메모리 높은 주소"로 순서대로 한다고 합니다.
스택이 거꾸로 자란다는 것은, 위에서 말한 다른 메모리의 영역과는 반대로 변수에 그 자리를 내어줄 때에 "메모리의 높은 주소"에서부터 "메모리 낮은 주소"로 한다는 것 입니다. 스택이 독특한거죠.
(2)예제를 통해 이해해 보아요...(스택은 거꾸로 자란다)
int a;
int b;
int c; //각각 a, b, c라는 전역변수가 순서대로 선언되었습니다.
int main( void )
{
char buffer1[7];
chat buffer2[7]; //main함수 안에서 각각 buffer1, buffer2라는 배열이 선언되었습니다.
}
이러한 내용을 가진 프로그램이 수행될 때 전체메모리의 구조를 그림으로 보도록 하겠습니다.
/-------------------------/ 메모리의 높은 숫자의 주소
| ret ① |
| |
| sfp(4byte)② |
| | Stack
| buffer1 ③ |
| |
| buffer2 ④ |
/-------------------------/
| | Heap
/-------------------------/
| c ③ |
| b ② | Data
| a ① |
/-------------------------/
| | Text
/-------------------------/ 메모리의 낮은 숫자의 주소
스택영역과 데이타영역을 비교해보니 '스택은 거꾸로 자란다'는 말이 이해되시죠? 먼저 선언된 순서에 따라 메모리에 자리를 잡는데 Data영역과 Stack영역이 반대로 진행되었습니다.
다시 한번 더 말하자면, 데이터영역은 변수가 선언된 순서대로 메모리의 낮은 주소에서 시작해서 할당이 되는데, 스택영역에서는 먼저 선언된 변수가 메모리 높은 주소에서 시작해서 할당 받는 것입니다.
(3)스택에 자리잡은 변수에 입력한 데이타가 들어간 모습
저는 처음에 스택이 거꾸로 자란다길레 스택에 있는 변수에 데이터가 들어갈 때 "메모리 높은 주소"에서부터 들어가는줄 알았습니다.(좋은 문서들을 주의깊게 잘 읽어보지 못한 탓이었겠죠 -_-;; 멍청하거나 띨빵하거나..) 혹시 저와같이 오해를 하는 분이 있을까 스택의 변수에 데이터가 들어가는 모습을 살펴보겠습니다.
/-------------------------/ 메모리의 높은 숫자의 주소
| ret |
/-------------------/
| sfb(4byte) |
/-------------------/
| 7 |
| 6 |
| 5 |
| 4 | buffer1
| 3 |
| 2 |
| 1 |
/-------------------/ Stack
| G |
| F |
| E |
| D | buffer2
| C |
| B |
| A |
/-------------------------/ 메모리의 낮은 숫자의 주소
자 이해되셨죠? 스택이 거꾸로 자란다는 것과 오해 없으시길 바랍니다.
여기까지 거꾸로 자란다는 스택의 특성을 알아보았습니다. 프로그램 수행시 메모리의 전체적인 모습도 대충 감이 오시죠?
2. 스택의 모습(함수안에서 함수가 호출될 때)
자 이번에는 main함수 외에 다른 함수도 있고 함수에 인자가 주어진 경우에는 스택이 어떤모습인지 살펴보겠습니다.
아래와 같은 소소의 프로그램이 있다고 가정합니다.
void function( int a, int b, int c ) //function함수에 세개의 인자가 주어져 있네요.
{
char buffer1[5]; //buffer1 배열을 선언
chat buffer2[5]; //buffer2 배열을 선언
}
int main( void ) //main함수가 시작됩니다.
{
int super; //지역변수 super 선언
function( 1, 2, 3 ); // function 함수에 인자 값을 주며 호출합니다.
}
이 프로그램이 수행될 때 전체 스택의 모습은 다음과 같게 될 것입니다.
메모리의 높은 숫자의 주소
/-------------------------/ 메인함수 콜
| ret |
/-------------------/
| sfb(4byte) |
/-------------------/
| | 변수 super // int super; Stack
/-------------------/
| c=3 |
| b=2 | function함수의 인자 // "함수의 인자"는 뒤의 것부터
| a=1 | 스택에 들어갑니다(c, b, a)
/-------------------/ function함수 콜 // 함수의 인자가 먼저 들어가고
| ret | 함수가 호출됩니다.
/-------------------/
| sfb(4byte) |
/-------------------/
| buffer1[5] |
/-------------------/
| buffer2[5] |
/-------------------------/
메모리의 낮은 숫자의 주소
자 함수내에서 함수가 호출될 경우와 함수에 인자가 주어진 경우 스택의 모습을 살펴보았습니다. 이해되셨죠?
3. 오버플로우
스택의 구조와 특성을 살펴보았습니다. 자 이제 본격적으로 오버플로우로 들어갑니다. 이해하는데 어려운 점은 없으니 천천히 읽어보도록 합시다.
void function( int a, int b, int c )
{
char buffer1[5];
chat buffer2[5];
}
int main( void )
{
int super;
function( 1, 2, 3 );
}
(이 예제 프로그램에서는 buffer1이나 buffer2에 입력하는 부분은 없습니다만 편의상 buffer2에 문자열이 입력되었다고 가정하고 설명을 해나가겠습니다)
이 소스를 보면 buffer1와 buffer2는 그 크기가 각각 5바이트로 정해놓았습니다. 그런데 우리는 이 5바이트를 넘는 것을 입력함으로써 버퍼오버플로우가 발생되게 하는 것입니다.
buffer2에 5바이트가 넘는 문자열을 넣으면 스택은 다음과 같이 됩니다. buffer2에 "ABCDEFGHI"(9바이트)가 들어갔다고 가정한 결과입니다.
윗 그림에서 function함수 부분만 잘라와서 설명을 하겠습니다.
메모리의 높은 숫자의 주소
/-------------------/ function함수 콜
| ret |
/-------------------/
| sfb(4byte) |
/-------------------/
| |
| I |
| H | buffer1[5]
| G |
| F |
/-------------------/
| E |
| D |
| C | buffer2[5]
| B |
| A |
/-------------------------/
메모리의 낮은 숫자의 주소
어떻습니까? 그림 이해 되시죠?
버퍼가 저장할 수 있는 데이터의 크기가 5바이트라고 해서 5바이트 까지만 저장을 하는 것이 아니라, 입력된 것을 모두 저장하기 위해 그냥 자기에게 할당되지 않은 다음 메모리영역에까지 써버러는 것입니다.
더욱 큰 문자열을 넣는다면 sfb부분과 ret부분까지 덮어 써 버릴 수가 있습니다.
이것이 바로 버퍼오버플로우 입니다.
이러한 현상은 이용자가 입력하는 문자열을 저장하는 버퍼의 크기가 5바이트일 뿐인데, 이용자로부터 입력을 받을 때에 5바이트를 초과하는 문자열을 입력할 수 있도록 프로그래밍한 경우 발생하는 현상입니다.
4. 버퍼오버플로우를 이용한 해킹
버퍼오버플로우를 이용하여 해킹을 하기 위해서는 ret가 무엇인지 알고 계셔야 합니다. 버퍼오버플로우에 대한 좋은 문서들이 많이 있으니 그런 문서를 보셨다면 충분히 아실거라 생각하고 이에 대한 설명은 생략하겠습니다.
ret는 함수가 임무를 수행하고 끝난뒤 다음 실행되어야할 명령이 위치한 메모리의 주소를 말하지요.
그런데 우리는 버퍼오버플로우를 이용하여 ret까지 덮어쓸 수가 있게 되었습니다.
따라서 버퍼오버플로우를 일으켜서 ret부분에 자신이 원하는 명령이 들어가 있는 메모리의 주소로 덮어쓴다면, 자신이 원하는 명령을 수행할 수 있도록 하는 것입니다.
대부분 해킹의 경우에는 쉘을 받아내는 명령을 수행하도록 하겠지요. 쉘을 실행시키는 코드를 메모리의 어딘가 저정해놓 뒤 그 주소를, ret부분에 써지도록 하면 함수 종료 후에 쉘을 실행시키는 코드가 실행되고, 따라서 쉘을 받게 되는 것입니다.
버퍼오버플루우가 발생할 수 있는 프로그램이 root의 setuid가 걸려있고, 쉘을 실행시키는 명령이나 코드가 들어가 있는 메모리의 주소를 ret에 덮어쓴다면 root의 쉘을 따낼 수 있는 것입니다. 이렇게 하여 버퍼오버플로우를 이용하여 해킹을 할 수 있습니다.
5. 버퍼에 대한 중요한 추가 설명
(1)리눅스에서는 변수에 메모리를 1워드 즉 4byte단위로 할당한다고 합니다. buffer1[5]와 같이 5바이트 크기를 선언하였지만 리눅스는 8바이트를 할당하는 것입니다. 11바이트를 선언하였다면 리눅스는 메모리의 12바이트를 그 변수에 할당하게 됩니다.
(이부분에 대해서는 정말 그러한지 직접 확인해보고 싶은데, 아래 설명할 gcc 2.96버전 이상에서 발생하는 쓰레기값과 붙어있어서 확인을 못하겠네요 -_-;)
(2)버퍼의 구조 변화
저는 지금까지 버퍼오버플로우의 기본적인 이해를 위해 gcc 2.96이전 버전으로 프로그램을 컴파일한 경우에 기초하여 설명을 하였습니다.
그런데 gcc 2.96이상으로 버전이 바뀌면서 버퍼 구조에 변화가 생겼습니다.
버퍼의 각 변수뒤에 쓰레기값이 형성된다고 합니다.
그림으로 살펴보죠.
메모리의 높은 숫자의 주소
/-------------------/ function함수 콜
| ret |
/-------------------/
| sfb(4byte) |
/-------------------/
| |
| 쓰레기 | (gcc 2.96이상 버전으로 컴파일 되면서,
| | 이 사이에 쓰레기 값이 형성됨)
/-------------------/
| |
| I(?) |
| H(?) | buffer1[5]
| G(?) |
| F(?) |
/-------------------/
| |
| |
| I(?) | 쓰레기
| H(?) | (gcc 2.96이상 버전으로 컴파일 되면서,
| G(?) | 이 사이에 쓰레기 값이 형성됨)
| F(?) |
/-------------------/
| E |
| D |
| C | buffer2[5]
| B |
| A |
/-------------------------/
메모리의 낮은 숫자의 주소
이렇게 됨으로써 FGHI가 어느부분에 들어간다고 확신할 수 가 없게 되었습니다.
그런데 이때 형성되는 쓰레기값들의 크기가 일정하지 않습니다. 따라서 윗 그림에서 오버플로우 된 FGHI는 buffer1에 덮어써지지 않을 가능성이 높아졌습니다.
이러한 현상으로 ret에 다른 주소값을 덮어쓰기가 어려워졌습니다. 크기를 알 수 없는 쓰레기값들 때문인것이죠.
그래서 딱히 눈에 보이는 방법이라면 입력하는 값을 하나씩 늘려가면서 매번 실행해보는 수밖에 없어보입니다.
자 여기까지 저 나름대로 공부한 버퍼오버플로우를 정리해 보았습니다. 메모리의 구조나 ret의 위치를 파악하는데 어셈블리나 gdb를 사용할 줄 안다면 큰 도움이 된다고 들었습니다. 저는 어셈블리나 gdb사용법에 대해서 아는 바가 없기때문에, 미흡한 점이 있다면 양해해 주시구요. 감사합니다.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
::: Stack Frame Pointer Overflow 개념 이해하기 :::
written by naska21 in WiseGuys
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
:: 목차 ::
0x00. 준비하기
0x01. 이해하기
0x02. 마무리
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
0x00. 준비하기
이 문서를 공부 하기 이전에 이 문서를 읽는 사람에겐 스택에 관한 배경 지식
이 미리 깔려 있어야하며, 기본적인 bof의 개념을 이해했길 바란다.
게다가 프로그램이 실행 되는 동안 스택에 어떠한 정보가 쌓여가는지, 그리고
그러한 정보들은 어떠한 역할을 하는지에 대해서도 이곳에서 물론 언급할 것이
지만 미리 알고 있다면 이해가 쉬울 것이다.
그리고 마지막으로 어셈블리어를 조금이라도 이해할 수 있다면 이해하는데 큰
도움이 될 것이다.
우선 우리가 어떤 프로그램을 실행할때 스택에는 어떠한 정보들이 쌓이게 될
까? 그림으로 이해를 돕자..
| ... | Low address
+------------+
| ... |
+------------+
| SFP |
+------------+
| RET |
+------------+ High address
어떤 프로그램을 실행하게 될때, 스택에는 실행 되는 함수에 관련된 정보들이
쌓이게 된다. 리턴 어드레스(eip), 프레임 포인터(sfp), 지역변수들...
리턴 어드레스에는 해당 함수가 종료되고 난 후 다음 실행 코드의 주소를 저장
하고 있다. 함수가 종료되고 난 후 프로세스는 ret를 스택에서 꺼내어 해당 주
소로 이동하여 다음 코드를 계속 수행하게 될 것이다. 변수의 boundary check를
하지 않은 취약한 프로그램에서 변수를 오버플로우 시켜 이 ret를 덮어쓰므로서
프로그램의 흐름을 원하는 곳으로 바꾸는 것이 바로 stack overflow 공격이다.
Frame pointer overflow는 ret가 아닌 sfp의 1바이트를 덮어 쓰므로써 우리가
원하는 대로 프로그램의 실행 흐름을 바꿀수가 있다. 우리가 이 문서에서 공부
할 내용은 바로 sfp인 것이다. 그렇다면 sfp는 무엇이고, 어떤 원리로 프로그램
의 실행 흐름을 바꾸는 가능하게 되는 것일까?
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
0x01. 이해하기
우선 미리 언급해 놓을 것은 프레임 포인터 오버플로우는 main()에서는 적용이
되지 않는다는 점이다. 그 이유는 나중에 알아 보겠다.
우선 어떤 프로시져가 수행될때 내부적으로 스택에 무슨 일을 하는지 어셈코드
를 살펴볼 필요가 있다. 프로그램의 실행 흐름을 알기 위해서이다.
이 코드에서는 ebp의 의미와 역할에 대해 알아볼 것이다. 이미 알고 있는 분은
다음으로 넘어가도 좋다.
main()
{
int a = 10;
printf("%d\n", a);
}
이 소스를 컴파일 하여 gdb로 디스어셈해 보겠다. 참고로 이 문서에서는 컴파일
옵션 (-mpreferred-stack-boundary=2)으로 컴파일 했다.
[root@test acm]# gdb -q ./sfp
(gdb) disas main
Dump of assembler code for function main:
0x8048460 <main>: push %ebp
0x8048461 <main+1>: mov %esp,%ebp
0x8048463 <main+3>: sub $0x4,%esp
0x8048466 <main+6>: movl $0xa,0xfffffffc(%ebp)
0x804846d <main+13>: pushl 0xfffffffc(%ebp)
0x8048470 <main+16>: push $0x80484e8
0x8048475 <main+21>: call 0x804833c <printf>
0x804847a <main+26>: add $0x8,%esp
0x804847d <main+29>: leave
0x804847e <main+30>: ret
0x804847f <main+31>: nop
End of assembler dump.
(gdb)
어셈 코드를 훑어 보자.
0x8048460 <main>: push %ebp
--> 현재의 ebp값을 스택에 저장한다. 바로 이부분이 sfp가 된다. 그럼 ret는?
ret가 스택에 쌓이는건 어셈코드에는 생략이 되어 있다. ebp값이 무엇인지는
뒤에 설명하도록 한다.
0x8048461 <main+1>: mov %esp,%ebp
--> 현재의 esp(스택포인터)값을 ebp에 저장한다. 그럼 현재 상태에서 스택을 그
려 보자..
| ... | Low address
+------------+
| ... |
esp(ebp)--->+------------+
| SFP |-> 이전 ebp값을 가지고 있다.
+------------+
| RET |
+------------+ High address
이 그림을 잘 눈여겨 봐 두시길..
0x8048463 <main+3>: sub $0x4,%esp
--> 이부분은 a라는 int형(4byte)변수를 사용하기 위해 4바이트 만큼 스택에 공
간을 할당하는 부분이다. esp에서 4를 뺀다. 즉 스택 포인터를 4바이트 만큼
이동시킨다. 어디로? 4를 뺐으므로 주소값이 감소 했다. 즉, 위쪽으로 올라
갈수록 주소값이 감소하므로, 현재 상태를 그림으로 보면,
(참고 : 스택의 한칸은 기본적으로 워드 단위(4byte)이다.)
| ... | Low address
esp---->+------------+
| a |
ebp---->+------------+
| SFP |
+------------+
| RET |
+------------+ High address
이런 형식이 되겠다. ebp는 그대로 있고 esp만 스택그림에서 위쪽으로 이동
하면서 지역변수 a를 사용하기 위한 공간을 할당 했다.
0x8048466 <main+6>: movl $0xa,0xfffffffc(%ebp)
--> a에 0xa(십진수 10)을 넣어주는 부분이다. 근데 이부분을 잘 보시라.
a라는 곳을 가르키기 위해 0xfffffffc(int형으로 -4를 나타냄)(%ebp)를 사용
하였다. 즉.. ebp를 기준으로 -4한 곳을 참조해 지역 변수 a에 값을 할당한
것이다.
***여기서 ebp의 역할을 설명하겠다. esp는 pop이나 push같이 스택에 값을 넣거
나 빼낼때 유동적으로 값이 변하면서 이동을 한다. 스택포인터는 항상 스택의
가장 윗부분을 가르키고 있어야 하기 때문이다. 따라서 지역변수에 접근하기
위해서는 상대적으로 거리가 항상 일정한 어떠한 기준점이 필요하게 된 것이다
그 기준점이 바로 ebp레지스터인 것이다.
0x804846d <main+13>: pushl 0xfffffffc(%ebp)
--> printf("%d\n", a);를 수행하기 위해 스택에 인자 a의 값을 먼저 넣어 준다.
여기서도 ebp값을 기준으로 지역 변수 a의 값을 참조하였다.
0x8048470 <main+16>: push $0x80484e8
--> printf("%d\n", a);에서 "%d\n"이 있는 주소를 push하였다.
0x8048475 <main+21>: call 0x804833c <printf>
--> printf함수를 호출했다.
0x804847a <main+26>: add $0x8,%esp
--> 인자를 넘겨 주기 위해 썼던 스택공간을 반환했다.
0x804847d <main+29>: leave
0x804847e <main+30>: ret
--> 나중에 다시 설명하겠다.
자.. 여기까지 ebp의 이해를 돕기 위해 어셈코드로 설명을 하였습니다.
아까 sfp에 이전 ebp값이 들어가는 걸 확인 할 수 있었습니다. 하지만
이것만 가지고는 sfp에 대한 이해가 부족할 것 같습니다.
그러면 sfp의 이해를 위해 하나의 소스만 더 봅시당..
(어.. 말투가 바꼈당..-0-;;)
sub()
{
int c;
int d;
}
main()
{
int a;
int b;
sub();
}
컴파일 해서 gdb로 살펴보면,
[root@test acm]# gcc sfp.c -o sfp -mpreferred-stack-boundary=2
main() 부분..
[root@test acm]# gdb -q ./sfp
(gdb) disas main
Dump of assembler code for function main:
0x8048438 <main>: push %ebp
0x8048439 <main+1>: mov %esp,%ebp
0x804843b <main+3>: sub $0x8,%esp
0x804843e <main+6>: call 0x8048430 <sub>
0x8048443 <main+11>: leave
0x8048444 <main+12>: ret
0x8048445 <main+13>: lea 0x0(%esi),%esi
0x8048448 <main+16>: nop
sub() 부분..
(gdb) disas sub
Dump of assembler code for function sub:
0x8048430 <sub>: push %ebp
0x8048431 <sub+1>: mov %esp,%ebp
0x8048433 <sub+3>: sub $0x8,%esp
0x8048436 <sub+6>: leave
0x8048437 <sub+7>: ret
End of assembler dump.
(gdb)
그럼 여기서 <main+3>과 <sub+3>에 break point를 걸어놓구 실행해 보면서
ebp, esp의 변화를 살펴 보겠습니다.
(gdb) b *main+3
Breakpoint 1 at 0x804843b
(gdb) b *sub+3
Breakpoint 2 at 0x8048433
(gdb) r
Starting program: /home/naska/acm/./sfp
Breakpoint 1, 0x0804843b in main ()
(gdb) info reg $ebp $esp
ebp 0xbffffa28 0xbffffa28
esp 0xbffffa28 0xbffffa28
이제부턴 스택을 옆으로 그리겠습니다.. 오른쪽이 높은 주소(스택의 바닥)
이고 왼쪽이 낮은 주소(스택의 top)입니다.
이 상태에서 스택을 그려 보면,
<main frame>
[sfp][ret]
^--ebp, esp
즉, 현재, ebp값과 esp값이 같죠..
계속 실행해보죠..
(gdb) ni
0x0804843e in main ()
(gdb) info reg $ebp $esp
ebp 0xbffffa28 0xbffffa28
esp 0xbffffa20 0xbffffa20
ni명령은 nexti, 어셈 한 명령을 실행시키는 겁니다.
호오.. esp값이 8감소 했습니다. ebp는 그대로 자신의 자리를 지키고 있군요.
0x804843b <main+3>: sub $0x8,%esp
이부분이 실행된 후가 되겠죠..
스택을 그려 보면,
<main frame>
[b][a][sfp][ret]
^-esp ^-ebp
계속 해보죠..
(gdb) ni
Breakpoint 2, 0x08048433 in sub ()
(gdb) info reg $ebp $esp
ebp 0xbffffa18 0xbffffa18
esp 0xbffffa18 0xbffffa18
이젠 sub()로 넘어 갔습니다. 어셈 코드를 보면 아시겠지만 sub()에서는 sub()의
지역변수가 있기 때문에 새로운 sub()의 ebp를 설정해줘야 합니다. 그래야 sub()
의 지역변수로 접근이 용이하기 때문이죠. 그런데 sub()가 끝난 후에는 main()의
ebp가 다시 필요하겠죠? 그래서 main()의 ebp를 sfp에 저장하는 겁니다.
현재 상태에서 스택을 덤프해 보죠..
(gdb) x/2x $esp
0xbffffa18: 0xbffffa28 0x08048443
sub()의 <sfp> sub()의 <ret>
아하~ sfp에 과연 main()의 ebp가 저장되어 있군요..
0x804843e <main+6>: call 0x8048430 <sub>
0x8048443 <main+11>: leave
여기서 0x08048443이 sub()가 call되고 나서 다음 명령코드의 주소이므로 ret가
확실한것 같네요..
그럼 현재의 스택을 그려보면,
<sub frame> <main frame>
[sfp][ret][b][a][sfp][ret]
ebp,esp-^ | ^
+-------------+
계속 실행합시당..
(gdb) ni
0x08048436 in sub ()
(gdb) info reg $ebp $esp
ebp 0xbffffa18 0xbffffa18
esp 0xbffffa10 0xbffffa10
여기서도 지역변수 c와 d를 할당하느라 스택포인터가 8byte감소했죠.
스택 상태는,
<sub frame> <main frame>
[d][c][sfp][ret][b][a][sfp][ret]
esp-^ ebp-^ | ^
+-------------+
계속 실행해 보면,
(gdb) ni
0x08048437 in sub ()
(gdb) info reg $ebp $esp
ebp 0xbffffa28 0xbffffa28
esp 0xbffffa1c 0xbffffa1c
오옷.. ebp가 main()의 ebp로 바꼈네요.. 흐흐.. 이부분은 leave명령을 실행한 결
과입니다. esp는 0xbffffa1c에 있네요.. 여기는 어딜까요~? 봅시당..
(gdb) x/x $esp
0xbffffa1c: 0x08048443
오호~ 바로 sub()의 ret이군요~.. 현재 leave명령을 실행하고 ret를 실행하기 전
스택의 상태 입니당..
그럼 여기서 leave명령에 대해 설명해 드리겠습니다.
leave명령 실행 결과후 우리는 ebp값이 이전 함수의 ebp값을 돌려받는걸 확인했구
요. esp는 ret에 위해하는것을 확인했습니다.
leave명령은
mov ebp, esp
pop ebp
이 두 명령을 수행합니다.
즉, esp값을 현재의 ebp즉, sfp가 있는곳으로 이동시킨 후, sfp값을 pop해서 ebp
에 저장하는 것입니다.(참고로 leave와 ret는 함수가 종료될때 실행되므로 지역변
수는 이때 소멸합니다.)
따라서 sfp에 들어있던 main()의 ebp값은 현재의 ebp값에 저장되고, esp값은 sfp를
가르키고 있다가 pop ebp에 의해 4가 증가하므로 ret를 가르키게 되는 것입니다.
계속 실행해 보면,
(gdb) ni
0x08048443 in main ()
(gdb) info reg $ebp $esp
ebp 0xbffffa28 0xbffffa28
esp 0xbffffa20 0xbffffa20
휴.. 이제 sub()가 호출되기 이전 상태로 돌아오게 되었군요..
여기서 ret수행시 스택에서 ret를 꺼내서 eip에 저장한후 eip에 저장된 주소의 실
행코드를 수행합니다.
(간단히 말해서 절차적 프로그래밍에서 다른곳으로 갔다가 되돌아 올때 goto문을
사용하는 것과 같습니다.:gw-basic)
자.. 여기까지 프로그램 수행 흐름을 쫘악 훑어 보면서 ebp, sfp에 대해 알아봤는
데요.. 그럼 어떻게 해서 sfp의 바뀐 1바이트가 프로그램 수행 흐름을 바꿀 수 있
을까요? 여기까지 이해를 다 하셨다면 이미 알고 계실텐데..
우선 main()이 호출되구요.
그 다음 main() 에서 sub()가 호출되었습니다.
여기서 overflow가 일어나서 sub()의 sfp부분만 값이 변경되었다고 합시다.
이젠 이 sub()가 종료되면서 leave를 수행합니다.
이때 esp는 sub()의 ebp즉.. sfp를 가르키게 될 것이고, pop ebp, 즉, main()의
ebp를 돌려 받는 과정에서 변경된 sfp값을 가져오게 되므로 main()의 ebp는 main()
의 sfp를 가르키는 값이 아닌 다른 값을 갖게 되겠지요..
그럼 여기서(leave를 수행하고 난 후) 정상적인 경우의 스택은
<sub> <main>
[sfp][ret][b][a][sfp][ret]
^--esp ^--ebp
아까처럼 sub()의 sfp가 변경된후 leave가 수행되었을 경우의 스택은
<sub> <main>
[sfp][ret][b][a][sfp][ret]
^--esp
*ebp:어딘가 이상한곳.. 예를 들어 원래 sub()의 sfp가 main()의 ebp
즉, main()의 &sfp값을 가지므로 bffffa08이었다고 할때, 오버플로
가 일어나서 sub()의 sfp가 bffffa48로 변경되었다고 하면,
main()의 ebp가 bffffa48이 되는 것이죠.. 원래 main()의 ebp는
bffffa08이었는데 말이죠..
이렇게 main()의 ebp가 바뀌고 나서, sub()의 ret가 수행되고 다시 계속하여
main()의 다음 명령이 수행되다가, main()의 leave가 수행되게 됩니다.
이때 mov ebp, esp에 의해 변경되어 버린 ebp로 esp가 이동하게 됩니다.
그리고 pop ebp에 의해 4바이트의 어떠한 값이 꺼내어 진후(이 때 pop명령에 의해
esp는 4가 증가하게 되겠죠..),
그곳에서 ret명령에 의해 eip값을 얻어 다음 명령을 수행하게 되죠..
따라서 이때 변경되버린 ebp+4에 &shellcode가 있다면 이 프로세스는 마치 그것이
return address인양 eip로 꺼내어 shellcode를 수행하게 되는 것입니다.
-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
0x02. 마무리
지금까지 우리는 sfp값의 변경에 의해 프로그램의 수행 흐름을 원하는 곳으로 바
꿀수 있는 원리에 대해 알아 보았습니다. 참고로 gcc2.96에서는 이런 경우가 드물
죠? 더미값 땜시 1byte 오버해가지고는 sfp를 바꾸기 힘들듯.. 움하하하~
것두 어떻게든 가능하도록 노력해봐야겠지요~.. 그럼 즐핵~ ;)
** 보너스 **
** 왜? main()의 sfp는 백날 덮어써도 프로그램 실행에는 영향을 미치지 못하는가?
저는 모든 것을 확실히 알지는 못하지만.. 제가 디버깅을 통해 아는것만 말씀드리
면..
main()의 ret를 x/16i <ret값>로 에셈코드를 살펴보면.. 우선..
main()이 끝난후 실행되는 코드는 __libc_start_main 의 코드라는걸 알수 있습니다.
이것을 쭈욱 끝까지 살펴보면
leave명령은 없고
ret만 있습니다.
아마도 여기서는 이전 ebp를 되돌려줄 필요가 없나봅니다.. 히히
sfp값의 변경으로 프로그램의 실행흐름이 바뀌어질때는 leave를 했을때 esp값이 바
뀌고 그로 인해 엉뚱한 eip를 꺼내오기 때문인데.. leave가 없으니 아무 상관이 없겠죠
그리고 하나더..
main()의 sfp값은?? 도대체 머시냐??
main()의 sfp값을 추적해 보면..
0x00000000
을 가르키고 있고
+4byte한 곳에는
어떤 주소가 있습니다. 그 주소를 어셈코드로 살펴보면 'hlt'가 나오는데요,
이 명령은 process halt로써.. 프로세스를 종료시키는 명령이죠..
흐.. 더 자세한 것은 저도 더 공부해 봐야 겠습니다..