古文岛逆向实战——工具详解

免责声明

本文仅记录在本人设备和合法取得样本上的学习研究过程,用于讨论 Android 应用结构、逆向分析工具和运行时调试方法。请仅在拥有软件、设备或明确授权的前提下复现。不得将文中工具和方法用于绕过付费机制、侵害著作权或开发者权益、传播修改版应用,亦不得用于任何违法用途。文中命令、接口与测试结果仅反映特定版本和环境,不构成长期有效性或安全性保证。因不当使用造成的损失或法律责任,由使用者自行承担。

本文详细介绍在古文岛 3.2.0 逆向过程中用到的每一个工具,包括它是什么、怎么工作、怎么安装、怎么用、以及在实际任务中起到了什么作用。


一、Python 标准库( zipfile / sqlite3 / lzma )

1.1 zipfile — APK 解压与文件操作

是什么:Python 内置的 ZIP 文件操作库。APK 本质上就是一个 ZIP 文件。

为什么用它:不需要安装 WinRAR/7-Zip,脚本里直接操作 APK 文件——解压、读取、列出文件列表、甚至重新打包。

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 1. 列出 APK 里的所有 .so 和 .dex 文件(判断技术栈)
import zipfile
z = zipfile.ZipFile('古文岛 3.2.0(320).apk')
for n in z.namelist():
if n.endswith('.dex') or n.endswith('.so'):
info = z.getinfo(n)
print(f'{info.file_size:>12} {n}')

# 2. 提取 libapp.so 到本地分析
z.extract('lib/arm64-v8a/libapp.so', 'extract_dir')

# 3. 验证重打包后的 APK 内容
z = zipfile.ZipFile('module.apk')
for f in ['lib/arm64-v8a/libfrida-gadget.so', 'assets/xposed_init']:
try:
print(f'{f}: {z.getinfo(f).file_size} bytes')
except KeyError:
print(f'{f}: MISSING!')

安装:Python 自带,无需安装。


1.2 sqlite3 — 数据库分析

是什么:Python 内置的 SQLite 数据库操作库。

为什么用它:Flutter 应用用 sqflite 插件做本地存储,底层就是 SQLite。直接用 Python 读数据库文件,查看表结构、数据内容,甚至修改数据。

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 1. 分析 shared_flutter.db 的表结构
import sqlite3
db = sqlite3.connect('shared_flutter.db')
cur = db.cursor()
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
print('Tables:', [t[0] for t in cur.fetchall()])

# 2. 查看 user 表(发现 VIP 字段!)
cur.execute('SELECT * FROM user')
rows = cur.fetchall()
cols = [d[0] for d in cur.description]
# 输出: cols = ['id', 'pwd', 'email', ..., 'vipTimeSpan', 'svipTimeSpan', ...]
# vipTimeSpan: 1658678400000 → 2022-07-25 (已过期)
# svipTimeSpan: 946656000000 → 2000-01-01 (早已过期)

# 3. 直接修改数据库验证效果
db.execute('UPDATE user SET svipTimeSpan=9999999999999, vipTimeSpan=9999999999999')
db.commit()
# 推回手机 → app 显示 SVIP!

# 4. 查看表结构(PRAGMA)
cur.execute('PRAGMA table_info(user)')
for col in cur.fetchall():
print(f'{col[1]:25s} {col[2]}')

关键发现:通过 sqlite3 发现了 user 表里的 vipTimeSpan 和 svipTimeSpan 字段,这是整个 LSPosed 方案的基础。

注意事项:如果 app 正在运行,数据库数据可能在 WAL 文件里(.db-wal),需要 pull 主 db + wal + shm 三个文件才能读到完整数据。

安装:Python 自带。


1.3 lzma — 解压 XZ 压缩文件

是什么:Python 内置的 LZMA/XZ 解压库。

为什么用它:frida-server 和 frida-gadget 从 GitHub 下载的是 .xz 压缩文件,需要解压。

1
2
3
import lzma
data = lzma.open('frida-server-16.7.19-android-arm64.so.xz').read()
open('frida-server-16', 'wb').write(data)

