体育apk重打包记录
# APK重打包后启动闪退分析 —— 深入逆向libapk-protect.so保护机制
## 一、问题现象:APK重打包后启动闪退
在对某游戏APK(包名:`com.qcfy01.xxxx`)进行重打包后,应用启动即闪退。通过 `adb logcat` 抓取日志,发现关键报错信息:
```
2026-06-08 14:28:51.995 17351-17351 apk_protect V RegisterNatives() --> ok
2026-06-08 14:28:52.273 17351-17351 apk_protect I z s
2026-06-08 14:28:52.301 17351-17351 apk_protect I json md5 2e8ac0c9fb4b4971aaad43d3cd5b9ddf, actual md5 1494E65A4528F7F9CAEFC35DD4F3A320
2026-06-08 14:28:52.301 17351-17351 apk_protect I single dex md5 not ok
2026-06-08 14:28:52.303 17351-17351 apk_protect I r m s
2026-06-08-14:28:52.303 17351-17351 apk_protect I json catmd5 b567516f51f05740e1a85b96605ab24c, actual catmd5 4CFB9F873A511558F75B73D7AD82D01A
2026-06-08 14:28:52.303 17351-17351 apk_protect I concat dex md5 not ok
2026-06-08 14:28:52.306 17351-17351 apk_protect I check not ok
2026-06-08 14:28:52.306 17351-17351 apk_protect I shame on you. A stupid hacker try cracking my game.
```
日志清晰地表明:**DEX文件的MD5校验失败**,应用检测到自身被篡改,主动终止运行。
---
## 二、定位日志:逐行分析保护流程
### 2.1 日志时间线还原
| 时间戳 | PID | Tag | 日志内容 | 含义 |
|--------|-----|------|----------|------|
| 14:28:51.995 | 17351 | apk_protect | `RegisterNatives() --> ok` | JNI动态注册成功 |
| 14:28:52.273 | 17351 | apk_protect | `z s` | 开始校验流程(混淆标记) |
| 14:28:52.301 | 17351 | apk_protect | `json md5 ..., actual md5 ...` | 读取JSON中的期望MD5,与实际MD5对比 |
| 14:28:52.301 | 17351 | apk_protect | `single dex md5 not ok` | 单个DEX的MD5校验失败 |
| 14:28:52.303 | 17351 | apk_protect | `r m s` | 进入拼接MD5校验(混淆标记) |
| 14:28:52.303 | 17351 | apk_protect | `json catmd5 ..., actual catmd5 ...` | 拼接DEX的MD5对比 |
| 14:28:52.303 | 17351 | apk_protect | `concat dex md5 not ok` | 拼接DEX的MD5校验失败 |
| 14:28:52.306 | 17351 | apk_protect | `check not ok` | 最终校验结果:不通过 |
| 14:28:52.306 | 17351 | apk_protect | `shame on you...` | 检测到篡改,应用退出 |
### 2.2 MD5校验对比
从日志可以直接提取两组MD5值:
**Single DEX MD5(单文件校验):**
| 项目 | 值 |
|------|----|
| JSON期望值 | `2e8ac0c9fb4b4971aaad43d3cd5b9ddf` |
| 实际计算值 | `1494E65A4528F7F9CAEFC35DD4F3A320` |
| 结果 | **不匹配** |
**Concat DEX MD5(拼接校验):**
| 项目 | 值 |
|------|----|
| JSON期望值 | `b567516f51f05740e1a85b96605ab24c` |
| 实际计算值 | `4CFB9F873A511558F75B73D7AD82D01A` |
| 结果 | **不匹配** |
两组校验均失败,说明重打包修改了`classes.dex`的内容,导致MD5值发生变化。
---
## 三、深入分析APK及SO修改方案
### 3.1 保护框架识别
通过JADX反编译APK,追踪保护加载链路:
```
AndroidManifest.xml
└─> Application: com.flamingo.sdk.main.ThirdSdkApplication
└─> attachBaseContext()
├─> System.loadLibrary("apk-protect") // 加载 libapk-protect.so
├─> NativeUtil.javaInit(context)
│ ├─> UnZipUtils.unzipDexFile() // 解压DEX到cache目录
│ └─> NativeUtil.init(context) // JNI native方法,执行校验
└─> 读取Manifest MetaData(gp_cp_appid等)
```
**关键Java类:**
- `ThirdSdkApplication` —— Application入口,加载SO
- `NativeUtil` —— 提供JNI桥接,`init()`和`doSomeThing()`为native方法
- `UnZipUtils` —— 解压APK中的DEX文件到缓存目录
- `MD5Helper` —— Java层的MD5计算工具类
- `InterfaceUtils` —— SO完整性校验(检查libapk-protect.so的MD5)
### 3.2 libapk-protect.so逆向分析
#### 3.2.1 SO基本信息
- 文件名:`libapk-protect.so`
- 架构:ARM64
- 内置库:**CryptoPP**(提供AES加解密、RSA、ECC等密码学算法)
- JSON解析库:**jsoncpp**
#### 3.2.2 导出函数
从SO的exports中定位到关键函数:
| 地址 | 函数名 | 说明 |
|------|--------|------|
| `0x202A08` | `JNI_OnLoad` | SO加载入口,执行RegisterNatives |
| `0x202728` | `init` | 核心校验函数(NativeUtil.init的JNI实现) |
| `0x202BC4` | `RegisterNatives` | 动态注册JNI方法 |
| `0x20E174` | `doSomeThing` | NativeUtil.doSomeThing的JNI实现 |
| `0x5A511` | `AESDecode` | AES解密函数 |
| `0x5A565` | `AESEncode` | AES加密函数 |
#### 3.2.3 关键字符串分析
通过提取SO的字符串表,梳理出完整保护逻辑涉及的所有关键字符串:
**校验相关:**
```
"third_encrypted_special_config.json" → AES加密的校验配置文件
"dex_count" → DEX文件数量
"dex_md5_list" → 各DEX的MD5列表
"dex_md5_md5" → DEX MD5的MD5
"protect_so_count" → 需校验的SO数量
"protect_so_md5_list" → 需校验的SO的MD5列表
"classes.dex" → 被校验的DEX文件名
"single dex md5 not ok" → 单DEX校验失败
"concat dex md5 not ok" → 拼接DEX校验失败
"check not ok" → 最终校验失败
"shame on you. A stupid hacker try cracking my game." → 篡改检测提示
```
**反调试相关:**
```
"TracerPid" → ptrace反调试检测
"/proc/%d/maps" → 内存映射监控(inotify)
"/proc/%d/status" → 进程状态检测
"/proc/%d/task/%d/status" → 线程状态检测
"/proc/net/tcp" → 网络端口检测(反frida)
"monitor_thread" → 后台监控线程
"inotify_init" → 文件系统变化监控
"inotify_add_watch" → 添加监控目标
```
**JNI注册相关:**
```
"RegisterNatives() --> ok" → JNI方法注册成功
"RegisterNatives() --> failed" → JNI方法注册失败
"com/flamingo/sdk/util/NativeUtil" → 注册的目标Java类
"init" → 注册的方法名
"doSomeThing" → 注册的方法名
```
**其他:**
```
"/data/data/" → 数据目录检测
"/cache/" → DEX缓存目录
"/cache/fake_" → 反Hook检测(检测fake框架)
"sourceDir" → APK路径获取
"getApplicationInfo" → 获取应用信息
"libapk-protect.so" → SO自身路径
"package_name" → 包名校验
"z s" / "r m s" → 混淆的日志标记
```
#### 3.2.4 保护逻辑流程图
```
┌─────────────────────────────────────────────────────────────┐
│ JNI_OnLoad (0x202A08) │
│ 1. RegisterNatives注册init/doSomeThing到NativeUtil类 │
│ 2. 创建monitor_thread后台监控线程 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ init() (0x202728) │
│ 1. 读取 assets/third_encrypted_special_config.json │
│ 2. AESDecode 解密JSON配置 │
│ 3. 提取 dex_count, dex_md5_list, dex_md5_md5 │
│ 4. 提取 protect_so_count, protect_so_md5_list │
│ 5. 获取APK路径(sourceDir) │
│ 6. 计算classes.dex的实际MD5 │
│ ├── 单个DEX MD5 对比 → "single dex md5 not ok" │
│ └── 所有DEX拼接MD5 对比 → "concat dex md5 not ok" │
│ 7. 校验libapk-protect.so自身的MD5 │
│ 8. 任何校验失败 → "check not ok" → 调用exit()终止进程 │
└──────────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ monitor_thread (后台线程) │
│ 1. inotify 监控 /proc/pid/maps(检测调试器附加) │
│ 2. 读取 /proc/pid/status 检查 TracerPid │
│ 3. 读取 /proc/net/tcp 检测frida-server端口 │
│ 4. 检测到异常 → 终止进程 │
└─────────────────────────────────────────────────────────────┘
```
### 3.3 SO修改方案
根据以上分析,有三种绕过保护的方案:
---
#### 方案一:修改加密配置文件(替换MD5值)
**原理:** 将`third_encrypted_special_config.json`中的期望MD5替换为重打包后的实际MD5。
**步骤:**
1. 计算重打包后DEX的实际MD5(日志已给出):
- Single MD5: `1494E65A4528F7F9CAEFC35DD4F3A320`
- Concat MD5: `4CFB9F873A511558F75B73D7AD82D01A`
2. 解密JSON配置文件:
- 配置文件使用AES加密,密钥硬编码在SO中
- 可以通过Frida Hook `AESDecode` 函数,在运行时dump解密后的明文
3. 修改JSON中的MD5字段,重新AES加密后替换回APK
4. 同时需要更新`libapk-protect.so`自身的MD5(如果配置中包含)
**优缺点:**
- 优点:不修改SO,隐蔽性较好
- 缺点:需要获取AES密钥,流程较复杂
---
#### 方案二:Patch SO校验逻辑(推荐)
**原理:** 直接修改`libapk-protect.so`中的校验分支,使MD5比较永远返回"通过"。
**步骤:**
**第一步:定位校验函数**
`init`函数地址为`0x202728`,在该函数中搜索对关键字符串的引用:
- 引用`"check not ok"`(`0x17cd30`)的代码位置即为校验判断点
- 引用`"shame on you..."`(`0x17f3a7`)的代码位置即为退出执行点


