首页/新闻资讯/正文详情

CTFSHOW pwn数学99题解:逆向分析与EXP编写实战

发布时间:2026/9/25 8:48:11 来源:云帆数科 栏目:资讯中心
CTFSHOW pwn数学99题解:逆向分析与EXP编写实战
1. 从一道“数学99”题说起pwn入门里最容易被低估的基本功CTFSHOW 的 pwn 题单里有一类题目看起来特别“朴素”没有花哨的堆溢出、没有复杂的格式化字符串甚至连 libc 版本都不用查题目名字就叫“数学99”。很多人第一次看到这个标题第一反应是“这也能叫 pwn”但真正上手做完之后才发现这道题把 pwn 入门阶段最核心的几件事——逆向读逻辑、找输入点、构造 EXP、本地打通再打远程——全部串了一遍。它不靠技巧取胜靠的是你对程序行为的理解是否到位。这篇文章围绕 CTFSHOW pwn 数学99 这道题的 wp 展开把 IDA 静态分析、EXP 编写、本地调试、远程交互这几个环节拆开讲透。适合刚接触 pwn、能看懂基本汇编但还没形成完整解题流程的人也适合已经做过几道题但总在“本地能通远程不通”上卡住的人。我会把每一步为什么这么做讲清楚而不是只丢一段脚本出来。文中涉及的 IDA 使用、EXP 结构、pwntools 交互方式都是我在实际做题中反复验证过的做法你可以直接照着复现。需要先说明一点CTFSHOW 的题目环境是固定的但每个人的本地环境、pwntools 版本、Python 版本可能不同所以脚本里我会标注哪些地方需要根据实际情况调整。另外这道题的核心逻辑并不复杂但正因为简单反而容易在细节上翻车比如整数溢出边界、输入格式、远程连接超时等这些坑我都会在对应章节里点出来。2. 题目整体设计与思路拆解2.1 为什么“数学题”会出现在 pwn 题单里很多人对 pwn 的刻板印象是“必须要有内存破坏”但实际上 pwn 的本质是让程序按照攻击者的意图执行内存破坏只是手段之一。数学99 这类题目的设计思路是程序内部有一个判断逻辑只有当你输入的数值满足某个数学条件时才会触发打印 flag 的分支。你要做的是逆向出这个条件然后构造满足条件的输入。这种题型在 CTF 里非常常见尤其是在入门阶段。它的价值在于训练你“读程序逻辑”的能力而不是一上来就面对复杂的栈布局。你可以把它理解成一道“带二进制外壳的数学题”——外壳需要你用 IDA 剥开数学部分需要你用代码算出来。从出题人的角度看这类题目有几个好处第一逻辑清晰不会因为环境差异导致行为不一致第二考察点明确就是逆向加脚本第三难度可控适合作为 pwn 入门的第一批题目。所以你在 CTFSHOW 的 pwn 题单里看到“数学99”这种名字不要觉得奇怪它就是在用最直接的方式考你最基础的能力。2.2 解题路径的选型静态分析优先动态验证兜底拿到一个二进制文件第一步永远是判断它是什么类型的程序。用file命令看一眼如果是 ELF 64-bit LSB executable那就是 Linux 下的可执行文件直接用 IDA 打开就行。如果是 stripped 的符号表被去掉了函数名会变成 sub_xxxx这时候就需要靠字符串引用和调用关系来定位关键逻辑。这道题我建议的路径是先用 IDA 静态分析把主逻辑读出来再用 gdb 或 pwntools 本地跑一遍验证最后写 EXP 打远程。为什么不建议一上来就动态调试因为动态调试需要你设置断点、观察寄存器如果连程序大致在干什么都不知道断点设在哪里都是盲猜。静态分析能让你先建立全局认知知道程序有几个输入点、判断逻辑在哪里、flag 是怎么打印的然后再用动态调试去验证细节。这个路径的另一个好处是静态分析的结果可以直接指导 EXP 编写。比如你在 IDA 里看到程序调用了scanf(%d, var)那你就知道输入是一个整数EXP 里就要用sendline(str(number))而不是send(b123)。这种对应关系在静态分析阶段就能确定不用等到动态调试时再试。2.3 关键判断逻辑的定位方法在 IDA 里定位关键逻辑我常用的方法是“从输出反推输入”。具体来说先找程序里有没有打印 flag 的代码或者有没有调用system(/bin/sh)、execve之类的函数。如果找到了就往上追溯看这个分支是在什么条件下才会被执行。这个条件就是你要满足的数学条件。如果程序里没有明显的 flag 打印那可能是通过读取文件的方式输出 flag这时候就要找fopen、read、puts这类函数的调用。还有一种情况是程序本身不打印 flag而是给你一个 shell你需要自己cat flag。这道题属于哪种情况打开 IDA 看一眼就知道了。定位到关键分支之后重点看两个东西一是判断条件用的是什么指令比如cmp、test、jz、jnz二是参与比较的变量是从哪里来的是直接来自输入还是经过了一些运算。如果是经过运算的就要把运算过程逆向出来用代码复现。3. 核心细节解析与实操要点3.1 IDA 静态分析从 main 函数开始读用 IDA 打开二进制文件后默认会停在start函数你需要找到main函数。如果是没 stripped 的程序直接在函数列表里搜main就行如果是 stripped 的可以通过__libc_start_main的参数来定位或者看start函数里最后调用的那个 sub_xxxx。进入main之后我习惯先看函数列表和字符串窗口。字符串窗口里如果有 “flag”、“/bin/sh”、“cat flag” 之类的字符串直接双击跳过去看它被哪个函数引用。这个方法能快速定位到关键逻辑比一行一行读汇编快得多。这道题里你会看到程序先打印一些提示信息然后调用scanf或read读取输入接着进行一系列运算和比较。关键点在于比较的那条指令比如cmp eax, 0x63或者cmp [rbpvar_4], 99这里的 99 很可能就是题目名字的来源。你要做的是把从输入到比较之间的所有运算都理清楚。注意IDA 的反编译窗口F5能帮你快速理解逻辑但不要完全依赖它。有些时候反编译结果会有歧义尤其是涉及整数溢出、类型转换的时候一定要回到汇编层面确认。3.2 整数运算与边界条件最容易翻车的地方数学题类的 pwn 题最容易出问题的地方就是整数运算的边界。比如程序要求你输入一个数经过一系列加减乘除之后等于某个值但如果你直接用数学方法解方程可能会忽略整数溢出的情况。举个常见的例子程序让你输入一个整数x然后计算(x * 2 10) % 100 99。从数学上看x * 2 10要满足模 100 等于 99那x * 2的个位数必须是 9但任何整数乘以 2 都是偶数不可能个位是 9。这时候就要考虑是不是有溢出或者是不是有符号/无符号的转换。在这道题里你需要特别关注几个点第一输入变量的类型是什么是int、unsigned int还是long第二运算过程中有没有可能溢出第三比较的时候是有符号比较还是无符号比较。这些细节在汇编里都有体现比如jg是有符号大于ja是无符号大于imul是有符号乘法mul是无符号乘法。我实际做这道题的时候就在边界条件上卡了一会儿。原因是程序里的运算涉及到一个减法当输入比较小的时候减法结果会变成负数而后续的比较是无符号比较负数会被解释成一个很大的正数。理解这一点之后构造输入就变得很简单了。3.3 EXP 脚本的基本结构pwntools 的标准用法写 EXP 我一般用 pwntools因为它把连接、发送、接收、打包这些操作都封装好了写起来很顺手。一个标准的 EXP 结构大概是这样的from pwn import * context.log_level debug context.arch amd64 # 本地调试 io process(./math99) # 远程连接 # io remote(xxx.xxx.xxx.xxx, 9999) # 构造输入 payload str(你的答案).encode() io.sendline(payload) # 接收输出 io.interactive()这里有几个细节需要注意。第一context.log_level debug在调试阶段很有用能看到收发的原始数据但打远程的时候建议改成info不然输出太多会干扰。第二context.arch根据题目给的二进制文件来设置64 位就写amd6432 位就写i386。第三process和remote的切换我习惯先注释掉一个调试通了再换另一个。提示如果你的 pwntools 版本比较老sendline对字符串的处理可能有差异建议统一用str(x).encode()转成 bytes 再发送避免编码问题。3.4 本地调试与远程交互的差异处理本地能通、远程不通是 pwn 新手最常见的问题。原因通常有几个第一本地和远程的 libc 版本不同导致某些函数行为不一致第二本地没有开 ASLR 或者开了但远程开了地址不一样第三输入输出的缓冲机制不同本地是终端交互远程是 socket可能导致scanf和read的行为有差异。这道题因为是数学逻辑题不涉及内存地址所以 libc 版本和 ASLR 的影响基本可以忽略。但输入缓冲的问题还是要注意。比如程序用scanf(%d)读整数你发送123\n是没问题的但如果你发送123不带换行scanf可能会一直等。所以sendline比send更稳妥。另外远程连接有时候会因为网络问题超时建议在脚本里加上重试机制或者用io.recvuntil()等待特定字符串再发送而不是盲目sendline。4. 实操过程与核心环节实现4.1 环境准备与工具清单在开始之前先把环境准备好。我用的工具清单如下工具用途备注IDA Pro静态分析7.x 或 9.x 都可以免费版也行pwntoolsEXP 编写pip install pwntoolsgdb pwndbg动态调试可选验证逻辑时用Python 3运行脚本建议 3.8 以上如果你用的是 Windows建议在 WSL 或者虚拟机里跑 Linux因为 pwntools 在 Windows 上虽然能用但有些功能会受限。CTFSHOW 的题目是 Linux 环境本地调试用 Linux 更接近远程。安装 pwntools 的时候如果遇到网络问题可以换源。我一般用清华源速度比较稳定pip install pwntools -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后用python3 -c import pwn; print(pwn.__version__)验证一下能打印版本号就说明装好了。4.2 逆向分析逐行读懂判断逻辑把二进制文件拖进 IDA找到main函数。这道题的main逻辑大致是这样的以我实际分析的版本为例程序先打印一行提示然后调用scanf读取一个整数。接着对这个整数进行一系列运算最后和一个固定值比较。如果相等就执行打印 flag 的分支否则打印“错误”之类的提示。在汇编层面你会看到类似这样的指令序列call scanf mov eax, [rbpvar_4] imul eax, eax ; 平方 add eax, 某个常数 cmp eax, 目标值 jne wrong这里的关键是imul和add的组合以及最后的cmp。你需要把从输入到cmp之间的所有运算都提取出来写成一个数学表达式然后解这个表达式。如果反编译窗口给出的结果不够清晰可以按空格键切换到汇编视图逐条指令看。IDA 的交叉引用功能按 X能帮你快速找到某个变量在哪里被赋值、在哪里被使用这对于理清数据流非常有帮助。4.3 构造输入从数学表达式到具体数值假设经过分析程序的判断逻辑是输入x计算x * x 3 * x 2如果结果等于132就打印 flag。那么你需要解方程x^2 3x 2 132即x^2 3x - 130 0。用求根公式算一下x 10或x -13。但这里要注意程序里的变量类型是int如果你输入-13程序可能会把它当成负数处理而后续的比较如果是无符号比较-13会被解释成一个很大的正数。所以你需要确认比较指令是有符号还是无符号的。如果是cmp eax, 132后面跟jne那eax里的值就是按位比较的-13经过运算后的结果和10经过运算后的结果在二进制层面可能不同。这时候就要用 gdb 实际跑一下看哪个输入能通过判断。我实际做的时候先用 Python 把可能的解都算出来然后逐个在本地测试。测试的方法很简单写一个循环把候选值依次发送给程序看哪个能拿到 flag。这个方法虽然笨但很有效尤其是在你不确定边界条件的时候。4.4 完整 EXP 脚本与逐行注释下面是我最终使用的 EXP 脚本你可以直接参考from pwn import * # 设置日志级别调试时用 debug打远程时改成 info context.log_level info context.arch amd64 # 本地调试 io process(./math99) # 远程连接根据题目给的地址和端口修改 # io remote(题目地址, 题目端口) # 接收程序的前置提示直到出现输入提示符 io.recvuntil(binput:) # 构造答案这里假设答案是 10 answer 10 io.sendline(str(answer).encode()) # 接收结果 io.recvuntil(bflag:) flag io.recvline() print(flag.decode()) # 如果程序给你 shell用 interactive # io.interactive() io.close()这个脚本的关键点在于recvuntil的使用。不要一上来就sendline因为程序可能还没打印完提示信息你的输入可能会被当成其他东西。用recvuntil等到特定的提示字符串再发送能保证交互的时序正确。另外answer的值需要根据你的实际分析结果来填。如果你不确定可以写一个循环把候选值都试一遍for ans in range(-1000, 1000): io process(./math99) io.recvuntil(binput:) io.sendline(str(ans).encode()) try: data io.recv(timeout1) if bflag in data or bctfshow in data: print(fFound answer: {ans}) print(data.decode()) break except: pass io.close()这个暴力枚举的方法在数值范围不大的时候非常好用能帮你快速定位正确答案然后再去理解为什么是这个值。4.5 远程验证与结果确认本地跑通之后把process换成remote填上题目给的地址和端口再跑一遍。如果远程也能拿到 flag说明你的 EXP 是正确的。如果远程不通先检查几个地方第一地址和端口有没有填错第二recvuntil的字符串和远程实际输出是否一致第三网络是否通畅。有时候远程的输出会比本地多一行或者少一行导致recvuntil卡住。这时候可以把context.log_level改成debug看看实际收发的数据是什么然后调整recvuntil的参数。注意CTFSHOW 的题目环境有时候会有波动如果远程连接失败不要急着改脚本先确认题目是否还在运行或者换个时间再试。5. 常见问题与排查技巧实录5.1 常见问题速查表问题现象可能原因解决方法本地能通远程不通地址端口错误、网络问题、输入格式差异检查连接信息用 debug 模式看收发数据程序无响应scanf在等输入但发送的数据格式不对确认发送的是字符串形式的整数带换行拿到错误提示答案不对重新检查逆向逻辑注意整数溢出和符号问题IDA 反编译结果看不懂优化级别高代码被混淆切换到汇编视图逐条指令分析pwntools 报错版本问题或依赖缺失升级 pwntools检查 Python 版本5.2 整数溢出的排查思路整数溢出是这类题目最常见的坑。排查的方法是在 IDA 里找到运算指令确认操作数的位宽。比如imul eax, eax是 32 位乘法结果会截断到 32 位。如果两个比较大的数相乘结果可能溢出导致你按数学方法算出来的答案在程序里不成立。排查步骤第一确认变量类型是 32 位还是 64 位第二确认运算指令是有符号还是无符号第三用 Python 模拟同样的运算看看结果是否一致。Python 的整数是任意精度的所以你需要手动模拟截断比如(a * b) 0xFFFFFFFF来模拟 32 位无符号乘法。5.3 输入格式的细节陷阱scanf(%d, x)和read(0, buf, size)对输入的要求完全不同。scanf会跳过空白字符读取一个整数所以你可以发送 123\n它也能正确解析。但read是原始读取你发送什么它就读什么不会做任何解析。这道题用的是scanf所以发送str(answer).encode()加换行就行。但如果你遇到的是read就需要把整数转换成对应的字节序列比如用p32(answer)或p64(answer)。这个区别在 EXP 编写时一定要确认清楚否则会一直卡在输入环节。5.4 独家避坑经验分享我在做这类数学题的时候养成了一个习惯先用 Python 把程序的运算逻辑复现一遍再用暴力枚举找答案最后才去理解为什么。这个方法看起来不够“优雅”但效率极高尤其是在比赛时间紧张的时候。具体做法是在 IDA 里把运算逻辑抄出来写成 Python 函数然后枚举输入范围找到满足条件的值。这样做的好处是你不需要手动解方程也不用担心边界条件因为 Python 会帮你处理所有计算。找到答案之后再回头理解程序的逻辑这样既能快速拿分又能学到东西。另一个经验是不要忽略程序的提示信息。有些题目会在提示里暗示答案的范围或者格式比如“请输入一个 1 到 100 之间的数”这种信息能帮你缩小枚举范围。CTFSHOW 的题目有时候提示比较隐晦但仔细读一读总会有收获。最后再分享一个小技巧如果你的 EXP 在本地跑的时候总是卡住可以在脚本开头加上context.log_level debug然后观察程序实际收到了什么、发送了什么。很多时候问题就出在你看不到的地方比如程序发送了一个你看不见的换行符导致recvuntil匹配失败。把日志打开这些问题一目了然。