安装:Python 自带。


二、androguard — APK 分析库

是什么:一个纯 Python 的 Android APK 分析库,能解析 APK 的 manifest、dex、资源,不需要 aapt 或 Android SDK。

为什么用它:

  • 不需要安装 Android SDK
  • 脚本化操作,可以批量分析
  • 能读取二进制 AndroidManifest.xml(MT 管理器和文件管理器都读不了)

安装:

1
pip install androguard

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 1. 读取 APK 基本信息
from androguard.core.apk import APK
a = APK('古文岛 3.2.0(320).apk')
print('package:', a.get_package()) # com.guwendao.gwd
print('app name:', a.get_app_name()) # 古文岛解锁
print('Application class:', a.get_attribute_value('application', 'name')) # com.guwendao.gwd.App
print('min sdk:', a.get_min_sdk_version()) # 26
print('target sdk:', a.get_target_sdk_version()) # 36

# 2. 解析 AndroidManifest(二进制 XML → 可读文本)
xml = a.get_android_manifest_axml().get_xml().decode('utf-8')
# 确认 LSPosed 模块的 meta-data 是否正确编译进 APK
if 'xposedmodule' in xml:
print('LSPosed 模块声明: OK')

# 3. 搜索 dex 里的类
from androguard.core.dex import DEX
d = DEX(open('classes.dex','rb').read())
for cls in d.get_classes():
name = cls.get_name()
if 'guwendao' in name:
print(f'{name} extends {cls.get_superclassname()}')
# 找到: Lcom/guwendao/gwd/App; extends Llocal/z/androidshared/AppShared;

# 4. 搜索 OkHttp 等第三方库
for cls in d.get_classes():
if 'okhttp3' in cls.get_name() or 'ResponseBody' in cls.get_name():
print(cls.get_name())

踩坑:androguard 4.x 默认输出大量 DEBUG 日志。需要在代码开头加:

1
2
import logging; logging.disable(logging.CRITICAL)
from loguru import logger; logger.remove()

三、apktool — APK 反编译/重编译

是什么:Android 逆向最基础的工具。把 APK 反编译成 smali(Dalvik 字节码的可读形式)+ 资源文件,修改后再重新编译成 APK。

工作原理:

1
2
3
APK → apktool d → smali 代码 + 资源 + manifest(可读)
修改 smali / 资源
APK ← apktool b ← 修改后的 smali + 资源

安装:

1
2
3
4
5
# 下载 jar 文件
curl -sL "https://github.com/iBotPeaches/Apktool/releases/download/v2.9.3/apktool_2.9.3.jar" -o apktool.jar

# 需要 Java 运行环境
java -jar apktool.jar --version

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 1. 反编译古文岛 APK
java -jar apktool.jar d "古文岛 3.2.0(320).apk" -o repack_src -f

# 反编译后的目录结构:
# repack_src/
# ├── AndroidManifest.xml ← 可读的 XML
# ├── smali/ ← classes.dex 反编译的 smali
# │ └── com/guwendao/gwd/
# │ ├── App.smali ← Application 类
# │ ├── MsgActivity.smali
# │ └── R$*.smali
# ├── smali_classes2/ ← classes2.dex 反编译的 smali
# ├── lib/ ← native 库
# │ └── arm64-v8a/
# │ ├── libapp.so
# │ ├── libflutter.so
# │ └── ...
# ├── assets/ ← Flutter 资源
# └── res/ ← Android 资源

# 2. 修改 App.smali(注入 hook 调用)
# 在 onCreate 方法里加入: invoke-static {p0}, VipHook->init(Context)

# 3. 添加新文件(frida-gadget.so, hook.js, VipHook.smali)

# 4. 重新编译
java -jar apktool.jar b repack_src -o output.apk

