USB键盘流量分析:从数据包捕获到按键还原的实战指南
如果你对网络安全感兴趣,或者参加过CTF竞赛,很可能遇到过一种名为“USB流量分析”的题目。这类题目通常会给你一个抓包文件,里面记录了USB设备(最常见的是键盘)与主机之间的通信数据,你的任务就是从这些看似杂乱无章的十六进制数据中,还原出用户实际按下了哪些键。这听起来像是数字时代的侦探工作,而掌握这项技能,不仅能帮你解决竞赛题目,更能让你深入理解计算机最基础的输入层是如何工作的。今天,我们就抛开那些复杂的理论,直接从实战出发,手把手带你走完从Wireshark抓包到Python脚本解码的全过程。无论你是刚接触安全的新手,还是想巩固基础的爱好者,这篇文章都将为你提供一个清晰、可操作的路线图。
1. 理解USB键盘流量的本质:数据包里的按键信号
在深入操作之前,我们得先搞清楚我们要分析的究竟是什么。当你按下键盘上的一个键,比如字母“A”,键盘内部的微控制器并不会直接发送一个字符“A”给电脑。相反,它遵循一套名为USB HID(Human Interface Device) 的协议,将你的按键动作编码成一个或多个标准的数据包。
一个典型的USB键盘数据包长度是8个字节(16个十六进制字符)。对于键盘流量分析,我们最关心的是其中两个关键字段:
- Modifier Byte(修饰键字节):通常位于数据包的第1个字节(偏移量0)。它用位掩码(bitmask)的方式表示是否同时按下了
Ctrl、Shift、Alt、GUI(Win/Cmd)等修饰键。例如,0x02通常代表左Shift键被按下。 - Keycode Byte(键码字节):通常位于数据包的第3个字节(偏移量2)。这个字节包含了一个数字,即HID Usage ID,它唯一对应键盘上的一个物理按键。注意,这个键码代表的是按键位置(例如“左上字母键”),而不是最终输出的字符。字符“a”和“A”可能对应同一个键码,区别在于是否有Shift修饰。
为了更直观地理解一个USB数据包的结构,我们可以看下面这个简化的表格:
| 字节偏移(十六进制) | 典型值示例 | 含义说明 |
|---|---|---|
| 0x00 | 00 | 修饰键状态。00表示没有按下Ctrl、Shift等。02通常表示左Shift按下。 |
| 0x01 | 00 | 保留字段,通常为00。 |
| 0x02 | 04 | 核心键码(HID Usage ID)。04对应着字母“A”和“a”的物理按键。 |
| 0x03-0x07 | 00 00 00 00 00 | 其他可能的键码(支持最多6键无冲)或填充字段,未按下时为00。 |
提示:实际抓取的数据包中,在USB协议层头部之外,我们直接看到的
usb.capdata字段通常就是这8个字节的HID报告数据。这也是我们后续用tshark命令提取的内容。
所以,分析USB键盘流量的核心任务就变成了:从海量的数据包中,过滤出这些8字节的HID报告,提取出每个包的键码和修饰键信息,最后根据HID规范将键码映射回人类可读的字符或按键名称。这个过程充满了“寻宝”的乐趣,因为你永远不知道下一个数据包里藏着哪个字母,它们最终会拼凑出怎样的秘密。
2. 数据获取与预处理:使用Wireshark和tshark
拿到一个.pcap或.pcapng格式的流量文件后,我们的第一站通常是图形化工具Wireshark。打开文件,你会看到成千上万行协议各异的数据包。如何快速定位到USB键盘流量呢?
在Wireshark中快速过滤:
在Wireshark顶部的过滤栏中输入过滤表达式:usb.transfer_type == 0x01 && usb.src == "1.x"。这里解释一下:
usb.transfer_type == 0x01筛选出中断传输(Interrupt Transfer)类型的包,这是HID设备(如键盘、鼠标)上报数据的主要方式。usb.src == "1.x"中的1.x需要替换成你的键盘设备在USB拓扑中的实际源地址。你可以先尝试过滤usb,然后在“协议”列找到USBHID或URB_INTERRUPT的数据包,查看其Source字段来确定。
找到键盘数据包后,在详情面板展开USB URB -> Leftover Capture Data,或者直接查找usb.capdata字段,你就能看到那8个字节的核心数据了。为了方便后续的脚本处理,我们通常不会在Wireshark里手动记录,而是使用其命令行伙伴——tshark。
使用tshark进行高效提取:
tshark是Wireshark的命令行版本,非常适合自动化数据提取。打开你的终端(Linux/macOS)或命令提示符/PowerShell(Windows),切换到流量文件所在目录,执行以下命令:
tshark -r flag.pcap -T fields -e usb.capdata -Y "usb.transfer_type == 0x01 && usb.src == \"1.2.0\"" > usbdata.txt
让我拆解一下这个命令:
-r flag.pcap:指定要读取的流量文件。-T fields:设置输出格式为“字段”,方便我们提取特定列。-e usb.capdata:指定要提取的字段是usb.capdata,即HID报告数据。-Y "usb.transfer_type == 0x01 && usb.src == \"1.2.0\"":这是显示过滤器(-Y),作用等同于Wireshark图形界面里的过滤栏。它确保只提取来自特定USB源地址的中断传输数据包。请务必将1.2.0替换为你实际分析文件中的正确地址。> usbdata.txt:将标准输出重定向到usbdata.txt文件,保存结果。
执行成功后,你会得到一个usbdata.txt文件,里面每一行可能是一个像00001e0000000000这样的16位十六进制字符串(对应8字节),也可能有一些空行。这就是我们进行解码的“原材料”。
3. 数据清洗与格式化:为解码做准备
直接提取出来的usbdata.txt可能并不完全规整。我们通常需要一个小脚本来进行数据清洗和格式化,使其更适合后续的键码映射。这个步骤主要做两件事:
- 去除空行:确保每一行都是有效数据。
- 格式化分隔:将连续的十六进制字符串用冒号分隔,如
00:00:1e:00:00:00:00:00,这样更符合我们阅读和解析的习惯。
下面是一个Python脚本示例,它完成了上述工作,并增加了一个简单的有效性检查(只处理长度为16的字符串,这是标准键盘数据长度):
# clean_usb_data.py
input_filename = 'usbdata.txt'
output_filename = 'out.txt'
with open(input_filename, 'r') as infile, open(output_filename, 'w') as outfile:
for line in infile:
raw_line = line.strip() # 去除首尾空白字符
if not raw_line: # 跳过空行
continue
# 检查长度是否为16(8字节的十六进制表示)
if len(raw_line) == 16:
# 每两个字符(一个字节)后加冒号分隔
formatted = ':'.join([raw_line[i:i+2] for i in range(0, 16, 2)])
outfile.write(formatted + '\n')
else:
# 可以在这里记录或打印非标准长度的行,用于调试
# print(f"跳过非常规长度行: {raw_line}")
pass
print(f"数据清洗完成,结果已保存至 {output_filename}")
运行这个脚本后,你将得到out.txt文件。现在,数据已经变得非常规整,每一行都像是一个清晰的“指令”:00:00:04:00:00:00:00:00。接下来,就是最激动人心的部分——从这些指令中翻译出真正的按键。
4. 核心解码:从HID键码到字符映射
这是整个流程的灵魂所在。我们需要一个HID Usage ID到字符的映射字典。这个字典是基于USB HID规范定义的。对于键盘来说,每个物理按键都有一个固定的Usage ID。例如,Usage ID 0x04 对应着字母“A”所在的键,0x05对应“B”,以此类推。
但是,这里有一个至关重要的细节:键码只代表按键,不区分大小写。0x04既可能是“a”也可能是“A”。决定最终输出字符的是修饰键字节(第一个字节)。如果第一个字节显示Shift被按下(例如值为0x02或0x20),那么0x04就应该映射为“A”,否则是“a”。
因此,一个健壮的解码脚本需要同时考虑键码和修饰键。下面是一个功能更完整的解码脚本示例:
# decode_keyboard.py
# HID Usage ID 到基础按键的映射(小写/无修饰状态)
hid_to_key = {
0x04: 'a', 0x05: 'b', 0x06: 'c', 0x07: 'd', 0x08: 'e',
0x09: 'f', 0x0A: 'g', 0x0B: 'h', 0x0C: 'i', 0x0D: 'j',
0x0E: 'k', 0x0F: 'l', 0x10: 'm', 0x11: 'n', 0x12: 'o',
0x13: 'p', 0x14: 'q', 0x15: 'r', 0x16: 's', 0x17: 't',
0x18: 'u', 0x19: 'v', 0x1A: 'w', 0x1B: 'x', 0x1C: 'y',
0x1D: 'z',
0x1E: '1', 0x1F: '2', 0x20: '3', 0x21: '4', 0x22: '5',
0x23: '6', 0x24: '7', 0x25: '8', 0x26: '9', 0x27: '0',
0x28: '\n', # Return (ENTER)
0x2A: '[DEL]', # Delete (Backspace)
0x2B: '\t', # Tab
0x2C: ' ', # Spacebar
0x2D: '-', 0x2E: '=', 0x2F: '[', 0x30: ']', 0x31: '\\',
0x33: ';', 0x34: '\'', 0x36: ',', 0x37: '.',
# 更多键码可以按需添加,如功能键F1-F12等
}
# Shift键影响下的字符映射(覆盖部分键)
shift_mapping = {
'1': '!', '2': '@', '3': '#', '4': '$', '5': '%',
'6': '^', '7': '&', '8': '*', '9': '(', '0': ')',
'-': '_', '=': '+', '[': '{', ']': '}', '\\': '|',
';': ':', '\'': '"', ',': '<', '.': '>',
}
# 字母的大写转换我们直接在代码中处理
def parse_usb_packet(packet_str):
"""解析格式为 xx:xx:xx:xx:xx:xx:xx:xx 的数据包字符串"""
parts = packet_str.strip().split(':')
if len(parts) != 8:
return None, None
try:
bytes_list = [int(p, 16) for p in parts]
except ValueError:
return None, None
modifier = bytes_list[0] # 修饰键字节
keycode = bytes_list[2] # 键码字节(通常在第3个位置)
return modifier, keycode
def decode_to_text(input_filename='out.txt'):
"""主解码函数"""
result = []
with open(input_filename, 'r') as f:
for line in f:
mod, key = parse_usb_packet(line)
if key is None or key == 0:
continue # 跳过无效或空按键(键码为0表示没有按键事件)
# 获取基础按键字符
base_char = hid_to_key.get(key, f'[0x{key:02X}]')
# 处理Shift修饰
# 常见左Shift位掩码是 0x02,右Shift是 0x20,有时也简单判断非0
is_shift_pressed = (mod & 0x22) != 0
final_char = base_char
if is_shift_pressed:
# 如果是字母,转换为大写
if base_char.isalpha():
final_char = base_char.upper()
# 如果是符号,查找Shift映射表
elif base_char in shift_mapping:
final_char = shift_mapping[base_char]
result.append(final_char)
return ''.join(result)
if __name__ == '__main__':
flag_text = decode_to_text()
print("解码出的文本内容:")
print("-" * 40)
print(flag_text)
print("-" * 40)
这个脚本比简单的映射更进了一步。它解析每个格式化后的数据包,分离出修饰键和键码。然后根据修饰键状态(特别是Shift)来决定最终的输出字符。运行这个脚本,你就有很大概率直接得到清晰的、包含大小写和标点的文本内容,很可能就是你要找的Flag。
5. 实战技巧与常见问题排查
理论流程走通了,但在实际分析,尤其是CTF比赛中,情况往往更复杂。下面分享几个我踩过坑后总结的实战技巧:
1. 识别并过滤“空包”
键盘会持续向主机发送中断报告,即使没有按键动作。这种报告通常键码字节为0x00。在解码时,我们需要忽略这些包,否则输出中会充斥大量空字符。上面脚本中的if key == 0: continue就做了这件事。更严格的做法是检查整个数据包是否全为零。
2. 处理按键释放事件
USB键盘协议中,按键按下和释放都会发送数据包。释放事件的数据包通常键码字段也为0x00。但有时,一个按键被释放时,其键码可能仍出现在数据包中,同时修饰键字节可能发生变化。一种简单的策略是:只关心键码从0变为非0的“按下”事件。这需要跟踪前一个数据包的状态,实现起来稍复杂,但对于输入还原通常足够准确。
3. 应对非标准数据包偏移
绝大多数情况下,键码在第3字节(索引2)。但我遇到过一些抓包环境或特殊题目,偏移量可能不同。如果你的解码结果全是乱码或[unknown],可以尝试打印出几个数据包的所有字节,手动分析规律。例如,先找出你认为的“空格键”(键码0x2c)对应的数据包,看它出现在哪个位置。
# 调试脚本片段:打印前10个非空包的结构
with open('out.txt', 'r') as f:
count = 0
for line in f:
parts = line.strip().split(':')
if any(p != '00' for p in parts): # 非全零包
print(f"包 {count}: {parts}")
count += 1
if count >= 10:
break
4. 特殊键和Flag格式
别忘了,Flag可能包含下划线_、花括号{}等字符。确保你的映射字典包含了这些符号对应的键码(例如,0x2d是-,但Shift+-才是_)。另外,CTF的Flag常有固定格式,如flag{...}或FLAG{...},如果解码出的文本接近这个格式但有些字符不对,可能是大小写或符号映射出了问题,需要回头检查修饰键处理逻辑。
5. 使用现成工具进行交叉验证
当你对自己的脚本没把握时,可以用一些社区成熟的工具来验证。比如USBKeyboardHacker这类开源小工具,或者一些在线解码网站(注意不要上传敏感数据)。用它们处理你的usbdata.txt,对比输出结果,能快速定位是数据提取问题还是解码逻辑问题。
从Wireshark里密密麻麻的数据包,到最终屏幕上显现出有意义的字符串,这个过程就像在数字海洋中打捞沉船宝藏。每一次成功的解码,都是对底层协议理解的一次加深。我刚开始做USB流量分析时,经常被各种偏移量和修饰键搞得晕头转向,但坚持手动分析几个数据包后,那种“原来如此”的顿悟感是无与伦比的。记住,最可靠的永远是你自己对协议的理解和调试输出的中间结果。现在,去找一个CTF的USB流量题目试试手吧,把文中的脚本当作起点,根据实际情况调整,享受从数据到秘密的破解之旅。

516

被折叠的 条评论
为什么被折叠?



