目录
  1. 1. 一、静态分析概述
  2. 2. 二、工具链搭建
    1. 2.1. 工具选择决策树
    2. 2.2. 各工具命令参考
  3. 3. 三、Manifest 审查
    1. 3.1. Manifest 安全审计清单
    2. 3.2. automation: 使用 drozer 自动审计
  4. 4. 四、DEX 反编译深入
    1. 4.1. JADX 使用技巧
  5. 5. 五、敏感 API 追踪
    1. 5.1. 高级搜索策略
  6. 6. 六、硬编码秘密搜索
    1. 6.1. 硬编码密钥的常见藏身之处
  7. 7. 七、Smali 代码阅读基础
    1. 7.1. Smali 指令集分类速查
    2. 7.2. Smali 方法结构模板
  8. 8. 八、反编译失败的应对策略
    1. 8.1. 原因1:DEX 被加固/加密
    2. 8.2. 原因2:控制流平坦化(Control Flow Flattening)
    3. 8.3. 原因3:字符串加密
  9. 9. 九、静态分析自动化
    1. 9.1. 使用 JADX 插件和脚本
    2. 9.2. 使用 Androguard 进行 Python 自动化
  10. 10. 面试常考问题
【逆向安全技术-实战篇】静态方式逆向应用

一、静态分析概述

静态分析是在不运行应用的前提下,通过反编译工具对 APK 文件进行解析的过程。相比动态调试,静态分析更安全(不触发反调试机制),是逆向工程的第一步。

核心流程为:APK 解包 → DEX 反编译 → Manifest 审查 → 敏感 API 追踪 → 硬编码搜索 → Smali 关键代码定位 → 攻击面分析 → 编写利用

┌─────────────────────────────────────────────────────────┐
│ 静态分析六层框架 │
├─────────────────────────────────────────────────────────┤
│ │
│ 第一层: 文件结构分析 │
│ zipinfo、unzip、file 命令检查 APK 内部文件 │
│ 识别加固特征、异常文件、隐藏载荷 │
│ │
│ 第二层: Manifest 安全审计 │
│ 导出组件、权限组合、debuggable 标志、backup 属性 │
│ │
│ 第三层: 资源与字符串分析 │
│ strings.xml、arrays.xml、raw 资源、assets 文件 │
│ │
│ 第四层: DEX 反编译分析 │
│ JADX/BytecodeViewer/GDA 多引擎交叉验证 │
│ 关键 API 追踪、数据流分析 │
│ │
│ 第五层: Smali 代码审查 │
│ 混淆代码阅读、加密算法还原、控制流分析 │
│ │
│ 第六层: Native 库分析 │
│ IDA/Ghidra 反汇编、符号分析、字符串提取 │
│ │
└─────────────────────────────────────────────────────────┘

二、工具链搭建

首选工具组合:

工具 用途 仓库地址
apktool 解包/重打包 APK https://github.com/iBotPeaches/Apktool
JADX DEX → Java 反编译 https://github.com/skylot/jadx
GDA DEX 反编译(支持混淆) https://github.com/charles2gan/GDA-android-reversing-tool
BytecodeViewer 多引擎反编译 https://github.com/Konloch/bytecode-viewer
ClassyShark APK 浏览器 https://github.com/google/android-classyshark
radare2/rizin 全面二进制分析 https://github.com/rizinorg/rizin
Ghidra NSA 开源逆向框架 https://github.com/NationalSecurityAgency/ghidra

工具选择决策树

你拿到一个 APK →
├─ 只想快速浏览代码 → JADX-GUI(直接打开 APK)
├─ 混淆严重,JADX 反编译失败 → GDA(专为混淆代码优化)
├─ 需要多引擎交叉验证 → BytecodeViewer(集成 6+ 反编译器)
├─ 需要分析加固壳 → apktool 解包后手动分析
├─ 需要分析 Native so → IDA Pro / Ghidra
└─ 需要脚本批量处理 → JADX 命令行 + Python 脚本

各工具命令参考

# 使用 apktool 解包 APK
apktool d target.apk -o output_dir

