1. 项目概述与核心价值

最近在分析一些主流电商App的通信安全机制时,我花了相当多的时间在“某A系电商App”的 x-sign 签名参数上。这个参数对于从事移动安全研究、风控策略分析或者单纯想理解大型应用如何保护其API接口的开发者来说,都是一个非常经典的案例。它不像一些简单的 MD5 或固定盐值的 HMAC ,而是采用了一套动态加载、运行时生成的复杂机制,直接硬怼静态分析往往无功而返。简单来说, x-sign 是App在发起网络请求时,为了验证请求的合法性与完整性,由客户端生成并附加在请求头或参数中的一个加密字符串。服务器端会用同样的逻辑进行验签,如果对不上,请求就直接被拒了。破解它,不仅能理解其风控逻辑,对编写合规的自动化测试脚本、进行深度的数据采集分析(请注意合规边界)也有很大帮助。

这次实战的目标,就是彻底拆解这个 x-sign 的生成过程。重点不在于“找到那个算法”,而在于理解它“如何被隐藏和保护”——也就是标题中的“动态加载机制”。我们会从抓包确认签名参数开始,一路深入到App的二进制世界,分析其如何通过动态加载技术(如 DexClassLoader Native SO 库加载)来保护核心的签名逻辑,并最终还原其生成流程。整个过程中,我会穿插大量我在实际逆向中踩过的坑和总结的技巧,希望能给你带来一次沉浸式的逆向工程体验。

2. 逆向环境准备与初步侦查

工欲善其事,必先利其器。逆向分析,尤其是针对加固和混淆比较严重的商业App,一个稳定、高效的环境是成功的一半。

2.1 工具链选型与配置

我的主力分析环境是一台 x86_64 架构的 Ubuntu 22.04 虚拟机,搭配 Android 9.0 的模拟器( Android Studio 自带的 AVD )。选择 Android 9 是因为它兼容性较好,且对 root 和调试的支持相对完善。手机我备了一台已 root Pixel 3 Android 10 ),用于验证一些在模拟器上可能受限的操作(如 Magisk 模块注入)。

核心工具清单:

  1. 抓包与调试

    • Charles / Fiddler :用于初始的HTTP/HTTPS流量抓取,确认 x-sign 参数的存在和位置。我更喜欢 Charles ,因为它的 Rewrite Map Local 功能在后续的算法模拟测试中非常有用。
    • Burp Suite :更专业的Web安全测试工具,用于深度拦截、重放和篡改请求,观察 x-sign 对请求变化的敏感性。
    • adb (Android Debug Bridge) :必备,用于安装应用、拉取文件、端口转发和 shell 交互。
  2. 静态分析

    • Jadx / JEB :反编译 APK ,查看 Java/Kotlin 代码。 Jadx 开源免费,搜索和跳转速度快,适合快速浏览。 JEB 商业软件,反编译质量更高,对混淆代码的解析能力更强,我通常用 Jadx 初筛,复杂逻辑用 JEB 深究。
    • IDA Pro / Ghidra :分析 Native 层( .so 库)的利器。 IDA 交互和插件生态好, Ghidra 免费且反编译能力惊人。我主要用 IDA 进行动态调试,用 Ghidra 做静态的交叉参考和反编译。
    • Apktool :用于解包 APK ,获取 Dex 、资源文件、 AndroidManifest.xml 等。
  3. 动态分析

    • Frida 本次逆向的灵魂工具 。它是一个动态插桩框架,可以在运行时注入 JavaScript 代码来拦截、修改函数调用,打印参数和返回值。对于动态加载的代码, Frida 几乎是唯一高效的追踪手段。
    • Objection :基于 Frida 的命令行工具,可以快速完成内存搜索、 SSL Pinning 绕过等常见任务。
    • Xposed :另一种强大的运行时劫持框架,需要 root 并安装框架模块。它更稳定,但不如 Frida 灵活和即时。在本案例中, Frida 是首选。
  4. 辅助工具

    • 模拟器/真机 :必须 root 或使用可调试的镜像。
    • Magisk root 管理工具,可以安装 Riru LSPosed 等模块来增强系统能力。
    • JustTrustMe / SSLUnpinning :用于绕过应用的 SSL证书绑定 ,让抓包工具能解密 HTTPS 流量。这是抓取 x-sign 的前提。

