370rokas/disk-analysis
GitHub: 370rokas/disk-analysis
一款基于 The Sleuth Kit 构建的磁盘镜像分析与文件提取 CLI 工具,支持多种输出格式和 Lua 脚本自动化。
Stars: 1 | Forks: 0
# disk-analysis (da)
一个用于磁盘镜像分析和文件提取的终端工具。基于 [The Sleuth Kit (TSK)](https://www.sleuthkit.org/) 构建,支持人类可读格式、CSV 和 JSON 输出,并带有用于自动化工作流的 Lua 脚本引擎。
## 快速开始
```
# 列出磁盘镜像上的分区
disk-analysis disk.img ls
# 显示 partition 1 的文件树
disk-analysis disk.img tree 1
# 从 partition 1 提取文件
disk-analysis disk.img extract 1 /etc/passwd ./passwd.txt
# 对镜像运行 Lua 脚本
disk-analysis disk.img script ./scripts/largest_files.lua
```
## 用法
```
disk-analysis [options] [args]
```
`` 是磁盘镜像文件(`.img`、`.bin`、`.dd` 等)或物理设备节点的路径。它必须存在且可读。
### 全局选项
| Flag | Description |
|---|---|
| `-j, --json` | 以 JSON 格式输出 |
| `-c, --csv` | 以 CSV 格式输出 |
| `-l, --log ` | 将日志消息写入文件 |
| `--lc, --console` | 将日志消息打印到控制台 |
`--json` 和 `--csv` 是互斥的。默认输出为人类可读的文本。
### `ls` — 列出分区
```
disk-analysis [options] ls
```
列出镜像中找到的每个非元数据分区。如果没有检测到分区表(例如,格式化为单个文件系统的原始 U 盘),则整个磁盘将作为 ID 为 `0` 的单个条目显示。
**人类可读输出:**
```
ID Name Byte Offset Byte Length Has Filesystem Filesystem Type
1 Linux (0x83) 1048576 10737418240 Y ext4
2 Linux swap 10738466816 2147483648 N None
```
**JSON 输出** (`--json`):
```
[
{
"id": 1,
"name": "Linux (0x83)",
"byte_offset": 1048576,
"byte_length": 10737418240,
"has_filesystem": true,
"fs_type": "ext4"
},
{
"id": 2,
"name": "Linux swap",
"byte_offset": 10738466816,
"byte_length": 2147483648,
"has_filesystem": false,
"fs_type": "None"
}
]
```
**CSV 输出** (`--csv`):
```
id,name,byte_offset,byte_length,has_filesystem,fs_type
1,"Linux (0x83)",1048576,10737418240,true,"ext4"
2,"Linux swap",10738466816,2147483648,false,"None"
```
### `tree` — 显示文件系统树
```
disk-analysis [options] tree
```
递归遍历 `` 上的文件系统,并打印每个文件和目录。请使用 `ls` 获取的 ID 作为 ``。
未分配的条目、`.`、`..` 以及 NTFS 的 `$OrphanFiles` 将被排除。硬链接的 inode 只显示一次;后续的出现将显示为指向首次出现的链接。
**人类可读输出:**
```
/ (size: 0, dir)
bin (size: 4096, dir)
bash (size: 1234576, file)
ls (size: 147912, file)
etc (size: 4096, dir)
hostname (size: 12, file)
passwd (size: 2048, file)
usr (size: 4096, dir)
bin (size: 4096, dir) → [link: /bin]
```
**JSON 输出** (`--json`):
```
{
"name": "",
"size": 0,
"is_directory": true,
"children": [
{
"name": "etc",
"size": 4096,
"is_directory": true,
"children": [
{
"name": "passwd",
"size": 2048,
"is_directory": false
}
]
},
{
"name": "lib",
"size": 0,
"is_directory": true,
"is_link": true,
"link_target": "/usr/lib"
}
]
}
```
**CSV 输出** (`--csv`) — `name` 列包含完整路径:
```
name,size,is_directory,linksTo
"/etc",4096,true,null
"/etc/passwd",2048,false,null
"/lib",0,true,/usr/lib
```
### `extract` — 提取文件
```
disk-analysis extract
```
从 `` 中的 ``(文件系统内的绝对路径)提取单个文件,并将其写入主机上的 ``。输出格式标志对此子命令无效。
```
# 从 partition 1 提取 /etc/shadow
disk-analysis disk.img extract 1 /etc/shadow ./shadow.txt
# 从 NTFS 镜像中提取二进制文件
disk-analysis disk.img extract 2 /Windows/System32/cmd.exe ./cmd.exe
```
### `script` — 运行 Lua 脚本
```
disk-analysis script [settings]
```
对加载的镜像执行 Lua 脚本。可选的 `settings` 字符串将原样通过 `da.settings` 传递给脚本,可以携带任意配置(路径、标志等)。
```
# 运行脚本
disk-analysis disk.img script ./scripts/carve_images.lua
# 使用设置字符串运行脚本
disk-analysis disk.img script ./scripts/export.lua "out=/tmp/export,ext=jpg"
```
## Lua 脚本
脚本可以访问 `da` 全局表,该表公开了镜像和 TSK 文件系统 API。
### 全局:`da`
| Name | Type | Description |
|---|---|---|
| `da.image_path` | string | 在命令行传递的磁盘镜像路径 |
| `da.settings` | string | 在脚本路径之后传递的可选设置参数 |
| `da.list_partitions()` | function | 返回一个包含 `PartitionInfo` 对象的表(数组) |
| `da.open_fs(id)` | function | 打开分区 `id` 上的文件系统,返回一个 `FileSystem` |
| `da.log_info(msg)` | function | 在 INFO 级别记录日志 |
| `da.log_warn(msg)` | function | 在 WARN 级别记录日志 |
| `da.log_error(msg)` | function | 在 ERROR 级别记录日志 |
### `PartitionInfo`
字段为只读。通过 `da.list_partitions()` 获取实例。
| Field | Type | Description |
|---|---|---|
| `.id` | integer | 分区 ID(与 `da.open_fs` 和 `tree`/`extract` 配合使用) |
| `.name` | string | 分区表中对该分区的描述 |
| `.byte_offset` | integer | 分区的起始字节位置 |
| `.byte_length` | integer | 分区的大小(以字节为单位) |
| `.has_filesystem` | boolean | 是否找到了可识别的文件系统 |
| `.fs_type` | string | 文件系统类型名称(例如 `"ext4"`、`"ntfs"`、`"fat32"`) |
### `FileSystem`
通过 `da.open_fs(id)` 获取。
| Method | Returns | Description |
|---|---|---|
| `fs:root()` | `FSEntry` | 文件系统的根目录条目 |
| `fs:extract(src, dest)` | boolean | 将 `src`(绝对文件系统路径)处的文件提取到主机上的 `dest` |
### `FSEntry`
| Method | Returns | Description |
|---|---|---|
| `entry:name()` | string | 文件或目录名 |
| `entry:full_path()` | string | 文件系统内的绝对路径(例如 `/etc/passwd`) |
| `entry:size()` | integer | 大小(以字节为单位,目录为 0) |
| `entry:is_directory()` | boolean | |
| `entry:is_valid()` | boolean | 如果此条目是硬链接占位符,则为 `false` |
| `entry:is_link()` | boolean | 对于首次出现之后的硬链接 inode,为 `true` |
| `entry:link_target()` | string | 首次出现的路径(仅在 `is_link()` 为 true 时有意义) |
| `entry:load_all_descendants()` | — | 递归加载所有子项到内存中 |
| `entry:children()` | table | `{ name → FSEntry }` 直接子项的映射(触发延迟加载) |
### 示例脚本
```
-- Export all .jpg files from every partition to /tmp/export/
local partitions = da.list_partitions()
for _, p in ipairs(partitions) do
if not p.has_filesystem then goto continue end
da.log_info(string.format("Scanning partition %d (%s)", p.id, p.fs_type))
local ok, fs = pcall(da.open_fs, p.id)
if not ok then goto continue end
local root = fs:root()
root:load_all_descendants()
local function walk(entry)
if entry:is_link() or not entry:is_valid() then return end
if entry:is_directory() then
for _, child in pairs(entry:children()) do
walk(child)
end
elseif entry:name():match("%.jpg$") then
local dest = "/tmp/export" .. entry:full_path()
if fs:extract(entry:full_path(), dest) then
da.log_info("Exported: " .. entry:full_path())
else
da.log_warn("Failed: " .. entry:full_path())
end
end
end
walk(root)
::continue::
end
```
## 日志
日志默认关闭。使用任一标志启用它 — 两者可以同时使用:
```
# 记录到文件
disk-analysis -l analysis.log disk.img tree 1
# 记录到控制台
disk-analysis --console disk.img ls
# 同时记录到两者
disk-analysis -l analysis.log --console disk.img script carve.lua
```
## 前置条件
在构建之前,请确保已安装以下内容:
* **构建工具:** [CMake](https://cmake.org/download/) (3.10+) 和 C++20 编译器。
* **包管理器:** [VCPKG](https://learn.microsoft.com/en-us/vcpkg/get_started/overview)。
* **库:** **SleuthKit (TSK)**
* **Linux:** `sudo apt install libtsk-dev`
* **macOS:** `brew install sleuthkit`
* **Windows:** 请遵循[编译说明](https://github.com/sleuthkit/sleuthkit/blob/develop/INSTALL.txt)。
## 构建
```
# 1. Clone the repository
git clone https://github.com/370rokas/disk-analysis.git
cd disk-analysis
# 2. 安装 vcpkg 依赖
vcpkg install
# 3. 配置并构建
cmake -B build
cmake --build build
# 4. 运行
./build/disk-analysis --help
```
要在启用 AddressSanitizer 的情况下构建(仅限开发环境):
```
cmake -B build -DENABLE_ASAN=ON
cmake --build build
```
## 项目结构
```
src/
├── main.cpp # Entry point — parses CLI and dispatches to actions
├── defs.hpp # ActionType enum
├── core/ # TSK wrappers and domain types
│ ├── disk.hpp # Disk image (TSK_IMG_INFO)
│ ├── volume.hpp # Partition table (TSK_VS_INFO) + PartitionInfo
│ ├── filesystem.hpp # Filesystem handle (TSK_FS_INFO)
│ ├── fsEntry.hpp/cpp # File/directory entry with lazy child loading
│ ├── context.hpp # Global singleton — config, disk handle, inode map
│ └── logger.hpp # spdlog initialisation
├── actions/ # Business logic
│ ├── partitions.hpp/cpp # Partition listing and filesystem access
│ └── extract.hpp/cpp # File extraction (shared by CLI and Lua)
├── ui/cli/ # Command-line interface
│ ├── cli.hpp # CLI11 parser + CliConfig struct
│ └── wrappers.hpp # Output formatters for each subcommand
└── scripting/ # Lua integration
├── lua.hpp/cpp # sol2 bindings and script runner
scripts/ # Example Lua scripts
```
## TODO
- [X] 实现基本的 TSK 包装器。
- [X] 实现对不同输出格式的支持(人类可读、CSV、JSON)。
- [X] 实现 LUA 脚本支持(`da.list_partitions`、`da.open_fs`、`da.extract`、日志绑定)。
- [X] 实现基本子命令:
- [X] `ls`:列出分区。
- [X] `tree`:显示文件系统树。
- [X] `extract`:提取特定文件。
- [X] 创建文档和使用示例。
- [ ] 核心功能的自动化测试。
- [X] 自动化构建和发布。
- [ ] (想法):用于 AI 代理的 MCP 服务器。
- [ ] (想法):带有实时文件系统浏览的交互式 TUI。
- [ ] (想法):支持 Windows 注册表配置单元和其他非文件系统数据结构。
## 许可证
本项目基于 MIT 许可证授权 - 有关详细信息,请参阅 [LICENSE](LICENSE) 文件。
标签:Bash脚本, Linux安全, Lua, rizin, The Sleuth Kit, 数字取证, 数据提取, 磁盘分析, 自动化脚本