# 使用 JADX 命令行反编译
jadx -d output_source target.apk

# 使用 JADX GUI 打开 APK
jadx-gui target.apk

# JADX 高级选项(处理混淆代码)
jadx --deobf --show-bad-code --escape-unicode \
--deobf-min 3 --deobf-max 5 \
target.apk -d output_source

# GDA 命令行模式(如果有)
# GDA 通常在 Windows 下运行,是 GUI 工具

# BytecodeViewer 命令行
java -jar BytecodeViewer.jar

# ClassyShark 浏览 APK
java -jar ClassyShark.jar -open target.apk

三、Manifest 审查

AndroidManifest.xml 是静态分析的起点,重点关注:

  • exported 组件:Activity、Service、BroadcastReceiver、ContentProvider 设置 android:exported="true" 意味着可被第三方调用
  • intent-filter:定义了组件响应的隐式 Intent
  • permissions:自定义权限可能暴露敏感操作
  • application 属性android:debuggableandroid:allowBackup 等安全相关属性

Manifest 安全审计清单

<!-- 高危示例1:导出且无权限保护的 Activity -->
<activity
android:name=".SecretActivity"
android:exported="true" />
<!-- 攻击:任意应用可以通过 startActivity() 直接启动 -->

<!-- 高危示例2:导出 ContentProvider 无读写权限管控 -->
<provider
android:name=".MyProvider"
android:authorities="com.example.provider"
android:exported="true" />
<!-- 攻击:任意应用可以查询/修改数据 -->

<!-- 高危示例3:可调试的应用 -->
<application
android:debuggable="true">
<!-- 攻击:可通过 adb/jdwp 连接调试器,获取运行时数据 -->

<!-- 高危示例4:允许备份 -->
<application
android:allowBackup="true">
<!-- 攻击:adb backup 导出应用数据到 PC -->

<!-- 高危示例5:自定义权限的保护级别过低 -->
<permission
android:name="com.example.SENSITIVE"
android:protectionLevel="normal" />
<!-- normal 级别:任何应用声明即可获得,无用户确认 -->
<!-- 应使用 signature 级别:仅相同签名的应用可获得 -->

automation: 使用 drozer 自动审计

drozer 是专门的 Android 安全审计框架:

# 启动 drozer
adb forward tcp:31415 tcp:31415
drozer console connect

# 获取攻击面摘要
dz> run app.package.attacksurface com.target.app

# 列出所有导出 Activity(含无权限保护的)
dz> run app.activity.info -a com.target.app

# 列出所有导出 ContentProvider
dz> run app.provider.info -a com.target.app

# 尝试从 Provider 读取数据
dz> run app.provider.query content://com.target.provider/data

# 扫描可注入的 SQL
dz> run scanner.provider.injection -a com.target.app

# 列出所有导出 Service
dz> run app.service.info -a com.target.app

四、DEX 反编译深入

JADX 使用技巧

1. 全局搜索(Ctrl+Shift+F)

搜索场景与关键词策略:

加密相关:
"SecretKeySpec" → AES/DES 密钥初始化
"Cipher" → 加密/解密操作入口点
"getInstance" → Cipher 实例创建(配合上下文判断算法)
"IvParameterSpec" → IV 向量(CBC 模式)
"KeyPairGenerator" → 非对称加密密钥生成

网络相关:
"HttpURLConnection" → 原生 HTTP 请求
"OkHttpClient" → OkHttp 网络库
"Retrofit" → Retrofit API 定义(拦截接口调用)
"baseUrl" → API 基础地址
"addInterceptor" → 网络拦截器(可能有签名/加密逻辑)

存储相关:
"SharedPreferences" → 本地键值存储
"SQLiteDatabase" → SQLite 数据库操作
"openOrCreateDatabase"
"MODE_WORLD_READABLE" → 危险的文件权限模式

Native 相关:
"System.loadLibrary" → Native 库加载入口
"System.load" → 动态加载 so(包括加密 so)

反调试相关:
"isDebuggerConnected"
"FLAG_DEBUGGABLE"
"/proc/self/status"
"TracerPid"
"ptrace"

