当前位置: 首页 > news >正文

2026FIC-agent在服务器取证侧的运用

写在前面,答案应该有三个错的,供参考

2026FIC-agent在服务器取证侧的运用

附录脚本

脚本1:E01/LVM/md RAID0/Btrfs 读取器

保存为 forensics_btrfs.py,在包含两个 E01 的目录下运行。

依赖示例:

pip install pyewf-python dissect.btrfs
import argparse
import os
import re
import sysimport pyewf
from dissect.btrfs import BtrfsEXTENT_SIZE = 8192 * 512
MD_DATA_OFFSET = 67584 * 512
MD_CHUNK_SIZE = 1024 * 512class EWF:def __init__(self, path):self.handle = pyewf.handle()self.handle.open(pyewf.glob(path))def read(self, offset, size):self.handle.seek(offset)return self.handle.read(size)def close(self):self.handle.close()class LV:def __init__(self, images, pvs, segments):self.images = imagesself.pvs = pvsself.segments = segmentsself.size = max((s["lv_start"] + s["count"]) * EXTENT_SIZE for s in segments)def read_at(self, offset, size):if offset >= self.size:return b""size = min(size, self.size - offset)out = bytearray()pos = offsetremaining = sizewhile remaining:for seg in self.segments:lo = seg["lv_start"] * EXTENT_SIZEhi = (seg["lv_start"] + seg["count"]) * EXTENT_SIZEif lo <= pos < hi:in_seg = pos - lon = min(remaining, hi - pos)pv_extent = seg["pv_start"] + in_seg // EXTENT_SIZErel = in_seg % EXTENT_SIZEn = min(n, EXTENT_SIZE - rel)image_name, part_start, pe_start = self.pvs[seg["pv"]]disk_offset = (part_start + pe_start) * 512 + pv_extent * EXTENT_SIZE + relout += self.images[image_name].read(disk_offset, n)pos += nremaining -= nbreakelse:n = min(remaining, 4096)out += b"\0" * npos += nremaining -= nreturn bytes(out)class MDFile:def __init__(self, components):self.components = componentsself.pos = 0self.size = min(c.size - MD_DATA_OFFSET for c in components) * len(components)def seek(self, offset, whence=0):if whence == 0:self.pos = offsetelif whence == 1:self.pos += offsetelif whence == 2:self.pos = self.size + offsetelse:raise ValueError(f"bad whence: {whence}")return self.posdef tell(self):return self.posdef read(self, size=-1):if size is None or size < 0:size = self.size - self.posdata = self.read_at(self.pos, size)self.pos += len(data)return datadef read_at(self, offset, size):if offset >= self.size:return b""size = min(size, self.size - offset)out = bytearray()pos = offsetremaining = sizendev = len(self.components)while remaining:stripe = pos // MD_CHUNK_SIZEdev = stripe % ndevchunk_index = stripe // ndevin_chunk = pos % MD_CHUNK_SIZEn = min(remaining, MD_CHUNK_SIZE - in_chunk)comp_offset = MD_DATA_OFFSET + chunk_index * MD_CHUNK_SIZE + in_chunkout += self.components[dev].read_at(comp_offset, n)pos += nremaining -= nreturn bytes(out)class Evidence:def __init__(self):self.images = {"sda": EWF("检材3-2.E01"),"sdb": EWF("检材3-1.E01"),}self.pvs = {"volum_pv0": ("sda", 8204288, 2048),"volum_pv1": ("sdb", 2048, 2048),"data_pv0": ("sda", 66797568, 2048),"data_pv1": ("sdb", 58593280, 2048),}root_lv = LV(self.images,self.pvs,[{"lv_start": 0, "count": 7152, "pv": "volum_pv0", "pv_start": 0},{"lv_start": 7152, "count": 7152, "pv": "volum_pv1", "pv_start": 0},],)data_lv = LV(self.images,self.pvs,[{"lv_start": 0, "count": 8207, "pv": "data_pv1", "pv_start": 0},{"lv_start": 8207, "count": 7205, "pv": "data_pv0", "pv_start": 0},],)self.btrfs = Btrfs(MDFile([data_lv, root_lv]))self.subvol = self.btrfs.find_subvolume("@rootfs")def close(self):for image in self.images.values():image.close()def node(self, path):return self.subvol.get(path)def read(self, path, max_bytes=None):fh = self.node(path).open()return fh.read() if max_bytes is None else fh.read(max_bytes)def text(self, path, max_bytes=None):return self.read(path, max_bytes).decode("utf-8", "replace")def listdir(self, path):for name, child in self.node(path).iterdir():if name in (".", ".."):continueyield name, childdef walk(self, path, max_depth=99):root = self.node(path)stack = [(path.rstrip("/") or "/", root, 0)]while stack:cur_path, node, depth = stack.pop()yield cur_path, nodeif depth >= max_depth or not node.is_dir():continuetry:children = list(node.iterdir())except Exception:continuefor name, child in reversed(children):if name in (".", ".."):continuechild_path = (cur_path.rstrip("/") + "/" + name) if cur_path != "/" else "/" + namestack.append((child_path, child, depth + 1))def cmd_cat(ev, args):sys.stdout.buffer.write(ev.read(args.path, args.bytes))def cmd_ls(ev, args):for name, child in ev.listdir(args.path):typ = "d" if child.is_dir() else "f" if child.is_file() else "l" if child.is_symlink() else "?"print(f"{typ} {child.size:>12} {child.mtime.isoformat()} {name}")def cmd_find(ev, args):regex = re.compile(args.pattern, re.I)for path, node in ev.walk(args.path, args.depth):if regex.search(path):typ = "d" if node.is_dir() else "f" if node.is_file() else "l" if node.is_symlink() else "?"print(f"{typ} {node.size:>12} {node.mtime.isoformat()} {path}")def cmd_grep(ev, args):regex = re.compile(args.pattern, re.I | re.S)for path, node in ev.walk(args.path, args.depth):if not node.is_file() or node.size > args.max_size:continuetry:data = node.open().read()text = data.decode("utf-8", "replace")except Exception:continueif regex.search(text):print(f"--- {path}")for line in text.splitlines():if re.search(args.pattern, line, re.I):print(line[:1000])def cmd_export(ev, args):node = ev.node(args.source)os.makedirs(os.path.dirname(os.path.abspath(args.dest)), exist_ok=True)with node.open() as src, open(args.dest, "wb") as dst:remaining = node.sizewhile remaining:chunk = src.read(min(args.chunk, remaining))if not chunk:breakdst.write(chunk)remaining -= len(chunk)print(args.dest)def main():sys.stdout.reconfigure(encoding="utf-8", errors="replace")parser = argparse.ArgumentParser()sub = parser.add_subparsers(dest="cmd", required=True)p = sub.add_parser("cat")p.add_argument("path")p.add_argument("--bytes", type=int)p.set_defaults(func=cmd_cat)p = sub.add_parser("ls")p.add_argument("path")p.set_defaults(func=cmd_ls)p = sub.add_parser("find")p.add_argument("path")p.add_argument("pattern")p.add_argument("--depth", type=int, default=99)p.set_defaults(func=cmd_find)p = sub.add_parser("grep")p.add_argument("path")p.add_argument("pattern")p.add_argument("--depth", type=int, default=99)p.add_argument("--max-size", type=int, default=2_000_000)p.set_defaults(func=cmd_grep)p = sub.add_parser("export")p.add_argument("source")p.add_argument("dest")p.add_argument("--chunk", type=int, default=8 * 1024 * 1024)p.set_defaults(func=cmd_export)ev = Evidence()try:args = parser.parse_args()args.func(ev, args)finally:ev.close()if __name__ == "__main__":main()

