简介:这是一款面向Windows用户的本地使用痕迹检测工具,适合需要核查电脑近期操作记录、排查外接设备接入情况的普通用户与运维人员。软件仅读取注册表中保存的记录,对注册表、文件及事件日志等信息进行归纳显示,可查看数月乃至数年内目录访问、文件访问、关键词搜索以及优盘或外接硬盘接入记录,不涉及上网记录检索,全程不联网、无数据传输,也不改写注册表。资源包共39个文件,以17个ico图标、5个exe可执行程序、4个bat批处理脚本为主,另含svg、txt、dat、dll、ini、xml等配置与说明文件,压缩包约4.5MB,结构紧凑、开箱即用。目前已有1658人学习下载,读者可借此快速掌握痕迹读取与日志归纳思路,用于日常自查或设备审计场景。
1. 一台 Windows 到底被谁动过:从使用痕迹检测说起
很多人第一次意识到 Windows 会“记仇”,往往是在一台公用机或者离职同事留下的电脑上。你打开浏览器,历史记录被清得干干净净,可系统深处还留着一串时间戳、一份 Prefetch、一段注册表键值,甚至一张缩略图缓存。Windows 使用痕迹检测工具要解决的,就是把这些散落在系统各处的“脚印”重新拼成一条可读的时间线。它适合三类人:做数字取证和应急响应的安全从业者、需要审计终端操作合规性的 IT 管理员,以及想搞清楚自己电脑有没有被翻动过的普通用户。这类工具通常不依赖单一数据源,而是把注册表、事件日志、文件系统元数据、浏览器数据库、最近使用记录等多个位置的信息交叉比对,最终输出一份带时间、路径、操作类型的痕迹清单。理解它的价值,不在于“监控”,而在于还原事实。
2. 痕迹都藏在哪:Windows 使用痕迹的六大数据源
2.1 注册表中的用户活动痕迹
注册表是 Windows 使用痕迹最密集的地方,没有之一。用户每次打开程序、插拔 U 盘、访问网络位置,系统都会在NTUSER.DAT和UsrClass.dat里留下记录。常见的键值包括HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs(最近打开的文档)、RunMRU(运行对话框输入过的命令)、TypedPaths(资源管理器地址栏手输路径)、MountPoints2(挂载过的卷)以及UserAssist(带 ROT13 编码的程序执行记录)。这些键值的特点是:普通清理软件很少碰,因为它们不属于“缓存”,而是系统功能的一部分。
检测工具读取注册表时,通常走离线挂载或在线读取两条路。离线挂载适合取证场景,把目标系统的NTUSER.DAT加载到当前系统的HKEY_USERS下分析,避免污染原始数据。在线读取则直接查当前用户 hive,适合本机自查。下面是一段用 Python 读取UserAssist并解码的示例,winreg是标准库,不需要额外安装:
import winreg import codecs import datetime # UserAssist 的 GUID 路径,64 位和 32 位系统略有差异 key_path = r"Software\Microsoft\Windows\CurrentVersion\Explorer\UserAssist" # ROT13 解码表,UserAssist 的值名经过 ROT13 编码 rot13 = str.maketrans( "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm" ) with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key_path) as root: sub_count = winreg.QueryInfoKey(root)[0] for i in range(sub_count): sub_name = winreg.EnumKey(root, i) # 每个子键下还有 Count 子键,里面才是真正的执行记录 with winreg.OpenKey(root, sub_name + r"\Count") as count_key: val_count = winreg.QueryInfoKey(count_key)[1] for j in range(val_count): name, data, _ = winreg.EnumValue(count_key, j) decoded = name.translate(rot13) # data 的前 8 字节是运行次数,后 8 字节是最后运行时间(FILETIME) run_count = int.from_bytes(data[4:8], "little") filetime = int.from_bytes(data[8:16], "little") if filetime > 0: dt = datetime.datetime(1601, 1, 1) + datetime.timedelta( microseconds=filetime // 10 ) print(f"{decoded} | 次数={run_count} | 最后运行={dt}")这段代码的关键点有三个。第一,UserAssist的值名是 ROT13 编码的,直接读出来是乱码,必须解码才能看到真实路径。第二,数据块的结构在不同 Windows 版本上有细微差别,Win7 以后通常是 72 字节,前 4 字节是会话 ID,接着 4 字节是运行次数,再 8 字节是 FILETIME。第三,winreg读取的是当前用户 hive,如果要分析其他用户的痕迹,需要先用reg load挂载对应的NTUSER.DAT,再改HKEY_USERS\<SID>路径。参数上,run_count和filetime的偏移量是最容易翻车的地方,建议先用已知程序验证一次再批量跑。
2.2 事件日志与文件系统元数据
事件日志是另一条主线。Windows 安全日志里的 4624(登录成功)、4634(注销)、4648(显式凭据登录)能还原账号使用时间线;系统日志里的 20001(即插即用设备安装)、6005/6006(事件日志服务启停)能反映设备接入和系统开关机。文件系统层面,NTFS 的$MFT记录每个文件的创建、修改、访问时间,$UsnJrnl则记录文件增删改的变更日志,哪怕文件被删了,USN 日志里可能还留着记录。Prefetch 文件(C:\Windows\Prefetch\*.pf)记录程序首次和最近一次执行时间以及加载的 DLL 列表,是判断“某个程序有没有跑过”的硬证据。
检测工具一般不会自己去解析$MFT原始结构,而是调用 Windows API 或者用fsutil、Get-ChildItem配合时间过滤。常见做法是先用fsutil usn readjournal C:导出 USN 日志,再用脚本过滤出目标时间段内的记录。Prefetch 的解析稍微麻烦一点,Win10 以后.pf文件是压缩的,需要先解压再按固定偏移读取。我一般会推荐用现成的解析库,比如 Python 的prefetch库或者pyscca,避免自己啃二进制格式。
# 导出 USN 日志到文件,注意需要管理员权限 fsutil usn readjournal C: csv > usn_log.csv # 查看 Prefetch 目录下最近修改的文件,按时间倒序 powershell "Get-ChildItem C:\Windows\Prefetch\*.pf | Sort-Object LastWriteTime -Descending | Select-Object -First 20 Name, LastWriteTime"fsutil usn readjournal的csv参数让输出变成逗号分隔,方便后续用 Python 或 Excel 处理。注意 USN 日志默认有大小上限,系统盘写入频繁的话可能只保留最近几天到几周的数据,想长期留存需要提前调大$UsnJrnl的最大尺寸。Prefetch 默认只保留最近 128 个程序的记录,Win10 以后这个数量可以调整,但普通用户很少改,所以看到的就是最近一段时间的执行痕迹。
2.3 浏览器与最近使用记录
浏览器痕迹是最直观的一类,Chrome、Edge、Firefox 都把历史、下载、Cookie、缓存放在 SQLite 数据库里。Chrome 和 Edge 的路径在%LOCALAPPDATA%\Google\Chrome\User Data\Default\History和%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History,Firefox 在%APPDATA%\Mozilla\Firefox\Profiles\<随机>.default\places.sqlite。这些数据库在浏览器运行时会被锁定,直接读可能失败,常见做法是先复制一份再解析,或者用sqlite3的只读模式打开。
import sqlite3 import shutil import os # 先复制一份,避免浏览器占用导致读取失败 src = os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History") dst = os.path.join(os.environ["TEMP"], "edge_history_copy.db") shutil.copy2(src, dst) conn = sqlite3.connect(dst) cursor = conn.cursor() # urls 表存 URL 和标题,visits 表存访问时间 cursor.execute(""" SELECT urls.url, urls.title, visits.visit_time FROM urls JOIN visits ON urls.id = visits.url ORDER BY visits.visit_time DESC LIMIT 50 """) for url, title, visit_time in cursor.fetchall(): # Chrome/Edge 的时间戳是 1601 年起的微秒数 print(url, title, visit_time) conn.close()这里的时间戳是 WebKit/Chrome 格式,从 1601 年 1 月 1 日起算的微秒数,和 Windows FILETIME 同源但单位不同,转换时除以 10 再转datetime即可。RecentDocs、Jump List、Quick Access里的最近文件记录则补充了“打开过哪些本地文件”这一维度,Jump List存在%APPDATA%\Microsoft\Windows\Recent\AutomaticDestinations下,每个文件对应一个程序的跳转列表,解析需要按 OLE 复合文档格式处理,手工做比较费劲,工具通常会调用现成解析器。
3. 自己动手:从零搭一个痕迹采集脚本
3.1 环境准备与依赖选择
要复现一个可用的痕迹检测流程,不需要装一堆重型工具。Python 3.8 以上加几个标准库和轻量第三方库就够了。核心依赖是winreg(标准库,读注册表)、sqlite3(标准库,读浏览器数据库)、python-evtx(解析 EVTX 事件日志)、pyscca(解析 Prefetch)。如果不想装第三方库,事件日志可以用wevtutil命令行导出成 XML 再解析,Prefetch 可以暂时跳过,但会丢掉一块重要证据。
# 建议在虚拟环境里装,避免污染系统 Python python -m venv trace_env trace_env\Scripts\activate pip install python-evtx pysccapython-evtx负责把二进制 EVTX 转成可读的 XML 结构,pyscca负责解压和解析 Prefetch。这两个库在 Windows 上安装一般不会出问题,如果pyscca编译失败,常见原因是缺少 C 编译环境,装一个 Visual Studio Build Tools 的 C++ 组件即可。环境准备好之后,建议先跑一个最小验证:读一条UserAssist记录、读一条 Edge 历史、导出一段安全日志,确认三条链路都通,再往下写整合脚本。
3.2 采集脚本的模块划分与参数
一个能用的采集脚本,我一般拆成四个模块:注册表采集、浏览器采集、事件日志采集、文件系统采集。每个模块独立输出 JSON,最后合并成一份带统一时间戳的报告。这样拆的好处是某个模块失败不影响其他模块,排查也方便。下面是一个整合入口的骨架:
import json import datetime import winreg import sqlite3 import shutil import os import subprocess def collect_registry(): """采集 UserAssist 和 RecentDocs""" results = [] # UserAssist 采集逻辑同上,略 # RecentDocs 采集 key = r"Software\Microsoft\Windows\CurrentVersion\Explorer\RecentDocs" with winreg.OpenKey(winreg.HKEY_CURRENT_USER, key) as k: count = winreg.QueryInfoKey(k)[1] for i in range(count): name, data, _ = winreg.EnumValue(k, i) # data 是 UTF-16LE 编码的文件名,末尾有 \x00\x00 try: filename = data.decode("utf-16-le").rstrip("\x00") results.append({"source": "RecentDocs", "name": filename}) except UnicodeDecodeError: continue return results def collect_browser(): """采集 Edge 历史,返回最近 100 条""" results = [] src = os.path.expandvars(r"%LOCALAPPDATA%\Microsoft\Edge\User Data\Default\History") if not os.path.exists(src): return results dst = os.path.join(os.environ["TEMP"], "edge_hist.db") shutil.copy2(src, dst) conn = sqlite3.connect(dst) for url, title, vt in conn.execute( "SELECT urls.url, urls.title, visits.visit_time FROM urls " "JOIN visits ON urls.id=visits.url ORDER BY visits.visit_time DESC LIMIT 100" ): results.append({"source": "Edge", "url": url, "title": title, "time": vt}) conn.close() return results def collect_eventlog(): """用 wevtutil 导出安全日志中的登录事件""" out = os.path.join(os.environ["TEMP"], "security_log.xml") subprocess.run( ["wevtutil", "qe", "Security", "/q:*[System[(EventID=4624)]]", "/f:xml", f"/rd:true", f"/c:50"], stdout=open(out, "w", encoding="utf-8"), check=False ) return [{"source": "EventLog", "file": out}] if __name__ == "__main__": report = { "generated_at": datetime.datetime.now().isoformat(), "registry": collect_registry(), "browser": collect_browser(), "eventlog": collect_eventlog(), } with open("trace_report.json", "w", encoding="utf-8") as f: json.dump(report, f, ensure_ascii=False, indent=2) print("采集完成,输出 trace_report.json")collect_registry里RecentDocs的值名是序号,数据是 UTF-16LE 编码的文件名,解码后要去掉末尾的\x00。collect_browser先复制再读,避免浏览器锁库。collect_eventlog用wevtutil的/rd:true表示倒序读取,/c:50限制 50 条,避免日志太大拖慢速度。参数上,/q后面的 XPath 可以改成其他 EventID,比如 4634 注销、4648 显式凭据,按需调整。整个脚本跑下来通常几秒到几十秒,取决于日志量和磁盘速度。
3.3 时间线合并与输出格式
采集到的原始数据时间格式五花八门:FILETIME、Unix 时间戳、WebKit 微秒、ISO 字符串。要合并成一条时间线,必须先统一成同一种格式。我一般统一转成 ISO 8601 字符串,带时区偏移,方便排序和阅读。下面是一个归一化函数:
import datetime def normalize_time(value, fmt): """把不同来源的时间统一成 ISO 字符串""" if fmt == "filetime": # Windows FILETIME,100 纳秒为单位,从 1601-01-01 起算 return (datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=value // 10)).isoformat() elif fmt == "webkit": # Chrome/Edge,微秒,从 1601-01-01 起算 return (datetime.datetime(1601, 1, 1) + datetime.timedelta(microseconds=value)).isoformat() elif fmt == "unix": return datetime.datetime.fromtimestamp(value).isoformat() return str(value)合并时按时间排序,每条记录带上source字段标明来源。输出格式建议 JSON 加一份 CSV,JSON 给程序读,CSV 给人看。CSV 里至少要有time、source、action、detail四列。如果要做成可视化时间线,可以把 CSV 导入到 Excel 或者用 Python 的matplotlib画散点图,横轴时间、纵轴来源,一眼就能看出哪个时间段活动密集。
4. 避坑与排查:痕迹检测里最容易翻车的五件事
4.1 权限不够导致注册表读不全
现象:脚本跑完发现UserAssist只有几条记录,或者HKEY_USERS下看不到其他用户的 hive。原因:HKCU只能读当前用户,其他用户的NTUSER.DAT默认没挂载,而且UserAssist的部分子键需要管理员权限才能枚举。解决:用管理员身份运行脚本,分析其他用户时先用reg load HKU\TempHive C:\Users\目标用户\NTUSER.DAT挂载,分析完再reg unload。注意挂载前确认目标 hive 没有被占用,否则会报“文件正在使用”。
4.2 浏览器数据库被锁
现象:sqlite3.connect报database is locked,或者读出来的历史记录是空的。原因:浏览器运行时会持有数据库的写锁,直接读可能拿到不一致的快照。解决:先shutil.copy2复制到临时目录再读,复制时如果也失败,说明浏览器正在写入,可以先用taskkill /im msedge.exe /f关掉浏览器再复制。注意复制前最好确认浏览器已经退出,否则复制出来的数据库可能缺少最近的 WAL 日志内容。
4.3 Prefetch 解析偏移量对不上
现象:用pyscca读出来的程序名是乱码,或者运行次数明显不对。原因:不同 Windows 版本的 Prefetch 格式有差异,Win7 和 Win10 的头部结构不一样,Win10 以后还多了压缩层。解决:用pyscca时先调用get_prefetch_info确认版本,再按版本选择偏移量。如果手工解析,先用一个已知程序(比如notepad.exe)的.pf文件验证偏移量,确认无误再批量处理。注意 Prefetch 默认只保留 128 个文件,如果目标程序很久没跑,可能已经被新记录挤掉了。
4.4 事件日志被覆盖或清空
现象:wevtutil导出安全日志时提示“日志已满”或者导出的记录时间范围很短。原因:安全日志默认大小有限(通常 20MB),高频登录场景下几天就可能滚动覆盖。解决:提前用wevtutil sl Security /ms:104857600把日志上限调到 100MB,或者配置日志转发到中央服务器。如果日志已经被清空,4624 这类事件就找不回来了,只能靠注册表和文件系统痕迹补。注意清空日志本身也会产生 1102 事件,这个反而可以作为“有人动过日志”的证据。
4.5 时间戳时区没对齐
现象:合并后的时间线里,浏览器记录和事件日志的时间差了好几个小时。原因:浏览器数据库存的是 UTC 时间,事件日志默认显示本地时间,注册表 FILETIME 也是 UTC,混在一起不转换就会错位。解决:所有时间在归一化时统一转成 UTC,输出时再按需转本地时区。Python 里用datetime.timezone.utc明确标注,避免fromtimestamp默认用本地时区带来的隐式转换。注意夏令时切换那几天的记录容易出玄学问题,建议直接以 UTC 为准。
5. 进阶技巧:把痕迹检测做成可复用的取证流程
单次采集只是起点,真正有价值的是把流程固化下来,变成可重复、可对比的取证习惯。我一般会做三件事:建立基线、定期快照、差异比对。基线是在系统“干净”状态下跑一次采集,把结果存成baseline.json;之后每次怀疑有异常,再跑一次采集,用脚本比对两份报告的差异。差异里新增的程序执行记录、新出现的 USB 挂载点、异常时间段的登录事件,就是重点排查对象。
比对脚本的核心逻辑是按source + detail做集合差集,下面是一个简化版:
import json def load_report(path): with open(path, encoding="utf-8") as f: return json.load(f) def diff_reports(base, current): """按 source 分组,找出新增条目""" base_set = {(r["source"], str(r.get("name", r.get("url", "")))) for r in base.get("registry", []) + base.get("browser", [])} new_items = [] for r in current.get("registry", []) + current.get("browser", []): key = (r["source"], str(r.get("name", r.get("url", "")))) if key not in base_set: new_items.append(r) return new_items base = load_report("baseline.json") current = load_report("trace_report.json") for item in diff_reports(base, current): print(item)这个比对只覆盖了注册表和浏览器,事件日志和文件系统可以按同样思路扩展。实际用的时候,我会把基线报告和每次的差异报告按日期归档,形成一份“痕迹变更日志”。这样即使某次采集漏了某个数据源,历史报告里还能找到线索。
另一个进阶方向是自动化。用 Windows 任务计划程序(Task Scheduler)每天定时跑一次采集脚本,输出到带日期的文件夹,配合日志轮转,就能在不装第三方监控软件的前提下,攒出一份本机使用痕迹的长期档案。任务计划里注意勾选“使用最高权限运行”,否则注册表和事件日志读不全。触发条件可以设成“用户登录时”加“每天固定时间”,兼顾实时性和完整性。
还有一个容易被忽略的技巧:采集之前先记录当前系统的时区和时间同步状态。w32tm /query /status能看出系统时间有没有被手动改过,如果时间被改过,所有痕迹的时间戳都会失真,这时候需要先校准再分析。我吃过一次亏,目标机器的时间被往前调了两天,导致整条时间线错位,后来每次采集前都强制走一遍w32tm /resync并记录同步结果。希望这些经验能帮到你,少走一点弯路。
本文还有配套的精品资源,点击获取