乐于分享
好东西不私藏

某音乐 App 逆向(二):calc签名算法深度逆向分析

某音乐 App 逆向(二):calc签名算法深度逆向分析

免责声明:本文所有分析内容仅供网络安全学习与研究,所有技术均已向相关厂商提交安全报告。请勿将本文技术用于未经授权的商业环境,一切后果自负。


摘要

本文记录了一次由 Claude Code(Anthropic)全程辅助完成的 Android 原生签名算法逆向分析。面对 tmesec_llvm 定制编译器产生的 CFF(控制流扁平化)混淆、Crypto 全量内联以及主动反 Frida 检测,人机协作完成了 14 个版本的 Hook 脚本迭代、22 组数学差分实验,最终通过”搭载实验”+ AttachCurrentThread 两项核心技术实现了 3ms/call 的高性能签名桥接,并用差分分析数学证明了该哈希函数为无法离线复现的自定义非线性函数。本文同时记录了大模型在真实移动安全研究中的具体协作方式与能力边界。


一、分析背景与目标

1.1 前序工作

在前期分析中,我们已完成:

  • 请求/响应编解码:M-Encoding m1 = Deflate 压缩 + 5 字节随机前缀(已完全还原)
  • 签名调用链定位CgiRequest.g0() → SignRequestHelper.c() → MERJni.calc()
  • calc I/O 格式确认calc(content, params) 返回 "sign_b64 mask_b64" 两段 Base64

1.2 本次分析目标

深入逆向 libmer.so 中 calc() 的签名算法,目标如下:

  1. 完全还原 sign 和 mask 的生成算法
  2. 实现 Python 版本,脱离设备独立计算签名
  3. 如果无法完全还原,构建可靠的 Frida 桥接方案

1.3 大模型辅助工作范式

本次分析的一个显著特点是:全程由 Claude Code 协同完成,而非传统的”人工逐行阅读反汇编”。

人机协作分工

角色
负责内容
人(研究员)
在真实设备上执行命令、运行脚本、采集原始输出;把控研究方向与伦理边界
Claude Code
分析输出、生成脚本、设计实验方案、解读二进制数据、提出假设并推导验证路径

Claude Code 在本次分析中的具体贡献

1. 静态分析加速:研究员将 radare2 的原始输出(函数列表、基本块统计、交叉引用)直接贴入对话,Claude Code 即刻识别出 CFF 混淆特征、定位 36KB 核心函数,并给出了后续动态分析的优先级建议。

2. Frida 脚本批量生成与迭代:14 个版本的 Hook 脚本均由 Claude Code 生成。每次脚本失败后,研究员将设备端的错误日志和崩溃信息反馈给 Claude Code,后者分析根本原因并直接输出修复版本,形成闭环迭代。平均每轮迭代耗时不超过 5 分钟。

**3. 关键技术突破的”灵感来源”**:

  • 搭载技术(Piggyback):在前 4 个版本因 JNI 线程限制持续崩溃后,Claude Code 提出”不要主动调用,搭载真实调用的便车”这一思路,直接打开了受控实验的大门
  • AttachCurrentThread 方案:Claude Code 查阅 JNI 规范后给出了通过 vm->AttachCurrentThread() 为任意线程合法创建 JNIEnv 的实现,解决了桥接方案中最核心的线程绑定问题
  • 差分分析框架:22 组实验的设计(单字节、位置测试、跨长度核比较)由 Claude Code 系统规划,目标是最大化对算法结构的信息增益

4. JNI vtable 调试:vtable 索引错误(211 → 208)也是 Claude Code 通过分析 JNI 规范和 arm64 指针布局,精确定位并修正的。

5. 本文档撰写:包括技术分析、架构图设计(SVG)、公众号格式化,均在 Claude Code 辅助下完成。

能力边界的客观认识

当然,大模型并非万能。本次分析中 Claude Code 无法替代人工完成的环节包括:

  • 在真实设备上执行命令并获取运行时数据
  • 判断哪些实验现象属于”值得深入”的异常(需要经验直觉)
  • 最终的研究伦理与方向把控

这种**”人负责执行与判断,AI 负责生成与推理”的协作模式**,使得整个分析在两天内完成——而同等深度的传统分析通常需要数周。

1.4 分析工作流总览

整个分析分为四个阶段:静态分析、动态追踪、算法分析、桥接方案构建。

