_rtld_global
프로세스가 등록되거나 종료될 때 쓰이는 변수와 영역들을 알고 있다면 이를 이용한 새로운 공격 기법을 개발할 수 있다고 한다.
그런 의미에서 프로세스를 어떤 방식으로 종료하는지 확인해보자
#include<stdio.h>
int main(){
return 0;
}
위와 같은 프로세스를 gdb를 통해 분석해보자
ret 명령에서 step into를 통해 들어가보면 __libc_start_main+243의 코드가 실행되고 이어 __GI_exit함수가 호출된다.
__GI_exit에는 __run_exit_handlers 함수가 존재하는 것을 확인할 수 있다.
__run_exit_handlers
/* 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, bool run_dtors)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__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 (true)
{
struct exit_function_list *cur;
__libc_lock_lock (__exit_funcs_lock);
restart:
cur = *listp;
if (cur == NULL)
{
/* Exit processing complete. We will not allow any more
atexit/on_exit registrations. */
__exit_funcs_done = true;
__libc_lock_unlock (__exit_funcs_lock);
break;
}
while (cur->idx > 0)
{
struct exit_function *const f = &cur->fns[--cur->idx];
const uint64_t new_exitfn_called = __new_exitfn_called;
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
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:
/* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
we must mark this function as ef_free. */
f->flavor = ef_free;
cxafct = f->func.cxa.fn;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
cxafct (f->func.cxa.arg, status);
break;
}
/* Re-lock again before looking at global state. */
__libc_lock_lock (__exit_funcs_lock);
if (__glibc_unlikely (new_exitfn_called != __new_exitfn_called))
/* The last exit function, or another thread, has registered
more exit functions. Start the loop over. */
goto restart;
}
*listp = cur->next;
if (*listp != NULL)
/* Don't free the last element in the chain, this is the statically
allocate element. */
free (cur);
__libc_lock_unlock (__exit_funcs_lock);
}
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());
_exit (status);
}
exit_function 구조체의 멤버 변수에 따른 함수 포인터를 호출한다.
return 명령어를 통해 프로세스를 종료한다면 _dl_fini함수를 호출한다.
_dl_fini
위의 코드는 _dl_fini함수의 일부분으로 _dl_load_lock을 인자로 __rtld_lock_lock_recursive함수를 호출하고 있다.
해당 함수는 dl_rtld_lock_recursive라는 함수 포인터이다.
해당 함수 포인터는 _rtld_global구조체의 멤버 변수이다.
다음은 gdb-peda에서 살펴본 _rtld_global구조체를 출력한 모습이다.
해당 구조체의 함수 포인터가 저장된 영역은 읽기 및 쓰기 권한이 존재하기 때문에 덮어써서 실행흐름을 제어할 수 있습니다.
프로세스를 로드할 때 호출되는 dl_main코드의 일부로, _rtld_global구조체의 dl_rtld_lock_recursive함수 포인터가 초기되는 것을 확인할 수 있다.
_rtld_global overwrite
1. 라이브러리 및 로더 베이스 주소 계산
Loader base address = Loader address - libc base address
2. _rtld_global 구조체 계산
gdb-peda$ p &_rtld_global._dl_load_lock
gdb-peda$ p &_rtld_global._dl_rtld_lock_recursive
Loader base address + rtld_global 구조체의 심볼 주소 = _rtld_global 구조체 주소
_dl_load_lock과 _dl_rtld_lock_recursive 함수 포인터 주소 구하기
3. _rtld_global 구조체 조작
프로그램을 종료시 _rtld_global 구조체의 _dl_load_lock을 인자로 _dl_rtld_lock_recursive 함수 포인터를 호출합니다.
따라서 dl_load_lock에 "/bin/sh" 또는 "sh" 문자열을 삽입하고 _dl_rtld_lock_recursive를 system 함수로 덮어쓰면 shell을 획득할 수 있습니다.
또는 _dl_rtld_lock_recursive에 one_shot gadget으로 덮어써서 shell을 획득할 수도 있습니다.