본문 바로가기
Security/System Hacking

[Dreamhack System Hacking Advanced] STAGE 4

by 단월໒꒱ 2022. 9. 25.

 

Background: _rtld_global

 

  이번에는 프로그램을 종료하는 과정을 이용한 공격 기법을 알아보기에 앞 라이브러리의 코드를 분석하면서 어떤 방식으로 프로세스를 종료하는지 알아보도록 하자.

 

  다음 코드는 종료하는 과정을 알아보기 위한 예제로, 컴파일해두자.

 

 

// Name: rtld.c
// Compile: gcc -o rtld rtld.c
int main() {
  return 0;
}

 

 

_rtld_global

 

  • __Gl_exit

  바로 위에서 컴파일한 예제 코드는 별다른 코드를 실행하지 않고 프로그램을 종료한다. 프로그램을 종료할 때에는 우리가 모르는 많은 코드들이 내부적으로 실행되는데, 디버깅으로 한번 살펴보자.

 

  먼저 다음과 같이 main 함수 내 리턴하는 명령어에 BP를 설정하고, step into를 통해 다음 코드를 확인해본다.

 

 

 

 

  디버깅 결과를 살펴보면, main 함수 내에서 리턴 명령어를 실행하면 스택 최상단에 있는 __libc_start_main+231의 코드가 실행되고, 내부에서 __GI_exit 함수를 호출하는 것을 볼 수 있다. 

  __GI_exit 함수 내부에서 또 다른 함수를 호출하는지 확인하기 위해 다시 한번 step into 명령어를 실행한다.

 

 

  다음은 __GI_exit 함수 내부의 모습으로, 또 다른 __run_exit_handlers 함수가 보인다.

 

 

 

 

이 __run_exit_handlers 함수는 코드의 크기가 크므로, 라이브러리 코드를 통해 분석해보자.

 

 

  • __run_exit_handlers

  다음은 __run_exit_handlers 함수의 코드로 exit_function 구조체의 멤버 변수에 따른 함수 포인터를 호출한다.

 

 

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;
	    }
	}

 

 

  해당 구조체의 모습은 아래와 같으며, 예제와 같이 리턴 명령어를 실행해 프로그램을 종료한다면 _dl_fini 함수를 호출한다.

 

 

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;
};

 

 

  • _dl_fini

  다음은 로더에 존재하는 _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)
    {
      /* Protect against concurrent loads and unloads.  */
      __rtld_lock_lock_recursive (GL(dl_load_lock));

 

 

  코드를 살펴보면, _dl_load_lock을 인자로 __rtld_lock_lock_recursive 함수를 호출하는 것을 볼 수 있고, 매크로를 보면, 해당 함수는 dl_rtld_lock_recursive라는 함수 포인터임도 알 수 있다.

  이 함수 포인터는 _rtld_global 구조체의 멤버 변수이다. 해당 구조체는 매우 방대하기 때문에 함수 포인터와 전달되는 인자인 dl_load_lock만을 살펴보자.

 

 

  • _rtld_global

  다음은 디버깅에서 _rtld_global 구조체를 출력한 내용이다.

 

 

 

 

  구조체 내 _dl_rtld_lock_recursive 함수 포인터에는 rtld_lock_default_lock_recursive 함수 주소를 저장하고 있다. 구조체의 함수 포인터가 저장된 영역은 읽기 및 쓰기 권한이 존재하기 때문에 덮어쓰는 것도 가능하다.

 

 

 _rtld_global 초기화

 

  다음은 프로세스를 로드할 때 호출되는 dl_main 코드의 일부이다.

 

 

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;

 

 

  _rtld_global 구조체의 dl_rtld_lock_recursive 함수 포인터가 초기화되는 것을 확인할 수 있다.

 

 

 

 

 

댓글