-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
::: 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로써.. 프로세스를 종료시키는 명령이죠..
흐.. 더 자세한 것은 저도 더 공부해 봐야 겠습니다..