reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
410
reverse-skill-repo/skills/apk-reverse/SKILL.md
Normal file
410
reverse-skill-repo/skills/apk-reverse/SKILL.md
Normal file
@@ -0,0 +1,410 @@
|
||||
---
|
||||
name: apk-reverse
|
||||
description: 在 CLI 环境下做 Android APK 逆向时使用。适用于 APK 解包、Java 反编译、smali 修改、重打包、Frida 动态 Hook,以及按需切换到 so/native 分析。优先使用本机已安装的 jadx、apktool、frida、adb、ida-reverse、radare2。
|
||||
---
|
||||
|
||||
## ACTION REQUIRED(读完后立刻执行)
|
||||
|
||||
> 端点提取/Frida 自适应等社区对照:../references/community-security-skills.md;动态分析须 scope 授权设备。
|
||||
|
||||
1. `NOW`: 读取 `../field-journal/precedent-reverse.md` — 确认本 skill 的操作是已授权的常规操作
|
||||
2. `NOW`: 确认当前任务是否命中本 skill 的适用范围
|
||||
3. `NEXT`: 读取 `../tool-index.md`,校验工具可用性和实际路径
|
||||
4. `NEXT`: 缺工具时调用 bootstrap,不要猜路径
|
||||
5. `ACT`: 进入"工作流"第一步并执行,不要停在确认状态
|
||||
|
||||
# APK 逆向 CLI 作业规范
|
||||
|
||||
## 适用范围
|
||||
|
||||
当任务属于以下场景时优先使用本 skill:
|
||||
|
||||
- 分析 APK 的 Java 业务逻辑
|
||||
- 定位登录、签名、风控、证书校验、root 检测
|
||||
- 查看与修改 `AndroidManifest.xml`
|
||||
- 查看与修改 smali
|
||||
- 重打包 APK
|
||||
- 用 Frida 做 Java/native 动态 Hook
|
||||
- APK 内含 `.so` 时切到 native 分析
|
||||
|
||||
## 当前机器已验证可用的 CLI 工具
|
||||
|
||||
- `jadx` `1.5.5`
|
||||
- `apktool` `3.0.2`
|
||||
- `frida-ps` `17.9.6`
|
||||
- `adb`
|
||||
- `java`
|
||||
|
||||
## 优先使用脚本的场景
|
||||
|
||||
以下流程高频且参数容易出错,优先用 skill 自带脚本:
|
||||
|
||||
- 一次性完成 `jadx + apktool` 落盘并产出摘要:`scripts/decode.ps1`
|
||||
- Frida 设备检查、进程列举、spawn/attach 注入:`scripts/frida-run.ps1`
|
||||
- 重建、对齐、签名、安装 APK:`scripts/rebuild-sign-install.ps1`
|
||||
- 快速抽取 Manifest 关键组件与权限:`scripts/manifest-summary.ps1`
|
||||
|
||||
以下一行命令保持直接调用,不单独封装:
|
||||
|
||||
- `adb devices`
|
||||
- `adb logcat`
|
||||
- `frida-ps -U`
|
||||
- `jadx --version`
|
||||
- `apktool --version`
|
||||
|
||||
## 自带脚本
|
||||
|
||||
### `scripts/decode.ps1`
|
||||
|
||||
用途:
|
||||
|
||||
- 统一跑 `jadx` 和 `apktool`
|
||||
- 默认在原 APK 同目录创建任务输出目录
|
||||
- 输出 `package`、`java_files`、`smali_dirs`、`so_files` 等摘要
|
||||
- 兼容 `jadx` 部分反编译错误但仍然有可用产物的情况
|
||||
|
||||
示例:
|
||||
|
||||
```powershell
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\decode.ps1" -ApkPath "D:\DOWNLOAD\app.apk" -Clean
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\decode.ps1" -ApkPath "D:\DOWNLOAD\app.apk" -Name demo -SkipJadx
|
||||
```
|
||||
|
||||
### `scripts/frida-run.ps1`
|
||||
|
||||
用途:
|
||||
|
||||
- 统一 Frida 的设备、进程、spawn/attach 入口
|
||||
- 避免手写参数时混淆 `-f`、`-n`、`-U`
|
||||
|
||||
示例:
|
||||
|
||||
```powershell
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\frida-run.ps1" -ListDevices
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\frida-run.ps1" -Usb -ListProcesses
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\frida-run.ps1" -Usb -Spawn -Package com.example.app -ScriptPath "D:\hooks\test.js"
|
||||
```
|
||||
|
||||
### `scripts/rebuild-sign-install.ps1`
|
||||
|
||||
用途:
|
||||
|
||||
- `apktool b` 重建 APK
|
||||
- `zipalign` 对齐
|
||||
- `apksigner` 签名与验签
|
||||
- 可选直接 `adb install`
|
||||
|
||||
示例:
|
||||
|
||||
```powershell
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\rebuild-sign-install.ps1" -ProjectDir "C:\work\apktool_out" -Clean
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\rebuild-sign-install.ps1" -ProjectDir "C:\work\apktool_out" -Install -Reinstall -DeviceSerial "127.0.0.1:7555"
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 默认生成并复用调试 keystore
|
||||
- 默认输出到 `ProjectDir` 同目录,便于和原始包、解包目录放在一起
|
||||
|
||||
### `scripts/manifest-summary.ps1`
|
||||
|
||||
用途:
|
||||
|
||||
- 抽取包名
|
||||
- 列权限
|
||||
- 列 activity/service/receiver/provider
|
||||
- 标出主启动 activity
|
||||
|
||||
示例:
|
||||
|
||||
```powershell
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\manifest-summary.ps1" -ManifestPath "C:\work\apktool_out\AndroidManifest.xml"
|
||||
```
|
||||
|
||||
如果要分析 `.so`、`lib/arm64-v8a/*.so`、`lib/armeabi-v7a/*.so`,再结合:
|
||||
|
||||
- `ida-reverse`
|
||||
- `radare2`
|
||||
|
||||
## 工具分工
|
||||
|
||||
### `jadx`
|
||||
|
||||
用于:
|
||||
|
||||
- Java 反编译阅读
|
||||
- 包名、类名、方法名搜索
|
||||
- 先从高层逻辑理解 APK
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
jadx -d jadx_out app.apk
|
||||
jadx --single-class com.example.LoginActivity -d jadx_out app.apk
|
||||
jadx --deobf -d jadx_out app.apk
|
||||
```
|
||||
|
||||
### `JEB Pro`(可选商业工具)
|
||||
|
||||
用于:
|
||||
|
||||
- Android DEX / APK / ARM 的交叉验证与深度反编译
|
||||
- 在 JADX 输出不完整或混淆较重时补充静态分析
|
||||
- 对同一目标的类、方法与调用关系进行第二工具链校验
|
||||
|
||||
边界:
|
||||
|
||||
- JEB Pro 是商业软件,必须由用户自行取得并安装有效许可证;本包不会下载、破解或规避许可。
|
||||
- 仅在 `tool-index` 已确认本机 JEB 可用时调用;否则继续使用 `jadx`、`apktool`、Ghidra、IDA 或 radare2。
|
||||
- 第三方 JEB MCP bridge 不是本包依赖。安装前必须按 `../ops/skill-supply-chain.md` 审阅源码、权限、网络行为和版本,再由用户明确确认注册。
|
||||
|
||||
### `apktool`
|
||||
|
||||
用于:
|
||||
|
||||
- 解包 APK
|
||||
- 查看和修改 `AndroidManifest.xml`
|
||||
- 查看和修改 smali
|
||||
- 重建 APK
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
apktool d app.apk -o apktool_out
|
||||
apktool b apktool_out -o rebuilt.apk
|
||||
```
|
||||
|
||||
### `frida`
|
||||
|
||||
用于:
|
||||
|
||||
- 动态观察 Java 方法调用
|
||||
- Hook native 导出函数
|
||||
- 绕过 root 检测、证书校验、调试检测
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
frida-ps -U
|
||||
frida -U -f com.example.app -l hook.js
|
||||
frida-trace -U -f com.example.app -j '*!*certificate*'
|
||||
```
|
||||
|
||||
### `adb`
|
||||
|
||||
用于:
|
||||
|
||||
- 设备连接
|
||||
- 安装 APK
|
||||
- 查看日志
|
||||
- 拉取文件
|
||||
|
||||
常用命令:
|
||||
|
||||
```bash
|
||||
adb devices
|
||||
adb install -r app.apk
|
||||
adb shell pm list packages
|
||||
adb logcat
|
||||
adb pull /data/local/tmp/file .
|
||||
```
|
||||
|
||||
## 推荐工作流
|
||||
|
||||
### 1. Triage
|
||||
|
||||
先确定 APK 大致构成,不急着改包或 Hook。
|
||||
|
||||
建议动作:
|
||||
|
||||
1. 用 `jadx -d jadx_out app.apk` 导出 Java 代码
|
||||
2. 用 `apktool d app.apk -o apktool_out` 导出 smali 和资源
|
||||
3. 先看:
|
||||
- `AndroidManifest.xml`
|
||||
- 主 `package`
|
||||
- `application`、`activity`、`service`、`receiver`
|
||||
- `lib/` 目录里是否有 `.so`
|
||||
|
||||
### 2. Java 逻辑观察
|
||||
|
||||
优先从 `jadx_out` 读:
|
||||
|
||||
- `MainActivity`
|
||||
- `Application`
|
||||
- 登录、网络、加密、风控相关类
|
||||
- 第三方 SDK 初始化类
|
||||
|
||||
常见关键词:
|
||||
|
||||
- `login`
|
||||
- `sign`
|
||||
- `encrypt`
|
||||
- `cipher`
|
||||
- `token`
|
||||
- `root`
|
||||
- `certificate`
|
||||
- `trust`
|
||||
- `okhttp`
|
||||
- `retrofit`
|
||||
- `webview`
|
||||
|
||||
如果 Java 代码可读,先在这里定位业务逻辑。
|
||||
|
||||
### 3. Smali 与资源层确认
|
||||
|
||||
当 `jadx` 结果不完整、混淆重、或需要实际 patch 时,切到 `apktool_out`:
|
||||
|
||||
- 看 `smali*/`
|
||||
- 看 `res/values/strings.xml`
|
||||
- 看 `AndroidManifest.xml`
|
||||
|
||||
优先 patch:
|
||||
|
||||
- `android:exported`
|
||||
- 调试标记
|
||||
- root 检测返回值
|
||||
- 登录验证逻辑
|
||||
- 证书校验分支
|
||||
|
||||
### 4. 重建与安装
|
||||
|
||||
修改后:
|
||||
|
||||
```bash
|
||||
apktool b apktool_out -o rebuilt.apk
|
||||
```
|
||||
|
||||
或者直接用脚本闭环:
|
||||
|
||||
```powershell
|
||||
pwsh -File "<skill-root>\apk-reverse\scripts\rebuild-sign-install.ps1" -ProjectDir "apktool_out" -Install -Reinstall -DeviceSerial "127.0.0.1:7555"
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 本 skill 只保证 `apktool` 重建链路
|
||||
- 若后续需要正式安装到设备,通常还需要签名流程
|
||||
- 如果任务进入签名/对齐,补充 `apksigner` / `zipalign`
|
||||
|
||||
### 5. 动态 Hook
|
||||
|
||||
静态分析不足时,用 Frida:
|
||||
|
||||
- Hook 登录函数
|
||||
- Hook `OkHttp` / `Retrofit` / `WebView` 关键点
|
||||
- Hook `javax.crypto`、`MessageDigest`
|
||||
- Hook root 检测函数
|
||||
- Hook SSL pinning 逻辑
|
||||
|
||||
原则:
|
||||
|
||||
- 先 Hook Java 层,再看是否需要 native Hook
|
||||
- 先打印参数与返回值,再决定是否主动修改返回值
|
||||
|
||||
建议:
|
||||
|
||||
- 简单一次性命令直接用 `frida-*`
|
||||
- 需要稳定复用的注入流程优先走 `scripts/frida-run.ps1`
|
||||
|
||||
### 6. Native `.so` 分流
|
||||
|
||||
如果 APK 中包含关键 `.so`:
|
||||
|
||||
- 用 `apktool` 或 `jadx` 找到 `lib/**/*.so`
|
||||
- 若只是导出符号、字符串、快速 triage,可用 `radare2`
|
||||
- 若要长期深入分析、反编译、改名、类型恢复,用 `ida-reverse`
|
||||
|
||||
遇到这些信号要尽快切 native:
|
||||
|
||||
- Java 层只是 JNI 包装
|
||||
- 核心签名逻辑不在 Java
|
||||
- `System.loadLibrary()` 后关键逻辑消失
|
||||
- 证书校验/风控在 `.so` 中
|
||||
|
||||
## 输出要求
|
||||
|
||||
最终至少说明:
|
||||
|
||||
- 入口组件与关键类
|
||||
- 关键逻辑在 Java、smali 还是 `.so`
|
||||
- 已确认的敏感点:登录、签名、root、SSL、WebView、JNI
|
||||
- 如果做了 patch,说明改了什么
|
||||
- 如果做了 Hook,说明 Hook 了哪个类/方法/导出函数
|
||||
|
||||
## 禁止事项
|
||||
|
||||
- 不要一开始就盲目改 smali
|
||||
- 不要在没看 manifest 和主入口前就写 Hook
|
||||
- 不要把 Java 反编译不完整直接等同于“逻辑不可分析”
|
||||
- 不要在 `.so` 明显承载核心逻辑时继续死磕 Java 层
|
||||
|
||||
## 快速命令备忘
|
||||
|
||||
```bash
|
||||
# 反编译 Java
|
||||
jadx -d jadx_out app.apk
|
||||
|
||||
# 解包 APK
|
||||
apktool d app.apk -o apktool_out
|
||||
|
||||
# 重建 APK
|
||||
apktool b apktool_out -o rebuilt.apk
|
||||
|
||||
# 设备与进程
|
||||
adb devices
|
||||
frida-ps -U
|
||||
|
||||
# 启动并注入
|
||||
frida -U -f com.example.app -l hook.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 路由上下文
|
||||
|
||||
**上游入口**: `skills/SKILL.md`(总控)、`routing.md`
|
||||
**下游出口**:
|
||||
- 核心逻辑在 `.so` → `ida-reverse/` 或 `radare2/`
|
||||
- 需动态 Hook/验证 → `reverse-engineering/tools-dynamic.md`(Frida 章节)
|
||||
- 通用逆向方法论 → `reverse-engineering/SKILL.md`
|
||||
|
||||
**同级关联模块**: `reverse-engineering/`(.so 分析和 Frida 进阶用法)
|
||||
|
||||
---
|
||||
|
||||
## 按需自举(On-Demand Bootstrap)
|
||||
|
||||
本 skill 的入口脚本已接入统一自举系统。缺少工具时不会直接报错,而是自动尝试安装。
|
||||
|
||||
### 自动化能力边界
|
||||
|
||||
| 工具 | 可自动安装 | 安装方式 | 说明 |
|
||||
|------|-----------|---------|------|
|
||||
| jadx | ✓ | GitHub Release ZIP | 自动下载解压到 `%USERPROFILE%\Tools\jadx\` |
|
||||
| apktool | ✓ | GitHub Release JAR + wrapper | 自动下载 jar 并生成 bat 到 `%USERPROFILE%\Tools\apktool\` |
|
||||
| JEB Pro | ✗ | 用户手动安装并提供有效许可证 | 可选的 Android / ARM 交叉验证工具;第三方 MCP bridge 需单独审计 |
|
||||
| frida / frida-ps | ✓ | pip install frida-tools | 需要 Python 已安装 |
|
||||
| adb | ✓ | winget / fallback path | 自动安装 Android Platform-Tools |
|
||||
| zipalign | ✗ | 需手动安装 Android Build-Tools | `sdkmanager "build-tools;35.0.0"` |
|
||||
| apksigner | ✗ | 需手动安装 Android Build-Tools | 同上 |
|
||||
|
||||
### 自举触发点
|
||||
|
||||
- `scripts/decode.ps1`:缺 jadx 或 apktool 时自动调用 `bootstrap-reverse.ps1`
|
||||
- `scripts/rebuild-sign-install.ps1`:缺 adb 或 apktool 时自动调用 bootstrap
|
||||
- `scripts/frida-run.ps1`:当前仍为手动检查(frida 通常已通过 pip 安装)
|
||||
|
||||
### 自举失败时
|
||||
|
||||
如果自动安装失败,脚本会抛出明确错误并附带手动安装链接。常见原因:
|
||||
- 网络不通(GitHub API / PyPI 不可达)
|
||||
- winget 不可用(Windows 版本过低)
|
||||
- Java 未安装(apktool 依赖 JDK)
|
||||
|
||||
|
||||
## 任务完成自检(声称完成前 MUST 通过)
|
||||
|
||||
- [ ] 我是否执行了工作流中的每一步(而不是只阅读)?
|
||||
- [ ] 我是否基于 `tool-index` 使用了真实工具路径?
|
||||
- [ ] 我是否产出了可复现证据(命令/脚本/截图/报告)?
|
||||
- [ ] 我是否完成并回写了 RULES 要求的 Checklist 项?
|
||||
@@ -0,0 +1,365 @@
|
||||
# Android 高级逆向参考
|
||||
|
||||
> 覆盖 Native SO 分析、Frida 高级用法、SSL Pinning 绕过、Root 检测对抗、加固脱壳、Flutter/React Native 逆向。
|
||||
|
||||
---
|
||||
|
||||
## Native SO 逆向
|
||||
|
||||
### 分析流程
|
||||
|
||||
```text
|
||||
1. 从 APK 中提取 .so 文件
|
||||
unzip app.apk lib/arm64-v8a/*.so -d extracted/
|
||||
|
||||
2. 确认架构和基本信息
|
||||
file libxxx.so
|
||||
rabin2 -I libxxx.so
|
||||
|
||||
3. 找 JNI 入口
|
||||
- 搜索 JNI_OnLoad(动态注册)
|
||||
- 搜索 Java_com_xxx_yyy(静态注册)
|
||||
- nm -D libxxx.so | grep -i java
|
||||
|
||||
4. IDA/Ghidra 加载分析
|
||||
- 导入 JNI 头文件(jni.h 类型)
|
||||
- 标注 JNIEnv* 参数
|
||||
- 找 RegisterNatives 调用(动态注册的函数表)
|
||||
|
||||
5. 定位关键逻辑
|
||||
- 从 Java 层 native 方法名追踪
|
||||
- 从字符串(密钥、URL、错误信息)交叉引用
|
||||
- 从 crypto 库函数(AES/MD5/SHA)调用追踪
|
||||
```
|
||||
|
||||
### JNI 函数注册
|
||||
|
||||
```c
|
||||
// 静态注册:函数名 = Java_包名_类名_方法名
|
||||
JNIEXPORT jstring JNICALL Java_com_example_app_Security_getSign(
|
||||
JNIEnv *env, jobject thiz, jstring input) { ... }
|
||||
|
||||
// 动态注册:在 JNI_OnLoad 中调用 RegisterNatives
|
||||
static JNINativeMethod methods[] = {
|
||||
{"getSign", "(Ljava/lang/String;)Ljava/lang/String;", (void*)native_getSign},
|
||||
};
|
||||
|
||||
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
|
||||
JNIEnv *env;
|
||||
vm->GetEnv((void**)&env, JNI_VERSION_1_6);
|
||||
jclass clazz = env->FindClass("com/example/app/Security");
|
||||
env->RegisterNatives(clazz, methods, sizeof(methods)/sizeof(methods[0]));
|
||||
return JNI_VERSION_1_6;
|
||||
}
|
||||
```
|
||||
|
||||
### IDA 中分析 JNI 的技巧
|
||||
|
||||
```text
|
||||
1. 导入 JNI 类型库
|
||||
File → Load File → Parse C Header → jni.h
|
||||
|
||||
2. 标注第一个参数为 JNIEnv*
|
||||
右键参数 → Set type → JNIEnv*
|
||||
这样 env->FindClass / env->GetMethodID 等调用会自动识别
|
||||
|
||||
3. 找 RegisterNatives
|
||||
搜索对 JNIEnv vtable offset 0x35C (ARM64) 的调用
|
||||
→ 第三个参数是 JNINativeMethod 数组
|
||||
→ 从数组中提取所有 native 函数地址
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Frida 高级用法
|
||||
|
||||
### Hook Native 函数
|
||||
|
||||
```javascript
|
||||
// Hook libc 函数
|
||||
Interceptor.attach(Module.findExportByName("libc.so", "open"), {
|
||||
onEnter: function(args) {
|
||||
this.path = args[0].readUtf8String();
|
||||
console.log("[open] " + this.path);
|
||||
},
|
||||
onLeave: function(retval) {
|
||||
if (this.path.includes("su") || this.path.includes("magisk")) {
|
||||
console.log("[open] Blocked root check: " + this.path);
|
||||
retval.replace(-1); // 返回失败
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Hook 自定义 SO 中的函数
|
||||
var base = Module.findBaseAddress("libsecurity.so");
|
||||
var targetFunc = base.add(0x1234); // 偏移地址
|
||||
Interceptor.attach(targetFunc, {
|
||||
onEnter: function(args) {
|
||||
console.log("arg0: " + args[0].readUtf8String());
|
||||
},
|
||||
onLeave: function(retval) {
|
||||
console.log("return: " + retval.readUtf8String());
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Hook Java 方法
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
// Hook 实例方法
|
||||
var Security = Java.use("com.example.app.Security");
|
||||
Security.getSign.implementation = function(input) {
|
||||
console.log("[getSign] input: " + input);
|
||||
var result = this.getSign(input); // 调用原方法
|
||||
console.log("[getSign] output: " + result);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Hook 构造函数
|
||||
Security.$init.overload('java.lang.String').implementation = function(key) {
|
||||
console.log("[Security.<init>] key: " + key);
|
||||
this.$init(key);
|
||||
};
|
||||
|
||||
// Hook 重载方法
|
||||
Security.encrypt.overload('java.lang.String', 'int').implementation = function(data, mode) {
|
||||
console.log("[encrypt] data=" + data + " mode=" + mode);
|
||||
return this.encrypt(data, mode);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 内存搜索与修改
|
||||
|
||||
```javascript
|
||||
// 搜索内存中的字符串
|
||||
Process.enumerateModules().forEach(function(module) {
|
||||
if (module.name === "libtarget.so") {
|
||||
Memory.scan(module.base, module.size, "48 65 6C 6C 6F", { // "Hello"
|
||||
onMatch: function(address, size) {
|
||||
console.log("Found at: " + address);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 修改内存(patch 指令)
|
||||
var addr = Module.findBaseAddress("libsecurity.so").add(0x5678);
|
||||
Memory.patchCode(addr, 4, function(code) {
|
||||
var writer = new Arm64Writer(code, {pc: addr});
|
||||
writer.putNop(); // 替换为 NOP
|
||||
writer.flush();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SSL Pinning 绕过
|
||||
|
||||
### 通用方案(推荐)
|
||||
|
||||
```javascript
|
||||
// Frida 通用 SSL Pinning 绕过
|
||||
// 来源: https://github.com/0xCD4/SSL-bypass
|
||||
Java.perform(function() {
|
||||
// 1. TrustManager 绕过
|
||||
var TrustManager = Java.registerClass({
|
||||
name: 'com.custom.TrustManager',
|
||||
implements: [Java.use('javax.net.ssl.X509TrustManager')],
|
||||
methods: {
|
||||
checkClientTrusted: function(chain, authType) {},
|
||||
checkServerTrusted: function(chain, authType) {},
|
||||
getAcceptedIssuers: function() { return []; }
|
||||
}
|
||||
});
|
||||
|
||||
// 2. SSLContext 替换
|
||||
var SSLContext = Java.use('javax.net.ssl.SSLContext');
|
||||
var sslContext = SSLContext.getInstance("TLS");
|
||||
sslContext.init(null, [TrustManager.$new()], null);
|
||||
|
||||
// 3. OkHttp CertificatePinner 绕过
|
||||
try {
|
||||
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
|
||||
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {};
|
||||
} catch(e) {}
|
||||
});
|
||||
```
|
||||
|
||||
### 各框架绕过
|
||||
|
||||
| 框架 | 绕过方法 |
|
||||
|------|---------|
|
||||
| OkHttp3 | Hook `CertificatePinner.check` 返回空 |
|
||||
| Retrofit | 同 OkHttp(底层用 OkHttp) |
|
||||
| Volley | Hook `HurlStack` 的 SSL 工厂 |
|
||||
| Flutter | Hook `dart:io` 的 `SecurityContext`(需要特殊脚本) |
|
||||
| React Native | Hook `OkHttpClientProvider` |
|
||||
| WebView | Hook `WebViewClient.onReceivedSslError` |
|
||||
|
||||
### Flutter 专项
|
||||
|
||||
```javascript
|
||||
// Flutter SSL Pinning 绕过(需要找到 ssl_verify_peer_cert 函数)
|
||||
var flutter_lib = Module.findBaseAddress("libflutter.so");
|
||||
// 搜索 ssl_verify_peer_cert 的特征码
|
||||
var pattern = "FF 03 05 D1 FD 7B 0F A9"; // ARM64 特征
|
||||
Memory.scan(flutter_lib, Module.findModuleByName("libflutter.so").size, pattern, {
|
||||
onMatch: function(address) {
|
||||
Interceptor.replace(address, new NativeCallback(function() {
|
||||
return 0; // 返回成功
|
||||
}, 'int', []));
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Root 检测绕过
|
||||
|
||||
### 常见检测方式
|
||||
|
||||
| 检测方式 | 绕过方法 |
|
||||
|---------|---------|
|
||||
| 检查 `/system/app/Superuser.apk` | Hook `File.exists()` 返回 false |
|
||||
| 检查 `su` 命令 | Hook `Runtime.exec()` 拦截 su 调用 |
|
||||
| 检查 `/proc/self/mounts` | Hook 文件读取,过滤 magisk 相关 |
|
||||
| SafetyNet/Play Integrity | Magisk Hide / Zygisk + Shamiko |
|
||||
| 检查 Magisk 包名 | 随机化 Magisk 包名 |
|
||||
| 检查 `/data/adb/` | Hook `opendir`/`access` |
|
||||
|
||||
### Frida 通用 Root 绕过
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
// Hook File.exists
|
||||
var File = Java.use("java.io.File");
|
||||
File.exists.implementation = function() {
|
||||
var path = this.getAbsolutePath();
|
||||
var blacklist = ["su", "Superuser", "magisk", "busybox", "xposed"];
|
||||
for (var i = 0; i < blacklist.length; i++) {
|
||||
if (path.toLowerCase().includes(blacklist[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.exists();
|
||||
};
|
||||
|
||||
// Hook System.getProperty
|
||||
var System = Java.use("java.lang.System");
|
||||
System.getProperty.overload('java.lang.String').implementation = function(key) {
|
||||
if (key === "ro.debuggable" || key === "ro.secure") {
|
||||
return "1";
|
||||
}
|
||||
return this.getProperty(key);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 加固/壳识别与脱壳
|
||||
|
||||
### 常见加固厂商
|
||||
|
||||
| 加固 | 识别特征 | 脱壳方式 |
|
||||
|------|---------|---------|
|
||||
| 360 加固 | `libjiagu.so`、`com.stub.StubApp` | FART / Frida dump dex |
|
||||
| 腾讯乐固 | `libshell*.so`、`com.tencent.StubShell` | FART / BlackDex |
|
||||
| 梆梆加固 | `libDexHelper.so`、`com.secneo.apkwrapper` | FART |
|
||||
| 爱加密 | `libexec.so`、`s.h.e.l.l` | Frida dump |
|
||||
| 网易易盾 | `libnesec.so` | Frida dump |
|
||||
| 娜迦 | `libnaga.so` | Frida dump |
|
||||
|
||||
### 通用脱壳方法
|
||||
|
||||
```text
|
||||
方法 1: FART(ART 环境脱壳)
|
||||
- 刷入 FART ROM 或使用 Frida 版 FART
|
||||
- 自动 dump 所有 ClassLoader 加载的 dex
|
||||
|
||||
方法 2: Frida DEX Dump
|
||||
- frida -U -f com.target.app -l dex_dump.js
|
||||
- 在 DexFile::OpenMemory 处 hook,dump 内存中的 dex
|
||||
|
||||
方法 3: BlackDex
|
||||
- 免 root 脱壳工具
|
||||
- 直接安装 BlackDex APK,选择目标应用脱壳
|
||||
|
||||
方法 4: 手动 dump
|
||||
- 用 Frida 枚举所有 ClassLoader
|
||||
- 找到应用的 ClassLoader → 获取 DexFile 对象
|
||||
- 读取 dex 内存区域并保存
|
||||
```
|
||||
|
||||
### Frida DEX Dump 脚本
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
Java.enumerateClassLoaders({
|
||||
onMatch: function(loader) {
|
||||
try {
|
||||
var dexFiles = loader.getDexFileList();
|
||||
console.log("ClassLoader: " + loader);
|
||||
console.log(" DEX files: " + dexFiles);
|
||||
} catch(e) {}
|
||||
},
|
||||
onComplete: function() {}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React Native / Flutter 逆向
|
||||
|
||||
### React Native
|
||||
|
||||
```text
|
||||
1. 解压 APK → assets/index.android.bundle(JS 代码)
|
||||
2. 格式化 JS → 搜索 API 地址、密钥、签名逻辑
|
||||
3. 如果有 Hermes 字节码(.hbc 文件)→ 用 hermes-dec 反编译
|
||||
4. Hook: 用 Frida hook Java 层的 ReactBridge
|
||||
```
|
||||
|
||||
### Flutter
|
||||
|
||||
```text
|
||||
1. Flutter 代码编译为 libapp.so(Dart AOT)
|
||||
2. 无法直接反编译为 Dart 源码
|
||||
3. 分析方法:
|
||||
- reFlutter 工具:patch libflutter.so 获取 snapshot
|
||||
- Doldrums:解析 Dart snapshot 恢复类/函数信息
|
||||
- Frida hook libflutter.so 中的关键函数
|
||||
4. 网络分析:Flutter 不走系统代理,需要特殊处理 SSL
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 工具速查
|
||||
|
||||
| 工具 | 用途 | 安装 |
|
||||
|------|------|------|
|
||||
| jadx | Java 反编译 | 已在 bootstrap 中 |
|
||||
| apktool | 解包/重打包 | 已在 bootstrap 中 |
|
||||
| Frida | 动态 Hook | `pip install frida-tools` |
|
||||
| Objection | Frida 封装(更易用) | `pip install objection` |
|
||||
| MobSF | 自动化移动安全分析 | Docker 部署 |
|
||||
| BlackDex | 免 root 脱壳 | APK 安装 |
|
||||
| FART | ART 脱壳 | 刷入 ROM 或 Frida 版 |
|
||||
| hermes-dec | Hermes 字节码反编译 | npm 安装 |
|
||||
| reFlutter | Flutter 逆向辅助 | pip 安装 |
|
||||
| Magisk + Shamiko | Root 隐藏 | 刷入 |
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
| 资源 | 说明 | 链接 |
|
||||
|------|------|------|
|
||||
| OWASP MASTG | 移动安全测试指南 | https://mas.owasp.org/ |
|
||||
| FridaBypassKit | 通用绕过框架 | https://github.com/okankurtuluss/FridaBypassKit |
|
||||
| SSL-bypass | 通用 SSL Pinning 绕过 | https://github.com/0xCD4/SSL-bypass |
|
||||
| awesome-frida | Frida 资源合集 | https://github.com/dweinstein/awesome-frida |
|
||||
| Android Security Awesome | Android 安全资源 | https://github.com/ashishb/android-security-awesome |
|
||||
@@ -0,0 +1,230 @@
|
||||
# APK 安全测试速查
|
||||
|
||||
> 基于 OWASP MASTG(Mobile Application Security Testing Guide)整理。
|
||||
> 覆盖静态分析、动态分析、网络通信、数据存储、认证授权、代码保护六大维度。
|
||||
|
||||
---
|
||||
|
||||
## 静态分析检查清单
|
||||
|
||||
### Manifest 审计
|
||||
|
||||
```text
|
||||
□ android:debuggable="true" → 可调试(生产环境不应出现)
|
||||
□ android:allowBackup="true" → 数据可备份提取
|
||||
□ android:exported="true" 的组件 → 暴露的 Activity/Service/Receiver/Provider
|
||||
□ 自定义权限 protectionLevel → 是否为 normal(应为 signature)
|
||||
□ intent-filter 中的 scheme → 自定义 deeplink 是否可被劫持
|
||||
□ android:usesCleartextTraffic="true" → 允许明文 HTTP
|
||||
□ minSdkVersion 过低 → 可能缺少安全特性
|
||||
```
|
||||
|
||||
### 代码审计关键点
|
||||
|
||||
```text
|
||||
□ 硬编码密钥/Token(搜索 "key"、"secret"、"password"、"api_key")
|
||||
□ 不安全的随机数(java.util.Random 而非 SecureRandom)
|
||||
□ 不安全的加密(ECB 模式、DES、MD5 用于密码)
|
||||
□ WebView 配置(setJavaScriptEnabled + addJavascriptInterface = RCE 风险)
|
||||
□ SQL 注入(rawQuery 拼接用户输入)
|
||||
□ 路径遍历(ContentProvider 的 openFile 未校验路径)
|
||||
□ 日志泄露(Log.d/Log.i 输出敏感信息)
|
||||
□ 剪贴板泄露(ClipboardManager 存储敏感数据)
|
||||
□ 隐式 Intent 泄露(sendBroadcast 未指定包名)
|
||||
```
|
||||
|
||||
### 第三方库审计
|
||||
|
||||
```text
|
||||
□ 过时的 OkHttp/Retrofit 版本(已知漏洞)
|
||||
□ 过时的 WebView 内核
|
||||
□ 含已知漏洞的 SDK(检查 CVE)
|
||||
□ 广告 SDK 数据收集范围
|
||||
□ 推送 SDK 配置(是否泄露 token)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 动态分析检查清单
|
||||
|
||||
### Frida Hook 优先目标
|
||||
|
||||
| 目标 | Hook 点 | 目的 |
|
||||
|------|---------|------|
|
||||
| 登录认证 | `LoginActivity.login()` | 观察凭证处理 |
|
||||
| 签名生成 | `*Sign*`、`*sign*`、`*encrypt*` | 还原签名算法 |
|
||||
| SSL Pinning | `CertificatePinner.check` | 绕过抓包 |
|
||||
| Root 检测 | `*root*`、`*su*`、`*magisk*` | 绕过检测 |
|
||||
| 加密操作 | `javax.crypto.Cipher` | 提取密钥/IV |
|
||||
| Token 存储 | `SharedPreferences.getString` | 观察 token 读写 |
|
||||
| 网络请求 | `OkHttpClient.newCall` | 观察请求构造 |
|
||||
|
||||
### 常用 Frida 一行命令
|
||||
|
||||
```bash
|
||||
# 追踪所有加密操作
|
||||
frida-trace -U -f com.target.app -j '*Cipher*!*'
|
||||
|
||||
# 追踪所有 HTTP 请求
|
||||
frida-trace -U -f com.target.app -j '*OkHttp*!*'
|
||||
|
||||
# 追踪 SharedPreferences 读写
|
||||
frida-trace -U -f com.target.app -j '*SharedPreferences*!*'
|
||||
|
||||
# 追踪所有 native 函数调用
|
||||
frida-trace -U -f com.target.app -i 'Java_*'
|
||||
```
|
||||
|
||||
### Objection 快速命令
|
||||
|
||||
```bash
|
||||
# 连接
|
||||
objection -g com.target.app explore
|
||||
|
||||
# 常用命令
|
||||
android hooking list activities
|
||||
android hooking list services
|
||||
android sslpinning disable
|
||||
android root disable
|
||||
android clipboard monitor
|
||||
env # 查看应用目录
|
||||
sqlite connect <db_path> # 连接数据库
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 网络通信安全
|
||||
|
||||
### 抓包配置
|
||||
|
||||
```text
|
||||
方法 1: 系统代理 + Burp/mitmproxy
|
||||
- 设置 WiFi 代理 → Burp 监听地址
|
||||
- 安装 CA 证书到设备
|
||||
- Android 7+ 需要 network_security_config 或 Frida 绕过
|
||||
|
||||
方法 2: VPN 模式(推荐)
|
||||
- 使用 HttpCanary / Packet Capture
|
||||
- 不需要 root,不需要配置代理
|
||||
- 但无法解密 SSL Pinning 的流量
|
||||
|
||||
方法 3: Frida + r2frida
|
||||
- 直接在进程内拦截网络调用
|
||||
- 不受代理/VPN 限制
|
||||
```
|
||||
|
||||
### 检查项
|
||||
|
||||
```text
|
||||
□ 是否使用 HTTPS(所有 API 调用)
|
||||
□ 是否有 SSL Pinning(证书绑定)
|
||||
□ 证书验证是否正确(不接受自签名)
|
||||
□ 是否有证书透明度(CT)检查
|
||||
□ API 密钥是否在请求中明文传输
|
||||
□ Token 是否有过期机制
|
||||
□ 是否有请求签名防篡改
|
||||
□ 是否有重放攻击防护(nonce/timestamp)
|
||||
□ WebSocket 是否加密
|
||||
□ 是否有敏感数据在 URL 参数中(会被日志记录)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据存储安全
|
||||
|
||||
### 检查位置
|
||||
|
||||
| 位置 | 风险 | 检查命令 |
|
||||
|------|------|---------|
|
||||
| SharedPreferences | 明文存储 token/密码 | `adb shell cat /data/data/pkg/shared_prefs/*.xml` |
|
||||
| SQLite 数据库 | 未加密的敏感数据 | `adb pull /data/data/pkg/databases/` |
|
||||
| 外部存储 | 任何应用可读 | `adb shell ls /sdcard/Android/data/pkg/` |
|
||||
| 应用日志 | 泄露调试信息 | `adb logcat \| grep pkg` |
|
||||
| 备份文件 | allowBackup=true | `adb backup -f backup.ab pkg` |
|
||||
| 键盘缓存 | 输入历史 | 检查 `inputType` 是否为 `textPassword` |
|
||||
| 截图保护 | 敏感页面可截图 | 检查 `FLAG_SECURE` |
|
||||
|
||||
### 加密存储方案对比
|
||||
|
||||
| 方案 | 安全性 | 说明 |
|
||||
|------|--------|------|
|
||||
| SharedPreferences 明文 | ❌ | root 后直接读取 |
|
||||
| EncryptedSharedPreferences | ✓ | AndroidX Security 库 |
|
||||
| SQLCipher | ✓ | 加密 SQLite |
|
||||
| Android Keystore | ✓✓ | 硬件级密钥保护 |
|
||||
| 自定义 AES 加密 | ⚠️ | 取决于密钥管理 |
|
||||
|
||||
---
|
||||
|
||||
## 认证与授权
|
||||
|
||||
### 常见漏洞
|
||||
|
||||
| 漏洞 | 测试方法 |
|
||||
|------|---------|
|
||||
| 弱密码策略 | 尝试 123456、password 等 |
|
||||
| 无锁定机制 | 暴力破解登录接口 |
|
||||
| Token 不过期 | 登出后重放旧 token |
|
||||
| 越权访问 | 修改请求中的 user_id |
|
||||
| 短信验证码可爆破 | 4/6 位数字无频率限制 |
|
||||
| OAuth 配置错误 | redirect_uri 可篡改 |
|
||||
| 生物认证绕过 | Hook BiometricPrompt |
|
||||
| 设备绑定绕过 | 修改 device_id |
|
||||
|
||||
### 测试 Payload
|
||||
|
||||
```bash
|
||||
# 越权测试
|
||||
curl -H "Authorization: Bearer USER_A_TOKEN" \
|
||||
"https://api.target.com/users/USER_B_ID/profile"
|
||||
|
||||
# Token 重放
|
||||
# 1. 正常登录获取 token
|
||||
# 2. 登出
|
||||
# 3. 用旧 token 请求 → 应该返回 401
|
||||
|
||||
# 短信验证码爆破
|
||||
for code in $(seq 0000 9999); do
|
||||
curl -X POST "https://api.target.com/verify" \
|
||||
-d "phone=13800138000&code=$code"
|
||||
done
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 代码保护评估
|
||||
|
||||
| 保护措施 | 检测方法 | 绕过难度 |
|
||||
|---------|---------|---------|
|
||||
| ProGuard 混淆 | jadx 查看类名是否为 a/b/c | 低(只是重命名) |
|
||||
| 字符串加密 | 搜索解密函数,Hook 获取明文 | 中 |
|
||||
| 反调试 | 尝试 attach debugger | 中(Frida 可绕过) |
|
||||
| Root 检测 | 在 root 设备上运行 | 中(通用脚本绕过) |
|
||||
| 模拟器检测 | 在模拟器上运行 | 低-中 |
|
||||
| 完整性校验 | 修改 APK 后安装 | 中(patch 校验函数) |
|
||||
| 加固/壳 | 查看入口类和 .so | 中-高(需脱壳) |
|
||||
| Native 保护 | 核心逻辑在 .so | 高(需 IDA 分析) |
|
||||
| VMP 虚拟化 | 代码被虚拟化执行 | 极高 |
|
||||
|
||||
---
|
||||
|
||||
## 快速测试流程(30 分钟)
|
||||
|
||||
```text
|
||||
1. [5min] 解包 + Manifest 审计
|
||||
apktool d app.apk
|
||||
检查 debuggable/allowBackup/exported/cleartext
|
||||
|
||||
2. [10min] 代码快速审计
|
||||
jadx -d out app.apk
|
||||
搜索: password, key, secret, token, http://
|
||||
|
||||
3. [5min] 网络测试
|
||||
配置代理 → 操作 APP → 检查是否有明文/弱加密
|
||||
|
||||
4. [5min] 存储检查
|
||||
adb shell → 检查 shared_prefs 和 databases
|
||||
|
||||
5. [5min] 动态验证
|
||||
Frida hook 关键函数 → 确认发现
|
||||
```
|
||||
@@ -0,0 +1,70 @@
|
||||
# Frida Bypass Kit — Android 通用安全绕过框架
|
||||
|
||||
> 来源:[FridaBypassKit](https://github.com/okankurtuluss/FridaBypassKit)(2025)
|
||||
> 适用场景:APK 动态分析时需要绕过 root 检测、SSL pinning、模拟器检测、反调试
|
||||
|
||||
## 概述
|
||||
|
||||
FridaBypassKit 是一个集成了四大绕过能力的 Frida 脚本,无需针对特定 APP 定制,开箱即用。
|
||||
|
||||
## 四大绕过能力
|
||||
|
||||
### 1. Root 检测绕过
|
||||
|
||||
- Hook `File.exists()` 隐藏 su 二进制
|
||||
- 拦截 `Runtime.exec()` 的 root 检查调用
|
||||
- 从 PackageManager 隐藏 root 相关包(Magisk、SuperSU 等)
|
||||
- 修改系统属性使设备看起来未 root
|
||||
|
||||
### 2. SSL Pinning 绕过
|
||||
|
||||
- Hook `TrustManagerImpl.verifyChain()`
|
||||
- Hook `TrustManagerImpl.checkTrustedRecursive()`
|
||||
- 绕过证书链验证
|
||||
- 返回空证书链避免校验
|
||||
- 兼容 OkHttp、Retrofit 和自定义实现
|
||||
|
||||
### 3. 模拟器检测绕过
|
||||
|
||||
- 伪造 TelephonyManager 返回值
|
||||
- 返回假电话号码和运营商名称
|
||||
- 修改 Build 属性
|
||||
|
||||
### 4. 反调试绕过
|
||||
|
||||
- Hook `Debug.isDebuggerConnected()`
|
||||
- 阻止调试器检测
|
||||
- 绕过反调试检查
|
||||
|
||||
## 使用方法
|
||||
|
||||
```bash
|
||||
# 前置条件
|
||||
pip install frida-tools
|
||||
adb push frida-server /data/local/tmp/
|
||||
adb shell chmod 755 /data/local/tmp/frida-server
|
||||
adb shell su -c /data/local/tmp/frida-server &
|
||||
|
||||
# 注入目标 APP
|
||||
frida -U -f com.example.app -l FridaBypassKit.js
|
||||
```
|
||||
|
||||
## 其他推荐 Frida 绕过脚本
|
||||
|
||||
| 项目 | 特点 | 链接 |
|
||||
|------|------|------|
|
||||
| httptoolkit/frida-interception-and-unpinning | 直接 MitM 所有 HTTPS 流量 | [GitHub](https://github.com/httptoolkit/frida-interception-and-unpinning) |
|
||||
| 0xCD4/SSL-bypass | 通用非定制 SSL 绕过 | [GitHub](https://github.com/0xCD4/SSL-bypass) |
|
||||
| incogbyte/ssl-bypass gist | 绕过常见 SSL pinning 方法 | [Gist](https://gist.github.com/incogbyte/1e0e2f38b5602e72b1380f21ba04b15e) |
|
||||
| Zero3141/Frida-OkHttp-Bypass | 专门针对 OkHttp CertificatePinner | [GitHub](https://github.com/Zero3141/Frida-OkHttp-Bypass) |
|
||||
|
||||
## 与本包的集成
|
||||
|
||||
在 `apk-reverse` 工作流中,当遇到以下情况时使用:
|
||||
|
||||
1. APP 检测到 root 拒绝运行 → 启用 Root Detection Bypass
|
||||
2. 抓包时 HTTPS 请求看不到明文 → 启用 SSL Pinning Bypass
|
||||
3. APP 检测到模拟器拒绝运行 → 启用 Emulator Detection Bypass
|
||||
4. 附加 Frida 后 APP 崩溃 → 启用 Debug Detection Bypass
|
||||
|
||||
推荐组合使用:先跑完整 FridaBypassKit,再针对性调整。
|
||||
@@ -0,0 +1,472 @@
|
||||
# Frida 实战脚本速查
|
||||
|
||||
> 精选自 [awesome-frida](https://github.com/dweinstein/awesome-frida)、[Frida-Mobile-Scripts](https://github.com/m0bilesecurity/Frida-Mobile-Scripts)、[frida-codeshare-scripts](https://github.com/zengfr/frida-codeshare-scripts) 等开源项目。
|
||||
> 按场景分类,直接复制使用。
|
||||
|
||||
---
|
||||
|
||||
## 通用 Hook 模板
|
||||
|
||||
### Hook 任意 Java 方法
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var TargetClass = Java.use("com.target.ClassName");
|
||||
|
||||
// Hook 无参方法
|
||||
TargetClass.methodName.implementation = function() {
|
||||
console.log("[*] methodName called");
|
||||
var ret = this.methodName();
|
||||
console.log("[*] return: " + ret);
|
||||
return ret;
|
||||
};
|
||||
|
||||
// Hook 有参方法
|
||||
TargetClass.methodName.overload('java.lang.String', 'int').implementation = function(str, num) {
|
||||
console.log("[*] methodName(" + str + ", " + num + ")");
|
||||
var ret = this.methodName(str, num);
|
||||
console.log("[*] return: " + ret);
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Hook 构造函数
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var TargetClass = Java.use("com.target.ClassName");
|
||||
TargetClass.$init.overload('java.lang.String').implementation = function(arg) {
|
||||
console.log("[*] new ClassName(" + arg + ")");
|
||||
this.$init(arg);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 枚举所有方法
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var TargetClass = Java.use("com.target.ClassName");
|
||||
var methods = TargetClass.class.getDeclaredMethods();
|
||||
methods.forEach(function(method) {
|
||||
console.log(method.toString());
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 加密/签名 Hook
|
||||
|
||||
### Hook AES 加解密
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var Cipher = Java.use("javax.crypto.Cipher");
|
||||
|
||||
Cipher.doFinal.overload('[B').implementation = function(input) {
|
||||
var mode = this.getOpmode ? this.getOpmode() : "?";
|
||||
console.log("[Cipher.doFinal] mode=" + mode);
|
||||
console.log(" input: " + bytesToHex(input));
|
||||
var result = this.doFinal(input);
|
||||
console.log(" output: " + bytesToHex(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
// 捕获密钥
|
||||
var SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
|
||||
SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function(key, algo) {
|
||||
console.log("[SecretKeySpec] algo=" + algo + " key=" + bytesToHex(key));
|
||||
this.$init(key, algo);
|
||||
};
|
||||
|
||||
// 捕获 IV
|
||||
var IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
|
||||
IvParameterSpec.$init.overload('[B').implementation = function(iv) {
|
||||
console.log("[IvParameterSpec] iv=" + bytesToHex(iv));
|
||||
this.$init(iv);
|
||||
};
|
||||
});
|
||||
|
||||
function bytesToHex(bytes) {
|
||||
var hex = [];
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
|
||||
}
|
||||
return hex.join('');
|
||||
}
|
||||
```
|
||||
|
||||
### Hook MD5/SHA
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var MessageDigest = Java.use("java.security.MessageDigest");
|
||||
|
||||
MessageDigest.digest.overload('[B').implementation = function(input) {
|
||||
console.log("[MessageDigest.digest] algo=" + this.getAlgorithm());
|
||||
console.log(" input: " + bytesToHex(input));
|
||||
var result = this.digest(input);
|
||||
console.log(" hash: " + bytesToHex(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
MessageDigest.digest.overload().implementation = function() {
|
||||
console.log("[MessageDigest.digest] algo=" + this.getAlgorithm());
|
||||
var result = this.digest();
|
||||
console.log(" hash: " + bytesToHex(result));
|
||||
return result;
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Hook HMAC
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var Mac = Java.use("javax.crypto.Mac");
|
||||
|
||||
Mac.doFinal.overload('[B').implementation = function(input) {
|
||||
console.log("[Mac.doFinal] algo=" + this.getAlgorithm());
|
||||
console.log(" input: " + bytesToHex(input));
|
||||
var result = this.doFinal(input);
|
||||
console.log(" mac: " + bytesToHex(result));
|
||||
return result;
|
||||
};
|
||||
|
||||
Mac.init.overload('java.security.Key').implementation = function(key) {
|
||||
var keyBytes = key.getEncoded();
|
||||
console.log("[Mac.init] key=" + bytesToHex(keyBytes));
|
||||
this.init(key);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 网络请求 Hook
|
||||
|
||||
### Hook OkHttp3 请求/响应
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var OkHttpClient = Java.use("okhttp3.OkHttpClient");
|
||||
var Interceptor = Java.use("okhttp3.Interceptor");
|
||||
|
||||
// Hook newCall 获取请求 URL
|
||||
var RealCall = Java.use("okhttp3.RealCall");
|
||||
RealCall.execute.implementation = function() {
|
||||
var request = this.request();
|
||||
console.log("[OkHttp] " + request.method() + " " + request.url().toString());
|
||||
var headers = request.headers();
|
||||
for (var i = 0; i < headers.size(); i++) {
|
||||
console.log(" " + headers.name(i) + ": " + headers.value(i));
|
||||
}
|
||||
var response = this.execute();
|
||||
console.log("[OkHttp] Response: " + response.code());
|
||||
return response;
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Hook URL 连接
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var URL = Java.use("java.net.URL");
|
||||
URL.openConnection.overload().implementation = function() {
|
||||
console.log("[URL] " + this.toString());
|
||||
return this.openConnection();
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Hook WebView
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var WebView = Java.use("android.webkit.WebView");
|
||||
WebView.loadUrl.overload('java.lang.String').implementation = function(url) {
|
||||
console.log("[WebView.loadUrl] " + url);
|
||||
this.loadUrl(url);
|
||||
};
|
||||
|
||||
WebView.evaluateJavascript.implementation = function(script, callback) {
|
||||
console.log("[WebView.evaluateJavascript] " + script.substring(0, 200));
|
||||
this.evaluateJavascript(script, callback);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 绕过类 Hook
|
||||
|
||||
### 通用 SSL Pinning 绕过
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
// OkHttp3 CertificatePinner
|
||||
try {
|
||||
var CertificatePinner = Java.use("okhttp3.CertificatePinner");
|
||||
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {
|
||||
console.log("[*] SSL Pinning bypassed (OkHttp3)");
|
||||
};
|
||||
} catch(e) {}
|
||||
|
||||
// TrustManagerImpl
|
||||
try {
|
||||
var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
|
||||
TrustManagerImpl.verifyChain.implementation = function(untrustedChain) {
|
||||
console.log("[*] SSL Pinning bypassed (TrustManagerImpl)");
|
||||
return untrustedChain;
|
||||
};
|
||||
} catch(e) {}
|
||||
|
||||
// X509TrustManager
|
||||
try {
|
||||
var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
|
||||
var TrustManager = Java.registerClass({
|
||||
name: "com.bypass.TrustManager",
|
||||
implements: [X509TrustManager],
|
||||
methods: {
|
||||
checkClientTrusted: function() {},
|
||||
checkServerTrusted: function() {},
|
||||
getAcceptedIssuers: function() { return []; }
|
||||
}
|
||||
});
|
||||
} catch(e) {}
|
||||
|
||||
// Network Security Config (Android 7+)
|
||||
try {
|
||||
var NetworkSecurityConfig = Java.use("android.security.net.config.NetworkSecurityConfig");
|
||||
NetworkSecurityConfig.isCleartextTrafficPermitted.implementation = function() { return true; };
|
||||
} catch(e) {}
|
||||
});
|
||||
```
|
||||
|
||||
### 通用 Root 检测绕过
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
// File.exists 绕过
|
||||
var File = Java.use("java.io.File");
|
||||
var rootPaths = ["su", "Superuser", "magisk", "busybox", "xposed",
|
||||
"/system/xbin/su", "/system/bin/su", "/sbin/su",
|
||||
"/data/local/xbin/su", "/data/local/bin/su"];
|
||||
|
||||
File.exists.implementation = function() {
|
||||
var path = this.getAbsolutePath();
|
||||
for (var i = 0; i < rootPaths.length; i++) {
|
||||
if (path.toLowerCase().indexOf(rootPaths[i].toLowerCase()) !== -1) {
|
||||
console.log("[Root] Blocked: " + path);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return this.exists();
|
||||
};
|
||||
|
||||
// Runtime.exec 绕过
|
||||
var Runtime = Java.use("java.lang.Runtime");
|
||||
Runtime.exec.overload('java.lang.String').implementation = function(cmd) {
|
||||
if (cmd.indexOf("su") !== -1 || cmd.indexOf("which") !== -1) {
|
||||
console.log("[Root] Blocked exec: " + cmd);
|
||||
throw Java.use("java.io.IOException").$new("Permission denied");
|
||||
}
|
||||
return this.exec(cmd);
|
||||
};
|
||||
|
||||
// Build.TAGS 绕过
|
||||
var Build = Java.use("android.os.Build");
|
||||
Build.TAGS.value = "release-keys";
|
||||
});
|
||||
```
|
||||
|
||||
### 反调试绕过
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
// Debug.isDebuggerConnected
|
||||
var Debug = Java.use("android.os.Debug");
|
||||
Debug.isDebuggerConnected.implementation = function() {
|
||||
console.log("[AntiDebug] isDebuggerConnected → false");
|
||||
return false;
|
||||
};
|
||||
|
||||
// TracerPid 检测绕过(native 层)
|
||||
var fopen = Module.findExportByName("libc.so", "fopen");
|
||||
Interceptor.attach(fopen, {
|
||||
onEnter: function(args) {
|
||||
this.path = args[0].readUtf8String();
|
||||
},
|
||||
onLeave: function(retval) {
|
||||
if (this.path && this.path.indexOf("/proc/") !== -1 && this.path.indexOf("/status") !== -1) {
|
||||
// 可以进一步 hook fgets 修改 TracerPid
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 模拟器检测绕过
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var Build = Java.use("android.os.Build");
|
||||
Build.FINGERPRINT.value = "google/walleye/walleye:8.1.0/OPM1.171019.011/4448085:user/release-keys";
|
||||
Build.MODEL.value = "Pixel 2";
|
||||
Build.MANUFACTURER.value = "Google";
|
||||
Build.BRAND.value = "google";
|
||||
Build.DEVICE.value = "walleye";
|
||||
Build.PRODUCT.value = "walleye";
|
||||
Build.HARDWARE.value = "walleye";
|
||||
|
||||
// TelephonyManager
|
||||
var TelephonyManager = Java.use("android.telephony.TelephonyManager");
|
||||
TelephonyManager.getDeviceId.implementation = function() { return "352099001761481"; };
|
||||
TelephonyManager.getSubscriberId.implementation = function() { return "310260000000000"; };
|
||||
TelephonyManager.getSimSerialNumber.implementation = function() { return "89014103211118510720"; };
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据存储 Hook
|
||||
|
||||
### Hook SharedPreferences
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var SharedPreferencesImpl = Java.use("android.app.SharedPreferencesImpl");
|
||||
|
||||
SharedPreferencesImpl.getString.implementation = function(key, defValue) {
|
||||
var value = this.getString(key, defValue);
|
||||
console.log("[SP.get] " + key + " = " + value);
|
||||
return value;
|
||||
};
|
||||
|
||||
var Editor = Java.use("android.app.SharedPreferencesImpl$EditorImpl");
|
||||
Editor.putString.implementation = function(key, value) {
|
||||
console.log("[SP.put] " + key + " = " + value);
|
||||
return this.putString(key, value);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### Hook SQLite
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var SQLiteDatabase = Java.use("android.database.sqlite.SQLiteDatabase");
|
||||
|
||||
SQLiteDatabase.rawQuery.implementation = function(sql, args) {
|
||||
console.log("[SQL] " + sql);
|
||||
if (args) console.log(" args: " + JSON.stringify(args));
|
||||
return this.rawQuery(sql, args);
|
||||
};
|
||||
|
||||
SQLiteDatabase.execSQL.overload('java.lang.String').implementation = function(sql) {
|
||||
console.log("[SQL.exec] " + sql);
|
||||
this.execSQL(sql);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 脱壳 Hook
|
||||
|
||||
### 通用 DEX Dump
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
Java.enumerateClassLoaders({
|
||||
onMatch: function(loader) {
|
||||
try {
|
||||
var pathList = Java.cast(loader, Java.use("dalvik.system.BaseDexClassLoader")).pathList.value;
|
||||
var dexElements = pathList.dexElements.value;
|
||||
for (var i = 0; i < dexElements.length; i++) {
|
||||
var dexFile = dexElements[i].dexFile.value;
|
||||
if (dexFile) {
|
||||
console.log("[DEX] " + dexFile.getName());
|
||||
// 可以进一步 dump dex 内容
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
},
|
||||
onComplete: function() {}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Hook ClassLoader.loadClass
|
||||
|
||||
```javascript
|
||||
Java.perform(function() {
|
||||
var ClassLoader = Java.use("java.lang.ClassLoader");
|
||||
ClassLoader.loadClass.overload('java.lang.String').implementation = function(name) {
|
||||
if (name.indexOf("com.target") !== -1) {
|
||||
console.log("[ClassLoader] " + name);
|
||||
}
|
||||
return this.loadClass(name);
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实用工具函数
|
||||
|
||||
```javascript
|
||||
// 字节数组转十六进制
|
||||
function bytesToHex(bytes) {
|
||||
if (!bytes) return "null";
|
||||
var hex = [];
|
||||
for (var i = 0; i < bytes.length; i++) {
|
||||
hex.push(('0' + (bytes[i] & 0xFF).toString(16)).slice(-2));
|
||||
}
|
||||
return hex.join('');
|
||||
}
|
||||
|
||||
// 打印调用栈
|
||||
function printStack() {
|
||||
console.log(Java.use("android.util.Log").getStackTraceString(
|
||||
Java.use("java.lang.Throwable").$new()));
|
||||
}
|
||||
|
||||
// 打印对象所有字段
|
||||
function printFields(obj) {
|
||||
var fields = obj.class.getDeclaredFields();
|
||||
fields.forEach(function(field) {
|
||||
field.setAccessible(true);
|
||||
try {
|
||||
console.log(" " + field.getName() + " = " + field.get(obj));
|
||||
} catch(e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索内存中的类实例
|
||||
function findInstances(className) {
|
||||
Java.choose(className, {
|
||||
onMatch: function(instance) {
|
||||
console.log("[Instance] " + instance);
|
||||
printFields(instance);
|
||||
},
|
||||
onComplete: function() {}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
| 资源 | 说明 | 链接 |
|
||||
|------|------|------|
|
||||
| Frida 官方文档 | API 参考 | https://frida.re/docs/ |
|
||||
| Frida CodeShare | 社区脚本分享 | https://codeshare.frida.re/ |
|
||||
| awesome-frida | 资源大全 | https://github.com/dweinstein/awesome-frida |
|
||||
| frida-codeshare-scripts | 全网最全脚本收集 | https://github.com/zengfr/frida-codeshare-scripts |
|
||||
| Objection | Frida 封装工具 | https://github.com/sensepost/objection |
|
||||
| r2frida | radare2 + Frida 集成 | https://github.com/nowsecure/r2frida |
|
||||
158
reverse-skill-repo/skills/apk-reverse/scripts/decode.ps1
Normal file
158
reverse-skill-repo/skills/apk-reverse/scripts/decode.ps1
Normal file
@@ -0,0 +1,158 @@
|
||||
#requires -Version 5
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ApkPath,
|
||||
|
||||
[string]$Name,
|
||||
|
||||
[string]$OutRoot,
|
||||
|
||||
[switch]$SkipJadx,
|
||||
|
||||
[switch]$SkipApktool,
|
||||
|
||||
[switch]$Clean
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
. (Join-Path $PSScriptRoot '..\..\scripts\lib\ToolDiscovery.ps1')
|
||||
|
||||
function Get-SafeName {
|
||||
param([Parameter(Mandatory = $true)][string]$PathValue)
|
||||
|
||||
$raw = [System.IO.Path]::GetFileNameWithoutExtension($PathValue)
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||
$raw = 'apk'
|
||||
}
|
||||
return ($raw -replace '[^A-Za-z0-9._-]', '_')
|
||||
}
|
||||
|
||||
function Get-DefaultOutRoot {
|
||||
param([Parameter(Mandatory = $true)][string]$ApkFilePath)
|
||||
|
||||
$parent = Split-Path -Path $ApkFilePath -Parent
|
||||
if ([string]::IsNullOrWhiteSpace($parent)) {
|
||||
return [System.IO.Directory]::GetCurrentDirectory()
|
||||
}
|
||||
return $parent
|
||||
}
|
||||
|
||||
function Get-ManifestPackage {
|
||||
param([Parameter(Mandatory = $true)][string]$ManifestPath)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
try {
|
||||
$xml = [xml](Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8)
|
||||
return [string]$xml.manifest.package
|
||||
}
|
||||
catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ApkPath)) {
|
||||
throw "APK not found: $ApkPath"
|
||||
}
|
||||
|
||||
$jadxSpec = $null
|
||||
$apktoolSpec = $null
|
||||
$bootstrapScript = Join-Path $PSScriptRoot '..\..\scripts\bootstrap-reverse.ps1'
|
||||
|
||||
if (-not $SkipJadx) {
|
||||
$jadxSpec = Resolve-ReverseToolSpec -Name 'jadx'
|
||||
if (-not $jadxSpec.Available) {
|
||||
Write-Host 'INFO: jadx not found, attempting auto-bootstrap...' -ForegroundColor Yellow
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrapScript -Capability @('jadx') -SkipRefresh
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Bootstrap failed for jadx. Please install manually: https://github.com/skylot/jadx'
|
||||
}
|
||||
$jadxSpec = Resolve-ReverseToolSpec -Name 'jadx'
|
||||
if (-not $jadxSpec.Available) {
|
||||
throw 'jadx still not available after bootstrap. Check installation at %USERPROFILE%\Tools\jadx\'
|
||||
}
|
||||
Write-Host 'INFO: jadx bootstrapped successfully.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
if (-not $SkipApktool) {
|
||||
$apktoolSpec = Resolve-ReverseToolSpec -Name 'apktool'
|
||||
if (-not $apktoolSpec.Available) {
|
||||
Write-Host 'INFO: apktool not found, attempting auto-bootstrap...' -ForegroundColor Yellow
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrapScript -Capability @('apktool') -SkipRefresh
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Bootstrap failed for apktool. Please install manually: https://apktool.org/'
|
||||
}
|
||||
$apktoolSpec = Resolve-ReverseToolSpec -Name 'apktool'
|
||||
if (-not $apktoolSpec.Available) {
|
||||
throw 'apktool still not available after bootstrap. Check installation at %USERPROFILE%\Tools\apktool\'
|
||||
}
|
||||
Write-Host 'INFO: apktool bootstrapped successfully.' -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
$taskName = if ($Name) { $Name } else { Get-SafeName -PathValue $ApkPath }
|
||||
$resolvedOutRoot = if ([string]::IsNullOrWhiteSpace($OutRoot)) { Get-DefaultOutRoot -ApkFilePath $ApkPath } else { $OutRoot }
|
||||
$taskRoot = Join-Path $resolvedOutRoot $taskName
|
||||
$jadxOut = Join-Path $taskRoot 'jadx'
|
||||
$apktoolOut = Join-Path $taskRoot 'apktool'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $resolvedOutRoot)) {
|
||||
New-Item -ItemType Directory -Path $resolvedOutRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
if ($Clean -and (Test-Path -LiteralPath $taskRoot)) {
|
||||
Remove-Item -LiteralPath $taskRoot -Recurse -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $taskRoot)) {
|
||||
New-Item -ItemType Directory -Path $taskRoot -Force | Out-Null
|
||||
}
|
||||
|
||||
$jadxExitCode = $null
|
||||
if (-not $SkipJadx) {
|
||||
if (Test-Path -LiteralPath $jadxOut) {
|
||||
Remove-Item -LiteralPath $jadxOut -Recurse -Force
|
||||
}
|
||||
& $jadxSpec.Command @($jadxSpec.PrefixArgs + @('-d', $jadxOut, $ApkPath))
|
||||
$jadxExitCode = $LASTEXITCODE
|
||||
}
|
||||
|
||||
$apktoolExitCode = $null
|
||||
if (-not $SkipApktool) {
|
||||
if (Test-Path -LiteralPath $apktoolOut) {
|
||||
Remove-Item -LiteralPath $apktoolOut -Recurse -Force
|
||||
}
|
||||
& $apktoolSpec.Command @($apktoolSpec.PrefixArgs + @('d', $ApkPath, '-o', $apktoolOut, '-f'))
|
||||
$apktoolExitCode = $LASTEXITCODE
|
||||
}
|
||||
|
||||
$manifestPath = Join-Path $apktoolOut 'AndroidManifest.xml'
|
||||
$packageName = if (-not $SkipApktool) { Get-ManifestPackage -ManifestPath $manifestPath } else { '' }
|
||||
$javaCount = if ((Test-Path -LiteralPath $jadxOut)) { (Get-ChildItem -LiteralPath $jadxOut -Recurse -File -Filter '*.java' | Measure-Object).Count } else { 0 }
|
||||
$smaliDirCount = if ((Test-Path -LiteralPath $apktoolOut)) { (Get-ChildItem -LiteralPath $apktoolOut -Directory -Filter 'smali*' | Measure-Object).Count } else { 0 }
|
||||
$libCount = if ((Test-Path -LiteralPath $apktoolOut)) { (Get-ChildItem -LiteralPath $apktoolOut -Recurse -File -Filter '*.so' | Measure-Object).Count } else { 0 }
|
||||
$resXmlCount = if ((Test-Path -LiteralPath $apktoolOut)) { (Get-ChildItem -LiteralPath $apktoolOut -Recurse -File -Filter '*.xml' | Measure-Object).Count } else { 0 }
|
||||
|
||||
"task_root=$taskRoot"
|
||||
"jadx_out=$jadxOut"
|
||||
"apktool_out=$apktoolOut"
|
||||
"package=$packageName"
|
||||
"jadx_exit_code=$jadxExitCode"
|
||||
"apktool_exit_code=$apktoolExitCode"
|
||||
"java_files=$javaCount"
|
||||
"smali_dirs=$smaliDirCount"
|
||||
"so_files=$libCount"
|
||||
"xml_files=$resXmlCount"
|
||||
|
||||
if (($jadxExitCode -ne $null) -and ($jadxExitCode -ne 0)) {
|
||||
"warning=jadx returned non-zero exit code; inspect output but treat exported sources as usable if present"
|
||||
}
|
||||
127
reverse-skill-repo/skills/apk-reverse/scripts/decode.sh
Normal file
127
reverse-skill-repo/skills/apk-reverse/scripts/decode.sh
Normal file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
# decode.sh — APK 解包(jadx 反编译 + apktool 解包)
|
||||
# 等价于 Windows 版的 decode.ps1
|
||||
#
|
||||
# 用法:
|
||||
# bash decode.sh <apk_path> [--name <task_name>] [--out <output_dir>]
|
||||
# [--skip-jadx] [--skip-apktool] [--clean]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KALI_BOOTSTRAP="$(cd "$SCRIPT_DIR/../../../kali/scripts" 2>/dev/null && pwd)/bootstrap-reverse.sh"
|
||||
|
||||
# ─── 参数解析 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
APK_PATH=""
|
||||
TASK_NAME=""
|
||||
OUT_ROOT=""
|
||||
SKIP_JADX=false
|
||||
SKIP_APKTOOL=false
|
||||
CLEAN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) TASK_NAME="$2"; shift 2 ;;
|
||||
--out) OUT_ROOT="$2"; shift 2 ;;
|
||||
--skip-jadx) SKIP_JADX=true; shift ;;
|
||||
--skip-apktool) SKIP_APKTOOL=true; shift ;;
|
||||
--clean) CLEAN=true; shift ;;
|
||||
-*) echo "未知选项: $1"; exit 1 ;;
|
||||
*) APK_PATH="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$APK_PATH" ]]; then
|
||||
echo "用法: $0 <apk_path> [--name <name>] [--out <dir>] [--skip-jadx] [--skip-apktool] [--clean]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$APK_PATH" ]]; then
|
||||
echo "ERR: APK 文件不存在: $APK_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── 工具检测与自动安装 ─────────────────────────────────────────────────────────────
|
||||
|
||||
ensure_tool() {
|
||||
local name="$1"
|
||||
if command -v "$name" &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
echo "INFO: $name 未找到,尝试自动安装..."
|
||||
if [[ -x "$KALI_BOOTSTRAP" ]]; then
|
||||
bash "$KALI_BOOTSTRAP" "$name" --skip-refresh 2>/dev/null || true
|
||||
fi
|
||||
if ! command -v "$name" &>/dev/null; then
|
||||
echo "ERR: $name 安装失败,请手动安装"
|
||||
return 1
|
||||
fi
|
||||
echo "INFO: $name 安装成功"
|
||||
}
|
||||
|
||||
[[ "$SKIP_JADX" != "true" ]] && ensure_tool "jadx"
|
||||
[[ "$SKIP_APKTOOL" != "true" ]] && ensure_tool "apktool"
|
||||
|
||||
# ─── 路径计算 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
APK_BASENAME=$(basename "$APK_PATH" .apk | sed 's/[^A-Za-z0-9._-]/_/g')
|
||||
TASK_NAME="${TASK_NAME:-$APK_BASENAME}"
|
||||
OUT_ROOT="${OUT_ROOT:-$(dirname "$APK_PATH")}"
|
||||
TASK_ROOT="$OUT_root:REDACTED"
|
||||
JADX_OUT="$TASK_ROOT/jadx"
|
||||
APKTOOL_OUT="$TASK_ROOT/apktool"
|
||||
|
||||
if [[ "$CLEAN" == "true" && -d "$TASK_ROOT" ]]; then
|
||||
rm -rf "$TASK_ROOT"
|
||||
fi
|
||||
|
||||
mkdir -p "$TASK_ROOT"
|
||||
|
||||
# ─── jadx 反编译 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
JADX_EXIT=0
|
||||
if [[ "$SKIP_JADX" != "true" ]]; then
|
||||
rm -rf "$JADX_OUT"
|
||||
echo "=== jadx 反编译 ==="
|
||||
jadx -d "$JADX_OUT" "$APK_PATH" || JADX_EXIT=$?
|
||||
fi
|
||||
|
||||
# ─── apktool 解包 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
APKTOOL_EXIT=0
|
||||
if [[ "$SKIP_APKTOOL" != "true" ]]; then
|
||||
rm -rf "$APKTOOL_OUT"
|
||||
echo "=== apktool 解包 ==="
|
||||
apktool d "$APK_PATH" -o "$APKTOOL_OUT" -f || APKTOOL_EXIT=$?
|
||||
fi
|
||||
|
||||
# ─── 统计输出 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
PACKAGE=""
|
||||
if [[ -f "$APKTOOL_OUT/AndroidManifest.xml" ]]; then
|
||||
PACKAGE=$(grep -oP 'package="[^"]*"' "$APKTOOL_OUT/AndroidManifest.xml" 2>/dev/null | head -1 | sed 's/package="//;s/"//')
|
||||
fi
|
||||
|
||||
JAVA_COUNT=0
|
||||
[[ -d "$JADX_OUT" ]] && JAVA_COUNT=$(find "$JADX_OUT" -name "*.java" | wc -l)
|
||||
|
||||
SMALI_DIRS=0
|
||||
[[ -d "$APKTOOL_OUT" ]] && SMALI_DIRS=$(find "$APKTOOL_OUT" -maxdepth 1 -type d -name "smali*" | wc -l)
|
||||
|
||||
SO_COUNT=0
|
||||
[[ -d "$APKTOOL_OUT" ]] && SO_COUNT=$(find "$APKTOOL_OUT" -name "*.so" | wc -l)
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo " APK 解包完成"
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo " task_root=$TASK_ROOT"
|
||||
echo " jadx_out=$JADX_OUT"
|
||||
echo " apktool_out=$APKTOOL_OUT"
|
||||
echo " package=$PACKAGE"
|
||||
echo " jadx_exit_code=$JADX_EXIT"
|
||||
echo " apktool_exit_code=$APKTOOL_EXIT"
|
||||
echo " java_files=$JAVA_COUNT"
|
||||
echo " smali_dirs=$SMALI_DIRS"
|
||||
echo " so_files=$SO_COUNT"
|
||||
146
reverse-skill-repo/skills/apk-reverse/scripts/frida-run.ps1
Normal file
146
reverse-skill-repo/skills/apk-reverse/scripts/frida-run.ps1
Normal file
@@ -0,0 +1,146 @@
|
||||
#requires -Version 5
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Package,
|
||||
|
||||
[string]$Process,
|
||||
|
||||
[string]$RemoteHost = '127.0.0.1:27042',
|
||||
|
||||
[string]$ScriptPath,
|
||||
|
||||
[switch]$Usb,
|
||||
|
||||
[switch]$Spawn,
|
||||
|
||||
[switch]$Pause,
|
||||
|
||||
[switch]$ListDevices,
|
||||
|
||||
[switch]$ListProcesses
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
function Get-PythonScriptCandidates {
|
||||
param([Parameter(Mandatory = $true)][string]$ExecutableName)
|
||||
|
||||
$roots = @(
|
||||
(Join-Path $env:APPDATA 'Python'),
|
||||
(Join-Path $env:LOCALAPPDATA 'Programs\Python')
|
||||
)
|
||||
|
||||
$candidates = New-Object System.Collections.Generic.List[string]
|
||||
foreach ($root in $roots) {
|
||||
if (-not (Test-Path -LiteralPath $root)) {
|
||||
continue
|
||||
}
|
||||
|
||||
$matches = Get-ChildItem -LiteralPath $root -Directory -ErrorAction SilentlyContinue
|
||||
foreach ($dir in $matches) {
|
||||
$candidate = Join-Path $dir.FullName (Join-Path 'Scripts' $ExecutableName)
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
$candidates.Add($candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $candidates
|
||||
}
|
||||
|
||||
function Get-ToolPath {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
|
||||
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
return $cmd.Source
|
||||
}
|
||||
|
||||
foreach ($candidate in Get-PythonScriptCandidates -ExecutableName ($Name + '.exe')) {
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
# Attempt auto-bootstrap for frida tools
|
||||
$bootstrapScript = Join-Path $PSScriptRoot '..\..\scripts\bootstrap-reverse.ps1'
|
||||
if (($Name -in @('frida', 'frida-ps', 'frida-ls-devices')) -and (Test-Path -LiteralPath $bootstrapScript)) {
|
||||
Write-Host "INFO: $Name not found, attempting auto-bootstrap (pip install frida-tools)..." -ForegroundColor Yellow
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrapScript -Capability @('frida') -SkipRefresh
|
||||
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
Write-Host "INFO: $Name bootstrapped successfully." -ForegroundColor Green
|
||||
return $cmd.Source
|
||||
}
|
||||
foreach ($candidate in Get-PythonScriptCandidates -ExecutableName ($Name + '.exe')) {
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw "Missing required CLI tool: $Name — install with: pip install frida-tools"
|
||||
}
|
||||
|
||||
$fridaLsDevices = Get-ToolPath -Name 'frida-ls-devices'
|
||||
$fridaPs = Get-ToolPath -Name 'frida-ps'
|
||||
$frida = Get-ToolPath -Name 'frida'
|
||||
$python = Get-Command python -ErrorAction SilentlyContinue
|
||||
|
||||
if (-not $python) {
|
||||
throw 'Missing required CLI tool: python'
|
||||
}
|
||||
|
||||
$pythonExe = $python.Source
|
||||
|
||||
if ($ListDevices) {
|
||||
& $pythonExe -c "import frida; [print(f'{d.id}`t{d.type}`t{d.name}') for d in frida.enumerate_devices()]"
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$target = if ($Package) { $Package } elseif ($Process) { $Process } else { '' }
|
||||
if ([string]::IsNullOrWhiteSpace($target) -and -not $ListProcesses) {
|
||||
throw 'Provide -Package or -Process, or use -ListProcesses.'
|
||||
}
|
||||
|
||||
$deviceFlag = if ($Usb) { '-U' } else { '-H' }
|
||||
|
||||
if ($ListProcesses) {
|
||||
$processArgs = @()
|
||||
if ($Usb) {
|
||||
$processArgs += '-U'
|
||||
}
|
||||
else {
|
||||
$processArgs += @('-H', $RemoteHost)
|
||||
}
|
||||
& $fridaPs @processArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
||||
throw "Frida script not found: $ScriptPath"
|
||||
}
|
||||
|
||||
$fridaArgs = @($deviceFlag)
|
||||
if (-not $Usb) {
|
||||
$fridaArgs += $RemoteHost
|
||||
}
|
||||
if ($Spawn) {
|
||||
$fridaArgs += '-f'
|
||||
} else {
|
||||
$fridaArgs += '-n'
|
||||
}
|
||||
$fridaArgs += $target
|
||||
$fridaArgs += '-l'
|
||||
$fridaArgs += $ScriptPath
|
||||
if (-not $Pause) {
|
||||
$fridaArgs += '--no-pause'
|
||||
}
|
||||
|
||||
& $frida @fridaArgs
|
||||
exit $LASTEXITCODE
|
||||
126
reverse-skill-repo/skills/apk-reverse/scripts/frida-run.sh
Normal file
126
reverse-skill-repo/skills/apk-reverse/scripts/frida-run.sh
Normal file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# frida-run.sh — Frida 动态注入脚本
|
||||
# 等价于 Windows 版的 frida-run.ps1
|
||||
#
|
||||
# 用法:
|
||||
# bash frida-run.sh --package <pkg> --script <js> [--usb] [--spawn]
|
||||
# bash frida-run.sh --list-devices
|
||||
# bash frida-run.sh --list-processes [--usb]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KALI_BOOTSTRAP="$(cd "$SCRIPT_DIR/../../../kali/scripts" 2>/dev/null && pwd)/bootstrap-reverse.sh"
|
||||
|
||||
# ─── 参数 ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
PACKAGE=""
|
||||
PROCESS=""
|
||||
REMOTE_HOST="127.0.0.1:27042"
|
||||
SCRIPT_PATH=""
|
||||
USB=false
|
||||
SPAWN=false
|
||||
PAUSE=false
|
||||
LIST_DEVICES=false
|
||||
LIST_PROCESSES=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--package|-p) PACKAGE="$2"; shift 2 ;;
|
||||
--process) PROCESS="$2"; shift 2 ;;
|
||||
--remote|-H) REMOTE_HOST="$2"; shift 2 ;;
|
||||
--script|-l) SCRIPT_PATH="$2"; shift 2 ;;
|
||||
--usb|-U) USB=true; shift ;;
|
||||
--spawn|-f) SPAWN=true; shift ;;
|
||||
--pause) PAUSE=true; shift ;;
|
||||
--list-devices) LIST_DEVICES=true; shift ;;
|
||||
--list-processes) LIST_PROCESSES=true; shift ;;
|
||||
-*) echo "未知选项: $1"; exit 1 ;;
|
||||
*) echo "未知参数: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ─── 工具检测 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
ensure_frida() {
|
||||
if command -v frida &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
echo "INFO: frida 未找到,尝试安装..."
|
||||
if [[ -x "$KALI_BOOTSTRAP" ]]; then
|
||||
bash "$KALI_BOOTSTRAP" frida --skip-refresh 2>/dev/null || true
|
||||
fi
|
||||
if ! command -v frida &>/dev/null; then
|
||||
echo "ERR: frida 不可用。安装: pip3 install frida-tools"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_frida
|
||||
|
||||
# ─── 列出设备 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$LIST_DEVICES" == "true" ]]; then
|
||||
frida-ls-devices 2>/dev/null || python3 -c "
|
||||
import frida
|
||||
for d in frida.enumerate_devices():
|
||||
print(f'{d.id}\t{d.type}\t{d.name}')
|
||||
"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─── 列出进程 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$LIST_PROCESSES" == "true" ]]; then
|
||||
if [[ "$USB" == "true" ]]; then
|
||||
frida-ps -U
|
||||
else
|
||||
frida-ps -H "$REMOTE_HOST"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ─── 注入执行 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
TARGET="${PACKAGE:-$PROCESS}"
|
||||
if [[ -z "$TARGET" ]]; then
|
||||
echo "用法: $0 --package <pkg> --script <js> [--usb] [--spawn]"
|
||||
echo " 或: $0 --list-devices"
|
||||
echo " 或: $0 --list-processes [--usb]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "$SCRIPT_PATH" || ! -f "$SCRIPT_PATH" ]]; then
|
||||
echo "ERR: Frida 脚本不存在: ${SCRIPT_PATH:-未指定}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FRIDA_ARGS=()
|
||||
|
||||
if [[ "$USB" == "true" ]]; then
|
||||
FRIDA_ARGS+=("-U")
|
||||
else
|
||||
FRIDA_ARGS+=("-H" "$REMOTE_HOST")
|
||||
fi
|
||||
|
||||
if [[ "$SPAWN" == "true" ]]; then
|
||||
FRIDA_ARGS+=("-f")
|
||||
else
|
||||
FRIDA_ARGS+=("-n")
|
||||
fi
|
||||
|
||||
FRIDA_ARGS+=("$TARGET")
|
||||
FRIDA_ARGS+=("-l" "$SCRIPT_PATH")
|
||||
|
||||
if [[ "$PAUSE" != "true" ]]; then
|
||||
FRIDA_ARGS+=("--no-pause")
|
||||
fi
|
||||
|
||||
echo "=== Frida 注入 ==="
|
||||
echo " 目标: $TARGET"
|
||||
echo " 脚本: $SCRIPT_PATH"
|
||||
echo " 模式: $([ "$SPAWN" == "true" ] && echo "spawn" || echo "attach")"
|
||||
echo " 连接: $([ "$USB" == "true" ] && echo "USB" || echo "$REMOTE_HOST")"
|
||||
echo ""
|
||||
|
||||
frida "${FRIDA_ARGS[@]}"
|
||||
@@ -0,0 +1,80 @@
|
||||
#requires -Version 5
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ManifestPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ManifestPath)) {
|
||||
throw "Manifest not found: $ManifestPath"
|
||||
}
|
||||
|
||||
$xml = [xml](Get-Content -LiteralPath $ManifestPath -Raw -Encoding UTF8)
|
||||
$androidNs = 'http://schemas.android.com/apk/res/android'
|
||||
$nsMgr = [System.Xml.XmlNamespaceManager]::new($xml.NameTable)
|
||||
$nsMgr.AddNamespace('android', $androidNs)
|
||||
|
||||
function Get-AndroidAttr {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]$Node,
|
||||
[Parameter(Mandatory = $true)][string]$Name
|
||||
)
|
||||
|
||||
return [string]$Node.GetAttribute($Name, $androidNs)
|
||||
}
|
||||
|
||||
$manifest = $xml.manifest
|
||||
$packageName = [string]$manifest.package
|
||||
|
||||
"package=$packageName"
|
||||
|
||||
$permissions = @($manifest.SelectNodes('uses-permission'))
|
||||
"permission_count=$($permissions.Count)"
|
||||
foreach ($perm in $permissions) {
|
||||
$name = Get-AndroidAttr -Node $perm -Name 'name'
|
||||
if ($name) {
|
||||
"permission=$name"
|
||||
}
|
||||
}
|
||||
|
||||
$componentQueries = @(
|
||||
@{ Label = 'activity'; Path = 'application/activity' },
|
||||
@{ Label = 'service'; Path = 'application/service' },
|
||||
@{ Label = 'receiver'; Path = 'application/receiver' },
|
||||
@{ Label = 'provider'; Path = 'application/provider' }
|
||||
)
|
||||
|
||||
foreach ($query in $componentQueries) {
|
||||
$nodes = @($manifest.SelectNodes($query.Path))
|
||||
($query.Label + '_count=' + $nodes.Count)
|
||||
foreach ($node in $nodes) {
|
||||
$name = Get-AndroidAttr -Node $node -Name 'name'
|
||||
$exported = Get-AndroidAttr -Node $node -Name 'exported'
|
||||
$enabled = Get-AndroidAttr -Node $node -Name 'enabled'
|
||||
($query.Label + '=' + $name + "`t" + $exported + "`t" + $enabled)
|
||||
}
|
||||
}
|
||||
|
||||
$mainActivities = @()
|
||||
$activities = @($manifest.SelectNodes('application/activity'))
|
||||
foreach ($activity in $activities) {
|
||||
$filters = @($activity.SelectNodes('intent-filter'))
|
||||
foreach ($filter in $filters) {
|
||||
$hasMain = @($filter.SelectNodes('action')).Where({ (Get-AndroidAttr -Node $_ -Name 'name') -eq 'android.intent.action.MAIN' }).Count -gt 0
|
||||
$hasLauncher = @($filter.SelectNodes('category')).Where({ (Get-AndroidAttr -Node $_ -Name 'name') -eq 'android.intent.category.LAUNCHER' }).Count -gt 0
|
||||
if ($hasMain -and $hasLauncher) {
|
||||
$mainActivities += (Get-AndroidAttr -Node $activity -Name 'name')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($entry in ($mainActivities | Select-Object -Unique)) {
|
||||
"main_activity=$entry"
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
#requires -Version 5
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$ProjectDir,
|
||||
|
||||
[string]$OutDir,
|
||||
|
||||
[string]$BaseName,
|
||||
|
||||
[string]$KeystorePath,
|
||||
|
||||
[string]$KeyAlias = 'androiddebugkey',
|
||||
|
||||
[string]$StorePass = 'android',
|
||||
|
||||
[string]$KeyPass = 'android',
|
||||
|
||||
[string]$DeviceSerial,
|
||||
|
||||
[switch]$Install,
|
||||
|
||||
[switch]$Reinstall,
|
||||
|
||||
[switch]$Clean
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($KeystorePath)) {
|
||||
$KeystorePath = Join-Path (Join-Path $env:USERPROFILE '.android') 'debug.keystore'
|
||||
}
|
||||
|
||||
function Get-ToolPath {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
|
||||
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
return $cmd.Source
|
||||
}
|
||||
|
||||
$sdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk'
|
||||
$buildTools = @()
|
||||
if (Test-Path -LiteralPath (Join-Path $sdkRoot 'build-tools')) {
|
||||
$buildTools = Get-ChildItem -LiteralPath (Join-Path $sdkRoot 'build-tools') -Directory -ErrorAction SilentlyContinue |
|
||||
Sort-Object -Property Name -Descending
|
||||
}
|
||||
|
||||
$fallbacks = @{
|
||||
'apktool' = @(
|
||||
(Join-Path $env:USERPROFILE 'Tools\apktool\apktool.bat')
|
||||
)
|
||||
'zipalign' = @($buildTools | ForEach-Object { Join-Path $_.FullName 'zipalign.exe' })
|
||||
'apksigner' = @($buildTools | ForEach-Object { Join-Path $_.FullName 'apksigner.bat' })
|
||||
'keytool' = @()
|
||||
'adb' = @(
|
||||
(Join-Path $sdkRoot 'platform-tools\adb.exe'),
|
||||
(Join-Path $env:USERPROFILE 'AppData\Local\Android\Sdk\platform-tools\adb.exe')
|
||||
)
|
||||
}
|
||||
|
||||
if ($fallbacks.Contains($Name)) {
|
||||
foreach ($candidate in $fallbacks[$Name]) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Attempt auto-bootstrap for supported tools
|
||||
$bootstrapScript = Join-Path $PSScriptRoot '..\..\scripts\bootstrap-reverse.ps1'
|
||||
$bootstrapSupported = @('adb', 'apktool')
|
||||
if ($Name -in $bootstrapSupported -and (Test-Path -LiteralPath $bootstrapScript)) {
|
||||
Write-Host "INFO: $Name not found, attempting auto-bootstrap..." -ForegroundColor Yellow
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $bootstrapScript -Capability @($Name) -SkipRefresh
|
||||
$cmd = Get-Command $Name -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
Write-Host "INFO: $Name bootstrapped successfully." -ForegroundColor Green
|
||||
return $cmd.Source
|
||||
}
|
||||
# Re-check fallbacks after bootstrap
|
||||
if ($fallbacks.Contains($Name)) {
|
||||
foreach ($candidate in $fallbacks[$Name]) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($candidate) -and (Test-Path -LiteralPath $candidate)) {
|
||||
Write-Host "INFO: $Name found at fallback path after bootstrap." -ForegroundColor Green
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Clear error message for tools that cannot be auto-bootstrapped
|
||||
$manualHint = switch ($Name) {
|
||||
'zipalign' { 'Install Android Build-Tools via Android SDK Manager (sdkmanager "build-tools;35.0.0")' }
|
||||
'apksigner' { 'Install Android Build-Tools via Android SDK Manager (sdkmanager "build-tools;35.0.0")' }
|
||||
default { "Install $Name manually" }
|
||||
}
|
||||
throw "Missing required CLI tool: $Name — $manualHint"
|
||||
}
|
||||
|
||||
function Ensure-DebugKeystore {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string]$Keytool,
|
||||
[Parameter(Mandatory = $true)][string]$Alias,
|
||||
[Parameter(Mandatory = $true)][string]$StorePassword,
|
||||
[Parameter(Mandatory = $true)][string]$KeyPassword
|
||||
)
|
||||
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
return
|
||||
}
|
||||
|
||||
$parent = Split-Path -Path $Path -Parent
|
||||
if (-not (Test-Path -LiteralPath $parent)) {
|
||||
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
||||
}
|
||||
|
||||
& $Keytool -genkeypair -v -keystore $Path -storepass $StorePassword -keypass $KeyPassword -alias $Alias -keyalg RSA -keysize 2048 -validity 10000 -dname 'CN=Android Debug,O=ReverseSkill,C=CN'
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'Failed to generate debug keystore.'
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ProjectDir)) {
|
||||
throw "Project directory not found: $ProjectDir"
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($OutDir)) {
|
||||
$projectParent = Split-Path -Path $ProjectDir -Parent
|
||||
if ([string]::IsNullOrWhiteSpace($projectParent)) {
|
||||
$projectParent = [System.IO.Directory]::GetCurrentDirectory()
|
||||
}
|
||||
$OutDir = $projectParent
|
||||
}
|
||||
|
||||
$apktool = Get-ToolPath -Name 'apktool'
|
||||
$zipalign = Get-ToolPath -Name 'zipalign'
|
||||
$apksigner = Get-ToolPath -Name 'apksigner'
|
||||
$keytool = Get-ToolPath -Name 'keytool'
|
||||
$adb = Get-ToolPath -Name 'adb'
|
||||
|
||||
Ensure-DebugKeystore -Path $KeystorePath -Keytool $keytool -Alias $KeyAlias -StorePassword $StorePass -KeyPassword $KeyPass
|
||||
|
||||
if (-not (Test-Path -LiteralPath $OutDir)) {
|
||||
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$name = if ($BaseName) { $BaseName } else { Split-Path -Path $ProjectDir -Leaf }
|
||||
$unsignedApk = Join-Path $OutDir ($name + '-unsigned.apk')
|
||||
$alignedApk = Join-Path $OutDir ($name + '-aligned.apk')
|
||||
$signedApk = Join-Path $OutDir ($name + '-signed.apk')
|
||||
|
||||
if ($Clean) {
|
||||
foreach ($path in @($unsignedApk, $alignedApk, $signedApk)) {
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
Remove-Item -LiteralPath $path -Force
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& $apktool b $ProjectDir -o $unsignedApk
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'apktool build failed.'
|
||||
}
|
||||
|
||||
& $zipalign -f -p 4 $unsignedApk $alignedApk
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'zipalign failed.'
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath $alignedApk -Destination $signedApk -Force
|
||||
& $apksigner sign --ks $KeystorePath --ks-key-alias $KeyAlias --ks-pass "pass:$StorePass" --key-pass "pass:$KeyPass" --out $signedApk $alignedApk
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'apksigner sign failed.'
|
||||
}
|
||||
|
||||
& $apksigner verify --print-certs $signedApk
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'apksigner verify failed.'
|
||||
}
|
||||
|
||||
"unsigned_apk=$unsignedApk"
|
||||
"aligned_apk=$alignedApk"
|
||||
"signed_apk=$signedApk"
|
||||
"keystore=$KeystorePath"
|
||||
|
||||
if ($Install) {
|
||||
$installArgs = @()
|
||||
if ($DeviceSerial) {
|
||||
$installArgs += '-s'
|
||||
$installArgs += $DeviceSerial
|
||||
}
|
||||
$installArgs += 'install'
|
||||
if ($Reinstall) {
|
||||
$installArgs += '-r'
|
||||
}
|
||||
$installArgs += $signedApk
|
||||
|
||||
& $adb @installArgs
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw 'adb install failed.'
|
||||
}
|
||||
|
||||
"install_device=$DeviceSerial"
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
# rebuild-sign-install.sh — APK 重打包 + 签名 + 安装
|
||||
# 等价于 Windows 版的 rebuild-sign-install.ps1
|
||||
#
|
||||
# 用法:
|
||||
# bash rebuild-sign-install.sh <project_dir> [--out <dir>] [--name <base>]
|
||||
# [--keystore <path>] [--install] [--reinstall]
|
||||
# [--device <serial>] [--clean]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
KALI_BOOTSTRAP="$(cd "$SCRIPT_DIR/../../../kali/scripts" 2>/dev/null && pwd)/bootstrap-reverse.sh"
|
||||
DEFAULT_KEYSTORE="$HOME/.android/debug.keystore"
|
||||
|
||||
# ─── 参数 ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
PROJECT_DIR=""
|
||||
OUT_DIR=""
|
||||
BASE_NAME=""
|
||||
KEYSTORE="$DEFAULT_KEYSTORE"
|
||||
KEY_ALIAS="androiddebugkey"
|
||||
STORE_PASS="android"
|
||||
KEY_PASS="android"
|
||||
DEVICE_SERIAL=""
|
||||
DO_INSTALL=false
|
||||
REINSTALL=false
|
||||
CLEAN=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--out) OUT_DIR="$2"; shift 2 ;;
|
||||
--name) BASE_NAME="$2"; shift 2 ;;
|
||||
--keystore) KEYSTORE="$2"; shift 2 ;;
|
||||
--alias) KEY_ALIAS="$2"; shift 2 ;;
|
||||
--store-pass) STORE_PASS="$2"; shift 2 ;;
|
||||
--key-pass) KEY_PASS="$2"; shift 2 ;;
|
||||
--device) DEVICE_SERIAL="$2"; shift 2 ;;
|
||||
--install) DO_INSTALL=true; shift ;;
|
||||
--reinstall) REINSTALL=true; DO_INSTALL=true; shift ;;
|
||||
--clean) CLEAN=true; shift ;;
|
||||
-*) echo "未知选项: $1"; exit 1 ;;
|
||||
*) PROJECT_DIR="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PROJECT_DIR" || ! -d "$PROJECT_DIR" ]]; then
|
||||
echo "用法: $0 <apktool_project_dir> [options]"
|
||||
echo " --out <dir> 输出目录(默认: 项目父目录)"
|
||||
echo " --name <base> 输出文件名前缀"
|
||||
echo " --keystore <path> 签名密钥库(默认: ~/.android/debug.keystore)"
|
||||
echo " --install 签名后安装到设备"
|
||||
echo " --reinstall 覆盖安装"
|
||||
echo " --device <serial> 指定设备"
|
||||
echo " --clean 清理旧产物"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ─── 工具检测 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
ensure_tool() {
|
||||
local name="$1"
|
||||
if command -v "$name" &>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
echo "INFO: $name 未找到,尝试自动安装..."
|
||||
if [[ -x "$KALI_BOOTSTRAP" ]]; then
|
||||
bash "$KALI_BOOTSTRAP" "$name" --skip-refresh 2>/dev/null || true
|
||||
fi
|
||||
if ! command -v "$name" &>/dev/null; then
|
||||
echo "ERR: $name 不可用。"
|
||||
case "$name" in
|
||||
zipalign|apksigner) echo " 安装: sudo apt install android-sdk-build-tools 或 sdkmanager 'build-tools;35.0.0'" ;;
|
||||
*) echo " 请手动安装 $name" ;;
|
||||
esac
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_tool "apktool"
|
||||
ensure_tool "zipalign"
|
||||
ensure_tool "apksigner"
|
||||
ensure_tool "keytool"
|
||||
[[ "$DO_INSTALL" == "true" ]] && ensure_tool "adb"
|
||||
|
||||
# ─── 生成 debug keystore(如果不存在) ─────────────────────────────────────────────
|
||||
|
||||
if [[ ! -f "$KEYSTORE" ]]; then
|
||||
echo "INFO: 生成 debug keystore: $KEYSTORE"
|
||||
keytool -genkeypair -v \
|
||||
-keystore "$KEYSTORE" \
|
||||
-storepass "$STORE_PASS" \
|
||||
-keypass "$KEY_PASS" \
|
||||
-alias "$KEY_ALIAS" \
|
||||
-keyalg RSA -keysize 2048 -validity 10000 \
|
||||
-dname "CN=Android Debug,O=ReverseSkill,C=CN"
|
||||
fi
|
||||
|
||||
# ─── 路径计算 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
OUT_DIR="${OUT_DIR:-$(dirname "$PROJECT_DIR")}"
|
||||
BASE_NAME="${BASE_NAME:-$(basename "$PROJECT_DIR")}"
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
UNSIGNED_APK="$OUT_DIR/${BASE_NAME}-unsigned.apk"
|
||||
ALIGNED_APK="$OUT_DIR/${BASE_NAME}-aligned.apk"
|
||||
SIGNED_APK="$OUT_DIR/${BASE_NAME}-signed.apk"
|
||||
|
||||
if [[ "$CLEAN" == "true" ]]; then
|
||||
rm -f "$UNSIGNED_APK" "$ALIGNED_APK" "$SIGNED_APK"
|
||||
fi
|
||||
|
||||
# ─── 重打包 ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "=== apktool 重打包 ==="
|
||||
apktool b "$PROJECT_DIR" -o "$UNSIGNED_APK"
|
||||
|
||||
# ─── 对齐 ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "=== zipalign 对齐 ==="
|
||||
zipalign -f -p 4 "$UNSIGNED_APK" "$ALIGNED_APK"
|
||||
|
||||
# ─── 签名 ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "=== apksigner 签名 ==="
|
||||
apksigner sign \
|
||||
--ks "$KEYSTORE" \
|
||||
--ks-key-alias "$KEY_ALIAS" \
|
||||
--ks-pass "pass:$STORE_PASS" \
|
||||
--key-pass "pass:$KEY_PASS" \
|
||||
--out "$SIGNED_APK" \
|
||||
"$ALIGNED_APK"
|
||||
|
||||
# ─── 验证 ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo "=== 验证签名 ==="
|
||||
apksigner verify --print-certs "$SIGNED_APK"
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo " APK 重打包完成"
|
||||
echo "═══════════════════════════════════════════"
|
||||
echo " unsigned_apk=$UNSIGNED_APK"
|
||||
echo " aligned_apk=$ALIGNED_APK"
|
||||
echo " signed_apk=$SIGNED_APK"
|
||||
echo " keystore=$KEYSTORE"
|
||||
|
||||
# ─── 安装 ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$DO_INSTALL" == "true" ]]; then
|
||||
echo "=== adb 安装 ==="
|
||||
ADB_ARGS=()
|
||||
[[ -n "$DEVICE_SERIAL" ]] && ADB_ARGS+=("-s" "$DEVICE_SERIAL")
|
||||
ADB_ARGS+=("install")
|
||||
[[ "$REINSTALL" == "true" ]] && ADB_ARGS+=("-r")
|
||||
ADB_ARGS+=("$SIGNED_APK")
|
||||
|
||||
adb "${ADB_ARGS[@]}"
|
||||
echo " install_device=${DEVICE_SERIAL:-default}"
|
||||
fi
|
||||
Reference in New Issue
Block a user