搜索全站

输入至少 2 个字符,查找相关内容。

↑↓ 选择 Enter 打开Esc 关闭

数据库取证通用方法

前面几篇都是具体引擎的具体做法。本篇讲骨架。

最后更新 2026-10-09版本 v2.0维护 DigiForensics查看历史 1

一、概述

前面几篇都是具体引擎的具体做法。本篇讲骨架。

不管你拿到的是手机里的 .db、服务器上的 ibdata1,还是缓存里的 dump.rdb,第一小时该怎么走、每一步产出什么、结论在什么情况下才算成立——这些跟引擎无关。

有三个问题最容易漏,而且漏了会一路错到结论。

⭐ 1. 时间线陷阱:datetime 没有时区

MySQL 的 DATETIME、PostgreSQL 的 timestamp without time zone、MongoDB 的 BSON Date、JSON 里的字符串时间——它们存储时都不带时区信息。 它们的含义完全依赖服务器/数据库当时的 time_zone 设置,而这个设置可以被随时改、且不记录在数据里。 一旦跨系统比对(应用日志、Nginx 访问日志、Syslog、内网其他服务器),8 小时甚至更多时差的错位几乎必然发生。

⭐ 2. 容器里的 TZ=UTC 陷阱

越来越多的服务跑在容器里。容器镜像默认时区就是 UTC,且经常与宿主机时区不一致。 运维看到的日志时间是 UTC,取证人员按本地时区解读,同一行日志会有两种"正确"读法。 必须确认数据库所在运行环境(宿主 or 容器)的实际时区,不能看"服务器在哪"来判断。

⭐ 3. 缺失 ≠ 未发生

数据库层面的"查不到"至少有六种完全不同的原因,任何一种都不能写成"没有这个行为": 已删除 / 未落盘 / 已过期清理 / 从未同步到该节点 / 归档窗口已过 / 检材提取不完整。

在取证流程中的位置:本篇是本板块所有具体引擎文章的前置方法论,读它应该在读 SQLite WAL 或 MySQL 之前。它给出一条固定的六个阶段:检材固定与环境记录 → 引擎识别 → 整体归档 → 时间基准归一 → 一致性与完整性校验 → 报告输出。每个阶段都有明确产出,跳过任何一个都会在后面某个环节以更贵的形式还回来——尤其是时间基准归一,它必须在任何查询执行之前完成,因为 mysqldump 一次就足以在检材副本上留下访问痕迹。上游依赖是 只读挂载与安全接入(归档动作必须只读、哈希先于分析)与 取证流程与原则(证据链与结论标准)。下游是本板块的三个具体篇目:SQLite 基础与结构、SQLite WAL 取证、MySQL 数据库取证、NoSQL 与 Redis 取证——它们各自的具体做法都建立在本篇的阶段划分之上,尤其时间基准这一段,四篇共用同一套纪律。

本篇处理的证据形态,横跨三类对象,判据各不相同:

类别 具体对象 判据
数据库文件 app.db、app.db-wal、app.db-shm(SQLite 三件套);ibdata1、各 .ibd、binlog.00000X、undo_001(InnoDB 家族);WiredTiger 的 collection-*.wt 与 WiredTiger.turtle;dump.rdb 与 AOF 分片 看目录构成与文件头,不看扩展名;三件套必须整体哈希,单独哈希主库没有意义
配置 my.cnf 与 ~/.my.cnf、postgresql.conf、redis.conf、/etc/mongod.conf;容器的 Dockerfile / compose / K8s manifest 读取顺序是配置文件 → 环境变量 → 命令行参数,后者覆盖前者,所以只看配置文件会得出错误配置
时区证据 三处独立来源:进程环境变量 TZ、数据库参数 @@time_zone / @@system_time_zone、日志里已渲染出来的时间;物证是 /etc/localtime 的符号链接指向与 /usr/share/zoneinfo/ 下的文件 /etc/localtime → Etc/UTC 是容器 UTC 时区最直接的物证;环境变量在内存镜像里也能捞到
完整性证据 归档包的 SHA-256、页级校验(如 SQLite 的 WAL 帧滚动校验和)、解析时的 PRAGMA integrity_check 一类只读校验 哈希要在分析前、分析后各算一次,两次一致才说明分析过程没有写坏副本

检材在什么状态下被接收、它包含哪些引擎的文件、时间基准是什么(依据哪三处证据得出)、文件与归档包是否完整一致、每个结论的覆盖范围到哪里。