分析工作流总览

二、静态分析:libmer.so 深度解剖

2.1 二进制概览

使用 radare2 对目标库进行静态分析:

# 查看文件基本信息r2 -q -c "iI" libs/lib/arm64-v8a/libmer.so
属性
架构
ARM aarch64(64-bit)
文件大小
2.3 MB
编译器
clang 6.0.0(tmesec_llvm,目标厂商安全团队定制 LLVM)
导出函数
2773(大部分为 OpenSSL API)
匿名函数
308(自定义逻辑)
NX
✓ 已启用
Stack Canary
✓ 已启用
PIC
✓ 已启用
RELRO
Full
调试符号
已完全剥离

值得注意的是:该 SO 由目标厂商安全团队定制的 LLVM 编译器(tmesec_llvm)编译,在标准编译优化之上叠加了深度代码混淆。这意味着用 IDA Pro、Ghidra 等常规工具打开后,反编译结果会异常混乱,大量逻辑无法正常还原为可读代码。

2.2 calc 函数定位

calc 并非通过标准 JNI 静态方法命名导出(如 Java_com_xxx_MERJni_calc),而是通过 RegisterNatives动态注册——这是一种常见的反 Hook 手段,能让逆向者难以通过符号名直接定位函数地址。

JNI 动态注册流程

通过 Hook RegisterNatives 本身,可以在 App 启动时动态捕获 calc 的函数指针:

// 拦截 JNI RegisterNatives, 捕获 calc 的运行时地址Interceptor.attach(registerNativesAddr, {onEnterfunction(args{var nMethods = args[3].toInt32();var methods = args[2];for (var i = 0; i < nMethods; i++) {var name = methods.add(i * 24).readPointer().readUtf8String();if (name === "calc") {// 每次运行地址因 ASLR 不同, 但相对 SO 基址的偏移固定                calcFnPtr = methods.add(i * 24 + 16).readPointer();            }        }    }});

2.3 控制流扁平化(CFF)混淆

calc 函数被 tmesec_llvm 编译器施加了控制流扁平化(Control Flow Flattening)混淆。其核心思想是:将原本有清晰顺序的代码块,全部打散成一个巨大的 switch-case 结构,通过一个”分发变量”控制执行流向,使静态分析工具无法还原原始逻辑。

控制流扁平化(CFF)原理

calc 函数混淆统计:

指标
函数偏移
0x6e098

(相对 SO 基址)
函数大小
9,216 字节
基本块数
580 个
平均块大小
~16 字节
外部调用
10 个(全在同一大函数内,大量内联)

580 个基本块对于一个单一函数而言是极高的——正常编写的同等逻辑函数通常只有几十个基本块。这使得 IDA 的 F5 反编译功能在此完全失效。

2.4 调用图分析

通过 radare2 的调用图分析,calc 函数共直接调用 5 个子函数:

函数偏移
大小
推测功能
fcn.0x6dd08
912 B
字符串处理
fcn.0x13bfd0
14.7 KB
哈希相关
fcn.0x8bbb8 36.8 KB ★ 核心计算(主要目标)
fcn.0x709a4
5.5 KB
格式化
fcn.0x71f50
1.2 KB
Base64 相关

核心发现fcn.0x8bbb8 是整个签名算法的心脏,包含 579 个基本块,大小达 36KB,是我们后续分析的主要目标。

2.5 加密常量搜索

# 搜索 SHA1/MD5 初始化常量, 判断使用了哪些标准算法r2 -q -c "/x 67452301; /x efcdab89; /x 98badcfe; /x 10325476" libmer.so

发现情况:

常量
类型
是否找到
0x67452301
MD5 init A
✓ 找到(两处)
0xEFCDAB89
MD5 init B
✓ 找到
0x98BADCFE
MD5 init C
✓ 找到
0x10325476
MD5 init D
✓ 找到
0xC3D2E1F0
SHA1 init E
✗ 未找到
0x5A827999
SHA1 轮常量
✗ 未找到
0xD76AA478
MD5 T[0]
✗ 未找到(运行时动态构造)

结论:库中存在 MD5 初始化常量,但 SHA1 常量和 MD5 轮常量缺失。轮常量在运行时由混淆代码动态构造,防止静态提取——这是 tmesec_llvm 的一项关键反提取策略。


三、动态分析:Frida Hook 14 轮迭代

3.1 分析策略

