Unidbg与HookZz实战:动态追踪与逆向魔改SHA1算法

📅 2026/7/26 11:55:39
Unidbg与HookZz实战:动态追踪与逆向魔改SHA1算法
1. 项目概述当Unidbg遇见魔改算法在移动安全逆向分析这个行当里最让人头疼的往往不是那些标准的加密算法而是经过开发者“精心”魔改过的版本。你费尽心思定位到了一个关键函数发现它在调用SHA1满心欢喜地掏出标准库准备模拟结果一跑出来的结果和真机对不上。这时候你就知道你碰上“魔改算法”这块硬骨头了。今天要聊的就是如何利用Unidbg这个强大的动态模拟执行框架结合HookZz这个精巧的Hook工具去深度追踪、逆向并最终复现一个被魔改过的SHA1算法。这不仅仅是调用一个API那么简单而是一场从黑盒到白盒的完整逆向实战。Unidbg允许我们在PC上模拟运行Android或iOS的本地库so文件而无需真机或模拟器这为动态分析提供了极大的便利。HookZz则为我们提供了在Unidbg环境中进行函数级、指令级Hook的能力让我们能够像在调试器中一样观察算法的每一步数据流转。面对一个魔改的SHA1我们的目标很明确不是去硬啃混淆后的汇编代码而是通过动态追踪看清数据在算法中的完整变换过程从而推导出魔改的具体逻辑。这个过程就像给一个密封的黑盒子装上透明的观察窗我们虽然不能直接看到内部的齿轮结构但能看清每一个小球数据进去后是如何被染色、变形、再排列后出来的从而反推出内部的加工规则。2. 核心工具链Unidbg与HookZz的黄金组合2.1 Unidbg跨平台的Native代码模拟器Unidbg的核心价值在于“模拟”而非“解释”。它模拟了ARM或ARM64的CPU指令集、内存管理、系统调用以及关键的JNI函数使得一个为Android编译的so库能够在你的Java或Python程序中“跑起来”。这解决了逆向分析中的一个核心痛点很多关键逻辑和加密算法都藏在so库里静态分析IDA Pro看汇编晦涩难懂动态调试又受限于环境需要root过的真机、对抗反调试等。Unidbg直接把战场拉到了我们熟悉的开发环境里。在实战中我们通常用Unidbg来加载目标so调用指定的JNI函数或Native函数并获取其返回值。对于加密算法我们往往关心输入明文和输出密文。通过构造不同的输入观察输出我们可以初步判断算法的性质。但面对魔改算法仅仅知道输入输出是不够的我们必须窥探中间过程。这就需要Hook。注意Unidbg的版本和配置对稳定性影响很大。建议使用活跃维护的Fork版本并仔细配置虚拟机参数如内存、CPU架构。不同的so库可能依赖特定的系统属性或文件需要在Unidbg环境中通过resolver进行合理的补环境操作否则可能导致so加载失败或函数执行异常。2.2 HookZz精准的指令级钩子框架HookZz是一个基于动态二进制插桩DBI思想的Hook框架它可以实现在目标指令执行前或执行后插入我们的回调函数。在Unidbg的语境下HookZz让我们能够Hook so库中任意地址的指令。这对于追踪算法流程至关重要。想象一下SHA1算法的标准流程初始化缓冲区 - 对输入数据进行填充 - 以512位数据块为单位进行80轮的主循环运算每轮更新5个状态变量A, B, C, D, E。魔改可能发生在任何环节可能是初始常量IV被替换了可能是填充规则变了可能是每轮运算中使用的非线性函数或常量被修改了甚至可能是在标准流程前后增加了额外的变换步骤。如果没有Hook我们只能看到输入和最终的输出中间80轮、每轮数十次运算全部是黑盒。有了HookZz我们可以Hook算法入口函数记录原始输入。Hook内存读写指令追踪那5个核心状态变量A, B, C, D, E在每一轮之后的值是如何变化的。标准SHA1的每一轮运算都会更新这些变量魔改的逻辑就藏在这些更新值的偏差里。Hook关键的逻辑判断或循环指令了解算法的控制流判断是否增加了额外的轮次或步骤。通过在这些关键点埋下“探针”我们就能得到一份算法执行的“心电图”所有数据的流动和变化都清晰可见。接下来就是对比这份“心电图”和标准SHA1的“心电图”找出所有异常的心跳魔改点。3. 实战部署搭建追踪环境与定位目标3.1 环境准备与Unidbg项目初始化首先你需要一个Java开发环境Maven或Gradle项目。将Unidbg的依赖添加到你的项目中。这里以一个简化的Maven依赖示例和核心代码结构为例// 示例创建一个简单的Unidbg模拟环境 public class ModifiedSHA1Tracer { public static void main(String[] args) { // 1. 创建Android模拟器 (这里以32位ARM为例) AndroidEmulator emulator new AndroidARMEmulator(com.example.target); Memory memory emulator.getMemory(); memory.setLibraryResolver(new AndroidResolver(23)); // API Level 23 // 2. 加载目标so库 Module module emulator.loadLibrary(new File(target_lib.so)); // 3. 找到目标函数 // 通常你需要通过导出符号或特征码来定位SHA1相关的函数。 // 假设我们已知函数符号为Java_com_example_app_NativeHelper_calculateSHA1 Number funcAddress module.findSymbolByName(Java_com_example_app_NativeHelper_calculateSHA1).getAddress(); System.out.println(Target Function Address: 0x Long.toHexString(funcAddress.longValue())); // 后续步骤将在此地址上使用HookZz进行Hook } }定位目标函数是第一步也是最需要经验的一步。如果so没有剥离符号你可以直接通过JNI_OnLoad或类似Java_开头的符号找到加密函数。如果符号被剥离你就需要通过静态分析IDA寻找特征比如标准SHA1初始化常量0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0在so数据段中的出现或者通过交叉引用找到使用这些常量的函数。3.2 使用HookZz植入关键探针HookZz在Unidbg中通常通过HookZzInstrument来使用。我们需要在关键地址上安装Hook。我们的策略是分层级的第一层函数入口Hook。记录传入的明文数据通常是JNI的jbyteArray参数。Instrumentation instrumentation new HookZzInstrument(emulator); emulator.getInstrumentation().addListener(instrumentation); // Hook函数入口 instrumentation.attach(funcAddress, new EnterCallback() { Override public void onEnter(Emulator? emulator, long address, Object[] args) { // args[1] 可能是代表明文的jobject (jbyteArray) Pointer inputPtr emulator.getObject(args[1]); byte[] inputData inputPtr.getByteArray(0, inputPtr.getSize()); System.out.println([ENTRY] Input Data (Hex): bytesToHex(inputData)); } });第二层内存写操作Hook关键。这是追踪状态变量的核心。我们需要找到算法中更新那5个状态变量假设存储在某个内存区域或寄存器的指令。通过静态分析你可能发现一个循环结构在循环体内有连续的5次STR存储指令到一片连续的内存地址。Hook这片内存区域的写操作。// 假设通过分析我们知道状态变量存储在基地址为 stateBase 的5个4字节整数上 long stateBase 0xCF00; // 示例地址 for (int i 0; i 5; i) { final int index i; instrumentation.attachWrite(stateBase i * 4, 4, new WriteCallback() { // 监控4字节写操作 Override public void onWrite(Emulator? emulator, long address, int size, long value) { // value 是写入的新值 System.out.printf([WRITE] State[%d] updated to: 0x%08X%n, index, value); // 我们可以记录下每一轮更新后的完整状态 [A, B, C, D, E] } }); }第三层循环计数器Hook。为了将状态更新与轮次对应起来最好能Hook循环计数器。找到控制80轮循环的指令比如一个递减并跳转的指令SUBSBNEHook它以便在每轮开始时或结束时打印轮次。instrumentation.attach(loopControlAddress, new EnterCallback() { int round 0; Override public void onEnter(Emulator? emulator, long address, Object[] args) { round; System.out.println([LOOP] Round: round); } });通过这三层Hook当你在Unidbg中调用目标函数时控制台会输出一份详细的执行日志输入数据、每一轮循环的编号、以及每一轮结束后5个状态变量的新值。4. 数据采集与魔改逻辑分析4.1 采集标准SHA1的参照数据在分析魔改算法之前你必须有一个“标尺”。你需要用同样的输入运行一个标准的、已知正确的SHA1算法比如用Java的MessageDigest.getInstance(SHA-1)并记录下它在每一轮之后的状态变量值。这需要你修改一个开源的标准SHA1实现例如Bouncy Castle的源码在它的轮函数内部插入日志代码输出每一轮后的A,B,C,D,E。得到两份日志一份来自标准SHA1我们的“标尺”一份来自被Hook的、魔改的so。将它们并排对比。4.2 对比分析与魔改点定位对比分析是核心的逆向思维过程。你可能会发现以下几种典型的魔改模式初始值IV魔改如果从第一轮开始状态变量的值就和标准值对不上但在后续轮次中每一轮的状态变化规律即本轮新状态与上一轮状态及输入数据块的关系却和标准算法一致那么魔改很可能只是替换了初始的5个魔术常量。这是最简单的魔改。轮常量K魔改SHA1的80轮中每20轮使用一个不同的常量Kt0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xCA62C1D6。如果你发现状态值在某个20轮区间开始出现系统性偏差但计算模式相同那么可能是这20轮所用的Kt被改成了别的值。非线性函数F魔改SHA1每20轮使用一个不同的布尔函数F。如果状态变化规律在20轮边界处发生了与标准算法不同的偏差可能是F函数被替换了。运算顺序或位运算魔改这是更复杂的魔改。可能是在标准的轮函数A (B leftrotate 5) F E K W[t]之外增加了额外的异或、加减或循环移位操作。这需要你仔细比对每一轮的计算结果。例如你发现魔改算法的A_new总是等于标准算法的A_new ^ 0x12345678那么这个异或操作就是魔改点。前后处理魔改可能在标准SHA1计算完成后对最终的160位5个状态变量结果再进行一次变换比如整体字节序反转、与一个固定值异或、或者再进行一次自定义的哈希。为了高效对比我通常会将日志导入到Excel或编写Python脚本进行差分分析。脚本会逐轮比较5个状态值一旦发现差异就高亮显示并尝试计算差异值的规律是固定的吗是否与轮次相关是否与输入数据相关。实操心得不要试图一次性理解所有差异。先从最大的、最明显的差异入手比如第一轮的结果就不同那就先聚焦初始化阶段。如果前面几十轮都一样从第60轮开始不同那就重点分析第60轮附近的逻辑可能是进入了使用不同Kt或F函数的阶段或者从这里开始插入了额外代码。分而治之是逆向复杂逻辑的不二法门。5. 算法复现与验证5.1 基于分析结果修改标准实现通过对比分析你假设出了魔改的规则。例如你发现魔改算法只是把初始的5个常量[A, B, C, D, E]从[0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]改成了[0x01234567, 0x89ABCDEF, 0xFEDCBA98, 0x76543210, 0xF0E1D2C3]注意这里只是举例实际魔改不会这么简单。那么你就基于一个标准的、易于修改的SHA1实现比如Python的hashlib源码或一个清晰的C语言实现找到初始化部分将常量替换掉。如果魔改涉及轮常量Kt就找到定义Kt数组的地方进行替换。如果魔改了非线性函数F就重写对应的函数。如果增加了额外的运算就在轮函数的对应位置插入你的代码。5.2 构造多组测试向量进行验证复现之后绝不能只用一个测试用例就宣告成功。你需要构造一个全面的测试集边界案例空输入、极短输入1字节、长度刚好超过一个块56字节因为SHA1填充后是64字节一个块、长度很长的输入。随机案例生成几十组随机长度的随机字节作为输入。针对性案例如果怀疑魔改与输入数据的某些位有关可以构造特定的模式如全0、全1、0xAA、0x55等。对于每一组测试向量分别运行原始目标so通过你的Unidbg Hook脚本调用并记录最终输出。你复现的算法程序。比较两者的输出是否完全一致。如果所有测试用例都通过那么恭喜你你已经成功逆向并复现了这个魔改SHA1算法。5.3 常见问题与排查技巧实录在实际操作中你几乎一定会遇到各种问题。下面是一个常见问题速查表问题现象可能原因排查思路与解决方案Unidbg加载so失败报错找不到依赖库so依赖了其他系统库或自定义库。使用memory.setLibraryResolver添加更多解析器或使用AndroidResolver自动处理常见系统库。对于自定义库需要将其一并加载。Hook的函数没有被调用函数地址定位错误函数被混淆或间接调用。1. 确认符号名或特征地址正确。2. 尝试Hook其调用者上层函数。3. 使用instruction tracing模式追踪代码执行流看是否跳过了目标函数。Hook后程序崩溃或行为异常Hook破坏了寄存器状态或栈平衡Hook点选择在了关键指令中间。1. 在Hook回调中务必小心处理寄存器使用emulator.getBackend().reg_read和reg_write时确保恢复原状。2. 尽量选择在函数入口第一条指令或函数出口返回指令前进行Hook。3. 对于指令级Hook确保Hook的是完整指令的边界。状态变量更新日志混乱无法与轮次对应写操作Hook的地址不准确有多个地方更新同一片内存。1. 静态分析更仔细确认状态变量的存储位置。可能是一个全局数组也可能在堆栈上。2. 在Hook写操作时同时打印调用栈Thread.currentThread().getStackTrace()区分不同上下文的写入。复现算法的结果与so输出大部分一致但偶尔不同魔改逻辑可能依赖于某些动态因素如时间戳、设备ID被硬编码在so中。1. 检查so中是否有访问/dev/urandom、gettimeofday或读取特定文件/proc信息的代码。2. 在Unidbg中Hook这些系统调用记录并模拟其返回值确保执行环境一致。对比发现差异毫无规律像是随机扰动算法可能使用了反调试或代码自修改技术。1. 检查so是否在运行时解密了部分代码段常见于加固so。2. 尝试在Unidbg中完整dump出解密后的内存代码段并基于此进行分析和Hook。独家避坑技巧在开始深度Hook之前先做一个“烟雾测试”。用几组简单的输入调用目标函数只Hook入口和出口确认Unidbg环境能正确运行并得到输出。然后写一个最简单的脚本比较so的输出和标准SHA1的输出确认它们确实不同确实是魔改的。这个简单的验证可以避免你在一个错误的方向比如so根本没被调用或者算法根本不是SHA1系上浪费大量时间。6. 从追踪到通用化构建算法识别模式库完成一次具体的魔改SHA1逆向后你的收获不应该只是一个能输出正确结果的脚本。更宝贵的是形成一套方法论和模式识别能力。你可以开始建立自己的“魔改算法特征库”特征一常量替换。记录下常见的被替换的IV和Kt值。有些开发团队会使用公司名、项目名的哈希值作为魔改常量。特征二填充规则变异。标准的SHA1填充是比特1后接一堆0最后64位是消息长度。有些魔改会改变这个规则比如先补0x80再补0x00或者长度编码使用大端序等。这可以通过Hook内存中填充后的完整消息块来发现。特征三附加变换。记录下那些在标准轮函数前后增加的常见操作比如对状态变量进行额外的循环移位、与轮次相关的常量进行加减等。当下次遇到另一个魔改哈希算法可能是MD5、SHA256的变种时你可以快速套用这套Hook追踪流程。首先Hook并对比初始状态判断是否为常量替换然后对比第一轮运算后的状态判断主逻辑是否被修改最后对比最终输出前的处理。这样你的分析效率会呈指数级提升。这个从具体实战中抽象出通用模式的过程正是逆向工程师从“工匠”走向“专家”的关键。工具Unidbg、HookZz是手臂而方法论和经验积累的大脑。每一次对魔改算法的成功逆向不仅解决了一个具体问题更是为你的大脑神经网络添加了一个强大的识别模式节点。