这篇文章的面向人群是之前没有做过USB的人或者是只会用工具解USB的人的,我将会基于一个完整的USB流量包来分析整个USB的流量传输的过程,以及我会写一下我开发Soer的一些经验什么的。(因为不太熟悉md和写文章,内容有不对的地方还请指正,谢谢各位师傅,可以添加笔者的QQ 2729913542)

这是我随便抓的一个我自己的设备的流量包,本文都将基于这个流量包进行分析:下载地址

进行USB流量分析的第一步就是要正确安装带有USBpcap的wireshark,否则wireshark将不能正确的显示USB的流量包

一个简单的例子

这个是未有正确安装USBpcap的wireshark解析出来的USB流量
20231102203534
这个是正确安装USBpcap的wireshark解析出来的USB流量
20231102203656

区别还是很明显的,这篇文章就不讲怎么安装软件了

USB流量的结构

打开流量包,可以看到有host和非host的IP

只要是一个正常完整的USB流量包,就可以看到有这部分的数据

20231102212937

这一部分是主机用来确认USB设备的基本信息的包

源IP为host

就是从本机发回USB设备的数据,从主机发回去的数据包类似于tcp三次握手的第二次握手,主机向设备发送报文,告诉设备:我收到了了你的数据

这类的包只是对USB设备发送,并不会存储我们需要的数据,可以忽略

源IP为非host的数据包

这种数据包可以分为两类,IP最后一位为0IP最后一位不为0 的两种。

ip最后一位不为0
这种包就是用来传输具体的数据的,比如

20231102213017

Leftover Capture Data 这个字段(相当于是payload),储存的就是由地址 1.6.2 发给电脑的数据,前面的 USB URB 以及 Frame 都是报文,并不存储信息

这种包的发送方都是设备方,

20231102213046

可以很明显的看出来,第一个有数据的包是从设备方发给主机的。

通常情况下,这个字段的数据就是我们需要的,当然也不排除出题人恶心人😋

IP最后一位为0

这种包就是电脑用来确认USB设备的信息的,这种包的发送方一定是主机方,回复方一定是设备方,

20231102213127

能很清晰的看到,在这种包的 Info 部分,主机在这个时候都是做的request操作,也可以证实前文码的那部分

这种包的回包里面会传输设备的基本信息和将要传输的数据特征和格式什么的,从这种包就可以读出一些信息,比如我现在查看其中一个回包的 DEVICE DESCRIPTOR 部分,

20231102213612

第一类包的 INFODESCRIPTOR Response DEVICE 在这里可以看到它的公司和设备名的id,可以通过在此网站查询该设备 USB ID List可能会需要kexue shangwang

这个也是Soer实现的一个基本原理

第二类包的 INFODESCRIPTOR Response CONFIGURATION 这里可以看到USB设备的基本信息什么的,不太重要

第三类包就是用来告诉设备可以开始传输数据了

一次正常的传输,就应该包括设备的确认和数据的传输,设备的确认,主机和设备有三次交互
20231102215626

然后就会开始传输数据了

简单总结一下上面写的,就是在USB流量中,只有源IP的尾部不为0的数据包才会有传输的,其他的数据对于解题来说,帮助不大的

简单了解了一下USB流量的结构,然后就可以开始分析数据了;
比赛中可能会遇到USB数据大概可以分成三类:键盘、鼠标、触摸板

键盘流量

这种流量遇见的可能会比较多,在这个流量包中,键盘的IP为 1.2.1 输入这个规则可以过滤usb.src == "1.2.1"
20231102222356