踩过的坑:

  1. 中文路径问题:apktool 的资源编译器 aapt 对中文路径编码有问题。目录名含中文(如”古文岛解锁”)会报 failed to open directory 错误。解决:把工作目录移到纯英文路径(如 /c/gwd_unlock/)。

  2. smali 语法严格:寄存器数目、方法签名、标签格式都很严格。一个 getResult() 写成字段访问而不是方法调用,编译就报 mismatched input '(' expecting COLON。

  3. apktool 缓存:修改 smali 后编译可能用了缓存。需要 rm -rf build/ 或加 --use-aapt2 强制重编译。


四、uber-apk-signer — APK 签名工具

是什么:一个 Java 的 APK 自动签名工具,集成了 zipalign + apksigner,一条命令完成对齐和签名。

为什么用它而不是 jarsigner:

  • jarsigner 需要 JDK(我们只有 JRE)
  • uber-apk-signer 只需要 JRE
  • 自动 zipalign + v2/v3 签名
  • 自动验证签名

安装:

1
curl -sL "https://github.com/patrickfav/uber-apk-signer/releases/download/v1.3.0/uber-apk-signer-1.3.0.jar" -o uber-apk-signer.jar

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 生成签名 keystore(用 keytool,JRE 自带)
keytool -genkeypair -alias unlock -keypass "$KEYSTORE_PASSWORD" \
-keystore unlock.keystore -storepass "$KEYSTORE_PASSWORD" \
-keyalg RSA -keysize 2048 -validity 10000 \
-dname "CN=Unlock,O=Unlock,C=CN"

# 2. 签名 APK
java -jar uber-apk-signer.jar -a module.apk \
--ks unlock.keystore --ksAlias unlock \
--ksPass "$KEYSTORE_PASSWORD" --ksKeyPass "$KEYSTORE_PASSWORD"

# 输出:
# - zipalign success
# - sign success
# - signature verified [v2, v3]
# Successfully processed 1 APKs

签名后的文件名:module-aligned-signed.apk(自动加了 -aligned-signed 后缀)


五、ida-pro-mcp (idalib) — IDA Pro MCP 服务

是什么:mrexodia/ida-pro-mcp 是一个把 IDA Pro 通过 MCP (Model Context Protocol) 暴露给 AI 的工具。AI 可以通过 JSON-RPC 调用 IDA 的分析功能——反编译、搜索、交叉引用等。

核心架构:

1
2
3
4
5
6
7
8
9
┌─────────────┐     MCP/JSON-RPC      ┌──────────────┐
│ AI 客户端 │ ←──────────────────→ │ idalib_server│
│ (ZCode等) │ SSE over HTTP │ (headless) │
└─────────────┘ └──────┬───────┘
│ 内嵌调用
┌──────┴───────┐
│ IDA idalib │
│ (分析引擎) │
└──────────────┘

关键特点:

  • 使用 idalib(headless SDK),不启动 IDA GUI
  • 跑在一个 python.exe 进程里
  • 65 个 MCP 工具(反编译、搜索、patch 等)
  • 通过 SSE (Server-Sent Events) 通信

安装与启动:

1
2
3
4
5
6
7
8
9
# ida-pro-mcp 安装在 Claude 插件目录
cd ~/.claude/plugins/cache/mrexodia/ida-pro-mcp/0.1.0

# 启动 server(加载 libapp.so)
.venv/Scripts/python.exe -m ida_pro_mcp.idalib_server \
--host 127.0.0.1 --port 1960 --verbose \
"C:/path/to/libapp.so"

# 等待 ~90 秒,server_health 返回 status:ok

通信协议:

1
2
3
4
5
6
7
8
9
10
11
1. 客户端 GET /sse → 建立 SSE 长连接
2. 服务端返回 data: /sse?session=xxx(endpoint 路径)
3. 客户端 POST /sse?session=xxx → 发送 JSON-RPC 请求
{
"method": "tools/call",
"params": {
"name": "decompile",
"arguments": {"addr": "0x9d13b0"}
}
}
4. 服务端通过 SSE 流返回响应

在本次任务中使用的工具:

