识别未知串行协议的帧长度
假设你正在逆向工程一个串行协议。你已经知道串行端口的正确配置(波特率等),并且你假设协议由等长的帧构建。
为简单起见,我们还将假设设备发送数据而不需要先从它请求数据。如果不是这种情况,你可以使用不同且更简单的方法(只需发送请求字符并查看
下一步是确定协议的帧长度。本文不仅详细介绍了你可以用来执行此操作的算法之一的两个变体,还提供了一个现成的 Python 脚本,你可以用于你自己的协议。 方法 1:使用 argmax 的自相关
我们将使用简单的数学方法来找出最可能的帧长度。这基于帧将具有高度自相似性的假设,即单个帧中的许多字节将匹配下一帧中的相应字节。
不要求每个帧中的所有字节都相同,但如果每个帧中的字节完全不同,该方法可能无法推断出正确的帧长度。
此方法基于自相关。虽然听起来复杂,但意思不过是将序列与其延迟/移位版本进行比较。
这意味着我们将执行以下步骤:
- 从串行设备读取一组字符
- 将字符集与其移位版本相关
- 帧长度是最大相似性发生的移位(使用
np.argmax)
作为相似性分数,我们将使用 1 如果字节相等否则 0。对于特定协议,引入单独的位匹配可能是更可行的方法,但这也会在过程中引入噪声。
对于我见过的大多数简单协议,此方法对 ASCII 和二进制都非常有效。
绘制相关性如下所示:
方法 2:多移位感知自相关
此修改后的算法适用于任何两帧之间相似性不显著或存在大量噪声的协议。对于此类协议,最大分数方法不会产生正确结果。
但是,我们可以使用等长帧协议的特性,即通过将帧移动(未知)帧长度的整数倍来获得高匹配分数。我们不是只取一个最大峰值,而是将任何长度的整数倍的所有分数相乘。
虽然此方法听起来与第一种相比不太复杂,但它有更多注意事项和陷阱,例如在相关性结果数组的后半部分的数组内没有整数倍,而第二个四分之一不太显著,因为没有太多倍数可以相乘。
脚本(见下文)通过仅计算可能结果空间的第一个四分之一来解决这些问题。使用 -n 参数增加脚本读取的字符数。
计算多移位感知相关性后,我们可以像第一种方法一样使用 argmax 找到最佳相关性。有时由于噪声,这会识别帧长度的倍数。你可以查看图表(使用 -p)并手动确定帧长度以找到正确的帧长度。
从结果中可以看出,“噪声”(在帧长度移位匹配之间,由字符之间的随机匹配引起)基本消失了。
在许多实际用例中,此算法会在图表中产生更明显的信号,但自动计算的帧大小将不正确,因为几种效应倾向于增加帧高度倍数的波瓣高度。因此,在将结果视为理所当然之前,建议查看图表(脚本中的 -p)。
自动化算法
以下是本文的 Python3 脚本,无需修改即可很好地工作,但对于某些协议,你可能需要调整它以满足你的需求:
#!/usr/bin/env python3
"""
ProtocolFrameLength.py
确定具有等长帧且每帧包含相似字节的未知串行协议的帧长度。
有关说明,请参见
https://techoverflow.net/2017/07/30/identifying-the-frame-length-for-an-unknown-serial-protocol/
示例用法:
$ python3 ProtocolFrameLength.py -b 115200 /dev/ttyACM0
"""
import serial
import numpy as np
import math
from functools import reduce
import operator
__author__ = "Uli Köhler"
__license__ = "Apache License v2.0"
__version__ = "1.0"
__email__ = "ukoehler@techoverflow.net"
def match_score(c1, c2):
"""
两个字符 c1 和 c2 的相关性分数。
使用简单的二进制分数
"""
if c1 is None or c2 is None: # 填充字符
return 0
return 1 if c1 == c2 else 0
def string_match_score(s1, s2):
assert len(s1) == len(s2)
ln = len(s1)
return sum(match_score(s1[i], s2[i]) for i in range(ln))
def compute_correlation_scores(chars, nomit=-1):
# 省略最后 nomit 个字符,因为单字符匹配会被过高估值
if nomit == -1: # 自动值
nomit = len(chars) // 10
corr = np.zeros(len(chars) - nomit)
# 注意:零移位的自相关始终为 1,故意省略!
for i in range(1, corr.size):
# 用 None 构建前缀
prefix = [None] * i
s2 = prefix + list(chars[:-i])
# 由于 None 和序列长度(有 i 个 None)可达到的最大分数进行归一化
corr[i] = string_match_score(chars, s2) / (len(chars) - i)
return corr
def print_most_likely_frame_length(correlations):
# 找到最大的相关系数。此模型不需要阈值
idx = np.argmax(correlations)
print("帧长度可能是 {} 字节".format(idx))
def plot_correlations(correlations):
from matplotlib import pyplot as plt
plt.style.use("ggplot")
plt.title("具有 16 字节帧的协议的相关性分数")
plt.gcf().set_size_inches(20,10)
plt.plot(correlations)
plt.title("相关性分数")
plt.ylabel("归一化相关性分数")
plt.xlabel("移位")
plt.show()
def multishift_adjust(correlations):
"""
多移位感知算法
"""
corr_multishift = np.zeros(correlations.size // 4)
for i in range(1, corr_multishift.size):
# 迭代 i 的倍数(包括 i 本身)
corr_multishift[i] = reduce(operator.mul,
(correlations[j] for j in range(i, correlations.size, i)), 1)
return corr_multishift
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('port', help='要使用的串行端口')
parser.add_argument('-b', '--baudrate', type=int, default=9600, help='要使用的波特率')
parser.add_argument('-n', '--num-bytes', type=int, default=200, help='要读取的字符数')
parser.add_argument('-m', '--multishift', action="store_true", help='使用多移位感知自相关算法')
parser.add_argument('-p', '--plot', action="store_true", help='绘制结果相关性矩阵')
args = parser.parse_args()
ser = serial.Serial(args.port, args.baudrate)
ser.reset_input_buffer()
chars = ser.read(args.num_bytes)
corr = compute_correlation_scores(chars)
if args.multishift:
corr = multishift_adjust(corr)
print_most_likely_frame_length(corr)
if args.plot:
plot_correlations(corr)用法示例:
python3 ProtocolFrameLength.py -b 115200 /dev/ttyACM0你可以使用 -m 使用多移位感知方法。
使用 -p 如上所示绘制结果。如果其中一种方法不起作用,建议绘制结果以查看图表中是否有未被检测到的可见内容
由于结果取决于读取的实际数据,建议执行多次运行并查看结果是否不同。