"数据库里没有记录"永远不等于"这件事没有发生"——time_zone 一旦被改过且无记录,你根本判断不出那条时间戳当初是按哪个时区写的,这时任何时间换算都是猜测,报告里必须把不确定性写出来而不是给一个看似精确的时间。查不到记录的六种成因彼此无法区分,需要靠同批次的其他工件(日志、备份、binlog 过期窗口)缩小范围,而不能靠数据库本身下结论。归档完整不等于内容完整:整体哈希一致只能证明文件没变,证明不了提取时就没漏。阴性结果必须限定到具体范围与时间窗,并列出未覆盖对象(未提取的应用目录、未纳入归档的日志、已过期的 binlog);而且"查不到"永远无法排除它从未被写入数据库这个可能之外的路径——比如数据只存在于客户端缓存或另一个副本库里。

读者前提:需要已经能识别检材属于哪种引擎(否则"阶段二:引擎识别"就是天书),至少读过本板块的 SQLite 或 MySQL 两篇之一;需要能读十六进制并理解偏移、页、大小端这类基本概念,因为完整性校验和时区物证的判断都要落到具体字节上;需要接受"证据链记录"本身就是交付物的一部分,而不是事后补写;还要理解 mysqldump 这类工具是有副作用的——它能触发 checkpoint、能写查询历史、能在文件系统上留下访问痕迹,所以任何导出动作必须在副本上做,并在采集记录里写明执行时间。


二、核心原理

2.1 数据库状态是文件集合,不是文件

任何数据库在某个时刻的状态,都是好几个文件一起定下来的。少一个,那个时刻的状态就不完整。

组成 作用 缺失后果
数据文件 表/集合/键的实际内容 无法读取
结构元数据 表定义、列类型、集合映射 读得出字节,读不出含义
日志(WAL / journal / binlog) 未落盘增量、历史值 丢失最新数据与删除前值
索引文件 加速与完整性辅助 部分查询退化,索引存在性无法验证
内存状态 未刷盘缓冲、活跃会话 关键数据完全不可见
配置 引擎、时区、字符集、加密 解释错误、时区错位、解密失败

通用规则就一条:能整体打包就整体打包,别做"选择性拷贝"。

选择性拷贝的错误是静默的。命令成功,包也出来了,一个警告都没有——只是内容少了。

2.2 时间的三种存储形态

形态 示例 带时区? 判读要求
Unix 时间戳 1724297599、MySQL INT、Redis AOF 中的 SETEX 是(UTC 基准) 转成 UTC 后再按目标时区呈现
带时区时间 2024-08-22T11:33:19+08:00、Java Instant 是 直接换算,但注意可能是客户端伪造的
无时区时间 MySQL DATETIME、MySQL binlog 事件时间、PostgreSQL timestamp without time zone、本地文件 mtime 呈现 否 必须声明所采用的时区假设

出错最多的就是第三类。DATETIME '2024-08-22 11:33:19' 写进去的时候按哪个时区理解,取决于连接会话的 time_zone;写完之后这个信息就永久没了。 取证能做的有限:把当时的配置和系统时区找出来当证据,声明假设,再在报告里标清这个假设影响到哪些结论。

2.3 容器化环境的时间链路

服务跑在容器里,时间基准得一层层往回追:

层级 可能的状态 后果
应用代码 可能显式设置 TZ、time.Local、X-TIMEZONE 应用层时间已被转换
运行时镜像 基础镜像几乎都是 UTC 容器内 date 是 UTC
编排平台 K8s Pod 可注入 TZ 环境变量;未注入则继承镜像设置 多数集群不设
数据库连接 JDBC/驱动可能把会话时区设为服务器时区 决定 DATETIME 如何解释
宿主机 可能为 UTC+8 宿主机时区≠容器时区

判读规则:

别看"服务器物理位置在哪",看"数据库进程实际读到的是什么"。 这个值有三个地方能拿到:进程环境变量 TZ、数据库参数 @@time_zone / @@system_time_zone、日志里已经渲染出来的时间。

宿主机时区

timedatectl 2>/dev/null | grep -E 'Time zone|RTC in' cat /etc/timezone 2>/dev/null

进程环境(内存镜像中)

strings -n 4 DigiForensics-mem.raw | grep -E '^TZ=|^TZ=' | sort | uniq -c

容器内(若有 shell 权限)

cat /etc/timezone 2>/dev/null readlink -f /etc/localtime

常见容器值:/usr/share/zoneinfo/Etc/UTC ← 关键线索

数据库参数

mysql -uroot -p -e "SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;"

/usr/share/zoneinfo/Etc/UTC 或 /etc/localtime → Etc/UTC 这种指向,就是容器 TZ=UTC 最直接的物证。

2.4 "查不到"的六种成因

