System Hacking - how plt & got works
pwnable을 공부하기 시작한지 얼마 되지 않았는데, 공부 중 got&plt 파트가 잘 이해되지 않아서 공부하면서 이를 이해한 흐름을 정리한다. PLT GOT 동작과정 분석포스팅을 통해 plt, got의 동작을 gdb로 따라가면서 분석 및 이해한다.
Code
사용할 코드는 puts()함수를 두번 호출하는 간단한 동작을 한다.
1
2
3
4
5
6
7
8
9
// Name: got.c
// Compile: gcc -o got got.c -no-pie
#include <stdio.h>
int main() {
puts("This is for");
puts("plt and got analyze");
}
puts@plt 호출
1
2
...
0x401148<main+18> call puts@plt
main함수에서 첫번째 puts()함수를 호출하는 부분에서 시작해보자. gdb로 step into!
got테이블 호출
1
2
3
0x401044<puts@plt+4> bnd jmp qword ptr [rip+0x2fcd]
| $rip+7+0x2fcd = 0x404018 <puts@got[plt]>
| 0x404018 -> 0x401030
jmp구문으로 바로 어딘가로 이동한다. 이동하는 메모리 주소를 확인해보면, 0x404018로 나타난다. 이는 0x401030을 저장하는 메모리 주소로, 0x401030는 나머지 puts()의 시작부분이다.
1
2
0x401034 push 0 : reloc_offset
0x401039 bnd jmp 0x401020
- 이후 스택에 0을 push하는데, 이는
reloc_offset이다. 우리 코드에서puts()함수 하나만 사용하므로 이는 0이 된다. 0x401020으로 이동한다.
push &link_map
1
0x401020 push qword ptr [rip+0x2fe2] // link_map 주소 push
0x401020에서는 스택에 [rip+0x2fe2]를 push하는데, 값을 확인해보면 $rip+6+0x2fe2 = 0x404008 -> 0x00007fb83a7f32e0로 나타난다.
여기서 0x00007fb83a7f32e0는 link_map 구조체의 주소다. link_map은 .got.plt 영역의 일부다.
link_map structure
CODEBROWSER:link.h에 따르면, link_map 구조체는 다음과같이 정의된다.
1
2
3
4
5
6
7
8
9
10
struct link_map
{
/* These first few members are part of the protocol with the debugger.
This is the same format used in SVR4. */
ElfW(Addr) l_addr; /* Difference between the address in the ELF
file and the addresses in memory. */
char *l_name; /* Absolute file name object was found in. */
ElfW(Dyn) *l_ld; /* Dynamic section of the shared object. */
struct link_map *l_next, *l_prev; /* Chain of loaded objects. */
};
실제로 0x00007fb83a7f32e0를 gdb로 확인해보면, 40byte를 차지하는 link_map영역의 값을 확인해 볼 수 있다. 여기서 l_ld : 0x0000000000403e20는 .dynamic 영역의 주소다.
1
2
3
4
5
6
7
{.got.plt section}::{link_map}
===============================================================
0x7fb83a7f32e0: 0x0000000000000000 // l_addr
0x7fb83a7f32e8: 0x00007fb83a7f3888 // l_name
0x7fb83a7f32f0: 0x0000000000403e20 // l_ld -> {.dynamic section}
0x7fb83a7f32f8: 0x00007fb83a7f3890 // l_next
0x7fb83a7f3300: 0x0000000000000000 //l_prev
link_map의 연장선인 dynamic영역도 마찬가지로, gdb로 살펴볼 수 있다. 여기에 나타나는 것은 Elf64_Dyn 구조체들이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{.link_map}::{.dynamic section}
===============================================================
{Elf64 Dyn} structs
0x403e20: 0x0000000000000001 0x0000000000000018
0x403e30: 0x000000000000000c 0x0000000000401000
0x403e40: 0x000000000000000d 0x0000000000401164
0x403e50: 0x0000000000000019 0x0000000000403e10
0x403e60: 0x000000000000001b 0x0000000000000008
0x403e70: 0x000000000000001a 0x0000000000403e18
0x403e80: 0x000000000000001c 0x0000000000000008
0x403e90: 0x000000006ffffef5 0x00000000004003b0
0x403ea0: 0x0000000000000005 0x0000000000400430 <<< d_tag, d_ptr : DT_STRTAB : d_un = d_ptr
0x403eb0: 0x0000000000000006 0x00000000004003d0
0x403ec0: 0x000000000000000a 0x0000000000000048
0x403ed0: 0x000000000000000b 0x0000000000000018
Elf64_Dyn 구조체의 정의 역시 CodeBrowser에서 찾아 볼 수 있다.
1
2
3
4
5
6
7
8
9
typedef struct
{
Elf64_Sxword d_tag; /* Dynamic entry type */
union
{
Elf64_Xword d_val; /* Integer value */
Elf64_Addr d_ptr; /* Address value */
} d_un;
} Elf64_Dyn;
이 구조체가 갖는 d_tag와 d_val, d_ptr에 따라 이 구조체의 의미가 결정된다. 예를들어, 0x403ea0의 0x5와 0x403ea8의 0x400430은 DT_STRTAB구조체의 d_val, d_ptr에 대응된다. gdb를 통해 d_ptr의 내용을 들여다보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{.link_map}::{.dynamic section}::{DT_STRTAB}::{.dynstr section}
===============================================================
0x400430: 0x5f6362696c5f5f00
0x400438: 0x616d5f7472617473
0x400440: 0x0073747570006e69
0x400448: 0x2e6f732e6362696c
0x400450: 0x5f4342494c470036
0x400458: 0x4c4700352e322e32
0x400460: 0x34332e325f434249
0x400468: 0x5f6e6f6d675f5f00
0x400470: 0x005f5f7472617473
'__libc_start_main'
'puts'
'libc.so.6'
'GLIBC_2.2.5'
'GLIBC_2.34'
'__gmon_start__'
DT_STRTAB구조체의 d_ptr을 타고 들어오면, .dynstr 영역이 나타난다. 이 영역에는 바이너리가 사용하는 심볼들의 문자열이 저장되어있다. 이번 코드에서는 puts()함수만 사용했기 때문에 다른 특별한 함수는 보이지 않는다.
_dl_runtime_resolve_xsavec 함수로 이동
link_map을 push하고 나서 가장 먼저오는 중요한 이벤트는 _dl_runtime_resolve_xsavec함수로의 이동이다. 여기서 0x404010주소가 등장하는데, 이는 .got영역의 일부다.
1
2
0x401026 bnd jmp qword ptr [rip + 0x2fe3] <_dl_runtime_resolve_xsavec>
// $rip+7+0x2fe3 = 0x404010 -> 0x00007fb83a7cdd30
자세하게는, 0x404010는 .got.plt+0x10이다. shell에서 readelf를 통해 다음과 같이 확인 가능하다.
1
2
3
4
5
6
readelf -S got | grep -A2 got // .got 영역 확인 용도
[23] .got PROGBITS 0000000000403ff0 # .got section 시작 주소
[24] .got.plt PROGBITS 0000000000404000 # .got.plt section 시작주소
# .got.plt[0]에는 .dynamic 영역의 주소가 저장됨
# .got.plt[1]에는 link_map의 시작주소가 저장됨.
# .got.plt[2]에는 symbol resolution 함수인 _dl_runtime_resolve()의 주소를 저장함.
실제로 이 영역들을 확인해보면 아래와 같다. 또한, 0x404018부터는 got테이블의 시작으로, 여기에 resolve 이후 함수의 실제 주소가 저장된다.
1
2
3
4
5
6
7
8
9
10
11
12
{.got}======================
0x403ff0: 0x00007fb83a5b2dc0 <__libc_start_main_impl> -> 0x89495741fa1e0ff3
0x403ff0: 0x0000000000000000
{.got.plt}=================
0x404000: 0x0000000000403e20 // & .dynamic section
0x404008: 0x00007fb83a7f32e0 // & link_map
0x404010: 0x00007fb83a7cdd30 // & _dl_runtime_resolve_xsavec>
0x404018: 0x0000000000401030 <puts@got.plt> // GOT TABLE entry : 함수 호출 전에는 PLT+7의 주소. 이후 함수 호출에는 함수의 실제 주소가 저장되는 부분. //reloc_offset 순서와 일치함
0x404020: 0x0000000000000000
0x40408: 0x0000000000000000
0x404030 <completed.0>: 0x0000000000000000
_dl_runtime_resolve_xsavec()함수로의 이동 후, 동작을 분석해보자.
1
2
3
4
5
6
7
{puts()}::{_dl_runtime_resolve_xsavec()}
0x7fb83a7cdda1 <_dl_runtime_resolve_xsavec+113> mov rsi, qword ptr [rbx + 0x10]
| $rbx+0x10 = 0x7ffcdbf0aed0 : 0x0 //reloc_offset
0x7fb83a7cdda5 <_dl_runtime_resolve_xsavec+117> mov rdi, qword ptr [rbx + 8]
| $rbx+0x8 = 0x7ffcdbf0aec8 : 0x00007fb83a7f32e0 //link_map 구조체 주소
0x7fb83a7cdda9 <_dl_runtime_resolve_xsavec+121> call _dl_fixup <_dl_fixup>
// _dl_fixup(&link_map, reloc_offset)형태로 호출
rsi에reloc_offset을 저장한다.rdi에link_map의 주소를 저장한다._dl_fixup(&link_map, reloc_offset)을 호출한다.
실제로 _dl_fixup에서 해당 함수의 실제 주소를 찾고, GOT테이블에 덮어씌우는 작업을 한다.
_dl_fixup(&link_map, reloc_offset) 호출
이제 _dl_fixup함수를 분석해보자.
1
2
3
4
5
6
7
8
9
{puts()}::{_dl_runtime_resolve_xsavec()}::{_dl_fixup()}
======================================================
0x7fb83a7cae9e <_dl_fixup+46> mov rdx, qword ptr [rbp + 0x68]
| $rbp+0x68 = 0x7fb83a7f3348 : 0x0000000000403ea0 // dynamic section의 d_tag : DT_STRTAB
0x7fb83a7caea8 <_dl_fixup+56> mov rdi, qword ptr [rdx + 8]
| $rdx+8 = 0x403ea8 : 0x0000000000400430 // DT_STRTAB의 d_ptr -> dynstr section의 주소
...
0x7fb83a7caeb3 <_dl_fixup+67> mov rdx, qword ptr [rdx + 8]
| $rdx+8 = 0x403f28 : 0x00000000004004e0 // .rela.plt section의 주소
rax에dynamic영역의DT_STRTAB의d_tag의 주소(0x0000000000403ea0)를 저장한다.rdi에DT_STRTAB의d_ptr(0x0000000000400430)을 저장한다. 이는dynstr영역의 주소를 가리키는것을 복기하자.rdx에.rela.plt영역의 주소(0x00000000004004e0)를 저장한다. 이는puts()함수의Elf64_Rela구조체의 주소다.
.rela.plt의 주소 역시 shelld에서 readelf로 받아올 수 있다. 확인해보면, 00000000004004e0가 시작점임을 알 수 있고, 이것이 rdx에 저장된 것이다.
1
2
readelf -S got | grep -A1 rela.plt
[11] .rela.plt RELA 00000000004004e0 // .rela.plt 의 주소 (Elf64_Rela struct 참고)
여기에 저장되는 Elf64_Rela라는 구조체가 있는데, 다음과 같은 구조를 갖는다.
1
2
3
4
5
6
typedef struct
{
Elf64_Addr r_offset; /* Address */
Elf64_Xword r_info; /* Relocation type and symbol index */
Elf64_Sxword r_addend; /* Addend */
} Elf64_Rela;
실제로 확인해보면 다음과 같다.
1
2
3
4
5
{.rela.plt}::{Elf64_Rela structure}
====================================
0x4004e0: 0x0000000000404018 // r_offset = reloc_offset
0x4004e8: 0x0000000200000007 // r_info
0x4004f0: 0x0000000000000000 // r_addend
만약 사용한 함수가 여러개라서, reloc_offset이 0x0가 아닌경우, 해당 offset만큼 3곱하고, 8바이트씩 더 곱해서 그에 맞는 Elf64_Rela 구조체를 가리키게 만든다.
이를 assembly코드와 같이 한줄한줄 따라가보면서 이해해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{puts()}::{_dl_runtime_resolve_xsavec()}::{_dl_fixup()}
========================================================
0x7fb83a7caea2 <_dl_fixup+50> mov ebx, esi // esi는 _dl_fixup 호출 당시 rsi의 하위 4바이트 이므로, reloc_offset에 해당함. rbx의 상위 비트에 값이 있더라도, ebx에 값을 쓰면 상위 바이트는 자동으로 0으로 설정됨.
0x7fb83a7caea4 <_dl_fixup+52> lea rcx, [rbx + rbx*2] // rbx=0이므로 rcx=>0 여기서 rbx는 reloc_offset. 이후 rcx는 reloc_offset*3을 의미함
0x7fb83a7caea8 <_dl_fixup+56> mov rdi, qword ptr [rdx + 8]
0x7fb83a7caeac <_dl_fixup+60> mov rdx, qword ptr [rbp + 0xf8] // rbp는 &link_map, 0xf8은 248바이트이므로, rdx=>6개의 link_map 끝부분을 의미함
0x7fb83a7caeb3 <_dl_fixup+67> mov rdx, qword ptr [rdx + 8] // rdx에 Elf64_Rela 구조체 주소 저장 (rdx+8은 GOT table 시작부분. 즉, 이후 rdx는 got테이블을 가리킴)
0x7fb83a7caeb7 <_dl_fixup+71> add rdi, r9 // rdi => 0x400430 (0x400430 + 0x0)
0x7fb83a7caeba <_dl_fixup+74> lea rsi, [rdx + rcx*8] // rsi => 0x4004e0 (rdx는 got table, rcx는 reloc_offset*3 이므로 rsi는 함수 호출 당시 전달했던 offset번째의 Elf64_Rela 구조체를 가리킴)
0x7fb83a7caebe <_dl_fixup+78> add rsi, r9
0x7fb83a7caec1 <_dl_fixup+81> mov r8, qword ptr [rsi + 8] // r8에는 puts() 함수의 Elf64_Rela의 r_info가 저장됨
// puts()의 r_info는 0x0000000200000007인데, 이는 symbol table index와 relocation type 정보를 담고있다.
0x7fb83a7caec5 <_dl_fixup+85> mov r12, qword ptr [rsi] // r12도 Elf64_Rela 객체 포인팅
0x7fb83a7caec8 <_dl_fixup+88> mov rdx, r8 // rdx는 r8의 값 : puts()의 r_info가 저장됨
0x7fb83a7caecb <_dl_fixup+91> add r12, rax // r12=>0x404018 (0x404018 + 0x0)
...
0x7fb83a7caed9 <_dl_fixup+105> lea rdx, [r10 + rdx*8] RDX => 0x400400 ◂— 0x1200000013 //r_info 상위바이트인 0x2로 어떤 주소를 구함
_dl_lookup_symbol_x 함수 호출
이후 _dl_lookup_symbol_x함수를 호출하는데, 이 함수가 갖는 내용이 너무 많으니 Call Stack으로 축약해서 그의 동작 결과만 알고가자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{puts()}::{_dl_runtime_resolve_xsavec()}::{_dl_fixup()}
========================================================
0x7fb83a7caf6c <_dl_fixup+252> call _dl_lookup_symbol_x <_dl_lookup_symbol_x>
rdi: 0x400443 ◂— 0x62696c0073747570 /* 'puts' */ //함수명 "puts"의 주소 in dynstr section
rsi: 0x7fb83a7f32e0 ◂— 0 //link_map 구조체의 주소
rdx: 0x7ffcdbf0ab00 —▸ 0x400400 ◂— 0x1200000013 //아까 r_info 상위바이트로 구한 주소를 저장하는 인자
rcx: 0x7fb83a7f3650 —▸ 0x7fb83a7f35a0 —▸ 0x7fb83a7b6690 —▸ 0x7fb83a7f32e0 ◂— 0
r8: 0x7fb83a7b66f8 —▸ 0x400452 ◂— 'GLIBC_2.2.5'
r9: 1
arg[6]: 1
arg[7]: 0
============== Call Stack 축약 ================
_dl_lookup_symbol_x() -> do_lookup_x() -> _dl_name_match_p()
//이 과정에서 라이브러리의 실제 주소를 rax로 리턴하고, puts()함수의 실제 offset을 stack을 통해 리턴함.
_dl_fixup 함수로 복귀
_dl_lookup_symbol_x함수 실행 후 획득한 라이브러리의 실제 주소와 puts()함수의 실제 offset을 통해 got를 수정하는 작업을 살펴보자
1
2
3
4
5
6
7
8
9
10
{puts()}::{_dl_runtime_resolve_xsavec()}::{_dl_fixup()}
========================================================
0x7fb83a7cafa3 <_dl_fixup+307> mov rax, qword ptr [r13] RAX, [0x7fb83a7b6160] => 0x7fb83a589000 ◂— 0x3010102464c457f //rax는 라이브러리 함수를 가리킴
0x7fb83a589000 : 0x03010102464c457f = "\177ELF\002\001\001\003" <- ELF파일 시그니처
0x7fb83a7cafa7 <_dl_fixup+311> add rax, qword ptr [rdx + 8] RAX => 0x7fb83a609e50 (puts) (0x7fb83a589000 + 0x80e50) // rdx+8에는 어떤 offset 0x80e50이 있다. 라이브러리에 offset을 더했더니 puts()의 진짜 주소가 구해졌다!
0x7fb83a7cafab <_dl_fixup+315> mov qword ptr [rsp + 8], rax [0x7ffcdbf0ab08] => 0x7fb83a609e50 (puts) ◂— endbr64
//해당 시점에 r12 레지스터에 GOT테이블의 주소, 0x404018이 들어있음
...
0x7fb83a7cafeb <_dl_fixup+379> mov qword ptr [r12], rax [puts@got[plt]] => 0x7fb83a609e50 (puts) ◂— endbr64 //여기서 [r12]에 rax를 저장함으로써, puts()함수의 주소를 GOT테이블에 덮어씌움!!
//이후 0x404018에는 0x00007fb83a609e50 (puts)가 들어가있음
_dl_runtime_resolve_xsavec 함수로 복귀
_dl_fixup()함수를 실행하고 나서 _dl_runtime_resolve_xsavec함수로 돌아와서 puts()를 실행한다.
1
2
3
4
5
{puts()}::{_dl_runtime_resolve_xsavec()}
========================================
0x7fb83a7cddae <_dl_runtime_resolve_xsavec+126> mov r11, rax //r11에 puts()의 실제 주소가 담김
...
0x7fb83a7cddea <_dl_runtime_resolve_xsavec+186> jmp r11 //puts() 실행!
main 함수로 복귀
마지막으로 _dl_runtime_resolve_xsavec함수에서 빠져나와서 main함수 흐름으로 돌아온다.
1
2
3
{main}
========================================
0x40114d <main+23> lea rax, [rip + 0xecd] //이후 main함수에 컴백
끝
이 거창한 과정을 거쳐서, got에 puts()함수의 실제 주소인 0x00007fb83a609e50를 저장하고 실행했다.