简介:USBViewer是一款面向IT运维人员、系统管理员及普通用户的U盘使用记录查询工具,用于查看计算机上所有曾连接过的USB设备历史信息,帮助排查设备问题、审计数据流动或为误删文件恢复提供线索。资源包共4个文件,包含可执行程序、HTML格式的说明文档、RTF使用手册以及界面截图,整体约547KB,体积轻巧便于携带。其中可执行文件用于运行查询,说明文档与手册介绍许可信息、系统需求及操作方式,截图则直观展示图形化界面。该工具可记录设备ID、制造商、产品名称、序列号等设备信息,并保留首次连接、最后连接及每次插拔的时间戳,同时支持记录通过USB设备进行的复制、移动、删除等文件操作,便于追踪数据流向与潜在安全风险。目前已有878人学习下载,适合需要监控USB设备活动、进行系统审计或查找丢失文件的用户参考使用。
1. 从一台公用电脑的 USB 痕迹说起:USBViewer 到底能查到什么
单位里那台放在走廊尽头的公用查询机,上周被人插过三个不同型号的 U 盘,系统日志里只留下「USB 存储设备已接入」这种含糊记录,具体是谁、什么时候、插了多久,一概查不到。这种场景下,USBViewer 这类 U 盘使用记录查询工具就有了用武之地。它做的事情很聚焦:把 Windows 系统里散落在注册表、事件日志、驱动安装记录中的 USB 设备痕迹汇总起来,还原出「哪个设备、什么时候接入、什么时候拔出、设备序列号是什么」这条时间线。适合谁用?做终端取证的、管公用电脑的、排查数据外带嫌疑的,以及单纯想搞清楚自己电脑被插过什么设备的人。它不是杀毒软件,也不做拦截,只做一件事——把已经发生的 USB 接入行为翻出来给你看。
2. USBViewer 的取证原理:注册表、事件日志与设备枚举三条线
2.1 为什么单看一个地方查不全
Windows 对 USB 设备的记录是分散的,这是很多人第一次用这类工具时最困惑的地方。设备第一次接入时,系统会在注册表USBSTOR键下写入设备型号和序列号;同时USB键下会记录设备的 VID(厂商 ID)和 PID(产品 ID);如果设备被挂载成盘符,MountPoints2里还会留下盘符和卷 GUID 的对应关系。光看USBSTOR只能知道「这个设备来过」,但不知道「什么时候来的」——时间信息得去事件日志里找。系统日志里的事件 ID 2003、2004、2100、2102 分别对应设备接入、拔出、驱动安装等动作,但事件日志默认只保留一段时间,滚动覆盖后就没了。所以一个合格的查询工具必须同时读这三条线,缺一条就会漏。
USBViewer 的思路就是把这三条线的数据做交叉比对:用序列号把注册表记录和事件日志串起来,再用 VID/PID 补全设备描述。这样出来的结果才是一份能拿得出手的记录,而不是一堆孤立的键值。
2.2 注册表里几个关键路径
要理解工具读什么,先得知道数据在哪。下面这几个路径是 USB 取证的核心,我一般会先手动确认一遍,再让工具去批量跑:
# 设备型号与序列号(最核心) HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USBSTOR # 设备 VID/PID 与首次接入信息 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\USB # 盘符挂载历史 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Enum\MountPoints2 # 设备安装时间(部分系统版本) HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceClassesUSBSTOR下面的子键名就是设备型号,比如Disk&Ven_Kingston&Prod_DataTraveler,再下一级是序列号。序列号这一层特别重要,因为同一型号的 U 盘可能有好几个,只有序列号能区分开。USB键下面则是 VID/PID 格式,比如VID_0951&PID_1666,它记录的是设备首次接入时的父设备信息,可以用来判断设备是不是原装接口接入的。
2.3 事件日志的读取与时间对齐
注册表告诉你「来过」,事件日志告诉你「什么时候」。USBViewer 会去读Microsoft-Windows-DriverFrameworks-UserMode/Operational这个日志通道,里面的事件 ID 2003 是设备接入、2004 是拔出、2100 是设备到达、2102 是设备移除。这些事件的时间戳精确到秒,比注册表里的InstallDate(只有日期)靠谱得多。
# 用 Python 读取 USB 相关事件日志的示意 import win32evtlog import xml.etree.ElementTree as ET def read_usb_events(channel, max_records=500): """读取指定通道的 USB 事件,返回时间与事件 ID 列表""" hand = win32evtlog.OpenEventLog(None, channel) flags = win32evtlog.EVENTLOG_BACKWARDS_READ | win32evtlog.EVENTLOG_SEQUENTIAL_READ events = [] while True: records = win32evtlog.ReadEventLog(hand, flags, 0) if not records: break for rec in records: # 只保留 USB 设备相关的事件 ID if rec.EventID in (2003, 2004, 2100, 2102): events.append({ "time": rec.TimeGenerated.Format(), "event_id": rec.EventID, "data": rec.StringInserts # 设备描述信息 }) if len(events) >= max_records: break return events这段代码的逻辑是:打开指定日志通道,用反向读取的方式从最新事件往前翻,只保留 USB 设备相关的四个事件 ID。StringInserts里通常包含设备实例路径,可以从中提取序列号。参数max_records用来控制读取量,避免在日志很大的机器上卡住。实际用的时候,如果事件日志已经被清空或滚动覆盖,这一步就只能返回空,这时候工具会退回到只用注册表数据,并在结果里标注「时间信息缺失」。
2.4 设备枚举与去重
三条线的数据拿到后,还要做去重和合并。同一个设备可能在USBSTOR和USB里都有记录,事件日志里也可能出现多次接入。USBViewer 的做法是以序列号为主键做聚合:同一个序列号下的所有记录合并成一条,首次接入时间取最早的事件,最后拔出时间取最晚的事件,接入次数累加。如果某个设备只有注册表记录没有事件日志,就标注为「时间未知」。
# 按序列号聚合设备记录的示意 def merge_records(reg_records, event_records): """将注册表记录与事件日志按序列号合并""" merged = {} for r in reg_records: sn = r["serial"] if sn not in merged: merged[sn] = { "serial": sn, "model": r["model"], "vid_pid": r["vid_pid"], "first_seen": None, "last_seen": None, "count": 0 } for e in event_records: sn = extract_serial(e["data"]) if sn and sn in merged: t = e["time"] if merged[sn]["first_seen"] is None or t < merged[sn]["first_seen"]: merged[sn]["first_seen"] = t if merged[sn]["last_seen"] is None or t > merged[sn]["last_seen"]: merged[sn]["last_seen"] = t merged[sn]["count"] += 1 return list(merged.values())这里的关键是extract_serial函数,它要从事件日志的设备实例路径里把序列号抠出来。不同厂商的序列号格式不一样,有的带下划线,有的带&分隔符,所以这个函数通常要写好几条正则来兜底。合并后的结果就是一份带时间线的设备清单,按首次接入时间排序,一眼就能看出哪台设备来得最早、哪台来得最频繁。
3. 上手实操:从解压到导出第一份 USB 记录报告
3.1 运行环境与权限准备
USBViewer 是绿色工具,解压后直接运行,但有一个硬性前提:必须以管理员权限启动。原因很简单,读HKEY_LOCAL_MACHINE\SYSTEM下的注册表键和系统事件日志都需要管理员权限,普通用户权限下工具能打开但读不到数据,结果就是一片空白。我一般会右键选择「以管理员身份运行」,或者在命令行里用runas提权。
# 以管理员权限启动(在命令行中) runas /user:Administrator "USBViewer.exe" # 或者直接右键 exe 文件,选择"以管理员身份运行"另一个要注意的是系统版本。Windows 7 到 Windows 11 都能跑,但事件日志的通道名称在 Win7 和 Win10 之后有差异。Win7 用的是Microsoft-Windows-DriverFrameworks-UserMode/Operational,Win10 之后部分事件迁移到了Microsoft-Windows-Kernel-PnP/Configuration。工具一般会自动判断系统版本选择对应通道,但如果发现事件日志读出来是空的,可以手动检查一下通道是否存在。
3.2 扫描与结果解读
启动后主界面通常分三个区域:设备列表、时间线视图、详情面板。点「扫描」按钮后,工具会依次读取注册表和事件日志,这个过程在设备多的机器上可能要十几秒。扫描完成后,设备列表里每一行代表一个曾经接入过的 USB 存储设备,列包括设备型号、序列号、VID/PID、首次接入时间、最后接入时间、接入次数。
这里有个容易误读的地方:列表里出现的设备不一定都是 U 盘。USBSTOR键下也会记录移动硬盘、读卡器、甚至某些手机以存储模式接入时的记录。判断是不是 U 盘,要看设备型号里有没有Disk&Ven_前缀,以及容量信息(如果有的话)。另外,有些设备会显示「序列号不可用」,这通常是因为设备本身没有烧录序列号,系统用了一个占位符代替,这种情况下同一型号的多个设备就无法区分了。
3.3 导出报告与字段说明
扫描结果可以导出成 CSV 或 HTML 报告。CSV 适合做进一步分析,HTML 适合直接存档或打印。导出时我一般会勾选「包含原始注册表路径」这个选项,这样每一条记录都能追溯到具体的注册表键,方便复核。
# 导出的 CSV 字段示例(表头) 设备型号,序列号,VID,PID,首次接入时间,最后接入时间,接入次数,注册表路径 Disk&Ven_Kingston&Prod_DataTraveler,001CC0EC34E1F0C1A7B3,0951,1666,2024-03-12 09:14:22,2024-03-12 17:45:10,3,USBSTOR\Disk&Ven_Kingston...字段里VID和PID是十六进制,0951是某厂商的 VID,1666是具体产品型号。首次接入时间和最后接入时间来自事件日志,如果事件日志缺失,这两列会显示「未知」。接入次数是事件日志里 2003 和 2004 配对统计出来的,如果只有接入没有拔出记录(比如系统直接断电),次数可能会偏少。
提示:导出的 CSV 用 Excel 打开时,序列号列可能被自动转成科学计数法。建议先用记事本打开确认,或者在 Excel 里把该列预设为文本格式再导入。
3.4 批量处理多台机器的记录
如果是排查一个机房或一批公用电脑,一台台手动跑效率太低。USBViewer 支持命令行参数,可以配合批处理脚本远程或本地批量执行。常见做法是把工具放到共享目录,用psexec或计划任务在每台机器上跑一遍,把导出的 CSV 按机器名命名后汇总。
# 批量扫描并导出,结果按机器名保存 for /f %%i in (computers.txt) do ( psexec \\%%i -s -d "C:\Tools\USBViewer.exe" /scan /export:C:\Logs\usb_%%i.csv )这段脚本的逻辑是:从computers.txt里逐行读取机器名,用psexec以系统权限在远程机器上启动 USBViewer,扫描后把结果导出到本地日志目录,文件名带上机器名以便区分。-s参数表示以系统权限运行,-d表示不等待远程进程结束。实际用的时候要注意远程机器得开启 admin$ 共享和远程注册表服务,否则psexec会连不上。
4. 避坑与排查:USBViewer 使用中的五个血泪教训
4.1 扫描结果为空,但明明插过 U 盘
现象:工具以管理员身份运行,扫描后设备列表一条记录都没有,但用户确信这台机器插过 U 盘。
原因:最常见的是系统被清理过。某些优化工具或安全软件会定期清理注册表里的 USB 记录,USBSTOR键被清空后,工具自然读不到。另一种可能是 U 盘接入时没有安装存储驱动(比如只充电的数据线),这种情况下系统不会在USBSTOR下写记录。
解决:先手动打开注册表编辑器,定位到USBSTOR键,看下面有没有子键。如果确实是空的,说明记录已被清理,只能从事件日志里找残留信息。如果事件日志也被清了,那就没有后悔药了。所以重要机器建议定期导出 USB 记录存档,别等出事再查。
4.2 时间显示为「未知」或明显不对
现象:设备列表里能看到型号和序列号,但首次接入时间和最后接入时间都是「未知」,或者显示的时间跟实际使用时间差了好几天。
原因:时间信息来自事件日志,如果日志通道被禁用、日志文件被覆盖、或者系统时间被改过,时间就会缺失或错乱。另外,Win7 和 Win10 的事件通道不同,工具如果没正确识别系统版本,也会读不到时间。
解决:先检查事件查看器里Microsoft-Windows-DriverFrameworks-UserMode/Operational通道是否存在且已启用。如果通道存在但没数据,说明日志已被滚动覆盖。这种情况下只能接受「时间未知」的结果,或者从其他来源(比如文件系统的最后访问时间)做旁证。系统时间被改过的情况比较麻烦,需要结合其他日志做时间线校正。
4.3 同一型号多个 U 盘无法区分
现象:查出来好几条记录,设备型号完全一样,序列号列显示「不可用」或一串相同的占位符,分不清哪个是哪个。
原因:部分廉价 U 盘主控没有烧录唯一序列号,系统在枚举时用了一个固定的占位符(比如000000000000)。这种情况下,同一型号的所有设备在注册表里看起来一模一样。
解决:这是硬件层面的限制,软件无法突破。能做的补救是结合MountPoints2里的盘符挂载记录和事件日志的时间戳做交叉推断——比如两个设备接入时间不同,可以按时间区分。但如果两个设备同时接入过,那就真的分不开了。采购 U 盘时尽量选有唯一序列号的型号,能省掉很多麻烦。
4.4 工具被杀毒软件拦截或删除
现象:解压后 exe 文件被安全软件直接隔离,或者运行到一半被终止。
原因:USB 记录查询工具的行为特征(读注册表敏感键、读事件日志、枚举设备)跟某些恶意软件有重叠,启发式引擎容易误报。这不是工具本身有问题,而是行为特征撞上了。
解决:在安全软件里把工具目录加入白名单,或者临时关闭实时防护后再运行。如果是在单位环境里,最好先跟安全管理员报备,避免触发终端管控告警。运行完后记得把白名单移除,别一直留着。
4.5 导出的 CSV 在 Excel 里乱码
现象:CSV 文件用 Excel 打开后中文变成乱码,或者所有内容挤在一列里。
原因:工具导出的 CSV 默认是 UTF-8 编码,而 Excel 在中文系统下默认用 GBK 打开,编码不匹配就乱码。另外,如果字段里包含逗号但没有正确转义,也会导致列错位。
解决:用记事本打开 CSV,另存为时把编码改成 ANSI 或 GBK,再用 Excel 打开。或者用 Excel 的「数据」→「从文本/CSV」导入功能,手动指定 UTF-8 编码和逗号分隔符。如果字段里有逗号,检查工具是否有转义选项,没有的话只能导入后手动调整列。
5. 进阶技巧:把 USB 记录做成可检索的时间线
5.1 用 SQLite 建一个 USB 记录库
单次导出的 CSV 只能看一台机器的一次扫描结果,如果要做长期监控或多机对比,最好把数据落到数据库里。我一般用 SQLite 建一张表,把每次扫描的结果插进去,加一个scan_time字段标记扫描批次。这样后面要查「某个序列号在哪些机器上出现过」或者「某台机器在某个时间段内插过哪些设备」,一句 SQL 就能搞定。
-- 建表语句 CREATE TABLE usb_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, machine_name TEXT NOT NULL, scan_time TEXT NOT NULL, model TEXT, serial TEXT, vid TEXT, pid TEXT, first_seen TEXT, last_seen TEXT, access_count INTEGER, reg_path TEXT ); -- 查询某个序列号出现过的所有机器 SELECT DISTINCT machine_name, first_seen FROM usb_records WHERE serial = '001CC0EC34E1F0C1A7B3' ORDER BY first_seen; -- 查询某台机器在指定时间段内的 USB 接入记录 SELECT model, serial, first_seen, last_seen FROM usb_records WHERE machine_name = 'PC-001' AND first_seen BETWEEN '2024-03-01' AND '2024-03-31' ORDER BY first_seen;第一条查询用来追踪一个特定 U 盘在哪些机器上用过,做横向排查时特别有用。第二条查询用来还原某台机器在某段时间内的设备接入情况,做时间线分析。scan_time字段虽然不参与查询,但保留它能让你知道每条记录是什么时候采集的,避免把不同时期的扫描结果混在一起。
5.2 用 Python 做时间线可视化
数据落到 SQLite 后,可以用 Python 的 matplotlib 画一条时间线,把每台设备的接入区间画成横条,一眼就能看出哪些设备在时间上有重叠。这个图在写报告的时候比表格直观得多。
import sqlite3 import matplotlib.pyplot as plt from datetime import datetime def plot_timeline(db_path, machine_name): """绘制指定机器的 USB 设备接入时间线""" conn = sqlite3.connect(db_path) cur = conn.cursor() cur.execute(""" SELECT model, serial, first_seen, last_seen FROM usb_records WHERE machine_name = ? AND first_seen IS NOT NULL ORDER BY first_seen """, (machine_name,)) rows = cur.fetchall() conn.close() fig, ax = plt.subplots(figsize=(12, 6)) for i, (model, serial, first, last) in enumerate(rows): start = datetime.strptime(first, "%Y-%m-%d %H:%M:%S") end = datetime.strptime(last, "%Y-%m-%d %H:%M:%S") if last else start ax.barh(i, (end - start).total_seconds() / 3600, left=start, height=0.5) ax.text(start, i, f"{model[:20]} ({serial[:8]})", va='center', fontsize=8) ax.set_xlabel("时间") ax.set_ylabel("设备序号") ax.set_title(f"{machine_name} USB 设备接入时间线") plt.tight_layout() plt.savefig(f"timeline_{machine_name}.png", dpi=150) plt.show()这段代码从 SQLite 里读出指定机器的所有设备记录,用barh画水平条,横轴是时间,纵轴是设备序号。每个条上标注设备型号和序列号前八位。first_seen和last_seen之间的时长就是设备的大致接入时长。如果last_seen为空(只有接入没有拔出记录),就按零时长处理,画成一个点。生成的图可以直接贴进报告里,比纯文字描述有说服力。
5.3 一个我踩过的坑:别把扫描结果当实时监控
最后说一个我自己的教训。有段时间我想用 USBViewer 做实时监控,写了个脚本每隔五分钟跑一次扫描,结果发现同一个设备在列表里反复出现,接入次数一直在涨。后来才想明白,每次扫描都是重新读一遍注册表和事件日志,事件日志里的历史事件会被重复统计,接入次数自然越滚越大。USBViewer 是事后取证工具,不是实时监控工具,它的定位是「查已经发生的事」,不是「盯着正在发生的事」。要做实时监控得用文件系统过滤驱动或者 WMI 事件订阅,那是另一个层面的东西了。
从那以后我每次用 USBViewer,都只把它当快照工具用:需要的时候跑一次,导出结果存档,需要对比的时候拿两次快照做 diff。这样数据干净,结论也可靠。希望这个经验能帮你少走一段弯路。
本文还有配套的精品资源,点击获取