面对高度混淆的目标,我们采用了四种互补的动态分析策略:

动态分析策略
  • 黑盒(纯 I/O):只 Hook calc 入口/出口,记录输入→输出映射。优点:零干扰;缺点:看不到内部结构
  • 灰盒(libc 追踪):Hook memcpysprintf 等 libc 函数,追踪内部数据流。优点:可见数据流;缺点:噪声和性能问题
  • 白盒(标准 API):Hook OpenSSL 导出的 SHA1HMACRSAEVP_*MD5_* 等函数。结果:全部未命中(Crypto 被完全内联)
  • 受控实验:精心设计测试用例替换 calc 输入,观察输出变化规律。优点:最高信息量,是最终突破方案

3.2 Hook 脚本迭代历程

我们共开发了 14 个版本的 Hook 脚本,每个版本解决一个特定问题:

Hook 脚本迭代演进

以下是关键节点:

v1(trace_calc_crypto.js:Hook 所有 OpenSSL 导出 API → 全部未触发。发现:calc 不调用任何标准 OpenSSL API,所有 Crypto 操作被完全内联进函数体。

v2/v3(trace_calc_sign_mask.js / trace_calc_v3.js:转向 Hook memcpy/memmove 追踪数据流 → 被 /proc/self/maps 读取产生的数百个噪声 memcpy 淹没,实际有效数据难以提取。

v5(trace_calc_io.js,突破!):放弃所有内部 Hook,只捕获 calc 入口/出口。成功捕获 5 次调用的完整 I/O,确认输出格式:sign_raw = 32B(12B prefix + 20B hash)mask_raw = 104B

💡 Claude Code 分析:在 v4 连续失败后,Claude Code 建议”内部 Hook 方案已触碰性能天花板,应当彻底转向黑盒 I/O 策略——calc 的入口和出口是我们真正需要的全部信息”。这一判断成为整个分析的转折点。

v8(trace_calc_piggyback.js,关键突破!):搭载技术——在真实 calc 调用上替换输入数据,绕过 JNI 线程限制。12 组受控实验全部成功,发现了 hash 仅依赖 content 的关键特性。

💡 Claude Code 提出的核心思路:面对 v6/v7 因 JNI 线程绑定接连崩溃,Claude Code 给出了”搭载(Piggyback)”方案——”我们不需要主动创建调用环境,只需拦截 App 自身发起的调用,在 onEnter 时悄悄替换参数,借用它的合法 env 和 jclass。” 这是整个研究中最具创造性的一步。

v9(trace_calc_experiment3.js,数学逆向):22 组精心设计的二进制实验,通过差分分析,数学证明了 hash 为自定义非线性函数,无法通过 I/O 推导还原。

💡 Claude Code 设计的实验矩阵:22 组测试用例由 Claude Code 系统规划,覆盖”单字节遍历 → 位置独立性检验 → 跨长度核比较 → ASCII 递增长度 → 边界值”五个维度,目标是以最少的设备交互次数,获得对算法结构的最大信息增益。

3.3 四大关键问题与解决

问题 1:OpenSSL API 全部未命中

calc 不通过 PLT(过程链接表)调用任何导出的 Crypto 符号。tmesec_llvm 将所有 SHA/MD5/HMAC 操作完全内联进 calc 函数体。

证据:在 memcpy 追踪中能看到 MD5 的 padding 字节(0x80 00 00...),证明 MD5 确实在执行,只是不走导出 API。

应对:放弃 Hook 标准 API 路线,改为追踪数据流和直接 I/O 分析。

问题 2:/proc/self/maps 噪声洪泛

# calc 内部会读取 /proc/self/maps 做反篡改检测:# 73d99c6000-73d9aa0000 r--p ... /data/.../libmer.so# 73d9aa0000-73d9c2a000 r-xp ... /data/.../libmer.so# (数百行, 每行产生 105B / 140B 的 memcpy 调用)

这是 libmer.so 的反篡改检测机制——通过解析 /proc/self/maps 确认自身未被修改或注入。其副作用是在 memcpy Hook 时产生大量噪声数据。

应对:通过偏移过滤(offset 0xa7920)识别噪声来源,最终选择直接去掉 memcpy Hook,采用纯 I/O 方案。

问题 3:JNIEnv 线程局部限制

这是最关键的技术难题。JNIEnv 与线程绑定,无法跨线程传递。从 Frida JS 线程直接调用 native calc 函数会遇到三种情况:

方案
尝试方式
结果
原因
A(直接调用)
NativeFunction

 传入空 jclass
❌ 访问违例崩溃
jclass=null 导致立即崩溃
B(Java 反射)
Java.perform()

 调用 MERJni.calc()
❌ SIGSEGV
App 检测到 Frida Java bridge,主动自杀
C(搭载技术)
拦截真实 calc 调用,替换输入
✅ 成功
借用 App 自身的有效 env + jclass
D(AttachCurrentThread)
为新线程创建 JNIEnv
✅ 最终方案
正式 JVM API,稳定高效

方案 C(搭载技术 Piggyback) 是重要中间突破——通过拦截 App 发出的真实 calc 调用,在 onEnter 时替换 args[2](content)和 args[3](params)为我们的测试数据,同时利用 App 提供的合法 JNI 环境执行签名。

搭载实验原理图

方案 D(AttachCurrentThread 是最终生产方案:首次 calc 调用时捕获 JavaVM 指针和 jclass 的 GlobalRef,后续 RPC 调用时通过 vm->AttachCurrentThread() 为当前线程创建合法的 JNIEnv,然后直接调用 calcFn

问题 4:JNI vtable 偏移错误

JNI 函数通过 vtable 间接调用。在 arm64 环境下,每个指针占 8 字节,函数索引需精确:

Index
函数
偏移
176
NewByteArray
1408
200
GetByteArrayRegion
1600
208 SetByteArrayRegion ✅ 1664
211
SetIntArrayRegion

 ❌
1688
219
GetJavaVM
1752

初始版本误用了 index 211(SetIntArrayRegion),导致 JNI 类型检查失败并 abort。修正为 208 后解决。


四、受控实验:calc 算法特性验证

4.1 搭载实验设计(12 组)

使用上一节的搭载技术,设计了 12 组对照实验,分三组变量进行隔离测试:

实验
content
params timestamp
目的
A
#3
{"t":"a"}
固定 777
基准
A
#4
{"t":"b"}
固定 777
content 变化
A
#5
{"t":"a"}
固定 777
重复验证
B
#7
{"t":"fixed"}
777
基准
B
#8
{"t":"fixed"}
778
timestamp +1
B
#9
{"t":"fixed"}
777
重复验证
C
#10~12
{"same":"1"}
固定 777
相同输入 ×3

关键结论:

  • ✅ sign_hash 仅依赖 content,不依赖 params 中的 timestamp:相同 content + 不同 timestamp → 完全相同的 hash
  • ✅ 同会话内的确定性:相同 content 连续调用 3 次 → hash 全部相同
  • ✅ mask 结构分层:前 48B 依赖 content,后 56B 依赖内部 gettimeofday() 时间戳

4.2 数学逆向实验(22 组)

在确认了基本特性后,通过 22 组精心设计的二进制输入进行差分分析,试图推导出哈希函数的数学结构:

实验数据(部分):

content
sign_hash(前 8 字节 hex)
\x00 04490685...
\x01 a711df0b...
\x02 19cbc34f...
\x00\x00 47942759...
\x01\x00 374407fe...
\x00\x01 4bee7d20...
"A" 9bd44515...
"AA" 1448f64f...
"AAA" 2bcad242...
"AAAA" 6f7a0a0e...

4.3 差分分析结论

线性检验

h(\x01) - h(\x00) = Kh(\x02) - h(\x00) = ? 应等于 2K实际: 完全不相等 → 对字节值非线性 ❌h(\xff) - h(\x00) = ? 应等于 255K实际: 完全不匹配 → 非线性 ❌

跨长度核比较

K[0](1字节输入) ≠ K[0](2字节输入) ≠ K[0](4字节输入)→ 位置贡献随 content 总长度变化, 无法独立分解 ❌

AAAA 预测验证:基于线性假设预测 h("AAAA"),与实际值完全不匹配。

最终结论:

sign_hash 是一个自定义的非线性哈希函数:

  1. 输出 20 字节(160 位)——与 SHA1 长度相同,但非 SHA1
  2. 对 content 字节值非线性
  3. 位置贡献随 content 总长度变化
  4. 依赖会话特定状态(每次 App 启动不同)
  5. 不依赖 params 中的 timestamp
  6. 同一会话内具有确定性
  7. 非标准 SHA1/MD5/HMAC/CRC 的任何已知组合
  8. 核心算法隐藏在 36KB CFF 混淆函数中,无法通过 I/O 推导还原

已通过实验排除的候选算法:

候选算法
排除原因
SHA1(content)
输出不匹配
MD5(content)
输出 16B ≠ 20B
SHA1(MD5(content))
输出不匹配
HMAC-SHA1(key, content)
所有已知 key 均不匹配
SHA1(salt + content)
输出不匹配
CRC32(content)
输出 4B ≠ 20B
线性可分哈希
差分分析已证伪
MD5(content XOR key)
输出不匹配

五、Frida 桥接方案:AttachCurrentThread

由于签名算法包含会话特定状态(无法离线复现),我们最终构建了一套基于 Frida 的 RPC 桥接方案,让 Python 能够按需远程调用设备上 App 的签名函数。

5.1 整体架构

Frida Bridge v2 架构图

整个方案分为两个阶段:初始化(自动等待 App 启动并捕获必要指针)和 调用(通过 RPC 随时请求签名)。

5.2 初始化时序

Bridge 初始化时序图

初始化流程:

  1. Python spawn 目标 App 进程,同时加载 Frida Agent
  2. Agent Hook RegisterNatives,捕获 calc 函数指针
  3. App 发出首个 API 请求,触发首次 calc 调用
  4. Agent 在 onEnter 中捕获 JavaVM 和 jclass 的 GlobalRef
  5. 通知 Python “桥接就绪”,后续可接受 RPC 调用

5.3 核心 JavaScript 技术点

// 步骤 1: 首次 calc 调用时捕获 JavaVM 指针var vt = env.readPointer();  // 读取 JNIEnv 的 vtablevar getJavaVM = new NativeFunction(    vt.add(219 * 8).readPointer(),  // index 219 = GetJavaVM'int', ['pointer''pointer']);var vmBuf = Memory.alloc(8);getJavaVM(env, vmBuf);javaVmPtr = vmBuf.readPointer();  // 保存 JavaVM 指针// 步骤 2: 为 jclass 创建 GlobalRef, 防止 GC 回收var newGlobalRef = new NativeFunction(    vt.add(21 * 8).readPointer(),  // index 21 = NewGlobalRef'pointer', ['pointer''pointer']);jclassPtr = newGlobalRef(env, args[1]);  // 从 onEnter 的 args[1] 获取// 步骤 3: RPC 调用时, 为当前线程创建新的 JNIEnvvar vmVt = javaVmPtr.readPointer();var attachCurrentThread = new NativeFunction(    vmVt.add(4 * 8).readPointer(),  // JavaVM vtable index 4 = AttachCurrentThread'int', ['pointer''pointer''pointer']);var envBuf = Memory.alloc(8);attachCurrentThread(javaVmPtr, envBuf, ptr(0));var newEnv = envBuf.readPointer();  // 获得与当前线程绑定的合法 JNIEnv// 步骤 4: 使用合法 env 直接调用 calcvar calcFn = new NativeFunction(calcFnPtr, 'pointer',    ['pointer''pointer''pointer''pointer']);var result = calcFn(newEnv, jclassPtr, contentArr, paramsArr);

关键技术要点总结:

  • **AttachCurrentThread**:JVM 官方 API,为非 JVM 线程(如 Frida Agent 线程)创建合法 JNIEnv,避免了直接使用其他线程的 JNIEnv 导致的崩溃
  • **NewGlobalRef**:将 jclass 从局部引用提升为全局引用,防止 JVM GC 在函数返回后将其回收
  • vtable 精确索引:arm64 环境下指针大小为 8 字节,所有 vtable 访问必须使用 index × 8 计算偏移

5.4 性能表现

测试项
结果
状态
单次调用延迟
~3 ms
✅ 极快
10 次连续调用
30 ms 总计
✅ 稳定
确定性(相同输入)
100% 一致
✅ 通过
不同 content
输出不同
✅ 通过
初始化到就绪
~5 秒
✅ 全自动
sign 长度
44 字符
✅ 正确
mask 长度
140 字符
✅ 正确

六、已验证的算法细节

6.1 calc 完整数据流

通过多轮实验,我们完整还原了 calc() 的外部行为和内部结构:

calc 内部执行流程

执行流程分为四个阶段:

Phase 1:基础哈希

  • 读取硬编码 SALT(已在二进制中提取,此处脱敏处理,不予公开)
  • hash1 = MD5(SALT)(已验证)
  • hash2 = MD5(hash1 + timestamp_ms),时间戳取自 gettimeofday()(已验证)

Phase 2:APK 完整性校验

  • 读取 /proc/self/maps(反篡改:确认内存映射正常)
  • 解析 APK zip 结构,提取证书签名哈希(APK_HASH,脱敏处理)
  • h_apk = MD5(APK_HASH[2:12])(已验证)

Phase 3:核心哈希计算(36KB CFF 混淆函数)

  • 输入:hash1、hash2、h_apk、硬编码固定哈希、content
  • 内部进行多轮迭代,约 14 轮,在两个内存地址交替写入 20 字节中间值
  • 输出:20 字节 sign_hash(自定义非线性算法)+ 104 字节 mask_data

Phase 4:签名组装

  • sign_raw = session_prefix(12B) + sign_hash(20B)
  • sign_b64 = Base64(sign_raw)(44 字符)
  • mask_b64 = Base64(mask_raw)(140 字符)
  • 返回:"sign_b64 mask_b64"

6.2 Sign 结构详解

Sign 结构详解

sign 由两部分拼接后 Base64 编码:

字节范围
内容
长度
特性
0-11
Session Prefix
12B ASCII 大写
每次 App 启动随机生成,会话内固定
12-31
Sign Hash
20B 二进制
由 content 决定,非线性,会话状态相关

这个设计使得 sign 同时包含了”会话标识”和”内容指纹”:服务端通过解析 sign 前 12 字节可以识别会话,后 20 字节用于验证请求内容的合法性。

6.3 已验证的 MD5 哈希链

MD5 双轮哈希链

MD5 双轮链已被完整验证:

Step 1: MD5(SALT) = hash1                  [已验证 ✅]Step 2: MD5(hash1 + timestamp_ms) = hash2  [已验证 ✅]APK:    MD5(APK_HASH[2:12]) = h_apk        [已验证 ✅]

注:SALT 值和 APK_HASH 为版本特定的硬编码值,已向厂商报告,本文不予公开完整值。


七、使用指南(研究环境)

⚠️ 以下内容仅供安全研究参考,需在已获授权的测试设备上进行。

7.1 环境准备

# 安装 Python 依赖pip3 install frida==17.8.2 frida-tools==17.8.2# 部署 frida-server 到测试设备adb push frida-server-17.8.2-android-arm64 /data/local/tmp/frida-serveradb shell "chmod +x /data/local/tmp/frida-server"adb shell "su -c '/data/local/tmp/frida-server -D &'"# 验证连接frida-ps -U | grep <package_name>

7.2 桥接方案使用示例

#!/usr/bin/env python3# 注意: 仅在授权测试设备上运行# 1. 启动桥接 (spawn 模式会重启 App)bridge = FridaCalcBridge()bridge.connect(spawn=True)bridge.wait_ready(timeout=60)  # 等待 App 初始化并触发首次 calc# 2. 请求签名计算content = build_request_body("some.api.Method""method_name", {"param""value"})params = build_params()           # 自动填入当前时间戳result = bridge.calc(content, params)print(f"sign: {result['sign']}")  # 44 字符 Base64print(f"mask: {result['mask']}")  # 140 字符 Base64# 3. 清理bridge.close()

八、安全防护机制评估

该 App 部署了多层安全机制,各层强度不一:

安全机制
防护强度
分析详情
tmesec_llvm CFF 控制流扁平化
★★★★★ 极高
580 基本块,36KB 核心函数,有效阻碍 IDA/Ghidra 反编译
Crypto 全量内联
★★★★☆ 高
所有 OpenSSL 操作不经导出符号,无法通过 Hook API 追踪
反 Frida 检测
★★★☆☆ 中高
检测 Java.perform() 后自杀,但纯 native hook 不受影响
/proc/self/maps

 反篡改
★★★☆☆ 中
读取内存映射做完整性检测,不影响 Frida Interceptor
APK 完整性校验
★★★☆☆ 中
验证 APK zip 和证书哈希,重打包需要额外处理
会话状态依赖
★★★☆☆ 中
每次启动不同的会话密钥,有效防止离线计算签名
动态 JNI 注册
★★☆☆☆ 低
RegisterNatives

 本身易被 Hook,难以掩盖入口点
硬编码 SALT
★☆☆☆☆ 极低
固定值,可通过运行时追踪或二进制搜索提取
M-Encoding m1
★☆☆☆☆ 极低
仅 Deflate 压缩 + 5B 随机前缀,非加密,可直接解压

总体评价:整体防护的强点在于编译期混淆(tmesec_llvm)和 Crypto 内联,这两项对静态分析构成了严重障碍。但运行时防护相对薄弱——纯 native 的 Frida Hook 完全不受反 Frida 检测影响。


九、结论

9.1 目标达成情况

目标
状态
说明
还原 sign 生成算法
⚠️ 部分完成
结构已知(12B prefix + 20B hash),核心哈希为 36KB CFF 混淆自定义函数,无法纯离线复现
还原 mask 生成算法
⚠️ 部分完成
结构已知(104B = 48B content 依赖 + 56B 时间依赖),具体算法未还原
Python 离线计算签名
❌ 不可行
哈希依赖会话状态,算法深度混淆,无法脱离设备计算
Frida 桥接方案
✅ 完成
AttachCurrentThread

 方案,3ms/call,100% 准确,生产可用
M-Encoding 编解码
✅ 完成
Python 实现,双向验证通过
MD5 哈希链验证
✅ 完成
SALT → hash1 → hash2,全部验证通过

9.2 技术贡献

本次分析产出了四项可复用的技术方法:

  1. 搭载实验技术(Piggyback):解决了无法直接从外部线程调用 native JNI 函数的问题,适用于所有需要在 App 进程内进行受控实验的场景
  2. AttachCurrentThread 桥接模式:解决了 JNIEnv 线程局部限制,实现了高性能 RPC——这是一个通用的 Frida 桥接范式,可应用于任何需要从外部驱动 JNI 函数的场景
  3. 差分分析方法:通过 22 组受控实验,用数学方法排除了大量候选算法,即使无法破解也能精确描述算法特性
  4. JNI vtable 索引校正:在 arm64 环境下精确验证了 SetByteArrayRegion 的正确偏移(index 208),并总结了常用 JNI 函数的 vtable 索引表

9.3 大模型辅助逆向:效率与范式的变化

本次分析是一次完整的”人机协作逆向”实践,从中可以总结出大模型在移动安全研究中的价值边界:

大模型擅长的

  • 代码生成与迭代:Frida 脚本从需求到可运行代码,秒级响应;错误修复有理有据,不是蒙
  • 知识密集型推理:JNI 规范、vtable 布局、arm64 调用约定——这类需要大量参考手册的推理,大模型有明显优势
  • 实验设计:给定约束条件(只能替换输入,无法直接调试),自动规划最优的实验矩阵
  • 多假设管理:同时维护”MD5链 / 自定义哈希 / HMAC 变体”等多个假设,随数据动态排除

大模型的局限

  • 无法直接操作设备,所有执行都依赖人在物理世界完成
  • 对某些高度上下文依赖的运行时现象(如特定内存布局、时序竞争)需要人反复描述才能准确理解
  • 复杂的 CFF 反混淆仍依赖专业工具(Ghidra 插件),大模型难以直接处理几十 KB 的原始汇编

整体效率提升:同等深度的逆向分析,传统方式通常需要数周;本次人机协作在 两天内完成了从零到可用桥接方案的全流程。大模型的核心价值在于消除了”知道方向但不知道怎么写”的摩擦,让研究员可以把精力集中在真正需要人类经验的判断上。

9.4 后续研究方向

  1. 静态逆向 36KB 核心函数:使用 Ghidra + 去混淆插件(如 D810)系统分析 fcn.0x8bbb8,尝试还原真实算法
  2. Unicorn 模拟执行:提取 libmer.so 的 calc 函数代码段,在 Unicorn 引擎中模拟执行,彻底脱离设备
  3. 服务端验证研究:分析服务端对 sign/mask 的校验逻辑和容错窗口,评估整体安全性
  4. detect() 函数逆向:分析反调试/完整性检测函数的具体实现逻辑

分析完成时间:2026-03-18

使用工具:Frida 17.8.2 · radare2 · jadx · Python 3.10

测试环境:Android 测试设备(已 root)

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 某音乐 App 逆向(二):calc签名算法深度逆向分析

猜你喜欢

  • 暂无文章