分析NTFS空闲空间与文件系统痕迹 是做什么的?
何时使用
- 在文件系统空闲空间中搜索隐藏或残留数据时
- 分析NTFS主文件表(MFT)条目以获取已删除文件元数据时
- 通过USN变更日志重构文件操作时
- 检测用于隐藏数据或恶意软件的备用数据流(ADS)时
- 在进行超出标准文件恢复范围的深度取证分析时
前提条件
- 具有NTFS文件系统的取证磁盘镜像
- Sleuth Kit工具包:istat, icat, fls, blkls, blkstat
- MFTECmd(Eric Zimmerman)用于MFT解析
- MFTExplorer用于交互式MFT分析
- 理解NTFS结构(MFT, $UsnJrnl, $LogFile, ADS)
- 安装了analyzeMFT或mft库的Python,用于自动解析
工作流程
步骤1:识别并提取NTFS文件系统痕迹
# 确定分区布局
mmls /cases/case-2024-001/images/evidence.dd
# 提取关键NTFS系统文件
# $MFT - 主文件表
icat -o 2048 /cases/case-2024-001/images/evidence.dd 0 > /cases/case-2024-001/ntfs/MFT
# $UsnJrnl:$J - USN变更日志
icat -o 2048 /cases/case-2024-001/images/evidence.dd 62-128 > /cases/case-2024-001/ntfs/UsnJrnl_J
# $LogFile - 事务日志
icat -o 2048 /cases/case-2024-001/images/evidence.dd 2 > /cases/case-2024-001/ntfs/LogFile
# 从卷中提取所有空闲空间
blkls -s -o 2048 /cases/case-2024-001/images/evidence.dd > /cases/case-2024-001/ntfs/slack_space.raw
# 获取文件系统信息
fsstat -o 2048 /cases/case-2024-001/images/evidence.dd | tee /cases/case-2024-001/ntfs/fs_info.txt
步骤2:分析主文件表(MFT)
# 使用MFTECmd解析MFT(Eric Zimmerman)
MFTECmd.exe -f "C:\cases\ntfs\MFT" --csv "C:\cases\analysis\" --csvf mft_analysis.csv
# 使用analyzeMFT进行解析(Python)
pip install analyzeMFT
analyzeMFT.py -f /cases/case-2024-001/ntfs/MFT \
-o /cases/case-2024-001/analysis/mft_analysis.csv \
-c
# 使用Python进行自定义MFT分析
python3 << 'PYEOF'
from mft import PyMft
import csv
mft = PyMft(open('/cases/case-2024-001/ntfs/MFT', 'rb').read())
deleted_files = []
suspicious_files = []
for entry in mft.entries():
if entry is None:
continue
filename = entry.get_filename()
if filename is None:
continue
is_deleted = not entry.is_active()
is_directory = entry.is_directory()
created = entry.get_created_timestamp()
modified = entry.get_modified_timestamp()
mft_modified = entry.get_mft_modified_timestamp()
size = entry.get_file_size()
# 标记已删除文件以待恢复
if is_deleted and not is_directory and size > 0:
deleted_files.append({
'filename': filename,
'size': size,
'created': str(created),
'modified': str(modified),
'entry_number': entry.entry_number
})
# 检测时间戳篡改(MFT修改时间 != $SI修改时间)
si_modified = entry.get_si_modified_timestamp()
fn_modified = entry.get_fn_modified_timestamp()
if si_modified and fn_modified:
if abs((si_modified - fn_modified).total_seconds()) > 86400: # >1天差异
suspicious_files.append({
'filename': filename,
'si_modified': str(si_modified),
'fn_modified': str(fn_modified),
'delta': str(si_modified - fn_modified)
})
print(f"=== 已删除文件(可恢复元数据) ===")
print(f"总数: {len(deleted_files)}")
for f in deleted_files[:20]:
print(f" [{f['modified']}] {f['filename']} ({f['size']} 字节)")
print(f"\n=== 潜在的时间戳篡改 ===")
print(f"可疑总数: {len(suspicious_files)}")
for f in suspicious_files[:10]:
print(f" {f['filename']}: $SI={f['si_modified']}, $FN={f['fn_modified']} (差异: {f['delta']})")
PYEOF
步骤3:分析空闲空间中的隐藏数据
# 在空闲空间中搜索字符串
strings -a /cases/case-2024-001/ntfs/slack_space.raw > /cases/case-2024-001/analysis/slack_strings.txt
# 在空闲空间中搜索特定模式
grep -iab "password\|secret\|confidential\|credit.card\|ssn" \
/cases/case-2024-001/ntfs/slack_space.raw > /cases/case-2024-001/analysis/slack_keywords.txt
# 分析单个文件的空闲空间
python3 << 'PYEOF'
import struct
# 文件空闲空间包括:
# 1. RAM空闲空间:文件结束到下一个扇区边界之间的字节(可能填充RAM内容或零)
# 2. 驱动器空闲空间:最后一个文件扇区之后簇中剩余的扇区
# 分析特定MFT条目的空闲空间
# 使用Sleuth Kit获取特定文件的空闲空间
import subprocess
# 获取文件详情
result = subprocess.run(
['istat', '-o', '2048', '/cases/case-2024-001/images/evidence.dd', '14523'],
capture_output=True, text=True
)
print(result.stdout)
# 输出显示数据运行 - 最后一个簇可能包含空闲数据
# 计算空闲空间大小:(分配大小 - 文件大小) 字节
PYEOF
# 在空闲空间中搜索文件签名(嵌入文件)
foremost -t jpg,pdf,zip -i /cases/case-2024-001/ntfs/slack_space.raw \
-o /cases/case-2024-001/carved/slack_carved/
# 使用bulk_extractor在空闲空间中查找结构化数据
bulk_extractor -o /cases/case-2024-001/analysis/bulk_extract/ \
/cases/case-2024-001/ntfs/slack_space.raw
步骤4:解析USN变更日志
# 使用MFTECmd解析USN日志
MFTECmd.exe -f "C:\cases\ntfs\UsnJrnl_J" --csv "C:\cases\analysis\" --csvf usn_journal.csv
# Python USN日志解析
pip install pyusn
python3 << 'PYEOF'
import struct
import csv
from datetime import datetime, timedelta
def parse_usn_record(data, offset):
"""解析单个USN_RECORD_V2记录。"""
if offset + 8 > len(data):
return None, offset
record_len = struct.unpack_from('<I', data, offset)[0]
if record_len < 56 or record_len > 65536 or offset + record_len > len(data):
return None, offset + 8
major_ver = struct.unpack_from('<H', data, offset + 4)[0]
if major_ver != 2:
return None, offset + record_len
mft_ref = struct.unpack_from('<Q', data, offset + 8)[0] & 0xFFFFFFFFFFFF
parent_ref = struct.unpack_from('<Q', data, offset + 16)[0] & 0xFFFFFFFFFFFF
usn = struct.unpack_from('<Q', data, offset + 24)[0]
timestamp = struct.unpack_from('<Q', data, offset + 32)[0]
reason = struct.unpack_from('<I', data, offset + 40)[0]
source_info = struct.unpack_from('<I', data, offset + 44)[0]
security_id = struct.unpack_from('<I', data, offset + 48)[0]
file_attrs = struct.unpack_from('<I', data, offset + 52)[0]
filename_len = struct.unpack_from('<H', data, offset + 56)[0]
filename_off = struct.unpack_from('<H', data, offset + 58)[0]
name = data[offset + filename_off:offset + filename_off + filename_len].decode('utf-16-le', errors='ignore')
# 将Windows FILETIME转换为datetime
ts = datetime(1601, 1, 1) + timedelta(microseconds=timestamp // 10)
# 解码原因标志
reasons = []
reason_flags = {
0x01: 'DATA_OVERWRITE', 0x02: 'DATA_EXTEND', 0x04: 'DATA_TRUNCATION',
0x10: 'NAMED_DATA_OVERWRITE', 0x20: 'NAMED_DATA_EXTEND',
0x100: 'FILE_CREATE', 0x200: 'FILE_DELETE', 0x400: 'EA_CHANGE',
0x800: 'SECURITY_CHANGE', 0x1000: 'RENAME_OLD_NAME', 0x2000: 'RENAME_NEW_NAME',
0x4000: 'INDEXABLE_CHANGE', 0x8000: 'BASIC_INFO_CHANGE',
0x10000: 'HARD_LINK_CHANGE', 0x20000: 'COMPRESSION_CHANGE',
0x40000: 'ENCRYPTION_CHANGE', 0x80000: 'OBJECT_ID_CHANGE',
0x100000: 'REPARSE_POINT_CHANGE', 0x200000: 'STREAM_CHANGE',
0x80000000: 'CLOSE'
}
for flag, desc in reason_flags.items():
if reason & flag:
reasons.append(desc)
record = {
'timestamp': ts.strftime('%Y-%m-%d %H:%M:%S'),
'filename': name,
'mft_entry': mft_ref,
'parent_entry': parent_ref,
'reasons': '|'.join(reasons),
'usn': usn
}
return record, offset + record_len
# 解析日志
with open('/cases/case-2024-001/ntfs/UsnJrnl_J', 'rb') as f:
data = f.read()
records = []
offset = 0
while offset < len(data) - 8:
record, offset = parse_usn_record(data, offset)
if record:
records.append(record)
else:
offset += 8 # 跳过零值
# 过滤删除事件
deletions = [r for r in records if 'FILE_DELETE' in r['reasons']]
creations = [r for r in records if 'FILE_CREATE' in r['reasons']]
renames = [r for r in records if 'RENAME_NEW_NAME' in r['reasons']]
print(f"USN记录总数: {len(records)}")
print(f"文件创建: {len(creations)}")
print(f"文件删除: {len(deletions)}")
print(f"文件重命名: {len(renames)}")
print("\n=== 最近的删除操作 ===")
for r in deletions[-20:]:
print(f" [{r['timestamp']}] 删除: {r['filename']} (MFT#{r['mft_entry']})")
# 将完整日志写入CSV
with open('/cases/case-2024-001/analysis/usn_journal.csv', 'w', newline='') as f:
writer = csv.DictWriter(f, fieldnames=['timestamp', 'filename', 'mft_entry', 'parent_entry', 'reasons', 'usn'])
writer.writeheader()
writer.writerows(records)
PYEOF
步骤5:检测并分析备用数据流(ADS)
# 列出镜像中的所有备用数据流
find /mnt/evidence -exec getfattr -d {} \; 2>/dev/null | grep -i "ads\|zone\|stream"
# 使用Sleuth Kit查找ADS
fls -r -o 2048 /cases/case-2024-001/images/evidence.dd | grep ":" | \
tee /cases/case-2024-001/analysis/ads_list.txt
# 提取特定ADS内容
# 格式: icat 镜像 inode:ads_name
icat -o 2048 /cases/case-2024-001/images/evidence.dd 14523:hidden_stream \
> /cases/case-2024-001/analysis/extracted_ads.bin
# 检查Zone.Identifier流(下载来源追踪)
fls -r -o 2048 /cases/case-2024-001/images/evidence.dd | grep "Zone.Identifier" | \
while read line; do
inode=$(echo "$line" | awk '{print $2}' | tr -d ':')
echo "=== $line ==="
icat -o 2048 /cases/case-2024-001/images/evidence.dd "${inode}:Zone.Identifier" 2>/dev/null
echo ""
done > /cases/case-2024-001/analysis/zone_identifiers.txt
# Zone.Identifier内容揭示:
# [ZoneTransfer]
# ZoneId=3 (3 = 互联网,表示文件是从互联网下载的)
# ReferrerUrl=https://malicious-site.com/payload.exe
# HostUrl=https://cdn.malicious-site.com/payload.exe
关键概念
| 概念 | 描述 |
|---|---|
| 文件空闲空间 | 文件结尾到簇边界之间的未使用空间,包含残留数据 |
| RAM空闲空间 | 从文件结尾到扇区边界之间的空闲空间部分(历史上填充了RAM内容) |
| MFT ($MFT) | 主文件表 - 包含每个文件条目的NTFS元数据数据库 |
| USN日志 ($UsnJrnl) | 记录NTFS上所有文件/目录修改的变更日志 |
| 备用数据流 | NTFS功能,允许每个文件拥有多个数据流(隐藏存储) |
| $STANDARD_INFORMATION | MFT属性,其时间戳可由用户模式应用程序修改 |
| $FILE_NAME | MFT属性,其时间戳仅可由内核修改 |
| 时间戳篡改 | 修改文件时间戳以逃避检测的反取证技术 |
工具与系统
| 工具 | 用途 |
|---|---|
| MFTECmd | Eric Zimmerman的MFT和USN日志解析器,支持CSV输出 |
| MFTExplorer | 用于MFT分析的交互式GUI工具 |
| analyzeMFT | Python MFT解析器,支持CSV/JSON输出 |
| The Sleuth Kit | 文件系统取证工具包(fls, icat, blkls, istat) |
| bulk_extractor | 从原始数据(包括空闲空间)中提取特征 |
| NTFS Log Tracker | 用于解析$LogFile事务记录的工具 |
| streams.exe | Sysinternals工具,用于列出NTFS备用数据流 |
| Plaso | 解析MFT和USN日志的超时间线工具 |
常见场景
场景1:通过时间戳篡改检测反取证行为 比较MFT条目中的$STANDARD_INFORMATION时间戳与$FILE_NAME时间戳,标记$SI时间戳早于$FN时间戳的文件(在正常操作中不可能),识别被篡改时间戳的文件作为故意操纵的证据,并与其它时间线证据关联。
场景2:备用数据流中的隐藏数据 扫描文件上除标准Zone.Identifier之外的ADS,提取ADS内容进行分析,检查存储在ADS中的隐藏可执行文件或文档,将ADS创建与用户活动时间线关联,记录发现作为证据。
场景3:从MFT重建已删除文件 解析MFT中未激活(已删除)的条目,提取已删除文件的文件名、大小和时间戳,如果数据簇未被覆盖,使用icat恢复文件内容,建立已删除证据文件列表,与USN日志删除事件关联。
场景4:从USN日志重构文件活动 解析调查期间的USN变更日志,识别文件创建、修改、重命名和删除事件,重构文件操作序列,检测数据暂存证据(创建、复制、压缩、删除模式),识别反取证文件擦除行为。
输出格式
文件系统痕迹分析:
卷: NTFS (分区2, 465 GB)
簇大小: 4096 字节
MFT分析:
条目总数: 456,789
活动文件: 234,567
已删除条目: 12,345 (其中8,901个具有可恢复元数据)
时间戳被篡改文件: 23 (检测到$SI/$FN不匹配)
USN日志:
解析记录数: 2,345,678
日期范围: 2024-01-01 至 2024-01-20
文件创建: 45,678
文件删除: 23,456
文件重命名: 12,345
备用数据流:
发现ADS总数: 1,234
Zone.Identifier: 890 (已下载文件)
自定义/可疑ADS: 5 (检测到隐藏数据)
空闲空间:
空闲空间总量: 12.3 GB
关键词命中数: 45 (密码, 信用卡)
从空闲空间刻录文件: 23
可疑发现:
- 23个文件时间戳被篡改
- 5个文件具有包含数据的隐藏ADS
- USN显示2024-01-18发生批量删除(反取证行为)
…
[翻译截断]