**第二步:Patch校验分支**
使用IDA Pro或Ghidra打开SO,在`init`函数中:
```arm64
; 原始代码(伪代码):
CMP W8, #0 ; 比较校验结果
B.EQ pass_check ; 如果MD5匹配则跳转通过
; ↓ 校验失败路径
BL exit ; 调用exit()退出
; Patch方案1:修改条件跳转为无条件跳转
B pass_check ; 强制跳转到校验通过分支(将B.EQ改为B)
; Patch方案2:NOP掉exit调用
NOP ; 将BL exit替换为NOP
NOP
```
**第三步:Patch monitor_thread**
反调试线程也需要处理,定位`monitor_thread`的线程函数:
- 将`inotify_add_watch`调用NOP掉
- 或将线程函数中的检测分支强制走"安全"路径
**第四步:修复SO**
由于SO可能也校验自身完整性(`protect_so_md5_list`),还需要:
- 同时修改配置中的SO MD5值,或
- Patch掉SO自身MD5校验的判断分支
**优缺点:**
- 优点:一劳永逸,修改后无需运行时干预
- 缺点:需要ARM64汇编基础,需仔细定位所有校验点
---
#### 方案三:运行时Hook(Frida脚本)
**原理:** 在运行时Hook关键系统调用,使校验逻辑返回"通过"。
**Frida脚本示例:**
```javascript
// Hook MD5比较:让strcmp在比较32位十六进制字符串时返回0
var strcmp = Module.findExportByName("libc.so", "strcmp");
Interceptor.attach(strcmp, {
onEnter: function(args) {
var s1 = args[0].readUtf8String();
var s2 = args[1].readUtf8String();
if (s1 && s2 && s1.length === 32 && s2.length === 32 &&
/^[0-9a-fA-F]{32}$/.test(s1)) {
this.isMd5Compare = true;
}
},
onLeave: function(retval) {
if (this.isMd5Compare) {
retval.replace(0); // 强制返回0(匹配)
}
}
});
// Hook exit/abort:阻止进程退出
var exit = Module.findExportByName("libc.so", "exit");
Interceptor.replace(exit, new NativeCallback(function(status) {
console.log("[*] exit(" + status + ") blocked!");
}, 'void', ['int']));
// Hook memcmp:让MD5内存比较始终相等
var memcmp = Module.findExportByName("libc.so", "memcmp");
Interceptor.attach(memcmp, {
onLeave: function(retval) {
// 如果比较长度为16(MD5摘要长度),强制返回0
if (this.compareLen === 16) {
retval.replace(0);
}
},
onEnter: function(args) {
this.compareLen = args[2].toInt32();
}
});
```
**优缺点:**
- 优点:无需修改文件,快速验证
- 缺点:每次运行都需要注入,容易被反Hook检测
---
## 四、总结
本文通过一次APK重打包闪退问题,完整分析了`libapk-protect.so`的保护机制:
1. **DEX完整性校验**:通过AES加密的JSON配置存储期望MD5,运行时计算实际MD5进行对比
2. **SO自身校验**:校验`libapk-protect.so`的MD5,防止SO被修改
3. **反调试保护**:TracerPid检测、inotify监控maps、TCP端口扫描
4. **后台监控线程**:持续运行的反调试检测
该保护方案的核心强度在于:**校验逻辑全部在Native层实现,使用AES加密存储期望值,并有持续监控线程**。但核心弱点也很明显:**校验结果最终要决定是否exit,只要找到这个判断分支并Patch掉,整个保护就失效了**。
逆向分析的关键路径:
```
日志 → 字符串定位 → 函数定位 → 逻辑分析 → Patch绕过
```
这是一条典型的"黑盒→白盒"分析链路,日志中的每一行信息都是定位保护逻辑的线索。

浙公网安备 33010602011771号