Keyword : PLT & GOT 심층 분석. 왜사용되나? 그리고 어떠한 상관 관계에 있는가?
우선 아래 몇가지 용어는 확실하게 정의하고 넘어가자.
.GOT(Global offset Table) | 함수 호출 시 가변적이면서도 빠르게 검색을 하기 위해 생성되는 TABLE 이다. 첫 호출시엔 코드 영역의 벡터 주소가 들어있고, 두번째 부터는 그 함수의 실제 주소가 들어가게 된다. |
.PLT(Procedure Linked Table) | 컴파일 타임에 생성되는 테이블로 어떠한 GOT영역의 주소를 참조 할지 정해져 있다. |
_dl_runtime_resolve | _dl_runtime_resolve 함수는 전달된 인자 값을 사용하여 호출된 함수의 실제 주소를 구한 후 GOT에 저장한 뒤 호출된 함수로 점프한다. (GOT을 두번째 참조 할때부턴 호출되지 않는다.) |
_init | 컴파일시 생성되는 함수로 미리 선언 되어 있는 코드 영역이다. |
정확하지 않을 수 있지만, 커널은 어떠한 함수가 호출 될때, 자주 호출될 법한, 빈도수 높은 함수들을 GOT 에 집어 넣고,
빠르게 사용하기 위해 위와 같은 구조로 설계 되어 있는 듯하다.
내가 이제부터 작성할 문서는 사실 GOT 영역을 컴파일시에 결정해 놓으면 PLT 과정이 생략되도 되지 않을까? 하는 생각에서 시작된 분석이다. 결론적으론 GOT 영역에 모든 함수의 실제 주소값을 다 넣어두게 된다면, GOT 자체의 크기도 너무 커져 버리고, 무의미하게 되는 것이다.
보통 검색을 하다보면 PLT 와 GOT 에 대해서만 설명이 많이 되어 있는데, 내가 생각하기에 결론적으로 중요한 핵심은
_dl_runtime_resolve 가 맡고 있었다. 이놈이 GOT에 함수주소들을 찾아 저장시켜줌으로써 아래와 같은 과정이 이뤄지는 것이다.
@Fedora_1stFloor TEST]$ readelf -S test [ 9] .rel.plt REL 08048288 000288 000018 08 A 4 11 4 [10] .init PROGBITS 080482a0 0002a0 000017 00 AX 0 0 4 [11] .plt PROGBITS 080482b8 0002b8 000040 04 AX 0 0 4 [12] .text PROGBITS 080482f8 0002f8 0001d0 00 AX 0 0 4 [13] .fini PROGBITS 080484c8 0004c8 00001a 00 AX 0 0 4 [14] .rodata PROGBITS 080484e4 0004e4 000011 00 A 0 0 4 [15] .eh_frame PROGBITS 080484f8 0004f8 000004 00 A 0 0 4 [16] .ctors PROGBITS 080494fc 0004fc 000008 00 WA 0 0 4 [17] .dtors PROGBITS 08049504 000504 000008 00 WA 0 0 4 [18] .jcr PROGBITS 0804950c 00050c 000004 00 WA 0 0 4 [19] .dynamic DYNAMIC 08049510 000510 0000c8 08 WA 5 0 4 [20] .got PROGBITS 080495d8 0005d8 000004 04 WA 0 0 4 [21] .got.plt PROGBITS 080495dc 0005dc 000018 04 WA 0 0 4 [22] .data PROGBITS 080495f4 0005f4 00000c 00 WA 0 0 4 [23] .bss NOBITS 08049600 000600 000004 00 WA 0 0 4 [12번] 텍스트 영역에는 컴파일시 생성된 어셈 코드들이 담겨 있다. 그리고 좀더 높은 영역으로 가다보면 main 함수를 거쳐 got.plt 영역까지 보이게 된다. (gdb) x/50xi 0x080482f8 0x80482f8 <_start>: xor %ebp,%ebp 0x80482fa <_start+2>: pop %esi 0x80482fb <_start+3>: mov %esp,%ecx |
아래는 printf 를 두번 호출해 코드의 디버깅 모습이다. 빨간색으로 표히산 부분들이 printf를 호출하는 부분인데 이를 트레이싱해보겠다.
(gdb) disass main Dump of assembler code for function main: 0x080483b8 <main+0>: push %ebp 0x080483b9 <main+1>: mov %esp,%ebp 0x080483bb <main+3>: sub $0x8,%esp 0x080483be <main+6>: and $0xfffffff0,%esp 0x080483c1 <main+9>: mov $0x0,%eax 0x080483c6 <main+14>: add $0xf,%eax 0x080483c9 <main+17>: add $0xf,%eax 0x080483cc <main+20>: shr $0x4,%eax 0x080483cf <main+23>: shl $0x4,%eax 0x080483d2 <main+26>: sub %eax,%esp 0x080483d4 <main+28>: sub $0xc,%esp 0x080483d7 <main+31>: push $0x80484ee 0x080483dc <main+36>: call 0x80482e8 <_init+72> 0x080483e1 <main+41>: add $0x10,%esp 0x080483e4 <main+44>: sub $0xc,%esp 0x080483e7 <main+47>: push $0x80484f0 0x080483ec <main+52>: call 0x80482e8 <_init+72> . . (중략) 처음 call 0x80482e8 <_init+72> 호출되는 부분이 PLT 이다. 이를 따라가보면 아래와 같다. (gdb) x/10xi 0x80482e8 0x80482e8 <_init+72>: jmp *0x80495f0 0x80482ee <_init+78>: push $0x10 0x80482f3 <_init+83>: jmp 0x80482b8 <_init+24> 처음 호출된 주소를 따라가면 jmp *0x80495f0 를 만나게 된다. 이를 살펴보면 GOT 영역이란 것을 알 수 있다. 여기까지는 PLT -> GOT을 참조하는 영역으로써 컴파일 시에 이미 결정 되어 있는 것이다. 그값을 보면 GOT영역이라는 것을 확연히 볼 수 있다. (gdb) x/12x 0x80495f0-20 //테이블을 전체를 보여주기 위해 20만큼 빼 보기좋게 표현했다. 0x80495dc <_GLOBAL_OFFSET_TABLE_>: 0x08049510 0x007194f8 0x0070e9e0 0x080482ce 0x80495ec <_GLOBAL_OFFSET_TABLE_+16>:0x00730d50 0x080482ee 0x00000000 0x00000000 이러한 테이블이 참조하는 영역은 아래와 같다. (gdb) x/10xi *0x80495f0 0x80482ee <_init+78>: push $0x10 0x80482f3 <_init+83>: jmp 0x80482b8 <_init+24> 위에서 보면 이러한 글로벌 테이블은 처음 호출시에는 위와같이 한번더 점프하게 되는데, 그영역을 봐보면 아래와같이 어떠한 인자값을 넣고, dl_runtime_resolve 영역으로 점프하게 된다. (gdb) x/3i 0x80482b8 0x80482b8 <_init+24>: pushl 0x80495e0 0x80482be <_init+30>: jmp *0x80495e4 _dl_runtime_resolve 에서는 printf의 코드 영역의 주소를 찾아 GOT 테이블에 삽입하고 호출 후, 다시 복귀하는 루틴으로 구성 되어 있다. (gdb) x/3i *0x80495e4 0x70e9e0 <_dl_runtime_resolve>: push %eax 0x70e9e1 <_dl_runtime_resolve+1>: push %ecx 0x70e9e2 <_dl_runtime_resolve+2>: push %edx 자 이제 두번째 call 0x80482e8 <_init+72> 호출되는 부분이 PLT 이다. 간단하게 break point 를 걸어놓고 한번 printf 가 호출한 뒤 디버깅을 똑같이 진행해 보겠다. (gdb) b *main+41 (gdb) c //함수를 한번 호출시킨뒤 계속 진행 (gdb) x/3xi 0x80482e8 0x80482e8 <_init+72>: jmp *0x80495f0 0x80482ee <_init+78>: push $0x10 0x80482f3 <_init+83>: jmp 0x80482b8 <_init+24> (gdb) x/12x 0x80495f0-20 //테이블을 보기 좋게 20을 빼준뒤 출력시켜 줌 0x80495dc <_GLOBAL_OFFSET_TABLE_>: 0x08049510 0x007194f8 0x0070e9e0 0x080482ce 0x80495ec <_GLOBAL_OFFSET_TABLE_+16>:0x00730d50 0x0075e660 0x00000000 0x00000000 (gdb) x/3xi *0x80495f0 0x75e660 <printf>: push %ebp 0x75e661 <printf+1>: mov %esp,%ebp 0x75e663 <printf+3>: lea 0xc(%ebp),%eax 위 과정을 보면 첫번째 호출 했을때의 과정이 사라지고 printf 함수가 바로 호출 되었음을 볼 수 있다. 즉 첫번째 과정에서 _dl_runtime_resolve 함수가 GOT 테이블에 printf 주소를 입력 시켜 놓은 것이다. |
'System_Hacking' 카테고리의 다른 글
GDB 명령어 완벽 가이드 (0) | 2013.06.10 |
---|---|
[R.O.P.] ropeme 사용하여 ROP 가젯 쉽게 구하기. (0) | 2013.04.19 |
구홍이 형이 쓰신 페도라 오버플로우 공략법 글 (0) | 2013.03.05 |
ARM 쉘코드 getgid 추가 (0) | 2012.12.01 |
ASLR 끄는법?! (0) | 2012.10.30 |