工具 用途 实际调用
server_health 检查分析是否完成 轮询直到 status:ok
survey_binary 全景分析 获得函数数(40772)、字符串数(261907)、段表
find_bytes 字节搜索 搜 “https://“ 的 hex,找到 18 个 URL
get_bytes 读取内存 批量读取 URL 地址的字节,解码出完整 URL
decompile Hex-Rays 反编译 反编译 4 个热点函数,全是 Dart VM stub
list_funcs 枚举函数 确认 40772 个函数全部 unnamed
imports 查看 imports 确认为空(符号剥离)
search_text 文本搜索 搜 VIP 字段名(strings cache 构建很慢)

survey_binary 返回的关键信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"statistics": {
"total_functions": 40772,
"named_functions": 1,
"total_strings": 261907
},
"segments": [
{"name": ".rodata", "start": "0x340", "end": "0x9b11e0"}, // Dart snapshot data
{"name": ".text", "start": "0x9c0000", "end": "0x1320860"} // 机器码
],
"entrypoints": [
{"addr": "0x9c0000", "name": "_kDartVmSnapshotInstructions"},
{"addr": "0x340", "name": "_kDartVmSnapshotData"}
]
}

踩坑:

  • idb_open 的参数名是 input_path 不是 path
  • find_bytes 的参数名是 patterns(复数)不是 pattern
  • get_bytes 的参数名是 regions(数组)不是 addr+size
  • search_text 需要 strings cache 构建(很慢,261907 条字符串)
  • idalib server 有 idle TTL,长时间不调用会自动退出

六、Python bytes search — 直接二进制搜索

是什么:不用任何工具,直接用 Python open().read() 读取 .so 文件的原始字节,用 data.find() 搜索。

为什么用它而不用 IDA:

  1. 速度:Python 直接读字节比 IDA 的 strings cache 快得多
  2. 绕过 Dart 字符串格式:Dart 的 OneByteString 有对象头,IDA 的 strings 引擎识别不全,但 data.find() 是原始字节匹配,不管格式
  3. 简单:几行 Python 就搞定

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data = open('libapp.so', 'rb').read()

# 搜索所有 VIP 相关字段名
for pat in [b'isVIP', b'isSVIP', b'isSVIPOnPlatform', b'isPermanentSVIP',
b'getSVIPRemainDays', b'verifyPayCompleted', b'getUserInfo']:
idx = data.find(pat)
if idx != -1:
# 读取上下文(前后各几十字节)
ctx = data[max(0, idx-30):idx+100]
s = ''.join(chr(b) if 32 <= b < 127 else '.' for b in ctx)
print(f'{pat.decode():30s} @ 0x{idx:x}: {s}')

# 搜索所有 .aspx API 路径
import re
for m in re.finditer(rb'[a-zA-Z0-9_/]+\.aspx', data):
print(f' 0x{m.start():x}: {m.group().decode()}')

发现的关键信息:

  • VIP 字段名:isVIP、isSVIP、isSVIPOnPlatform、isPermanentSVIP、isVipRequired
  • API 路径:router/user/getUserInfo.aspx、Ali/AliCheck.aspx、Wechat/WxCheck.aspx
  • Dart 源码路径:package:flutter_module/mod/vip/svip_page/svip_page_controller.dart
  • Docker snapshot build id:1ce86630892e2dca9a8543fdb8ed8ed8
  • 编译标记:product compressed-pointers dedup_instructions

七、adb (Android Debug Bridge) — 手机调试

是什么:Android 官方的调试工具,通过 USB 连接手机,执行命令、安装应用、查看日志、传输文件。

在本次任务中的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 1. 设备管理
adb devices # 列出连接的设备
adb kill-server && adb start-server # 重启 adb daemon

# 2. 应用安装/卸载
adb install -r module.apk # 安装(-r 覆盖安装)
adb uninstall com.guwendao.gwd # 卸载

# 3. 启动/停止应用
adb shell am force-stop com.guwendao.gwd # 强制停止
adb shell monkey -p com.guwendao.gwd -c android.intent.category.LAUNCHER 1 # 启动