签名校验:
"getPackageInfo"
"signatures"
"GET_SIGNATURES"

2. 交叉引用(Xref)分析

在 JADX 中右键方法/字段 → Find Usage(Alt+F7),追踪调用链:

定位关键逻辑的标准流程:

找到目标 API 调用
→ Find Usage 追溯调用者
→ 继续 Find Usage 追溯上层调用者
→ 重复直到达到入口点(通常是从 Activity.onCreate/Service.onStart 等出发)

反之也可以从入口点向下追踪:
找到入口 Activity
→ 分析 onCreate() 中的初始化逻辑
→ 追踪到核心方法的调用

3. 反混淆辅助

JADX 的 --deobf 选项可以重命名混淆的类名和方法名:

# deobf-min 控制最短名称长度(小于此长度则重命名)
# deobf-max 控制最长名称长度(大于此长度则重命名)
jadx --deobf --deobf-min 3 --deobf-max 5 target.apk -d output_source

手动反混淆技巧:

  • 混淆的类名通常为 a.class, b.class, aa.class 等短名称
  • 在 JADX 中手动按 n 键可以给类/方法重命名(仅影响当前查看,不修改文件)
  • 通过调用关系推断功能:如果一个类大量使用 Cipher API,它很可能是加密工具类

五、敏感 API 追踪

在 JADX 中按 Ctrl+Shift+F 全局搜索以下关键字符串:

搜索关键词 目的
SecretKeySpec / Cipher 加密算法及密钥
HttpURLConnection / OkHttp 网络请求及后端地址
SharedPreferences / SQLiteDatabase 本地存储敏感数据
getSignature 签名校验逻辑
System.loadLibrary Native 库加载入口
Toast.makeText 调试信息残留

高级搜索策略

基于正则表达式的模式搜索:

# 在 jadx 导出的所有 Java 文件中搜索
cd jadx_output/

# IP 地址
grep -rE "[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}" --include="*.java"

# URL 模式
grep -rE "https?://[a-zA-Z0-9.-]+" --include="*.java"

# Base64 编码的字符串(长度 > 20)
grep -rE '"[A-Za-z0-9+/]{20,}={0,2}"' --include="*.java"

# 十六进制密钥
grep -rE "\{ ?0x[0-9a-fA-F]{2}" --include="*.java"

# 硬编码盐值
grep -rEi "salt|iv|nonce" --include="*.java"

六、硬编码秘密搜索

攻击者最常利用的一类漏洞就是硬编码。在 JADX 中搜索:

"http://" / "https://"     → 后端 API 地址
"secret" / "password" / "key" / "token" → 密钥
"AES" / "DES" / "RSA" → 加密算法常量
"BEGIN RSA PRIVATE KEY" → 硬编码私钥

硬编码密钥的常见藏身之处

// 1. 直接字符串常量(最常见,也最危险)
private static final String AES_KEY = "1234567890abcdef";
private static final String API_TOKEN = "Bearer eyJhbGciOiJIUzI1NiJ9...";

// 2. 字符数组(稍好一点,但静态分析仍可见)
private static final char[] ENC_KEY = {'k', 'e', 'y', '1', '2', '3'};

// 3. 资源文件中(strings.xml, raw/ 目录)
// 在 res/values/strings.xml 中:
// <string name="api_key">sk_live_1234567890abcdef</string>

自动搜索脚本:

#!/usr/bin/env python3
"""扫描 JADX 输出目录中的硬编码秘密"""

import os
import re
from pathlib import Path

PATTERNS = {
'api_key': r'(?:api[_-]?key|apikey)\s*[:=]\s*["\']([^"\']{8,})["\']',
'aws_key': r'AKIA[0-9A-Z]{16}',
'private_key': r'-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----',
'jwt_token': r'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}',
'url_with_creds': r'https?://[^:]+:[^@]+@[a-zA-Z0-9.-]+',
'password': r'(?:password|passwd|pwd)\s*[:=]\s*["\']([^"\']{4,})["\']',
'base64_20plus': r'["\'][A-Za-z0-9+/]{20,}={0,2}["\']',
}

