车辆内部有线通信有 20 多种不同的通信协议。大多数车辆使用五到十种不同的协议进行内部通信。原始设备制造商 (OEM) 使用哪种通信协议的决定通常是通过通信技术成本和最终汽车价格之间的权衡来做出的。ECU 间通信的四种主要通信技术是控制器局域网 (CAN)、FlexRay、本地互连网络 (LIN) 和汽车以太网。出于安全考虑,这些是车辆有线通信最相关的协议。
LIN 是用于低数据速率的单线通信协议。车辆的执行器和传感器与充当 LIN 主机的 ECU 交换信息。可以通过 LIN 进行软件更新,但 LIN 从机通常不需要软件更新,因为它们的功能有限。
CAN 是迄今为止车辆中最常用的用于 ECU 间通信的通信技术。在较旧或更便宜的车辆中,CAN 仍然是车辆主干通信的主要协议。车辆运行期间的安全关键通信、诊断信息和软件更新通过 CAN 在 ECU 之间传输。协议本身缺乏安全特性,结合通用性,使得 CAN 成为安全调查的主要协议。
FlexRay 联盟将 FlexRay 设计为 CAN 的继任者。现代车辆对通信带宽的要求更高。根据设计,FlexRay 是一种用于 ECU 间通信的快速可靠的通信协议。FlexRay 组件比 CAN 组件更昂贵,导致 OEM 更有选择性地使用。
最近的上层汽车采用汽车以太网,这是一种用于内部车辆通信的新骨干技术。快速增长的带宽需求已经取代了 FlexRay。这些需求的主要原因是驾驶员辅助和自动驾驶功能。只有开放系统互连 (OSI) 模型的物理层(第 1 层)将以太网 (IEEE 802.3) 与汽车以太网 (BroadR-Reach) 区分开来。这种设计决策带来了多种优势。例如,操作系统的通信堆栈可以在不修改和路由、过滤和防火墙系统的情况下使用。汽车以太网组件已经比 FlexRay 组件便宜,这将导致汽车拓扑结构中 CAN 和汽车以太网是最常用的通信协议。
第一辆配备 CAN 总线的车辆使用具有线路总线拓扑的单一网络。如今,一些价格较低的车辆仍然使用一两个共享的 CAN 总线网络进行内部通信。这种拓扑的缺点是它的脆弱性和缺乏网络分离。车辆的所有 ECU 都连接在共享总线上。由于 CAN 不支持其协议定义中的安全功能,因此该总线上的任何参与者都可以直接与所有其他参与者进行通信,这使得攻击者可以通过破坏单个 ECU 来影响所有 ECU,甚至是安全关键的 ECU。该网络的整体安全级别由最弱参与者的安全级别给出。
中央网关 (GW) 拓扑可以在价格较高的旧车和中价到低价的近期汽车中找到。集中式 GW ECU 分离特定领域的子网络。这允许 OEM 将所有具有远程攻击面的 ECU 封装在一个子网络中。具有安全关键功能的 ECU 位于单独的 CAN 网络中。除了 CAN,FlexRay 还可以用作单独网络域内的通信协议。此拓扑中的安全关键网络的安全性主要取决于中央 GW ECU 的安全性。这种架构通过域分离提高了车辆的整体安全级别。在攻击者通过任意攻击面成功利用 ECU 后,需要第二个可利用漏洞或逻辑错误来破坏不同的域,车内的安全关键网络。第二个漏洞利用或逻辑错误对于克服中央 GW ECU 的网络分离是必要的。
在最新的高价车辆中可以找到具有中央 GW 和域控制器 (DC) 的新拓扑。具有自动驾驶和驾驶员辅助功能的现代车辆对带宽不断增长的需求导致了这种拓扑结构。汽车以太网网络用作整个车辆的通信骨干网。通过 DC 与中央 GW 连接的各个域构成了车辆的主干。各个 DC 可以控制和调节域与车辆骨干网之间的数据通信。这种拓扑结构通过与作为网关和防火墙的各个 DC 到车辆骨干网络的强大网络分离实现了非常高的安全级别。OEM 在此安全改进旁边具有动态信息路由的优势,这是按需特性 (FoD) 服务的推动力。
CAN 通信技术于 1983 年发明,是一种基于消息的稳健车辆总线通信系统。Robert Bosch GmbH 在 CAN 标准中设计了多种通信功能,以实现用于控制器局域网的稳健且计算高效的协议。CAN 通信行为的显着特点是用于传输错误的内部状态机。此状态机实现了故障静默行为,以保护安全关键网络免受笨拙的白痴节点的影响。如果发生接收错误 (REC) 或传输错误 (TEC) 的特定限制,CAN 驱动程序会将其状态从错误主动更改为错误被动,最后变为总线关闭。
近年来,该协议规范被滥用于针对车辆 CAN 网络的拒绝服务 (DoS) 攻击和信息收集攻击。乔等人。通过滥用 ECU 的总线关闭状态,演示了针对 CAN 网络的 DoS 攻击。在特定节点的 CAN 帧中注入通信错误会导致受攻击节点的传输错误计数很高,从而迫使受攻击节点进入总线关闭状态。2019 年 Kulandaivel 等人。将此攻击与统计分析相结合,在车载网络中实现快速且廉价的网络映射. 他们结合了总线关闭攻击应用于节点之前和之后的 CAN 网络流量的统计分析。ECU 受到攻击后网络流量中所有丢失的 CAN 帧现在都可以映射到受到攻击的 ECU,帮助研究人员识别 CAN 帧的源 ECU。Ken Tindell 在 2019 年发布了对 CAN 的低级攻击的综合总结。
上图显示了通过网络传输的 CAN 帧及其字段。对于信息交换,只有仲裁、控制和数据字段是相关的。这些是普通应用软件可以访问的唯一字段。所有其他字段都在硬件层上进行评估,并且在大多数情况下,不会转发给应用程序。数据字段具有可变长度,最多可容纳 8 个字节。数据域的长度由控制域内的数据长度码指定。此示例的重要变体是具有扩展仲裁字段的 CAN 帧和控制器局域网灵活数据速率 (CAN FD) 协议。在 Linux 上,每个接收到的 CAN 帧都会传递给 SocketCAN。SocketCAN 允许通过操作系统的网络套接字处理 CAN。下图显示了帧结构,如果用户级应用程序从 CAN 套接字接收数据,CAN 帧是如何编码的。
通过车辆或独立域中的 CAN 通信,ECU 交换传感器数据和控制输入;这些数据主要是不安全的,并且可以被攻击者修改。攻击者可以轻松地欺骗 CAN 总线上的传感器值,以触发其他 ECU 的恶意反应。Miller 和 Valasek 在他们对汽车网络的研究中描述了这种欺骗攻击。为防止对通过 CAN 传输的安全关键数据的攻击,汽车开放系统架构 (AUTOSAR) 发布了安全车载通信规范 。
CAN 协议仅支持 8 个字节的数据。诊断操作或 ECU 编程等用例需要比 CAN 协议支持的负载高得多的负载。为此,汽车行业标准化了传输层 (ISO-TP) (ISO 15765-2) 协议
可扩展的面向服务的 IP 中间件 (SOME/IP) 定义了汽车网络中数据通信的新理念。SOME/IP 用于在最新的车载网络中的网络域控制器之间交换数据。SOME/IP 支持订阅和通知机制,允许域控制器根据车辆状态动态订阅另一个域控制器提供的数据。SOME/IP 在域控制器和车辆正常运行期间所需的网关之间传输数据。SOME/IP 的用例类似于 CAN 通信的用例。主要目的是ECU之间传感器和执行器数据的信息交换。这种用法强调将某些/IP 通信作为网络攻击的奖励目标。
通用测量和校准协议 (XCP),CAN 校准协议 (CCP) 的继承者,是汽车系统的校准协议,由 ASAM eV 于 2003 年标准化。XCP 的主要用途是在 ECU 或车辆开发的测试和校准阶段. CCP 专为在 CAN 上使用而设计。CCP 中没有消息超过 CAN 的 8 字节限制。为了克服这个限制,XCP 旨在与广泛的传输协议兼容。XCP 可用于 CAN、CAN FD、串行外设接口 (SPI)、以太网、通用串行总线 (USB) 和 FlexRay。CCP 和 XCP 的特性非常相似;但是,XCP 具有更大的功能范围和对数据效率的优化。
两种协议都具有基于会话的通信过程,并支持通过主节点和多个从节点之间的种子和密钥机制进行身份验证。主节点通常是工程个人计算机 (PC)。在车辆中,从节点是用于配置的 ECU。XCP 还支持仿真。车辆工程师可以通过 XCP 调试 MATLAB Simulink 模型。在这种情况下,模拟模型充当 XCP 从节点。CCP 和 XCP 可以读取和写入 ECU 的内存。另一个主要特点是数据采集。两种协议都支持允许工程师使用感兴趣的内存地址配置所谓的数据采集列表的过程。此类列表中指定的所有内存都将被定期读取,并在所选通信通道上的 CCP 或 XCP 数据采集 (DAQ) 数据包中进行广播。下图概述了 XCP 中所有支持的通信和数据包类型。在命令传输对象 (CTO) 区域中,所有通信都遵循始终由 XCP 主站发起的请求和响应过程。命令包 (CMD) 可以接收命令响应包 (RES)、错误 (ERR) 包、事件包 (EV) 或服务请求包 (SERV) 作为响应。通过 CTO CMD 配置从机后,从机可以侦听刺激 (STIM) 数据包并定期发送配置的 DAQ 数据包。下图中的资源部分指示了攻击者可能滥用的此协议(编程 (PGM)、校准 (CAL)、DAQ、STIM)的可能攻击面。此类协议对车辆的安全性和安全性至关重要。
以下示例假设 Scapy 会话中的 CAN 层已加载。如果不是,可以在 Scapy 会话中使用以下命令加载 CAN 层:
>>> load_layer("can")
创建标准 CAN 帧:
>>> frame = CAN(identifier=0x200, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')
创建扩展 CAN 帧:
frame = CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08')
>>> frame.show()
###[ CAN ]###
flags= extended
identifier= 0x10010000
length= 8
reserved= 0
data= '\x01\x02\x03\x04\x05\x06\x07\x08'
CAN 帧可以从pcap
文件中写入和读取:
x = CAN(identifier=0x7ff,length=8,data=b'\x01\x02\x03\x04\x05\x06\x07\x08')
wrpcap('/tmp/scapyPcapTest.pcap', x, append=False)
y = rdpcap('/tmp/scapyPcapTest.pcap', 1)
此外,可以从candump
输出和日志文件中导入 CAN 帧。类CandumpReader
可以像使用socket
对象一样使用。这允许您使用sniff
Scapy 的其他功能:
with CandumpReader("candump.log") as sock:
can_msgs = sniff(count=50, opened_socket=sock)
为了支持 DBC 文件格式, Scapy 中添加SignalFields
了SignalPacket
类。SignalFields
只能在SignalPacket
. 多路复用器字段 (MUX) 可以通过ConditionalFields
. 以下示例演示了用法:
DBC Example:
BO_ 4 muxTestFrame: 7 TEST_ECU
SG_ myMuxer M : 53|3@1+ (1,0) [0|0] "" CCL_TEST
SG_ muxSig4 m0 : 25|7@1- (1,0) [0|0] "" CCL_TEST
SG_ muxSig3 m0 : 16|9@1+ (1,0) [0|0] "" CCL_TEST
SG_ muxSig2 m0 : 15|8@0- (1,0) [0|0] "" CCL_TEST
SG_ muxSig1 m0 : 0|8@1- (1,0) [0|0] "" CCL_TEST
SG_ muxSig5 m1 : 22|7@1- (0.01,0) [0|0] "" CCL_TEST
SG_ muxSig6 m1 : 32|9@1+ (2,10) [0|0] "mV" CCL_TEST
SG_ muxSig7 m1 : 2|8@0- (0.5,0) [0|0] "" CCL_TEST
SG_ muxSig8 m1 : 0|6@1- (10,0) [0|0] "" CCL_TEST
SG_ muxSig9 : 40|8@1- (100,-5) [0|0] "V" CCL_TEST
BO_ 3 testFrameFloat: 8 TEST_ECU
SG_ floatSignal2 : 32|32@1- (1,0) [0|0] "" CCL_TEST
SG_ floatSignal1 : 7|32@0- (1,0) [0|0] "" CCL_TEST
此 DBC 描述的 Scapy 实现:
class muxTestFrame(SignalPacket):
fields_desc = [
LEUnsignedSignalField("myMuxer", default=0, start=53, size=3),
ConditionalField(LESignedSignalField("muxSig4", default=0, start=25, size=7), lambda p: p.myMuxer == 0),
ConditionalField(LEUnsignedSignalField("muxSig3", default=0, start=16, size=9), lambda p: p.myMuxer == 0),
ConditionalField(BESignedSignalField("muxSig2", default=0, start=15, size=8), lambda p: p.myMuxer == 0),
ConditionalField(LESignedSignalField("muxSig1", default=0, start=0, size=8), lambda p: p.myMuxer == 0),
ConditionalField(LESignedSignalField("muxSig5", default=0, start=22, size=7, scaling=0.01), lambda p: p.myMuxer == 1),
ConditionalField(LEUnsignedSignalField("muxSig6", default=0, start=32, size=9, scaling=2, offset=10, unit="mV"), lambda p: p.myMuxer == 1),
ConditionalField(BESignedSignalField("muxSig7", default=0, start=2, size=8, scaling=0.5), lambda p: p.myMuxer == 1),
ConditionalField(LESignedSignalField("muxSig8", default=0, start=3, size=3, scaling=10), lambda p: p.myMuxer == 1),
LESignedSignalField("muxSig9", default=0, start=41, size=7, scaling=100, offset=-5, unit="V"),
]
class testFrameFloat(SignalPacket):
fields_desc = [
LEFloatSignalField("floatSignal2", default=0, start=32),
BEFloatSignalField("floatSignal1", default=0, start=7)
]
bind_layers(SignalHeader, muxTestFrame, identifier=0x123)
bind_layers(SignalHeader, testFrameFloat, identifier=0x321)
dbc_sock = CANSocket("can0", basecls=SignalHeader)
pkt = SignalHeader()/testFrameFloat(floatSignal2=3.4)
dbc_sock.send(pkt)
此示例使用类SignalHeader
作为标题。有效负载由 individual 指定SignalPackets
。 bind_layers
将标头与取决于 CAN 标识符的有效载荷组合在一起。如果您想直接SignalPackets
从您的 接收,请将CANSocket
参数提供basecls
给您的init
函数CANSocket
。
创建一个简单的原生 CANSocket:
conf.contribs['CANSocket'] = {'use-python-can': False} #(default)
load_contrib('cansocket')
# Simple Socket
socket = CANSocket(channel="vcan0")
创建本机 CANSocket 仅侦听 Id == 0x200 的消息:
socket = CANSocket(channel="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x7FF}])
创建本机 CANSocket 仅侦听 Id >= 0x200 和 Id <= 0x2ff 的消息:
socket = CANSocket(channel="vcan0", can_filters=[{'can_id': 0x200, 'can_mask': 0x700}])
创建本机 CANSocket 仅侦听 Id != 0x200 的消息:
socket = CANSocket(channel="vcan0", can_filters=[{'can_id': 0x200 | CAN_INV_FILTER, 'can_mask': 0x7FF}])
创建具有多个 can_filters 的本机 CANSocket:
socket = CANSocket(channel='vcan0', can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff},
{'can_id': 0x400, 'can_mask': 0x7ff},
{'can_id': 0x600, 'can_mask': 0x7ff},
{'can_id': 0x7ff, 'can_mask': 0x7ff}])
创建一个本机 CANSocket,它也接收自己的消息:
socket = CANSocket(channel="vcan0", receive_own_messages=True)
嗅探 CANSocket:
在 Windows、OSX 或 Linux 上使用各种 CAN 接口需要 python-can。python-can 库通过 CANSocket 对象使用。要创建 python-can CANSocket 对象,interface.Bus
必须使用 python-can 对象的所有参数来初始化 CANSocket。
创建python-can CANSocket的方法:
conf.contribs['CANSocket'] = {'use-python-can': True}
load_contrib('cansocket')
创建一个简单的python-can CANSocket:
socket = CANSocket(bustype='socketcan', channel='vcan0', bitrate=250000)
创建一个带有多个过滤器的 python-can CANSocket:
socket = CANSocket(bustype='socketcan', channel='vcan0', bitrate=250000,
can_filters=[{'can_id': 0x200, 'can_mask': 0x7ff},
{'can_id': 0x400, 'can_mask': 0x7ff},
{'can_id': 0x600, 'can_mask': 0x7ff},
{'can_id': 0x7ff, 'can_mask': 0x7ff}])
这个例子展示了如何在虚拟 CAN 接口上使用桥接和嗅探。对于现实世界的应用,请使用真正的 CAN 接口。在 Linux 终端上设置两个 vcan:
sudo modprobe vcan
sudo ip link add name vcan0 type vcan
sudo ip link add name vcan1 type vcan
sudo ip link set dev vcan0 up
sudo ip link set dev vcan1 up
导入模块:
import threading
load_contrib('cansocket')
load_layer("can")
创建可以攻击的套接字:
socket0 = CANSocket(channel='vcan0')
socket1 = CANSocket(channel='vcan1')
创建一个函数以使用线程发送数据包:
def sendPacket():
sleep(0.2)
socket0.send(CAN(flags='extended', identifier=0x10010000, length=8, data=b'\x01\x02\x03\x04\x05\x06\x07\x08'))
创建转发或更改数据包的功能:
def forwarding(pkt):
return pkt
创建一个在两个套接字之间桥接和嗅探的函数:
def bridge():
bSocket0 = CANSocket(channel='vcan0')
bSocket1 = CANSocket(channel='vcan1')
bridge_and_sniff(if1=bSocket0, if2=bSocket1, xfrm12=forwarding, xfrm21=forwarding, timeout=1)
bSocket0.close()
bSocket1.close()
创建用于发送数据包以及桥接和嗅探的线程:
threadBridge = threading.Thread(target=bridge)
threadSender = threading.Thread(target=sendMessage)
启动线程:
threadBridge.start()
threadSender.start()
嗅探数据包:
packets = socket1.sniff(timeout=0.3)
关闭套接字:
socket0.close()
socket1.close()
CCP源自CAN。CAN 报头是 CCP 帧的一部分。CCP 有两种类型的消息对象。一种称为命令接收对象(CRO),另一种称为数据传输对象(DTO)。通常 CRO 被发送到 Ecu,而 DTO 从 Ecu 接收。如果一个 DTO 回答 CRO,则该信息是通过计数器字段 (ctr) 实现的。如果两个对象具有相同的计数器值,则可以从关联的 CRO 对象的命令中解释 DTO 对象的有效负载。
创建 CRO 消息:
load_contrib('automotive.ccp')
CCP(identifier=0x700)/CRO(ctr=1)/CONNECT(station_address=0x02)
CCP(identifier=0x711)/CRO(ctr=2)/GET_SEED(resource=2)
CCP(identifier=0x711)/CRO(ctr=3)/UNLOCK(key=b"123456")
如果我们对 Ecu 的 DTO 不感兴趣,我们可以像这样发送 CRO 消息:发送 CRO 消息:
pkt = CCP(identifier=0x700)/CRO(ctr=1)/CONNECT(station_address=0x02)
sock = CANSocket(bustype='socketcan', channel='vcan0')
sock.send(pkt)
如果我们对 Ecu 的 DTO 感兴趣,我们需要将 CANSocket 的 basecls 参数设置为 CCP,我们需要使用 sr1:发送 CRO 消息:
cro = CCP(identifier=0x700)/CRO(ctr=0x53)/PROGRAM_6(data=b"\x10\x11\x12\x10\x11\x12")
sock = CANSocket(bustype='socketcan', channel='vcan0', basecls=CCP)
dto = sock.sr1(cro)
dto.show()
###[ CAN Calibration Protocol ]###
flags=
identifier= 0x700
length= 8
reserved= 0
###[ DTO ]###
packet_id= 0xff
return_code= acknowledge / no error
ctr= 83
###[ PROGRAM_6_DTO ]###
MTA0_extension= 2
MTA0_address= 0x34002006
由于 sr1 调用了 answers 函数,我们的 DTO 对象的有效负载被我们的 CRO 对象的命令解释。
XCP 是 CCP 的继承者。它可用于多种协议。Scapy 包括 CAN、UDP 和 TCP。XCP 有两种消息类型:命令传输对象(CTO)和数据传输对象(DTO)。CTO 发送给 Ecu 的是请求(命令),Ecu 必须以肯定响应或错误来回复。此外,Ecu 可以发送 CTO 通知主设备异步事件 (EV) 或请求服务执行 (SERV)。Ecu 发送的 DTO 称为 DAQ(数据采集),包括测量值。Ecu 接收的 DTO 用于周期性刺激,称为 STIM(刺激)。
创建 CTO 消息:
CTORequest() / Connect()
CTORequest() / GetDaqResolutionInfo()
CTORequest() / GetSeed(mode=0x01, resource=0x00)
要通过 CAN 发送消息,必须添加标头:
pkt = XCPOnCAN(identifier=0x700) / CTORequest() / Connect()
sock = CANSocket(iface=can.interface.Bus(bustype='socketcan', channel='vcan0'))
sock.send(pkt)
如果我们对 Ecu 的响应感兴趣,我们需要将 CANSocket 的 basecls 参数设置为 XCPonCAN 并且我们需要使用 sr1:发送 CTO 消息:
sock = CANSocket(bustype='socketcan', channel='vcan0', basecls=XCPonCAN)
dto = sock.sr1(pkt)
由于 sr1 调用了 answers 函数,我们的 XCP 响应对象的有效负载被我们的 CTO 对象的命令解释。否则无法解释。第一条消息应该始终是“CONNECT”消息,Ecu 的响应决定了消息的读取方式。例如:字节顺序。否则,必须在 contrib 配置中设置地址粒度以及 DTO 和 CTO 的最大大小:
conf.contribs['XCP']['Address_Granularity_Byte'] = 1 # Can be 1, 2 or 4
conf.contribs['XCP']['MAX_CTO'] = 8
conf.contribs['XCP']['MAX_DTO'] = 8
如果您不希望在收到消息后设置此项,您也可以禁用该功能:
conf.contribs['XCP']['allow_byte_order_change'] = False
conf.contribs['XCP']['allow_ag_change'] = False
conf.contribs['XCP']['allow_cto_and_dto_change'] = False
要通过 TCP 或 UDP 发送 pkt,必须使用另一个标头。TCP:
prt1, prt2 = 12345, 54321
XCPOnTCP(sport=prt1, dport=prt2) / CTORequest() / Connect()
UDP:
XCPOnUDP(sport=prt1, dport=prt2) / CTORequest() / Connect()