然后来看一下它传输的数据。这个时候,我们一个个地点是不是很麻烦,而且不直观?
这个时候就要用上 tshark 这个命令行工具,linux下可以用(windows也有,但是我喜欢用linux版的
用这个命令 tshark -r USB_Example.pcapng -T fields -Y 'usb.src==1.2.1' -e usbhid.data
20231102223008
还是可以比较清晰地看到哪个位置是有数据的

那么这时候我们可以把两个位置的数据给提取出来,然后分别分析
tshark -r USB_Example.pcapng -T fields -Y 'usb.src==1.2.1' -e usbhid.data > usbhid.data
用如下代码提取

1
2
3
file_data = open("usbhid.data").readlines()
for i in file_data:
print(i[4:6] ,end=" ")

20231108202549
结果大概是这样,然后再去USB keyboard的官方文档去看看USB_hidusage_pdf 大概在82页
然后前面的Usage ID代表的就是流量包抓取的值,这个时候,只需要做个替换就行了

1
2
3
4
5
keyboard_dir = {
"04":"a", "05":"b", "06":"c", "07":"d", "08":"e","09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j","0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o","13":"p", "14":"q", "15":"r", "16":"s", "17":"t","18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y","1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4","22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"\n","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":" ","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>","46":"<PRTSC>","47":"<SCR>","48":"<PAUSE>","49":"<INSERT>","4a":"<HOME>","4b":"<PGUP>","4c":"<DEL FORWARD>","4d":"<END>","4e":"<PGDW>","4f":"<RIGHTARROW>","50":"<LEFTARROW>","51":"<DOWNARROW>","52":"<UPARRWO>","00":"","":""}
file_data = open("usbhid.data").readlines()
for i in file_data:
print(keyboard_dir(i[4:6]) ,end=" ")

20231108203936
第一个问题,这个为什么会有这么多重复的?因为如果你按一个键按久了,那么主机接收完一次数据,还会继续接收同一个键的数据,就会有重复的,这个没办法避免,只有还原出结果了再手动处理

第二个问题,
20231108204211
这个第二列的数据是什么?是因为,如果按键太快了,主机在一个周期接收了两个数据,那么就会将接收到的第二个数据移到第二个byte去。但是在下个周期,这个数据又会重新发送给主机

然后还有个Shift的情况,因为我本人的编程习惯,我觉得upper() lower()这两个函数很爽,我会偏向于用这两个,这个有一篇文章写得很好了,我就少写点了
这里就只是贴一下我的还原键盘的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def Value_2_PlainText(data_list):
normal_Keys = {
"04":"a", "05":"b", "06":"c", "07":"d", "08":"e","09":"f", "0a":"g", "0b":"h", "0c":"i", "0d":"j","0e":"k", "0f":"l", "10":"m", "11":"n", "12":"o","13":"p", "14":"q", "15":"r", "16":"s", "17":"t","18":"u", "19":"v", "1a":"w", "1b":"x", "1c":"y","1d":"z","1e":"1", "1f":"2", "20":"3", "21":"4","22":"5", "23":"6","24":"7","25":"8","26":"9","27":"0","28":"\n","29":"<ESC>","2a":"<DEL>", "2b":"\t","2c":" ","2d":"-","2e":"=","2f":"[","30":"]","31":"\\","32":"<NON>","33":";","34":"'","35":"<GA>","36":",","37":".","38":"/","39":"<CAP>","3a":"<F1>","3b":"<F2>", "3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>","46":"<PRTSC>","47":"<SCR>","48":"<PAUSE>","49":"<INSERT>","4a":"<HOME>","4b":"<PGUP>","4c":"<DEL FORWARD>","4d":"<END>","4e":"<PGDW>","4f":"<RIGHTARROW>","50":"<LEFTARROW>","51":"<DOWNARROW>","52":"<UPARRWO>","00":"","":""}
shift_Keys = {
"04":"A", "05":"B", "06":"C", "07":"D", "08":"E", "09":"F", "0a":"G", "0b":"H", "0c":"I", "0d":"J", "0e":"K", "0f":"L", "10":"M", "11":"N", "12":"O", "13":"P", "14":"Q", "15":"R", "16":"S", "17":"T","18":"U", "19":"V", "1a":"W", "1b":"X", "1c":"Y","1d":"Z","1e":"!", "1f":"@", "20":"#", "21":"$","22":"%", "23":"^","24":"&","25":"*","26":"(","27":")","28":"\n","29":"<ESC>","2a":"<DEL>","2b":"\t","2c":" ","2d":"_","2e":"+","2f":"{","30":"}","31":"|","32":"<NON>","33":"\"","34":":","35":"<GA>","36":"<","37":">","38":"?","39":"<CAP>","3a":"<F1>","3b":"<F2>","3c":"<F3>","3d":"<F4>","3e":"<F5>","3f":"<F6>","40":"<F7>","41":"<F8>","42":"<F9>","43":"<F10>","44":"<F11>","45":"<F12>","46":"<PRTSC>","47":"<SCR>","48":"<PAUSE>","49":"<INSERT>","4a":"<HOME>","4b":"<PGUP>","4c":"<DEL FORWARD>","4d":"<END>","4e":"<PGDW>","4f":"<RIGHTARROW>","50":"<LEFTARROW>","51":"<DOWNARROW>","52":"<UPARRWO>","00":""}
CAP_Count = 0 # 默认开始是小写
result = ""

Func_Choice = input('需要输出所有的功能键吗?[y/n]:').upper()

for i in data_list:
i = i.strip("\n")
single_press = i[4:6]
Function_press_bit = str(bin(int(i[0:2] ,16))[2:].zfill(8)[::-1])

Function_press_dir = {"[Ctrl]":0 ,"[Shift]":0 ,"[Alt]":0 ,"[Win]":0}
# 计算功能键的按下情况
if Function_press_bit[0] != "0" or Function_press_bit[4] != "0":
Function_press_dir["[Ctrl]"] = 1
if Function_press_bit[1] != "0" or Function_press_bit[5] != "0":
Function_press_dir["[Shift]"] = 1
if Function_press_bit[2] != "0" or Function_press_bit[6] != "0":
Function_press_dir["[Alt]"] = 1
if Function_press_bit[3] != "0" or Function_press_bit[7] != "0":
Function_press_dir["[Win]"] = 1

# 判断是否为有效press
if (single_press not in normal_Keys) or (single_press not in shift_Keys):
continue

# 排除重复press
if i[4:6] != "00" and i[6:8] != "00":
continue

# 判断按下大写键没
if single_press == "39":
CAP_Count += 1
continue

# 输出功能键组合
if Func_Choice == "Y":
if 1 in Function_press_dir.values() and (Function_press_dir["[Alt]"] == 1 or Function_press_dir["[Ctrl]"] == 1 or Function_press_dir["[Win]"] == 1):
for Func_tuple in Function_press_dir.items():
if Func_tuple[1] == 1:
print(f"{Func_tuple[0]} {shift_Keys[single_press]}")

# 输出按下的键位
CAP_Judge = (CAP_Count + Function_press_dir["[Shift]"])%2

if Function_press_dir["[Shift]"] == 0:
if CAP_Judge == 0:
print(normal_Keys[single_press].lower() ,end="")
elif CAP_Judge == 1:
print(normal_Keys[single_press].upper() ,end="")
else:
if CAP_Judge == 0:
print(shift_Keys[single_press].lower() ,end="")
elif CAP_Judge == 1:
print(shift_Keys[single_press].upper() ,end="")

print("\n" ,"-"*50)

键盘流量

鼠标流量

基础的鼠标流量看这一篇就能懂
鼠标流量

然后这篇讲一下不太常规的鼠标流量,还是用原来的这个流量包,第一步先把鼠标的数据给提出来
tshark -r USB_Example.pcapng -T fields -Y 'usb.src==1.5.2' -e usbhid.data > usbhid.data
20231108205825

能很明显地看出来前面02是整个数据都有的,那么就可以认为这个02就是标示位,
然后再找一下有效数据,这一步偏经验一点,但是也有技巧,就是看哪一位既有大字节(ff fe fd…)或者是小字节(01 02 03…),

所以关键就是需要找到哪个位置是小字节和大字节都出现过的,因为我现在遇到过的鼠标都是一个字节存储位移的,所以只需要找两个字节(一个x,一个y)就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import matplotlib.pyplot as plt

file = open("usbhid.data").readlines()
posx ,posy = 0 ,0
listx ,listy = [] ,[]
for i in file:
offsetx = int(i[6:8] ,16)
offsety = int(i[10:12] ,16)
if offsetx > 127:
offsetx -= 255
if offsety > 127:
offsety -= 255
# 记录位置
posx += offsetx
posy += offsety
# 储存位置到列表中
listx.append(posx)
listy.append(posy)

plt.scatter(listx ,listy)
plt.show()

这个脚本的大致逻辑就是,读取每一行鼠标的数据,再读取偏移,再将偏移存储到一个变量中,再将这个变量的值存储到一个列表中,再用pyplot画出来

数位板流量

这个没啥技巧,这个就是纯尝试。
他的数据形式是以坐标形式保存的,
圣zys写得比我明了zysgmzb’s blog

写一下怎么试的吧,因为现在网上只出现过两种数位板流量,两个流量都是来着Wacom公司的板子,所以就可以找一下共同点。

20231110164918
20231110164942

乍一看,很乱,然后这个时候,找每一行的共同点就行了,然后再忽略掉共同点
提取出来的数据就是他的坐标数据

20231110165506
20231110165547

然后再试有效位就行了,我直接贴Soer里面的函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def CTL_480(data_list):
'''
型号:CTL-480 [Intuos Pen (S)] (0x030e)
HID Data(usbhid.data): 02f1560e790fe9022800
[0:4]==02f1是有效数据 x坐标[4:8] y坐标[8:12]
坐标储存是小端储存
'''
print("[+] 型号:Wacom CTL-480 [Intuos Pen (S)] (0x030e)\n")
x_list ,y_list = [],[]
for i in data_list:
i = i.strip("\n")
if i[0:4] == "02f1":
x_pos = int(i[4:6] ,16) + int(i[6:8] ,16)*256
y_pos = int(i[8:10] ,16) + int(i[10:12] ,16)*256
x_list.append(x_pos)
y_list.append(-y_pos)
plt.scatter(x=x_list ,y=y_list ,c='hotpink')
plt.title("CTL 480")
plt.show()

def PTH_660(data_list):
'''
型号:PTH-660 [Intuos Pro (M)] (0x0357)
HID Data(usbhid.data): 10614157000a1f00bd191404000000000084308094420810004208
[16:18]!=00即为有效数据 x坐标[4:8] y坐标[10:14]
坐标储存是小端储存
'''
print("[+] Wacom 型号:PTH-660 [Intuos Pro (M)] (0x0357)\n")
x_list ,y_list = [],[]
for i in data_list:
i = i.strip("\n")
if len(i) != 54:
continue
if i[16:18] != "00" :
x_pos = int(i[4:6] ,16) + int(i[6:8] ,16)*256
y_pos = int(i[10:12] ,16) + int(i[12:14] ,16)*256
x_list.append(x_pos)
y_list.append(-y_pos)
plt.scatter(x=x_list ,y=y_list ,c='hotpink')
plt.title("PTH 660")
plt.show()

要用的时候,直接调这两个函数就行了。

这里贴上USBFlow_Soer的地址,https://github.com/y1shiny1shin/USBFlow_Soer 有github账号的还是给小弟我点个star吧,求求了

如果,我是说如果的话,装好usbpcap了,抓个USB的包发给小弟我,我好继续做这个项目,求求了
用QQ:2729913542 或者是邮箱 y1shin@163.com联系我都行,求求了