针对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,没有浪费多余的等待时间。

BACnet MSTP 帧失步

在MSTP领域有2个帧的概念:

  1. BACnet MSTP数据链路层的帧,它由最少8个字节组成,包括:2个前导字节为0x55, 0xff,帧类型,目标MAC地址,源MAC地址,2个字节的数据长度,crc8校验,如果数据长度不为零,还包括数据与数据较验。这里把这种帧称为MSTP帧。
  2. EIA-485帧,它由位组成,包括一个起始位,数据位,较验位,停止位。MSTP采用NRZ不归零编码,8个数据位,无较验位,一个停止位。起始位为0低电平,停止位为1高电平,数据位低位优先传输。这里把这种帧称为“字节帧”。

BACnet MSTP的接收有限状态机采用前导字节分辨MSTP帧的起始。如果在中途超过Tframe_abourt(最小60位时间,但是允许采用更大的值,最大可达100毫秒)没有收到数据或错误,则放弃帧,重新开始搜寻帧前导字节。

因为前导字节可以出现在MSTP帧的数据部分(Addendum 135-2012an 引入的扩展帧的数据及数据较验部分采用COBS编码避免出现前导字节)所以如果出现帧失步,接收状态机有可能会把前一个MSTP帧的数据部分认为是新的MSTP帧。

MSTP帧之间最小时间间隔是Tturnaround(40位时间)小于Tframe_abort,如果接收状态机对MSTP帧的解析失步,有可能会跨过帧间隔继续解析。

MSTP帧失步有几个可能原因:

  1. 发送设备与接收设备的程序漏洞,这可以通过代码审查与除错解决。
  2. 时间精度. BACnet MSTP标准仅要求1%精度的定时器,分辨率最小5毫秒。而应答方的最大延迟时间与等待方的最小超时之间的时间冗余只有5毫秒(如Tusage_delay与Tusage_timeout, Treply_delay与Treply_timeout),因此非常容易触发冲突引起失步。
  3. 总线上噪声。噪声导致的接收错误或数据校验错引起失步。

有人可以争辨说MSTP帧有crc校验保护。即使不考虑恶意设备(这里是一个例子)和随机数据碰撞, 现实中仍然有可能在帧数据中包含完整的MSTP帧。

(修订于2021.06.25)例如,市场上已经很多支持MSTP包捕捉的产品。捕获的数据也可能通过BACnet服务来传输(PrivateTransfer或AtomicReadFile?),假设当数据传输经过一个MSTP子网时,总线上出现电子噪音,会发生什么情况呢?

这些帧失步的后果不仅仅是可能导致总线拥堵,甚至设备的误动作(如果数据部分的帧是个APDU),更有甚者如果错误的帧是路由配置包的话,可能导致整个BACnet互联网络瘫痪。


字节帧失步

字节帧的失步,可能原因一般是:数据噪声,不当的终端电阻,总线未偏置引起。失步的表现有两个,一是将数据位的1解析为总线的空闲,二是将数据位的0解析为新字节的起始位。失步除了导致接收到错误的数据,如果发生在MSTP帧的最后一个字节,将可能引起对总线空闲时间的测量误差(可能过长或过短)。

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

一次最大发包数与根据令牌占用时间

从固件版本2.0开始,BACRouter引入一个新的特性:根据令牌占用时间的一次最大发包数。

在BACnet MSTP标准中,一个主站得到令牌后,可以发送“一次最大发包数”的包后,再传递出令牌。“一次最大发包数”的默认值是1。路由器作为流量汇聚点,提高这个值可以改进网络交换带宽,但是会增加令牌占用时间。大多数路由的建议值为5到20。

MSTP作为常见的现场总线,通常由控制器、传感器、执行器互联,这些设备构成直接的控制回路,数据交换延迟通常要得到保证。我们建议设备得到令牌的时间间隔要小于1秒 。

路由器发送的NPDU的长度通常在10~50字节之间。但是最大可达到501字节(或扩展帧的1497字节)。越大的帧需要越长的时间来收发。

对于需要回应的NPDU来说,路由器需要等待目标设备应答。通常目标设备需要更长的时间来处理长帧,即路由器需要等待更长的时间。

所以同样的“一次最大发包数”,每次路由持用令牌的时间变化很大,对MSTP总线的延迟保证非常不利。

为避免这个问题,我们引进“根据令牌占用时间”特性来限制路由器持有令牌的时间。这个特性启用后。路由器不计算发包数,而是计量持有令牌的时间,当时间达到:

每字节发送时长 * 32 * “一次最大发包数”

就不再发送新帧,并传出令牌。例如“一次最大发包数”为10, 波特率为76.8kbps,每字节发送时长为0.13毫秒,则最大令牌持有时间为:

0.13 * 32 * 10 = 41.6 毫秒.

这个特性可以通过WebUI方便地启用与关闭。