成因 判据 报告应写
已删除 主库无记录,但 binlog/undo/空闲区有痕迹 "已被删除,删除前值可从 X 恢复"
未落盘 WAL/AOF/内存中有,磁盘文件中无 "存在于 X,尚未持久化"
已过期清理 日志窗口外,无任何痕迹 "在已获取的日志窗口(A 至 B)内未见,窗口外无法判断"
未同步到该节点 从库/副本缺少主库有的事件 "本节点未同步,不代表未发生"
已归档轮转 文件序号跳变、目录被压缩移走 "存在日志缺口,该时段不可判断"
检材提取不完整 只提了主库、漏了伴随文件 "检材不完整,已回源补提"

这一张表应当直接印在报告模板里。 每一条"未发现"的结论旁边,必须标注它属于上表哪一类。不标注的"未发现"在质证环节无法成立。

2.5 完整性的三个层级

委托方嘴里的"数据库已恢复",可能是三件完全不同的事。

层级 含义 验证方式
设备已解锁 手机/主机已能正常进入系统 能否开机、能否登录
存储已解密 全盘/FBE/BitLocker 已解开,文件可读 文件系统能否挂载、目录能否列出
数据库已解密 数据库自身的表空间加密/字段加密已解开 能否 SELECT 出明文

报告里这三层必须分开说。 "手机已解锁、磁盘镜像已解密,但目标数据库为 SQLCipher 加密且密钥未获取"——这是一个完整、准确、对委托方有实质价值的结论,不能写成"数据无法获取"。

归档副本的哈希怎么算、用 MD5 还是 SHA-256、要不要登记进哈希数据库,见哈希与完整性校验。

这几个数字后面的每一处时间、每一个结论,都要靠上面的记录兜底。


三、操作步骤

3.1 阶段一:检材固定、环境与时区链路记录

1) 哈希先行,之后一切修改只在副本上

sha256sum DigiForensics.dd DigiForensics-mem.raw | tee hashes.txt

2) 记录时区链路(★ 这一步的产出必须写进报告)

timedatectl 2>/dev/null | grep -E 'Time zone' readlink -f /etc/localtime # 常见容器值:/usr/share/zoneinfo/Etc/UTC cat /etc/timezone 2>/dev/null strings -n 4 DigiForensics-mem.raw | grep -x 'TZ=[A-Za-z/_+-]*' | sort -u

3) 判断是否容器

ls /.dockerenv 2>/dev/null && echo "容器" cat /proc/1/cgroup 2>/dev/null | head -3 systemd-detect-virt --container 2>/dev/null

4) 数据库进程与配置

ps aux | grep -E '[m]ysqld|[m]ongod|[r]edis-server|[p]ostgres' ls /etc/my.cnf /etc/mysql/ /etc/mongod.conf /etc/redis/redis.conf 2>/dev/null mysql -uroot -p -e "SELECT @@global.time_zone, @@session.time_zone, @@system_time_zone;"

时区链路的结论格式(写入报告):

宿主机时区:Asia/Shanghai (UTC+8);容器内 /etc/localtime → Etc/UTC;数据库 @@system_time_zone = UTC。据此判断:本机数据库写入的 DATETIME 与 binlog 事件时间均按 UTC 解释。

/usr/share/zoneinfo/Etc/UTC 或 /etc/localtime → Etc/UTC 这类指向,是容器 TZ=UTC 最直接的物证。

3.2 阶段二:引擎识别

通用特征扫描

find /mnt/case -maxdepth 4 ( -name '.db' -o -name '.ibd' -o -name 'ibdata*'
-o -name '.frm' -o -name '.wt' -o -name 'dump.rdb' -o -name 'appendonly*'
-o -name 'PG_VERSION' -o -name 'ib_logfile*' -o -name '-journal' -o -name '-wal' )
2>/dev/null | head -40

魔术字节

xxd -l 16 app.db | grep -q '5351 4c69 7465' && echo "SQLite 明文"

若首字节不是 'S' → 可能是加密库,判定加密扩展(以实际库为准)

xxd -l 9 dump.rdb # 5245 4449 5300 0006 02 → REDIS + 版本号 cat base/PG_VERSION # PostgreSQL 纯文本版本号

命中 引擎 首要动作
SQLite format 3 文件头 SQLite 确认并提取 -wal/-shm(见 09)
ibdata1 / *.ibd MySQL InnoDB 先定版本(见 10)
binlog.0000* MySQL 优先攻 binlog
WiredTiger / collection-*.wt MongoDB 必须取 catalog 文件(见 11)
dump.rdb / appendonly* Redis 必须做内存取证
PG_VERSION / pg_wal/ PostgreSQL 取 pg_wal 与 pg_xact

3.3 阶段三:整体归档

原则:宁可多不可少;不要跨时间点分别拷贝

cd /mnt/case/var/lib/mysql tar -cf mysql.tar ibdata1 ibtmp1 mysql binlog.* mysql-bin.* 2>/dev/null find . -name '.ibd' -o -name '.frm' | tar -cf mysql-tables.tar -T -

cd /mnt/case/var/lib