注意 :所有分析应仅用于安全研究、学习目的,并在自己拥有合法权限的应用上进行。切勿对他人资产或服务进行未授权的测试。

2.2 目标确认与抓包初探

首先,从正规渠道安装目标App。启动 Charles 并配置好代理,在手机上安装 Charles 的根证书并配置代理。为了绕过 SSL Pinning ,我在 root 后的手机上安装了 Magisk 模块 “SSLUnpinning” (具体模块名可能随时间变化,原理都是劫持证书验证函数)。

打开App,进行一些常规操作,比如浏览商品、搜索关键词。在 Charles 中观察发出的请求。很快就能发现,关键的API请求(如搜索接口、商品详情接口)的 URL 或请求头中,包含一个名为 x-sign 的参数,其值是一长串看似随机的十六进制或 Base64 字符串。

初步观察要点:

  • 位置 x-sign 可能在 Header 里,也可能在 POST Body GET Query 参数中。
  • 变化性 :对同一个请求重复发送, x-sign 每次都会变吗?还是在一定时间内(或参数不变时)固定?实测发现,该App的 x-sign 每次请求都不同 ,即使请求参数完全一样。这说明签名算法里很可能引入了时间戳或随机数。
  • 关联性 :尝试修改请求中的一个参数(如搜索关键词), x-sign 会彻底改变。这说明它是对 全部或部分请求数据 进行了签名。

至此,我们确认了目标,并知道它是一个动态变化的签名。下一步就是打开 APK ,看看代码里有没有明显的线索。

3. 静态分析:寻找签名逻辑的蛛丝马迹

Apktool 解包 APK ,然后用 Jadx 打开主要的 Dex 文件。第一步通常是全局搜索字符串 “x-sign”

3.1 代码搜索与初步定位

搜索结果显示,代码中直接出现 “x-sign” 字符串的地方很少,而且大多是在网络库的拦截器( Interceptor )或工具类中,用于设置请求头。例如,你可能会找到一个名为 SignInterceptor SecurityUtils 的类。点进去看,核心的签名计算方法往往是调用另一个方法,比如:

String xSign = SecurityHelper.generateSign(url, params, timestamp, ...);
requestBuilder.header("x-sign", xSign);

跟进去 SecurityHelper.generateSign ,发现它可能只是一个外壳,内部又调用了 Native 方法( native 关键字)或者通过反射加载了某个类。 这是第一个重要信号:核心逻辑可能不在主 Dex 中。

3.2 动态加载线索挖掘

Jadx 中搜索关键词如 “DexClassLoader” “PathClassLoader” “loadLibrary” “System.load” “assets” “/data/data/包名/” 。很快,你会发现一些有趣的代码片段:

  1. Asset资源解密加载 :可能有一个 AssetManager 在读取 assets 目录下的一个加密文件(如 sign.dat , security.jar ),然后在内存中解密,最后通过 DexClassLoader 加载。
  2. 网络下载加载 :App启动后,可能从一个固定的 URL 下载一个 JAR DEX 文件,保存到应用私有目录,再动态加载。这需要抓包观察启动时的流量。
  3. Native层加载 :大量的 System.loadLibrary(“security”) 调用,意味着算法实现在 libsecurity.so 这样的原生库里。 .so 库文件可以在 apk lib/ 目录下找到,也可能被加密后放在 assets ,运行时解密释放。

在该 A 系电商App的案例中,我通过静态分析结合字符串交叉引用,发现了一个关键类。这个类在初始化时,会从 assets 读取一个加密的 JAR 文件,使用一个硬编码在代码里的 AES 密钥进行解密,然后将解密后的 DEX 文件写入 /data/data/包名/cache/ 目录,最后创建一个 DexClassLoader 来加载它。

实操心得 :遇到字符串被混淆的情况(如变成 a.a.a.a ),不要慌。关注方法调用关系。如果一个方法里出现了 “DexClassLoader” “loadClass” “getMethod” 这些关键词,那它八九不离十就是动态加载的入口。同时,注意 assets res/raw 目录下是否有大小异常、后缀名奇怪的文件。