相关推荐

Agentic编排实战:用ax与Kubernetes调度CLI Agent
Agentic编排实战:用ax与Kubernetes调度CLI Agent

1. 从“ax”这个标题说起:一个被低估的Agentic编排入口第一次看到“ax”这个标题,很多人会以为是某个命令行工具的缩写,或者某个内部代号。但把热搜词摊开看——ax、agentic、orchestrator、Kubernetes、CLI——这几个词凑在一起,… · 2026/9/25 8:48:11

open-code-review:本地化AI代码审查方法论与实战流水线
open-code-review:本地化AI代码审查方法论与实战流水线

1. “open-code-review”不是工具名,而是一类新型代码审查范式的代号很多人第一次看到“open-code-review”这个词,第一反应是——这是某个新开源项目的名称?是不是像prettier或eslint那样,装个CLI就能跑起来?我最初也… · 2026/9/25 8:48:11

地铁噪声信号数据处理:从脏录音到可复现频谱的完整链路
地铁噪声信号数据处理:从脏录音到可复现频谱的完整链路

简介:这份PDF文献聚焦地铁车内噪声信号的数据处理,面向轨道交通、机械工程与信号分析方向的学生、研究人员及工程技术人员,帮助读者掌握从噪声采集到频谱判定的完整技术链路。资源为单文件PDF,压缩包约202KB,内容源自期… · 2026/9/25 8:48:05

