一、概述
APK 是移动取证里第一份也是最容易被跳过的证据。
很多分析者拿到手机就直接翻 /data/data/,略过了两个更基础的问题:这个应用到底是什么?(包名、版本、签名证书、下载来源)它在设备上能做什么?(申请了哪些权限、哪些组件被导出、是否允许明文通信)
两个问题的答案都在 AndroidManifest.xml 里。而且不需要解密、不需要 root、不需要任何反编译工具 —— aapt dump badging 一条命令就能读出最关键的一批字段。
跳过这一步会发生什么:后面所有分析都建立在「这是一个赌博类 App」「这是一次正常的社交应用使用」这类未经核实的判断上。
包名对不上、版本比设备上装的旧、签名与官方不一致 —— 这三个事实任意一个成立,都可能改变定性方向,而它们只在 APK 里写着。
同样常见的失败是成本误判:判断不出加固与否,就没法决定接下来是静态读代码还是必须上动态。两条路的工作量差着数量级。
它在取证流程里的位置:同时服务采集与分析两环,且在分析环的最前面。
采集环的用法是:APK 从设备侧取出后先做基本属性提取。这条路对未 root 设备同样成立(pm path 无需权限),因此它常常是唯一能在无 root 情况下取得的能力画像来源。
分析环的用法是:根据 Manifest 声明反推该看私有目录里的哪些文件(见 应用私有数据),以及根据加固判定决定是否进入 Frida 动态 Hook。
上游依赖 APK 的获取与固定(见 第 1 步)。下游支撑三件事:身份认定(签名是密钥层面的唯一权威来源)、能力画像(权限组合与导出组件)、证据解释(为什么这个库打不开)。
涉及的证据形态(具体到文件名与结构):
| 形态 |
具体位置 |
结构特征 |
| 包声明 |
AndroidManifest.xml |
二进制 AXML(字符串池 + 资源 ID),不能直接文本打开 |
| 业务代码 |
classes.dex / classes2.dex… |
Dalvik 字节码,多 dex 是现代应用的常态 |
| 签名 |
META-INF/MANIFEST.MF、*.SF、*.RSA |
v1 方案;v2/v3/v4 签名不在 META-INF 里,须用 apksigner 验证 |
| native 库 |
lib/{arm64-v8a,armeabi-v7a,x86}/ |
加固识别的主要依据,库名与体积是信号 |
| 原始资源 |
assets/、resources.arsc |
硬编码 API 地址、密钥、渠道号的常驻位置 |
| 安装台账 |
/data/system/packages.xml 的包条目 |
与 APK 的 package 字段交叉验证对应关系 |
| 证书身份 |
签名证书的 SHA-256 指纹 |
同签名多包名 = 同一开发者或同一批打包工具 |
这是哪个应用、什么版本、由谁签名、从哪装的、申请了哪些权限、哪些组件可被外部调用、是否加固。回答这些不需要解密任何数据。
应用运行时拿到了什么数据(权限声明 ≠ 实际调用);数据库里存了什么(加固与密钥在 lib/ 里);签名一致就代表同一开发者(证书可能是共享的);allowBackup=true 在 Android 12 之后意味着能用 adb backup(该路径已废弃,覆盖面不可控,见 Android 数据存储总览)。
本文不做的事:不在检材上执行 APK、不做脱壳、不构造任何针对商业应用保护机制的绕过手段。
加固识别的结论到此为止:判定「有壳」,然后说明下一步需要什么条件。
反取证视角下同样重要 —— 本篇的字段集也是识别「APK 被重打包替换」的判据清单,见 反取证手法与时间戳篡改检测。
读者前提:ZIP 结构、XML 基础,以及对 Android 组件模型(Activity / Service / BroadcastReceiver / ContentProvider)有基本认识。命令行工具只需要 aapt(或 aapt2)与 apktool。
开始之前有一件事必须先确认:APK 是从检材上原样取下来的吗?
按 第 1 步 固定哈希之后再看后面的内容。本篇所有结论都建立在「这份 APK 就是设备上那个」这个前提上,前提不成立时,权限画像与加固判定都会指向错误的对象。
二、核心原理
2.1 APK 的内部结构
APK 本质是一个 ZIP 包,但内部有一份编译后的二进制 XML(AXML):
app.apk (ZIP)
├── AndroidManifest.xml ← ★ 编译后的二进制 XML,需 aapt/aapt2 解析
├── classes.dex ← ★ Dalvik 字节码(主代码);classes2.dex… 为多 dex
├── lib/{arm64-v8a,armeabi-v7a,x86_64}/ ← native 库,按 ABI 分目录
├── assets/ ← 原始资源(不编译),可能含配置、密钥、字体
├── res/ ← 编译后的资源(layout、drawable、values)
├── resources.arsc ← 资源表(字符串、样式、ID 映射)
└── META-INF/ ← ★ 签名:MANIFEST.MF(全文件 SHA-1 清单)、*.SF、*.RSA/*.DSA/*.EC
| 组成 |
取证价值 |
关键点 |
AndroidManifest.xml |
★★★★★ |
所有权限与组件的源头 |
META-INF/ |
★★★★★ |
签名证书是密钥层面身份的唯一权威来源 |
classes*.dex |
★★★★ |
业务逻辑,jadx 反编译 |
lib/*.so |
★★★ |
加固识别的主要依据 |
assets/ |
★★★ |
常含硬编码配置、密钥、渠道号 |
resources.arsc / res/ |
★★ |
硬编码字符串常在前者 |
一个实用的取证结论:resources.arsc 与 assets/ 中的硬编码字符串(API 地址、密钥、渠道号)经常是免费的高价值线索——strings 扫一遍常常几秒就能找到明文密钥或后台地址。
2.2 Manifest 的关键字段
AndroidManifest.xml 是 XML,但存的是二进制 AXML 格式(字符串池 + 资源 ID),不能直接用文本编辑器打开。
| 字段 |
取证意义 |
高价值判定 |
package |
应用唯一标识 |
与 packages.xml 交叉验证包的对应关系 |
versionCode / versionName |
版本 |
版本差异可能对应不同功能或漏洞修复 |
android:debuggable |
是否调试包 |
true = 测试包,正式发布应用应为 false |
android:allowBackup |
是否允许备份 |
true = 数据可被 adb backup 提取(Android 12 前) |
android:usesCleartextTraffic |
允许明文 HTTP |
true = 存在流量被窃听风险 |
android:networkSecurityConfig |
自定义网络安全配置 |
指向 XML 资源文件,指定是否信任用户 CA |
android:usesPermission / exported |
申请的权限 / 组件可被外部调用 |
见下方风险分级;exported=true 的组件可能被恶意 App 调用 |
android:sharedUserId |
共享 UID |
多个应用共用 UID = 可互相读数据 |
requestLegacyExternalStorage / appComponentFactory |
绕过分区存储 / 组件工厂 |
前者 Android 10 常用,后者自定义即暗示加固或热修复框架 |
这可以支持「该应用由非官方渠道/非正规流程打包」的判断,是「这批 APK 是同一个人生成的一批工具」的强线索。但仍是线索,不是单独定案依据。
allowBackup=true 的取证意义:Android 12 之前 adb backup 可据此提取应用数据。虽已废弃,但它是判断「该应用数据可被其他手段绕过提取」的指标。
很多金融类应用会显式设为 false。反过来说,true 意味着开发者未做这层保护。
2.3 权限风险分级
| 等级 |
权限 |
取证价值 |
| 极高 |
READ_SMS、RECEIVE_SMS、SEND_SMS、READ_CALL_LOG、WRITE_CALL_LOG |
短信与通话是核心证据来源,能读就能伪造 |
| 高 |
ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION、CAMERA、RECORD_AUDIO、READ_CONTACTS、READ_PHONE_STATE |
位置、影像、录音、通讯录 |
| 中 |
READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE、READ_MEDIA_IMAGES、READ_PHONE_NUMBERS、GET_ACCOUNTS、READ_CALENDAR |
媒体文档与身份关联 |
| 低 |
INTERNET、ACCESS_NETWORK_STATE、ACCESS_WIFI_STATE、VIBRATE、WAKE_LOCK |
网络与硬件,几乎所有应用都有 |
实战的读法:不是「权限越多越可疑」,而是看权限组合。
例如一个短视频应用同时持有 CAMERA + RECORD_AUDIO + READ_CONTACTS + ACCESS_FINE_LOCATION,且全部在 targetSdkVersion 允许的范围内被授予 —— 这本身就是一个值得追问的事实。
⚠️ targetSdkVersion 决定了权限的实际约束强度。 Android 运行时权限模型(用户可手动关闭)对 targetSdkVersion ≥ 23 的应用生效;老应用则安装即全给。取证时需结合 dumpsys package 的实际授予状态,而不是只看 Manifest 声明。
2.4 加固识别
加固的核心思路是把真正的 Application 类替换掉,在启动最早阶段解密 DEX 内存,因此 jadx 看到的只是壳代码。
识别通常看六类信号:
- so 库名:
libjiagu*.so、libshell*.so、libDexHelper.so、libprotect*.so
Application 类名:指向 StubApplication、ShellApplication 等第三方壳包,且与包名无关
- DEX 数量:只有一个很小的
classes.dex,真实代码在 assets 的加密文件里
- assets 加密文件:
assets/jiagu_*.dat、assets/*.bin,体积与业务量成正比
- 自定义
appComponentFactory 或非常规 multidex 实现
- 冷启动明显变慢(加密解密开销)
jadx 结果中主类几乎无业务方法、只有大量 native 声明,是综合佐证。
前四项属强信号,单独出现即应列为「疑似加固」,不能仅凭启动耗时下结论。
unzip -l app.apk | grep '\.so$' # 41 1234567 lib/arm64-v8a/libjiagu_v1.so ← 壳
# # 88 234567 lib/arm64-v8a/libappcore.so ← 业务
unzip -p app.apk assets/ 2>/dev/null | head
取证意义:加固不是「无法取证」,而是改变了成本结构。
静态只能确认「这是哪个应用、声明了什么权限」。要拿到运行时数据(尤其是数据库密钥),就必须走 04 Frida 的动态路径。
报告里应写明这一点,让委托方知道结论的边界。
三、操作步骤
第 1 步:获取并固定 APK
PKG=com.digiforensics.sample
adb shell pm path $PKG # package:/data/app/~~Hk2==/com.digiforensics.sample-1a2b3c==/base.apk
adb pull /data/app/~~Hk2==/com.digiforensics.sample-1a2b3c==/base.apk ./base.apk
sha256sum base.apk | tee base.apk.sha256 ; unzip -t base.apk | tail -2 # 期望 No errors detected
# 整个 /data/app/<目录> 取走(split apk 才可能有多个 dex)
APPDIR=$(basename $(dirname $(adb shell pm path $PKG | sed 's/package://' | tr -d '\r')))
adb shell su -c "tar -cf - -C /data/app $APPDIR" > appdir.tar && tar -xvf appdir.tar
第 2 步:读基本属性(aapt)
# 环境:Android SDK build-tools(Ubuntu: sudo apt install aapt,或从 Android Studio 的 build-tools 取 aapt2)
aapt dump badging base.apk
# package: name='com.digiforensics.sample' versionCode='148' versionName='12.3.1'
# sdkVersion:'26' ← minSdkVersion
# targetSdkVersion:'33' ← targetSdkVersion
# application-label:'样本应用'
# uses-permission: name='android.permission.READ_SMS' / ACCESS_FINE_LOCATION / CAMERA / RECORD_AUDIO
# application-debuggable ← 出现即 debuggable=true
# launchable-activity: name='com.digiforensics.sample.MainActivity'
# 单独看权限(更干净)/ 完整 XML 树(所有组件与属性)
aapt dump permissions base.apk ; aapt dump xmltree base.apk AndroidManifest.xml
# E: manifest (line=2)
# A: android:versionCode(0x0101021b)=(type 0x10)0x94 ← 148
# A: android:debuggable(0x0101000f)=(type 0x12)0xffffffff ← true 的特殊编码
# E: application ... E: activity (line=42) A: android:name(0x01010003)="...MainActivity"
# A: android:exported(0x01010010)=(type 0x12)0xffffffff
读 AXML 输出的技巧:android:debuggable 与 exported 为 true 时,aapt 显示为 0xffffffff 而不是 0x1,因为它把类型编码在高 8 位。看到 0xffffffff 就要想到「布尔真」,这是初学者常困惑的地方。
apktool d -f -o app-decoded base.apk ; apktool b app-decoded -o app-rebuilt.apk
# 网络安全配置是最高频的取证命中点
cat app-decoded/res/xml/network_security_config.xml
# <base-config cleartextTrafficPermitted="true"> <trust-anchors>
# <certificates src="system"/> <certificates src="user"/> ← ★ 信任用户 CA,可被中间人代理
# </trust-anchors> </base-config>
apktool 的取证用途:把二进制 AXML 转成可读文本(能精确 grep);读上述 network_security_config.xml 判断是否信任用户 CA;读 res/values/strings.xml 与 res/values-zh-rCN/ 核对多语言资源;用 Smali 精确到字节码行确认调用点;资源哈希可用于判断是否为重打包。
第 4 步:jadx 反编译 Java
# 环境:sudo apt install jadx(或用 jadx-gui)
jadx -d app-jadx base.apk --no-res ; ls app-jadx/sources/com/digiforensics/sample/
grep -rn "SQLiteDatabase\|openOrCreateDatabase" app-jadx/sources/ | head # 库名与是否加密
grep -rn "SecretKeySpec\|Cipher.getInstance" app-jadx/sources/ | head # 密钥从哪来
grep -rn "getDeviceId\|ANDROID_ID\|Build.SERIAL" app-jadx/sources/ | head # 设备信息是否派生 key
三类必查的调用(它们决定数据能不能拿到):
openOrCreateDatabase / SQLiteOpenHelper 决定库文件名与是否加密。
Cipher.getInstance / SecretKeySpec 决定密钥在哪算出来。
getDeviceId / ANDROID_ID / Build.SERIAL 说明设备标识参与运算,这类 key 常由设备信息派生。
关键认识:如果 jadx 里搜不到 Cipher、只看到一个自定义 Application 类和一个壳 so,那基本可以确认加固。此时不要在静态上继续投入时间,直接转 04。
第 5 步:签名验证与身份认定
# 环境:Android SDK build-tools 的 apksigner
apksigner verify --verbose --print-certs base.apk
# Verified using v1 scheme (JAR signing): true ← 支持旧签名
# Verified using v2 scheme (APK Signature Scheme v2): true
# Verified using v3 scheme (APK Signature Scheme v3): true Number of signers: 1
# Signer #1 certificate DN: CN=Sample Dev, O=DigiForensics, C=CN
# Signer #1 certificate SHA-256 digest: 3f9a...(密钥层面身份的权威标识)
# 提取证书(keytool 或 openssl)
unzip -o base.apk META-INF/*.RSA ; keytool -printcert -file META-INF/CERT.RSA
openssl pkcs7 -inform DER -in META-INF/CERT.RSA -print_certs -noout
签名的取证价值极高:签名证书的 SHA-256 是唯一能证明「两个 APK 使用同一签名证书与同一私钥」的指标。
但它证明的是密钥层面的同一性,不能直接认定现实中的开发者身份或所用打包工具。证书可能由他人代管、可能复制自公开模板、也可能被冒用。
涉赌涉诈案件中,常见做法是一个签名下出现多个包名变体(正规壳 + 赌博包)。把同签名的所有 APK 归为一组,是常见切入点。
结论应表述为「同签名主体、同一次批量打包行为」,并结合 DN、有效期、包名与安装来源交叉印证。
v1/v2/v3 签名的取舍意义:只勾选 v1 的应用兼容性最好(可被 zip 篡改)但可被重签名;开启 v2/v3 后可防篡改。
若一份「官方」应用只用了 v1 签名,是一个值得注意的配置线索 —— 但它只是线索,单独不能证明侧载。 很多内部打包、老旧工具链或第三方应用市场分发的包也只签 v1。
第 6 步:加固识别汇总
# ① so 库 / ② assets 加密文件(体积大的 bin、dat)
unzip -l base.apk | grep -E '\.so$' | awk '{print $NF}'
unzip -l base.apk | awk '$1 > 100000 {print $1, $NF}' | sort -rn | head
# ③ Application 类 / ④ dex 数量与体积
aapt dump xmltree base.apk AndroidManifest.xml | grep -A3 'E: application' | head -8
unzip -l base.apk | grep 'classes.*\.dex'
# ⑤ strings 扫敏感内容(assets + arsc)
mkdir -p unz && cd unz && unzip -q ../base.apk
strings -n 6 resources.arsc assets/* 2>/dev/null | \
grep -iE 'http://|https://|api|key|secret|aes|password' | sort -u | head -40
加固判定的证据链写法(报告中应这样表述):
该 APK 经 apktool 解包后,assets 目录含 jiagu_config.dat(84,112 字节);lib/arm64-v8a/ 下含 libjiagu.so(312 KB);AndroidManifest.xml 中 android:name 指向 com.digiforensics.shell.StubApplication,而 classes.dex 仅 92 KB 且无业务类名。上述四项特征共同表明该应用已加固;静态分析无法获取其数据库密钥初始化逻辑,故转入动态分析。
第 7 步:验证