def scan_directory(root_dir):
for java_file in Path(root_dir).rglob("*.java"):
try:
content = java_file.read_text(errors='ignore')
for name, pattern in PATTERNS.items():
for match in re.finditer(pattern, content, re.IGNORECASE):
line_no = content[:match.start()].count('\n') + 1
print(f"[{name}] {java_file}:{line_no}{match.group()[:80]}")
except Exception as e:
pass

if __name__ == "__main__":
import sys
scan_directory(sys.argv[1] if len(sys.argv) > 1 else ".")

七、Smali 代码阅读基础

当 JADX 反编译失败(如混淆过度的类)时,需要直接阅读 Smali 代码:

Smali 指令集分类速查

# ===== 移动与赋值指令 =====
move vA, vB # vA = vB
move/from16 vA, vB # vA = vB(vA 用 8 位索引,vB 用 16 位索引)
move-object vA, vB # 对象引用赋值
const/4 vA, #+B # vA = B(4 位有符号常量,范围 -8~7)
const/16 vA, #+BBBB # vA = BBBB(16 位有符号常量)
const vA, #+BBBBBBBB # vA = BBBBBBBB(32 位有符号常量)
const-string vA, "string" # vA = "string"

# ===== 方法调用指令 =====
invoke-virtual {params}, method_to_call # 虚方法调用(多态)
invoke-direct {params}, method_to_call # 直接方法调用(构造器、private、super)
invoke-static {params}, method_to_call # 静态方法调用
invoke-interface {params}, method_to_call # 接口方法调用
invoke-super {params}, method_to_call # 父类方法调用

# ===== 字段操作指令 =====
iget vA, vB, field_id # vA = vB.field(读取实例字段)
iput vA, vB, field_id # vB.field = vA(写入实例字段)
sget vA, field_id # vA = Class.field(读取静态字段)
sput vA, field_id # Class.field = vA(写入静态字段)

# ===== 返回指令 =====
return-void # 无返回值
return vA # 返回 32 位值
return-wide vA # 返回 64 位值
return-object vA # 返回对象引用

# ===== 条件跳转指令 =====
if-eq vA, vB, :label # if (vA == vB) goto label
if-ne vA, vB, :label # if (vA != vB) goto label
if-lt vA, vB, :label # if (vA < vB) goto label
if-ge vA, vB, :label # if (vA >= vB) goto label
if-gt vA, vB, :label # if (vA > vB) goto label
if-le vA, vB, :label # if (vA <= vB) goto label
if-eqz vA, :label # if (vA == 0) goto label
if-nez vA, :label # if (vA != 0) goto label
if-ltz vA, :label # if (vA < 0) goto label
if-gez vA, :label # if (vA >= 0) goto label

# ===== 无条件跳转 =====
goto :label # goto label
goto/16 :label # goto label(16 位偏移)
goto/32 :label # goto label(32 位偏移)

# ===== 数组操作 =====
aget vA, vB, vC # vA = vB[vC]
aput vA, vB, vC # vB[vC] = vA
new-array vA, vB, type # vA = new type[vB]
array-length vA, vB # vA = vB.length

# ===== 对象操作 =====
new-instance vA, type # vA = new type()
check-cast vA, type # vA = (type) vA
instance-of vA, vB, type # vA = (vB instanceof type)

Smali 方法结构模板

# 完整方法结构解析
.method public doSomething(Ljava/lang/String;I)Z # 方法签名
.registers 6 # 总寄存器数
.locals 3 # 局部变量数(不含参数)

# 参数说明(非静态方法)
# p0 = this (Lcom/example/MainActivity;)
# p1 = 第一个参数 String
# p2 = 第二个参数 int
# v0-v2 = 局部变量

.param p1, "input" # Ljava/lang/String;
.param p2, "count" # I

.prologue # 方法体开始
.line 42 # 对应 Java 源码第 42 行

# 方法体指令...
const/4 v0, 0x1
if-nez p2, :cond_0

