프로그램의 실행 순서
간단한 코드를 통해서 확인해보겠습니다.
// gcc -o main main.c -fPIC -pie
#include <stdio.h>
int main(){
printf("main start!\n");
return 1;
}
C
복사
위 코드를 컴파일 한 후 gdb를 통해서 디버깅해보겠습니다.
main+22에 bp를 걸어주고 ret를 확인하면 아래와 같습니다.
info frame 명령어를 통해서 saved rip가 0x7fc63b1d0840인 것을 확인할 수 있고 이는 __libc_start_main+240의 주소입니다.
다음 프레임도 같은 방식으로 추적해보면
saved rip가 0x561553800649이며, _start+41인 것을 확인할 수 있습니다.
마지막으로 frame 2를 확인해보면 saved rip가 없는 것을 확인할 수 있습니다.
_start > __libc_start_main > main
위와 같은 순서로 프로로그램이 실행되는 것을 확인할 수 있으며, 간단 함수의 기능은 아래와 같습니다.
_start
프로그램의 실행시 argc, argv 인자를 저장하고 스택을 초기화 한 뒤 __libc_start_main을 호출하여 인자를 전달합니다.
__libc_start_main
.init, .fini 섹션 작업과 관련된 함수들을 호출하고 메임 함수를 호출합니다. 자세한 실행 내용이 궁금하신 분들은 아래의 블로그글도 같이 봐주시면 이해가 편할 듯 합니다.
프로그램의 종료 순서
main
main+22에 bp를 걸고 si를 통해서 다음 step으로 넘어가보겠습니다.
__libc_start_main
__libc_start_main+240로 이동한 것을 확인할 수 있고, 해당 부분을 분석하면 실행하는 함수들을 확인할 수 있습니다.
__GI_exit
먼저 실행하는 함수인 __GI_exit 함수를 분석해보겠습니다.
__run_exit_handlers
si를 통해서 __run_exit_handlers 함수도 계속 분석을 진행하면 __GI___call_tls_dtors을 call 하는 것을 확인할 수 있으며, 해당 함수는 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 구조체의 함수 포인터를 조작하여 프로그램의 실행흐름을 변경할 수 있습니다.