Frida Stalker指令级追踪实战:逆向OLLVM混淆的AES算法

Frida Stalker指令级追踪实战:逆向OLLVM混淆的AES算法 1. 项目概述当指令级追踪遇上代码混淆逆向工程领域里代码混淆和反调试技术就像一场永不停歇的攻防战。OLLVMObfuscator LLVM作为一款强大的开源代码混淆框架通过控制流平坦化、虚假分支插入、指令替换等手段能将原本清晰的算法逻辑搅成一团乱麻让静态分析工具几乎失效。而AES高级加密标准算法作为现代密码学的基石广泛用于各类应用的数据保护。当这两者结合——一个被OLLVM重度混淆的AES加密实现——传统的逆向手段往往会陷入僵局。这时候动态分析工具就成了破局的关键。Frida这个强大的动态插桩工具包为我们提供了在运行时窥探和操纵应用的能力。而Frida Stalker作为其指令级追踪引擎允许我们以单条机器指令的精度追踪目标代码的执行流。想象一下即使面对被OLLVM“打碎”并“重组”得面目全非的控制流Stalker也能像一位不知疲倦的侦探忠实地记录下CPU实际执行的每一条指令。这为我们从混沌的执行轨迹中逆向还原出原始的AES算法逻辑提供了前所未有的可能性。这个实战项目的核心目标就是利用Frida Stalker的指令级追踪能力穿透OLLVM混淆制造的迷雾定位、理解并最终“破解”此处指逆向分析出算法逻辑和密钥一个被混淆的AES加密函数。它适合有一定逆向基础熟悉ARM/ARM64汇编并对动态分析感兴趣的安全研究人员、移动应用安全工程师以及对底层执行机制好奇的开发者。通过这个过程你不仅能学会如何对抗高级混淆更能深入理解程序在CPU层面的真实行为。2. 核心思路与技术选型解析面对一个被OLLVM混淆的AES算法直接进行静态分析犹如阅读一本被撕碎后随机重组的书。我们的策略是放弃在“碎片”中寻找逻辑转而观察“阅读者”CPU如何一页一页地翻看这些碎片。这就是动态指令级追踪的思路。2.1 为什么选择Frida Stalker在动态分析工具中我们有多种选择比如基于断点的调试器GDB/LLDB、基于模拟器的动态分析Unicorn等。但针对指令级追踪这个特定场景Frida Stalker具有独特优势高效率与低开销Stalker在目标进程中直接进行代码重编译和插桩避免了模拟器带来的巨大性能损耗。它只追踪我们指定的内存范围可以精准聚焦于目标函数减少无关指令的干扰。对混淆的强对抗性OLLVM混淆尤其是控制流平坦化会破坏函数的基本块结构引入大量间接跳转。传统的基于基本块或函数粒度的追踪很容易跟丢。Stalker的指令级粒度确保了无论控制流如何“乱跳”只要是在我们监控的内存区域每一条被执行的指令都无所遁形。与Frida生态无缝集成我们可以轻松地用JavaScript或Python编写Stalker的追踪逻辑、处理回调数据并利用Frida的其他功能如内存读写、拦截函数进行联动分析构建一个完整的动态分析工作流。非侵入式与隐蔽性相对于下断点Stalker的追踪更为隐蔽对目标程序执行流的干扰相对较小降低了触发反调试机制的风险当然目标程序也可能检测Frida本身。相比之下纯调试器断点追踪粒度太粗且容易被反调试检测全系统模拟器如QEMU又过于笨重。因此Stalker是在真实设备或环境中对混淆代码进行细粒度动态分析的理想选择。2.2 整体逆向策略设计我们的逆向策略可以概括为“由动至静由细至宏”阶段一定位与隔离。首先我们需要在目标App中找到疑似AES加密的函数入口。这可能通过字符串搜索如“AES”、算法模式“CBC”、“ECB”等、导入表分析查找libcrypto.so相关函数或通过监控加密相关API如Android的Cipher类的调用栈来实现。一旦找到候选地址范围我们就用Stalker对其进行隔离追踪。阶段二指令流捕获与清洗。启动Stalker追踪目标地址区间捕获所有执行的机器指令。得到的原始指令流会包含大量由OLLVM插入的垃圾指令、虚假分支跳转以及调度器逻辑。我们需要通过分析指令模式过滤掉这些混淆“噪音”。阶段三模式识别与算法还原。在清洗后的指令流中寻找AES算法的特征模式。例如查找T表查表操作在ARM上可能表现为LDR指令从某个固定地址加载数据、异或操作EOR指令、特定的轮常数加载等。通过识别出加密轮循环、密钥扩展等结构逐步还原出AES的完整执行流程。阶段四关键数据提取。在还原的流程中最关键的一步是提取轮密钥Round Key。通过追踪数据流向观察哪些内存地址或寄存器在每一轮加密中与明文进行异或并结合AES密钥扩展算法的知识可以逆向推导出原始的AES密钥。阶段五验证与重构。使用提取出的密钥用标准的AES库对已知明文进行加密与目标程序的输出对比验证逆向结果的正确性。最终可以重构出一个清晰、去混淆的AES算法伪代码或实现。这个策略的核心在于我们不试图去理解混淆后的控制流图而是直接分析CPU执行的真实指令序列从中提取出与算法核心计算相关的“信号”。3. 环境搭建与目标定位实战工欲善其事必先利其器。在开始追踪之前我们需要一个稳定的分析环境。3.1 Frida环境配置与避坑指南首先确保你的分析机通常是PC和测试设备Root过的Android手机或模拟器上安装了正确版本的Frida。# 在分析机上安装Frida-tools pip install frida-tools注意Frida-server的版本必须与PC端的frida和frida-tools版本严格匹配否则会出现连接失败或不可预知的问题。使用frida --version查看本地版本然后去GitHub Releases下载对应版本的frida-server-{version}-android-{arch}.xz。将下载的frida-server推送到设备并运行adb push frida-server /data/local/tmp/ adb shell su cd /data/local/tmp chmod 755 frida-server ./frida-server 在PC上使用frida-ps -U查看设备进程列表确认连接成功。实操心得如果遇到应用闪退或无法附加很可能是应用内置了Frida检测。常见的检测手段包括检查/proc/self/maps中是否存在frida-agent字符串、检查端口默认27042是否开放、或使用ptrace反调试。对抗方法包括重命名frida-server、使用非默认端口./frida-server -l 0.0.0.0:8080、或者使用更高级的绕过技术如修改frida-gum代码。对于本次实战我们假设目标应用没有强力的反Frida措施或我们已经通过其他方式绕过了它。3.2 目标函数定位技巧假设我们有一个名为com.example.encryptedapp的应用其中包含混淆的AES加密。定位方法有多种字符串搜索法使用Frida的Memory.scan()API搜索内存中可能存在的算法标识字符串如“AES/ECB/PKCS5Padding”。找到字符串后可以查看其引用定位到附近的函数。// 示例Frida脚本片段 Memory.scan(0, Process.enumerateRanges(r--)[0].size, AES, { onMatch: function(address, size){ console.log(Found at: address.toString()); }, onComplete: function(){} });模块导出表分析如果App使用了系统的Crypto库如OpenSSL可以枚举libcrypto.so的导出函数并挂钩AES_encrypt等函数。但OLLVM混淆通常用于保护自定义的、静态链接的算法实现所以这个方法可能不适用。高层API监控法对于Android Java层加密可以挂钩javax.crypto.Cipher类的doFinal方法。通过回溯调用栈找到最终执行加密的Native函数地址。这是非常有效的一招。Interceptor.attach(Module.findExportByName(libjavacrypto.so, Cipher_doFinal), { onEnter: function(args) { console.log(Cipher_doFinal called!); // 打印调用栈寻找Native层入口 console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\n)); } });特征指令模式搜索如果我们对AES的ARM汇编实现有大致了解比如知道它会频繁使用ldr从某地址加载数据做查表可以直接在内存中搜索这些指令模式。这需要更深的汇编功底。假设通过上述方法我们定位到目标Native函数位于模块libnative-lib.so的偏移0x1234处其内存范围大致是0x1234到0x1500。这个地址范围就是我们接下来要交给Stalker的“狩猎场”。4. Frida Stalker深度配置与指令捕获定位到目标后真正的重头戏——指令级追踪开始了。Frida Stalker的使用需要精细的配置。4.1 Stalker基础配置与参数详解我们需要创建一个Stalker对象并配置其追踪行为。// 假设我们已经获取了目标函数的起始地址targetFuncStart let stalker new Stalker(targetThread); // 通常追踪当前线程Thread.currentThreadId() // 配置Stalker选项 let stalkerOptions { events: { // 我们关心指令执行事件 }, // 设置要追踪的代码区域这是提高效率的关键 range: { base: targetFuncStart, size: 0x300 // 假设函数大小约0x300字节 }, // 其他选项... }; Stalker.follow(targetThreadId, stalkerOptions);关键配置项解析range:极其重要。务必指定一个尽可能精确的地址范围。如果不指定Stalker会追踪整个进程的所有线程代码产生海量数据且极易崩溃。通过之前的静态分析或试探性挂钩尽量缩小这个范围。events: 定义需要收集的事件类型。对于指令还原我们主要需要call调用、ret返回、exec普通指令执行和block基本块事件。但为了获取每一条指令我们需要监听exec事件。transform: 一个可选的回调函数允许我们在指令被重新编译前对其进行修改或插入探针。这是一个高级功能可用于实现自定义的指令过滤或标记。4.2 指令流收集与原始数据解析配置好事件监听后我们需要一个队列来收集Stalker产生的事件数据。let eventQueue []; Stalker.flush(); // 清空旧数据 Stalker.follow(targetThreadId, { events: { exec: true, // 收集所有指令执行事件 call: true, // 收集调用事件有助于理解函数边界 ret: true, block: true // 基本块事件有助于在混淆流中划分段落 }, onReceive: function (events) { // events是一个二进制数据块 let parsed Stalker.parse(events, { stringify: false, // 我们不直接转字符串而是获取结构化数据以便后续分析 annotate: true // 尝试反汇编指令 }); eventQueue.push(...parsed); } }); // 触发目标函数执行例如通过Interceptor调用或模拟用户操作 // ... // 函数执行完毕后停止追踪 Stalker.unfollow(targetThreadId); // 现在eventQueue中包含了追踪期间的所有事件eventQueue中的每个事件对象通常包含地址pc、操作码opcode、反汇编文本disassembly、可能的操作数等信息。对于exec事件这就是一条条被执行的指令。注意事项指令级追踪会产生巨量数据。一个简单的函数调用可能产生成千上万条指令记录。务必在测试时先对非常小的、已知的代码片段进行追踪验证脚本的稳定性和数据准确性再应用到复杂的混淆函数上。同时确保你的脚本有足够的逻辑来处理和清空eventQueue避免内存耗尽。5. 从混沌指令流中清洗OLLVM混淆拿到原始的指令流后你会发现它杂乱无章充满了“噪音”。OLLVM的控制流平坦化会将原代码分割成许多基本块并通过一个“调度器”来决定下一个执行哪个块。我们的任务就是过滤掉调度器相关的代码找出真正执行AES计算的“信号”。5.1 识别OLLVM控制流平坦化模式典型的OLLVM控制流平坦化会呈现以下模式统一的入口和出口所有真实的基本块都通过一个共享的“入口块”和“出口块”连接。状态变量与调度器有一个或多个寄存器或内存位置作为“状态变量”。调度器代码根据当前状态变量的值通过一个大的跳转表通常是switch-case的汇编实现跳转到对应的真实基本块。真实基本块每个真实基本块执行一部分原逻辑最后会更新状态变量然后跳转回调度器。在ARM汇编中这通常表现为频繁的ldr指令从某个固定地址状态变量地址加载值到寄存器。cmp指令比较状态值。一系列条件分支b.eq,b.ne,b.gt等或者通过adrldrbr实现的跳转表跳转。真实基本块结束后通过str指令写回一个新的状态值然后一个无条件的b指令跳回调度器循环开始处。5.2 构建指令过滤与清洗策略我们的清洗脚本需要识别并跳过这些模式。一个简单的策略是识别调度器循环寻找一个代码区域它包含一个加载-比较-分支的循环结构并且被频繁地跳转回来。这个区域的指令可以被标记为“调度器代码”。过滤跳转指令忽略所有直接跳转到调度器区域开始地址的b指令。关注计算与内存访问保留那些包含算术运算add,eor,and等、内存加载/存储ldr,str特别是访问非调度器相关内存地址的、以及特定函数调用如果可识别的指令。基于基本块Block事件进行分段Stalker的block事件标志着一个新基本块的开始。我们可以利用这个事件将指令流按基本块分组。然后分析每个基本块的特征如果它只包含状态操作和跳转则很可能是调度器或连接块如果它包含密集的计算和内存访问则可能是“真实块”。// 伪代码简单的清洗逻辑 let cleanedInstructions []; let inScheduler false; let schedulerStartAddr null; for (let event of eventQueue) { if (event.type block) { // 新基本块开始根据其入口指令判断是否为调度器 if (isSchedulerBlock(event)) { inScheduler true; schedulerStartAddr event[0].address; // 假设event[0]是该块第一条指令 } else { inScheduler false; } continue; } if (event.type exec) { // 如果当前在调度器块内且指令是跳回调度器开头则跳过 if (inScheduler isJumpToScheduler(event, schedulerStartAddr)) { continue; } // 跳过纯粹的寄存器移动或状态设置指令如 mov, cmp 用于调度 if (isSchedulerRelatedMoveOrCmp(event)) { continue; } // 保留这条指令 cleanedInstructions.push({ addr: event.address, disasm: event.disassembly, opcode: event.opcode }); } } function isSchedulerBlock(blockEvent) { // 启发式规则块内前几条指令包含加载固定地址、比较、分支 let firstFewInstrs blockEvent.instructions.slice(0, 5); return firstFewInstrs.some(instr instr.disasm.includes(ldr)) firstFewInstrs.some(instr instr.disasm.includes(cmp)) firstFewInstrs.some(instr instr.disasm.includes(b.)); }经过这样的清洗cleanedInstructions数组中的指令序列应该会清晰很多去除了大部分由混淆引入的控制流跳转更接近于算法原本的顺序执行逻辑尽管可能还不是完全连续。6. AES算法特征识别与密钥提取清洗后的指令流虽然去除了控制流噪音但仍然是低级的机器指令。我们需要从中识别出AES算法的“指纹”。6.1 AES加密的汇编级特征一个典型的AES加密实现尤其是查表实现的T-table方案在汇编层面会表现出以下特征这些是我们需要寻找的模式查表操作T-table Access这是最显著的特征。AES的每一轮加密除最后一轮都涉及4次查表对应4个T表Te0, Te1, Te2, Te3。在ARM汇编中这通常表现为ldrb或ldr指令从一个基地址加上偏移的内存位置加载数据到寄存器。这个基地址在加密过程中是固定不变的T表的起始地址。偏移量通常由某个寄存器的值经过运算决定这个寄存器存放的是中间状态字节。// 示例可能是 Te0 表查表 ldr r1, [r0, #0x1234] ; r0是Te0表基址0x1234是编译时确定的偏移常量的一部分 ldrb r2, [r1, r3, LSL #2] ; r3是索引左移2位因为表项是32位/4字节异或操作XORAES的轮密钥加AddRoundKey和列混合后的步骤都涉及异或。对应ARM的eor指令。eor r4, r4, r5 ; 轮密钥加或状态字节混合轮常数Rcon的使用在密钥扩展中会用到轮常数。程序中可能会预定义一个小数组Rcon表或者直接使用立即数。查找对特定内存地址小数组的加载或者代码中出现的如0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36等AES相关的魔术数字。循环结构AES-128有10轮加密。在指令流中你可能会看到一个循环结构执行大致相似的操作序列10次或9次完整轮1次最终轮。虽然OLLVM打乱了循环但通过识别重复出现的查表-异或模式序列可以推断出轮循环。内存访问模式观察ldr/str指令访问的内存地址。加密的输入明文、输出密文以及中间状态通常会存放在连续的或特定的内存区域如栈上或堆分配的缓冲区。跟踪这些缓冲区的读写顺序有助于理解数据流。6.2 逆向推导轮密钥与原始密钥这是整个实战中最关键也最具挑战性的一步。我们的目标是找到每一轮加密中与状态进行异或的轮密钥。定位轮密钥加AddRoundKey步骤在清洗后的指令流中寻找一个模式在每一轮或每一轮的开始/结束的查表操作附近出现一个或多个eor指令其中一个操作数是来自查表计算后的状态值另一个操作数是从某个内存地址或寄存器加载的值。这个“另一个操作数”很可能就是该轮的轮密钥或其一部分。数据流追踪当找到疑似轮密钥加的eor指令后向前追溯eor指令的源操作数来自哪里。它可能是直接从某个固定地址.rodata段存储了扩展后的密钥加载的也可能是经过一些简单计算如从上一轮密钥推导得到的。使用Frida的Memory.readByteArray()等API可以在指令执行时动态读取这些内存地址的值。关联密钥扩展逻辑AES的轮密钥是通过密钥扩展算法从原始密钥推导出来的。如果你能定位到密钥扩展函数可能在加密函数之前被调用其特征是包含大量的eor、ldr、str以及可能用到Rcon并追踪其产生的数据就能直接拿到所有轮密钥甚至原始密钥。动态Hook提取更直接的方法是在识别出轮密钥加的位置后用Frida的Interceptor挂钩那条eor指令所在的地址或包含它的函数在指令执行时直接打印或保存参与异或的两个操作数的值。这样就能直接捕获到运行时使用的轮密钥片段。// 假设我们通过分析确定指令地址 0xdeadbeef 处执行了关键的轮密钥加异或 let keyXorAddr ptr(0xdeadbeef); Interceptor.attach(keyXorAddr, { onEnter: function(args) { // 通过ARM上下文读取寄存器值。例如指令是 eor R0, R1, R2 let statePart this.context.r1; // 假设R1是状态 let roundKeyPart this.context.r2; // 假设R2是轮密钥部分 console.log([KeyXor] State: 0x${statePart.toString(16)}, RoundKey: 0x${roundKeyPart.toString(16)}); // 记录下 roundKeyPart logRoundKey(roundKeyPart); } });拼图与验证收集到所有轮的轮密钥片段可能是4字节的word后按照AES密钥扩展的顺序排列。对于AES-128第一轮使用的轮密钥就是原始密钥本身对于加密初始轮密钥加使用的就是原始密钥。通过对比捕获的轮密钥和标准AES密钥扩展算法可以验证并可能反向推导出原始密钥。如果直接捕获到了密钥扩展过程的输出那就更简单了。实操心得这个过程极其依赖对ARM汇编的熟悉程度和对AES算法细节的深刻理解。建议先用一个未混淆的、开源的AES ARM汇编实现如OpenSSL的aes_core.c对应的汇编进行练习熟悉其指令模式。在实际分析中耐心和细致的记录记录每条重要指令的地址、操作、涉及的寄存器和内存地址是成功的关键。可以编写辅助脚本来自动化搜索特征指令模式并高亮显示。7. 实战问题排查与性能调优在实际操作中你一定会遇到各种问题。这里记录一些典型的坑和解决方案。7.1 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案Frida附加失败或进程崩溃1. Frida-server版本不匹配。2. 目标进程有反调试/反Frida保护。3. 设备未Root或权限不足。1. 检查frida --version与server版本。2. 尝试frida -U -f com.example.app --no-pause在启动时注入。使用检测绕过脚本或修改frida特征。3. 确认adb shell后已su并使用ps | grep frida确认server运行。Stalker.follow()后无事件产生1. 指定的range不正确未覆盖执行代码。2. 追踪的线程错误。3. 目标函数未被触发执行。1. 扩大range范围或先不加range参数进行全局追踪仅用于调试会卡死。通过Module.findExportByName等确认函数地址。2. 使用Process.enumerateThreads()确认线程ID或挂钩函数入口打印线程ID。3. 确保你的脚本能触发目标函数如通过UI操作或调用导出函数。指令事件数据量巨大脚本卡死或内存溢出追踪范围过大或追踪时间过长。1.务必设置精确的range。2. 在onReceive回调中及时处理并清空事件数据不要无限制堆积。3. 只追踪关键路径例如在函数入口开始follow在函数返回时unfollow。4. 考虑使用transform进行过滤只收集特定指令如内存访问、分支。清洗后指令流依然混乱无法识别算法1. 混淆强度高真实计算也被分散。2. 清洗规则过于简单或错误。3. 目标函数可能不是纯AES或与其他逻辑交织。1. 尝试基于数据流分析重点关注对特定内存区域输入/输出缓冲区的读写指令链忽略控制流。2. 优化清洗规则可能需要结合动态污点分析更高级的思路标记源头数据并追踪其传播。3. 确认目标函数是否正确。尝试用已知明文/密文对进行测试观察哪些指令区域对输入变化敏感。无法准确定位轮密钥加操作1. 指令模式识别有误。2. 编译器优化导致指令重排或融合。3. 使用了不同的AES实现如位切片法。1. 复习AES的ARM汇编实现使用未混淆的参考代码进行对比学习。2. 在eor指令处下硬件断点如果环境支持观察前后上下文。3. 如果使用查表法先全力定位T表地址。找到表地址后追踪所有访问该地址区域的指令其附近的eor很可能就是轮密钥加。提取的轮密钥无法推导出原始密钥1. 提取的轮密钥片段顺序错误或不完整。2. 算法可能是AES-192或AES-256密钥扩展不同。3. 存在额外的白盒加密表或编码。1. 重新核对AES密钥扩展算法确保按正确轮序排列捕获的密钥字。2. 尝试AES-192/256的密钥扩展进行匹配。3. 这可能进入了白盒密码学范畴需要分析额外的编码/解码变换。此时单纯追踪指令可能不够需结合对内存中查找表的分析。7.2 Stalker性能调优与高级技巧使用transform进行预过滤这是提升性能和数据质量的大杀器。在transform回调中你可以直接操作ARM指令流插入自定义的“收集探针”或直接跳过不感兴趣的指令块。Stalker.follow(threadId, { transform: function (iterator) { let instruction iterator.next(); do { // 示例只收集内存加载和异或指令 if (instruction.mnemonic.startsWith(ldr) || instruction.mnemonic eor) { iterator.putCallout(function (context) { // 在指令执行时记录上下文信息到自定义队列 logInstruction(context); }); } // 继续下一条指令不插入Stalker默认的收集代码节省开销 iterator.keep(); } while ((instruction iterator.next()) ! null); } });这样onReceive里收到的事件会大大减少只包含我们关心的指令且我们可以通过callout直接获取执行时的寄存器上下文非常强大。结合使用Instruction解析器Frida提供了Instruction对象可以方便地解析指令的详细信息操作码、操作数、寄存器列表等比单纯分析反汇编字符串更可靠。分阶段追踪不要试图一次性追踪整个复杂函数。可以先追踪整个函数大致了解其范围和解构。然后针对疑似密钥扩展、加密主循环等不同阶段进行多次独立的、范围更精确的追踪并对比结果。保存与回放将eventQueue原始数据或清洗后的指令流保存到文件。这允许你离线分析或者在不同阶段运行不同的分析脚本而无需重新执行耗时的动态追踪过程。8. 案例复盘与经验总结回顾整个实战过程从定位混淆的AES函数到最终提取出密钥其核心思想是利用动态执行的确定性来对抗静态结构的混沌。OLLVM可以改变代码的布局和流程但无法改变算法最终要执行的那些核心运算指令查表、异或等。Stalker正是抓住了这个本质。我个人在多次类似实战中的体会是前期分析投入越多后期追踪越轻松不要一上来就无脑上Stalker。尽可能利用静态分析哪怕只有一点点信息、字符串搜索、高层API挂钩等手段将目标函数的地址范围缩小到极致。一个精确的range参数能节省你99%的数据处理时间。理解算法比理解工具更重要如果你对AES算法的每一步在汇编层面可能如何实现没有概念那么面对一堆ldr和eor指令你将毫无头绪。在动手前找一份清晰的ARM平台AES源码或汇编认真走读一遍脑子里建立起“算法特征”到“指令模式”的映射。迭代式分析与假设验证逆向工程很少能一蹴而就。通常是“猜测-验证-修正”的循环。例如先猜测某个内存区域是T表然后验证追踪到的指令是否频繁访问它猜测某组eor是轮密钥加然后通过修改输入观察输出是否按预期变化来验证。工具链的灵活组合Frida Stalker不是孤立的。在这个案例中我们结合了Memory.scan、Interceptor.attach、Stalker.follow等多种能力。在实际更复杂的场景中可能还需要结合IDA Pro的静态视图、动态调试器如lldb的断点功能甚至自定义的模拟执行如Unicorn Engine来进行混合分析。耐心与记录是关键指令级追踪会产生海量数据分析过程可能枯燥且漫长。务必养成良好的记录习惯对重要的地址、发现的特征、提出的假设进行详细注释。编写一些辅助脚本来自动化搜索和标记可以极大地提升效率。最后这个技术不仅限于破解AES混淆。任何被混淆的核心算法如哈希函数、自定义加密协议、许可证校验逻辑只要其核心运算在CPU指令层面有迹可循都可以尝试用Frida Stalker这条“穿云箭”来洞穿迷雾。掌握它你就拥有了在动态分析领域与最强代码混淆正面交锋的利器。