关键词:Android 分区、userdata、FBE、SELinux、ADB、多用户
难度:入门
前置知识:磁盘分区与 GPT 结构、Linux 目录结构、哈希与只读挂载规范
相关文章:应用私有数据与外部存储、APK 逆向与 Manifest 分析
一、概述
Android 取证最难的不是"读不出来"。
是读出来的那一份数据,到底代表哪个时刻的设备状态。
手机几乎从不关机。userdata 是全盘加密的,没有密钥就是一整片随机字节。/data/data/ 上还压着 SELinux,不是 root 就进不去。更麻烦的是,一台设备上可能同时跑着机主、工作资料、访客三套彼此隔离的数据。
真正把人带偏的失败模式,是读到了一部分,然后以为读全了。
设备锁着的时候,adb shell ls /data/data/ 不报错,只是给回一份很短的包名列表。这份输出和"设备上只装过这几个应用"在终端上根本分不出来。案件于是沿着"该设备使用痕迹很少"走下去,真正的证据一次都没碰到。
所以本篇要解决的就一件事:动手之前先把结论边界划出来。
它在流程里的位置是采集环节的前置判定层。不产出证据材料,只决定后面能不能产出证据。
上游是镜像到底是块设备还是逻辑提取、ro.crypto.* 属性读不读得到;下游是应用数据提取(第 4 步)、即时通讯数据定位(见 即时通讯取证)和结论表述(见 4.4 结论表述)。
跳过这一层直接提取,后面每一章的结论都失去边界。
涉及的证据形态(具体到路径与结构,不是"日志文件"这种大类):
| 形态 |
具体位置 |
结构特征 |
| 设备状态快照 |
adb devices -l、getprop、pm list users、getenforce |
几 KB 纯文本,是结论边界本身 |
| 包管理台账 |
/data/system/packages.xml |
单行长 XML,含 firstInstallTime / installerPackageName,含卸载保留条目 |
| 加密策略 |
ro.crypto.state / type / mountstate、/data/misc/vold/ |
block=FDE / file=FBE,决定 CE 与 DE 分层 |
| CE 数据层 |
/data/user/<userId>/<包名>/ |
凭据解锁后才挂载,历史路径 /data/data/ 是软链 |
| DE 数据层 |
/data/user_de/<userId>/ |
开机即可读,与锁屏凭据无关 |
| 分区表 |
/dev/block/by-name/、/proc/partitions、GPT |
注意 super 动态分区与 _a/_b 双槽 |
| 系统日志 |
adb bugreport 打包的 logcat + dumpsys |
压缩包,含全量服务转储 |
能回答什么:这台设备处于什么加密状态;能拿到哪些用户档案、哪些数据层;哪些应用装过、什么时候装的、从哪装的;本次提取的覆盖上限在哪里。
不能回答什么:具体应用里存了什么内容(那是 应用私有数据 与 SQLite 那几篇的事);设备被锁定时的 CE 内容,拿不到就记为受限,别写成"没有";设备上发生过什么行为,得先有时间线;服务器侧数据不在检材范围内。
一句话概括:本篇给的是"能做什么"的前置判断,不是"做了什么"的结论。
本文不做的事:不讨论 bootloader 解锁,也不提供 FRP 绕过。设备已经锁定时的手段也不写——不是没写,是这类手段在取证里就是攻击。
设备处理不了的时候,正确动作是三步:停止操作、记录状态、回到委托方沟通。
读者前提:磁盘分区与 GPT 结构、Linux 目录树与权限模型、哈希与只读挂载的规范(见 取证流程与原则)。文件级深度解析在 NTFS 结构与 MFT 解析 与 ext4 结构与 inode 里已经铺过。
getprop 都没跑过的话,先按 三、操作步骤 的第 1 步做一遍。别直接跳到提取。
另外有个技术前提值得先看一眼:委托方交来的手机如果是锁屏且未授权解锁,TWRP 恢复分区取证 讲的物理镜像路线才是对的,本篇的逻辑提取路线不适用。
二、核心原理
2.1 分区结构:哪些分区有取证价值
现代 Android 使用 GPT 分区表,/dev/block/by-name/ 下的软链接是标准访问方式。
| 分区 |
常见名 |
内容 |
取证价值 |
| 引导程序 |
bootloader |
厂商引导链(高通 sbl/abl/devcfg/tz/hyp) |
★★ 设备识别 |
| 内核/启动 |
boot、init_boot |
启动镜像。init_boot 为 Android 13 起新增,具体划分随版本与设备而异,以实际分区表为准 |
★★★ 伪造启动、反取证可能留痕 |
| 恢复 / 系统 / 厂商 |
recovery / system / vendor |
恢复镜像;AOSP 框架与系统应用(Android 10+ 只读,priv-app 可作分析对象);OEM HAL 与内核模块 |
★★ ~ ★★★ |
| 用户数据 |
** userdata** |
** /data 全部内容,本篇主战场** |
★★★★★ |
| 逻辑容器 / 启动状态 |
super / misc |
动态分区容器,容纳 system/vendor/product/odm;引导计数与恢复标记 |
★★ |
必须掌握的现代变化:Android 10 引入 A/B 无缝更新与 Virtual A/B,系统分区变成 super 内的逻辑分区,同一物理分区上同时存在 _a、_b 两个槽位。只读一侧会漏掉另一槽的旧系统版本与已删除系统应用数据。
adb shell ls -l /dev/block/by-name/ ; adb shell cat /proc/partitions # 设备侧真实布局(优先)
fdisk -l DigiForensics.dd # 镜像侧先确认有无 super 动态分区
partx -s -o START,SECTORS,NAME DigiForensics.dd # util-linux 的 partx
2.2 /data 是取证主战场
/data
├── data/ ← 历史路径,Android 7+ 已是 /data/user/0/ 的兼容软链
│ └── <包名>/
├── user/ ← ★ 实际存放处
│ ├── 0/ ← 主用户
│ └── 10/ ← 工作资料 / 次要用户(userId 常见 10–15)
├── user_de/ ← ★ Device-Encrypted(DE)数据,锁屏前即可读
│ └── 0/
├── app/ ← 预装应用 APK
├── media/ ← 内部共享存储(DCIM、Download 等,FUSE 呈现)
├── misc/
│ ├── keystore/ ← 应用密钥材料(受 TEE 保护)
│ └── vold/ ← FBE 加密策略
└── system/ ← 注意:这不是 system 分区,而是用户数据
├── packages.xml ← ★★ 全部已安装应用与版本、uid、安装时间戳
└── users/0/
两条最关键的结构事实:
(1)/data/data/<包名> 与 /data/user/<userId>/<包名> 是同一份数据。 Android 7.0 之后应用私有目录已经迁到 /data/user/<userId>/,/data/data 变成兼容软链。
两处都要看。不同工具、不同系统版本吐出来的路径不一样,只查一个容易漏。
adb shell su -c "ls -ld /data/data /data/user/0" # 典型输出:/data/data -> /data/user/0
(2)/data/user_de/0/ 是"解锁前可读"的那一半。 FBE 把凭据加密(CE)与设备加密(DE)分开,开机后、用户输入锁屏密码之前,系统只解密 DE 部分。
设备锁着的时候,user_de 下的东西照样读得到。这是一份常被忽略的免费证据源。
| 目录 |
全称 |
可读时机 |
典型内容 |
/data/user/0/ |
Credential-Encrypted |
必须已解锁 |
聊天记录、账号 token、私密文件 |
/data/user_de/0/ |
Device-Encrypted |
开机即可读 |
闹钟、后台数据、部分缓存 |
/data/system/packages.xml 是性价比最高的单个文件:一次回答"这台设备装过什么、什么被卸载过"。
2.3 加密状态判断:三个属性的组合
这是第一个要做的判断,而且有确定依据:StorageManager.isEncrypted() / isFileEncryptedNativeOnly() / isBlockEncrypted() 读的就是下面这三个属性。
ro.crypto.state 取 encrypted / unencrypted;ro.crypto.type 取 block(FDE)/ file(FBE)/ aes(早期块加密遗留)/ legacy;ro.crypto.mountstate 取 mounted / unmounted,指示 DE 部分是否已挂载。
FBE 和 FDE 的取证差异:
| 维度 |
FDE(type=block) |
FBE(type=file) |
| 加密粒度 |
整个 userdata 扇区 |
每个文件 + 文件名(文件名用 AES-CBC-CTS 加密) |
| 锁屏后 |
/data 整体不可读 |
/data/user/0 不可读,/data/user_de/0 可读 |
| 物理镜像价值 |
无密钥基本无用 |
密钥未解时连文件名都看不到 |
| 典型版本 |
Android 5.0–9 |
Android 7.0 起(Android 10 起强制 FBE) |
| 密钥保护 |
用户凭据经 Keymaster 保护 |
硬件密钥派生 512 位文件密钥,用户凭据只保护硬件密钥 |
# ① 加密三件套(最重要的一条判断)+ ② 一次性固定所有加密相关属性 + ③ fscrypt 挂载点
adb shell getprop ro.crypto.state ; adb shell getprop ro.crypto.type ; adb shell getprop ro.crypto.mountstate
adb shell getprop | grep -iE 'crypto|encrypt|keystore|vold|selinux' | tee Android-crypto-props.txt
sha256sum Android-crypto-props.txt ; adb shell mount | grep -iE 'fscrypt|/data'
重要的现实:FBE 的文件密钥在 TEE(可信执行环境)内由硬件密钥派生,即使有 root 也拿不到明文文件密钥。root 能读到的是"已解锁状态下被密钥解密的明文数据",以及 /data/misc/vold 下的加密策略结构。"有了 root 就等于能解 FBE"是错误认知——root 的价值在于跨过 SELinux 权限墙,不是破开 TEE。
2.4 SELinux 与 root:两道墙不是一回事
DAC(传统权限位)下 shell 不属于任何应用 uid,可 su 提权到 root;SELinux 是强制访问控制标签,root 也会被策略拒绝,需 permissive 或策略绕过。
adb shell getenforce # Enforcing(正常)/ Permissive(只记录不拦)
adb shell su -c id # uid=0(root) → 已 root
adb shell su -c "id -Z" # u:r:magisk:s0 ← 验证 su 后的域
取证价值常被低估的一点:SuperSU / Magisk 这类 root 工具会在设备上留下自己的数据库与日志(哪个应用何时被授予 root、授权时是否录屏/录 touch)。在网络诈骗、赌博、刷单类案件中,"root 授权记录"本身就是"该设备被用于自动化操作"的直接证据。
2.5 多用户与工作资料
adb shell pm list users
# UserInfo{0:机主:13} running
# UserInfo{10:工作资料:30} ← 工作资料常见 userId=10
userId 0 是机主(主体数据,但不一定是嫌疑人使用的数据);10–14 通常是工作资料,业务 App 数据与个人账号互不可见,必须单独提取;其余为访客、访客分身,常用于"临时"使用,是反侦查行为的重要指标。
工作资料为什么重要:企业微信、钉钉在 work profile 下有独立沙盒,聊天记录与个人账号互不可见。只取 /data/user/0 会系统性漏掉全部工作场景证据。
iOS 侧没有这套分区与多用户机制,取证走的是 iTunes 备份与沙盒路径两条路,Manifest.db 怎么索引、备份怎么解密见第 06 篇。
三、操作步骤
第 1 步:建立连接并固定设备状态
adb devices -l | tee adb-devices.txt # 授权状态本身是检材信息
{
echo "=== date ==="; adb shell date; echo "=== getprop ==="; adb shell getprop
echo "=== id ==="; adb shell id; echo "=== getenforce ==="; adb shell getenforce
echo "=== users ==="; adb shell pm list users; echo "=== packages ==="; adb shell pm list packages -f -U
} > device-state.txt 2>&1
sha256sum device-state.txt
第 2 步:判断加密与访问级别
adb shell getprop ro.crypto.state ; adb shell getprop ro.crypto.type # encrypted/unencrypted;block(FDE)/file(FBE)
adb shell getprop ro.crypto.mountstate ; adb shell mount | grep '/data/user/0' # mounted → CE 已挂载
adb shell id ; adb shell su -c id ; adb shell su -c "ls /data/data/" # 真能列出目录才算穿透成功
第 3 步:解锁与提权(仅在已授权检材上执行)
PKG=com.digiforensics.sample
adb shell mount | grep '/data/user/0' # ① FBE 设备须先在设备端输入锁屏凭据完成解锁
adb shell su -c "ls -la /data/data/" # ② 已 root 设备
adb shell pm path $PKG # ③ 未 root 的替代路径:定位 APK,无需 root
adb shell dumpsys package $PKG | head -60 # 权限、组件、签名
adb shell "run-as $PKG" ls -la # 仅 debuggable=true 的应用
⚠️ adb shell su -c 会在检材上执行命令,写入系统日志与 shell 历史。 正式案件应:① 在检查笔录中记录操作依据;② 每次操作后重新哈希关键文件确认未被改写;③ 只读取不写入——不要 rm、不要改权限、不要 settings put。
第 4 步:提取应用私有数据
PKG=com.digiforensics.sample
adb shell su -c "du -sh /data/data/$PKG"
adb shell su -c "find /data/data/$PKG -maxdepth 2 -type d" | tee app-dirs.txt
# ① 整目录 tar 流式导出(首选;tar 包可哈希为单一证据容器)
adb shell su -c "tar -cf - -C /data/data $PKG" > extracted-$PKG.tar
tar -tf extracted-$PKG.tar | head -50 # 先列不拆,确认包完整
mkdir -p extracted/$PKG && tar -xf extracted-$PKG.tar -C extracted/$PKG
# ② 按目录 pull(tar 不可用时)
adb pull /data/data/$PKG/databases extracted/$PKG/
# ③ 立即哈希固定
find extracted/$PKG -type f -exec sha256sum {} \; > hashes-app.txt
adb pull 对权限与符号链接敏感。 目录中有 socket、特殊设备文件或 SELinux 拒绝路径时 pull 会失败,此时用 tar(方式 ①)更稳。
第 5 步:提取系统级工件与 adb backup 的现状
adb pull /data/system/packages.xml extracted/ # 最高性价比
adb pull /sdcard/Download/ extracted/
adb pull /sdcard/Android/media/ extracted/
adb bugreport device-bugreport.zip # 含 logcat + 全量 dumpsys
sha256sum device-bugreport.zip
adb backup 的现状:Android ≤ 11 尚可运行但需设备端人工确认,且大量应用 allowBackup="false" 直接拒绝;Android 12/13 各版本与厂商 ROM 行为不一致。能否使用不能靠记忆版本号,必须实测(看是否弹出确认、是否返回 Unable to backup)。覆盖面不可控,不应作为取证方案——现代主流路径是 root + 目录级提取;allowBackup 检查方法见 03。
第 6 步:验证
# ① 抽样比对设备侧与主机侧哈希(不一致 → 检查是否因 WAL 未同步或权限截断)
adb shell su -c "md5sum /data/data/com.digiforensics.sample/databases/app.db"
md5sum extracted/com.digiforensics.sample/databases/app.db
# ② 记录时间基准(设备时间用户可改 + 分析机时间 + 时差)
adb shell date; date
# ③ 确认关键文件可读
sqlite3 extracted/com.digiforensics.sample/databases/app.db ".tables"
# 报 "file is not a database" → 可能是加密库,见 [09](/wiki/02-sqlite-wal-09dfe6b3)
四、常见陷阱
Android 取证最贵的错误不是「没读到」,而是读到了一部分却以为读全了。
这一章把四类最容易出错的环节拆开:访问级别、路径覆盖、状态固定、结论表述。每一条都给机制、代价和可执行的替代动作。
4.1 加密、权限与解锁状态
陷阱 1