3.3 Native层分析入口定位

如果静态分析 Java 层只找到 native 方法声明,那么战场就要转移到 Native 层。用 Apktool 解包后,在 lib/ 目录下找到对应的 .so 文件(可能有 armeabi-v7a , arm64-v8a 等多个版本)。用 IDA Pro Ghidra 打开 arm64-v8a 版本(兼容性好)。

在导出函数表中搜索 Java 层对应的 native 方法名。 JNI 函数名格式通常为 Java_包名_类名_方法名 。由于包名和类名可能被混淆,你可以先在 Jadx 里找到那个 native 方法所在的类的完整路径,然后拼接出可能的函数名进行搜索。如果找不到,可以查看 JNI_OnLoad 函数,这里通常会有动态注册函数的逻辑。

找到对应的 Native 函数后,反编译它。你会看到它可能在做以下几件事:

  1. Java 层接收参数(字符串、字节数组等)。
  2. 调用其他 C/C++ 函数进行复杂的加密运算(可能是自定义算法,也可能是标准算法如 HMAC-SHA256 但密钥是动态获取的)。
  3. 将计算结果返回给 Java 层。

难点在于 :算法可能被 OLLVM 等工具进行了控制流扁平化、指令替换等混淆,导致反编译的代码逻辑支离破碎,难以阅读。

4. 动态分析:用Frida撬开运行时黑盒

当静态分析走入死胡同,动态分析就是破局的钥匙。我们的策略是: Frida 挂钩关键节点,监视输入输出,逐步逼近核心算法。

4.1 Frida脚本编写基础

首先在电脑上安装 Frida frida-tools ,在手机上安装 frida-server (版本需与电脑端匹配)。启动 frida-server 后,使用 frida -U -f com.target.app 命令附加到目标App。

我们的 JavaScript 挂钩脚本主要使用 Interceptor 模块。一个典型的挂钩 Java 方法的脚本如下:

Java.perform(function () {
    // 找到目标类,注意混淆后的类名
    var TargetClass = Java.use("com.xxx.xxx.SecurityHelper");
    // 挂钩目标方法
    TargetClass.generateSign.implementation = function (url, params, timestamp) {
        console.log("[*] generateSign called!");
        console.log("    url: " + url);
        console.log("    params: " + params);
        console.log("    timestamp: " + timestamp);
        // 调用原方法获取结果
        var result = this.generateSign(url, params, timestamp);
        console.log("    result: " + result);
        // 将结果返回
        return result;
    };
});

4.2 挂钩动态加载的类

对于动态加载的类,直接使用 Java.use 可能找不到,因为它在原始的 ClassLoader 里不存在。我们需要先获取到加载了这些类的 DexClassLoader 实例,或者更简单——挂钩 ClassLoader loadClass 方法。

Java.perform(function () {
    // 挂钩系统ClassLoader的loadClass方法
    var ClassLoader = Java.use("java.lang.ClassLoader");
    ClassLoader.loadClass.overload('java.lang.String').implementation = function (className) {
        // 过滤出我们关心的、动态加载的类名(可能包含特定关键字)
        if (className.indexOf("security") !== -1 || className.indexOf("sign") !== -1) {
            console.log("[*] Loading dynamic class: " + className);
            // 打印调用栈,看看是谁在加载这个类
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));
        }
        // 继续原流程
        return this.loadClass(className);
    };
});

通过这种方式,当动态 JAR 被加载时,我们能捕获到被加载的类名。记下这些类名(例如 com.sec.dynamic.SignCore ),然后就可以用 Java.use 去挂钩它们里面的具体方法了。

4.3 挂钩Native函数

如果核心逻辑在 .so 里,我们需要挂钩 Native 函数。首先用 Module.findExportByName 找到函数地址。

