针对BACnet MSTP的帧失步攻击

如前面的文章 “BACnet MSTP 帧失步” 所指, BACnet MSTP 有一个帧失步的设计缺陷,但是是否可以利用这个缺陷,在完全遵守协议的前提下,对MSTP总线进行破坏呢?

设计这个攻击,我们先做以下几个假设:

  1. 总线上至少有3个设备,MAC地址分别为1, 8, 10。其中设备1是精心设计用来发动攻击的,设备8与10是无辜的。
  2. 设备1支持扩展帧,设备8与10不支持。
  3. 这3个设备的定时器都足够精确。

设备1的工作流程如下:

  1. 得到令牌,发送A帧
  2. 传递令牌到其它设备
  3. 再次得到令牌时,发送B帧
  4. 传递令牌到其它设备
  5. 重复步骤1.

A帧是一个合法的私有数据帧,十六进制数据如下:

55 ff 80 ff 01 00 1d a3 02 2b 72 fe 55 ff 03 08 01 00 11 a0 ff 55 ff 21 01 08 00 09 ce d4 f3 55 ff 00 01 08 00 00 bf

B帧也是一个合法的私有数据帧,十六进制数据如下:

55 ff 80 ff 01 00 1d a3 02 2b fe dc 55 ff 03 0a 01 00 11 b1 ff 55 ff 21 01 0a 00 09 fd 8a 51 55 ff 00 01 0a 00 00 8c

如果没有帧失步,一切都将正常运行。但是可能几小时,也可能几天后,设备8对设备1发出的A帧失步了,错过了A帧的帧头(设备10如对B帧失步,也是同样的效果),则设备8继续扫描A帧的数据部分,发现另一个有效帧:

55 ff 03 08 01 00 11 a0 ff 55 ff 21 01 08 00 09 ce d4 f3 55 ff 00 01 08 00 00 bf

这是发给设备8的Test-Request帧,设备8等待Tturnaround后发送Test-Response帧进行应答:

55 ff 04 01 08 00 11 ae ff 55 ff 21 01 08 00 09 ce d4 f3 55 ff 00 01 08 00 00 bf

但是此时,设备1正在传出令牌:

55 ff 00 02 01 00 00 73

令牌帧与Test_Response的前8个字节冲突了,对于设备10来说,收到了几个错误字节后,继续扫描,在Test_Response的数据部分又发现了一个帧:

55 ff 21 01 08 00 09 ce d4 f3 55 ff 00 01 08 00 00 bf

对设备10来说,这不是发给它的帧,所以他进入SKIP-DATA状态,抛弃数据,等这个帧结束,但是直到设备8发完数据,设备10还差一个字节来结束帧,他将继续等待。

对设备1来说,它发出令牌帧后,收到如下数据:

55 ff 21 01 08 00 09 ce d4 f3 55 ff 00 01 08 00 00 bf

这是一个扩展数据帧,因为它支持扩展帧,所以他按 Addendum 135-2012an规定的流程校验帧头,发现数据长度过短,中断前帧后又开始扫描,发现新帧:

55 ff 00 01 08 00 00 bf

这是一个发给设备1的令牌帧,设备1又得到令牌,经过Tturnaround后,重新发送B帧:

55 ff 80 ff 01 00 1d a3 02 2b fe dc 55 ff 03 0a 01 00 11 b1 ff 55 ff 21 01 0a 00 09 fd 8a 51 55 ff 00 01 0a 00 00 8c

在前面提到,设备10还差1个字节来结束前面一帧,因Tframe_abort>Ttrurnaround,所以解析没有中断,B帧的第一个55字节被设备10抛弃,然后开始扫描新帧,发现了:

55 ff 03 0a 01 00 11 b1 ff 55 ff 21 01 0a 00 09 fd 8a 51 55 ff 00 01 0a 00 00 8c

这是一个发给设备10的Test-Request帧,事情又开始重复。

从上面可以看出,每个设备都严格地遵守标准,但是一旦帧失步发生,整条总线就永远地瘫痪了。

更多信息见:MSTP帧失步解决方案

MSTP帧失步解决方案

于2021.7.13更新

我们曾在下面的文章中讨论过BACnet MSTP协议中有帧失步的弱点:

BACnet MSTP 帧失步

针对 BACnet MSTP 的帧失步攻击

对于BACRouter 来说,怎么来防范这个漏洞呢?让我们从标准找线索:

9.5.2 变量

SilenceTimer(翻译为静默计时器): 名义精度5ms的计时器,每当总线上有活动或每发送一个字节后清零

9.5.3 参数

Tframe_gap(翻译为字节间隔): 节点在发送一个帧时,在两个字节之间允许的最长的空闲时间: 20位时间。以我们的经验市面上几乎所有MSTP设备的字节间隔为0

Tturnaround: 节点接收到最后一个字节与开始发送之间的最小时间间隔: 40位时间

Tpostdrive: 节点发送完最后一个字节的停止位,到关闭485的驱动器之间的最长时间: 15位时间

9.5.5 发送帧流程

当SilenceTimer小于Tturnaround时, 等待 (Tturnaround – SilenceTimer)时间

9.2.3 时序

驱动器关闭: 节点应该在一个帧的最后字节的停止位起的Tpostdrive时间内关闭驱动器。标准允许但不鼓励,在帧的最后一个字节后再发送一个填充字节,如果填充字节被使用,它必须是0xFF,填充字节不被认为是帧的一部分,且应该包括在Tpostdrive时间内。

(此处并不明确Tturnaround是否包括填充字节的时间,但是在135.1的测试标准的12.1.3.4章节就描述得很清晰:Tturnaround从最后一个字节的停止位后开始计时,如果节点采用了填充字节,则应该由填充字节前一个字节的停止位后开始)

如此可见,在一个正常帧内,2个字节之间的最大空闲时间为20位时间,如果计上前一个字节拖尾的“1”位,则最长的总线空闲时间为29位时间(假设前一个字节为0xFF)。

考虑到填充字节,两帧之间的最小总线空闲时间为:Tturnaround – Tpostdrive + 9 (填充字节拖尾的”1″的位数)= 34位时间

所以BACRouter采用一个改进的接收有限状态机:

  1. 在一个MSTP帧中,字节间隔如大于20位时间,认为MSTP帧中断。
  2. 总线空闲大于等于33位时间,认为新的MSTP帧出现。
  3. 为了尽量兼容部分不遵守Tturnaround的设备,所有在有效MSTP帧后的数据认为是新的一帧。

在115200波特率下,一个数据位的时间仅8.7微秒,为了精确地测量空闲时间,BACRouter采用了5微秒精度的定时器,它有效地防止帧失步出现,并且 在115.2kbps下达到98.8%的带宽利用率 因为BACRouter发包时精确地遵守40位的Tturnaround,没有浪费多余的等待时间。