繁花剧场App API加密分析报告
一、项目背景
1.1 目标信息
| 项目 | 内容 |
|---|---|
| 目标App | 繁花剧场 |
| 包名 | com.dzhong.fhjc |
| 开发商 | 北京点众快看科技有限公司 |
| API域名 | flowerapi.kydca.cn |
| 接口路径 | /app-video-portal/portal/client/1001 |
| 请求方式 | POST |
| 协议 | HTTP/2.0 over HTTPS |
1.2 问题描述
通过Charles抓包获取到的API响应内容为全量加密数据,响应体是一个极长的十六进制字符串,无法直接读取业务数据。需要分析其加密机制并找到解密方法。
1.3 抓包数据样例
{
"code": 0,
"userId": 2787935422,
"timestamp": 1774666357752,
"data": {
"aab421b86a24fff2d4e2507828f3c909...": "b0a75f6b848f63bd74b5a493d5891ad4..."
}
}
data字段的key和value都是长十六进制字符串- 长度约3200字节(hex解码后)
- 响应大小约111KB,压缩率35.8%
二、Java层代码分析
2.1 反编译工具
使用 jadx-gui-1.5.5-with-jre-win 打开APK进行反编译。
2.2 包结构
com.dzhong.fhjc.apk
├── d.z.s/ # 混淆后的主代码包
│ ├── N.java # Native调用入口
│ ├── h.java # 密钥管理类
│ ├── T.java # 空类
│ └── dzfanhua.java # 合成类
├── okhttp3/ # 网络请求库
├── retrofit2/ # API接口库
└── 其他第三方SDK(广告、统计等)
2.3 核心解密类:N.java
package d.z.s;
import android.content.Context;
import kotlin.jvm.internal.so;
public final class N {
public static final N INSTANCE = new N();
static {
try {
System.loadLibrary("dzst"); // 加载Native库
} catch (UnsatisfiedLinkError e) {
e.printStackTrace();
}
}
private N() { }
// Native方法,实现在 libdzst.so 中
public final native String ns(Context context, String str);
// Java层调用入口
public final String sr(Context context, String payload) {
so.hr(context, "context");
so.hr(payload, "payload");
try {
return ns(context, payload); // 调用Native方法
} catch (Throwable unused) {
return null;
}
}
}
关键发现:
- 加载了名为
dzst的Native库(对应文件libdzst.so) ns()是native方法,真正的解密逻辑在C/C++代码中sr()是Java层唯一入口,接收加密字符串,返回解密结果
2.4 密钥管理类:h.java
package d.z.s;
import android.security.keystore.KeyGenParameterSpec;
import android.util.Base64;
import java.security.KeyStore;
import java.security.Signature;
// ... 其他import
public final class h {
public static final h dzfanhua = new h();
// 获取证书链
public final String T() { ... }
// RSA签名
public final byte[] a(byte[] bArr) {
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
Key key = keyStore.getKey("request_sign_key", null);
// ... 使用SHA256withRSA签名
}
// 创建TEE密钥对
public final void dzfanhua() {
// 创建别名 "request_sign_key" 的RSA密钥对(2048位)
// 存储在Android KeyStore(TEE安全区域)
}
// 获取公钥
public final String h() { ... }
// 对payload进行签名并Base64编码
public final String j(String payload) {
return Base64.encodeToString(a(payload.getBytes()), 2);
}
}
关键发现:
- 密钥别名:
request_sign_key - 签名算法:
SHA256withRSA - 密钥存储在 Android KeyStore(硬件级TEE安全区域)
- 密钥对在App首次运行时创建,不可导出
三、Native库分析
3.1 提取SO文件
# 解压APK
unzip com.dzhong.fhjc.apk -d apk_extract
# 定位SO文件(arm64-v8a架构)
ls apk_extract/lib/arm64-v8a/libdzst.so
- 文件大小:9,664 字节(约9.5KB)
- 架构:ARM 64位
- 编译信息:Clang 18.0.1,Android NDK r522817
3.2 字符串提取
使用 strings 命令提取可读字符串:
strings libdzst.so
关键字符串列表:
| 字符串 | 用途推测 |
|---|---|
Java_d_z_s_N_ns |
JNI函数名,Java层native方法对应 |
MySuperSecretSalt_DoNotLeak |
盐值,用于密钥派生 |
native_sign_ |
签名验证相关 |
SHA-256 |
哈希算法 |
getPackageInfo |
获取包信息,用于签名验证 |
signatures |
获取App签名 |
unknown_sig |
签名验证失败提示 |
%02x |
十六进制格式化 |
| 自定义字符表 | 1cD2eF-GHIJ_KMOQSUWYbdfhjlnprtvxzACEgikL3mN4oP5qR6sT7uV8wX9yZ0aB;:^]\[@?>=< |
3.3 JNI函数验证
SO文件中包含JNI导出函数:
Java_d_z_s_N_ns
对应Java层的:
public final native String ns(Context context, String str);
参数和返回值类型通过JNI签名 (Landroid/content/Context;Ljava/lang/String;)Ljava/lang/String; 确认。
3.4 自定义字符表分析
发现一个非标准的Base64字符表:
1cD2eF-GHIJ_KMOQSUWYbdfhjlnprtvxzACEgikL3mN4oP5qR6sT7uV8wX9yZ0aB;:^]\[@?>=<
特征:
- 长度:约80字符
- 包含字母、数字、特殊符号
- 可能用于自定义编码,替代标准Base64
四、加密流程推测
基于以上分析,推断完整流程如下:
┌─────────────────────────────────────────────────────────────────┐
│ 服务端 │
│ 业务数据 → JSON → AES加密 → Hex编码 → 返回给客户端 │
└─────────────────────────────────────────────────────────────────┘
↓ HTTPS
┌─────────────────────────────────────────────────────────────────┐
│ 客户端(繁花剧场) │
│ │
│ 1. 收到加密响应(Hex字符串) │
│ ↓ │
│ 2. Java层调用 N.sr(context, encrypted) │
│ ↓ │
│ 3. JNI调用 → libdzst.so 中的 Java_d_z_s_N_ns │
│ ↓ │
│ 4. Native层获取设备签名 │
│ - 从 Android KeyStore 读取 "request_sign_key" 证书 │
│ - 提取App签名信息 │
│ ↓ │
│ 5. 密钥派生 │
│ key = SHA-256(盐值 + 签名数据) │
│ 盐值 = "MySuperSecretSalt_DoNotLeak" │
│ ↓ │
│ 6. AES解密 │
│ - Hex解码 → 字节数组 │
│ - 使用派生出的key进行AES解密 │
│ - 去除PKCS7填充 │
│ ↓ │
│ 7. 返回明文JSON │
│ ↓ │
│ 8. App解析JSON,渲染UI │
└─────────────────────────────────────────────────────────────────┘
未确定参数:
- AES模式(CBC / ECB / GCM / CTR)
- IV生成方式(固定/随机/从密钥派生)
- 签名数据的完整组成(包名 + 证书 + 设备ID?)
五、反调试机制分析
5.1 现象
- Frida附加时App立即崩溃退出
- objection同样被检测
- Charles抓包不受影响(因为是系统代理层)
5.2 推测检测点
| 检测方式 | 说明 |
|---|---|
/proc/self/maps 扫描 |
检测内存映射中是否包含 frida、gum-js、linjector |
| 端口扫描 | 检查 127.0.0.1:27042 是否可连接 |
ptrace 自附加 |
检测是否被调试器附加 |
isDebuggerConnected() |
Android API检测调试器 |
| 签名校验 | 验证自身签名是否被篡改(防止重打包) |
5.3 检测代码位置
反调试逻辑在 Native层(libdzst.so),不在Java层。这是为什么jadx中搜不到明显反调试代码的原因。
六、工具测试结果
6.1 成功工具
| 工具 | 用途 | 结果 |
|---|---|---|
| Charles | 抓包 | ✅ 成功抓取加密响应 |
| jadx | 反编译Java | ✅ 成功定位关键类 |
| apktool | 解包APK | ✅ 可解包和重打包 |
| strings | 提取SO字符串 | ✅ 提取到盐值和函数名 |
6.2 失败工具及原因
| 工具 | 失败原因 |
|---|---|
| Frida | Native层检测到Frida后自杀 |
| objection | 基于Frida,同样被检测 |
| r0capture | 依赖Frida,且App可能不走系统代理 |
| Python静态分析 | SO文件被混淆,需完整逆向 |
七、可行解决方案对比
| 方案 | 难度 | 成功率 | 详细说明 |
|---|---|---|---|
| 修改APK移除反调试 | ⭐⭐⭐ | 90% | 解包→删除反调试smali代码→重打包签名→Frida注入 |
| 逆向SO库 | ⭐⭐⭐⭐⭐ | 70% | IDA Pro分析→定位AES函数→提取密钥→Python实现 |
| Hook Native函数 | ⭐⭐⭐⭐ | 60% | 过反调试后Hook Java_d_z_s_N_ns |
| Frida Gadget注入 | ⭐⭐⭐ | 85% | 将frida-gadget.so打包进APK,绕过端口检测 |
八、具体实施步骤(修改APK方案)
8.1 准备工具
# 下载apktool
wget https://github.com/iBotPeaches/Apktool/releases/download/v2.9.3/apktool_2.9.3.jar
# 下载uber-apk-signer
wget https://github.com/patrickfav/uber-apk-signer/releases/download/v1.3.0/uber-apk-signer-1.3.0.jar
8.2 解包
java -jar apktool_2.9.3.jar d com.dzhong.fhjc.apk -o fanhua_unpacked
8.3 搜索反调试代码
# 搜索isDebuggerConnected
grep -r "isDebuggerConnected" fanhua_unpacked/smali/
# 搜索Debug检测
grep -r "Debug" fanhua_unpacked/smali/ | grep -i "isDebugger"
# 搜索ptrace
grep -r "ptrace" fanhua_unpacked/smali/
8.4 删除反调试代码
找到相关smali文件后,删除或注释以下类型的代码:
# 典型反调试代码
invoke-static {}, Landroid/os/Debug;->isDebuggerConnected()Z
move-result v0
if-eqz v0, :cond_xxx
8.5 重打包
java -jar apktool_2.9.3.jar b fanhua_unpacked -o fanhua_mod.apk
8.6 签名
java -jar uber-apk-signer-1.3.0.jar -a fanhua_mod.apk
8.7 安装测试
adb install fanhua_mod-aligned-debugSigned.apk
8.8 Frida注入
frida -U -f com.dzhong.fhjc -l hook_decrypt.js
九、静态分析SO库方案(备选)
如需深入逆向 libdzst.so,可使用以下工具:
| 工具 | 用途 |
|---|---|
| IDA Pro 8.x | 反汇编、静态分析 |
| Ghidra | 免费替代方案 |
| Unicorn | 模拟执行 |
| Frida | 动态Hook(需过反调试) |
9.1 IDA Pro分析步骤
- 打开
libdzst.so,选择ARM64架构 - 找到导出函数
Java_d_z_s_N_ns - 分析函数流程,定位AES解密调用
- 提取密钥派生逻辑
- 识别加密模式(通过查找
AES_*_cbc或AES_*_ecb等函数)
十、已提取的关键数据
10.1 文件清单
D:\short_videos\
├── com.dzhong.fhjc.apk # 原始APK
├── apk_extract\lib\arm64-v8a\libdzst.so # Native库
├── fanhua_unpacked\ # apktool解包目录
├── strings.txt # SO提取的字符串
└── hook_decrypt.js # Frida脚本(未成功)
10.2 关键字符串
盐值: MySuperSecretSalt_DoNotLeak
JNI函数: Java_d_z_s_N_ns
哈希算法: SHA-256
签名相关: native_sign_, signatures, getPackageInfo
10.3 加密数据样例(hex,前128字节)
aab421b86a24fff2d4e2507828f3c90982bfb58502177abc699f3b4c2f3af02d
a7c38c44e1491b1cb812e89ec4e2b4de248f27580ea6dfb79d2eddb750ba005e
1c6ceee52f6337dc1d35c52d37d9807366e05cdea5af591e38edbc88b0a75f6b
848f63bd74b5a493d5891ad45062015fbc0257ef5469df79791dc1a228421064
十一、未解决问题
- AES加密模式:CBC、ECB、GCM还是CTR?
- IV生成方式:固定IV?还是从密钥派生?
- 签名数据组成:具体用了哪些数据作为签名输入?
- 自定义编码表:如何映射回标准Base64?
- 反调试精确位置:具体在SO的哪个函数中检测Frida?
十二、后续工作建议
如需继续推进,优先级如下:
- 修改APK移除反调试(最快验证方案)
- 用修改后的APK + Frida获取明文
- 对比多组加密数据和明文,反推算法
- 用Python实现独立解密脚本
十三、环境信息
| 项目 | 配置 |
|---|---|
| 操作系统 | Windows 11 |
| 模拟器 | 雷电模拟器 9.0 (Android 9) |
| 分析工具 | jadx-gui 1.5.5, Frida 17.9.1, apktool 2.9.3 |
| Python版本 | 3.10+ |
评论区