invoke-direct {p0, p1}, Lcom/example/MyClass;->helper(Ljava/lang/String;)V

:cond_0
return v0
.end method

Smali 中的寄存器命名规则:本地变量用 v0, v1...,方法参数用 p0, p1...(p0 在非静态方法中代表 this)。

八、反编译失败的应对策略

原因1:DEX 被加固/加密

症状: JADX 报错 “Invalid dex magic” 或仅反编译出少量壳代码

判断方法:

# 检查 classes.dex 大小
ls -la target/classes.dex
# 如果只有几十 KB,很可能是壳

# 查看 Manifest 是否有壳的特征类
apktool d target.apk -o output
grep -r "StubShell\|StubApp\|Wrapper\|ProxyApplication" output/AndroidManifest.xml

# 查看 lib/ 目录下是否有壳的特征 SO
ls output/lib/armeabi-v7a/
# libtup.so → 腾讯乐固
# libjiagu.so → 360加固
# libDexHelper.so → 顶象

应对: 先脱壳(参考加固逆向专题文章),再进行静态分析。

原因2:控制流平坦化(Control Flow Flattening)

症状: JADX 反编译出的代码包含大量 while(true) { switch(...) { case ... } } 结构

应对策略:

  • 使用 GDA Pro(对控制流平坦化有专门的还原算法)
  • 手动分析:识别 switch 的条件变量,追踪其赋值逻辑,还原原始控制流
  • 使用 angr 或 Miasm 进行符号执行辅助分析

原因3:字符串加密

症状: 代码中看不到任何有意义的字符串常量,全是乱码或 base64

定位解密函数:

// 在 JADX 中搜索这种模式
// 1. 搜索 "getString" 或应用自定义的字符串获取方法
// 2. 关注静态初始化块 <clinit> 中的字符串处理
// 3. 寻找大量 const-string 结合 for 循环的方法(可能是批量解密)

// 典型的字符串解密模式:
public static String d(String encrypted) {
char[] chars = encrypted.toCharArray();
for (int i = 0; i < chars.length; i++) {
chars[i] = (char) (chars[i] ^ KEY[i % KEY.length]);
}
return new String(chars);
}

九、静态分析自动化

使用 JADX 插件和脚本

# jadx_analysis.py — 使用 jadx API 进行自动化分析
# 需要引入 jadx 的 Python 绑定或使用 subprocess

import subprocess
import json
import os

def batch_decompile(apk_dir, output_dir):
"""批量反编译目录中的 APK"""
for apk in os.listdir(apk_dir):
if apk.endswith('.apk'):
output_path = os.path.join(output_dir, apk.replace('.apk', ''))
subprocess.run([
'jadx', '--deobf', '--show-bad-code',
os.path.join(apk_dir, apk),
'-d', output_path
])

def search_patterns(source_dir, patterns):
"""在反编译输出中搜索模式"""
import re
results = {}
for root, dirs, files in os.walk(source_dir):
for file in files:
if file.endswith('.java'):
filepath = os.path.join(root, file)
with open(filepath, 'r', errors='ignore') as f:
content = f.read()
for name, pattern in patterns.items():
matches = re.findall(pattern, content)
if matches:
if name not in results:
results[name] = []
results[name].append({
'file': filepath,
'matches': matches
})
return results

# 使用示例
patterns = {
'crypto': r'javax\.crypto\.(Cipher|SecretKeySpec|KeyGenerator)',
'network': r'(HttpURLConnection|OkHttpClient|Retrofit)',
'reflection': r'java\.lang\.reflect\.(Method|Field)',
}

使用 Androguard 进行 Python 自动化

# androguard 是 Python 编写的 Android 分析框架
from androguard.core.bytecodes.apk import APK
from androguard.core.bytecodes.dvm import DalvikVMFormat
from androguard.core.analysis.analysis import Analysis

# 加载 APK
apk = APK("target.apk")

# 获取基本信息
print(f"Package: {apk.get_package()}")
print(f"Main Activity: {apk.get_main_activity()}")
print(f"Permissions: {apk.get_permissions()}")
print(f"Activities: {apk.get_activities()}")
print(f"Services: {apk.get_services()}")
print(f"Receivers: {apk.get_receivers()}")
print(f"Providers: {apk.get_providers()}")

