CTF逆向实战:C++逆向与异或加密算法分析

CTF逆向实战:C++逆向与异或加密算法分析 1. 项目背景与挑战从一道CTF题看C逆向的复杂性最近在复盘一些经典的CTF逆向题目2019年红帽杯的CHILDRE这道题给我留下了很深的印象。它不像一些简单的CrackMe只考察基础的汇编阅读和栈帧分析而是把C逆向、函数调用约定、编译器修饰符、加密算法黑盒分析以及最后的暴力破解这几个点巧妙地串联在了一起。很多刚接触逆向的朋友看到C编译出来的那一大堆修饰过的函数名和复杂的对象结构就头疼这道题可以说是一个非常好的综合训练场。它要求你不仅仅能看懂汇编还得理解编译器背后的行为逻辑甚至需要一些密码学的直觉和脚本编写能力。今天我就结合这道题把整个分析思路和解题过程掰开揉碎了讲一遍希望能帮你建立起一套处理这类复杂C逆向问题的实战方法。题目通常是一个可执行文件运行后可能要求输入一个flag或者序列号。我们的目标就是通过逆向分析找到正确的输入是什么或者写出一个能生成正确输入的脚本。CHILDRE这个名字可能暗示了程序内部有类似“孩子”或“分支”的结构或者是某种算法的代称这需要我们进入程序内部才能揭晓。2. 初探与静态分析面对“面目全非”的C符号拿到一个Windows平台的C逆向题我的习惯是先用PEiD或Exeinfo PE快速扫一眼有没有加壳。幸运的是这类比赛题大多不加壳或者只用简单的UPXCHILDRE通常是无壳的。确认无壳后直接拖进IDA Pro。打开IDA迎面而来的第一个挑战就是函数列表里那些“面目全非”的名字。比如你可能看到_ZNKSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7compareEPKc、_Z41__static_initialization_and_destruction_0ii这类天书般的字符串。这就是C的Name Mangling名字修饰。编译器为了支持函数重载、命名空间、类成员函数等特性会把函数名、参数类型、所属类等信息编码成一个唯一的内部符号。对于逆向分析来说这既是坏事也是好事。坏事是它可读性极差好事是它包含了丰富的信息一旦我们能解析它就能还原出函数原本的签名。注意IDA Pro通常内置了Demangle反修饰功能。你可以在Options - Demangled Names里选择显示反修饰后的名字如GNU C或Visual C。如果IDA没有自动识别可以尝试使用FLAIR工具包生成对应的签名文件.sig或者更简单地使用在线工具或cfilt命令Linux下进行手动反修饰。对于这道题确保IDA能正确显示std::string等STL相关的函数名至关重要。在反修饰的帮助下我们能在函数列表里看到一些更清晰的名字比如main、一些构造函数、析构函数以及关键的用户输入处理函数。首先定位到main函数。在C中main的识别有时需要点技巧因为真正的入口点是mainCRTStartup它会进行一系列全局初始化然后才调用main。我们可以通过搜索字符串引用ShiftF12来辅助定位比如搜索程序运行时输出的提示语如“Please input your flag:”或“Wrong!”、“Correct!”等。找到main函数后按F5使用Hex-Rays插件生成伪代码。这时你会看到C风格的代码充满了std::string、std::cout、std::cin以及可能的各种类对象。伪代码虽然友好但绝不能完全依赖它尤其是涉及底层内存操作或编译器优化时必须结合汇编代码空格键切换视图进行验证。3. 核心逻辑剖析函数调用约定与黑盒分析在main函数的伪代码中我们通常能看到用户输入被存储到一个变量比如std::string user_input中。然后这个输入会被传递给一个或多个函数进行处理最后将处理结果与一个预设的值进行比较。这里就引出了第二个关键点函数调用约定。在32位程序中常见的调用约定有__cdecl、__stdcall、__thiscallC成员函数专用等。它们规定了函数参数如何压栈、栈平衡由谁调用者还是被调用者负责。在64位程序中CHILDRE很可能是64位参数优先通过寄存器传递RCX, RDX, R8, R9用于前四个整数或指针参数XMM0-XMM3用于浮点多余的才用栈。IDA的伪代码通常能很好地处理这些约定但我们在看汇编时心里要有数。例如看到一个Call指令前后有很多mov指令操作RCX, RDX寄存器那很可能就是在准备参数。假设我们在main中看到类似这样的代码std::string processed some_mysterious_function(user_input); if ( processed some_constant_string ) { std::cout Correct!; }那么some_mysterious_function就是我们的核心分析目标。双击跟进去。进入这个函数后你可能会看到循环、条件判断、以及大量的算术或逻辑运算。这就是题目的“加密”或“校验”逻辑所在。CHILDRE题目的一个典型特征是使用了异或XOR操作。在汇编中异或指令是xor。异或操作在CTF中非常常见因为它具有可逆性A xor B C 则A xor C B且常被用于简单的加密或数据混淆。我们的分析策略是进行黑盒分析。即我们不一定需要完全理解每一行代码的精确含义尤其是那些经过高度优化的编译器代码而是需要弄清楚这个函数的“输入-输出”映射关系。它到底对我的输入做了什么识别关键数据流跟踪你的输入user_input在这个函数中是如何被使用的。它被当成了字符数组const char*还是被转换成整数进行处理关注那些对输入字符串进行遍历for循环while循环的代码。定位常数与变换在循环内部寻找与你的输入进行运算的常数。这些常数可能是直接出现在代码里的数字如0x5A也可能是从某个全局变量或函数中取出的值。异或运算通常就发生在这里例如input[i] ^ key[i]或input[i] ^ 0x37。观察结果生成函数最终返回了什么是一个新的字符串一个布尔值还是一个整数CHILDRE很可能返回一个字符串用于和预设的密文进行比较。举个例子你可能会在伪代码中看到这样的核心循环for ( int i 0; i input_length; i ) { processed_data[i] input_string[i] ^ xor_key[i % key_length]; }或者更复杂一些异或的key是动态生成的或者运算前还对输入进行了位移、加减等操作。此时一个有效的方法是进行动态调试。使用x64dbg或OllyDbg附加到程序在关键函数入口、循环开始处下断点。运行程序输入一个简单的、有规律的测试字符串比如”AAAAAAA…”或”123456…”然后单步执行观察寄存器和内存中数据的变化。你可以清楚地看到你的’A’ASCII 0x41和某个key值异或后变成了什么。这比单纯静态阅读要直观得多。4. 关键算法还原异或运算与常量推导通过静态分析和动态调试的结合我们目标是把some_mysterious_function的算法用Python或C语言清晰地描述出来。假设我们分析出算法如下这是一个简化的示例模型实际题目会更复杂预设一个常量字符串密文比如encrypted_flag “\x1F\x0A\x1E\x00\x0D\x0A\x17\x00…”长度是n。程序读取用户输入user_input长度也必须为n。对用户输入进行变换对于每个字符user_input[i]先加上它在字符串中的索引i然后与一个固定的魔数0x55进行异或得到tmp。再用tmp与一个密钥字节key[i]密钥可能是另一个常量数组进行异或。将最终结果与encrypted_flag[i]进行比较必须完全相等。用公式表示就是( (user_input[i] i) ^ 0x55 ) ^ key[i] encrypted_flag[i]由于异或操作满足结合律和交换律且A ^ B C等价于A C ^ B我们可以进行逆推。我们已知encrypted_flag[i]从IDA的二进制数据中可提取也知道key[i]通过分析代码找到的常量数组还知道0x55和i。那么正确的user_input[i]应该是user_input[i] ( (encrypted_flag[i] ^ key[i]) ^ 0x55 ) - i注意这里的运算顺序和类型。异或^的优先级高于减法-但为了清晰可以加括号。同时要小心整数溢出问题在C/C中char类型运算可能会发生符号扩展但在Python中我们可以用 0xFF来模拟字节操作。所以解题脚本的核心就是一个从尾至首的逆运算过程。我们需要从IDA中提取出encrypted_flag和key这两个关键的常量数组。在IDA中你可以双击这些常量跳转到数据定义的地方然后使用Edit - Export Data或者用脚本如IDAPython将其批量导出为C/Python的数组格式。5. 编写破解脚本与细节处理基于第4步推导出的算法编写Python解密脚本就水到渠成了。但这里有几个细节必须注意这也是实战中容易踩坑的地方数据提取的准确性IDA中显示的数据可能是十六进制db 1Fh也可能是十进制db 31还可能显示为字符db ‘\x1F’。务必确认你复制出来的字节值是正确的。最好的方法是查看十六进制视图Hex View-1直接复制原始的hex值。对于字符串常量还要注意是否以null结尾计算长度时不要包含结尾的\x00。运算的字节边界所有的异或、加减运算都必须在0-255一个字节的范围内进行。在Python中即使中间结果暂时超出了这个范围最终在赋值给字符时也需要用 0xFF进行截断以模拟C中char类型的行为。例如correct_char ((encrypted_flag[i] ^ key[i]) ^ 0x55) - i correct_char correct_char 0xFF # 确保结果在0-255之间索引i的处理注意i是作为整数参与运算的。在逆运算中- i可能导致负数通过 0xFF可以将其转换回有效的字节值。例如-1 0xFF的结果是255。编码问题最后拼接出来的字符串可能就是flag。但有时flag可能包含不可打印字符。你需要检查输出如果看到\x7f这类字符要确认题目是否要求flag是可读字符串。有时算法逆推后得到的字节序列需要再用某种编码如base64解码一次或者本身就是flag{…}的格式。一个完整的解密脚本框架如下#!/usr/bin/env python3 # 从IDA中提取的常量数组 encrypted_flag [0x1F, 0x0A, 0x1E, 0x00, 0x0D, 0x0A, 0x17, 0x00, ...] # 替换为实际数据 xor_key [0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x6B, 0x65, 0x79, ...] # 替换为实际数据 const_xor 0x55 flag_len len(encrypted_flag) flag [] for i in range(flag_len): # 逆向算法 enc ^ key ^ const (input i) tmp encrypted_flag[i] ^ xor_key[i % len(xor_key)] tmp tmp ^ const_xor # 然后 tmp input[i] i, 所以 input[i] tmp - i input_char (tmp - i) 0xFF flag.append(chr(input_char)) print(‘’.join(flag))运行这个脚本你很可能就直接得到了flag格式可能是flag{…}或RCTF{…}等。6. 当逆推不可行时爆破策略的考量然而不是所有的题目都能如此干净利落地进行数学逆推。有时算法可能非常复杂或者中间引入了非可逆的操作比如除以一个数但结果取整了导致无法直接从输出推导输入。又或者密钥空间很小但算法本身很复杂。这时我们就需要考虑爆破Brute-Force。CHILDRE题目有时也会设计得需要部分爆破。爆破不是漫无目的地瞎试而是基于我们对算法和约束条件的了解进行有方向的、缩小范围的尝试。常见的爆破场景包括密钥空间小如果加密用的key是有限的几种可能比如一个4位数字PIN码那么我们可以遍历所有可能的key用每个key去解密encrypted_flag然后判断解密出的字符串是否满足flag的格式例如以”flag{“开头包含可打印字符等。部分字符已知flag通常有固定格式如flag{32位hex}或flag{单词_单词}。我们可以利用这些已知字符去反推密钥的一部分或者验证我们的解密算法是否正确。多分支校验程序可能对输入的不同部分进行不同的校验。我们可以先集中精力破解第一部分利用第一部分正确输入带来的反馈比如程序输出“第一部分正确”来缩小后续破解的范围。这在动态调试中尤其有用你可以修改内存中的中间结果观察程序分支的走向。编写爆破脚本时要注意效率。如果密钥空间是2^32约43亿用Python暴力遍历可能太慢。这时需要思考是否有更巧妙的办法比如利用算法漏洞、数学性质或者将核心算法用C语言重写并编译执行以提高速度。但在CTF比赛中题目设计的爆破范围通常是合理的可能在2^241600万以内用优化的Python脚本在可接受时间内几分钟到十几分钟是可以跑出来的。7. 实战中的陷阱与经验总结回顾这道CHILDRE的解题过程有几个陷阱是新手特别容易掉进去的盲目相信F5伪代码Hex-Rays生成的伪代码并非百分百准确特别是当代码经过激进优化、或使用了某些不常见的编译器特性时。对于关键的计算部分一定要对照汇编指令看。比如一个char类型的变量在伪代码里显示为int在运算时可能被符号扩展导致你的逆运算公式出错。在汇编层面你会看到movsx符号扩展或movzx零扩展指令这对理解数据类型至关重要。忽略函数调用约定和栈平衡在手动分析调用栈或修改代码进行调试时如果搞错了调用约定可能会导致栈指针错乱程序崩溃。在64位程序中虽然前几个参数用寄存器但栈空间依然需要为它们“预留”shadow space这是容易被忽略的地方。异或密钥的循环使用题目中的异或密钥key长度可能比输入短这时会循环使用key[i % len(key)]。在提取key和编写解密脚本时必须注意这个循环模式模运算%不能漏。动态调试时的环境差异有些程序会检测调试器或者因为环境变量、路径问题导致动态运行时的行为与静态分析不同。如果动态调试时程序行为异常可以考虑使用ScyllaHide等插件隐藏调试器或者检查程序是否依赖了特定的文件、注册表项。我个人在处理这类题目的习惯是静态分析IDA看流程 - 动态验证x64dbg看数据流 - 提取常量写IDAPython脚本或手动记录 - 推导算法在纸上画清楚 - 编写解密脚本Python边写边测试 - 最终获取Flag。每一步都要有耐心尤其是面对一堆修饰过的C函数名时不要慌利用好反修饰工具抓住main函数和字符串引用这两个突破口层层深入总能理清脉络。这道CHILDRE题涵盖的知识点很典型吃透它对于解决大多数中等级别的C逆向题目你会感觉顺手很多。