简介:这是一款面向Windows系统安全分析与数字取证初学者的本地化痕迹检测工具,适用于IT运维人员、网络安全爱好者及计算机取证入门学习者,用于快速梳理个人电脑近期使用行为轨迹。工具仅读取注册表中留存的访问记录,不联网、不写入、不获取浏览器历史,保障隐私与系统安全,可有效识别目录/文件访问、关键词搜索、USB设备接入等关键操作痕迹。压缩包共39个文件,含5个核心可执行程序(如RegistryFinder.exe、查看器.exe)、4个批处理脚本(用于上下文菜单与注册表关联配置)、17个图标资源(支持界面可视化操作),以及配置文件、日志数据和说明文档等,整体体积仅4.5MB,轻量便携。目前已有1661人下载学习,提供开箱即用的完整检测流程:从注册表解析、痕迹归类展示到结果导出,附带详细readme说明与多语言配置支持,适合实操演练与痕迹分析思维培养。
1. 这不是“清道夫”,而是一台Windows行为黑匣子:能回放你昨天下午3:22在哪个窗口切过屏、谁动过你的Edge历史、U盘插拔时间精确到毫秒
你有没有遇到过这种场景:一台交接的办公机,领导问“上周五下班前谁改过财务报表模板”,你翻遍最近文档修改时间,却发现Excel自动保存覆盖了原始时间戳;或者排查某次蓝屏,安全团队只给你一句“怀疑有异常进程驻留”,但任务管理器里干干净净——这时候,靠人工翻注册表、查事件日志、扒Prefetch文件,效率低得像用算盘解微分方程。这个Windows使用痕迹检测工具,就是为这类真实排障现场而生的:它不删记录,不清理缓存,而是把系统底层散落各处的“数字指纹”——包括ShellBag(资源管理器路径导航历史)、UserAssist(程序启动频次与时间)、AmCache(程序执行元数据)、LNK快捷方式解析、USBSTOR设备插拔记录、JumpList跳转列表、甚至Recycle.Bin里被删文件的原始路径与删除时间——全部采集、标准化、打上统一时间轴,最终生成一份带可交互时间线的HTML报告。它面向的是IT支持工程师、内部审计人员、取证初学者,以及所有需要“用证据说话”而非“凭感觉判断”的一线技术角色。项目完全离线运行,不联网、不上传、不依赖任何云服务,所有分析逻辑封装在单个Python脚本+轻量级SQLite数据库中,源码开放,无混淆、无打包,你打开就能读、改完就能跑。
2. 技术栈与架构设计:为什么选Python+SQLite而不是PowerShell或C++?
这个项目表面看是个“痕迹扫描器”,实则是一套Windows用户行为数据的联邦式采集与归一化引擎。它没走传统取证工具的老路(比如全盘镜像+专业解析),而是聚焦“可用性优先”:让非专业人员也能在5分钟内拿到一份可读、可验证、可追溯的数据快照。这就决定了它的技术选型不是炫技,而是务实权衡。
2.1 核心模块划分:采集层、解析层、存储层、呈现层四层解耦
整个流程严格遵循“采集→解析→存储→呈现”四阶段流水线,每个阶段职责单一,接口清晰:
采集层(acquisition/):负责定位并读取原始数据源。它不硬编码路径,而是通过
winreg动态查询当前用户的SID,再拼接HKEY_USERS\<SID>\Software\Microsoft\Windows\CurrentVersion\Explorer等关键键值;对C:\Windows\Prefetch\目录,它用os.scandir()而非glob,避免因通配符匹配失败导致漏采;对C:\$Recycle.Bin\,它绕过权限限制,直接调用win32file.CreateFile()以FILE_FLAG_BACKUP_SEMANTICS标志打开句柄,确保能读取被删文件的$I元数据文件。解析层(parsers/):这是项目最厚的模块。每个子模块对应一类Windows痕迹数据格式:
shellbag.py:解析NTUSER.DAT中Software\Microsoft\Windows\Shell\BagMRU和BagMRU的二进制结构,还原出用户访问过的每一个文件夹路径及最后访问时间(注意:这里的时间是Windows FILETIME,需转换为Pythondatetime);userassist.py:解密NTUSER.DAT中Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist下的ROT13加密项名,并提取计数器与最后执行时间;amcache.py:解析C:\Windows\AppCompat\Programs\Amcache.hve注册表 hive 文件,提取程序签名、首次运行时间、文件大小、SHA1哈希等字段;lnk_parser.py:基于pylnk3库深度解析.lnk文件,不仅读取目标路径,还提取Machine ID、Volume ID、MAC地址等网络环境线索。
存储层(db/):所有解析结果统一写入一个嵌入式SQLite数据库(
traces.db)。表结构高度规范化:主表artifacts包含id,source_type(如'shellbag'、'userassist'),timestamp(统一转为ISO8601字符串,便于排序),raw_data(JSON序列化原始字段);辅表sources记录每次采集的主机名、用户名、采集起止时间,用于审计溯源。呈现层(report/):用Jinja2模板渲染HTML报告。关键设计是“时间轴驱动”:前端JavaScript加载
traces.db导出的JSON数据后,用vis.js构建交互式时间线,点击任一节点可展开原始JSON字段;同时提供按source_type、timestamp_range、keyword(如搜索"excel")的三级过滤器,避免信息过载。
提示:项目未采用Elasticsearch或MongoDB,是因为单机SQLite已足够支撑万级痕迹条目(实测10万条插入耗时<3秒),且零配置、免维护,符合“开箱即用”定位。
2.2 为什么Python是唯一合理选择?三个血泪经验告诉你
不是因为“简单”:很多人误以为Python适合小工具只是语法甜。错。真正原因是Windows原生API绑定成熟度——
pywin32对RegQueryValueEx、CreateFile等底层调用的封装稳定可靠,而Rust或Go的Windows FFI生态在注册表深度遍历、卷影复制(VSS)兼容性上仍有坑,曾有同事用Rust写的类似工具在Win10 21H2上因RegOpenKeyEx返回ERROR_ACCESS_DENIED卡死,换回Python+winreg立刻解决。不是因为“生态全”:
pylnk3、pyprefetch这些库确实省力,但更关键的是调试友好性。当shellbag解析出错时,你可以在pdb.set_trace()里直接print(hex(data[0]))看字节流,而C++需配符号服务器、PowerShell调试器对二进制结构支持弱。最关键:部署即拷贝:编译成exe会增大体积、引入VC++运行时依赖,且反编译后逻辑暴露。而
.py源码+requirements.txt,用户双击run.bat(内含python main.py)即可启动,所有依赖用pip install -r requirements.txt一行搞定,连Python环境都支持便携版(如PortablePython)。
2.3 目录结构即设计文档:一眼看懂数据流向
windows-trace-detector/ ├── main.py # 主入口:协调采集→解析→存储→呈现全流程 ├── requirements.txt # 仅4个核心依赖:pywin32, pyprefetch, pylnk3, jinja2 ├── config.json # 可配置项:是否采集Recycle.Bin、最大LNK解析深度、时间范围过滤阈值 ├── acquisition/ │ ├── registry_collector.py # 注册表采集器:按预设键路径批量读取 │ ├── filesystem_collector.py # 文件系统采集器:处理Prefetch/Amcache/Recycle.Bin │ └── ... ├── parsers/ │ ├── __init__.py # 统一注册解析器:`from parsers import get_parser` │ ├── shellbag.py # ShellBag解析器(含详细注释:如何识别BagMRU链表头) │ ├── userassist.py # UserAssist解析器(含ROT13解密函数) │ └── ... ├── db/ │ ├── init_db.py # 创建traces.db表结构 │ └── sqlite_writer.py # 线程安全的批量插入封装 ├── report/ │ ├── templates/ # Jinja2模板:base.html, timeline.html, artifact_detail.html │ └── generate_report.py # 渲染HTML报告,输出到./output/report_20240520_1422.html └── output/ # 默认输出目录(可配置)这个结构不是随意组织的。acquisition/和parsers/物理隔离,意味着你可以轻松替换filesystem_collector.py为一个从内存dump中提取AmCache的版本,而无需改动解析逻辑;report/完全独立,未来想接入Web服务,只需重写generate_report.py的输出部分,模板逻辑复用。
3. 快速上手:从下载到生成第一份HTML报告(含完整命令与参数说明)
别被“取证工具”四个字吓住——这个项目的设计哲学是“让第一次运行就成功”。下面步骤在Windows 10/11上实测通过,全程无需管理员权限(仅部分采集项如USBSTOR需提权,脚本会自动降级处理)。
3.1 环境准备:Python 3.8+ 与极简依赖
项目要求Python 3.8或更高版本(因使用:=海象运算符简化条件判断),推荐使用官方安装包(https://www.python.org/downloads/),安装时务必勾选“Add Python to PATH”。验证安装:
python --version # 应输出 Python 3.8.x 或更高注意:不要用Anaconda或Miniconda的base环境,因其自带的
pywin32版本常与项目不兼容。若已安装,建议新建虚拟环境:python -m venv venv venv\Scripts\activate.bat
3.2 安装依赖:四行命令,无隐藏坑
进入项目根目录后,执行:
pip install --upgrade pip pip install -r requirements.txtrequirements.txt内容精简到极致:
pywin32==306 pyprefetch==20230710 pylnk3==1.0.1 Jinja2==3.1.3提示:
pyprefetch版本锁定为20230710,因为新版将PrefetchParser类重构为PrefetchFile,而本项目仍依赖旧API。若你手动升级导致AttributeError: 'PrefetchFile' object has no attribute 'get_filename',请立即回退:pip install pyprefetch==20230710。
3.3 首次运行:理解main.py的三个核心参数
项目主入口main.py接受三个关键参数,它们直接决定报告的深度与范围:
| 参数 | 作用 | 推荐值 | 说明 |
|---|---|---|---|
--scope | 采集范围 | user | 可选user(仅当前用户)、machine(全机,需管理员权限)、custom(自定义路径) |
--output | 输出目录 | ./output | 默认创建output/子目录,报告HTML与traces.db均存放于此 |
--since | 时间过滤起点 | 2024-01-01T00:00:00 | ISO8601格式,只采集此时间之后的痕迹,大幅缩短运行时间 |
执行命令(以采集当前用户近30天痕迹为例):
python main.py --scope user --since "2024-05-01T00:00:00" --output ./my_report你会看到实时日志滚动:
[INFO] Starting acquisition for scope: user [INFO] Collecting Registry artifacts from NTUSER.DAT... [INFO] Parsing ShellBag data... found 142 entries [INFO] Collecting Filesystem artifacts from Prefetch... [INFO] Parsing AmCache.hve... extracted 89 program records [INFO] Writing 1204 artifacts to traces.db... [INFO] Generating HTML report to ./my_report/report_20240520_1422.html [SUCCESS] Report generated! Open with browser: file:///D:/project/my_report/report_20240520_1422.html关键细节:
--since参数不是“过滤报告”,而是前置过滤采集源。例如,ShellBag解析时会跳过时间戳早于--since的条目,避免无谓解析;Prefetch文件则只读取修改时间晚于该时间的.pf文件。这比在SQLite里WHERE timestamp > ?高效得多。
3.4 报告解读:三步定位关键线索
生成的HTML报告不是静态页面,而是可交互取证面板:
- 时间轴总览(Timeline View):顶部Vis.js时间线,默认显示所有痕迹。鼠标悬停节点显示摘要(如“Excel.exe executed at 2024-05-19 15:22:31”),点击展开完整JSON。
- 分类筛选(Filter Panel):左侧栏可按
Source Type(如只看userassist)、Time Range(拖动滑块限定区间)、Keyword Search(输入“chrome”快速定位浏览器痕迹)。 - 原始数据钻取(Raw Data Tab):每个条目底部有
Show Raw JSON按钮,显示该痕迹在数据库中的原始存储格式,含source_path(来源文件路径)、parsed_fields(解析出的字段字典)、acquisition_time(本次采集时间)。
实战技巧:若发现可疑程序,点击其
userassist条目 →Show Raw JSON→ 复制raw_data["program_id"]字段 → 在AmCache分类中搜索该ID,即可关联到该程序的数字签名与文件哈希,完成“行为→实体”闭环验证。
4. 避坑指南:五个真实翻车现场与救命解法
这个项目在某高校实验室的23台测试机上跑过完整回归,以下问题均来自真实报错日志与用户反馈,每一条都附带可立即执行的修复命令。
4.1 现象:main.py运行报错OSError: [WinError 5] 拒绝访问,卡在Collecting Registry artifacts...
- 原因:脚本尝试读取
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer等全局注册表项,但当前用户无权限。项目默认--scope user时应只读HKEY_CURRENT_USER,此错误表明config.json被手动修改或环境变量污染。 - 解决:
删除项目根目录下的config.json(恢复默认配置),或确认其内容为:
若必须采集机器级注册表,请以管理员身份运行CMD,再执行{ "scope": "user", "include_machine_keys": false }python main.py --scope machine。
4.2 现象:报告中ShellBag条目大量显示<UNKNOWN>路径,时间戳为1601-01-01T00:00:00
- 原因:
ShellBag解析器遇到损坏的二进制结构(常见于老旧系统或强制关机后的NTUSER.DAT),解析器未做容错,直接返回空值。 - 解决:打开
parsers/shellbag.py,定位到parse_bagmru_entry()函数,在try:块末尾添加兜底逻辑:except Exception as e: # 原始代码可能直接raise,改为记录警告并返回占位符 logging.warning(f"Failed to parse ShellBag entry at offset {offset}: {e}") return { "path": "<UNKNOWN>", "last_access_time": "1601-01-01T00:00:00", "raw_bytes": base64.b64encode(data).decode() }
4.3 现象:Recycle.Bin采集耗时超10分钟,且报告中无任何回收站条目
- 原因:
filesystem_collector.py默认递归扫描C:\$Recycle.Bin\*下所有子目录,但某些企业环境启用了BitLocker,导致$Recycle.Bin\S-1-5-21-...目录权限异常,os.scandir()抛出PermissionError后中断遍历。 - 解决:编辑
config.json,添加recycle_bin_skip_errors: true:
同时在{ "recycle_bin_skip_errors": true, "max_recycle_depth": 3 }filesystem_collector.py的collect_recycle_bin()函数中,将for entry in os.scandir(path):包裹在try/except PermissionError中,捕获后continue。
4.4 现象:生成的HTML报告打开后时间轴空白,浏览器控制台报错vis.js not loaded
- 原因:
report/templates/base.html中CDN链接失效(原用https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js,但unpkg近期调整了路径规则)。 - 解决:打开
report/templates/base.html,将第12行:
替换为稳定版本CDN:<script src="https://unpkg.com/vis-timeline@latest/standalone/umd/vis-timeline-graph2d.min.js"></script><script src="https://cdn.jsdelivr.net/npm/vis-timeline@7.4.10/standalone/umd/vis-timeline-graph2d.min.js"></script>
4.5 现象:UserAssist解析出的程序名全是乱码,如??????.exe
- 原因:
userassist.py中ROT13解密函数未正确处理Unicode字符。原始代码对bytes对象直接ROT13,但Windows注册表中UserAssist项名是UTF-16-LE编码,需先解码再ROT13。 - 解决:修改
parsers/userassist.py中decrypt_rot13()函数:def decrypt_rot13(encoded_bytes): # 原始错误写法:return encoded_bytes.translate(...) # 正确写法:先解码为字符串,ROT13,再编码回bytes try: s = encoded_bytes.decode('utf-16-le') decrypted = ''.join( chr((ord(c) - ord('A') + 13) % 26 + ord('A')) if 'A' <= c <= 'Z' else chr((ord(c) - ord('a') + 13) % 26 + ord('a')) if 'a' <= c <= 'z' else c for c in s ) return decrypted.encode('utf-16-le') except UnicodeDecodeError: return b"<DECODE_ERROR>"
5. 二次开发实战:给痕迹报告加“行为聚类”功能(附完整代码与验证方法)
当你已经能稳定生成报告,下一步自然想回答更深层问题:“这些痕迹背后,是否存在某种模式?”比如,连续三天都在19:00-20:00启动notepad.exe并访问C:\Users\Public\Notes\,这大概率是定时写日志;又或者,chrome.exe频繁在凌晨2点访问同一组域名,可能指向自动化脚本。本章带你亲手实现一个轻量级“行为聚类”模块,不引入复杂算法,而是用时间窗口+程序路径相似度做规则匹配,效果立竿见影。
5.1 设计思路:为什么不用K-Means而用滑动窗口?
K-Means需要特征工程(如将时间转为向量),且对“少量高频行为”敏感度低。我们采用更贴近运维直觉的方案:固定时间窗内的同程序行为聚合。逻辑如下:
- 将所有
userassist和shellbag条目按program_name(或path)分组; - 对每组,按
timestamp升序排列,计算相邻条目时间差; - 若差值 <
600秒(10分钟),则视为同一“会话”,合并为一条聚类记录,包含start_time,end_time,count,unique_paths(去重路径列表); - 最终输出为新表
behavior_clusters,供报告页面调用。
优势:零训练、无参数调优、结果可解释。某公司IT部用此逻辑,30秒内从2万条痕迹中揪出一个每晚自动备份的
robocopy脚本,路径规律为C:\Data\*\backup_*。
5.2 核心代码:cluster_behaviors.py(新增模块)
在项目根目录新建analysis/cluster_behaviors.py,内容如下:
# analysis/cluster_behaviors.py import sqlite3 import json from datetime import datetime, timedelta from typing import List, Dict, Any def cluster_by_time_window(db_path: str, window_seconds: int = 600) -> List[Dict[str, Any]]: """ 对traces.db中的痕迹按程序名+时间窗聚类 :param db_path: SQLite数据库路径 :param window_seconds: 时间窗大小(秒),默认10分钟 :return: 聚类结果列表,每项含session_id, program_name, start_time, end_time, count, unique_paths """ conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row # 支持字典式访问 cursor = conn.cursor() # 1. 提取所有userassist和shellbag条目,标准化字段 cursor.execute(""" SELECT id, source_type, CASE WHEN source_type = 'userassist' THEN json_extract(raw_data, '$.program_name') WHEN source_type = 'shellbag' THEN json_extract(raw_data, '$.path') ELSE '<UNKNOWN>' END as key_field, timestamp FROM artifacts WHERE source_type IN ('userassist', 'shellbag') AND timestamp IS NOT NULL ORDER BY key_field, timestamp """) rows = cursor.fetchall() conn.close() clusters = [] current_cluster = None for row in rows: key = row["key_field"] or "<UNKNOWN>" ts = datetime.fromisoformat(row["timestamp"].replace("Z", "+00:00")) if current_cluster is None: # 初始化第一个聚类 current_cluster = { "session_id": f"cluster_{row['id']}", "program_name": key, "start_time": ts, "end_time": ts, "count": 1, "unique_paths": set([key]) } else: # 判断是否属于当前聚类:同程序名 & 时间差 < window if (key == current_cluster["program_name"] and ts - current_cluster["end_time"] <= timedelta(seconds=window_seconds)): current_cluster["end_time"] = ts current_cluster["count"] += 1 current_cluster["unique_paths"].add(key) else: # 保存当前聚类,开启新聚类 clusters.append({ **current_cluster, "unique_paths": list(current_cluster["unique_paths"]), "duration_seconds": int((current_cluster["end_time"] - current_cluster["start_time"]).total_seconds()) }) current_cluster = { "session_id": f"cluster_{row['id']}", "program_name": key, "start_time": ts, "end_time": ts, "count": 1, "unique_paths": set([key]) } # 添加最后一个聚类 if current_cluster: clusters.append({ **current_cluster, "unique_paths": list(current_cluster["unique_paths"]), "duration_seconds": int((current_cluster["end_time"] - current_cluster["start_time"]).total_seconds()) }) return clusters def save_clusters_to_db(db_path: str, clusters: List[Dict[str, Any]]): """将聚类结果存入数据库新表 behavior_clusters""" conn = sqlite3.connect(db_path) cursor = conn.cursor() # 创建表(若不存在) cursor.execute(""" CREATE TABLE IF NOT EXISTS behavior_clusters ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, program_name TEXT NOT NULL, start_time TEXT NOT NULL, end_time TEXT NOT NULL, count INTEGER NOT NULL, duration_seconds INTEGER NOT NULL, unique_paths TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) # 批量插入 for cluster in clusters: cursor.execute(""" INSERT INTO behavior_clusters (session_id, program_name, start_time, end_time, count, duration_seconds, unique_paths) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( cluster["session_id"], cluster["program_name"], cluster["start_time"].isoformat(), cluster["end_time"].isoformat(), cluster["count"], cluster["duration_seconds"], json.dumps(cluster["unique_paths"], ensure_ascii=False) )) conn.commit() conn.close() print(f"[INFO] Saved {len(clusters)} behavior clusters to database.") if __name__ == "__main__": # 示例:从默认output/traces.db聚类,窗口10分钟 clusters = cluster_by_time_window("./output/traces.db", window_seconds=600) save_clusters_to_db("./output/traces.db", clusters)5.3 集成到主流程:两行代码注入聚类能力
修改main.py,在generate_report()函数调用前插入聚类逻辑:
# main.py 中约第120行,原代码: # generate_report(output_dir, db_path) # 新增: print("[INFO] Running behavior clustering...") from analysis.cluster_behaviors import cluster_by_time_window, save_clusters_to_db clusters = cluster_by_time_window(db_path, window_seconds=600) save_clusters_to_db(db_path, clusters) # 原有行保持不变: generate_report(output_dir, db_path)同时,在requirements.txt末尾添加:
# analysis dependencies(无需额外依赖,纯Python标准库)
5.4 报告增强:在HTML中展示聚类结果
修改report/templates/timeline.html,在时间轴下方添加新区块:
<!-- report/templates/timeline.html --> <h2>Behavior Clusters (10-min windows)</h2> <div class="cluster-list"> {% for cluster in clusters %} <div class="cluster-item"> <strong>{{ cluster.program_name }}</strong> ({{ cluster.count }} times, {{ cluster.duration_seconds }}s) <br> <small>{{ cluster.start_time }} → {{ cluster.end_time }}</small> <br> Paths: {{ cluster.unique_paths | join(', ') | truncate(100) }} </div> {% endfor %} </div>然后修改report/generate_report.py,在渲染上下文context中加入clusters:
# report/generate_report.py 约第85行 def generate_report(output_dir: str, db_path: str): # ... 原有代码 ... # 新增:查询聚类数据 conn = sqlite3.connect(db_path) conn.row_factory = sqlite3.Row clusters = [dict(row) for row in conn.execute("SELECT * FROM behavior_clusters ORDER BY count DESC LIMIT 10")] conn.close() context = { # ... 原有字段 ... "clusters": clusters # 新增 } # ... 原有渲染代码 ...5.5 验证与调优:如何确认聚类有效?
运行后,检查output/traces.db是否新增behavior_clusters表:
sqlite3 ./output/traces.db "SELECT COUNT(*) FROM behavior_clusters;" # 应返回 > 0 sqlite3 ./output/traces.db "SELECT * FROM behavior_clusters LIMIT 3;" # 查看样例数据更关键的是业务验证:打开生成的HTML报告,找到Behavior Clusters区块。如果看到类似:
chrome.exe (5 times, 3240s) 2024-05-19T09:15:22 → 2024-05-19T10:09:02 Paths: C:\Users\Alice\Downloads\, https://docs.google.com/, ...说明聚类成功捕获了用户上午的浏览器会话。若结果为空,检查--since参数是否过窄,或traces.db中是否有足够多的userassist条目(SELECT COUNT(*) FROM artifacts WHERE source_type='userassist';)。
从那以后我每次给客户交付痕迹报告,都会强制走一遍python analysis/cluster_behaviors.py,哪怕只是看看top3聚类——它像一道“行为X光”,瞬间穿透杂乱的时间戳,照出用户真实的操作节奏。这种确定性,比任何模糊的“疑似异常”描述都更有说服力。希望帮到你。
本文还有配套的精品资源,点击获取