# 获取 DEX 对象
dex = DalvikVMFormat(apk.get_dex())
analysis = Analysis(dex)

# 搜索特定字符串
for string in dex.get_strings():
if 'password' in str(string).lower():
print(f"Found: {string}")

# 搜索方法调用
for method in dex.get_methods():
if 'Ljavax/crypto/Cipher' in method.get_descriptor():
print(f"Cipher usage in: {method.get_class_name()}->{method.get_name()}")

面试常考问题

Q1:静态分析和动态分析的区别?什么时候优先选择静态分析?

A:静态分析不运行程序,通过反编译查看代码和资源;动态分析需要运行程序并通过调试器/Hook 观察运行时行为。当目标应用有反调试/反Hook机制时优先静态分析;当你想快速了解应用整体架构、搜索硬编码秘密时也优先静态分析。此外,静态分析是任何逆向任务的起点——没有静态分析提供的代码地图,动态调试就像在黑暗中摸索,效率极低。两者的最佳实践是协同:先用静态分析建立整体认知和定位关键代码,再用动态分析验证假设和获取运行时数据。

Q2:JADX 反编译失败的原因有哪些?如何应对?

A:常见原因包括:DEX 被加固/加密、代码被深度混淆(控制流平坦化)、使用了 JADX 不支持的字节码特性。应对方法:先判断是否加固(用 apktool 解包看是否有壳特征文件),若是则先脱壳;若是混淆导致,可尝试 GDA 或 BytecodeViewer 的多个引擎交替使用;最后手段是直接阅读 Smali 代码。另外,JADX 的 --show-bad-code 参数非常重要——即使反编译不完全,它也会输出”尽力而为”的伪代码,在许多情况下已经足够理解逻辑。

Q3:如何在大量反编译代码中快速定位关键逻辑?

A:采用”由外向内”的策略:先看 Manifest 找到入口组件 → 在入口组件的 onCreate 中找核心逻辑调用 → 通过字符串搜索定位相关类 → 利用 JADX 的交叉引用(Xref)功能追溯调用链 → 使用”查字符串→查常量→查方法签名”的搜索技巧逐层深入。核心技巧:从你能确定的东西开始(如一个已知的 URL 字符串、一个特定的 API 调用),然后通过 Xref 向上下追溯,而不是试图从海量代码中”猜”出关键逻辑在哪里。

Q4:什么是交叉引用(Xref)分析?在逆向中如何使用?

A:交叉引用是指在代码中查找某个符号(类、方法、字段、字符串)被哪些地方引用。在 JADX 中,右键一个符号 → “Find Usage”(Alt+F7)即可列出所有引用点。在逆向中的典型用法:(1) 找到一个关键方法(如 encrypt())→ 查谁调用了它 → 找到加密的触发入口;(2) 找到一个常量字符串(如 API 地址)→ 查引用 → 找到网络请求的构造代码;(3) 找到一个敏感字段(如 secretKey)→ 查读取 → 找到密钥的使用处 → 查写入 → 找到密钥的生成/加载处。交叉引用是追踪调用链、建立代码地图的核心手段。

Q5:面对使用了 ProGuard/R8 混淆的应用,静态分析有哪些应对策略?

A:(1) 利用 JADX 的 deobf 功能自动重命名短名称;(2) 通过类成员和方法签名推断功能——例如一个类包含 encrypt(byte[], byte[])decrypt(byte[], byte[]) 方法,它很可能是加密工具类;(3) 关注未被混淆的元素——字符串常量(加密前)、Android 系统 API 调用、第三方库的类名通常是保留的;(4) 使用反混淆映射——如果应用发布时保留了 mapping.txt(Google Play 的 deobfuscation file),可以直接将混淆名映射回原始名;(5) 动态调试辅助——在关键 API 处设断点,观察实际运行的类和调用关系,再回到静态分析中标记这些类。

打赏
  • 微信
  • 支付宝

评论