1. 项目概述:一次从“壳”到“芯”的逆向实战
最近在复盘NewStarCTF2025第三周的逆向题目,感觉这周的题目设计得相当有层次,从基础的脱壳技巧,一路深入到RC4和SM4这两种密码算法的实战分析。对于刚接触CTF逆向或者想系统提升逆向工程能力的朋友来说,这套题目是个非常棒的练手材料。它不像一些“炫技”题那样堆砌冷门技巧,而是紧扣逆向工程中几个非常核心且实用的技能点:识别与处理加壳程序、分析自定义算法、以及逆向现代密码学实现。
简单来说,这周的挑战可以概括为“三板斧”:第一斧,劈开程序外层的“保护壳”(UPX魔改版);第二斧,理清程序内部的数据流和逻辑(可能涉及RC4流密码);第三斧,最终攻克核心的数据加解密逻辑(SM4分组密码)。这个过程完整模拟了分析一个经过保护的、内含敏感逻辑的恶意软件或商业软件模块的实战路径。无论你是CTF选手,还是对软件安全、恶意代码分析感兴趣的安全研究员,掌握这套组合拳都至关重要。
接下来,我会结合题目,把这“三板斧”的每一步拆开揉碎了讲,不仅告诉你“怎么做”,更重点分享“为什么这么做”以及“我踩过哪些坑”。我们会从最外层的壳开始。
2. 逆向工程核心思路与工具链准备
逆向工程不是漫无目的地乱看汇编代码,尤其是在CTF或实战中,时间有限,必须有清晰的思路和顺手的工具。面对“NewStarCTF2025-WEEK3”这样的题目,我的核心思路是“由外及内,动静结合”。
由外及内,指的是先处理最外层的保护(如加壳、混淆),再分析内部的业务逻辑和算法。这就像剥洋葱,得一层一层来。题目提示了“魔改UPX”,这就是最外层,我们必须先解决它,才能看到里面RC4和SM4的代码。
动静结合,则是方法论。“静”指静态分析,在不运行程序的情况下,通过反汇编工具(如IDA Pro, Ghidra)查看代码结构、字符串、导入表等信息,快速把握程序框架。“动”指动态分析,使用调试器(如x64dbg, OllyDbg)实际运行程序,观察其运行时行为、内存数据变化、关键函数调用等。两者结合,静态分析给我们地图,动态分析带我们走通这条路。
工欲善其事,必先利其器。下面是我处理这类题目时必备的工具链,并解释为什么选它们:
静态分析主力:IDA Pro + Ghidra
- IDA Pro:逆向界的“瑞士军刀”,交互式反汇编器。它的反编译插件(Hex-Rays Decompiler)能将汇编代码转换成近似高级语言(C语言)的伪代码,极大提升分析效率。对于识别标准库函数、分析程序流图(CFG)至关重要。我通常用IDA进行首次快速扫描和关键函数定位。
- Ghidra:NSA开源的反汇编工具套件。它的优势在于完全免费、功能强大,且反编译引擎效果不俗。我经常将Ghidra作为IDA的补充,特别是其强大的搜索和交叉引用(XREF)功能,在分析大型二进制文件或寻找特定模式(如密码算法的S盒、常量表)时非常有用。
动态调试利器:x64dbg
- x64dbg:开源、强大的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”壳
UPX(Ultimate 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等区段。魔改版可能重命名了这些区段,或者修改了其属性,干扰分析工具的识别。
在动态调试中,我们需要找到OEP(Original 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 在二进制中识别RC4
RC4算法主要包括两个部分:密钥调度算法(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。
- 在调试器中,找到PRGA循环中计算
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 = b"secret_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和CK:SM4算法在密钥扩展和轮函数中使用了系统参数FK(4个32位字)和固定参数CK(32个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的值。
- 修改线性变换L:SM4的L变换是固定的,修改它会彻底改变算法。
- 增加或减少轮数:非标准的轮数。
应对策略:
- 提取算法参数:使用调试器,在算法初始化阶段,从内存中Dump出S盒、FK、CK等所有常量数组。
- 模拟算法:根据提取的参数,用Python重新实现这个“魔改SM4”算法。即使轮函数被修改,只要你能通过动态调试理清每一轮的操作顺序(异或、查表、移位等),就能复现。
- 利用已知条件:如果题目是加密了一个已知文件(例如,一个BMP图片的头部是固定的),可以利用已知明文攻击,来验证你提取的参数和逆向的算法逻辑是否正确。
- 编写解密脚本:SM4是对称加密,解密算法与加密算法类似,只是轮密钥的使用顺序相反。如果你成功实现了加密算法,只需调整轮密钥的顺序即可实现解密。或者,如果题目只是要求获取加密后的某个结果,你可能只需要模拟加密过程。
# 伪代码,展示思路 def sm4_decrypt(ciphertext, key, iv=None, mode='CBC'): # 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,这道题目覆盖了从外到内、从通用技巧到专业知识的完整链条。通过这样的实战练习,你收获的不仅是几道题的解法,更是一套应对未知二进制文件的分析方法论。最后,保持耐心,享受一步步揭开程序面纱的过程,这才是逆向最大的乐趣所在。