概述
Android 逆向工程的第一步是熟悉工具链。本文梳理逆向开发中最常用的命令行工具及其使用场景,覆盖 APK 拆解、DEX 分析、SO 调试、网络抓包等核心环节。每个命令都会深入其底层原理、典型输出解读以及实战中的常见陷阱。
Android 逆向工具链按工作流可以划分为以下阶段:
[APK 获取] → [资源/Manifest 审查] → [DEX 反编译] → [Native SO 分析] ↓ [Smali 修改/插桩] ↓ [重打包/签名] → [动态调试/验证]
|
一、APK 拆解与重打包
apktool 是 Android 逆向的瑞士军刀,用于反编译 APK 中的资源文件和 smali 代码。其内部基于 smali/baksmali 和 aapt 构建。
apktool d target.apk -o output_dir
apktool b output_dir -o repacked.apk
apktool d -s target.apk -o output_dir
apktool d -f target.apk -o output_dir
apktool d --no-res target.apk -o output_dir
apktool if framework-res.apk apktool d -p framework_dir target.apk -o output_dir
|
| 参数 |
含义 |
d / decode |
解码/反编译 |
b / build |
重新打包 |
-s |
跳过 dex 解码(保留 classes.dex 原始状态) |
-r |
跳过资源解码 |
-f |
强制删除目标目录覆盖 |
-p |
指定 framework 资源目录 |
--only-main-classes |
仅反编译主 dex(处理 multi-dex) |
apktool 输出目录结构详解:
output_dir/ ├── AndroidManifest.xml # 反编译后的 Manifest(可读文本) ├── apktool.yml # apktool 元数据(版本、sdk 信息等) ├── original/ # 原始未解码的文件 │ ├── AndroidManifest.xml # 原始二进制 Manifest │ └── META-INF/ # 原始签名文件 ├── res/ # 解码后的资源文件 │ ├── values/ │ │ ├── strings.xml # 字符串资源(含混淆前后的映射) │ │ ├── styles.xml │ │ └── public.xml # 资源 ID 映射表(关键!) │ └── ... └── smali/ # Smali 代码 └── com/ └── example/ └── MainActivity.smali
|
面试要点:apktool 反编译后生成的是 smali 代码而非 Java 源码,smali 是 Dalvik/ART 虚拟机的汇编语言,每一个寄存器操作都清晰可见。public.xml 文件记录了资源 ID 与资源名称的映射关系,在分析资源引用时非常重要。
apktool 常见问题与解决:
java -Xmx4G -jar apktool.jar d target.apk -o output_dir
|
1.2 aapt / aapt2(资源检查)
aapt(Android Asset Packaging Tool)是分析 APK 结构的高效工具,无需完整反编译即可获取关键信息:
aapt dump badging target.apk
aapt list target.apk
aapt dump resources target.apk | grep -E "string|permission"
aapt2 dump strings target.apk aapt2 dump resources target.apk
aapt dump permissions target.apk aapt dump badging target.apk | grep uses-permission
|
aapt 在逆向中的典型用途:
- 快速获取包名和主 Activity 名称(用于
adb shell am start)
- 审计权限组合(判断应用是否有可疑权限申请)
- 查看 SDK 版本范围(推断可用的攻击面)
1.3 jarsigner / apksigner
Android 签名经过了三代演进,逆向工程师需要理解每一代的特点以应对不同的签名校验场景。
jarsigner -verbose -keystore debug.keystore -storepass android app.apk androiddebugkey
apksigner sign --ks debug.keystore --ks-pass pass:android app.apk
apksigner verify -v app.apk
keytool -list -v -keystore debug.keystore -storepass android
keytool -printcert -jarfile app.apk | grep SHA
|
签名版本演进原理:
V1 (JAR Signature): META-INF/MANIFEST.MF → META-INF/CERT.SF → META-INF/CERT.RSA 对每个 JAR entry 分别计算哈希,性能差,修改资源即破坏签名
V2 (APK Signature Scheme): 在 APK 文件 ZIP 结构中的 Central Directory 之前插入签名块 对整个文件内容(不含签名块自身)计算哈希,验证快
V3 (APK Signature Scheme): 在 V2 基础上支持密钥轮换(key rotation) 签名块中包含签名证书的完整链条
V4 (APK Signature Scheme): 签名信息独立存储在 .idsig 文件中 用于 Android 11+ 的增量安装
|
Android 7.0 (API 24) 开始引入 APK Signature Scheme v2,对整个文件进行签名校验,比 V1 逐条目签名性能大幅提升。Android 9.0 (API 28) 进一步引入 V3 支持密钥轮换。
签名绕过技术的对应关系:
- V1 签名校验:Hook
java.security.Signature.verify() 或修改签名文件
- V2/V3 签名校验:需要修改后使用 apksigner 重新签名,同时 Hook
PackageManager.getPackageInfo() 返回原始签名信息
- 自定义校验(Native 层):通常 Hook
dlopen/dlsym 拦截 so 中的签名对比逻辑
1.4 zipalign
zipalign -v 4 input.apk output.apk
zipalign -c -v 4 app.apk
apktool b output_dir -o unaligned.apk zipalign -v 4 unaligned.apk final.apk apksigner sign --ks debug.keystore final.apk
|
注意顺序:必须先 zipalign 再 apksigner,否则对 V2/V3 签名块的对齐修改会导致签名失效。
二、DEX / 字节码分析
2.1 d8 / dx(DEX 编译器)
dx --dex --output=classes.dex input.jar
d8 --release --output out_dir input.jar
d8 --output out_dir --lib android.jar classes.dex
java -jar dx.jar --dex --multi-dex --output=merged *.dex
|
2.2 dexdump
dexdump -d classes.dex | head -50
dexdump classes.dex | grep -A 20 "Class descriptor"
dexdump classes.dex | grep -E "string|password|key|token|secret|url|http"
dexdump -d classes.dex | grep -A 30 "Method descriptor"
|
dexdump 输出解读:
Class #0 - Class descriptor : 'Lcom/example/MainActivity;' Access flags : 0x0001 (PUBLIC) Superclass : 'Landroid/app/Activity;' ... #0 : (in Lcom/example/MainActivity;) name : 'onCreate' type : '(Landroid/os/Bundle;)V' access : 0x0004 (PROTECTED) code - registers : 5 ins : 2 outs : 2 insns size : 42 16-bit code units
|
2.3 baksmali / smali
baksmali d classes.dex -o smali_output/
smali a smali_output/ -o new_classes.dex
baksmali d -a 30 classes.dex -o smali_output/
baksmali d --code-offsets classes.dex -o smali_output/
baksmali d classes.dex -o smali_output/ --classes com/example/MainActivity.smali
for dex in classes*.dex; do baksmali d $dex -o smali_${dex%.dex}/ done
|
smali/baksmali 是理解字节码注入、插桩、反混淆的核心工具。当你需要修改应用逻辑(如跳过付费验证),就是在 smali 层面操作。
Smali 指令类型速查:
const/4 v0, 0x1 const-string v0, "hello" move v0, v1
invoke-static {v0}, LClass;->method(I)V invoke-virtual {v0, v1}, LClass;->method(LString;)Z invoke-direct {p0}, LObject;-><init>()V
iget v0, p0, LClass;->field:I iput v0, p0, LClass;->field:I
if-eqz v0, :label if-nez v0, :label if-lt v0, v1, :label
return-void return v0 return-object v0
|
2.4 dex2jar + JD-GUI / JADX
DEX 到 Java 的转换链:
d2j-dex2jar.sh classes.dex -o classes.jar
jadx -d output_source target.apk jadx-gui target.apk
jadx --deobf --show-bad-code --escape-unicode target.apk -d output_source
|
2.5 deopt / dexopt 与 ART 编译器
理解 DEX 在 ART 中的优化过程:
adb shell ls /data/app/com.example-*/oat/arm64/
java -jar oat2dex.jar boot.oat java -jar oat2dex.jar app.odex
adb shell dex2oat --runtime-arg -Xms64m --dex-file=/data/app/test.apk \ --oat-file=/data/app/test.odex
|
三、SO / Native 层分析
3.1 objdump(binutils)
objdump -T libtarget.so
objdump -d libtarget.so | less
objdump -p libtarget.so | grep NEEDED
objdump -d -j .text libtarget.so
objdump -R libtarget.so
objdump -S libtarget.so | less
|
objdump 反汇编输出解读(ARM64 示例):
00000000000010a4 <Java_com_example_Utils_encrypt>: 10a4: d10083ff sub sp, sp, #0x20 10a8: a9017bfd stp x29, x30, [sp, #16] 10ac: 910043fd add x29, sp, #0x10 10b0: f9000fe0 str x0, [sp, #24] ; JNIEnv* → sp+24 10b4: f9000be1 str x1, [sp, #16] ; jclass/jobject → sp+16 10b8: 94000005 bl 10cc <aes_encrypt_internal>
|
3.2 readelf
readelf -h libtarget.so
readelf -S libtarget.so
readelf -l libtarget.so
readelf -s libtarget.so
readelf -d libtarget.so
readelf -r libtarget.so
readelf -n libtarget.so
readelf --notes libtarget.so | grep "Build ID"
|
关键段(Section)说明:
| 段名 |
用途 |
逆向关注点 |
.text |
可执行代码 |
核心逻辑、算法实现 |
.rodata |
只读数据 |
硬编码字符串、常量、Key/IV |
.data |
已初始化全局变量 |
加密状态标志、配置 |
.bss |
未初始化全局变量 |
运行时分配缓冲区 |
.plt |
过程链接表 |
外部函数调用入口(Hook 目标) |
.got |
全局偏移表 |
外部函数实际地址(GOT 劫持) |
.init_array |
初始化函数指针数组 |
JNI_OnLoad 之前的初始化 |
.fini_array |
析构函数指针数组 |
卸载时的清理逻辑 |
.dynamic |
动态链接信息 |
SO 依赖、重定位表位置 |
3.3 nm
nm -D libtarget.so
nm --dynamic libtarget.so
nm -u libtarget.so
nm -n libtarget.so
nm --print-size -D libtarget.so | sort -k2 -n
|
3.4 strings — 快速字符串扫描
strings libtarget.so
strings -n 8 libtarget.so
strings -e l libtarget.so strings -e b libtarget.so
strings target.apk | grep -iE "secret|password|key|token|http|api"
strings libtarget.so | grep -E "^[A-Za-z0-9+/]{20,}={0,2}$"
|
3.5 objcopy / strip / patchelf
file libtarget.so
objcopy --strip-debug libtarget.so libtarget_stripped.so
patchelf --add-needed libinject.so libtarget.so
patchelf --set-rpath /data/local/tmp libtarget.so
patchelf --set-soname libhooked.so libtarget.so
|
四、动态调试工具
4.1 adb 逆向专用命令
adb shell cat /proc/<pid>/maps
adb shell cat /proc/<pid>/status | grep -E "TracerPid|State|Name"
adb shell pm list packages -f
adb shell pm list packages -3
adb shell dumpsys package <package_name> | grep -A 5 "signatures"
adb shell pm path <package_name>
adb shell pm path <package_name> adb pull /data/app/com.example-xxxxx/base.apk .
adb shell pm clear <package_name>
adb shell am start -D -n com.example/.MainActivity
adb shell am start -D -n com.example/.MainActivity -e debug true
adb shell am force-stop <package_name>
adb shell dumpsys activity activities | grep mResumedActivity
adb logcat -s AndroidRuntime:E ActivityManager:W
adb logcat -v threadtime > full_log.txt
adb logcat --pid=$(adb shell pidof com.example)
adb shell getprop ro.build.version.sdk adb shell getprop ro.product.cpu.abi adb shell getprop ro.debuggable
|
4.2 strace
adb shell strace -p <pid>
adb shell strace -e open,read,write,close -p <pid>
adb shell strace -e connect,sendto,recvfrom -p <pid>
adb shell strace -e fork,clone,ptrace -p <pid>
adb shell strace -t -p <pid>
adb shell strace -T -p <pid>
adb shell strace -f -p <pid>
adb shell strace -o /sdcard/strace.log -p <pid> adb pull /sdcard/strace.log
|
strace 输出解读示例:
openat(AT_FDCWD, "/sdcard/secret.txt", O_RDWR|O_CREAT, 0600) = 23 write(23, "encrypted_data_here...", 1024) = 1024 close(23) = 0 connect(16, {sa_family=AF_INET, sin_port=htons(443), sin_addr=inet_addr("103.45.67.89")}, 16) = -1 EINPROGRESS
|
从以上输出可以判断:(1) 应用在操作 /sdcard/ 下的文件进行加密写入;(2) 连接到一个远程服务器的 443 端口。
4.3 frida(动态插桩)
adb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server &
frida-ps -U
frida-ps -Uai
frida -U -f com.example.app -l script.js --no-pause
frida -U -p 12345 -l script.js
frida -U com.example.app -l script.js
frida -H 192.168.1.100:27042 -f com.example.app
|
Frida 基础示例脚本:
Java.perform(function() { var System = Java.use("java.lang.System"); System.exit.implementation = function(code) { console.log("[!] System.exit(" + code + ") blocked!"); }; });
Interceptor.attach(Module.findExportByName("libtarget.so", "Java_com_example_Utils_encrypt"), { onEnter: function(args) { console.log("[+] encrypt() called"); console.log("[+] JNIEnv*: " + args[0]); console.log("[+] jstring: " + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()); }, onLeave: function(retval) { console.log("[+] encrypt() returned: " + Java.vm.getEnv().getStringUtfChars(retval, null).readCString()); } });
|
4.4 ltrace(库调用追踪)
adb push ltrace-arm64 /data/local/tmp/ adb shell chmod 755 /data/local/tmp/ltrace-arm64
adb shell /data/local/tmp/ltrace-arm64 -e libc.so -p <pid>
adb shell /data/local/tmp/ltrace-arm64 -e libtarget.so -p <pid>
|
五、网络抓包
5.1 tcpdump
adb shell tcpdump -i wlan0 -w /sdcard/capture.pcap
adb shell tcpdump -i wlan0 port 443 -w /sdcard/capture.pcap
adb shell tcpdump -i wlan0 host 192.168.1.100 -w /sdcard/capture.pcap
adb shell tcpdump -i wlan0 -s 0 -w /sdcard/capture.pcap
adb shell tcpdump -i wlan0 -A port 53
adb pull /sdcard/capture.pcap wireshark capture.pcap
|
5.2 Charles / mitmproxy
adb shell settings put global http_proxy <proxy_ip>:8888
adb shell settings put global http_proxy :0
openssl x509 -inform PEM -subject_hash_old -in charles.pem | head -1
cp charles.pem 3a3b4c5d.0
adb root adb remount adb push 3a3b4c5d.0 /system/etc/security/cacerts/ adb shell chmod 644 /system/etc/security/cacerts/3a3b4c5d.0 adb reboot
|
mitmproxy 透明代理模式:
mitmproxy --mode transparent --listen-port 8080
adb shell iptables -t nat -A OUTPUT -p tcp --dport 80 -j REDIRECT --to-port 8080 adb shell iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port 8080
adb shell iptables -t nat -F
|
Android 7.0+ HTTPS 抓包关键:系统默认不信任用户安装的 CA 证书。需要将 Charles/mitmproxy 证书安装为系统证书(root 后推送到 /system/etc/security/cacerts/),或使用 Xposed 模块(如 JustTrustMe)Hook 证书校验。
SSL Pinning 绕过技术:
| 技术 |
原理 |
工具 |
| 系统证书注入 |
将代理证书作为系统信任证书 |
adb push cert /system/etc/security/cacerts/ |
| Hook TrustManager |
拦截 SSL 上下文初始化 |
Frida: SSLContext.init() |
| Hook OkHttp |
绕过 OkHttp CertificatePinner |
Frida: CertificatePinner.check() |
| 修改 Network Config |
Patch Manifest 中网络安全配置 |
apktool 反编译后修改 XML |
| Virtual Xposed |
在虚拟环境中运行 JustTrustMe 模块 |
VirtualXposed + JustTrustMe |
六、反调试与绕过工具
6.1 反调试检测命令
adb shell cat /proc/<pid>/status | grep TracerPid
adb shell cat /proc/<pid>/stat
adb shell netstat -anp | grep -E "23946|27042"
adb shell cat /proc/<pid>/wchan
|
6.2 绕过工具
magiskhide add com.target.app
adb push hluda-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/hluda-server adb shell /data/local/tmp/hluda-server -l 0.0.0.0:27043 &
adb shell LD_PRELOAD=/data/local/tmp/bypass.so am start -n com.example/.MainActivity
|
七、命令速查表
| 场景 |
命令 |
| 反编译 APK |
apktool d app.apk |
| 查看 APK 信息 |
aapt dump badging app.apk |
| 查看 DEX 结构 |
dexdump -d classes.dex |
| DEX → smali |
baksmali d classes.dex |
| DEX → Java |
jadx -d out app.apk |
| 反汇编 SO |
objdump -d lib.so |
| 查看 SO 符号 |
nm -D lib.so |
| 提取 SO 字符串 |
strings -n 8 lib.so |
| 查看 SO 段信息 |
readelf -S lib.so |
| 查看 ELF 头 |
readelf -h lib.so |
| 查看进程内存 |
adb shell cat /proc/<pid>/maps |
| 查看调试状态 |
adb shell cat /proc/<pid>/status | grep TracerPid |
| 抓取网络包 |
adb shell tcpdump -i wlan0 -w /sdcard/cap.pcap |
| 签名验证 |
apksigner verify app.apk |
| 动态插桩 |
frida -U -p <pid> -l script.js |
| 追踪系统调用 |
adb shell strace -p <pid> |
| Patch SO 依赖 |
patchelf --add-needed lib.so target.so |
八、高级实战:完整逆向工作流
以下是一个完整的 Android 逆向分析流程,整合了上述所有命令:
#!/bin/bash
TARGET_APK="target.apk" OUTPUT_DIR="analysis_output" mkdir -p $OUTPUT_DIR
echo "[*] Stage 1: Static Reconnaissance" echo " [1.1] APK info..." aapt dump badging $TARGET_APK > $OUTPUT_DIR/apk_info.txt echo " [1.2] Extracting strings..." strings $TARGET_APK | grep -iE "secret|key|password|token|http|api|encrypt" > $OUTPUT_DIR/sensitive_strings.txt echo " [1.3] Checking signature..." apksigner verify -v --print-certs $TARGET_APK > $OUTPUT_DIR/signature.txt
echo "[*] Stage 2: Decompilation" apktool d -f $TARGET_APK -o $OUTPUT_DIR/apktool_out jadx --deobf --show-bad-code $TARGET_APK -d $OUTPUT_DIR/jadx_out
echo "[*] Stage 3: Security Audit" grep -r "exported=\"true\"" $OUTPUT_DIR/apktool_out/AndroidManifest.xml > $OUTPUT_DIR/exported_components.txt grep "uses-permission" $OUTPUT_DIR/apktool_out/AndroidManifest.xml | sort | uniq > $OUTPUT_DIR/permissions.txt
echo "[*] Stage 4: Native Analysis" for so in $(find $OUTPUT_DIR/apktool_out/lib -name "*.so"); do echo " Analyzing $(basename $so)..." readelf -h $so > $OUTPUT_DIR/native_$(basename $so)_elf_header.txt 2>/dev/null strings -n 8 $so > $OUTPUT_DIR/native_$(basename $so)_strings.txt 2>/dev/null nm -D $so > $OUTPUT_DIR/native_$(basename $so)_symbols.txt 2>/dev/null done
echo "[+] Analysis complete. Results in $OUTPUT_DIR/"
|
面试常考问题
Q1: apktool 反编译出的 smali 与 Java 源码的关系?
smali 是 DEX 字节码的文本表示,每个 .smali 文件对应一个 Java 类,每一行对应 Dalvik 虚拟机的寄存器操作指令。理解 smali 是做字节码修改和插桩的基本功。具体而言:(1) smali 基于寄存器架构,而 Java 字节码基于操作数栈,这是两者在实现层面的核心差异;(2) smali 中的 .locals N 声明了局部变量数量,非静态方法还会额外使用 p0 代表 this 引用;(3) JADX 等工具将 smali 反编译为 Java 后可能丢失精确的控制流信息(尤其在混淆代码中),而直接阅读 smali 可以得到准确的执行路径。
Q2: Android 7.0+ 抓 HTTPS 包为什么需要额外处理?
Android 7.0 引入 Network Security Config,默认只信任系统预装的 CA 证书。用户安装的证书(如 Charles 根证书)对 targetSdkVersion >= 24 的应用无效。根本原因在于 libcore/security/TrustedCertificateStore.java 中区分了 system 和 user 两个证书存储,当应用 Network Security Config 未显式信任用户证书时,SSL 握手阶段只从 system 信任库中查找证书链。解决方法:root 后安装为系统证书、Hook 证书校验、或修改应用 network_security_config.xml。
Q3: apksigner V2 签名比 V1 好在哪?
V1 逐个对 JAR 条目签名,验证慢且 APK 解压后签名信息丢失,仅保护 META-INF/ 目录下的 MANIFEST.MF 中列出的文件;V2 对整个 APK 文件(APK Signing Block 之前的部分)签名,安装时一次性验证整个文件完整性,效率更高,且能检测整个文件是否被篡改,包括 ZIP 元数据。V3 进一步支持密钥轮换,允许应用在不丢失旧签名信任的前提下更新签名密钥。
Q4: readelf -S 和 readelf -l 的区别?为什么两者都需要查看?
readelf -S 显示 Section Header Table(节头表),反映编译链接时的布局视图(.text、.data、.rodata 等),主要用于静态分析和符号定位。readelf -l 显示 Program Header Table(程序头表),反映运行时加载视图(LOAD 段、DYNAMIC 段等),决定 SO 在内存中的实际映射。在逆向分析中,两者结合能精确判断:某段代码在内存中的实际地址(通过 LOAD 段的 p_vaddr 和 p_offset 计算)、哪些节会被加载到内存(通过段到节的映射关系)、以及哪些数据在运行时可能被修改(如 .got 所在的段标记为可读写)。
Q5: Frida 注入失败(”Failed to spawn: unable to connect to remote frida-server”)怎么办?
排查步骤:(1) 确认 frida-server 版本与本地 frida-tools 版本匹配——两者必须完全一致(通过 frida --version 核对);(2) 确认 frida-server 在设备上正常运行——adb shell ps | grep frida 检查进程是否存在;(3) 检查端口转发——adb forward tcp:27042 tcp:27042 是否正确建立;(4) 对于 Android 12+,app 的 zygote 进程限制可能导致 spawn 失败,尝试用 attach 模式代替(frida -U com.app -l script.js);(5) 确认设备架构与 frida-server 架构匹配——用 adb shell getprop ro.product.cpu.abi 查看设备 CPU 架构,下载对应的 frida-server 二进制文件(arm、arm64、x86、x86_64)。