常用方式:

python .\forensics_btrfs.py cat /etc/fstab
python .\forensics_btrfs.py ls /var/www/html/maccms10
python .\forensics_btrfs.py grep /var/www/html/maccms10 "site_icp|site_url"
python .\forensics_btrfs.py export /media/zfs C:\Users\Alexander\AppData\Local\Temp\media_zfs.img

脚本2:ZFS 对象文件导出

zdb 可以列目录和数据块。下面脚本用于从 ZFS 对象号导出普通文件。

#!/usr/bin/env bash
set -euo pipefailPOOL_DIR="/mnt/c/Users/Alexander/AppData/Local/Temp"
POOL="db"
DATASET="db/tidb"extract_obj() {obj="$1"out="$2"size="$3": > "$out"/usr/sbin/zdb -e -p "$POOL_DIR" -dddddd "$DATASET" "$obj" |awk '$2=="L0" && $3 ~ /^0:/ {split($3,b,":");split($4,a,"L/");gsub("P","",a[2]);print b[1]":"b[2]":"a[1]"/"a[2]":dr"}' |while read -r spec; do/usr/sbin/zdb -e -p "$POOL_DIR" -R "$POOL" "$spec" 2>/dev/null >> "$out"donetruncate -s "$size" "$out"
}mkdir -p /tmp/mysql_mac2
extract_obj 87627 /tmp/mysql_mac2/mac_user.MYD 5907272
extract_obj 87626 /tmp/mysql_mac2/mac_user.MYI 1705984
extract_obj 87625 /tmp/mysql_mac2/mac_user_422.sdi 31223
extract_obj 98319 /tmp/mysql_mac2/binlog.000009 6651621
ls -lh /tmp/mysql_mac2

