思路
ai做的wp,我们只需要明白这题要怎么做,并且问MCP(见https://blog.youkuaiyun.com/aa_troux/article/details/150859160?spm=1001.2014.3001.5501)。
做题的思路是先找主要函数main:
- 看 main:return sub_140013960(sub_1400013A0, argc, argv);
- 这里 sub_140013960 是个运行时/框架函数,真正的业务逻辑是把 sub_1400013A0 当回调调用。
然后从一连串函数调用末尾找到打印成功或失败信息的位置,再顺藤摸瓜找到 if(check(...)) 的 check 函数。再详细了解函数内容明白校验与约束条件,按 Win x64 调用约定:a1=RCX=flag 指针,a2=RDX=长度,然后利用符号执行angr。
- 起点:起点地址(sub_140001103(0x140001103))
- 目标:函数内“返回 1”的地址(0x14000131D)
- 参数:RCX=指向我们放 flag 的内存,RDX=长度 0x1c
- 给 flag 加上可见约束(前缀 "FLAG"、长度 28,必要时可限定可打印),其余交给 angr;7 字节滚动校验由 angr 自动满足(因为它在路径条件里)
由此可以向gpt5询问如何写脚本利用。
“你正在使用IDA打开一个CTF题目(Rusted_from_the_Rain,Rust语言编译的程序),请分析该程序,告诉我 IDA 里确认到的两个地址(函数入口和返回 1 的地址)。”
- 函数入口地址:0x140001103(sub_140001103)
- 返回 1 的地址:0x14000131D
AIのwp
概览
- 类型:Reverse + 符号执行(angr)
- 目标:恢复正确的 flag(作为命令行参数传入)
- 结论要点:
- 校验函数:sub_140001103(char* a1, unsigned long long a2)
- flag 长度固定 0x1c(28)
- 前 4 字节必须为 "FLAG"
- 函数返回 1 为成功;找到返回 1 的地址 0x14000131D
- 用 angr 从校验函数入口直接起跑,施加约束后探索到成功地址求解出 flag
环境与工具
- 系统:Linux
- 逆向:IDA Pro
- 求解:Python 3 + angr + claripy
- 备注:PE 程序在 Linux 上直接用 angr 装载即可(auto_load_libs=False),常见库调用由 angr 的 SimProcedure 模拟
题目初探
- 文件:ctf_normie_cpu-14f48b9bc8ff3d1d.exe(PE32+,x86-64)
- 运行方式:程序从命令行读取 flag(argv[1])作为输入
- 字符串中可看到 “Yep, congrats.” 和 “Try again.”,表明存在成功/失败两个分支
静态分析(IDA)
-
定位主流程
- 在字符串窗口(Shift+F12)搜索 “congrats” 或 “Try again”,跟踪引用到调用处。
- 可见在调用某个校验函数后,根据返回值选择打印不同的字符串。
-
校验函数签名与调用惯例
- 函数原型:sub_140001103(char* a1, unsigned __int64 a2)
- Windows x64 调用约定:RCX, RDX, R8, R9 传前四个参数;这里 RCX=a1(flag 指针),RDX=a2(长度)。
-
关键约束与返回点
- 在函数内部可见对长度的检查:a2 必须为 0x1c。
- 对前缀的检查:"FLAG"。
- 成功分支最终会设置 EAX=1 并返回;在本二进制中,返回 1 的地址为 0x14000131D(GOOD_ADDR)。
- 函数入口地址为 0x140001103(CHECK_FUN)。
符号执行(angr)
思路:不从程序入口跑(避免 Windows 运行时/初始化的复杂性),而是直接从 sub_140001103 的入口建一个状态;把 a1 指定为我们放置 flag 的内存地址,把 a2 固定为 0x1c;将 flag 符号化并加入必要约束,然后让 angr 寻找能到达“返回 1”地址的路径,最后求解出具体字节。
import angr
import claripy
BIN = "./ctf_normie_cpu-14f48b9bc8ff3d1d.exe"
CHECK_FUN = 0x140001103 # sub_140001103 的入口
GOOD_ADDR = 0x14000131D # 返回 1 的地址(成功分支)
FLAG_LEN = 0x1c # 28 字节
BUF_ADDR = 0x600000 # 任一未用、可写的内存地址
def main():
proj = angr.Project(BIN, auto_load_libs=False)
# 更贴合调用约定的启动方式(会设置好 RCX/RDX 等)
state = proj.factory.call_state(CHECK_FUN, BUF_ADDR, FLAG_LEN)
# 准备符号化的 flag,并写入内存
flag = claripy.BVS("flag", FLAG_LEN * 8)
state.memory.store(BUF_ADDR, flag)
# 约束:前缀 + 可打印 ASCII
state.solver.add(flag.get_bytes(0, 4) == claripy.BVV(b"FLAG"))
for i in range(FLAG_LEN):
b = flag.get_byte(i)
state.solver.add(b >= claripy.BVV(0x20, 8))
state.solver.add(b <= claripy.BVV(0x7e, 8))
# 如已知失败返回 0 的基本块地址,可加 avoid=[...]
sm = proj.factory.simgr(state)
sm.explore(find=GOOD_ADDR)
if sm.found:
s = sm.found[0]
model = s.solver.eval(flag, cast_to=bytes)
print("flag:", model.decode("ascii", errors="ignore"))
else:
print("没有找到路径,请核对地址或增加 avoid。")
if __name__ == "__main__":
main()
运行与结果
- 运行命令:python3 solve.py
- angr 会从 CHECK_FUN 开始执行,带着我们设定的约束寻找能到达 GOOD_ADDR 的路径。
- 找到后会给出一个满足所有路径与显式约束的 flag,并打印类似:
- flag: FLAG................................
要点与坑位
- 起点与目标地址要在 IDA 中核对一致:
- 起点:sub_140001103 的函数入口 0x140001103
- 目标:返回 1 的地址 0x14000131D(也可用其所在基本块入口)
- 字符约束的意义:
- 通过限制可打印 ASCII,避免空字节导致字符串函数提前终止,减少搜索空间,加速求解。
- RCX/RDX 的设置:
- 若用 blank_state,需要手动设置 RCX/RDX 并写内存;用 call_state 可自动处理调用约定(推荐)。
- 性能优化建议:
- 如能定位失败分支的“返回 0”地址,可 sm.explore(find=GOOD_ADDR, avoid=[BAD_ADDR]) 加速。
- 若路径复杂,可以把起始地址改为更接近成功分支的基本块,进一步减少搜索空间。
- PE on Linux:
- auto_load_libs=False 即可在 Linux 上装载 Windows PE;常见库调用由 angr 的 SimProcedures 处理,无需真实 DLL。
总结
- 核心流程:逆向明确参数/约束 → 锁定成功地址 → angr 从校验函数入口符号执行并施加约束 → 探索到成功分支 → 求解输入。
- 本题难点不在复杂算法,而在于正确建模 Windows x64 调用约定、给足约束,并把探索目标精确到“返回 1”的地址。给出的两个关键地址(0x140001103 与 0x14000131D)正好让 angr 快速收敛。
839

被折叠的 条评论
为什么被折叠?



