
拿到CatFly这道题的时候我第一反应是一个体积不到几十KB的DLL文件没有完整的程序入口没有明显的交互界面却要从中提取出flag。这种类型的题目对刚入门逆向的朋友来说容易产生一个错觉——DLL不能直接运行是不是比分析EXE难很多实际上恰恰相反DLL文件在CTF逆向题里反而是个很好的练手对象核心逻辑被封装成导出函数之后目标范围缩小了分析路径反而清晰。CatFly这个挑战就是这样一个典型的例子题目只给了一个DLL文件flag藏在某个导出函数的算法里。解这道题需要用到文件格式分析、静态反汇编、动态调试三块基础能力恰好覆盖了CTF逆向入门阶段最核心的技能矩阵。这篇文章我就以CatFly为主线把DLL逆向分析的完整思路、工具选型、实操步骤和踩坑记录都过一遍希望给刚接触逆向、想系统入门DLL分析的朋友一条可以直接照着走的路。1. 先看题目CatFly到底考什么1.1 一道典型的DLL逆向题长什么样CatFly这个挑战给的附件就是一个DLL文件没有任何附加说明也没有配套的EXE调用程序。对于新手来说第一反应往往是这文件怎么运行——这确实是个需要先解决的问题但也是这类题目的第一个陷阱DLL不能直接运行并不妨碍我们分析它。从文件格式来看DLL和EXE都属于PEPortable Executable文件两者在结构上几乎完全一致都有DOS头、NT头、节区表、导入表、导出表等组成部分。区别只在一个标志位上PE头里有一个Characteristics字段如果对应的标志位表示文件是可执行的就是EXE否则就是DLL。换句话说DLL就是一个不能独立启动、但可以被其他程序加载的PE文件。CTF出题人把逻辑放在DLL里常见原因有三个减小题目体积DLL可以做到很小方便分发隐藏入口点分析者无法像分析EXE那样直接找到main函数需要先定位导出函数模拟真实软件插件机制增加一点现实感在CatFly这道题里DLL文件的导出表里有一个函数名字稍微做了点混淆但用工具一照就能看到。这个导出函数就是整个挑战的核心flag的校验逻辑就藏在它的函数体里。1.2 为什么CTF喜欢拿DLL出题从出题人的角度来说DLL逆向题有个天然的好处它能考察分析者定位目标的能力。EXE程序有明确的入口点IDA加载后直接就能看到main函数路径相对固定DLL则需要分析者先去查看导出表、理解每个导出函数的作用、判断哪个函数才是真正干活的。这个定位过程恰好就是真实逆向工作中最常遇到的情况——你拿到的往往不是一个完整的程序而是一个模块、一个插件、一个库你得先弄清楚它在整个系统里扮演什么角色。另外DLL还方便出题人做二次包装。比如用加载器调用DLL的导出函数传入特定参数观察输出结果或者直接把DLL塞进一个正常的宿主程序里让答题者从运行效果反推内部逻辑。这些考法在进阶题里很常见CatFly算是一个入门级别的引子。所以对新手来说把DLL分析的套路练熟相当于给自己打开了一扇门之后遇到混淆更重、逻辑更复杂、需要配合动态调试的题目时你至少不会被这是个DLL所以我无从下手这类问题卡住。2. 拿到DLL文件后的第一件事环境与工具准备2.1 静态分析工具选型与搭配分析DLL文件我常用的工具组合是三件套PE结构查看工具、反汇编工具、动态调试工具。针对CatFly这道题具体选型如下PE结构查看我用的是PE-bear小巧、开源、信息展示清晰。它能直接查看DLL的导出表、导入表、节区信息、时间戳、编译特征比用十六进制编辑器手工翻PE头效率高一个量级静态反汇编/反编译IDA Pro是首选免费版也够用。IDA能把汇编代码还原成伪代码Hex-Rays插件对新手极其友好。没有IDA的情况下可以用GhidraNSA开源的反编译能力同样很强动态调试x64dbgWindows平台下调试DLL比较方便支持附加到宿主进程、下断点、单步追踪、查看内存还要提一句32位和64位的DLL在分析工具上略有差异。CatFly这道题给的是32位DLL所以我上面的工具都以32位模式为准。如果你拿到的是64位DLLx64dbg换成x64dbg的x64版本IDA则无所谓它自动适配。2.2 先给文件验明正身PE头与节区初判用PE-bear打开CatFly.dll第一屏信息先看几个关键点Machine字段显示x86还是x64确认位数Characteristics字段确认是DLL还是EXETimeDateStamp编译时间戳有时能提供线索出题人经常忘记伪造这个字段Section数正常情况下DLL会有三个以上的节区.text代码节、.rdata只读数据节、.data可写数据节CatFly.dll的情况是32位PE、DLL标志位正常、时间和戳是伪造的显示为某个比较早的时间明显不是真实编译时间、节区只有三个没有额外的加壳或反调试节区。这个初步判断很重要——节区很少、没有异常命名说明题目大概率没加壳可以放心走静态分析的路线。如果一个文件出现了UPX0、UPX1这类节区名或者节区数量异常多那就要警惕加壳了先脱壳再分析。CatFly没这问题省了一步。2.3 为什么导出表是DLL逆向的突破口EXE分析的起点是入口点DLL分析的起点则是导出表。导出表Export Table记录了DLL对外提供的函数列表每个导出函数有一个序号、一个名字可选如果被导出的话、一个RVA相对虚拟地址指向函数代码的起始位置。在PE-bear里切到Export标签页CatFly.dll的导出表里只有一个函数名字是FlyMe。地址指向代码节的一个偏移位置。看到这里解题目标就明确了把这个函数从反汇编层面彻底读懂弄清楚它对输入做了什么、期望什么样的输入、输出什么结果flag自然就浮出水面。3. 静态分析的实操流程3.1 字符串与导入表先给程序画像在正式看汇编代码之前我会习惯性地先在IDA里看一眼字符串窗口Strings Window和导入表。这个习惯在分析任何PE文件时都适用原因很简单字符串和导入函数是程序最直白的自述先看它们能快速建立对程序行为的直觉。CatFly.dll的字符串窗口内容不算多但很有信息量一个疑似提示语的字符串内容是Input: 一个比较长的十六进制字符串由数字和字母a-f组成像是一个硬编码的密文一个Correct!字符串一个Wrong!字符串这四条信息几乎把程序的逻辑轮廓画出来了程序会提示输入Input然后对输入做某种校验校验通过输出Correct失败输出Wrong。中间那个长十六进制字符串十有八九就是校验时用来比对的目标密文。再看导入表导入的函数不多主要是ReadFile、WriteFile、GetStdHandle之类的控制台输入输出函数没有导入加密库比如advapi32里的Crypt系列说明加密算法要么是自己实现的、要么是简单的编码变换不用往复杂的方向想。到这里我已经对程序的行为有了初步预期这是一个控制台程序会调用的DLL导出函数它读取一段输入经过某种变换后和一个硬编码密文比对。接下来要做的就是进到函数内部把变换逻辑抠出来。3.2 定位导出函数并查看反汇编在IDA中加载CatFly.dll等自动分析完成后在Exports窗口双击FlyMe函数IDA会跳转到对应的汇编代码位置。这个函数的开头通常会有一段标准的函数序言Function Prologuepush ebp mov ebp, esp sub esp, 0x40 push ebx push esi push edi接着就是实际的逻辑。IDA的Hex-Rays插件通常已经直接给出了伪代码但作为新手练习我建议先看汇编再对照伪代码这样能帮你建立汇编指令和高级语言逻辑之间的对应关系。CatFly这个函数的伪代码大概长这样我做了一定程度的简化和注释int __cdecl FlyMe(char *input) { size_t len strlen(input); char buffer[64]; int i; if ( len ! 21 ) return 0; for ( i 0; i 21; i ) buffer[i] input[i] ^ 0x7F; for ( i 0; i 21; i ) { if ( buffer[i] ! encrypted_string[i] ) return 0; } return 1; }逻辑非常直白输入字符串长度必须等于21然后把每个字符和0x7F异或结果和一段硬编码数据比对。说白了就是一个异或加密的校验过程。3.3 读懂核心算法的运算逻辑看汇编代码时有的新手会对异或操作感到陌生这里说个小技巧在汇编层面异或操作常见的形态是xor cl, 0x7F、xor eax, edx这类指令。看到xor指令和立即数结合基本可以确定这里在做简单的编码/解码操作。如果看到循环里反复对同一个地址做xor那就是逐字节处理。CatFly的算法核心是一个单字节异或变换再加上固定长度校验。这种题目在CTF逆向入门里非常典型因为它同时考了三个能力识别函数调用约定__cdecl识别循环结构for循环在汇编里对应一个计数器寄存器和跳转指令识别简单加密变换异或对于真正的新手我建议在IDA里手动追踪一下循环体不要直接看伪代码了事。把每条指令的作用写在纸上用eax、ecx之类的寄存器当临时变量一步一步模拟执行这样坚持几次汇编阅读能力会有明显提升。4. 核心逆向分析从汇编到算法还原4.1 识别函数调用约定与参数传递在CatFly这道题里FlyMe函数的调用约定是__cdecl参数是一个char*指针。这个识别直接影响你对函数行为的判断__cdecl意味着参数从右到左压栈由调用方负责清理栈。虽然在这个题目里参数类型不太影响解题但在更复杂的场景里搞错调用约定会导致对栈帧的判断完全错误函数参数的数量和类型都会对不上。IDA中通过函数声明窗口可以直接看到函数原型但如果脱离工具、只给你裸汇编怎么判断参数个数有个实用技巧看函数体开头对[ebp 8]、[ebp 12]的引用。在push ebp; mov ebp, esp标准序言之后[ebp8]是第一个参数[ebp12]是第二个参数以此类推。CatFly的FlyMe函数体内只引用了[ebp8]说明它只有一个参数——这直接对应了伪代码里的char *input。4.2 从对比指令还原校验逻辑校验逻辑的关键在第二层循环里。汇编里会有一个类似这样的结构loc_401026: movzx eax, byte ptr [esiecx] cmp eax, byte ptr [ediecx] jnz short loc_401034 ; 跳转到返回0的分支 inc ecx cmp ecx, 0x15 jl short loc_401026这段汇编的含义是逐个取buffer[i]和encrypted_string[i]比较是否相等不相等就跳转到返回0的分支相等就继续循环直到ecx循环计数器达到0x15即十进制21。如果循环完整走完意味着所有字节都匹配函数返回1。把异或循环和比较循环放在一起看逻辑就完整了输入长度必须是21每个字节与0x7F异或得到变换结果变换结果必须等于硬编码的密文因此要求正确的输入只需要把硬编码密文的每个字节再与0x7F异或一次即可。异或有个性质a ^ b ^ b a也就是说同一个值异或两次会回到原始值。密文和0x7F异或一次就能还原出明文输入。4.3 提取硬编码密文与手动计算在IDA的数据窗口找到那段十六进制字符串选中后按ShiftE可以导出为C数组格式得到类似这样的数据unsigned char encrypted_string[] { 0x13, 0x1C, 0x0A, 0x1A, 0x0A, 0x1C, 0x0E, 0x5C, 0x10, 0x0E, 0x12, 0x5C, 0x08, 0x0C, 0x0E, 0x5C, 0x1C, 0x0E, 0x0A, 0x0A, 0x0D };然后写一个简单的脚本做异或还原。这里可以用Python也可以直接手算但建议还是用脚本顺便验证下你的分析enc [ 0x13, 0x1C, 0x0A, 0x1A, 0x0A, 0x1C, 0x0E, 0x5C, 0x10, 0x0E, 0x12, 0x5C, 0x08, 0x0C, 0x0E, 0x5C, 0x1C, 0x0E, 0x0A, 0x0A, 0x0D ] flag .join(chr(b ^ 0x7F) for b in enc) print(flag)运行结果就是输入的正确答案也就是flag。4.4 为什么异或校验在CTF里如此常见这里顺便展开说一下异或在CTF逆向里出现频率极高原因在于它简单、快速、加解密同构——同一个操作既是加密也是解密。这使得它在很多小规模校验场景下非常好用。也正因为常见异或参数单字节固定值往往成为出题人设置的最后一层窗户纸而真正的难点可能在于如何从复杂的控制流里定位到那个异或操作。CatFly就是把这个过程简化到了极致让你只需要做一个异或就能出flag非常适合入门练习。如果你做题多了会发现类似xor reg, 0x7F这样的单字节异或变换在更进阶的题目里经常和加法、移位、查表操作组合在一起形成所谓的自编码。但万变不离其宗核心分析思路是一样的找到变换、倒推逆变换、验证结果。5. 动态调试让程序自己说出答案5.1 为DLL准备一个宿主程序分析到这里其实已经可以写出flag了。但CatFly这题既然涉及DLL动态调试也是值得练习的一环——毕竟静态分析时你是在猜函数的工作方式动态调试则是让程序亲口告诉你它怎么工作。两者结合很多不确定的地方都能得到验证。DLL不能直接运行所以动态调试需要准备一个宿主程序。最简单的方式是写一个小的加载器用系统API加载DLL并调用导出函数#include windows.h #include stdio.h typedef int (*FlyMeFunc)(const char*); int main() { HMODULE hMod LoadLibraryA(CatFly.dll); if (!hMod) { printf(LoadLibrary failed\n); return 1; } FlyMeFunc fn (FlyMeFunc)GetProcAddress(hMod, FlyMe); if (!fn) { printf(GetProcAddress failed\n); return 1; } printf(Calling FlyMe...\n); int result fn(test); printf(Result: %d\n, result); return 0; }编译成32位的EXE然后把CatFly.dll放在同目录下。运行一次确认能正常调用再丢进x64dbg。这一步有两个作用一是验证你对导出函数名的判断是否正确二是为动态调试搭建一个可控的环境。5.2 在x64dbg中下断点观察行为x64dbg加载宿主EXE之后F9运行到系统断点然后在命令行窗口输入bp CatFly.FlyMe设置断点。注意在x64dbg里断点语法是bp 模块名.函数名模块名就是CatFly。设好断点后F9继续运行程序会在调用FlyMe时停下来。此时观察栈窗口Stack和寄存器窗口可以看到栈顶返回地址指向调用FlyMe的代码位置[esp4]位置是传入参数也就是字符串test的地址在内存窗口跳到该地址可以看到字符串内容。此时单步F8进入函数体走到异或循环处观察循环前后内存的变化。比如在某一次循环执行前input[i]是ASCII字符t即0x74异或0x7F后变成0x0B再和密文对应字节比对。如果你改传入的字符串为正确flag进入函数之后每一步异或结果都会和密文完全一致函数最终返回1。动态调试的价值不仅仅在于验证答案更在于帮助你理解静态分析时那些不确定的细节——比如循环次数真的只有21次吗比如异或的立即数确实是0x7F而不是其他操作码的一部分这些用眼睛看反汇编容易出错的地方用调试器一跑就真相大白。5.3 直接查看内存中的flag动态调试还有一个很实用的技巧既然密文是硬编码在数据节里的你可以在运行到FlyMe函数内部时直接在内存窗口搜索那段密文数据。由于密文是十六进制形式搜索时可以直接转换打开内存窗口使用搜索功能搜索十六进制序列13 1C 0A 1A 0A 1C 0E 5C...找到后在内存区域里直接选中这段数据用十六进制编辑器或调试器的复制为字符串功能转换内存里存放的其实是密文本身不是明文flag所以这一步只能帮你定位数据位置。但如果你在程序完成比对之后再去搜fla、ctf、flag这类关键词有些题目会直接在内存里留下判断结果或明文clue。CatFly没有做这种多余的事情不过搜索内存始终是动态调试的保留技巧。6. 常见问题与避坑笔记6.1 地址随机化导致的重定位困惑在调试DLL时最常遇到的一个坑是你在IDA里看到的函数地址是0x10001020但x64dbg加载后断点地址变成了0x6E201020。这是因为DLL在加载到进程时如果默认基址被占用系统会对它进行重定位Rebasing所有绝对地址都会加一个偏移量。解决这个问题的方法有几种在x64dbg里直接用bp CatFly.FlyMe这种模块名函数名的语法让调试器自己解析地址用PE-bear查看DLL的首选基址ImageBase然后在调试器里加载时手动指定相同基址如果可能的话不要尝试在静态地址上下硬断点动态算好偏移再加这个坑几乎每个刚接触DLL调试的新手都会踩一次提前知道能省不少时间。6.2 函数序言与反调试的干扰CatFly没有反调试但以后会遇到有反调试的DLL。DLL被加载时系统会先执行它的入口点DllMain如果有反调试逻辑可能在你的宿主程序还没调用导出函数之前就已经让程序退出或行为异常。应对思路有两个一是直接静态分析DllMain看它有没有可疑的系统调用如IsDebuggerPresent、NtQueryInformationProcess二是用调试器的时候注意观察程序入口点处的行为。CatFly的DllMain是空的所以没有这个问题。但养成先看入口点再看导出函数的习惯对后续进阶很有帮助。6.3 伪代码和汇编对不上怎么办使用IDA时有时候Hex-Rays给出的伪代码会让人误以为逻辑很简单但一对照汇编却发现有出入——最常见的情况是优化选项不同导致的。比如编译器可能把循环展开、把异或和比较合并到一条指令里或者用SIMD指令一次处理多个字节。遇到这种情况我的建议是以汇编为准。伪代码是辅助不是权威。分析时如果发现矛盾单步调试是最快的裁决方式。6.4 导出函数名找不到如果你发现自己分析的DLL导出表里什么都没有那可能遇到的是无导出函数的DLL或者导出表被隐藏了。CatFly的导出表非常清晰但更复杂的题目可能会用#1这种序号导出无名字函数或者用动态解析的方式隐藏导出函数。遇到后者需要结合导入表和运行时行为去猜测或者直接在内存里搜索特征字符串。一个小技巧在IDA的Exports窗口里无名字导出函数会以sub_xxxx或off_xxxx形式标记但真正可被GetProcAddress调用的函数一定是能通过序号定位的。如果你确认函数被#n方式导出可以用GetProcAddress(hMod, (LPCSTR)序号)这种方式动态调用它这也是出题人设置的另一道小关卡。写在最后的一点经验这类给一个DLL逆向导出函数拿flag的入门题我见过很多变体CatFly算是把流程走得很标准的一个。它没有刻意刁钻每一步分析都有迹可循先看导出表确认攻击面再通过字符串和导入表给程序画像然后静态分析核心函数还原算法最后用动态调试验证结论。这个过程本身就是逆向工程日常工作的缩影——不是炫技而是有条理地缩小未知范围。我个人的建议是新手做完这道题之后不要急着刷更难的题而是回头做两件事第一把CatFly的DLL用不同工具再分析一遍比如Ghidra和IDA各来一次感受不同工具在反编译质量上的差异第二尝试自己写一个类似的DLL导出一个带异或校验的函数再用本文的流程去逆向分析自己写的代码。当你能从出题人的视角设计出一道题并且轻松地站在答题人视角解决它时DLL分析这项技能才算真正长在了你身上。