Loading

体育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`)的代码位置即为退出执行点

image
image


**第二步: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绕过
```

这是一条典型的"黑盒→白盒"分析链路,日志中的每一行信息都是定位保护逻辑的线索。
posted @ 2026-06-08 15:37  loz2015  阅读(7)  评论(0)    收藏  举报