ARTICLE DETAIL

资讯详情

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

CTF逆向入门:从IDA静态分析到base64算法识别与Python脚本还原

CTF逆向入门:从IDA静态分析到base64算法识别与Python脚本还原 做 CTF 逆向最怕的不是题目难而是拿到一个陌生文件之后不知道从哪下手。BUUCTF 上的 reverse3 算是很多人的第一道“有点意思”的逆向题它不像单纯的签到题那样直接给 flag也没有壳、没有混淆、没有反调试就是一道规规矩矩的 Windows RE 题考察你对 IDA 的熟练程度、对 base64 一类常见编码算法的敏感度以及最基本的 Python 还原能力。这篇文章就以 reverse3 为主线把从查壳、静态分析、算法识别到脚本还原的完整流程拆开讲一遍适合刚接触 CTF 逆向、准备系统刷 BUUCTF 的新手。我在实际刷题过程中发现很多新手卡在这道题上的原因并不是不会写脚本而是读不懂 F5 出来的伪代码里那一层一层的函数调用和赋值关系。所以这篇文章会把“为什么要这么做”讲清楚而不是只丢一个能出 flag 的脚本了事。1. 入手 reverse3先搞清楚题目在考什么1.1 一道典型的算法识别型逆向题reverse3 这道题从难度上看属于逆向入门偏进阶的层次。它的程序本身是一个 Windows 下的 PE 文件没有加壳也没有太多反分析手段主要难点集中在“如何从 IDA 反编译结果中看出程序的自定义加密流程”。这个词叫“算法识别”是 CTF 逆向里绕不开的基本功。你拿到一个程序发现它对输入做了一连串操作最后拿去跟一个固定字符串比较。你需要的是把这串操作逐条还原成可逆的公式然后写脚本把目标字符串倒推回输入。听上去很简单但实际操作里经常会遇到两个坑一是认不出某个函数其实是 base64二是看不出程序对 base64 的密码表做了手脚。reverse3 刚好两个坑都踩一点。1.2 这题适合什么基础的人来练如果你已经会打开 IDA按 F5 看伪代码知道什么是栈变量、什么是字符串比较那 reverse3 对你来说最值得练的就是“如何从一堆变量名和函数参数里认出算法”。如果你连 IDA 都还没用过我也建议从这道题开始因为它的代码量不大main 函数里逻辑非常清晰是很好的“第一道独立逆向题”。我在指导新人刷 BUUCTF 时通常会把 reverse3 放在 reverse1、reverse2 之后因为前面两道题的 flag 获取方式更偏“阅读理解”而 reverse3 真正引入了“写脚本还原算法”这个环节。刷完这一道你对 IDA 的 F5、字符串窗口、函数调用参数这三个操作会有质的理解。2. 查壳与运行观察别一上来就拖进 IDA2.1 用好 Exeinfo PE 或 DIE十秒钟判断文件类型很多新手拿到题目直接拖进 IDA有时候反而把自己搞晕。我个人的习惯是先丢进 Exeinfo PE 或者 Detect It Easy 看一眼信息这玩意儿比 IDA 启动快得多能直接告诉我文件是 32 位还是 64 位、有没有壳、编译器信息是什么。reverse3 就是这个流程下的标准样例显示结果是普通的 VC 32 位程序无壳。这个信息有什么用首先32 位程序在 IDA 里的调用约定通常是 __cdecl 或 __stdcallF5 以后看到的函数参数顺序比较直观。其次无壳意味着省去了脱壳这一步入口点就是正常的程序逻辑开始不会跳到奇怪的 OEP 上。注意查壳不是多此一举。如果你在 PE 信息里看到 UPX、ASPack 之类的壳第一件事应该是脱壳或者动态转储而不是直接静态分析。reverse3 没壳省了这道工序但流程不能少。2.2 运行程序观察输入和回显把题目文件放到 Windows 虚拟机或者本地 Windows 环境里双击运行窗口会显示类似input flag:的提示然后等待输入。随便输一串字符回车后程序没有任何回显直接结束。这一步看起来很朴素但能帮你建立“程序行为”的直觉它要求输入一个字符串内部判断正确与否但没有输出“wrong answer”之类的提示。这意味着最终比较成功的分支可能只是一句printf(right flag!\n)或者压根没有提示。后面在 IDA 里你就能看到这题的输出分支非常简陋但这不影响判断。如果你不想在 Windows 上跑用 Wine 也能执行大部分这种简单 PE。不过我个人建议准备一个 Win7/Win10 的虚拟机或者独立环境因为后续动态调试还是要用到。3. IDA 静态分析把 main 的加密逻辑一条条捋清楚3.1 F5 还原出来的核心伪代码长什么样用 IDA 打开 reverse3找到 main 函数直接按 F5。第一次看到伪代码的人可能会觉得变量很多、很乱但不要慌逆向阅读伪代码是有套路的先找输入函数再找比较函数中间夹着的就是你真正要分析的加密逻辑。在 reverse3 的 main 伪代码里能看到scanf(%20s, Str)之类的输入语句这基本就是读取 flag 的入口。往下看就会遇到一个for循环对输入的每一位做了一次加法操作紧接着是一个名为sub_411AB0的函数调用然后又是另一个for循环再做一次逐位加法最后是strcmp和成功分支。把这条线抽出来加密链其实非常直白输入字符串先被逐位处理然后经过一个函数变换再被逐位处理最后跟某个目标字符串比较。目标字符串通常就躺在伪代码里比如e3nifIH9b_CndH这样一个字符串常量。注意这个字符串看起来根本不像是正常 flag所以它一定是被处理过的最终密文而不是明文。3.2 三个核心环节输入处理、核心变换、二次处理伪代码里第一个for循环非常关键它长这样for ( i 0; i v4; i ) Str[i] i;翻译成人话就是输入字符串的第 i 个字符ASCII 码加上它的下标 i。比如第 0 位加 0第 1 位加 1第 2 位加 2依此类推。这是一个很典型的“逐位偏移”操作看起来简单但很多人会在写脚本还原时搞反方向。接着程序调用了sub_411AB0这个函数把处理后的输入给变换成了另一串字符。从名字上看它只是个内部函数但点进去看就会发现里面有大量的查表操作和位运算这基本就是 base64 编码的核心逻辑。base64 编码本身就是把 3 个字节变成 4 个可见字符所以输入长度经过这个函数之后会发生变化。你在还原时需要牢牢记住这一点遇到长度改变的地方大概率就是编解码类算法。最后一个for循环跟第一个类似也是对变换结果逐位加上下标。整个加密流程就是对输入逐位加下标对结果做 base64 编码对 base64 结果再次逐位加下标与固定目标串比较。如果还原脚本时忘记处理其中任何一层最后解出来的都会是乱码。3.3 不要急着看 sub_411AB0 的每一行汇编很多新手一看到sub_411AB0内部几百行代码就头皮发麻其实完全没必要逐行分析。CTF 逆向里对于这类编码函数更高效的做法是“看特征”函数里有没有一张 64 个字符的表有没有一堆移位和位与操作有没有 4、 6这类的位移。只要这几个特征同时出现基本可以锁定为 base64 家族。当然你还可以用更暴力也更实用的办法拿一小段已知字符串在动态调试里观察这个函数输入输出或者直接写个 Python 脚本用标准 base64 对同样输入编码对比结果。如果一致说明是标准 base64如果不一致那就要考虑是不是换了密码表。reverse3 的坑就藏在这里。4. 核心算法识别base64 与自定义密码表4.1 密码表藏在哪IDA 里怎么定位它base64 算法的核心是一张 64 字符的索引表标准表是ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/。如果程序里用的是标准表你直接调 Python 的base64库就能解。如果程序为了增加难度把这张表里的字符重新排列过那你还按标准表解就一定会得到乱码。在 IDA 中定位密码表最快的方法是进入sub_411AB0函数在伪代码或者反汇编里查找类似off_、aAbcdefghijklmn这样的全局地址引用。那个地址指向的字符串就是编码时使用的表。把它复制出来跟标准表做对比就能看出不同。reverse3 这道题对我来说比较有意思的地方是很多人一开始用标准 base64 去解目标串解出来是一段可打印但明显不是 flag 的字符串然后就开始怀疑人生。其实这正是因为程序里的 base64 表不是标准表。你要做的不是强行用标准表解而是回到程序里把那张真实使用的表挖出来。4.2 自定义表怎么影响还原结果如果程序把标准表换成了自定义表那么编码过程是输入字节按 6 位一组切分得到的 0 到 63 的索引不再对应标准表的字符而是对应自定义表里相应位置的字符。反过来解码时你就必须先把你拿到的密文字符在自定义表里的位置找出来换成标准表里同样位置的字符然后才能用base64.b64decode()解。听起来有点绕但写代码时常见做法就两种。第一种是直接把自定义表写死然后以它为基准把密文中的每个字符翻译成索引按索引去标准表取替换字符最后交给 Python base64 解码。第二种更简单直接用str.maketrans(CUSTOM_TABLE, STD_TABLE)做一次字符串映射把所有密文字符一次性替换回标准字符然后解码。这里面最容易出的问题是你把表从 IDA 里复制出来时字符串结尾的\0也带进去了或者漏了最后一个字符。所以复制表之后一定要检查长度是不是 64。不是 64 的话说明你复制的位置不对或者表里还藏着别的字符。4.3 别忘了那个看似不起眼的“逐位加法”base64 表搞定了很多人觉得自己已经拿到 flag结果脚本跑出来还是怪怪的。这时候要回头检查加密链的另外一层逐位加下标。在 main 的加密链里逐位加法一共出现两次。第二次对 base64 结果做的逐位加法会把原本正常的 base64 字符全部“顶”到别的 ASCII 字符上去。还原的时候第一次操作应该是把目标串每一位 ASCII 码减去对应下标得到一个看起来像 base64 的字符串第二次操作才是把自密码表的问题处理掉。有朋友在写脚本时把加减方向搞反导致越解越乱。这里教大家一个自检技巧你先手动猜一个输入比如123456按伪代码的逻辑在纸上或者 Python 里跑一遍加密看结果是不是和目标串的格式一致。如果一致说明你理解的加密链没问题如果不一致那八成是方向搞反了。5. 编写 Python 脚本还原 flag5.1 搭一个能跑的脚本骨架看完了算法逻辑接下来就是写脚本。我的建议是先写一个最朴素的骨架把加密链一步步倒过来不要一开始就追求什么“一行式解出 flag”。下面这个脚本骨架里我用注释把每一步对应的逆向操作标清楚import base64 # 程序里的目标串 target e3nifIH9b_CndH # 第一步去掉第二次逐位加下标的影响 # 因为加密时是“每一位加下标”所以解密时每一位减下标 base64_str for i in range(len(target)): base64_str chr(ord(target[i]) - i) print(处理下标后的字符串:, base64_str) # 第二步处理自定义base64密码表 # 这里需要替换成你从IDA里复制的真实密码表 CUSTOM_TABLE ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/ STD_TABLE ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/ trans str.maketrans(CUSTOM_TABLE, STD_TABLE) normal_b64 base64_str.translate(trans) # 有些base64变体可能缺少补齐字符这里补一下 while len(normal_b64) % 4 ! 0: normal_b64 # 第三步标准base64解码 flag_part base64.b64decode(normal_b64) print(最终还原:, flag_part)5.2 关键点如何把“IDA 里的表”安全地搬进脚本很多新手看了上面脚本会问CUSTOM_TABLE到底该填什么如果你在 IDA 里看到的还是一个跟标准表长得差不多的字符串那就直接用标准表如果你看到一个明显被改过的 64 字符表就把它替换进去。要注意的是不要手打这张表。复制粘贴是基本功但更稳妥的做法是在 IDA 的十六进制窗口里选中那一串数据用Copy data功能复制成一个字符串避免手滑打错字符。base64 表里经常有那种肉眼难以区分的字符比如0和Ol和1一旦复制错一位解出来的就是乱码。如果你在 IDA 里找不到自定义表还有一种方法动态调试。在调用sub_411AB0之前下一个断点运行到断点后查看传给函数的参数特别是那个被当作“第三/第四个参数”的指针指向的内容就是密码表。这个方法对其他类似题目同样适用。5.3 脚本跑完以后怎么验证对不对脚本输出不一定直接就是flag{...}因为这道题程序本身只要求输入满足加密链而 BUUCTF 平台的 flag 提交要求是完整的flag{}包裹格式。如果你解出来的字符串本身就是flag{...}直接提交即可。如果解出来是一串看起来像英文单词的字符没有花括号就要再检查一下是漏了哪一步。我的习惯是拿到还原结果之后再写一个正向加密的脚本把我解出来的字符串重新按加密链处理一遍看最终结果是不是和目标串逐字节相等。这一步叫“逆向验证”能一次性排查掉绝大多数操作顺序和加减方向的问题。很多人解不出题是因为中间漏了一个操作但如果做了验证马上就能发现问题在哪。6. 动态调试与常见问题实录6.1 解密结果乱码的排查顺序我见过太多人在脚本这一步卡住解出来是b\x8b\xf1...这种乱码然后完全不知道问题出在哪。这里给大家一个排查顺序按这个顺序来基本都能找到问题。第一先确认你减下标的字符和目标串是不是同一个。程序中比较的目标串有时候不止一处可能有一个副本在初始化时被修改过你用的却是原始常量。第二确认你减下标的范围是否和加密时一致。如果加密循环是从 0 到 strlen 减 1那你解密也必须对同样长度的每一位操作。第三确认 base64 这个步骤到底是编码还是解码。有人把加密链里的编码函数当成了解密函数最后当然不对。还有一个小细节如果 base64 解码出来的字符串里带着不可见字符大概率不是最后答案而是说明你跳过了一个“中间层”。比如 reverse3 这样的流程解一次 base64 得到的可能不是最终输入而是经过了下标偏移的中间结果还需要再减一次下标。6.2 IDA 动态调试的两个实用技巧静态分析解决 80% 的问题但有的时候你需要确认某个函数的真实行为这时候动态调试就是最后的手段。reverse3 没有反调试所以完全可以直接在 IDA 里按 F2 下断点按 F9 运行然后单步跟踪。第一个技巧在strcmp处下断点。运行到断点时寄存器或栈里会同时出现两个字符串参数一个是你输入经过加密后的结果另一个是程序期望的目标。这时候你就能直观地看到自己哪一步做对了哪一步做错了。第二个技巧在sub_411AB0调用前下断点查看它传入的参数。如果能看到某个参数是一个很长的字符串而且这个字符串由 64 个可见字符组成那基本就是密码表。把鼠标移到参数地址上IDA 会直接显示字符串内容非常方便。6.3 从 reverse3 开始养成的三个好习惯刷完 reverse3我觉得最大的收获不是知道了某个具体 flag而是养成了几个可以复用到后面几十道题的逆向习惯。第一个习惯是“先画加密链再动手写脚本”。不管题目多简单拿到伪代码我都会先把“输入 - 变换 - 比较”的链路画出来哪怕只是在草稿纸上写几个箭头。这样能避免反复看代码时忘记变量之间的关系。第二个习惯是“永远怀疑密码表”。CTF 逆向里 base64 的变种非常多有些题只改表里的几个字符有些题直接把整张表倒过来。如果你发现解出来前几个字节是乱码不要急着看后面的字符先回到密码表上找问题。第三个习惯是“正向加密脚本与逆向还原脚本配套开发”。写还原脚本时顺手写一个正向加密的验证脚本两边的逻辑互为镜像。一旦两边能对上答案基本就稳了。reverse3 整体来说是一道值得反复咀嚼的题目。它把 IDA 静态分析、算法特征识别、Python 脚本还原这三个逆向核心能力串在一起难度曲线非常友好。做完之后再去看 reverse4、reverse5你会发现很多题目只是在这个套路基础上加了 DES/AES 或者 RC4 这类更复杂的算法而分析流程都是相通的。我个人在实际操作中最深的一点体会是这类题拼的不是智商而是细心。你在 IDA 里漏看一个for循环在脚本里写反一个加号整个结果就会面目全非。所以每次卡住的时候我都会强迫自己从头把伪代码重新读一遍而不是反复修改脚本参数去试。耐心读完那段十几行的逻辑答案往往比想象中更简单。
返回列表