Java.perform(function () {
    // 假设我们知道so库名和函数符号
    var moduleName = "libsecurity.so";
    var funcName = "Java_com_xxx_xxx_SecurityHelper_generateSignNative"; // 或函数在so内的偏移地址
    var funcPtr = Module.findExportByName(moduleName, funcName);
    
    if (funcPtr) {
        console.log("[*] Found target function at: " + funcPtr);
        Interceptor.attach(funcPtr, {
            onEnter: function (args) {
                // args[0]是JNIEnv*, args[1]是jobject, args[2]开始是Java传入的参数
                // 打印或处理参数
                console.log("[*] Native generateSign called.");
                // 可以将参数转换为字符串打印,这里需要根据JNI类型手动解析,比较复杂
            },
            onLeave: function (retval) {
                // retval是返回值
                console.log("[*] Native function returned.");
                // 打印返回值,同样需要根据类型解析
            }
        });
    } else {
        console.log("[-] Function not found!");
        // 尝试枚举so的所有导出函数
        Module.enumerateExports(moduleName).forEach(function (exp) {
            console.log(exp.name + " at " + exp.address.toString());
        });
    }
});

更高级的技巧 :如果函数没有被导出(静态分析发现是内部函数),或者被混淆了,我们可以挂钩 JNI_OnLoad ,或者通过 Module.findBaseAddress 加上在 IDA 中分析出的偏移量来定位函数地址。甚至可以使用 Frida Stalker 功能追踪代码执行流程,但这对性能和技巧要求很高。

4.4 追踪数据流与算法还原

通过挂钩 Java 层和 Native 层的多个关键函数,我们可以像调试一样,打印出每一步的输入和输出。例如:

  1. 挂钩网络框架的拦截器,拿到原始的请求参数( URL Query Body Headers )。
  2. 挂钩签名入口方法,看到这些参数被传递进去。
  3. 挂钩内部的数据处理函数(如参数排序、拼接、 UTF-8 编码)。
  4. 挂钩加密函数(如 MessageDigest.getInstance(“SHA-256”) Cipher.getInstance(“AES/ECB/PKCS5Padding”) ),获取密钥和加密结果。

在这个过程中,我总结了几条关键经验:

  • 顺序很重要 :从外到内,层层挂钩。先找到设置 x-sign 头的地方,再逆向找到生成它的方法。
  • 关注上下文 :除了参数,还要打印调用栈( Thread.backtrace )。这能帮你理解函数的调用链,发现意想不到的调用者。
  • 对比多次调用 :发起两个仅有细微差别的请求(比如改一个参数值),对比 Frida 打印的日志,看数据在哪个处理环节开始产生差异。这能快速定位到签名算法的核心计算部分。
  • 密钥从哪里来 :密钥往往是动态获取的。它可能来自:
    • 一个固定的字符串,但被拆分成多段隐藏在代码的不同位置。
    • 对设备 IMEI Android ID 等设备信息进行某种变换。
    • 从服务器下发的令牌( token ),每次会话可能不同。
    • 通过 Native 层从硬件或系统特定区域读取。 需要挂钩所有可能的密钥来源函数。

5. 核心机制解析:动态加载与算法保护策略

通过动静结合的分析,我最终梳理出了该 A 系电商App x-sign 的大致保护策略,这是一个典型的“多层防御,动态混淆”的架构。

5.1 第一层:代码分离与动态加载

APK 中的签名逻辑只是一个空壳。真正的签名算法被编译在一个独立的 JAR 文件中,该文件使用 AES-CBC 模式加密后,存放在 assets 目录下。 App 启动或首次需要使用签名时,会从 assets 读取密文,利用硬编码在 Java 代码(可能经过简单变换)中的密钥进行解密,将解密后的 DEX/JAR 文件写入应用缓存目录。随后,使用一个自定义的 DexClassLoader (其父加载器为应用自身的 ClassLoader )来加载这个 DEX 文件,并通过反射调用其中的入口类和方法。

这样做的好处:

  • 增加静态分析难度 :反编译主 APK 看不到核心逻辑。
  • 便于更新 :可以通过热更新替换 assets 中的加密 JAR ,从而更换签名算法,而无需发布新版本 APK
  • 对抗自动化 :一些简单的脱壳工具可能无法处理这种自定义的动态加载。

5.2 第二层:Native层加固与白盒加密

动态加载的 Java 代码本身可能也不是算法的终点。在跟踪中发现,这个动态 JAR 里的 Java 方法,最终又通过 JNI 调用了一个或多个 Native 方法(在 libsecurity.so 中)。核心的哈希计算、加密操作都在 Native 层完成。

