Home
System Hacking
📗

_rtld_global overwite 분석

Type
Vuln
생성 일시
2024/02/24 10:07
종류
rtld

프로그램의 실행 순서

간단한 코드를 통해서 확인해보겠습니다.
// gcc -o main main.c -fPIC -pie #include <stdio.h> int main(){ printf("main start!\n"); return 1; }
C
복사
위 코드를 컴파일 한 후 gdb를 통해서 디버깅해보겠습니다.
main+22bp를 걸어주고 ret를 확인하면 아래와 같습니다.
info frame 명령어를 통해서 saved rip0x7fc63b1d0840인 것을 확인할 수 있고 이는 __libc_start_main+240의 주소입니다.
다음 프레임도 같은 방식으로 추적해보면
saved rip0x561553800649이며, _start+41인 것을 확인할 수 있습니다.
마지막으로 frame 2를 확인해보면 saved rip가 없는 것을 확인할 수 있습니다.

_start > __libc_start_main > main

위와 같은 순서로 프로로그램이 실행되는 것을 확인할 수 있으며, 간단 함수의 기능은 아래와 같습니다.

_start

프로그램의 실행시 argc, argv 인자를 저장하고 스택을 초기화 한 뒤 __libc_start_main을 호출하여 인자를 전달합니다.

__libc_start_main

.init, .fini 섹션 작업과 관련된 함수들을 호출하고 메임 함수를 호출합니다. 자세한 실행 내용이 궁금하신 분들은 아래의 블로그글도 같이 봐주시면 이해가 편할 듯 합니다.

프로그램의 종료 순서

main

main+22bp를 걸고 si를 통해서 다음 step으로 넘어가보겠습니다.

__libc_start_main

__libc_start_main+240로 이동한 것을 확인할 수 있고, 해당 부분을 분석하면 실행하는 함수들을 확인할 수 있습니다.

__GI_exit

먼저 실행하는 함수인 __GI_exit 함수를 분석해보겠습니다.

__run_exit_handlers

si를 통해서 __run_exit_handlers 함수도 계속 분석을 진행하면 __GI___call_tls_dtorscall 하는 것을 확인할 수 있으며, 해당 함수는 rtld와 관련이 없으므로 계속 진행하겠습니다.
다음으로 call rdx를 진행하는데 rdx의 값을 보면 _dl_fini인 것을 확인 할 수 있습니다.

_dl_fini

쭉 타고 넘어오면 _rtld_global+3848 위치에 있는 함수를 실행하는것을 확인할 수 있습니다.
해당 영역은 writeable임으로 임의의 주소에 쓰게 취약점이 존재할 경우 해당 영역에 함수를 전달하여 호출할 수 있습니다.

__rtld_global overwrite 취약점

해당 취약점을 이해하기 위해서 먼저 종료시 실행되는 __run_exit_handlers 함수의 C 코드를 먼저 확인하겠습니다.

__run_exit_handlers Function

/* Call all functions registered with `atexit' and `on_exit', in the reverse of the order in which they were registered perform stdio cleanup, and terminate program execution with STATUS. */ void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit) { /* First, call the TLS destructors. */ #ifndef SHARED if (&__call_tls_dtors != NULL) #endif __call_tls_dtors (); /* We do it this way to handle recursive calls to exit () made by the functions registered with `atexit' and `on_exit'. We call everyone on the list and use the status value in the last exit (). */ while (*listp != NULL) { struct exit_function_list *cur = *listp; while (cur->idx > 0) { const struct exit_function *const f = &cur->fns[--cur->idx]; switch (f->flavor) { void (*atfct) (void); void (*onfct) (int status, void *arg); void (*cxafct) (void *arg, int status); case ef_free: case ef_us: break; case ef_on: onfct = f->func.on.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (onfct); #endif onfct (status, f->func.on.arg); break; case ef_at: atfct = f->func.at; #ifdef PTR_DEMANGLE PTR_DEMANGLE (atfct); #endif atfct (); break; case ef_cxa: cxafct = f->func.cxa.fn; #ifdef PTR_DEMANGLE PTR_DEMANGLE (cxafct); #endif cxafct (f->func.cxa.arg, status); break; } } *listp = cur->next; if (*listp != NULL) /* Don't free the last element in the chain, this is the statically allocate element. */ free (cur); } if (run_list_atexit) RUN_HOOK (__libc_atexit, ()); _exit (status); }
C
복사
코드에서 확인 가능하듯 exit_function 구조체의 멤버 변수에 접근하여 함수 포인터를 호출합니다.

exit_function Struct

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; };
C
복사
즉 위에서 분석한 내용을 토대로 __run_exit_handlers 함수는 _dl_fini 함수를 실행합니다.

__rtld_lock_lock_recursive Macro

# define __rtld_lock_lock_recursive(NAME) \ GL(dl_rtld_lock_recursive) (&(NAME).mutex)
C
복사
GL 매크로는 주로 GNU C 라이브러리(glibc)나 비슷한 시스템 라이브러리에서 사용되며, 동적 링킹(dynamic linking)이나 심볼(symbol) 관리에 관련된 코드에서 종종 보입니다.

_dl_fini Function

void internal_function _dl_fini (void) { #ifdef SHARED int do_audit = 0; again: #endif for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns) { /* Protect against concurrent loads and unloads. */ __rtld_lock_lock_recursive (GL(dl_load_lock));
C
복사
_dl_fini 함수는 __rtld_lock_lock_recursive 함수를 호출하고 인자로 dl_load_lock을 전달합니다. 해당 함수는 매크로로 확인해보면dl_rtld_lock_recursive 함수입니다.
해당 함수는 _rtld_global 구조체의 멤버 변수로 만들어집니다. 해당 구조체는 매우 방대하기 때문에 함수 포인터와 전달되는 인자인 dl_load_lock 만을 확인하겠습니다.
p _rtld_global ... _dl_load_lock = { mutex = { __data = { __lock = 0x0, __count = 0x0, __owner = 0x0, __nusers = 0x0, __kind = 0x1, __spins = 0x0, __elision = 0x0, __list = { __prev = 0x0, __next = 0x0 } }, __size = '\000' <repeats 16 times>, "\001", '\000' <repeats 22 times>, __align = 0x0 } }, ... _dl_rtld_lock_recursive = 0x7feab2459c90 <rtld_lock_default_lock_recursive>, ... }
Bash
복사
_dl_rtld_lock_recursive 함수의 위치를 확인해보면 writable임을 확인할 수 있습니다.

dl_main Function

static void dl_main (const ElfW(Phdr) *phdr, ElfW(Word) phnum, ElfW(Addr) *user_entry, ElfW(auxv_t) *auxv) { GL(dl_init_static_tls) = &_dl_nothread_init_static_tls; #if defined SHARED && defined _LIBC_REENTRANT \ && defined __rtld_lock_default_lock_recursive GL(dl_rtld_lock_recursive) = rtld_lock_default_lock_recursive; GL(dl_rtld_unlock_recursive) = rtld_lock_default_unlock_recursive; #endif ...
C
복사
dl_main Function에서 dl_rtld_lock_recursive을 확인해보면 초기화 하는 것을 확인할 수 있습니다.
위에서 분석한 내용을 토대로 dl_rtld_lock_recursive가 실행하는 함수는 _rtld_global+3848임을 확인할 수 있으며, 이는 아래와 같이 &_rtld_global._dl_rtld_lock_recursive임을 확인할 수 있습니다.

Exploit

임의의 영역에 쓰기 취약점이 존재하면 _rtld_global 구조체의 함수 포인터를 조작하여 프로그램의 실행흐름을 변경할 수 있습니다.

Reference