1. Introduction
리눅스 프로그램을 실행하면 프로그램에 명시된 코드의 결과값이 우리에게 출력된다.
프로그램이 실행되어 프로세스로 등록될 때에는 프로그램에 명시된 코드 뿐만이 아니라
프로그램에서 쓰이는 변수를 관리하기 위한 영역을 할당하는 등의 다양한 코드가 로더에 의해 실행된다.
이번에는 프로그램을 종료하는 과정을 이용한 공격 기법을 알아보기 전에
라이브러리의 코드를 분석하면서 어떤 방식으로 프로세스를 종료하는지 알아보자.
예제 코드
int main() {
return 0;
}
2. _rtld_global
2.1 __GI_exit
앞에서 컴파일한 코드는 별다른 코드를 실행하지 않고 프로그램을 종료한다.
프로그램을 종료할 때에는 우리가 모르는 많은 코드들이 내부적으로 실행되는데, 디버깅을 통해 알아보자
main 함수 내 리턴하는 명령어에 브레이크 포인트를 설정하고 확인해 보자
main 함수 내에서 리턴 명령어를 실행하면 스택 최상단에 있는 libc_start_main+231의 코드가 실행되고,
내부에서 GI_exit 함수를 호출하는 것을 볼 수 있다.
GI_exit 함수 내부의 모습으로, 또 다른 run_exit_handlers 함수가 등장한다.
해당 함수는 코드의 크기가 크기때문에 라이브러리 코드를 통해 분석해보자.
2.2 __run_exit_handlers
2.2.1 __run_exit_handlers 함수 코드
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
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;
}
}
이는 run_exit_handlers 함수 코드로,
exit_fuction 구조체의 멤버 변수에 따른 함수 포인터를 호출한다.
2.2.2 exit_function 구조체
struct exit_function
{
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;
};
위에 리턴만 하는 예제와 같이 리턴 명령어를 실행해 프로그램을 종료한다면
_dl_fini 함수를 호출한다.
2.3 _dl_fini
# define __rtld_lock_lock_recursive(NAME) \
GL(dl_rtld_lock_recursive) (&(NAME).mutex)
void
_dl_fini (void)
{
#ifdef SHARED
int do_audit = 0;
again:
#endif
for (Lmid_t ns = GL(dl_nns) - 1; ns >= 0; --ns)
{
__rtld_lock_lock_recursive (GL(dl_load_lock));
이 코드는 로더에 존재하는 _dl_fini 함수 코드의 일부이다.
코드를 보면, _dl_load_lock을 인자로 __rtld_lock_lock_recursive 함수를 호출하는 것을 볼 수 있다.
매크로를 확인해보면, 해당 함수는 dl_rtld_lock_recursive라는 함수 포인터임을 알 수 있다.
해당 함수 포인터는 _rtld_global 구조체의 멤버 변수이다.
해당 구조체는 매우 커서 함수 포인터와 전달되는 인자인 dl_load_lock 만을 알아보자
2.4 _rtld_global
gdb에서 _rtld_global 구조체를 출력한 모습이다.
구조체 내 _dl_rtld_lock_recursive 함수 포인터에는 rtld_lock_default_lock_recursive 함수 주소를 저장하고 있다.
구조체의 함수 포인터가 저장된 영역은 읽기 및 쓰기 권한이 존재하기 때문에 덮어쓰는 것도 가능하다.
보면 _rtld_global 구조체가 매핑되는 영역은 "/lib/x86-64-linux-gnu/ld-2.27.so 이다.
_rtld_global 초기화
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;
이 코드는 "프로세스를 로드"할 때 호출되는 dl_main 코드의 일부로,
(종료할 때는 호출이 안됨)
_rtld_global 구조체의 dl_rtld_lock_recursive 함수 포인터가 초기화되는 것을 확인할 수 있다.
마치며
리턴 명령어는 실제로 로더에서 다양한 함수를 호출해 프로그램을 종료한다는 것을 알 수 있었다.
호출하는 함수 중 _rtld_global 구조체 내 함수 포인터는 프로세스가 실행되면서 초기화되며,
이는 읽고 쓸 수 있는 영역에 위치하기 때문에 임의 주소에 값을 쓸 수 있는 취약점이 있다면
이를 덮어써서 실행 흐름을 조작할 수 있다.
Reference