关键词:即时通讯、聊天记录、SQLite、消息数据库、完整性哈希、时间线
难度:进阶
前置知识:SQLite 结构与查询、应用私有目录结构、哈希与时间戳规范
相关文章:Android 数据存储总览、SQLite-WAL 取证、Frida 动态 Hook
一、概述
即时通讯数据在移动取证里证据价值最高,也最容易整块拿不到。
难的不是解析。难的是解析之前的每一环都可能悄悄失败:包名找错、数据目录漏了用户、数据库只提了主库、加密没解开、时间戳量级判错。任何一环出错,产出的都是一份看起来完整、实际残缺的结果。
本篇给的是一条完整链路:找包名 → 找数据目录 → 找数据库文件 → 找加密 key → 解密 → 解析表。第三章把它归成五步操作。
为什么反复强调"可迁移"。国内案件碰到的 IM 应用版本众多,目录结构随时变,加密方案各不相同。任何绑定具体路径或具体算法的知识都会过期,上面这条判断框架不会。
本篇给框架,不给某一版应用的目录字典。
缺了这条框架,最常见的后果是"记录只有 8000 条"这种结论。设备上实际有 12000 条,最近 4000 条还躺在 -wal 里没回写到主库。主库能正常打开、能列出表名、能查记录数,全程不报错(见 2.3)。更麻烦的是时间戳量级判错之后把"案发前"读成"案发后",整条时间线方向全反。
在取证流程中的位置:本篇位于分析环的核心,是把 Android 数据存储总览 提取到的应用目录变成案件可用证据的主要路径。
上游依赖私有目录的完整提取与用户档案枚举,多用户与工作资料必须分别处理。下游支撑三类结论:聊天内容的事实认定、时间线对齐、范围与缺口的书面表述。它同时是 APK 逆向与 Manifest 分析、Frida 动态 Hook、应用私有数据 三篇共同的下游出口。
证据价值上,本篇反复强调三条硬规矩:
- 聊天记录必须做完整性哈希——它是最容易被质疑"被篡改 / 被选择性提取"的对象。
- 时间线必须能对齐——IM 应用时间戳格式不统一(秒 / 毫秒、Cocoa / Unix、本地时区),错一格就可能把"案发前"读成"案发后"。
- 提取范围必须写清楚——"未获取"要写成"未获取",不能沉默跳过。
本篇涉及的证据形态(保留真实产品的强制文件名):
| 形态 |
具体位置 |
说明 |
| 库文件 |
EnMicroMsg.db(微信)、msg.db(WhatsApp)、r0.db/r1.db(QQ 按账号分库) |
主体,普遍为 SQLite |
| WAL 三件套 |
同名 .db-wal、.db-shm |
必须同目录,-wal 可能是数 MB 量级 |
| 账号目录 |
MicroMsg/<32位十六进制>/ |
多账号 / 多设备的维度线索 |
| 媒体目录 |
files/、/sdcard/Tencent/WeChat/ |
图片、视频、语音、文档 |
| 外部目录 |
Android/media/<包名>/(卸载后保留) |
见 2.2 |
| 完整性记录 |
三件套的哈希、PRAGMA integrity_check 结果、合并前后 count(*) 差值 |
差值就是差点漏掉的记录量,要写进报告 |
能回答什么:某个时间点上账号登录过哪些账号、与谁的对话内容是什么、消息数量与时间范围、哪些内容已被删除但仍可从 freelist 恢复。
不能回答什么:对方是否阅读,已读回执不能替代。消息是否被伪造,IM 应用的消息可被本机改写。聊天记录里的时间戳是否可信——应用层时间戳可被改,只有与文件系统 mtime、服务器侧记录交叉印证时强度才够。已卸载、Android/data 又被清除、委托方也没给导出文件的情况,本篇方法覆盖不了。
关于获取解密 key 的边界:合法途径包括委托方自愿提供、应用内导出的明文文件、在已获书面授权的检材上做动态分析(见 第 4 步)、内存镜像中已解密的数据库页、应用自身的导出功能。
不建议针对商业 App 编写通用解密工具并分发。取证的目标是这一个检材里的证据,不是产品化。本篇不描述任何应用的密钥派生实现。
读者前提:需要 SQLite 基础与结构 的读写能力、应用私有数据与 SharedPreferences 的目录判读,以及 时间戳换算 里的量级判定原则。
建议按五步顺序走,别跳步。五步里任何一步的结论都是下一步的前提。
二、核心原理
2.1 五步法的每一步在解决什么
| 步骤 |
解决什么 |
常见失败原因 |
| ① 找包名 |
确定目标进程与数据目录的入口 |
同名应用有多个包名(主应用/轻应用/分身) |
| ② 找数据目录 |
确定 /data/user/0/<包名>/ 下的具体子目录 |
多用户、应用分身导致路径不同 |
| ③ 找数据库文件 |
定位 *.db 及其伴随文件 |
只看 .db 漏了 -wal/-shm |
| ④ 找加密 key |
判断是否加密、从哪拿 |
未区分 SQLCipher 与应用自加密 |
最容易被跳过、也最致命的是第 ③ 步的"完整提取"。详见 09。
2.2 公开可知的 IM 应用数据目录
下面这些目录结构都是广为公开的。各版本会变,务必以实际提取结果为准:
| 应用 |
包名 |
数据目录要点 |
备注 |
| 微信 |
com.tencent.mm |
MicroMsg/(主数据)、files/(媒体与文档)、EnMicroMsg.db |
聊天记录库需 key;/sdcard/Tencent/WeChat/ 是外部目录 |
| QQ |
com.tencent.mobileqq |
files/、r0.db / r1.db …(按账号分库)、msg.db |
多账号分文件是 QQ 的特点 |
| WhatsApp |
com.whatsapp |
databases/msgstore.db |
通常需要转换或专用解析器 |
| Telegram |
org.telegram.messenger |
databases/(多个 *.db) |
部分内容在 files/ |
⚠️ 关于微信 MicroMsg 目录:MicroMsg/<32位MD5形式的目录>/ 是公开可见的组织方式,用来区分账号与设备。这个 32 位十六进制串的来源与应用内存储的设备标识有关,可以当"多账号/多设备"的判定线索,但具体派生算法属于应用内部实现,本篇不做描述。
⚠️ 关于加密:EnMicroMsg.db 这类库使用了加密,这是公开信息,strings 或 PRAGMA 都能探测确认。如何获得解密所需的 key,属于具体应用的逆向范畴,可能触及法律与授权边界。
合法途径有四种:① 委托方自愿提供、应用内导出的明文文件;② 在已获书面授权的检材上做动态分析(见 04);③ 内存 dump 中已解密的数据库页,若案件中存在内存镜像;④ 应用自身的「导出聊天记录」功能产生的明文文件。
不建议针对商业 App 编写通用解密工具并分发给他人使用。取证的目标是获取这一个检材中的证据,不是产品化。
2.3 关键:-wal 与 -shm 必须一起提取
这是本篇最需要强调的技术点,因为它的后果是误判。app.db 是主库,app.db-wal 是预写日志,最近的写入在这里,app.db-shm 是共享内存索引。
只提取 app.db 的话,sqlite3 app.db ".tables" 照样能列出表,message 表只有 8000 条,设备上实际有 12000 条。最近 4000 条还在 WAL 里没回写。
据此写下的「记录只有 8000 条」是错的。
正确做法是三个文件放在同一目录。ls -l extracted/databases/ 确认三件套齐全,先 PRAGMA wal_checkpoint(TRUNCATE) 把 WAL 回写再计数。详见 09-SQLite-WAL取证。
2.4 提取后的验证与时间线
数据库层面的验证,每一项都要在报告里体现:PRAGMA integrity_check 看结构是否完好,.tables 看表是否可读,SELECT count(*) 看记录数。
时间戳有三个坑。秒和毫秒混用,数值长度差 1000 倍。Cocoa 时间,iOS 生态常用,基准 2001-01-01,得加 978307200。时区,数据库存 UTC 而界面显示本地时区,SQLite 的 unixepoch 返回 UTC,要按当地时区换算。
# 安全的转换方式:先判断量级再转换
sqlite3 app.db "
SELECT CASE
WHEN ts > 1000000000000000 THEN datetime(ts/1000000 + 978307200, 'unixepoch', '+8 hours')
WHEN ts > 1000000000000 THEN datetime(ts/1000 + 978307200, 'unixepoch', '+8 hours')
WHEN ts > 1000000000 THEN datetime(ts + 978307200, 'unixepoch', '+8 hours')
END AS time_str
FROM message ORDER BY ts LIMIT 20;"
Cocoa 时间基准 978307200 = 2001-01-01 00:00:00 UTC 的 Unix 秒数。加这个数只是换算基准,是否真的是 Cocoa 时间还得结合字段含义与量级判断(数值落在 0–约 8 亿之间才是 Cocoa)。别盲目加。
三、操作步骤
第 1–2 步:确认包名、提取完整数据目录(含 WAL)
# 列出所有 IM 相关包(关键词覆盖,务必补全目标应用)
adb shell pm list packages -f | grep -iE 'tencent|whatsapp|telegram|weibo|line|imo'
aapt dump badging base.apk | grep -E '^package|application-label' # 确认包名对应哪个应用
adb shell pm list users # 多用户
adb shell su -c "ls -d /data/user/*/com.tencent.mm 2>/dev/null" # 分身
PKG=com.tencent.mm
mkdir -p extracted && cd extracted
adb shell su -c "tar -cf - -C /data/user/0 $PKG" > im.tar # ★ db / -wal / -shm 一起带走
sha256sum im.tar && tar -tf im.tar | wc -l && tar -xf im.tar
ls -l $PKG/databases/ $PKG/MicroMsg/*/EnMicroMsg.db* 2>/dev/null # ★ 确认三件套齐全
find . -type f -exec sha256sum {} \; > hashes.txt
第 3 步:判断是否加密
# 方法 A:能否被识别为 SQLite
head -c 16 $PKG/MicroMsg/*/EnMicroMsg.db | xxd
# 期望:5351 4c69 7465 2066 6f72 6d61 7420 33 = "SQLite format 3"
# 实际:9a3f 27c1 ...(随机字节) ← 加密
# 方法 B:让 sqlite3 自己报告
sqlite3 $PKG/MicroMsg/*/EnMicroMsg.db ".tables"
# file is not a database ← 确认非明文 SQLite
# database disk image is malformed ← 另一种表现
# 方法 C:熵值(加密文件接近 8.0)
ent $PKG/MicroMsg/*/EnMicroMsg.db 2>/dev/null || \
python3 -c "
import math,collections,sys
d=open(sys.argv[1],'rb').read()
c=collections.Counter(d); n=len(d)
print('entropy = %.3f bits/byte' % (-sum((v/n)*math.log2(v/n) for v in c.values())))
" $PKG/MicroMsg/*/EnMicroMsg.db
# entropy = 7.998 bits/byte ← 接近 8 = 加密或压缩
第 4 步:获取 key(合法途径)
| 途径 |
适用性 |
说明 |
| 委托方提供明文导出 |
★★★★★ 最优先 |
应用内"导出聊天记录"功能产生的文件 |
| 已授权的动态分析 |
★★★★ |
见 04 |
| 内存镜像中的明文 |
★★★ |
若案件含 DigiForensics-mem.raw,可能含已解密页 |
| 应用登录的另一台设备 |
★★★ |
需另行取得该设备检材或授权 |
strings -n 8 DigiForensics-mem.raw | grep -c 'SQLite format 3' # 远多于磁盘库数 → 内存中存在已解密的库
第 5 步:解密与表结构分析
# ① 永远在副本上操作(三件套一起复制)
cp EnMicroMsg.db EnMicroMsg-copy.db
cp EnMicroMsg.db-wal EnMicroMsg-copy.db-wal 2>/dev/null
cp EnMicroMsg.db-shm EnMicroMsg-copy.db-shm 2>/dev/null
# ② SQLCipher 类加密的标准处理
sqlite3 EnMicroMsg-copy.db "PRAGMA key='<取得的key>';"
sqlite3 EnMicroMsg-copy.db ".tables"
sqlite3 EnMicroMsg-copy.db "PRAGMA integrity_check;"
# ③ 读表结构(先看结构,再决定 SELECT 什么)
sqlite3 EnMicroMsg-copy.db ".schema" ; sqlite3 EnMicroMsg-copy.db ".schema message"
# ④ 记录数与时间范围
sqlite3 EnMicroMsg-copy.db "SELECT count(*) FROM message;"
sqlite3 EnMicroMsg-copy.db "
SELECT datetime(min(CreateTime)/1000,'unixepoch','+8 hours') AS 最早,
datetime(max(CreateTime)/1000,'unixepoch','+8 hours') AS 最晚 FROM message;"
⚠️ 表名与字段名因应用与版本而异(微信历史上用过 message、Msg、chatroom_* 等不同结构;新版主库常按会话分表)。不要把某个版本的表名当作通用知识。 正确做法:.tables 看全貌 → .schema 看结构 → 再写查询。
第 6 步:导出、固化与三层验证
# ① 导出为 CSV(注意编码:e4bda0=UTF-8;b4f3=GBK,中文编码混杂是常态)
sqlite3 -header -csv EnMicroMsg-copy.db \
"SELECT id, CreateTime/1000 AS ts, Type, Status, StrContent FROM message
WHERE CreateTime BETWEEN 1717000000000 AND 1718000000000;" > messages-export.csv
file messages-export.csv && wc -l messages-export.csv
# ② 数据层验证
sqlite3 EnMicroMsg-copy.db "PRAGMA integrity_check;" # ok
sqlite3 EnMicroMsg-copy.db "SELECT count(*) FROM message;" # 与导出行数一致
# ③ 交叉层:同一时间点在其他工件中是否有对应痕迹
grep -rn '2024-08-15 21:33' extracted/shared_prefs/ | head
# prefs 中的 last_login_time 与消息时间吻合 → 两个独立来源印证 ✓
# ④ 完整性层:原始包 / 解密副本 / 导出件 三层哈希
sha256sum im.tar EnMicroMsg-copy.db messages-export.csv | tee export-hashes.txt
四、常见陷阱
聊天记录既是案件核心证据,也最容易被技术细节做成「假阴性」和「假时间线」。
这一章按实际踩坑顺序排:提取完整性 → 时间语义 → 表结构认知 → 范围覆盖 → 结论表述。前两条造成的误判代价远高于后面几条,因为它们直接改变事实认定。
4.1 提取完整性
陷阱 1:只提主库 EnMicroMsg.db,不带 -wal / -shm
现象:从设备提取到 EnMicroMsg.db,sqlite3 用口令打开后 .tables 正常列出 message 表,count(*) 得到 18402 条,时间范围截至 2024-08-24。报告写下「该时段聊天记录共 18402 条,此后无记录」。
为什么会误判:IM 应用几乎全用 WAL(预写日志)模式,新的消息先追加到 -wal 文件,事务提交后不立刻回写主库,要等 checkpoint 才批量合并。本案的 -wal 达 8 MB,说明有大量数据还没回写。
单独打开 EnMicroMsg.db 时,SQLite 读到的是上一次 checkpoint 时的完整快照。结构完好,integrity_check 通过,count(*) 返回一个完全合理的数字,唯独缺了最近这一段。它不会给任何提示。
误判代价:这是本领域最贵的一个错误,因为它制造的是一个看起来完全正常的错误结论。
本案实际数据是 24193 条、截至 2024-08-31 23:33。只读主库得到 18402 条、截至 2024-08-24。5791 条消息被完整遗漏,而这 5791 条恰好覆盖了委托要求查明的那一周。
更麻烦的是这个结论会向下传导。「此后无记录」会被用来论证「约定没有后续跟进」「双方在案发后没有再联系」,而那些记录就在同一个提取目录的隔壁文件里。委托方拿着这个结论去做后续处置(质疑举报、认定事实),方向完全错位。
正确做法:整个目录打包,三个文件必须同在。提取后第一件事是确认三件套齐全,并把它们的体量当作提取完整性的第一道判据:
PKG=com.tencent.mm
# ① 目录级导出(-wal / -shm 天然包含)
adb shell su -c "tar -cf - -C /data/user/0 $PKG" > im.tar
sha256sum im.tar && tar -xf im.tar
# ② 第一时间确认三件套齐全,并看 -wal 的体量
ls -l $PKG/MicroMsg/*/EnMicroMsg.db*
# 412662784 EnMicroMsg.db
# 8388608 EnMicroMsg.db-wal ← 8 MB:有大量数据未回写
# 32768 EnMicroMsg.db-shm
# ③ 反事实验证(强烈建议做一次,成本极低价值极高)
# 只用主库打开,得到"错误答案"
cp EnMicroMsg.db test-mainonly.db
sqlite3 test-mainonly.db "PRAGMA key='<口令>';"
sqlite3 test-mainonly.db "SELECT count(*), max(CreateTime) FROM message;"
# 18402 | 1724459642000 ← 偏早 7 天
# ④ 完整三件套,得到"正确答案"
cp EnMicroMsg.db DigiForensics-analysis.db && cp EnMicroMsg.db-wal DigiForensics-analysis.db-wal && cp EnMicroMsg.db-shm DigiForensics-analysis.db-shm
sqlite3 DigiForensics-analysis.db "PRAGMA key='<口令>';"
sqlite3 DigiForensics-analysis.db "SELECT count(*), max(CreateTime) FROM message;"
# 24193 | 1725118405000 ← 正确
第 ③ 步是本篇最值得固化的一个动作。 两条命令就把「我漏了多少」量化成一个具体数字,5791 条。这个数字可以直接写进报告的局限性说明,成为覆盖范围的客观依据。在聊天记录这类核心证据上,反事实验证的成本几乎为零。
陷阱 2:把 WAL 里的「看不到」当成「已被删除」
现象:完整提取三件套后,打开数据库发现某些消息「不存在」,报告写「用户在 2024-08-20 删除了与 X 的相关聊天」,并把删除时间作为行为认定。
为什么会误判:这是把「未读取」误读为「已删除」的定性错误,而且它发生在最不该含糊的地方——「删除他人聊天」在多个司法辖区都是明确的独立行为,有时伴随独立的法律责任。
技术上,消息「看不到」至少有四种完全不同的原因:① 还在 -wal 里没回写,不是删除,是没读;② 在主库但被查询条件排除了,比如条件里带了时间范围或会话过滤;③ 应用层的「删除」在很多 IM 里是软删除,记录还在,只是被标记了状态位,用户界面不再显示;④ 真的被清理了,应用执行了数据清理或用户清过缓存。这四种在 SQL 层面的表现可能完全一致。
更关键的是,消息内容确实可能以另一种形式存在。多数 IM 会把被删除的引用、转发、撤回提示等写入会话的扩