ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

逆向工程入门:BUUCTF RE刷题实战笔记与工具链详解

逆向工程入门:BUUCTF RE刷题实战笔记与工具链详解 有段时间我打开BUUCTF的RE分区看着慢慢变长的题单起了个很随意的标题看心情写。在这个标题下我攒了一堆零散的逆向笔记和脚本碎片。真正开始刷之后我发现“看心情”其实是种被低估的学习策略——状态差的时候写点简单题热身状态好的时候啃硬骨头比硬着头皮按顺序往下刷效率高得多。这篇东西不打算写成系统教程更像是我把自己的私下笔记摊开把BUUCTF上RE题最常出现的套路、最常用的工具、以及那些我踩过的坑按照我实际做题的顺序重新整理一遍。适合刚接触逆向、想在BUUCTF上动手刷题但不知道从哪下手的读者也适合那些刷了几道题、卡在“看得懂但要写出flag总是差一步”阶段的朋友。1. 这道“看心情写”的RE题单到底在刷什么1.1 为什么选BUUCTF当RE刷题主战场BUUCTF最讨人喜欢的地方是它把各个CTF比赛的历年真题收集到一个平台不需要你满世界找附件、找远程端口。对RE来说这意味着你能在几周内接触到不同出题风格的题目国内高校赛的、强网杯的、各类国际赛的全部集中在同一个网页里。做题的时候不用切换十几个平台每道题有固定附件有提交flag的地方甚至有的题还有提示板块。它的题目难度分布也比较合理从最简单的“逆向签到题”到需要花一整天慢慢调的函数级混淆题都有。我自己的体会是把BUUCTF当RE“专项训练场”非常合适。因为分类明确你想练ELF就刷ELF想练PE就刷PE想练安卓逆向也有对应的APK题。这种分类环境能让练习有的放矢而不是每个比赛都混着大量自己不会的题型。所以我的“看心情写”其实只解决一个问题——今天练哪方面的逆袭能力而不是漫无目的地乱刷。1.2 “看心情”不等于乱刷选题的几类标准如果真的一点章法没有刷题效率会很低。我的做法是提前把RE题按几个粗糙标签分类加壳题、算法题、游戏题、混淆题、迷宫题、安卓题。然后“看心情”指的是在某一类里挑当天感兴趣的一道而不是今天随便点开哪个算哪个。举个例子连着做了两三道纯算法题之后脑子对TEA、RC4这类算法模式已经比较熟这时候我会临时切一道花指令题换换场景让大脑换一种分析模式。这种切换本身也是逆向训练里重要的一部分因为真实工作中你不会知道自己下一份样本是什么风格。有意识地调换题目类型比一直死磕同一类题更容易积累全面的经验。此外我还会设定一个硬性条件每道题至少要写到“提取出完整的flag字符串”这一步才能算完。很多题目你感觉思路通了、脚本跑了、结果也有了但最后提交时发现大小写搞反、少一位、编码方式理解错了这一类细节会直接决定“会不会做”和“能不能得分”之间的差距。2. 拿到一个RE题前三分钟的静态侦察决定一半成败2.1 永远从文件类型和外壳开始我见过不少人拿到附件直接拖进IDA然后盯着一段没有任何上下文的机器码发呆半钟头。我自己的习惯是先做一套固定动作。第一步永远是file在Linux里看文件类型file 题目附件 # 常见结果ELF 64-bit LSB executable # PE32 executable (console) Intel 80386 # MS-DOS executable # Java archive (JAR) # Android package (APK)如果是一条ELF文件就顺便看一下有没有strip、arch大小以及PIE是否开启readelf -h 题目附件 | grep Type checksec --file题目附件 # 如果你有pwntools如果是Windows PE我习惯用Exeinfo PE或者DIEDetect It Easy再查一下壳。DIE的界面更直观拖进去就能看到编译器信息、是否加壳、加的是什么壳。很多时候“看似复杂的题”其实只是UPX壳直接命令行upx -d 附件.exe能解决一大半问题。这一步的意义在于决定后续策略。静态能看到正常代码就不用浪费时间在动态调试上如果发现壳先想到脱壳而不是直接硬逆。去年有一阵子我连续几次在PE题上栽跟头后来一查全是没先查壳把UPX压缩代码当成了原始逻辑去分析白白浪费一小时。2.2 字符串、导入表、入口点三个最快的信息源过完文件类型我基本会打开IDA或者Ghidra但第一件事不是看反汇编而是先看字符串窗口。ShiftF12在IDA里会列出所有字符串这一步能带给你大量信息如果程序有交互流程比如要你“输入flag”或者“Wrong/Correct”字符串窗口里会直接出现这些提示如果程序是从文件读入数据你会看到文件名、打开失败等报错字符串如果程序调用了系统命令你甚至能看到system(./getflag)这类逻辑尾巴如果题目用了加密算法字符串里经常能找到特征常量比如TEA的0x9E3779B9、RC4的S盒初始化所需的密钥提示、或者某个表名。然后我会快速扫一遍导入表。Windows PE在IDA的Imports窗口里能看到调用了哪些API如果一堆GetAsyncKeyState、SetWindowsHookEx那多半是键盘记录类程序如果大量CreateFile、ReadFile这是文件读取型。Linux ELF则主要关注puts/printf/scanf/strcmp有strcmp的题通常意味着比较逻辑藏在某个函数里可以顺着交叉引用找关键点。最后看一眼入口点。如果入口点不是我们常见的main、_start或者PE的WinMain而是落在某个奇怪的地址那就要警惕是不是有自定义启动逻辑或者入口被混淆改写。很多时候这些“不自然”的位置恰恰是出题人藏东西的地方。2.3 静态侦察的产出先给题目“建档案”把上面信息集中起来我会在笔记里写一行“题目档案”差不多长这样[jocker] PE32 | 无壳(?待确认) | 有大量花指令 | 提示字符串“Locker”“You are wrong” [snake] ELF64 | 非PIE | 游戏逻辑型 | 有关键字符串“score”“flag” [findme] PE32(MFC?) | 有大量导出函数 | 需定位主回调 | 字符串分散建档案是给自己省时间因为你经常会在一个晚上同时开三四个题大脑容易混。几行字能让你快速回到之前的上下文。更重要的是通过画像你能大致预测自己要用什么解法比如看到“OLLVM混淆”可能就需要准备好Angr或者花时间动态跟踪看到“算法题”就直接准备Z3和Python脚本库。不要高估自己的记忆力RE分析中上下文切换的成本特别高档案越清晰切换代价越低。3. 主逻辑拆解三板斧F5太顺是运气不顺才见功底3.1 第一板斧C裸题的直接F5BUUCTF里有很多签到级别的题逻辑就是C语言裸写的读入字符串经过一个简单函数变换再和明文或者某个内存地址比较。这类题的IDA反编译页面非常干净F5之后就是主函数int __cdecl main(int argc, const char **argv) { char input[64]; scanf(%s, input); if ( strcmp(encrypt(input), 目标串) 0 ) puts(Right); else puts(Wrong); }遇到这种题别急着去手动模拟每个字节的逻辑。我的做法是先确定strcmp的第二个参数地址然后在调试器里直接下断点程序跑到strcmp时读它的参数。如果比较参数是全局数组一种简单粗暴的方式是直接看数据段里“目标串”附近的字节答案常常就明文躺在那里。这里有个小技巧IDA里选中strcmp的调用点按X看交叉引用能快速确认谁是参与比较的数据。千万不要在没有搞清楚比较函数之前就去读整个程序的伪代码那样很容易被无关变量带偏。3.2 第二板斧算法识别——TEA、RC4、Base64换表签到题之外BUUCTF最常见的一类就是算法题。出题人把flag经过某种加密变换之后存在内存里你输入的内容也走同样变换然后比较。常见的算法有以下几种每个都有比较明显的特征算法特征识别方式TEA/XTEA/XXTEA常量0x9E3779B9、delta参与加法/异或搜索常量看是否有循环处理8字节块RC4S盒初始化循环256次 swap密钥长度不定看到256长度的数组和swap循环基本可以锁定AES固定的S盒、轮常量表加密流程分轮搜索S盒表通常为256字节常量数组Base64变种有64字符表且和标准A-Za-z0-9/不同字符串窗口直接看表自写的异或/位运算循环里反复对一个key异或伪代码里有重复的^和固定key识别出算法之后解题方向就变成“翻译”脚本。比如TEA直接把密文、密钥、delta抄进Python脚本用标准TEA解密流程跑一遍即可。这里最容易翻车的是加密模式理解错很多出题人会魔改轮数或者把字节序倒过来导致你用标准库解出来全是乱码。我的经验是一边解一边看输出是否可打印如果乱码先尝试反转字节序再尝试调整密文分组顺序。3.3 第三板斧动态调试确认关键比较点静态分析有时候会走到死胡同尤其是遇到动态生成密钥、解密后再比较的情况。这时候就需要上调试器。Windows平台我的主力是x64dbgLinux就是gdb配合pwndbg插件。做法是找到比较位置通常以strcmp、memcmp或者逐字节循环体现。在比较位置下断点。运行程序随便输入一串aaaa之类的有规律内容。程序断在比较位置时直接看寄存器或者栈上的目标地址内容目标字符串大概率会被调试器以明文字符串展示出来。这个技巧在很多场景下比静态推演快得多。因为程序都已经把密文或者目标串准备好了你不需要自己再从内存里捞出来做二次解密。尤其适用于那些“解密函数写在前面、比较写在后面”的题断点一停内存里就是解好的明文。3.4 用脚本把“能看懂的题”变成“能跑出flag的题”Z3示例有些题目逻辑不复杂但变换过程中用到了大量位运算和数组索引手动逆很容易出错。我会选择上Z3约束求解器。最常见的场景是from z3 import * flag_len 32 flag [BitVec(ff{i}, 8) for i in range(flag_len)] s Solver() for i in range(flag_len): # 常见约束可打印字符 s.add(flag[i] 0x20) s.add(flag[i] 0x7e) # 把IDA里F5出来的运算语句翻译成z3约束 # 例如data[i] (flag[i] ^ 0x2a) 7 # 那么约束就是 (flag[i] ^ 0x2a) 7 target[i] for i in range(flag_len): s.add((flag[i] ^ 0x2a) 7 target[i]) if s.check() sat: m s.model() print(bytes([m[flag[i]].as_long() for i in range(flag_len)]))很多人觉得Z3很神秘其实就是把“已知输出求满足所有方程的输入”这个数学问题丢给求解器。逆转题的常规操作就是把IDA里的表达式逐行翻译成Z3约束然后用check() model()捞结果。我自己在BUUCTF上至少有五分之一的题是靠Z3跑出来的特别是当伪代码里出现((x 3) | (x 5)) (y ^ 0x55)这种位运算时手算绝对不如约束求解。4. 回看几个BUUCTF题型的实战思路jocker、snake、findme4.1 jocker花指令和假逻辑的挑战我记得自己第一次刷jocker这道题时直接被入口处的花指令劝退。程序在运行之前会对代码段做大量的修改IDA静态F5出来的结果完全不可读全是跳来跳去的无效分支和看起来极不自然的赋值操作。当时我犯的最大的错误是想把每一个指令都读懂再继续结果花了一个多小时还在原地打转。后来我的做法换了方向先不管花指令直接动调。用x64dbg打开程序让它跑起来看它输出了什么输入点什么。jocker这道题让人印象深刻的地方是它有一个“locker”的概念程序最开始会对你输入的字符串做一个假的校验让你以为逻辑在这里而真正的校验藏在一堆被混淆过的代码后面。动态跟的过程中我直接把断点下在比较函数上一路放行程序跑到真正比较输入缓冲区的位置时周围的内存里就出现了处理后的字符串。再把这个字符串和我的输入联系到一起很快就能发现函数真正的逻辑只是某种旋转加异或根本不是刚开始F5看到的那一坨“天书”。这种题给我最大的教训是静态看不懂不代表动调也看不懂。混淆代码常常只骗静态分析运行时真实路径很清晰。看心情刷题的时候遇到这种题千万别赌气跟它死磕先把动态跑起来程序自己会告诉你答案在哪。4.2 snake游戏题要找的不是“高分”是隐藏的flag触发条件snake这道题我最早以为要手动玩贪吃蛇玩到某个分数才能爆flag。后来发现自己想多了。这类游戏题的通用解法很简单程序主循环一直在处理蛇的移动、食物生成、分数更新但flag通常藏在某个全局变量或者分支里。你要做的事情是搜索关卡、分数、蛇身长度相关的变量然后顺藤摸瓜找到那个“一旦满足条件就输出flag”的分支。我当时是先找到游戏的渲染循环然后在更新分数的代码处下断点。通过观察寄存器找出食物坐标、蛇头坐标、分数变量存储的位置。接下来就是修改内存直接把分数改成极大值或者直接把“是否通关”的布尔变量改成1看程序有没有跳转到输出flag的路径。很多游戏题都是这种设计你不需要真正通关只需要让通关条件为真。这里我要多说一句游戏题的逆向思路比具体某道题更重要。因为恶意软件分析里经常遇到类似场景比如一个程序向服务器请求授权如果服务器返回特定值才解密核心逻辑。逆向者需要绕过验证函数而不是真的把服务器部署一遍。多刷几道游戏题对这种“绕过验证点”的敏感度会提高很多。4.3 findme在函数海洋里精准定位主逻辑提到findme这种题我心里浮现的画面就是IDA的Functions窗口一拉到底的滚动条。它不像snake那样整体是游戏循环而是把所有代码塞在很多小函数里每个函数看起来都像在算东西但真正涉及flag可能只有其中几个。遇到这种结构最忌讳的就是从头到尾逐个F5。我的方法是先搜字符串。程序大概率会在某个地方输出“flag”或者读取文件通过对这些字符串的交叉引用一层层往回跳往往能定位到最外层主逻辑。如果字符串不可见就搜“错误提示”“输入”这类交互提示词。再不行就看可能参与比较的表数据比如有一块256字节的S盒、一块64字节的Base64表顺着数据的引用也能找到函数。如果交叉引用太多太乱我会退一步用Ghidra。Ghidra的“函数调用图”视图很直观能看出哪些函数是核心节点。findme这类题通常只有一两个核心中枢函数调用图上大量函数都指向它你用拓扑视角扫一眼就能找出可疑对象。定位目标函数比理解所有函数重要得多这是所有“函数海洋”型题目共同的核心原则。4.4 顺带聊隔壁分类zips这类和RE互通的杂项题BUUCTF上有个让我印象深刻的题是[GUET-CTF2019]zips严格来说它被归到杂项MISC分类但做题过程充满了RE元素。很多人看到zip附件就开始爆破密码爆破一轮发现碰了壁最后发现其实要通过分析压缩包内部某个Python脚本或者数据块逆向出真正的密码或者拼接方式。我的看法是RE能力实际上是一种跨分类的底层能力。MISC里的隐写、流量、压缩包伪加密经常会在某个环节里藏着一个需要你逆向的算法PWN题里的shellcode分析也需要你读懂机器码。所以在看心情刷RE题的过程中偶尔点开一道MISC题不会亏反而能帮你把“分析型思维”磨得更圆。RE的核心不是会用某个工具而是能在陌生二进制里快速建立假设并验证这个能力放到MISC、PWN甚至漏洞分析里全部通用。5. 看心情刷题最容易踩的坑我基本都踩了一遍5.1 UPX脱壳和手动修复的坑先说结论PE加UPX壳九成情况下会想到用upx -d一键脱壳但成功率没有想象中那么高。很多BUUCTF题故意使用修改过的UPX或者主程序头部被改过导致官方脱壳工具直接报错“Invalid UPX header”。这时候就要手动脱。手动脱的第一步是找原始入口点最常用的方法是ESP定律或者在x64dbg里对栈顶下硬件断点利用程序解压完代码段后执行push指令跳回OEPOriginal Entry Point的瞬间拦截到真正的入口。到了OEP之后把进程完整dump下来再用Scylla或者ImportREC修复IAT。修复IAT的时候要特别注意程序可能有多个IAT区块Scylla如果只解析出一张表要检查导入函数是否完整缺了的话运行起来就直接蹦“无法定位程序输入点”之类的错误。我自己脱壳失败的原因十有八九是dump时机太早代码段还没解密完。判断时机是否正确的技巧是在跳到OEP时检查一下.text段头所在地址附近的字节是否已经是真实代码特征比如连续的push ebp; mov ebp, esp或者sub rsp, xxx如果看起来像随机字节说明还没解完不要急着dump。5.2 反调试和OLLVM混淆别跟花指令较劲BUUCTF的RE题里有一批题目喜欢上OLLVM尤其是控制流平坦化flatten功能。经过平坦化后伪代码会变成一堆switch-case的分发结构看半天也看不出原来的条件分支在哪。我记得自己第一次遇到时心想“这程序是疯了吧”差点直接放弃。后来我学到几个实用技巧不要试图在伪代码里重建原始逻辑而是找数据流。看哪些变量被赋值后一直存活到最后参与比较顺藤摸瓜就够。如果遇到底层只有一堆mov eax, xxx; jmp switch的模板直接跳过中间过程只关心关键函数的输入输出。使用Angr或者Triton这类符号执行工具直接跑很多平坦化题目在这种工具面前等于没混淆。Angr最常用的接口是import angr p angr.Project(./题目) state p.factory.entry_state() simgr p.factory.simulation_manager(state) # 寻找包含目标地址的分支 simgr.explore(find0x目标地址, avoid0x错误地址) if simgr.found: print(simgr.found[0].posix.dumps(0))不过要提醒一句Angr处理简单混淆题可以遇到能膨胀几万条指令的复杂样本时符号执行也很容易被路径爆炸拖死。所以正确顺序是先试着动态看懂再看能不能用z3模拟最后才上Angr。工具是用来辅助理解的不是用来逃避理解的这句话我在刷OLLVM题时反复对自己说。5.3 环境坑glibc、dll缺失、架构差异有一类坑和题目逻辑无关但特别消耗做题心情。比如下载的ELF题在本地跑不起来报GLIBC_2.34 not found或者Windows程序在自己的Win11纯净环境里缺几个VC运行库还有的题是32位程序需要你有32位运行库支持。我现在的常规处理是配一个专门用来做题的Linux虚拟机系统保持在Debian或者Ubuntu长期支持版并在里面装好gcc-multilib、g-multilib确保32位ELF能跑。Windows的题目则统一用Windows 10虚拟机里的x64dbg和IDA避免在真实工作机里装一堆来路不明的运行库。如果deploy到一个临时容器里跑题比如GLIBC版本不匹配最省事的选择是直接用题目自带的Dockerfile启动环境很多高版本CTF题都会附带容器配置。另外提一句安卓RE题也会踩环境坑。APK解包之后如果要动态调试得用合适的模拟器或真机Android SDK版本和架构经常不一致。这些坑在“看心情刷题”的语境下尤其烦人因为你本来只是想随手做一道题放松一下结果花半天配环境。所以我建议把环境调试当成一次性的基础设施投入把自己常用的镜像、依赖、插件先装好之后所有题都用同一套环境问题会少很多。6. 给同样“看心情”的RE新手一份精简工具与心态备忘6.1 我最终留下的工具链反复折腾之后我现在的RE工具链精简到以下几样IDA Pro主力静态分析F5伪代码对做题效率提升极大。Ghidra免费替代当IDA许可证不在手边时主力使用函数调用图分析很舒服。x64dbg / gdbpwndbgWindows和Linux动态调试器。x64dbg的界面比OllyDbg现代插件生态也够用。DIE / Exeinfo PE查壳DIE的识别精度高界面直观。UPX一键脱壳工具遇到简单壳直接秒解。Python z3-solver angr pwntools脚本化求解三件套。Z3解约束Angr跑符号执行pwntools处理ELF/远程交互。010 Editor看二进制文件结构、hex流修文件头时非常有用。Frida移动端动态插桩偶尔刷安卓题时用。这套工具链覆盖了PE、ELF、APK三类最常见题型的全部环节。不要再额外装一堆“看起来很厉害”的工具工具越少越好你才能越用越熟。尤其是新手我见过有人电脑里装了十几种逆向工具结果每种都用得半生不熟遇到题目反而不知道该选哪个。6.2 心态和刷题顺序建议最后想聊点跟技术无关但特别重要的东西。RE刷题和做数学题很像状态的起伏非常明显。看心情写这个标题之所以能成立是因为逆向工程真的需要“心流状态”你掉进一个混淆函数的兔子洞里如果心态崩了手会开始乱点断点会乱下看伪代码会越看越烦躁。所以我给自己定了几个规矩第一卡住超过四十分钟就暂停。去喝水、散步、甚至换一道题让大脑后台继续处理。很多时候你回来两分钟就发现了刚才漏掉的一个小细节。第二每道题做完都写一行复盘不管是解题思路还是踩的坑。这个习惯看起来很简单但真到了三个月后你回头看自己写过的东西会发现过去的“坑”已经变成肌肉记忆。第三不要妄想每道题都能独立做出来。有些题目确实需要看WriteUp才能补上知识盲区。看WriteUp不是丢人的事关键是看完之后要能自己手动复现一遍而不是脑中“懂了”就翻页。6.3 把“看心情”变成可持续的刷题节奏我理想的刷题节奏是一周抽三四个晚上每次一道题状态好就加一道。选题目时参考上面的分类哪类弱就补哪类。遇到明显超出当前水平的难题先记下来放到一个月后再回来试。这样做的好处是知识盲区会在时间间隔中被其他题目自然填充等回头再看那道难题时你可能会突然发现自己的工具栈已经能覆盖它了。BUUCTF的RE题单其实很耐刷。有些题我隔了大半年重新拿起来仍能榨出以前没注意到的新知识点。这也是为什么我坚持维护那个“看心情写”题单的原因——逆向上限不取决于你把自己逼得多紧而取决于你能否在一个个“看着头疼”的函数里养成“先分析、再怀疑、后验证”的固定动作。动作对了心情自然会顺。希望这篇杂乱但实用的笔记能成为你也在BUUCTF上打开第一道RE题时的一点底气。
返回列表