
Apk Reverse
- 202 installs
- 18.1k repo stars
- Updated August 4, 2026
- zhaoxuya520/reverse-skill
Guide an agent through Android APK reverse-engineering tasks when you need to inspect or audit a mobile app package.
About
apk-reverse is an agent skill from the reverse-skill collection aimed at Android APK reverse-engineering scenarios. Prism’s ingested readme for this entry did not expose human-readable procedure text, so treat the skill as a mobile security helper whose concrete commands and tooling should be confirmed in the source repository after install. Solo builders might still catalog it when they need agent-assisted decompilation, manifest review, or static analysis prep as part of shipping or vetting an Android build. Use it only on packages you are entitled to analyze, and combine outputs with manual verification because automated reverse engineering can mislabel obfuscated or stripped binaries.
- Named for APK reverse-engineering workflows in the reverse-skill collection
- Intended for mobile Android package analysis rather than server API design
- Fits security-minded solo builders validating app behavior or third-party SDKs
- Pair with legal/ethical constraints—only analyze apps you own or are authorized to test
- Prism listing lacks readable SKILL.md body; verify steps inside the repo before production use
Apk Reverse by the numbers
- 202 all-time installs (skills.sh)
- +35 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #447 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zhaoxuya520/reverse-skill --skill apk-reverseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 202 |
|---|---|
| repo stars | ★ 18.1k |
| Last updated | August 4, 2026 |
| Repository | zhaoxuya520/reverse-skill ↗ |
What it does
Guide an agent through Android APK reverse-engineering tasks when you need to inspect or audit a mobile app package.
Files
APK 逆向 CLI 作业规范
适用范围
当任务属于以下场景时优先使用本 skill:
- 分析 APK 的 Java 业务逻辑
- 定位登录、签名、风控、证书校验、root 检测
- 查看与修改
AndroidManifest.xml - 查看与修改 smali
- 重打包 APK
- 用 Frida 做 Java/native 动态 Hook
- APK 内含
.so时切到 native 分析
当前机器已验证可用的 CLI 工具
jadx1.5.5apktool3.0.2frida-ps17.9.6adbjava
优先使用脚本的场景
以下流程高频且参数容易出错,优先用 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 devicesadb logcatfrida-ps -Ujadx --versionapktool --version
自带脚本
scripts/decode.ps1
用途:
- 统一跑
jadx和apktool - 默认在原 APK 同目录创建任务输出目录
- 输出
package、java_files、smali_dirs、so_files等摘要 - 兼容
jadx部分反编译错误但仍然有可用产物的情况
示例:
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 -SkipJadxscripts/frida-run.ps1
用途:
- 统一 Frida 的设备、进程、spawn/attach 入口
- 避免手写参数时混淆
-f、-n、-U
示例:
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重建 APKzipalign对齐apksigner签名与验签- 可选直接
adb install
示例:
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
示例:
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-reverseradare2
工具分工
jadx
用于:
- Java 反编译阅读
- 包名、类名、方法名搜索
- 先从高层逻辑理解 APK
常用命令:
jadx -d jadx_out app.apk
jadx --single-class com.example.LoginActivity -d jadx_out app.apk
jadx --deobf -d jadx_out app.apkapktool
用于:
- 解包 APK
- 查看和修改
AndroidManifest.xml - 查看和修改 smali
- 重建 APK
常用命令:
apktool d app.apk -o apktool_out
apktool b apktool_out -o rebuilt.apkfrida
用于:
- 动态观察 Java 方法调用
- Hook native 导出函数
- 绕过 root 检测、证书校验、调试检测
常用命令:
frida-ps -U
frida -U -f com.example.app -l hook.js
frida-trace -U -f com.example.app -j '*!*certificate*'adb
用于:
- 设备连接
- 安装 APK
- 查看日志
- 拉取文件
常用命令:
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、receiverlib/目录里是否有.so
2. Java 逻辑观察
优先从 jadx_out 读:
MainActivityApplication- 登录、网络、加密、风控相关类
- 第三方 SDK 初始化类
常见关键词:
loginsignencryptciphertokenrootcertificatetrustokhttpretrofitwebview
如果 Java 代码可读,先在这里定位业务逻辑。
3. Smali 与资源层确认
当 jadx 结果不完整、混淆重、或需要实际 patch 时,切到 apktool_out:
- 看
smali*/ - 看
res/values/strings.xml - 看
AndroidManifest.xml
优先 patch:
android:exported- 调试标记
- root 检测返回值
- 登录验证逻辑
- 证书校验分支
4. 重建与安装
修改后:
apktool b apktool_out -o rebuilt.apk或者直接用脚本闭环:
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 层
快速命令备忘
# 反编译 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\ |
| 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.ps1scripts/rebuild-sign-install.ps1:缺 adb 或 apktool 时自动调用 bootstrapscripts/frida-run.ps1:当前仍为手动检查(frida 通常已通过 pip 安装)
自举失败时
如果自动安装失败,脚本会抛出明确错误并附带手动安装链接。常见原因:
- 网络不通(GitHub API / PyPI 不可达)
- winget 不可用(Windows 版本过低)
- Java 未安装(apktool 依赖 JDK)
0�
f0�
*�H��
��
� �0� �0�� *�H��
�����0��0��*�H��
��@0�<0f *�H��
0Y08 *�H��
0+
6�iv��S�-Gh#���9' 0*�H��
0 `�He*3��Gd�+�ڃb?�q���xu���,~b�zmg���
+ܣ2��,���t�6�7/�W$3����}w�ؐ�ζ�����
Hߞ���r��!���!�y�R:�r6�J9~��)�R�f�����Ae��۹Q�@�+=O��@F���1s4�����K�Bb3.��}�{��y!�&�@bk�}�8f�g�u����:����WZ��g�-C��S��Nϛ�Ie�q�=i��3�^%�t��v~x@�-06�>�@X�*������StڴG��ۆ[�b�H�q�@�1�eaoȩ����vi�a1qsK�8ֲ?�:��*6K��Q�SP��j���YN�IT�ONX��4��
�I��k.�b���.}�\v�p�$%�?��:5Z�(`i��B��o�u��u�U���=�@/��GrAr�L���&��W�2F���dc�u�.�����;�f���>բD�� ��q�:�3k���V��Y0̶�c�&����}�l!-۠��~��j��@��o��_!��u)��RF���svv����T�6�ۋ߅X�+hؾ��KŚ���x¢L���u�����M�
��;IoS�I>��T�%Ū�ũ�킔�?A�U`��W�^`��b�H�l�:p�*���]ъ�U�9�=©5,i �#
aNډ��p�oPi&�E͏�L~ʕ.���z6zv���)l9��bN�?�!of��i������>�^�C1]�tV��5��]�7����q:��6��hi��=6�U���<\����"�I�q��T���M���n�%Ֆ0Y�+����՜3Ӆ$%�IU�a��?�K��!��N5�.K�*�;�(�
6�O٣ž��j�?���~K��^�[���$��&��J��U�Cf���n�l���4�<�W0��BQHIP�F�,�"a�Aoy��D�*�=Aփv�E0ؿJ��~~�M�E��=T^���������k��J�i�#����1�X����հFC� �Y���k���[�(B�:WG�ti�l�'����͢DS��h�3;�
$n����i�lrI|3�s��C��[�V�j�Q@h�6��^���~��襦sP>�9��m��m5��%6��ˢ���JG���\�9��hV<Z+^?y$v��wY�9�|F���-'��[�ӊiT� 0�+Ͷ��a���"qYǚ��Nք�x�ɏ���M��-�1R0- *�H��
1 androiddebugkey0! *�H��
1Time 17782373978780�1 *�H��
��"0�0� *�H��
0f *�H��
0Y08 *�H��
0+�B+wЁ+G����
�F��' 0*�H��
0 `�He*Kyd�ج�8u}�������-�ߗS�� �=��F[b;�ڳT���ѷw�sr��յ�>~_X�F��~s.�%���p��~=��p��a����)vv^��V�����iK�2x]S�5�@4��a&���ĝ�_x��U��z�%���.�2~�H���K3F�q�Z�����T����rj-
^*lQ���QM�?�Lu���0�O��D��mNZ-����ko����@��jڽH�rV5����{ւ���y�8��o'�u�Ks�[G߰��6�%�����K.���n���S�)�q��c_�D��|��p�ȝ���5Ŏи���;�4A����ɥ&�V�l�O7o�\:�t�r������'R��1��AW9��"��8<�\hkw�o�H�/�p��1�0{�9��An����%w4ӵ��&|�{-p�=6�����ˊ�_L/nz?18*y/�%h��{X
ql�A�����Vc���]h�iW���1��3�v< �|��w�f%%rY�u틢o�U�%��T�ת�
z}C9��8%S�Z�gɰwX5V������wL�[j[�|S9�;V��(*Kڽ6V9�W�,��U�L2��l�_�Bb;���.�l��ຢa�Ȥh4�D�0 ����`���|~n.`�Vp�,��(��Ez5f��=� ����;@�j�rtB�Y�}���7�s%d��(��4��@6��h�T��@*�"�"UC|>������"KM�.%"� �<z�l�?e@:*)4jr[R�
�e��`���(��'��kf�[��b�d������X�К�K��A�K�g���/��fȋ�(S��ďܘO~x]94b�ߊiA�������zh�Β���c�6h�a���{��vhpVl`�'�3����&H�-Ma,%X��|5L�Jڜ���k����0M010
`�He ︍+>ˉ���q���7+3�<�3����\3��u�<��������{�b'Android 高级逆向参考
覆盖 Native SO 分析、Frida 高级用法、SSL Pinning 绕过、Root 检测对抗、加固脱壳、Flutter/React Native 逆向。
---
Native SO 逆向
分析流程
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 函数注册
// 静态注册:函数名 = 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 的技巧
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 函数
// 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 方法
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);
};
});内存搜索与修改
// 搜索内存中的字符串
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 绕过
通用方案(推荐)
// 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 专项
// 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 绕过
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 |
通用脱壳方法
方法 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 脚本
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
1. 解压 APK → assets/index.android.bundle(JS 代码)
2. 格式化 JS → 搜索 API 地址、密钥、签名逻辑
3. 如果有 Hermes 字节码(.hbc 文件)→ 用 hermes-dec 反编译
4. Hook: 用 Frida hook Java 层的 ReactBridgeFlutter
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 |
APK 安全测试速查
基于 OWASP MASTG(Mobile Application Security Testing Guide)整理。
覆盖静态分析、动态分析、网络通信、数据存储、认证授权、代码保护六大维度。
---
静态分析检查清单
Manifest 审计
□ 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 过低 → 可能缺少安全特性代码审计关键点
□ 硬编码密钥/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 未指定包名)第三方库审计
□ 过时的 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 一行命令
# 追踪所有加密操作
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 快速命令
# 连接
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> # 连接数据库---
网络通信安全
抓包配置
方法 1: 系统代理 + Burp/mitmproxy
- 设置 WiFi 代理 → Burp 监听地址
- 安装 CA 证书到设备
- Android 7+ 需要 network_security_config 或 Frida 绕过
方法 2: VPN 模式(推荐)
- 使用 HttpCanary / Packet Capture
- 不需要 root,不需要配置代理
- 但无法解密 SSL Pinning 的流量
方法 3: Frida + r2frida
- 直接在进程内拦截网络调用
- 不受代理/VPN 限制检查项
□ 是否使用 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 \ |
| 备份文件 | 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
# 越权测试
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 分钟)
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 关键函数 → 确认发现Frida Bypass Kit — Android 通用安全绕过框架
来源: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() - 阻止调试器检测
- 绕过反调试检查
使用方法
# 前置条件
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 |
| 0xCD4/SSL-bypass | 通用非定制 SSL 绕过 | GitHub |
| incogbyte/ssl-bypass gist | 绕过常见 SSL pinning 方法 | Gist |
| Zero3141/Frida-OkHttp-Bypass | 专门针对 OkHttp CertificatePinner | GitHub |
与本包的集成
在 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,再针对性调整。
Frida 实战脚本速查
精选自 awesome-frida、Frida-Mobile-Scripts、frida-codeshare-scripts 等开源项目。
按场景分类,直接复制使用。
---
通用 Hook 模板
Hook 任意 Java 方法
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 构造函数
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);
};
});枚举所有方法
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 加解密
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
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
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 请求/响应
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 连接
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
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 绕过
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 检测绕过
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";
});反调试绕过
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
}
}
});
});模拟器检测绕过
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
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
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
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
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);
};
});---
实用工具函数
// 字节数组转十六进制
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 |
#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"
}
#!/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/$TASK_NAME"
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"
#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) {
$escapedRemoteHost = $RemoteHost.Replace("'", "''")
$pythonFlag = if ($Usb) { 'usb' } else { 'remote-host' }
& $pythonExe -c "import frida; manager = frida.get_device_manager(); device = frida.get_usb_device() if '$pythonFlag' == 'usb' else manager.add_remote_device('$escapedRemoteHost'); [print(f'{p.pid}`t{p.name}') for p in device.enumerate_processes()]"
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
#!/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[@]}"
#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"
}
#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 $PSScriptRoot '..\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"
}
#!/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="$SCRIPT_DIR/../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> 签名密钥库(默认: 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