libsecurity.so 本身也经过了加固:

  • 符号表剥离 :导出函数表中找不到有意义的函数名。
  • 控制流混淆 :使用 OLLVM 等工具进行了混淆, IDA 反编译出的代码充满了巨大的 switch-case 和不透明的谓词,逻辑难以直接阅读。
  • 字符串加密 .so 文件中的明文字符串(如算法名称 “SHA256” 、密钥常量)都被加密,运行时动态解密。
  • 反调试 :可能内置了 ptrace 检测、时间差检测等简单的反调试手段。

5.3 第三层:算法本身的复杂性

即使拿到了算法逻辑,其本身也具备一定复杂度:

  1. 参数规范化 :并非对所有请求参数签名。它会先对 URL GET 参数、 POST Form JSON Body 进行规范化处理,包括按字典序排序、去除空值、统一编码等。
  2. 拼接盐值 :将规范化后的参数字符串,与多个“盐值”( Salt )进行拼接。这些盐值可能包括:
    • 一个固定的 App 密钥(隐藏在 Native 层)。
    • 当前时间戳(精确到秒或毫秒)。
    • 一个来自服务器的随机数(可能藏在某个 Cookie 或响应头里)。
    • 设备指纹的哈希值。
  3. 多层哈希/加密 :拼接后的字符串可能先进行一次 MD5 ,结果再和另一个盐值拼接,然后进行 HMAC-SHA256 。最终结果可能还会进行一次 Base64 或十六进制编码。
  4. 密钥分散 :主密钥可能不直接使用,而是通过一个分散算法,结合请求的 URL 路径或其他参数,生成当次请求使用的临时密钥。

5.4 第四层:环境检测与风控联动

在生成签名的前后,代码可能会执行一些环境检测:

  • Root/模拟器检测 :如果检测到异常环境,可能返回一个错误的签名,导致服务器端拒绝服务,或者触发更严厉的风控策略。
  • 调试器检测 :如果发现被调试,可能进入死循环或崩溃。
  • Hook检测 :可能会检查 Java 层某些关键类(如 java.lang.ClassLoader )的方法是否被 Xposed Frida 等工具挂钩。

这些检测不一定直接阻止签名生成,但可能使生成的签名无效,从而让逆向者误以为算法分析错误。

6. 算法复现与验证

理解了原理,下一步就是用 Python JavaScript 等语言复现这个签名算法。这不是简单的抄代码,而是一个验证和细化的过程。

6.1 数据采集与参数映射

首先,你需要收集足够多的样本数据。使用 Frida 脚本,在挂钩了最终生成签名的方法后,同时打印出该方法的所有输入参数(已处理好的待签名字符串、密钥、时间戳等)和输出的签名。收集10-20组不同请求(不同接口、不同参数)的数据。

