关键词:MongoDB、WiredTiger、oplog、journal、BSON、Redis、RDB、AOF、内存取证
难度:高级
前置知识:数据库取证通用方法、内存取证基础、日志文件结构
相关文章:数据库取证通用方法、MySQL 数据库取证
一、概述
这两类存储放在同一篇,不是凑数——它们共享一个最关键的取证特征:大量有价值的数据可能不在磁盘文件里。
| 存储 |
磁盘上的主要证据 |
不在磁盘上的部分 |
核心风险 |
| MongoDB |
WiredTiger 数据文件、oplog(逻辑集合)、journal |
未刷盘的内存缓冲、事务快照 |
按 data.db(旧引擎)去找,会完全找错文件 |
| Redis |
RDB 快照、AOF 日志 |
★ 未落盘的内存数据集——可能占全部数据的大部分 |
只查 RDB 会漏掉"最后快照之后"的关键操作 |
需要刻进认知的两条:
⭐ Redis 的主要证据很可能根本不在磁盘上
RDB 是周期快照,AOF 是写命令追加但可被重写裁剪。两次快照之间、以及尚未落盘的全部操作,只存在于 Redis 进程的内存里。 因此 Redis 取证必须把内存镜像作为一等检材,不能只拷 dump.rdb。
⭐ MongoDB 的 oplog 是逻辑集合,物理上是 WiredTiger 文件
oplog 存在 local 库的 oplog.rs 集合中,但它不是独立文件,而是通过 catalog 映射到 collection-*.wt 上的普通集合。"去找一个叫 oplog 的文件"是错误路径。
在取证流程中的位置:本篇属于分析环节,但它的第一个动作发生在采集规划时——服务器类案件的检材清单里必须提前写明 Redis 的内存镜像,只申请磁盘文件就来不及了。上游依赖是只读挂载与安全接入:为读 oplog 而启动一个隔离的 mongod 实例是本领域少见的写风险动作,必须在副本上做,原件目录挂 chmod -R a-w。时间线对齐按 数据库取证通用方法 的时区纪律走;Redis 的会话键要与内存里的连接态互相印证时,用 内存中的凭据与密钥 那条路径。
本篇处理的证据形态,MongoDB 侧落在 /var/lib/mongodb/ 下:
| 证据 |
文件或字段 |
取证价值 |
| 引擎配置 |
WiredTiger |
决定用哪套工具、能否整体归档 |
| 数据块文件 |
collection-0--<oid>.wt、index-*.wt |
实际数据与索引所在 |
| catalog |
WiredTiger.turtle |
集合名到文件名的映射表,解不出它就只能看到一堆无名块文件 |
| journal |
WiredTigerLog/log-0000000001.wt |
未 checkpoint 的写入、未提交事务 |
| oplog |
local 库的 oplog.rs 集合 |
逻辑变更流水,op 字段区分 i/u/d,ts 是时间戳 |
| 旧引擎 |
data.db |
见于 3.x 时代,按它去找会完全找错文件 |
| 启动参数 |
/etc/mongod.conf 的 security、keyFile 段 |
是否启用加密与认证 |
Redis 侧分配置与数据两处。配置里要看 dir 与 dbfilename(默认组合 /var/lib/redis/dump.rdb,别默认路径,实例可能改过)、save 策略行(它决定 RDB 快照周期,也就是两次快照之间的空窗有多大)、appendonly yes 时 appendonlydir/ 里的 AOF 分片、以及 appendfsync 的取值——everysec 意味着最多丢一秒的写入,丢的是一个时间窗口而不是一个文件。rename-command 被改过、masterauth 出现、bind 暴露到 0.0.0.0,都是现成的攻击痕迹,不必等分析阶段再去挖。
本篇能回答什么:MongoDB 有哪些集合、每集合的物理文件在哪、oplog 里最近发生过哪些写删改(尤其是已删除文档的旧值)、journal 里有没有未提交或已回滚的事务;Redis 当前有哪些键、键的类型与 TTL、INFO 层面的运行时统计、AOF 或 RDB 覆盖到的最后一个时间点,以及配置层面有没有被削弱的安全项。
本篇不能回答什么:磁盘文件给不出"当前"状态——Redis 的写入若未落盘,只有内存镜像能补;MongoDB 的 checkpoint 间隔也会让磁盘落后于实际。两者都证明不了**"某个键或某个文档从未存在过":Redis 的 LRU 淘汰会主动删键,AOF 重写会把旧命令裁掉,MongoDB 的集合被 drop 后旧数据块可能还留在文件里但元数据已失联。oplog 里的记录只证明某条命令被执行过**,不证明它属于哪次业务请求;$natural 是存储序不是时间序。跨系统比对时间时,BSON Date 与 AOF 时间戳都要按 数据库取证通用方法 的规则处理时区。
读者前提:需要能区分「事务日志」与「数据文件」这类概念——WiredTiger 的 journal 是预写日志,AOF 是命令追加,两者都不是备份,没开它们也不等于数据不安全,只是取证路径变了;需要知道集合与文件的映射关系不是靠文件名直读出来的,WiredTiger.turtle 是绕不开的一步;需要理解内存镜像取证的前提条件(见 内存证据的可采性与报告),否则很容易把「Redis 里没找到」写成「Redis 里没有」。没接触过这两种存储的读者,先看本章的目录构成判定引擎,再回来看后面的工具链。
二、核心原理
2.1 MongoDB 的实际文件布局
现代 MongoDB(3.2 起默认 WiredTiger 引擎)的数据目录长这样:
/var/lib/mongodb/
├── WiredTiger # ★ 引擎主元数据文件(catalog 映射在这里)
├── WiredTiger.turtle # 统计与元信息日志
├── WiredTigerLog.* # 引擎 journal(= 引擎自己的 redo)
├── journal/ # 早期版本(mmapv1 时代)的 journal 目录
├── collection-0.wt # 集合数据
├── index-1.wt # 索引
└── diagnostic.data # FTDC 诊断快照(可能含慢查询、连接信息)
| 事实 |
取证含义 |
| 集合数据与索引分文件存放 |
collection-N.wt 与 index-N.wt 必须配套取,单取 collection 得不到索引字段 |
| 文件名数字 N 不代表集合名 |
必须解析 WiredTiger catalog 文件才能建立"N → 集合名"映射 |
WiredTiger(无扩展名)是元数据主文件 |
它是打开数据库的入口,缺它整库不可映射 |
| 实际数据是 BSON 文档 |
一个文档可能跨多个 extent(4 KB 块),不是行式结构 |
关于 data.db:这是 **mmapv1 引擎时代(≤3.0)**的布局。不能把「读 data.db 前若干字节之后即为 BSON」当作所有版本的通则——WiredTiger 的块结构、压缩(Snappy/Zstd)与加密都与之完全不同。先判引擎,再谈解析。
2.2 oplog:逻辑集合,不是文件
oplog 记录每个写操作的 BSON 形式,是逻辑序列而非物理页。
① 恢复删除前值。 op: "d" 的条目含被删文档的完整原始 BSON,与 MySQL binlog 的价值完全对应。
② 重建时间线。 每条含 ts(秒 + 递增计数)、wall(客户端时间)、op、ns(库.集合)。
③ 证明时间基准。 可用于校准本机时间与其他来源的偏差。
关键提醒:oplog 位于 local 库,其保留策略由大小上限而非时间决定,是一个可被 truncate 的滚动窗口。因此「oplog 里没有」绝不能等同于「没发生过」。
未加密的 WiredTiger .wt 也可用专用工具直接解析,但版本匹配要求高,结论必须以工具实际输出为准。
2.3 Redis 的持久化三形态
| 形态 |
文件 |
内容 |
缺失时的影响 |
| RDB |
dump.rdb |
某一时刻的完整数据集快照(二进制紧凑格式,头部为 REDIS<版本号>) |
回到最近一次快照,之后全部操作不可见 |
| AOF |
appendonlydir/appendonly.aof.manifest + *.incr.aof + *.base.rdb(6.x 为单文件 appendonly.aof) |
写命令追加日志 |
丢失 AOF → 无增量可补 |
| 无持久化 |
无 |
全部数据只在内存 |
重启即全部丢失,取证只能靠内存镜像 |
redis-check-rdb dump.rdb # 校验与统计(不加载数据)
rdbtools --dump dump.rdb > rdbdump.json
AOF 的两种形态(版本不同,文件名与结构完全不同,必须先确定版本再选工具):
| 版本 |
形态 |
文件 |
取证要点 |
| 6.x 及更早 |
单文件 AOF |
appendonly.aof |
RESP 文本命令流,删改是原地重写,被后续命令覆盖的旧值无法保留 |
| 7.0 起默认 |
多部分 AOF |
appendonlydir/ 下的 appendonly.aof.manifest + *.base.rdb + *.incr.aof |
manifest 列出每个文件的 seq 与角色;*.base.rdb 是某时刻基线,*.incr.aof 是其后的增量命令 |
版本判据有三处可互证:目录中是否存在 appendonlydir/ 与 .manifest、内存中的 redis_version 字段、镜像内 redis.conf 文本。
看错版本会导致整个 AOF 段解析为空,且不会报错。
文件内 SELECT 指示数据库切换,MULTI/EXEC 指示事务块。
★ 一条必须记住的局限:DEL key 命令本身只含 key 名,不含旧值。Redis 的删除前值只能从 RDB 快照、更早的 AOF 或内存镜像中取得,这与 MySQL ROW 格式 binlog 直接给出 Delete_rows 旧值镜像有本质差别。这是 Redis 取证必须依赖内存镜像的根本原因之一。
2.4 内存取证:Redis 的核心战场
DigiForensics-mem.raw 中要拿的东西:
| 目标 |
取证价值 |
| 键空间与键清单 |
完整键列表,是「存在哪些数据」的直接证明 |
| 键类型标记 |
$(string) *(list) -(set) ~(hash),决定如何解释后续字节 |
| 键名中的业务标识 |
订单号、用户 ID、会话 ID、token |
| 嵌入的 JSON / protobuf |
可能直接包含业务内容 |
| 过期时间字段 |
区分「已过期未删除」与「仍有效」 |
AOF 缓冲区 aof_buf |
尚未写入磁盘的写命令——关键增量 |
★ aof_buf 往往是最有价值、也最易被忽视的目标。 它是"已经执行但还没落盘"的命令序列。若关键行为发生在最后一次 AOF 重写之后、且进程尚未刷盘,那么这些行为在磁盘上完全不存在,只有内存里有。
先确认 Redis 是否在内存中,避免在错误的镜像上做无效工作
strings -n 8 DigiForensics-mem.raw | grep -E '^redis_version|^run_id' | head
2.5 主动导出的边界与风险
若服务端可达,redis-cli 提供了几条取证友好的路径:
| 命令 |
性质 |
取证适用性 |
INFO / CONFIG GET |
只读 |
优先使用 |
--rdb <file> |
客户端拉取当前数据集快照 |
可用;但它是快照,不是进程内存镜像 |
DUMP key |
序列化单个键 |
需 RDB 格式;可与 RDB 交叉验证 |
BGSAVE |
服务端写 RDB |
会修改文件系统,取证中应先评估 |
DEBUG RELOAD |
清空并从 RDB 重新加载 |
⚠️ 高危:内存中未落盘的数据将被永久销毁 |
DEBUG RELOAD 在取证中应当被视为禁止操作。 它的作用是"丢掉当前内存、用磁盘快照重载"——如果内存里有未落盘的关键数据,执行它等于主动销毁证据。 该命令在部分版本中已被禁用或需特殊编译选项,行为以实际版本为准。
取证纪律:只读优先;任何可能改变状态的命令,必须先书面说明影响并取得委托方确认。
库跑在容器里时,RDB 文件与 dump 出来的副本都在卷内,需要先确定它挂在哪个卷上再动手,见第 03 篇。
三、操作步骤
3.1 MongoDB:确定引擎、归档与文件映射
1) 固定检材
sha256sum DigiForensics.dd DigiForensics-mem.raw | tee hashes.txt
2) 引擎判定
ls /mnt/case/var/lib/mongodb/WiredTiger 2>/dev/null && echo "→ WiredTiger"
ls /mnt/case/var/lib/mongodb/data.db 2>/dev/null && echo "→ mmapv1(老版本)"
3) 整体归档(catalog + collection + index + journal 一个都不能少)
cd /mnt/case/var/lib/mongodb
tar -cf mongo-db.tar WiredTiger WiredTiger.turtle WiredTigerLog.*
collection-.wt index-.wt diagnostic.data 2>/dev/null
sha256sum mongo-db.tar
4) 建立 N → 集合名映射(复制后启动,禁止在原始路径上启动)
cp -a /mnt/case/var/lib/mongodb /work/mongo-copy
chmod -R a-w /mnt/case/var/lib/mongodb # 原始目录立刻置只读
mongod --dbpath /work/mongo-copy --port 27018 --bind_ip 127.0.0.1 --fork
mongosh --port 27018 --quiet --eval
'db.adminCommand({listDatabases:1}).databases.forEach(d=>print(d.name))'
最常见的错误:只拷 collection-*.wt 而漏掉 WiredTiger。没有 catalog 文件就无法知道哪个文件对应哪个集合,此时拿到的文件只是一堆无法解释的块。
3.2 MongoDB:oplog 时间线重建
mongosh --port 27018 --quiet --eval '
const s = db.getSiblingDB("local");
s.oplog.rs.find({op:{$in:["d","u","i"]}}).sort({$natural:-1}).limit(20)
.forEach(e => printjson({ts: e.ts.toString(), op: e.op, ns: e.ns, o: e.o}));
// 顺带核算 oplog 窗口(必须写进报告)
const a = s.oplog.rs.find().sort({$natural:1}).limit(1).toArray()[0];
const b = s.oplog.rs.find().sort({$natural:-1}).limit(1).toArray()[0];
print("oplog 覆盖跨度(秒):" + (b.ts.t - a.ts.t));'
op |
含义 |
旧值可读? |
i |
insert |
— |
u |
update |
★ 含变更前后的字段 |
d |
delete |
★ 含被删文档完整 BSON |
c |
command(如 drop) |
视命令而定 |
oplog 中的删除事件形态:
{ ts: Timestamp({ t: 1724371999, i: 1 }),
wall: ISODate('2024-08-22T11:33:19.000+08:00'),
op: 'd',
ns: 'shopdb.orders',
o: { _id: ObjectId('66a4c2f1e3b2a10012ab34cd'),
orderNo: 'SO20240822000931',
amount: Decimal128('8800.00'),
userId: 'U100245',
status: 'paid' } }
这条 oplog 条目独立证明:2024-08-22 该订单存在,金额 8800.00,状态 paid,随后被删除。
即使集合中已无此文档,oplog 只要还在窗口内,事实就可复原。
3.3 MongoDB:加密实例的处理
若启用了 WiredTiger 加密,.wt 内容不可解析。
处理顺序:① 找配置中 encryption.keyFile 指向的密钥文件、配置中心凭据、运维交接材料;② 在内存镜像中搜索 master key(配置加载时会将其放入进程内存);③ 两者皆无 → 明确记录"无法解密",说明影响范围,不得推测内容。
grep -Ei 'encryption|keyFile|enableEncryption' /etc/mongod.conf
xxd /etc/mongo-keyfile | head -6 # 密钥文件本身是 96 字节原始密钥
3.4 Redis:定位、归档与离线解析
1) 定位配置与数据目录
ps aux | grep '[r]edis-server'
grep -E '^\s*(dir|dbfilename|appendonly|appendfsync|save|rename-command)' /etc/redis/redis.conf
2) 归档(AOF 目录整体取走,manifest 不能缺)—— 两种版本二选一
cd /mnt/case/var/lib/redis
tar -cf redis-data.tar dump.rdb appendonlydir/ 2>/dev/null # 7.0+ 多部分 AOF
tar -cf redis-data.tar dump.rdb appendonly.aof 2>/dev/null # 6.x 单文件 AOF
sha256sum redis-data.tar
3) 离线解析(manifest 列出 base/incr 两部分)
redis-check-rdb dump.rdb
rdbtools --dump dump.rdb > rdbdump.json 2>/dev/null
cat appendonlydir/appendonly.aof.manifest
| 配置项 |
取证意义 |
dir + dbfilename |
数据文件实际路径——常被改到非默认目录,容易漏掉 |
appendfsync |
always(每条落盘)/ everysec(最多丢 1 秒)/ no(由 OS 决定,可能丢很多) |
save |
RDB 快照策略;save 900 1 = 900 秒内 1 次修改就存 |
rename-command |
是否禁用了危险命令;被禁用的 DEBUG 印证 2.5 的风险提示 |
AOF 命令解码器(RESP 协议,可直接运行):
cat > resp.py <<'PY'
import sys
d = open(sys.argv[1], 'rb').read()
第三个参数为输出上限,仅用于抽样预览;不传则完整解码全文件
cap = int(sys.argv[2]) if len(sys.argv) > 2 else None
i = n = 0
while i < len(d) and (cap is None or n < cap):
if d[i:i+1] != b'*': # 跳过非数组头(SELECT、
、注释等)
i += 1; continue
j = d.find(b'\r
', i)
if j < 0: break
argc, args, i = int(d[i+1:j]), [], j + 2
for _ in range(argc):
if d[i:i+1] != b'$': args = []; break
j = d.find(b'\r
', i); ln = int(d[i+1:j]); i = j + 2
args.append(d[i:i+ln].decode('utf-8', 'replace')); i += ln + 2
if args:
n += 1; print(f"[{n}] {' '.join(args)}")
print(f"# 共解码 {n} 条命令", file=sys.stderr)
PY
python3 resp.py appendonlydir/appendonly.aof.1.incr.aof > aof-cmds.txt # 完整解码
python3 resp.py appendonlydir/appendonly.aof.1.incr.aof 20 # 仅抽样看格式
grep -an 'DEL|UNLINK|FLUSHALL|FLUSHDB|CONFIG SET' aof-cmds.txt | head -40
统计口径:aof-cmds.txt 的行数是命令条数(一条 AOF 命令一行,MULTI/EXEC 块内命令同样逐条计数)。取证报告中引用"解码出 N 条命令"时必须说明这个口径,不要把它与"数据条数"或"键数量"混用。
3.5 Redis:内存镜像分析(重点)
1) 确认 Redis 驻留痕迹与配置
strings -n 6 DigiForensics-mem.raw | grep -E 'redis_version|redis_mode|run_id' | sort -u
2) 键空间清单(★ 最重要的一步)
典型形态:order:SO20240822000931 / session:9f3a2b7c41e5 / cart:8d2f1a
strings -n 4 DigiForensics-mem.raw | grep -E '^[a-zA-Z][a-zA-Z0-9_]*:[A-Za-z0-9_.:-]+$'
| sort -u > keys.txt
wc -l keys.txt && head -20 keys.txt
3) 业务内容:嵌入 JSON 的值
strings -n 12 DigiForensics-mem.raw | grep -E '^{"(orderId|userId|amount|toke