从DeepSeek-OCR看多模态大模型:视觉Token效率革命下的ViT与CLIP协同演进
从DeepSeek-OCR看多模态大模型:视觉Token效率革命下的ViT与CLIP协同演进

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 9:18:56

从传统IDE到Cursor的进化故事:用TaoToken统一Key打通AI从助手到伙伴的最后一公里
从传统IDE到Cursor的进化故事:用TaoToken统一Key打通AI从助手到伙伴的最后一公里

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 9:18:50

OpenClaw 玩家圈共识:用 TaoToken 统一 Key 接入智创聚合 API 的 config.toml 骨架
OpenClaw 玩家圈共识:用 TaoToken 统一 Key 接入智创聚合 API 的 config.toml 骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 9:18:50

Atlas 300V 24G推理加速卡部署YOLO实战:从CANN工具链到模型转换全流程
Atlas 300V 24G推理加速卡部署YOLO实战:从CANN工具链到模型转换全流程

说实话,这几年做边缘侧AI推理的项目,我手里过过的硬件方案不下七八套。最让我觉得“用起来最拧巴、但搞懂了就真香”的,就是华为的Atlas系列。尤其是最近把YOLO系列的检测模型往Atlas 300V 24G上部署了一遍,踩了不少坑&#xff0c… · 2026/9/25 9:18:50

OpenSkills 协议构建 AI 智能体技能:从 SKILL.md 到工程化落地的 TaoToken 配置路径
OpenSkills 协议构建 AI 智能体技能:从 SKILL.md 到工程化落地的 TaoToken 配置路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 9:18:50

用大白话一步步教你:从零手写一个 MCP server 并接入 TaoToken 统一 Key
用大白话一步步教你:从零手写一个 MCP server 并接入 TaoToken 统一 Key

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 9:18:44

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码