함수 호출 과정(Runtime Resolve)
1. Dynamic Link 시 함수 호출 과정
1. PLT, GOT
이전 글에서 짧게 설명하고 넘어갔던 Runtime Resolve 에 대해 자세히 알아보자
// Name : got.c
//Compile : gcc -o got got.c -no-pie -fno-stack-protector -m32
#include <stdio.h>
int main() {
puts("Resolving address of 'puts'.");
puts("Get address from GOT");
}
got.c 파일을 만들고 컴파일 한다.
Dump of assembler code for function main:
0x08049176 <+0>: lea ecx,[esp+0x4]
0x0804917a <+4>: and esp,0xfffffff0
0x0804917d <+7>: push DWORD PTR [ecx-0x4]
0x08049180 <+10>: push ebp
0x08049181 <+11>: mov ebp,esp
0x08049183 <+13>: push ebx
0x08049184 <+14>: push ecx
0x08049185 <+15>: call 0x80490b0 <__x86.get_pc_thunk.bx>
0x0804918a <+20>: add ebx,0x2e76
0x08049190 <+26>: sub esp,0xc
0x08049193 <+29>: lea eax,[ebx-0x1ff8]
0x08049199 <+35>: push eax
0x0804919a <+36>: call 0x8049050 <puts@plt>
0x0804919f <+41>: add esp,0x10
0x080491a2 <+44>: sub esp,0xc
0x080491a5 <+47>: lea eax,[ebx-0x1fdb]
0x080491ab <+53>: push eax
0x080491ac <+54>: call 0x8049050 <puts@plt>
0x080491b1 <+59>: add esp,0x10
0x080491b4 <+62>: mov eax,0x0
0x080491b9 <+67>: lea esp,[ebp-0x8]
0x080491bc <+70>: pop ecx
0x080491bd <+71>: pop ebx
0x080491be <+72>: pop ebp
0x080491bf <+73>: lea esp,[ecx-0x4]
0x080491c2 <+76>: ret
End of assembler dump.
main 함수의 어셈블리이다.
call puts@plt 부분에 break 를 걸고 실행해보자.
puts@plt 가 호출되는 부분에서 break 된 것을 확인할 수 있다.
call 하는 주소( 0X8049050 )로 이동하면 PLT 가 있다.
PLT 는 GOT 를 참조한다고 했으니 0x804c010 은 GOT일 것이다.
0x804c010은 GOT 인 것을 확인할 수 있다.
함수가 처음 호출되었기 때문에 실제 함수의 주소값이 쓰여있지 않고,
PLT+6 의 주소가 쓰여있다.
함수를 호출하면 함수의 PLT 로 이동한다.
PLT 는 GOT 로 jmp 하고, GOT 에는 라이브러리에 있는 실제 함수의 주소가 저장되어 있어 함수가 호출된다.
함수가 처음 호출되는 경우는 GOT 에 함수의 주소가 아닌 PLT+6 가 저장되어 있다.
여기부터 Dynamic Linking 의 시작이다.
2. _dl_runtime_resolve 호출 전
PLT+6 로 이동하면 0x8 을 스택에 push 하고
0x8049030 으로 jmp 한다.
여기서 0x8 은 reloc_offset 인데 자세한건 아래에서 설명하겠다.
0x8049030 으로 jmp 한 뒤
스택에 0x804c004 의 값인 0x7ffda40 을 push 한다.
여기서 0x7ffda40 은 Link_map 구조체의 주소다.
Link_map 은 라이브러리의 정보를 담고있는 구조체이다. 이것을 이용해 여러가지 테이블의 주소를 구한다.
그 다음 0x804c008의 값인 0xf7fd8ff0 으로 jmp 한다 .
0xf7fd8ff0 은 _dl_runtime_resolve 의 주소이다.
2. _dl_runtime_resolve
1. _dl_runtime_resolve
Dump of assembler code for function _dl_runtime_resolve:
0xf7fd8ff0 <+0>: endbr32
0xf7fd8ff4 <+4>: push eax
0xf7fd8ff5 <+5>: push ecx
0xf7fd8ff6 <+6>: push edx
0xf7fd8ff7 <+7>: mov edx,DWORD PTR [esp+0x10]
0xf7fd8ffb <+11>: mov eax,DWORD PTR [esp+0xc]
0xf7fd8fff <+15>: call 0xf7fd6ea0 <_dl_fixup>
0xf7fd9004 <+20>: pop edx
0xf7fd9005 <+21>: mov ecx,DWORD PTR [esp]
0xf7fd9008 <+24>: mov DWORD PTR [esp],eax
0xf7fd900b <+27>: mov eax,DWORD PTR [esp+0x4]
0xf7fd900f <+31>: ret 0xc
End of assembler dump.
_dl_runtime_resolve 의 어셈블리이다.
_dl_runtime_resolve 함수는 _dl_fixup 함수를 호출한다.
어셈블리를 순서대로 따라가보자.
위에서 reloc_offset(0x8) 을 스택에 가장 먼저 push 하고,
그 다음으로 Link_map(0x7ffda40) 을 스택에 push 했다.
_dl_runtime_resolve 함수로 들어와서 eax, ecx, edx 를 순서대로 push 했다.
esp+0x10 에 있는 값을 edx 에 넣고,
esp+0xc 에 있는 값을 eax 에 넣었다.
그 다음 _dl_fixup 함수를 호출한다.
_dl_fixup 함수는 eax (Link_map) 와 edx (0x8) 의 값을 인자로 받아오는 것을 알 수 있다.
3. _dl_fixup
1. _dl_fixup
_dl_fixup 함수는 길기 때문에 필요한 부분만 보도록 하자.
Link_map 구조체 (eax) 의 주소를 ebp 로 복사한다.
그리고 eax(Link_map)+0x38 의 값을 eax 로 복사한다.
[eax+0x38] 에 있는 값을 확인해보자.
[eax+0x38] 에는 어떤 구조체 주소가 들어있다.
이 구조체는 8byte 로 구성되어 있는데 두번째 4byte 에 DYNSYM 영역의 시작주소 0x0804820c 가 저장되어 있다.
DYNSYM 영역은 아래에서 자세히 설명하겠다.
그 다음 edx (relloc_offset) 의 값을 esp+0xc 에 복사한다.
2. DYNSYM 영역
ebp (Link_map) 를 edx 에 복사한다.
그 다음 eax + 0x4 의 값을 edi 에 복사한다.
[eax+0x4] 에 있는 값을 확인해보자.
아까 위에서 구한 DYNSYM 영역의 시작 주소 0x0804820c 주소가 들어있다.
DYNSYM 영역은 16byte 의 구조체로 구성되어 있고, 처음 16byte 는 0으로 세팅된다.
DYNSYM 을 이루는 구조체의 정확한 이름은 Elf32_Sym 이다.
구조는 아래와 같다.
typedef struct {
Elf32_Word st_name
Elf32_Addr st_value
Elf32_Word st_size
unsigned char st_info
unsigned char st_other
Elf32_Section st_shndx
} Elf32_Sym
위의 DYNSYM 테이블의 마지막 값으로 보면 아래와 같다.
0x00000022, 0x00000000, 0x00000000, 0x00, 0x00, 0x0012
중요한 것은 st_name 과 st_other 두 가지이다.
st_name(1번째) 은 함수 이름 위치의 offset 이다. STRTAB 에서의 Index 를 가지고 있다.
st_other(5번째) 는 3과 AND(&) 연산을 해서 0인지 아닌지로 이미 로딩된 함수인지 아닌지를 판단한다.
3. STRTAB
_dl_fixup+56에서는
ebp(Link_map)+0x34 의 값을 eax 에 복사한다.
[ebp+0x34] 의 값을 확인해보자
8byte 구조체 주소를 확인할 수 있다.
구조체의 두 번째 4byte 에는 STRTAB의 시작주소 0x0804825c 가 담겨있다.
STRTAB 은 바이너리 내에서 사용되는 함수명이 문자열로 저장된 영역이다.
(.dynstr 영역에 해당한다)
_dl_fixup+59 에서는
esp+0xc 의 값을 ecx 에 복사한다.
[esp+0xc] 를 확인해보자
relloc_offset (0x8) 이 담겨있는 것을 확인할 수 있다.
_dl_fixup+63 에서는
마지막으로 eax+4 를 esi 에 복사한다.
아까 위에서 eax 에는 8byte 구조체 주소가 들어있는 것을 확인했다.
구조체의 두 번째 4byte 에는 STRTAB 의 시작 주소가 담겨있었다.
eax+4 의 값은 STRTAB 의 시작 주소일 것이다.
eax+4 에는 STRTAB 의 시작 주소 0x0804825c 가 들어있는 것을 확인할 수 있다.
esi 에는 STRTAB 이 들어간다.
4. JMPREL
ebp(Link_map)+0x7c 에 있는 값을 eax 에 복사한다.
ebp+0x7c 의 값을 확인해보자
ebp+0x7c 에는 8byte 구조체 주소 0x0804bf94 가 담겨있다.
구조체의 두 번째 4byte 에는 JMPREL 의 시작주소 0x080482f4 가 담겨있다.
(.rel.plt 영역에 해당한다)
JMPREL 은 Elf32_Rel 형식의 구조체들로 이루어져 있다.
Elf32_Rel 구조체 형식은 아래와 같다.
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel
여기서 r_offset 은 GOT 의 주소이다.
r_info 는 첫 번째 1byte 는 재배치 타입, 두 번째 byte 부터 DYNSYM 테이블에서의 index 이다.
위의 JMPREL 테이블의 마지막 값으로 보면
0x0804c010 0x000002 0x07
순서대로 GOT 주소, DYNSYM index, 재배치 타입이다.
5. GOT 주소 구하기
eax+4 의 값과 ecx 를 더하고 있다.
eax+4 와 ecx 을 확인해보자
eax+4 에는 JMPREL 시작 주소가 담겨있다.
ecx 에는 relloc_offset 인 0x8 이 담겨있다.
JMPREL 에 relloc_offset 을 더하면 puts 가 사용하는 Elf32_Rel 구조체의 주소가 된다.
GOT 의 주소가 처음 확인했던 puts 함수의 GOT 와 일치한다.
relloc_offset 은 JMPREL 영역에서 호출한 함수를 찾기 위한 값인 것을 알 수 있다.
puts 함수의 DYNSYM index 는 0x2 이고, 재배치 타입은 0x7 이다
6. 함수 이름 구하기
위에서 얘기한 DYNSYM 테이블 (0x0804820c)을 다시 확인해보자
JMPREL 테이블에서 index 는 DYNSYM 테이블에서의 index 를 말한다.
puts 함수의 index 는 0x2 이므로 2번째 위치가 된다.
DYNSYM 은 Elf32_Sym 구조체로 이루어져 있다고 했다.
Elf32_Sym 구조체로 보면
0x00000022, 0x00000000, 0x00000000, 0x00, 0x00, 0x0012
첫 번째 (0x00000022) 는 함수 이름 위치의 offset 이다.
다섯 번째 (0x00) 는 3과 & 연산을 해서 0이냐 아니냐로 이미 로딩된 함수인지 아닌지 판단한다.
0x00 은 3과 & 연산했을 때 0이 나오는 값이므로 로딩되지 않은 함수라고 판단한다.
0x22 가 함수 이름 위치의 offset 인지 확인해보자
전에 구했던 STRTAB 에 0x22 를 더하면 puts 문자열이 나온다.
이제 gdb 에서 확인해보자
esp+0x30 에 있는 값을 esi 로 복사하고 있다.
[esp+0x30] 을 확인해보자
esp+0x30 에는 STRTAB 주소 0x0804825c 가 담겨있다.
그 다음으로 esi (STRTAB) 와 eax 를 더한다.
eax 의 값을 확인해보자
eax 에는 DYNSYM 테이블에서 함수 이름의 offset 이 저장되어 있다.
아까 위에서 STRTAB + 함수 이름의 offset 을 더하면 함수 이름이 나온다고 했다.
esi 에 함수 이름 puts 가 들어가 있는 것을 확인할 수 있다.
이렇게 알아낸 문자열의 주소를 esi 에 넣고
_dl_lookup_symbol_x 함수를 호출한다
4. _dl_fixup (_dl_lookup_symbol_x 호출 후)
1. _dl_lookup_symbol_x
_dl_lookup_symbol_x 함수는 라이브러리 내의 SYMTAB 주소와 라이브러리 시작 주소를 얻어온다.
SYMTAB 에는 라이브러리에 존재하는 실제 함수 주소들의 offset (라이브러리 시작 주소에서부터의 offset) 이 담겨있다
_dl_lookup_symbol_x 함수가 종료되면
eax 에 라이브러리 시작 주소 (0xf7d82000) 가 저장된 걸 확인할 수 있다.
ecx 에는 SYMTAB 주소 (0xf7d8da94) 가 저장된 걸 확인할 수 있다
2. 함수의 실제 주소 가져오기
다시 _dl_fixup 함수로 돌아와서 필요한 부분만 보도록 하겠다.
eax 에 저장된 라이브러리 시작 주소를 edi 에 복사한다.
esp+0x28 에 저장된 값을 eax 에 복사한다.
[esp+0x28] 에 저장된 값을 확인해보자
esp+0x28 에는 SYMTAB 주소 (0xf7d8da94) 가 담겨있다.
edi 에 저장된 라이브러리 시작 주소를 edx 에 복사한다.
그 다음 edx(라이브러리 시작 주소) 와 eax+0x4 를 더한다.
[eax+0x4] 를 확인해보자
eax+0x4 에는 아까 위에서 봤던 SYMTAB 내에 있는 실제 함수의 offset 이 담겨있다.
edx(라이브러리 시작 주소) 와 eax+0x4(실제 함수의 offset) 를 더해서 실제 함수의 주소를 구한다.
puts 의 실제 주소인 0xf7df52a0 이 edx 에 저장된다.
3. GOT 에 실제 함수 주소 쓰기
esp+0x2c 의 값을 eax 에 복사한다.
[esp+0x2c] 를 확인해보자
esp+0x2c 에는 라이브러리에 있는 puts 의 실제 주소가 담겨있다.
eax 에 puts 실제 주소가 들어간다.
_dl_fixup+354 로 jmp 한다.
esp+0x18 의 값을 edi 에 복사한다.
[esp+0x18] 을 확인해보자
esp+0x18 에는 puts 의 GOT 주소가 담겨있다.
edi 에 puts 의 GOT 주소가 들어간다.
그 다음 eax 에 있는 값을 edi 에 복사한다.
아까 위에서 eax 에는 puts 실제 주소가 담겨있고, edi 에는 puts GOT 가 담겨있다고 했다.
그럼 puts 의 GOT 주소에 puts 의 실제 주소가 담기는지 확인해보자
PLT+6 를 가리키던 GOT 가 puts 실제 함수의 주소로 바뀐 것을 확인할 수 있다.
Reference
https://blackperl-security.gitlab.io/blog/2016/03/08/2016-03-08-pltgot-02/
'Hacking > Pwnable' 카테고리의 다른 글
RELRO (0) | 2024.07.15 |
---|---|
GOT Overwrite (1) | 2024.06.30 |
공유 라이브러리(PLT, GOT) (0) | 2024.06.17 |
NX-bit, ASLR (0) | 2024.05.10 |
스택 카나리 (Stack Canary) (0) | 2024.05.03 |