# 4. 日志查看(最常用)
adb logcat -c # 清空日志
adb logcat -d # 打印所有日志
adb logcat -d | grep "GWDUnlock" # 过滤关键词
adb logcat -d --pid=$(adb shell pidof com.guwendao.gwd) # 只看某进程

# 5. 文件传输
adb push local_file /data/local/tmp/remote_file # 推到手机
adb pull /data/local/tmp/remote_file local_file # 从手机拉

# 6. root 操作(需要 KernelSU/Magisk)
adb shell "su -c 'cat /data/data/com.guwendao.gwd/shared_prefs/*.xml'"
adb shell "su -c 'cp /data/data/com.guwendao.gwd/app_flutter/shared_flutter.db /sdcard/'"

# 7. 端口转发(frida 通信)
adb forward tcp:27042 tcp:27042

踩坑:

  • Git Bash 的路径转换会破坏 adb 的参数。/data/local/tmp/ 被 Git Bash 转成 D:/Program Files/Git/data/local/tmp/。解决:用 // 前缀或 MSYS_NO_PATHCONV=1
  • adb install 在 MIUI/HyperOS 上默认拒绝安装。需要在手机设置里开启「USB安装」
  • adb shell pm list packages | grep xxx 可以快速检查应用是否安装

八、LSPosed — Java 层 Hook 框架

是什么:LSPosed 是 Xposed 框架的现代继承者,基于 Zygisk/Riru 注入。它能在 app 启动时注入 hook 代码到目标进程,拦截/替换 Java 方法的调用。

工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
手机开机
↓
Zygisk 注入 LSPosed daemon 到 Zygote 进程
↓
每个 app 启动时,LSPosed 检查该 app 是否在某个模块的作用域内
↓
如果是,加载模块的 dex 到 app 进程
↓
调用模块的 handleLoadPackage() 方法
↓
模块用 XposedHelpers.findAndHookMethod() 注册 hook
↓
app 运行时,被 hook 的方法调用会先经过模块的回调

LSPosed 模块的结构:

1
2
3
4
module.apk
├── AndroidManifest.xml ← 必须声明 xposedmodule meta-data
├── assets/xposed_init ← 文件内容: 入口类全名
└── classes.dex ← 编译后的 hook 代码

AndroidManifest.xml 必须包含:

1
2
3
<meta-data android:name="xposedmodule" android:value="true" />
<meta-data android:name="xposeddescription" android:value="模块描述" />
<meta-data android:name="xposedminversion" android:value="93" />

Xposed Hook API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// hook 静态方法
XposedHelpers.findAndHookMethod(
"类全名", // 如 "android.database.sqlite.SQLiteDatabase"
classLoader, // 目标 app 的 ClassLoader
"方法名", // 如 "execSQL"
参数类型1, // 如 String.class
参数类型2, // 如 Object[].class
new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) {
// 方法执行前调用
// param.args[0] = 第一个参数
// param.thisObject = 调用对象
// 可以修改 param.args 或 param.setResult()
}

@Override
protected void afterHookedMethod(MethodHookParam param) {
// 方法执行后调用
// param.getResult() = 返回值
// param.setResult(新值) = 修改返回值
}
}
);

在本次任务中的使用:

hook 了 SQLiteDatabase.execSQL(String, Object[]) 方法:

  • beforeHookedMethod:检查 SQL 是否含 svipTimeSpan/vipTimeSpan,遍历 Object[] bindArgs,把 Long 值替换成 9999999999999
  • afterHookedMethod:检查 SQL 是否是 INSERT/UPDATE/REPLACE user 表,如果是则执行 UPDATE user SET svipTimeSpan=9999999999999(带防递归标志位)

LSPosed 的限制:

  • 只能 hook Java 层方法
  • 不能 hook Native 层(C/C++ 函数)
  • 不能 hook Dart 层(Flutter 的 Dart 代码不走 Java 虚拟机)
  • 这就是为什么会员中心页面(走 Dart 网络层)无法被拦截

九、KernelSU — 内核级 Root

是什么:KernelSU 是一种基于 Linux 内核的 root 方案,直接在内核层面实现 su 权限管理和模块系统。相比 Magisk(用户空间),KernelSU 更隐蔽、更难被检测。