制作一个表格,记录每次请求的:

  • 原始请求( URL , Method , Headers , Body
  • 通过 Frida 捕获的、传递给核心签名函数的 所有输入参数
  • 最终的 x-sign 值。

6.2 逐步复现与差分测试

根据动态分析推断出的算法步骤,开始编写复现代码。

  1. 参数规范化 :对比样本,验证你的规范化逻辑(排序、编码、拼接格式)是否与 App 一致。可以写一个测试,用你的规范化函数处理原始请求,看结果是否与 Frida 捕获的“待签名字符串”一致。
  2. 盐值拼接 :确认所有盐值及其拼接顺序。时间戳和随机数需要动态获取,固定密钥需要从 Native 层或代码中提取出来。
  3. 哈希/加密 :使用正确的算法( SHA256 , HMAC 等)和编码( Hex , Base64 )。注意 HMAC 的密钥是字节数组,不是字符串。
  4. 最终编码 :核对最终的编码格式。

差分测试法 :固定除一个变量外的所有输入(比如固定密钥、时间戳,只改变一个请求参数),分别用 App 和你的复现代码计算签名。如果两者产生的签名变化规律一致(比如都完全改变了),说明你的算法在这个环节很可能是正确的。如果不一致,就要仔细检查差异点。

6.3 处理动态因子

算法中最麻烦的是动态因子,如时间戳和服务器下发的随机数。

  • 时间戳 :需要与服务器时间同步。 App 可能使用服务器返回的时间,也可能使用本地时间。可以通过对比签名中的时间戳和请求发生的时间来推断。复现时,需要模拟相同的时间戳获取逻辑。
  • 服务器随机数 :这个值通常包含在某个先前请求的服务器响应中(比如登录接口返回的 token 里可能嵌了一个 nonce )。你需要分析会话流程,找到这个值的来源,并在复现时模拟相同的获取和传递过程。

6.4 验证与调试

编写一个简单的测试脚本,模拟 App 发送请求。使用 Python requests 库,按照 App 的格式组装 Headers Body ,其中 x-sign 使用你的复现算法生成。发送请求,观察服务器响应。

  • 成功 :返回正常数据( HTTP 200 且有业务数据)。恭喜你,算法复现基本成功。
  • 失败 :返回签名错误( HTTP 403 或特定的错误码)。需要重新检查:
    • 是否漏掉了某个请求头或参数?有些 App 会把 User-Agent 、设备ID等也纳入签名。
    • 时间戳的精度问题?(秒 vs 毫秒)
    • 字符串编码问题?( UTF-8 vs GBK
    • 盐值的值或顺序不对?
    • 服务器随机数已经过期?

避坑技巧 :在复现算法时,尽量使用 Frida App 运行时 dump 出关键中间变量(如拼接后的字符串、密钥字节数组、哈希中间结果),与你本地复现的每一步结果进行 逐字节对比 。这是最直接有效的调试方法。

7. 常见问题与排查技巧实录

在逆向 x-sign 这类动态签名机制时,你会遇到各种各样的问题。下面是我记录的一些典型问题及解决思路。

7.1 抓包看不到HTTPS流量/证书错误

  • 问题 :配置好代理后, App 无法联网或 Charles 里看到全是 TLS 握手失败的 CONNECT 请求。
  • 原因 App 启用了 SSL Pinning (证书绑定)。
  • 解决
    1. Xposed/EdXposed/LSPosed + JustTrustMe :在已 root 且安装了 Xposed 框架的设备上,安装 JustTrustMe 模块。这是最方便的方法。
    2. Frida脚本绕过 :使用 Frida 脚本挂钩证书验证相关的 Java 方法(如 TrustManager CertificatePinner )或 Native SSL_CTX_set_cert_verify_callback 函数,使其直接返回成功。 Objection 工具的 android sslpinning disable 命令可以自动完成。
    3. 修改APK :反编译 APK ,找到证书绑定的代码(搜索 CertificatePinner TrustManager ),将其注释或修改,然后重打包签名安装。过程繁琐,且可能触发其他签名校验。

7.2 Frida附加失败或进程崩溃

  • 问题 frida -U -f com.xxx 命令执行后, App 闪退或 Frida 无法附加。
  • 原因 App 有反调试/反 Frida 检测。
  • 解决
    1. 检查frida-server :确保手机上的 frida-server 版本与电脑 frida 版本一致,且已正确运行( adb shell ps | grep frida 能看到进程)。
    2. 更换端口 :默认端口 27042 可能被检测。启动 frida-server 时指定其他端口: ./frida-server -l 0.0.0.0:8080 ,连接时用 frida -H 手机IP:8080 -f com.xxx
    3. 隐藏Frida特征 :重命名 frida-server 二进制文件,修改其通信端口和特征字符串。有现成的工具如 frida-server patch 版或 objection patch 功能可以尝试。
    4. 绕过检测 :编写 Frida 脚本,在 App 启动早期就挂钩反调试检测函数(如 android.os.Debug.isDebuggerConnected() syscall 调用等),使其返回 false 。这需要先静态分析找到检测点。
    5. 使用更隐蔽的模式 :尝试使用 Spawn 模式( -f )而不是 Attach 模式,有时 Attach 更容易被检测。

7.3 动态加载的类找不到

  • 问题 :知道动态 JAR 被加载了,但在 Frida 中用 Java.use(“com.sec.dynamic.SignCore”) 时报错 ClassNotFoundException
  • 原因 Frida 默认使用的是 Java 层的 ClassLoader ,可能不是加载那个动态类的 ClassLoader
  • 解决
    1. 枚举所有ClassLoader :在挂钩 loadClass 时,不仅打印类名,也打印 this 对象(即调用者的 ClassLoader )。找到加载目标类的那个 ClassLoader 实例。
    2. 使用Java.choose Java.choose 可以在堆上查找已存在的对象实例。你可以先找到那个 DexClassLoader 的实例。
    Java.perform(function () {
        Java.choose("dalvik.system.DexClassLoader", {
            onMatch: function(instance) {
                // 检查instance的路径是否包含我们关心的动态jar路径
                var path = instance.pathList.dexElements[0].dexFile.mCookie;
                console.log("Found DexClassLoader: " + instance);
                // 用这个instance去loadClass
                var dynamicClass = instance.loadClass("com.sec.dynamic.SignCore");
                Java.use(dynamicClass).someMethod.implementation = ...
            },
            onComplete: function() {}
        });
    });
    
    1. 从已知类获取 :如果动态类被某个已知的类(如入口类)引用,可以先 Java.use 那个已知类,然后通过它的类对象获取 ClassLoader

7.4 Native函数地址无法定位

  • 问题 :在 IDA 里看到了函数,但用 Module.findExportByName 找不到。
  • 原因 :函数不是导出函数( local symbol ),或者符号被剥离了。
  • 解决
    1. 使用偏移地址 :在 IDA 中查看函数的相对偏移( Offset )。假设 libsecurity.so 的基地址是 0x7a12340000 ,函数在 0x7a12345678 ,那么偏移就是 0x5678 。在 Frida 中:
    var moduleBase = Module.findBaseAddress("libsecurity.so");
    var funcPtr = moduleBase.add(0x5678);
    Interceptor.attach(funcPtr, ...);
    
    1. 特征码搜索 :如果函数有独特的字节序列(指令特征码),可以用 Memory.scan 在模块内存中搜索。
    2. 挂钩JNI_OnLoad :在 JNI_OnLoad 里, App 会动态注册 Native 函数。挂钩 JNI_OnLoad ,打印 RegisterNatives 调用的参数,就能得到函数指针和其对应的 Java 方法名。

7.5 算法复现结果总差一点

  • 问题 :所有步骤似乎都对,但生成的签名服务器就是不认。
  • 原因 :往往是细节问题。
  • 排查清单
    • 编码 :确保每一步的字符串编码一致。 Java 默认 UTF-16 ,但转换成字节数组进行哈希时通常是 UTF-8 。用 Frida 打印出 Java 层调用 MessageDigest.update() 时传入的字节数组,与你 Python 字符串.encode(‘utf-8’) 的结果进行 hex 对比。
    • 空格与空值 :参数拼接时, App 可能过滤了值为 null 或空字符串 “” 的键值对,你的复现是否也过滤了?键值对之间的连接符是 & 还是 & URL 是否需要 encode
    • 时间戳 :时间戳是秒还是毫秒?是 Unix 时间戳还是本地时间?是否需要除以 1000 或进行其他转换?用 Frida 打印出参与计算的时间戳数值。
    • 密钥格式 :密钥是字符串还是字节数组?如果是字符串,是否需要先进行 Base64 解码或 Hex 解码? HMAC 的密钥是原始字节。
    • 哈希输出 MD5 SHA256 的输出是 16 进制字符串( Hex )还是 Base64 ?字母是大写还是小写? Frida 可以打印出最终的字节数组,你转成 Hex Base64 对比一下。
    • 多一次哈希 :是否对第一次哈希的结果又进行了一次哈希?
    • 隐藏参数 :是否有一个固定的、写在代码里但你没发现的“魔数”( Magic Number )也参与了拼接?仔细搜索 Native 层或动态 JAR 中的常量数组。

逆向工程就像解一个多维度的谜题,需要耐心、细致的观察和不断的假设验证。面对 x-sign 这种动态加载的签名机制,静态分析提供地图,动态分析提供导航,而最终的复现则是亲手走一遍这条路。每一次成功破解,不仅是对技术的提升,更是对大型应用安全设计思路的一次深刻理解。记住,思路和技巧远比记住某个 API 调用更重要。希望这篇长文记录的经验和踩过的坑,能让你在下一次逆向之旅中更加从容。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