公司动态
CTF逆向实战:从魔改UPX脱壳到RC4、SM4密码算法分析
1. 项目概述一次从“壳”到“芯”的逆向实战最近在复盘NewStarCTF2025第三周的逆向题目感觉这周的题目设计得相当有层次从基础的脱壳技巧一路深入到RC4和SM4这两种密码算法的实战分析。对于刚接触CTF逆向或者想系统提升逆向工程能力的朋友来说这套题目是个非常棒的练手材料。它不像一些“炫技”题那样堆砌冷门技巧而是紧扣逆向工程中几个非常核心且实用的技能点识别与处理加壳程序、分析自定义算法、以及逆向现代密码学实现。简单来说这周的挑战可以概括为“三板斧”第一斧劈开程序外层的“保护壳”UPX魔改版第二斧理清程序内部的数据流和逻辑可能涉及RC4流密码第三斧最终攻克核心的数据加解密逻辑SM4分组密码。这个过程完整模拟了分析一个经过保护的、内含敏感逻辑的恶意软件或商业软件模块的实战路径。无论你是CTF选手还是对软件安全、恶意代码分析感兴趣的安全研究员掌握这套组合拳都至关重要。接下来我会结合题目把这“三板斧”的每一步拆开揉碎了讲不仅告诉你“怎么做”更重点分享“为什么这么做”以及“我踩过哪些坑”。我们会从最外层的壳开始。2. 逆向工程核心思路与工具链准备逆向工程不是漫无目的地乱看汇编代码尤其是在CTF或实战中时间有限必须有清晰的思路和顺手的工具。面对“NewStarCTF2025-WEEK3”这样的题目我的核心思路是“由外及内动静结合”。由外及内指的是先处理最外层的保护如加壳、混淆再分析内部的业务逻辑和算法。这就像剥洋葱得一层一层来。题目提示了“魔改UPX”这就是最外层我们必须先解决它才能看到里面RC4和SM4的代码。动静结合则是方法论。“静”指静态分析在不运行程序的情况下通过反汇编工具如IDA Pro, Ghidra查看代码结构、字符串、导入表等信息快速把握程序框架。“动”指动态分析使用调试器如x64dbg, OllyDbg实际运行程序观察其运行时行为、内存数据变化、关键函数调用等。两者结合静态分析给我们地图动态分析带我们走通这条路。工欲善其事必先利其器。下面是我处理这类题目时必备的工具链并解释为什么选它们静态分析主力IDA Pro GhidraIDA Pro逆向界的“瑞士军刀”交互式反汇编器。它的反编译插件Hex-Rays Decompiler能将汇编代码转换成近似高级语言C语言的伪代码极大提升分析效率。对于识别标准库函数、分析程序流图CFG至关重要。我通常用IDA进行首次快速扫描和关键函数定位。GhidraNSA开源的反汇编工具套件。它的优势在于完全免费、功能强大且反编译引擎效果不俗。我经常将Ghidra作为IDA的补充特别是其强大的搜索和交叉引用XREF功能在分析大型二进制文件或寻找特定模式如密码算法的S盒、常量表时非常有用。动态调试利器x64dbgx64dbg开源、强大的Windows调试器支持32位和64位应用。界面友好插件生态丰富。相比老牌的OllyDbg它对现代Windows程序的支持更好。动态调试是我们验证静态分析猜想、解密运行时数据、绕过反调试机制的“手术刀”。辅助工具集Detect It Easy (DIE)或Exeinfo PE用于快速识别文件类型、编译器、以及最重要的——是否加壳及壳的类型。这是我们的“第一眼”诊断工具。Python pwntools/ctypes用于编写解密脚本、模拟算法、与题目服务器交互如果是Pwn题。密码学题目几乎离不开Python进行快速验证。CyberChef一个强大的Web端编解码、加密解密、数据分析工具。对于快速尝试一些编码Base64, Hex或简单密码学操作XOR, 一些古典密码非常方便可以作为思路验证的“草稿纸”。提示工具没有绝对的好坏只有是否顺手。建议新手先从 x64dbg 和 Ghidra (免费) 入手掌握基本方法后再根据需求考虑IDA Pro等商业工具。有了清晰的思路和工具我们就可以直面第一个挑战了那个被“魔改”过的UPX壳。3. 挑战一解剖“魔改UPX”壳UPXUltimate Packer for eXecutables是一个著名的开源压缩壳因其压缩率高、使用广泛而成为CTF和恶意分析的常客。标准UPX壳有成熟的脱壳工具如upx -d和手动脱壳套路。但“魔改”二字意味着出题人修改了UPX的源码或加壳流程使得标准方法失效。这正是题目的第一个考点你是否真正理解UPX壳的工作原理而不是只会用工具。3.1 识别与确认魔改点首先用Detect It Easy检查目标程序。它很可能仍然会识别为“UPX”但版本信息或某些特征段如UPX0, UPX1的命名、大小可能异常。或者直接用upx -d尝试脱壳大概率会失败并提示“NotPackedException”或类似的错误。这就是“魔改”的直观证据。接下来我们需要在静态分析中寻找魔改的痕迹。用IDA加载程序查看入口点Entry Point。标准的UPX壳入口代码有一套固定的模式通常是pusha保存所有寄存器然后经过一系列popa、跳转最终跳到原始程序入口点OEP。魔改可能发生在以下几个地方入口指令序列被修改比如pusha/popa被替换成其他指令或者中间添加了垃圾指令、花指令。壳的Stub代码被修改UPX在压缩代码前会先解压自身。这段自解压逻辑Stub可能被修改增加了解压密钥或变换了解压算法。区段Section信息被篡改UPX通常生成UPX0、UPX1等区段。魔改版可能重命名了这些区段或者修改了其属性干扰分析工具的识别。在动态调试中我们需要找到OEPOriginal Entry Point原始入口点。这是脱壳的关键。手动寻找OEP的经典方法是“单步步入F7配合内存断点”。3.2 手动脱壳实战步骤假设我们用x64dbg加载了目标程序。初步单步与观察在程序入口点开始单步F7。注意观察栈Stack和寄存器Registers的变化。UPX壳通常会先将原始压缩后的代码/数据解压到某个内存区域通常是UPX0段。寻找“大跳转”壳代码执行完毕后必然会通过一个jmp或retn指令跳转到解压后的原始代码处这个跳转的目标地址就是OEP。这个跳转往往是一个跨段的长跳转目标地址可能是一个看起来“不像壳代码”的地址例如代码段开头通常是push ebp; mov ebp, esp这样的函数序言。利用内存访问断点更高效的方法是在UPX1通常存放压缩数据区段上设置“内存访问断点”。当壳代码读取压缩数据准备解压时调试器会中断。然后我们可以在解压逻辑结束后在解压目标区段UPX0上设置“内存执行断点”。当壳尝试跳转到解压后的代码执行时调试器会再次中断此时中断的位置就非常接近OEP。到达OEP并Dump成功停在OEP后例如看到典型的push ebp我们需要将整个进程的内存镜像转储Dump出来并修复其导入表IAT使其能够独立运行。x64dbg的插件如Scylla可以自动化这个过程。在OEP处打开Scylla插件。点击“IAT Autosearch”自动搜索导入表。点击“Get Imports”获取导入函数列表。检查列表中是否有无效或畸形的函数指针这可能是魔改导致的可能需要手动修复。最后点击“Dump”选择保存路径再点击“Fix Dump”修复刚Dump的文件。3.3 针对魔改的应对技巧代码混淆如果壳代码里添加了花指令无意义指令如push eax; pop eax干扰反汇编需要耐心识别并忽略或者使用调试器的“运行到光标处”功能跳过无关代码块。反调试陷阱魔改壳可能加入了简单的反调试如IsDebuggerPresent检查、NtGlobalFlag检测等。在x64dbg中可以使用插件如TitanHide或手动修改标志位来绕过。修改解压逻辑这是较难的魔改。可能需要动态跟踪解压算法的每一个步骤理解其变换甚至自己写一个解压脚本。在这种情况下重点观察解压循环和内存数据的写入操作。实操心得手动脱壳时养成随时在关键地址下注释Comment和标签Label的习惯。记录下“此处开始解压循环”、“此处写入解密后代码”等信息能极大帮助理清壳的逻辑。对于魔改UPX成功脱壳并运行Dump后的程序是进入下一阶段分析的门票。4. 挑战二逆向RC4流密码算法成功脱壳后我们就能在IDA中清晰地看到程序的原始逻辑。根据题目描述接下来很可能遇到RC4算法。RC4是一种流密码曾经广泛应用于SSL/TLS等协议中现在虽不推荐用于新系统但其结构简单是学习密码学逆向的经典目标。4.1 在二进制中识别RC4RC4算法主要包括两个部分密钥调度算法KSA和伪随机子密码生成算法PRGA。在汇编/伪代码中我们可以通过以下特征来识别它256字节的S盒数组RC4内部维护一个256字节的状态数组S[0]到S[255]。在代码中你可能会看到一个256字节的静态数组初始化通常按顺序0x00, 0x01, ... 0xFF或者看到两个嵌套循环外层i从0到255内层j的计算涉及密钥对这个数组进行打乱。这是KSA。两个索引变量i和j算法会维护两个索引通常初始为0在PRGA中它们会不断更新i (i 1) mod 256,j (j S[i]) mod 256然后交换S[i]和S[j]输出S[(S[i] S[j]) mod 256]作为密钥流字节。异或XOR操作流密码的核心是生成的密钥流与明文逐字节异或得到密文反之亦然。因此你会看到一个循环在循环体内数据字节与一个来自某个计算过程的字节进行XOR。在IDA的伪代码视图中寻找类似下面的模式// KSA 可能的样子 for (i 0; i 256; i) { S[i] i; } for (i 0; i 256; i) { j (j S[i] key[i % key_len]) % 256; swap(S[i], S[j]); } // PRGA 可能的样子 i j 0; for (k 0; k data_len; k) { i (i 1) % 256; j (j S[i]) % 256; swap(S[i], S[j]); keystream_byte S[(S[i] S[j]) % 256]; ciphertext[k] plaintext[k] ^ keystream_byte; // 或反之 }4.2 动态提取密钥与解密识别出RC4后我们的目标通常是提取出加密时使用的密钥或者直接模拟解密过程。静态分析提取密钥如果密钥是硬编码在程序中的字符串如flag{this_is_a_secret_key}你可以通过搜索字符串、交叉引用找到它。如果密钥是通过某些计算生成的例如由用户输入经过哈希变换而来就需要逆向整个密钥生成逻辑。动态调试获取密钥流有时密钥复杂但我们可以通过动态调试在RC4初始化后KSA完成、加密开始前直接从内存中Dump出完整的256字节S盒。因为S盒是密钥的唯一代表。或者更直接地在XOR操作发生处下断点直接捕获生成的密钥流字节。在调试器中找到PRGA循环中计算keystream_byte并执行XOR的指令。下硬件断点或条件断点记录下每个keystream_byte的值。如果已知部分明文例如CTF题中常见的已知文件头格式PK\x03\x04for ZIP,\x7fELFfor ELF可以利用它直接计算出密钥流keystream ciphertext ^ known_plaintext。4.3 编写解密脚本一旦获取了密钥或初始S盒就可以用Python轻松实现解密。Python的cryptography库或arc4模块某些第三方库可以直接使用但为了理解自己实现一遍也很简单def rc4_ksa(key): S list(range(256)) j 0 for i in range(256): j (j S[i] key[i % len(key)]) % 256 S[i], S[j] S[j], S[i] return S def rc4_prga(S, data): i j 0 out [] for byte in data: i (i 1) % 256 j (j S[i]) % 256 S[i], S[j] S[j], S[i] k S[(S[i] S[j]) % 256] out.append(byte ^ k) return bytes(out) # 假设从逆向中得到的密钥 key bsecret_key_from_reverse # 假设从文件中读取的密文 ciphertext open(encrypted.bin, rb).read() S rc4_ksa(key) plaintext rc4_prga(S, ciphertext) print(plaintext)注意事项RC4加解密使用相同的密钥和流程。确保你的解密脚本使用的S盒状态与加密开始时一致。如果加密前对数据进行了其他处理如填充、编码解密后也需要相应逆处理。5. 挑战三攻克SM4分组密码算法SM4是我国商用密码标准之一是一种分组密码分组长度和密钥长度均为128位。在CTF中出现通常考察对国密算法的了解以及逆向自定义实现的能力。题目可能直接链接了SM4的库也可能是自己实现了一个SM4算法。5.1 识别SM4算法特征SM4算法相对复杂但有一些显著特征可以帮助我们在二进制中定位固定的S盒SM4使用一个固定的、公开的8位输入8位输出的S盒Substitution Box。这个S盒是一个256字节的常量数组。如果你在程序的静态数据区.rdata段看到一个庞大的、看起来随机的256字节数组极有可能是S盒。你可以将找到的数组与标准SM4的S盒进行比对来确认。轮常数FK和CKSM4算法在密钥扩展和轮函数中使用了系统参数FK4个32位字和固定参数CK32个32位字。这些也是公开的常量。在代码中可能会看到这些常量的加载。32轮迭代SM4算法对每个128位分组进行32轮相同的变换。在反编译代码中你可能会看到一个循环32次的结构循环体内包含异或、S盒查找、线性变换L等操作。密钥扩展SM4需要先将128位加密密钥扩展成32个轮密钥每个32位。会有一个独立的密钥扩展函数它也使用S盒和CK常数。5.2 分析加密模式与数据流识别出SM4后下一步是确定其工作模式。常见的有ECB电子密码本和CBC密码分组链接。ECB模式每个分组独立加密相同的明文分组对应相同的密文分组。在代码中可能就是一个简单的循环对每个128位16字节块调用同一个加密函数。CBC模式需要初始化向量IV且每个分组的加密依赖于前一个分组的密文。代码中会出现一个IV变量或缓冲区并且在加密/解密循环中会有将前一个密文块与当前明文块异或加密时或参与运算解密时的操作。在动态调试时你需要找到加密函数的入口点。观察传入的参数哪个是输入缓冲区明文/密文哪个是输出缓冲区哪个是密钥以及是否有IV。跟踪一个完整分组的加密过程验证其是否符合SM4的32轮结构。5.3 逆向自定义实现与解密出题人可能会对标准SM4进行轻微修改以增加难度例如修改S盒使用一个自定义的S盒。你需要从代码或内存中提取出这个修改后的S盒。修改轮常数修改FK或CK的值。修改线性变换LSM4的L变换是固定的修改它会彻底改变算法。增加或减少轮数非标准的轮数。应对策略提取算法参数使用调试器在算法初始化阶段从内存中Dump出S盒、FK、CK等所有常量数组。模拟算法根据提取的参数用Python重新实现这个“魔改SM4”算法。即使轮函数被修改只要你能通过动态调试理清每一轮的操作顺序异或、查表、移位等就能复现。利用已知条件如果题目是加密了一个已知文件例如一个BMP图片的头部是固定的可以利用已知明文攻击来验证你提取的参数和逆向的算法逻辑是否正确。编写解密脚本SM4是对称加密解密算法与加密算法类似只是轮密钥的使用顺序相反。如果你成功实现了加密算法只需调整轮密钥的顺序即可实现解密。或者如果题目只是要求获取加密后的某个结果你可能只需要模拟加密过程。# 伪代码展示思路 def sm4_decrypt(ciphertext, key, ivNone, modeCBC): # 1. 从逆向的二进制中提取出的自定义S盒、CK等 custom_sbox [...] # 256 bytes custom_ck [...] # 32 dwords # 2. 实现自定义的密钥扩展函数生成轮密钥 rk[i] rk key_expansion(key, custom_sbox, custom_ck) # 3. 根据模式进行解密 if mode ECB: # 简单分块使用逆序的rk解密 for block in split_blocks(ciphertext, 16): plain_block sm4_decrypt_block(block, rk[::-1]) # 轮密钥逆序 ... elif mode CBC: # 需要IV解密后与前一个密文块异或 prev_block iv for block in split_blocks(ciphertext, 16): temp_block sm4_decrypt_block(block, rk[::-1]) plain_block xor(temp_block, prev_block) prev_block block # CBC模式解密时前一个密文块是当前输入块 ... return plaintext实操心得面对复杂的自定义密码算法不要试图一次性理解全部汇编代码。先通过特征如大S盒定位到算法模块然后动态跟踪一小段数据比如一个16字节块的完整处理流程。用调试器记录下每一步操作后数据的变化像做实验一样逐步归纳出算法步骤。这个过程虽然耗时但却是逆向工程的核心能力。6. 完整解题流程串联与心得现在我们把整个WEEK3的解题思路串联起来形成一个完整的作战流程文件侦察使用DIE/Exeinfo PE检查程序确认是UPX加壳且可能被魔改。手动脱壳用x64dbg加载程序。单步跟踪或使用内存断点找到那个通往OEP的“大跳转”。在OEP处使用Scylla进行Dump和IAT修复得到可分析的脱壳程序。静态分析入口将脱壳后的程序放入IDA进行分析。搜索字符串查看导入函数寻找可能提示算法如“encrypt”、“decrypt”、“key”、“RC4”、“SM4”或程序逻辑的线索。定位密码学函数根据字符串交叉引用或函数调用图找到核心处理函数。在函数内部根据特征256字节数组、异或循环、32轮结构等判断是RC4还是SM4或是两者的组合例如用RC4生成SM4的密钥。动态验证与数据提取用x64dbg附加或加载脱壳后的程序确保它能运行。在识别出的密码学函数入口、S盒初始化处、XOR操作处下断点。运行程序观察并记录密钥、IV、S盒等关键数据。如果是输入-输出型题目可以输入测试数据跟踪其完整处理流程。算法复现与解密根据静态分析和动态调试获得的信息用Python复现完整的加解密流程。将题目提供的密文或需要加密的明文作为输入运行脚本得到flag或关键数据。提交与验证将得到的flag提交到平台验证。踩坑记录与心得魔改壳的OEP寻找有时候那个“大跳转”的目标地址本身也被混淆了跳过去后可能还有一小段垃圾代码才到真正的OEP。耐心单步几步或者看看跳转目标地址附近的代码是否具有函数序言特征。IAT修复失败Scylla自动搜索IAT可能不完整特别是程序使用了延迟加载Delay Load或魔改壳破坏了IAT结构。此时需要手动查找IAT区域并对照导入函数名手动添加指针。这是一个细致活。RC4密钥的动态获取如果密钥是运行时计算出来的且计算过程复杂与其逆向密钥生成算法不如直接在RC4初始化函数KSA结束后从内存中把整个S盒数组Dump出来。这个S盒就是密钥的等价物可以直接用于解密。SM4模式判断错误误把CBC模式当成ECB会导致解密出的数据开头几个块是乱码。务必注意代码中是否有IV以及异或操作的数据流方向。一个技巧是如果密文长度不是16的整数倍很可能使用了PKCS#7等填充方式这也能间接提示是分组密码模式。自定义算法的验证自己用Python复现算法后一定要用调试器中捕获的中间数据如第一轮加密后的状态、第一轮轮密钥等进行比对确保每一步都完全一致。差之毫厘谬以千里。逆向工程尤其是结合了密码学的题目就像在解一个多层的谜题。它考验的不仅仅是工具的使用更是对程序执行逻辑、底层数据结构和密码学原理的深刻理解。从魔改UPX壳到RC4和SM4这道题目覆盖了从外到内、从通用技巧到专业知识的完整链条。通过这样的实战练习你收获的不仅是几道题的解法更是一套应对未知二进制文件的分析方法论。最后保持耐心享受一步步揭开程序面纱的过程这才是逆向最大的乐趣所在。