常用 zdb 查看目录:

zdb -e -p /mnt/c/Users/Alexander/AppData/Local/Temp -dddd db/tidb 34
zdb -e -p /mnt/c/Users/Alexander/AppData/Local/Temp -dddd db/tidb 59030
zdb -e -p /mnt/c/Users/Alexander/AppData/Local/Temp -dddd db/tidb 87726

脚本3:还原 binlog 并查询用户答案

需要 MySQL 8.0 工具:

apt-get install -y mysql-server-8.0

保存为 restore_mac_user.sh 后在 WSL root 下运行。

#!/usr/bin/env bash
set -euo pipefailDATADIR=/tmp/mysql_forensic_run
SOCK=/tmp/mysql_forensic.sock
BINLOG=/tmp/mysql_mac2/binlog.000009rm -rf "$DATADIR"
mkdir -p "$DATADIR"/usr/sbin/mysqld --initialize-insecure --datadir="$DATADIR" --user=root --log-error=/tmp/mysql_forensic_init.err
/usr/sbin/mysqld \--datadir="$DATADIR" \--socket="$SOCK" \--skip-networking \--pid-file=/tmp/mysql_forensic.pid \--log-error=/tmp/mysql_forensic.err \--user=root \--daemonizefor _ in $(seq 1 30); domysqladmin --protocol=SOCKET --socket="$SOCK" ping >/dev/null 2>&1 && breaksleep 1
donemysql --protocol=SOCKET --socket="$SOCK" -uroot <<'SQL'
DROP DATABASE IF EXISTS mac2;
CREATE DATABASE mac2 DEFAULT CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci;
USE mac2;
CREATE TABLE `mac_user` (`user_id` int unsigned NOT NULL AUTO_INCREMENT,`group_id` varchar(255) NOT NULL DEFAULT '0',`user_name` varchar(30) NOT NULL DEFAULT '',`user_pwd` varchar(32) NOT NULL DEFAULT '',`user_nick_name` varchar(30) NOT NULL DEFAULT '',`user_qq` varchar(16) NOT NULL DEFAULT '',`user_email` varchar(30) NOT NULL DEFAULT '',`user_phone` varchar(16) NOT NULL DEFAULT '',`user_status` tinyint unsigned NOT NULL DEFAULT '0',`user_portrait` varchar(100) NOT NULL DEFAULT '',`user_portrait_thumb` varchar(100) NOT NULL DEFAULT '',`user_openid_qq` varchar(40) NOT NULL DEFAULT '',`user_openid_weixin` varchar(40) NOT NULL DEFAULT '',`user_question` varchar(255) NOT NULL DEFAULT '',`user_answer` varchar(255) NOT NULL DEFAULT '',`user_points` int unsigned NOT NULL DEFAULT '0',`user_points_froze` int unsigned NOT NULL DEFAULT '0',`user_reg_time` int unsigned NOT NULL DEFAULT '0',`user_reg_ip` int unsigned NOT NULL DEFAULT '0',`user_login_time` int unsigned NOT NULL DEFAULT '0',`user_login_ip` int unsigned NOT NULL DEFAULT '0',`user_last_login_time` int unsigned NOT NULL DEFAULT '0',`user_last_login_ip` int unsigned NOT NULL DEFAULT '0',`user_login_num` smallint unsigned NOT NULL DEFAULT '0',`user_extend` smallint unsigned NOT NULL DEFAULT '0',`user_random` varchar(32) NOT NULL DEFAULT '',`user_end_time` int unsigned NOT NULL DEFAULT '0',`user_pid` int unsigned NOT NULL DEFAULT '0',`user_pid_2` int unsigned NOT NULL DEFAULT '0',`user_pid_3` int unsigned NOT NULL DEFAULT '0',PRIMARY KEY (`user_id`),KEY `type_id` (`group_id`),KEY `user_name` (`user_name`),KEY `user_reg_time` (`user_reg_time`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci;
SQLmysqlbinlog --stop-position=6650505 "$BINLOG" | mysql --protocol=SOCKET --socket="$SOCK" -urootmysql --protocol=SOCKET --socket="$SOCK" -uroot <<'SQL'
USE mac2;
SELECT COUNT(*) AS user_count FROM mac_user;SELECT DATE_FORMAT(FROM_UNIXTIME(user_reg_time),'%Y/%c/%e') AS reg_date, COUNT(*) AS cnt
FROM mac_user
GROUP BY reg_date
ORDER BY cnt DESC, reg_date ASC
LIMIT 10;SELECT user_id,user_name,INET_NTOA(user_last_login_ip) AS last_login_ip,user_last_login_time,FROM_UNIXTIME(user_last_login_time) AS last_login_time_text
FROM mac_user
WHERE user_name='Ma Hui Mei' OR user_nick_name='Ma Hui Mei';
SQLmysqladmin --protocol=SOCKET --socket="$SOCK" -uroot shutdown

脚本4:SM3 计算

$tmp = Join-Path $env:TEMP 'nginx_default_forensic.conf'
python .\forensics_btrfs.py export /etc/nginx/sites-available/default $tmp
wsl -u root bash -lc 'python3 - <<PY
import hashlib
p="/mnt/c/Users/Alexander/AppData/Local/Temp/nginx_default_forensic.conf"
print(hashlib.new("sm3", open(p,"rb").read()).hexdigest().upper())
PY'
Remove-Item -LiteralPath $tmp -Force

取证思路

检材给出两个 E01 镜像。磁盘不是普通单盘文件系统,而是多层组合:

  • E01 内部为 GPT 分区;
  • 多个分区组成 LVM PV;
  • LVM 中的两个 LV 再组成 mdadm RAID0;
  • RAID0 上是 Btrfs,根子卷为 @rootfs
  • /media/zfs 是一个 ZFS 池文件,里面放着 LXC 容器根文件系统;
  • 网站数据库位于 LXC 容器内的 MySQL/TiDB 相关数据中。

因此先写了一个 Python 读取器,直接从 E01 -> LVM -> md RAID0 -> Btrfs 读文件,避免破坏原始证据。

关键证据

服务器部分

1. 操作系统版本

读取:

python .\forensics_btrfs.py cat /usr/lib/os-release

关键字段:

PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
DEBIAN_VERSION_FULL=13.0

答案:13.0

2. 根分区 UUID

读取:

python .\forensics_btrfs.py cat /etc/fstab

关键内容:

UUID=3231e52f-5e15-44c4-b224-e29cb4201c0e / btrfs defaults,subvol=@rootfs 0 0

答案:3231e52f-5e15-44c4-b224-e29cb4201c0e

3. 最新 Docker 镜像创建时间

Docker 配置显示数据目录为 /data

python .\forensics_btrfs.py cat /etc/docker/daemon.json

然后查看镜像元数据:

python .\forensics_btrfs.py find /data/image/overlay2/imagedb/content/sha256 ".*\.json"
python .\forensics_btrfs.py cat /data/image/overlay2/imagedb/content/sha256/<image-json>

最新镜像 JSON 的 created 字段为:

2026-04-16T07:15:50.535713491Z

4. 根分区快照路径

Btrfs 子卷中发现:

@rootfs
@rootfs/root/history

在挂载后的根路径中对应:

/root/history

5. 后台管理入口文件名

网站目录为 /var/www/html/maccms10。入口文件中 user.php 定义了后台入口:

python .\forensics_btrfs.py cat /var/www/html/maccms10/user.php

答案:user.php

6-7. ICP 与主域名

读取站点配置:

python .\forensics_btrfs.py cat /var/www/html/maccms10/application/extra/maccms.php

关键字段:

'site_url' => 'www.2026fic.forensix',
'site_icp' => 'icp1919810',

答案:

icp1919810
2026fic.forensix

8. 分类3中视频的拼音

从 MacCMS 类型缓存/安装数据中定位 type_id = 3

python .\forensics_btrfs.py grep /var/www/html/maccms10 "type_id.*3|fenlei3"

关键字段:

type_id: 3
type_name: 分类3
type_mid: 1
type_en: fenlei3

答案:fenlei3

9. 站点设置页面前端模板源文件

运行缓存模板里包含源文件注释,定位到设置页模板:

python .\forensics_btrfs.py grep /var/www/html/maccms10/runtime/temp "site\[template_dir\]|view_new/system/config"

源文件为:

application/admin/view_new/system/config.html

10. 伪静态规则配置文件 SM3

实际 Nginx 站点配置为:

/etc/nginx/sites-available/default

导出后计算 SM3:

$tmp = Join-Path $env:TEMP 'nginx_default_forensic.conf'
python .\forensics_btrfs.py export /etc/nginx/sites-available/default $tmp
wsl -u root bash -lc 'python3 - <<PY
import hashlib
p="/mnt/c/Users/Alexander/AppData/Local/Temp/nginx_default_forensic.conf"
print(hashlib.new("sm3", open(p,"rb").read()).hexdigest().upper())
PY'

结果:

E73407468E6F52AF54C7B14632EEEB9BE25B05106D06C4C3085FC843C223793F

11. 数据库 IP

网站数据库配置:

python .\forensics_btrfs.py cat /var/www/html/maccms10/application/database.php

关键字段:

'hostname' => 'mytidb',
'database' => 'mac2',
'username' => 'aa',
'password' => '123456',
'hostport' => '3306',

再读 hosts:

python .\forensics_btrfs.py cat /etc/hosts

关键内容:

10.0.3.100 mytidb

答案:10.0.3.100

12. 数据库使用的容器技术

读取 LXC 配置:

python .\forensics_btrfs.py cat /var/lib/lxc/mytidb/config

关键字段:

lxc.rootfs.path = dir:/db/tidb
lxc.net.0.type = veth

答案:LXC

13. 4000 端口备份数据库版本

先导出 ZFS 池文件:

python .\forensics_btrfs.py export /media/zfs C:\Users\Alexander\AppData\Local\Temp\media_zfs.img

zdb 查看池和数据集:

zdb -e -p /mnt/c/Users/Alexander/AppData/Local/Temp db
zdb -e -p /mnt/c/Users/Alexander/AppData/Local/Temp -dddd db/tidb 34

在 LXC 根文件系统的 /root/.tiup/data/*/tiup_process_meta 中发现 TiUP playground 参数:

"/root/.tiup/components/playground/v1.16.5/tiup-playground",
"v7.5.0",
"--host",
"0.0.0.0"

TiDB 默认 SQL 端口为 4000,备份数据库版本为:

v7.5.0

14-15. 用户注册日期统计与马慧美最后登录 IP

LXC 容器内 MySQL 数据目录为:

/var/lib/mysql/mac2

关键文件:

mac_user.MYD
mac_user.MYI
mac_user_422.sdi
binlog.000009

直接挂 MyISAM 时索引状态不一致,因此采用 mysqlbinlog 还原 mac_user 行事件到临时 MySQL,再查询:

SELECT DATE_FORMAT(FROM_UNIXTIME(user_reg_time),'%Y/%c/%e') AS d, COUNT(*) AS c
FROM mac_user
GROUP BY d
ORDER BY c DESC, d ASC
LIMIT 10;SELECT user_id,user_name,INET_NTOA(user_last_login_ip),user_last_login_time,FROM_UNIXTIME(user_last_login_time)
FROM mac_user
WHERE user_name='Ma Hui Mei' OR user_nick_name='Ma Hui Mei';

结果:

2026/4/15  28933
2026/4/16  132324236  Ma Hui Mei  51.43.21.163  1776282701  2026-04-16 03:51:41

答案:

2026/4/15
51.43.21.163

16. 未被使用的文件系统

证据中使用了:

  • Btrfs:根文件系统;
  • LVM:磁盘卷管理;
  • ZFS:/media/zfs 池文件;
  • vfat:EFI 分区;
  • udf/iso9660:光驱配置。

未见 NTFS 使用痕迹,因此选:

A. ntfs  
C. xfs

17. 安装的数据库服务

主机上有 PostgreSQL 17:

python .\forensics_btrfs.py cat /var/lib/dpkg/status | Select-String -Pattern 'Package: postgresql' -Context 0,3

LXC 容器中有 MySQL 数据目录和 MySQL 8.0.45 SDI 元数据:

{"mysqld_version_id":80045}

TiUP playground 中有 TiDB:

v7.5.0

所以答案为:

A、C、D

最终答案

题号 答案
1 13.0
2 3231e52f-5e15-44c4-b224-e29cb4201c0e
3 2026-04-16T07:15:50.535713491Z
4 /root/history
5 user.php
6 icp1919810
7 www.2026fic.forensix
8 fenlei3
9 application/admin/view_new/system/config.html
10 E73407468E6F52AF54C7B14632EEEB9BE25B05106D06C4C3085FC843C223793F
11 10.0.3.100
12 LXC
13 v7.5.0
14 2026/4/15
15 51.43.21.163
16 A. ntfs C. xfs
17 A、C、D,即 mysql、tidb、postgresql
http://www.jsqmd.com/news/703798/

相关文章:

  • Bedrock Launcher:为Minecraft Bedrock版带来Java版启动器体验的革命性工具
  • VCSA 6.5证书过期连环坑:从重置密码到一键修复脚本的完整踩坑实录
  • java面试必问26:ThreadLocal 原理及场景:从源码到内存泄漏,一篇讲透
  • 终极WinAsar指南:三步告别命令行,轻松搞定Electron asar文件管理
  • MIT App Inventor完整指南:如何零基础快速开发Android和iOS应用
  • 莫氏鸡煲(3–4人份)
  • vue打包后在测试环境没问题,生产环境内容加载一半,接口请求不出来问题
  • 终极指南:IPXWrapper让Windows 11经典游戏重获联机能力
  • 统计容忍区间:概念、计算与Python实现
  • 别光刷LeetCode了!用ZJUT OJ这几道经典题,夯实你的C++基础与STL应用
  • 告别Docker?手把手教你为K8s v1.23配置Containerd容器运行时(附与Docker对比)
  • Poor Man‘s T-SQL Formatter:企业级SQL代码规范化的架构设计与工程实践
  • Space Thumbnails:革命性解决Windows资源管理器3D模型预览难题的智能方案
  • JDBC 从入门到入库:查询、插入、更新、删除操作
  • 从零到精通:3D打印切片软件Cura的终极入门指南
  • 从TensorFlow到BM1684:手把手教你将PyTorch模型部署到算能AI边缘盒子的完整流程
  • 如何快速搭建AI绘画训练环境?kohya_ss终极解决方案让你10分钟上手!
  • 视频转PPT终极指南:3分钟自动提取视频中的幻灯片内容
  • 苦瓜肉片
  • 如何快速清理电脑中的重复图片:AntiDupl.NET 智能去重工具完全指南
  • 2026年电池包检漏液公司实力推荐,测漏液/检漏液/中性检漏液/液冷板检漏液/无腐蚀检漏液 - 品牌策略师
  • F3D三维查看器:如何快速预览3D模型而不必等待?
  • Wan2.1功能体验:提示词增强功能让视频生成更简单
  • SELECT、FROM、WHERE
  • 新手必看:无需代码,用Ollama轻松玩转Llama-3.2-3B大模型
  • MusicPlayer2终极指南:打造完美本地音乐播放体验的完整解决方案
  • 从源码看门道:Android安全模式(Safe Mode)的触发逻辑与厂商定制化魔改
  • 第3篇:数据的运算——让数据动起来 python中文编程
  • 小红书数据采集架构设计:自动化与网络拦截的融合解决方案
  • 明日方舟自动化神器MAA:如何用智能助手彻底解放你的游戏时间