프로그램의 실행 순서
간단한 코드를 통해서 확인해보겠습니다.
// 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 구조체의 함수 포인터를 조작하여 프로그램의 실행흐름을 변경할 수 있습니다.





















