一、概述
应用私有目录是移动取证里数据密度最高、体积最小、解析难度最低的那一块。
物理镜像动辄上百 GB,一个应用私有目录可能只有几 MB。账号标识、登录状态、配置项、本地缓存、密钥痕迹,全挤在这几 MB 里。
它也是每个案件里最先被打开、也最先出结论的目录。
其中 shared_prefs/ 尤其特殊:明文 XML,不需要任何解密工具,文本编辑器直接读。用户 ID、服务器地址、上次登录时间、搜索历史、已保存的账号摘要都在这里。
少掉这一层,分析人员就得先去啃数据库的表结构。而数据库里能回答的问题,往往比 prefs 少得多。
没有这套读法会发生什么:结论会从「应用记录了什么」退化成「应用目录里有若干文件」。
同一个应用、同一个版本,目录里躺着七八个子目录。挑哪个、先看什么、哪个可以丢,直接决定报告有没有实际内容。
更隐蔽的失败是看起来有结论,其实是错的。只读 app.db 而漏了 -wal,数据库会安静地退回上一次 checkpoint,最近几天的记录凭空消失,不报任何错(见 陷阱 1)。
它在流程里的位置是分析环节的最前段。输入是 Android 数据存储总览 已经提取出来的应用私有目录,输出是可供时间线与关联分析的明文配置和数据库路径。
上游依赖提取工艺与完整性校验(流式导出、哈希固定、tr -d '\r' 这类批量细节见 陷阱 9)。下游支撑三条线:
涉及的证据形态(具体到文件名与结构):
| 形态 |
具体位置 |
结构特征 |
| 偏好配置 |
shared_prefs/<name>.xml |
根标签 <map>,键在 name 属性上,值类型由元素标签决定 |
| 旧版偏好 |
shared_prefs/<name>.xml.bak |
上一个完整版本,可能比主文件更新鲜 |
| 加密偏好 |
shared_prefs/encrypted_shared_prefs/ |
androidx.security 写入的密文,无 XML 声明 |
| 业务库 |
databases/app.db + -wal + -shm |
三件套必须同在,漏 WAL 丢最近写入 |
| 任意文件 |
files/、cache/、code_cache/ |
cache/ 可重建但含"访问过"的独立旁证 |
| 密钥材料 |
no_backup/*.key |
高熵二进制、无文件头,取证只固定哈希 |
| 加固 so |
lib/<abi>/*.so 与 APK 内同名目录 |
库名含 jiagu/packer 等即加固信号 |
| 外部目录 |
/sdcard/Android/data/ 与 /sdcard/Android/media/ |
前者卸载即删,后者卸载保留,是卸载取证唯一入口 |
这个应用登录过哪些账号、上次活跃在什么时候、连过哪些服务器地址、搜索过什么关键词、是否被加固、卸载后还留下什么。
聊天内容与文件传输记录在加密库里能不能读出来(取决于加固与密钥材料,见 2.3);应用服务端存了什么;cache/ 里的内容是不是用户真实看过——它只证明访问,不证明使用。
一句话:本篇给的是「从文件里能读出什么」,不是「用户做了什么」。
本文不做的事:不做应用运行时分析,不提取任何凭据的明文用途,不尝试在线使用检材内的会话令牌。遇到加密库时只做三件事:识别、固定哈希、标注解密条件。
读者前提:Linux 目录结构、XML 基础、ADB 基本操作,以及一个可执行的 Python 环境(本篇的解析脚本只用标准库)。
读 二、核心原理 之前,先确认设备侧已经完成目录级提取并做过哈希。本篇的分析动作全部建立在「目录完整」这个前提上,前提不成立时本章所有读法都不适用。
二、核心原理
2.1 应用私有目录的七个子目录
/data/data/com.digiforensics.sample/ (等价于 /data/user/0/com.digiforensics.sample/)
├── shared_prefs/ ← ★ XML 键值配置,明文,首要目标
├── databases/ ← ★ SQLite 数据库 + -wal / -shm
├── files/ ← 任意文件(图片、文档、下载、自定义格式)
├── cache/ ← 临时缓存,可安全丢弃但对判断"访问过"有价值
├── no_backup/ ← 应用声明的"不参与备份"数据
├── lib/ ← ★ 应用自带 so 库(可能是加固壳)
└── code_cache/ ← 优化后的 DEX(Android 11+)
优先级:shared_prefs/ 与 databases/ 并列第一(明文配置与业务数据密度最高);files/ 与 lib/ 次之(导出文件与加固识别,见 03);no_backup/ 第三(部分加密密钥材料、临时 DB 在此);cache/ 与 code_cache/ 最后(前者能证明"曾访问过某功能",后者含混淆后的类结构)。
2.2 SharedPreferences 的文件格式
每个 shared_prefs/<prefs_name>.xml 对应一个偏好设置文件(对应代码里的 getSharedPreferences(name, mode))。格式是标准 Android XML:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="user_id">8827341</string>
<boolean name="is_logged_in" value="true" />
<int name="login_count" value="17" />
<long name="last_login_time" value="1717213492000" />
<float name="upload_quality" value="0.75" />
<string name="last_search">优盤清倉</string>
</map>
解析要点(这几条决定提取准不准):根标签是** <map>**,不是 <preferences>,也不是 PreferenceScreen。键在 name 属性上,不在元素名上,所有键值都是同一层级的子元素。值类型由元素标签决定:string 是文本,int / long 是数值,float 是浮点,boolean 写成 value="true"。
long 时间戳几乎都是毫秒级,而且必须 64 位解析。1717213492000 用 32 位会溢出成负数。
还有三条容易翻车的:
- ①
.bak 文件:写入中断会留下 <name>.xml.bak,它是上一个完整版本,内容可能比当前文件更新鲜
- ②
encrypted_shared_prefs/:Android 7+ 的 androidx.security 库使用,内容是密文,不能按普通 XML 解析
- ③ 重复键:同名键可能重复出现(写入异常,或从
.bak 恢复所致),按出现顺序全部读取并记录次数,不要只取第一个
.bak 的实战价值经常被忽略。 Android 写 SharedPreferences 是「写临时文件 → 改名覆盖」,异常中断(应用被杀、空间不足)会留下 .bak。用户已经「退出登录」(当前文件 is_logged_in=false),.bak 里还留着 true 和旧账号。两个文件的时间戳对比本身就是一条时间线证据。
# 提取与初筛(shared_prefs 全是明文 XML,可直接 grep)
adb shell su -c "tar -cf - -C /data/data/com.digiforensics.sample shared_prefs" > prefs.tar
tar -xvf prefs.tar
# 全文搜索敏感关键词(一次性扫出所有应用配置)
grep -ril -E 'password|passwd|token|secret|api_key|session|cookie|auth' extracted/ \
| tee suspicious-prefs.txt
2.3 lib/ 目录:加固识别的第一现场
/data/data/com.digiforensics.sample/lib/arm64-v8a/
├── libappcore.so
├── libnative-lib.so
└── libjiagu.so ← 名称含"jiagu/加固/bang"等即强加固信号
/data/app/ 下安装的 APK 解包后也有 lib/<abi>/,两处应合并检查。
这套结构要回答的问题是:这个应用运行时到底加载了哪些 native 代码。
商业 App 里,加固库的存在意味着静态分析只能看到壳,解密 key 通常在 so 运行时初始化。这直接决定 05 即时通讯取证 中「能否拿到数据库密钥」这条路径的成本。见 03 的加固识别与 04 的动态分析。
PKG=com.digiforensics.sample
adb shell su -c "find /data/data/$PKG/lib /data/app/*/$PKG*/lib -name '*.so' 2>/dev/null" \
| tee so-list.txt
file extracted/lib/arm64-v8a/*.so # 确认架构与是否被剥符号
2.4 外部存储与分区存储的变化
| Android 版本 |
存储模型 |
取证影响 |
| ≤ 9(Pie) |
传统全权限,任一应用读写整个 /sdcard |
第三方应用数据散落在 /sdcard/<App名>/ |
| 10(Q) |
分区存储:应用只能访问自己的专属目录 + 媒体库 |
旧路径大量失效 |
| 11–12 |
分区存储 + 权限收紧 |
更多数据留在应用私有目录 |
| 13–15 |
作用域化更严,READ_MEDIA_* 细分 |
内外部差异进一步扩大 |
Android 10+ 的实际目录形态(≤9 时代数据散落在 /sdcard/<App名>/,分区存储后这些旧路径大量失效):
| 路径 |
内容 |
备注 |
/sdcard/Android/data/<包名>/ |
应用专属外部目录 |
卸载应用时会被一并删除(取证价值高但易灭失) |
/sdcard/Android/media/<包名>/ |
Android 11+ 的媒体专属目录 |
卸载后保留,是卸载应用的取证入口 |
/sdcard/Download/ |
下载目录 |
安装包、转账凭证导出常在此 |
/sdcard/DCIM/、Pictures/、Movies/、Documents/、Android/obb/<包名>/ |
媒体库(MediaStore 索引源)与扩展资源包 |
Android 10+ 需通过 MediaStore 访问 |
两个高价值推论:① 已卸载应用的数据线索主要在 /sdcard/Android/media/<包名>/——Android/data 随卸载清除但 Android/media 保留(Android 11+ 明确如此),这是"卸载后取证"的唯一常规入口。② /sdcard/Download/ 是安装包与导出文件的高发地,赌博、诈骗类案件的 APK 残留、转账截图导出常在此(见 01 案例)。
adb shell su -c "ls -la /sdcard/" | tee sdcard-root.txt
adb shell su -c "du -sh /sdcard/Android/data/* /sdcard/Android/media/* 2>/dev/null" | sort -h | tail -20
adb pull /sdcard/Download/ extracted/Download/
三、操作步骤
第 1–2 步:定位并流式导出
PKG=com.digiforensics.sample
adb shell pm path $PKG # APK 位置(/data/app/...)
adb shell su -c "du -sh /data/data/$PKG" # 体积,决定提取策略
adb shell su -c "tar -cf - -C /data/data $PKG" > app.tar
sha256sum app.tar | tee app.tar.sha256 # tar 包本身先哈希固定
tar -tf app.tar | head -60 && tar -xf app.tar # 先列后拆,确认包完整
find extracted -type f -exec sha256sum {} \; > hashes-app.txt
第 3 步:优先处理 shared_prefs/
ls -l extracted/shared_prefs/
find extracted/shared_prefs -name '*.bak' -ls # 别漏 .bak
python3 parse_prefs.py extracted/shared_prefs prefs-report.csv
column -s, -t prefs-report.csv | head -40
可直接运行的 Python 脚本(批量解析并汇总 SharedPreferences):
#!/usr/bin/env python3
# 批量解析 shared_prefs/*.xml 汇总为 CSV。Python 3.8+,仅标准库
# 用法: python3 parse_prefs.py <shared_prefs目录> <输出CSV>
import csv, datetime, os, sys
import xml.etree.ElementTree as ET
SENSITIVE = ("password", "passwd", "pwd", "token", "secret", "key",
"session", "cookie", "auth", "sign", "credential", "salt")
def human_time(v):
"""毫秒/秒时间戳 → 可读时间;明显不是时间戳则原样返回。"""
try:
n = int(v)
except (TypeError, ValueError):
return v
if 1_000_000_000 < n < 4_000_000_000: # 秒级
n *= 1000
if 946_684_800_000 < n < 4_102_444_800_000: # 2000 ~ 2100
return datetime.datetime.utcfromtimestamp(
n / 1000).strftime("%Y-%m-%d %H:%M:%S")
return v
def parse_file(path):
"""返回 [(key, type, value, flag), ...];解析失败返回空表。"""
rows = []
try:
root = ET.parse(path).getroot()
except ET.ParseError as e:
print(f"[!] 解析失败 {path}: {e}", file=sys.stderr)
return rows
for el in root: # 根标签通常是 <map>
key = el.get("name", "")
value = el.get("value")
if value is None:
value = (el.text or "").strip()
flag = "SENSITIVE" if any(s in key.lower() for s in SENSITIVE) else ""
if el.tag == "long" and not flag:
value = human_time(value) # 疑似时间戳则转换
rows.append((key, el.tag, value, flag))
return rows
src, out = sys.argv[1], sys.argv[2]
total = sens = 0
with open(out, "w", newline="", encoding="utf-8") as fh:
w = csv.writer(fh)
w.writerow(["prefs_file", "key", "type", "value", "flag"])
for fn in sorted(os.listdir(src)):
if not fn.endswith((".xml", ".xml.bak")):
continue
for key, vtype, value, flag in parse_file(os.path.join(src, fn)):
w.writerow([fn, key, vtype, value, flag])
total += 1
sens += 1 if flag else 0
print(f"共 {total} 个键值,其中敏感键 {sens} 个 → {out}")
第 4 步:databases/ 正确提取(不要漏 WAL)
ls -l extracted/databases/ # app.db / app.db-wal / app.db-shm
adb shell su -c "tar -cf - -C /data/data/com.digiforensics.sample databases" > db.tar
tar -xvf db.tar
sqlite3 extracted/databases/app.db "PRAGMA integrity_check;"
sqlite3 extracted/databases/app.db ".tables"
-wal / -shm 漏掉的后果:数据库会"退回"到上次 checkpoint 的状态,最近写入的数据全部消失。这会让人误判"数据被删了",是移动取证里最高频的误判之一。详见 09-SQLite-WAL取证。
第 5–7 步:外部存储、跨应用搜索与验证
adb pull /sdcard/Android/data/com.digiforensics.sample/ extracted/ext-data/
adb pull /sdcard/Android/media/com.digiforensics.sample/ extracted/ext-media/ # 卸载后仍保留
adb pull /sdcard/Download/ extracted/Download/
find extracted -type f -exec sha256sum {} \; > hashes-ext.txt
# 跨应用:一次性抓出所有应用的 shared_prefs
mkdir -p all-prefs
for d in $(adb shell su -c "ls /data/data/" | tr -d '\r'); do # tr -d '\r' 必须加
adb shell su -c "tar -cf - -C /data/data/$d shared_prefs 2>/dev/null" \
| tar -xf - -C all-prefs 2>/dev/null
done
grep -ril -E 'password|token|secret|api_key|session' all-prefs/ | head -40
# 验证:设备端与本地哈希一致 + 库完整 + prefs 时间线
adb shell su -c "md5sum /data/data/com.digiforensics.sample/databases/app.db"
md5sum extracted/databases/app.db
sqlite3 extracted/databases/app.db "PRAGMA integrity_check;" # 期望 ok
find extracted/shared_prefs -type f -printf '%T@ %p\
' | sort -n
四、常见陷阱
应用私有目录是「用文本编辑器就能读」的部分。
正因为门槛低,这一章的陷阱几乎全部来自偷懒式的省事动作:只拉一个文件、只读主文件、凭记忆拼路径、用 32 位整数存时间戳。
每一个都会安静地产出一份看似合理的错误