感谢deepseek
deepseek flash nb!

基于 APK pie_V2.5.3_release_06031753_ldy_253_0508.apk 分析
、
一、项目概览
| 项目 |
值 |
| 包名 |
com.fruit.goodgood |
| 版本号 |
2.5.3 (Code 15) |
| 技术栈 |
Flutter + Dart |
| 热更新 |
Shorebird |
| CPU 架构 |
仅 arm64-v8a |
二、抓包内容
从 Reqable 导出的请求/响应数据:
| 端口 |
请求路径 |
用途 |
| 4242 |
POST /v2_0/libraryDetail |
视频详情 API(加密) |
| 4243 |
POST /v2_0/universalAdvert |
广告列表 API(加密) |
三、API 加密机制
请求与响应的 body 均使用同一套加密方案:
{"encrypt_data":"...","encrypt_key":"..."}
3.1 算法拆解
| 层级 |
算法 |
作用 |
| 外层 |
RSA-PKCS1 (2048 bit) |
加密 AES 密钥 |
| 内层 |
AES-128-CBC + PKCS7 |
加密实际数据 |
| 密钥嵌套 |
RSA 解密结果 = {"key":"...","iv":"..."} |
内含 AES 密钥与 IV |
3.2 密钥文件位置
位于 assets/flutter_assets/assets/key/,包含客户端私钥与服务端公钥配对。
3.3 解密流程
响应 body: {"encrypt_key":"...", "encrypt_data":"..."}
│
▼
┌─────────────────────────────────────────────┐
│ ① RSA-PKCS1 解密 encrypt_key │
│ 用客户端私钥 │
│ 得到: {"key":"5+e/utNudfRV73FRdZz03w==", │
│ "iv":"ShO4AGaX/n79EkBb2K08pA=="} │
└─────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ ② AES-128-CBC/PKCS7 解密 encrypt_data │
│ 用上一步得到的 key 和 iv │
└─────────────────────────────────────────────┘
│
▼
原始 JSON 响应
3.4 解密示例
libraryDetail 接口(点击展开)
```json
{
"code": 1,
"msg": "请求成功",
"data": {
"library_id": 1997,
"title": "爱上同父异母的姐姐",
"film_id": "360780",
"detail": [{
"video_id": 3123,
"encryption_url": "(在线播放 HLS 路径)",
"download_url": "(mp4 相对路径)"
}]
}
}
```
universalAdvert 接口(点击展开)
```json
{
"code": 1,
"msg": "请求成功",
"data": {
"list": [
{"ad_id": 520, "title": "one游戏.710136", "outer_type_link": "https://example.com"},
{"ad_id": 552, "title": "<第一直播>", "outer_type_link": "https://example.com"},
...
]
}
}
```
</details>
---
## 四、Reqable 解密脚本
### 4.1 纯 Python 实现(零外部依赖)
核心逻辑:
**① RSA-PKCS1 解密(用 `pow` 实现模幂):**
```python
def _rsa_decrypt(ct_bytes, n, d):
ct = int.from_bytes(ct_bytes, 'big')
pt = pow(ct, d, n) # 模幂运算
plain = pt.to_bytes(256, 'big')
# PKCS#1 v1.5 去填充: 00 02 [随机] 00 [数据]
assert plain[0] == 0 and plain[1] == 2
i = 2
while plain[i] != 0:
i += 1
return plain[i+1:]
```
**② AES-128-CBC 解密(手写全部轮函数):**
实现了 AES 解密所需的 InvSubBytes、InvShiftRows、InvMixColumns、AddRoundKey,约 150 行。
**③ Reqable 适配:**
```python
def onResponse(context, response):
body_text = str(response.body) # HttpBody → str
if '"encrypt_key"' not in body_text:
return response # 跳过非加密响应
plain = decrypt_body(body_text)
response.body = plain.decode('utf-8')
response.headers['Content-Type'] = 'application/json; charset=utf-8'
return response
```
### 4.2 配置步骤
1. Reqable → **设置 → 脚本 → 添加脚本**
2. 粘贴脚本代码(需包含 RSA 私钥解析与 AES 解密)
3. 匹配规则:
- 域名:目标 API 域名
- 方向:**响应**
4. 保存后重新请求,响应体会自动变为明文 JSON
---
## 五、视频下载
### 5.1 下载直链
API 响应中 `detail[].download_url` 是**未加密的 mp4 直链**(相对路径)。
### 5.2 获取完整 URL
进入"我的下载"页面,暂停某条目后点"恢复下载",Reqable 会抓到完整的 mp4 链接。此时可看到实际 CDN 域名,拼接方式如下:
```
CDN域名 + download_url = 完整可下载链接
```
域名由 App 动态下发,API 中返回的 `download_url` 拼接同一域名即可下载其他视频。
---
## 六、API 域名
从 `libapp.so` 中提取到多组硬编码 API 域名,用于容灾切换。实际抓包中使用的域名来自同一套分发体系。
其他服务域名包括视频 CDN、对象存储(华为云 OBS)、统计分析(Matomo)和分享链接。
---
## 七、AOT 快照中的关键函数
| 符号 | 说明 |
|------|------|
| `package:fruitpie/utils/encrypt_tool.dart` | API 加解密工具 |
| `package:fruitpie/utils/video_decrypt.dart` | 视频解密 |
| `package:fruitpie/services/net_manager.dart` | 网络请求管理器 |
| `checkCDNDomain` | 动态解析 CDN 域名 |
| `_baseUrl` | API 基础 URL |
| `_normalizeDownloadUrl` | 下载 URL 拼接 |
| `_getVideoUrl` | 视频 URL 获取 |
---
## 八、使用到的工具
| 工具 | 用途 |
|------|------|
| APKTool / 解压 | 解包 APK |
| Reqable | HTTP 抓包 & 脚本解密 |
| PowerShell / .NET | 验证 RSA/AES 解密算法 |
| `libapp.so` 字符串搜索 | 提取 Dart 符号与硬编码字符串 |
| Python | 纯 Python 解密实现(零依赖)|
第一直播>