在本次任务中的使用:

  1. 提供 root 权限:adb shell "su -c '命令'" 执行需要 root 的操作(读取 /data/data 下的文件)
  2. 模块系统:创建了 KernelSU 模块(post-fs-data.sh + service.sh),开机自动把 hook.js 放到 /data/local/tmp/
  3. SELinux 控制:su -c 'setenforce 0' 切换到 Permissive 模式(frida 注入需要)
1
2
3
4
# 读取 app 私有数据(需要 root)
adb shell "su -c 'ls /data/data/com.guwendao.gwd/databases/'"
adb shell "su -c 'cat /data/data/com.guwendao.gwd/shared_prefs/*.xml'"
adb shell "su -c 'cp /data/data/com.guwendao.gwd/app_flutter/shared_flutter.db /sdcard/'"

KernelSU 模块结构:

1
2
3
4
5
6
7
module.zip
├── module.prop ← 模块信息(id, name, version, description)
├── post-fs-data.sh ← 开机早期执行(文件系统挂载后)
├── service.sh ← 开机完成后执行
└── system/ ← systemless 替换的系统文件
└── etc/
└── guwendao_hook.js

十、frida-server / frida-gadget — 运行时 Hook

10.1 frida-server

是什么:frida 的服务端程序,以 root 权限运行在手机上,接受 PC 端 frida CLI 的指令,向目标进程注入 agent。

工作原理:

1
2
3
4
5
6
7
PC 端 frida CLI  ←→  手机 frida-server  ←→  目标 app 进程
| | |
|--- spawn/attach ---→|---- ptrace --------→|
| |---- inject agent ----→|
|--- load script ----→|---- execute JS ----→|
| | |
|←-- console.log -----|←-- messages --------|

在本次任务中的使用(失败):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 推送 frida-server
adb push frida-server /data/local/tmp/frida-server
adb shell "su -c 'chmod 755 /data/local/tmp/frida-server'"
adb shell "su -c 'nohup /data/local/tmp/frida-server -D &'"

# PC 端注入
frida -U -f com.guwendao.gwd -l hook.js
# 或用 Python API
import frida
device = frida.get_usb_device()
pid = device.spawn(['com.guwendao.gwd'])
session = device.attach(pid)
script = session.create_script(js_code)
script.load()
device.resume(pid)

失败原因:

  • frida 17.19.0:spawn 后 attach 失败 agent connection closed unexpectedly
  • frida 16.7.19:attach 超时 TimedOutError
  • 崩溃日志显示 signal 11 (SIGSEGV), x8 = 0x46524944 ("FRID") — frida 注入导致 null pointer dereference
  • Android 15 (SDK 35) + HyperOS 的安全机制限制了 frida 的 ptrace 注入

10.2 frida-gadget

是什么:frida 的一个特殊共享库(.so),嵌入到 APK 里,app 启动时 System.loadLibrary 加载它,自动执行 JS 脚本。不需要 frida-server、不需要 root、不需要电脑。

工作原理:

1
2
3
4
5
6
7
8
9
app 启动
↓
App.onCreate() → System.loadLibrary("frida-gadget")
↓
gadget 读取 libfrida-gadget.config.so(JSON 配置)
↓
根据 config 加载指定的 JS 脚本
↓
在 app 进程内执行 hook(SSL_read, SharedPreferences 等)

config 格式:

1
2
3
4
5
6
7
{
"interaction": {
"type": "script",
"path": "/data/local/tmp/hook.js",
"on_change": "reload"
}
}

JS hook 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// hook SSL_read — 拦截所有 HTTPS 响应
var ssl_read = Module.findExportByName(null, "SSL_read");
Interceptor.attach(ssl_read, {
onLeave: function(retval) {
var n = retval.toInt32();
var data = Memory.readUtf8String(this.buf, n);
if (data && data.indexOf('"isSVIP"') !== -1) {
// 篡改 JSON: "isSVIP":false → "isSVIP":true
var patched = data.replace('"isSVIP":false', '"isSVIP":true ');
Memory.writeUtf8String(this.buf, patched);
}
}
});

// hook SharedPreferences
Java.perform(function() {
var SP = Java.use("android.app.SharedPreferencesImpl");
SP.getBoolean.implementation = function(key, def) {
if (key.toLowerCase().indexOf("vip") !== -1) return true;
return this.getBoolean(key, def);
};
});

在本次任务中的使用(失败):

  • gadget .so 成功加载(logcat 确认 Load libfrida-gadget.so ... ok)
  • 但 config 文件没被读取——gadget 回退到 listen 模式而非 script 模式
  • 导致 JS 脚本没执行,hook 没生效
  • 可能原因:Android 15 的 native library loading 机制变化,gadget 无法通过 dladdr 找到 config 文件路径

十一、strings(二进制字符串提取)

是什么:Unix 工具,从二进制文件中提取可打印字符串。

在本次任务中的使用:

1
2
3
4
5
6
# 检查 dex 是否有业务代码
strings classes.dex | grep -iE "vip|member|guwendao"
# 结果:空(证明 dex 是空壳)

# 检查 libflutter.so 确认 Flutter 版本
strings libflutter.so | grep -iE "dart.*version|flutter.*version"

限制:对 Dart AOT 的 OneByteString 不完全有效——Dart 字符串有对象头(不是以 \0 结尾的标准 C 字符串),strings 可能漏掉一些。所以还是 Python data.find() 更可靠。


十二、Git Bash 的问题

不是工具,但严重影响工作效率。

Git Bash 在 Windows 上有一个路径自动转换的问题:当你写 /data/local/tmp/,Git Bash 会把它转换成 Windows 路径 D:/Program Files/Git/data/local/tmp/。

1
2
3
4
5
6
7
8
9
10
11
12
# 错误 — 被转换了
adb push file /data/local/tmp/file
# Error: remote couldn't create file: D:/Program Files/Git/data/local/tmp/file

# 解决 1 — 用 // 前缀
adb push file //data/local/tmp/file

# 解决 2 — 设置环境变量
MSYS_NO_PATHCONV=1 adb push file /data/local/tmp/file

# 解决 3 — 用 Windows 路径
adb push "C:\gwd_unlock\file" /data/local/tmp/file

工具链全览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
┌─ 第一步:判断技术栈 ─────────────────────────────┐
│ Python zipfile → 看 lib 目录 → 发现 Flutter │
│ strings → 搜 dex → 确认空壳 │
└───────────────────────────────────────────────────┘
↓
┌─ 第二步:分析 libapp.so ──────────────────────────┐
│ ida-pro-mcp (idalib) → survey_binary → 全景图 │
│ Python bytes search → 搜 VIP 字段名、API 路径 │
│ ida-pro-mcp decompile → 反编译热点函数 │
└───────────────────────────────────────────────────┘
↓
┌─ 第三步:找 VIP 存储 ─────────────────────────────┐
│ adb + root → 读 /data/data/ 下的文件 │
│ sqlite3 → 分析 shared_flutter.db → 发现 user 表 │
│ sqlite3 → UPDATE user SET svipTimeSpan=... → 验证│
└───────────────────────────────────────────────────┘
↓
┌─ 第四步:写 hook ─────────────────────────────────┐
│ apktool → 反编译 APK → smali │
│ 手写 smali → MainHook + SqlHook + CvHook │
│ apktool → 重新编译 │
│ uber-apk-signer → 签名 │
│ adb install → 安装 │
└───────────────────────────────────────────────────┘
↓
┌─ 第五步:测试与调试 ──────────────────────────────┐
│ adb shell monkey → 启动 app │
│ adb logcat → 看 GWDUnlock 日志 │
│ 根据 VerifyError / 卡死 / 不生效 → 修 smali → 重编译│
└───────────────────────────────────────────────────┘

每个工具都有它的适用场景和局限。逆向工程的核心能力不是学会某个工具,而是知道在什么情况下用什么工具,以及当工具失效时怎么换思路。