❝
DoIP诊断协议是汽车安全领域的重要基础知识,在缺少整车情况下,已有的仿真方案都存在缺陷。基于开发板或CANOE的仿真方案需要硬件支持且环境配置复杂令人烦躁,基于软件的开源仿真方案对基于DoIP的UDS协议诊断实现都存在细节错误。为了解决这个问题,本文基于Python开发了一套高度可扩展的DoIP协议仿真靶场,并演示了如何在该靶场上完成基于路由激活的逻辑地址扫描,安全访问服务密钥爆破,读写服务数据项扫描等测试项,以及如何二次开发扩展仿真能力。相关代码已开源至GitHub
❞
问题说明
问题的本质很简单,已有的DoIP协议仿真方案都存在问题,于是我自己实现了一套仿真方案,仿真方案包括DoIP协议仿真(doipserver),以及一个基于DoIP协议的UDS诊断模块(doipclient)。本文的组织结构如下:
-
简单介绍DoIP协议 -
如何使用DoIP仿真靶场 -
如何二次开发扩展仿真能力
DoIP协议简单介绍
DoIP的全称是是Diagnostic communication over Internet Protocol(基于IP的诊断通信),它是基于车载以太网的汽车诊断协议。在一个DoIP通信网络中,可以简单认为存在两种DoIP实体,DoIP网关与DoIP节点。可以认为每一个DoIP节点都对应着一个汽车零部件,每个DoIP节点都有一个逻辑地址(Logical Address)与之关联,绝大多数DoIP数据包都会包含source_address与target_address,这两个字段对应着该数据包的来源逻辑地址与目的逻辑地址。DoIP网关的责任之一就是根据DoIP数据包中的target_address将数据包转发到正确的DoIP节点。
DoIP报文的基本格式如下,其中协议版本号一般为2,也有3,但是少见,协议版本号的取反值 + 协议版本号 = 0xFF,Payload类型指示Payload种类。
|协议版本号(Byte)|协议版本号的取反值(Byte)|Payload类型(Short)|Payload长度(Int)|DoIP Payload(Bytes)|
DoIP v2协议支持的Payload类型如下图所示,我们要关注的有0x005与0x0006,这两个Payload用于路由激活,只有完成路由激活后才能进行诊断,以及0x8001,0x8001用于完成基于DoIP协议的UDS诊断,注意0x8002与0x8003这两个Payload有一定的迷惑性,进行UDS诊断时,ECU对诊断的回复也是通过0x8001 Payload完成的,0x8002与0x8003是用来确认诊断消息被ECU正确收到,或者收到但不能正常解释等情况的。
使用DoIP仿真靶场
以下几行代码就能仿真一个DoIP网络,DoIP网关运行在0.0.0.0:13400地址上,网络中有两个DoIP节点,逻辑地址0x0e80的DoIP节点的安全访问服务的pincode值为b”2345″,逻辑地址0x1010的DoIP节点的安全访问服务的pincode值为”4321″,因为没有显示指定密钥算法,它们的安全访问密钥算法都是默认的DoIPNode.calc_key。
import doipsimu.doipserver as doipserver
from doipsimu.doipclient import DoIPSocket, UdsOverDoIP
# 创建doip网关
gw = doipserver.DoIPGateway(protocol_version=2)
# 创建并在doip网关中添加ecu节点, 设置逻辑地址以及PINCODE
ecu1 = doipserver.DoIPNode(logical_address=0x0e80, pincode=b"2345")
ecu2 = doipserver.DoIPNode(logical_address=0x1010, pincode=b"4321")
gw.add_node(ecu1)
gw.add_node(ecu2)
# 启动仿真
gw.start()
# 停止仿真
gw.stop()
接下来我们可以尝试基于UDS诊断模块诊断这个仿真的DoIP网络,下面这段代码先进入扩展会话,然后请求安全访问服务的随机数种子,基于随机数种子计算访问密钥,在通过安全访问验证后,通过写数据服务对0x1234这一数据项写入”HELLO WORLD”,然后通过读数据服务验证0x1234数据项是否被成功写入。完整的运行输出如下图所示。
# 开始尝试诊断仿真的网关
ds = DoIPSocket(source_address=0x1010, target_address=0x0e80)
uds = UdsOverDoIP(ds)
# 进入扩展会话
uds.open_extended_session()
# 请求种子
o = uds.request_seed()
while o is None:
o = uds.request_seed()
seed, code = o
print("[+]请求到种子", seed)
key = doipserver.DoIPNode.calc_key(seed, b"2345")
print("[+]根据种子计算密钥", key)
if uds.send_key(key=key) == 0:
print("[+]成功进入27服务")
if uds.write_did(0x1234, b"HELLO, WORLD!") == 0:
print("[+]成功通过$2E服务在0x1234写入HELLO WORLD")
print("[+]通过$22服务读取0x1234", uds.read_did(0x1234))
else:
print("[+]$2E服务写入失败")
else:
print("[-]进入27服务失败")
# 退出扩展会话
uds.exit_extended_session()
下面是对UDS诊断模块使用的一些演示
-
枚举22服务可以读取的DID
# 枚举 $22 服务可以读取的DID
for did in range(0, 0xFFFF):
o = uds.read_did(did)
if o is None:
continue
data, code = o
if code == 0:
print("[+] 成功读取 0x%04x %s" % (did, str(data)))
Output:
INFO: Routing activation successful! Target address set to: 0xe80
[+] 成功读取 0x0001 b'Hu.Jiacheng'
[+] 成功读取 0x0002 b'Wang.Zhiyi'
[+] 成功读取 0x0003 b'Zhang.Chengao'
[+] 成功读取 0x0004 b'Cheng.Rui'
[+] 成功读取 0x0005 b'Lian.Xiaowu'
-
逻辑地址扫描
# 扫描逻辑地址
# 不自动进行路由激活
ds = DoIPSocket(source_address=0x1010, target_address=0x0e80, activate_routing=False)
for i in range(0, 0xFFFF):
# 手动路由激活检查是否能激活成功
if ds.activate_routing(source_address=i, target_address=0):
print("[+] 发现逻辑地址 0x%04x" % (i, ))
Output:
[+] 发现逻辑地址 0x0e80
[+] 发现逻辑地址 0x1010
二次开发扩展仿真能力
为安全访问服务增加请求次数限制
默认的安全访问服务仿真没有对请求次数限制,可以重新实现安全访问服务增加随机数种子请求次数限制。第一步是在创建DoIP节点后调用add_uds_handler方法,使用自定义handler处理安全访问服务。每个handler的函数原型是固定的,pkt参数是接收到的DoIP数据包,可以在pkt中解析UDS诊断参数等,session是当前UDS诊断会话的上下文,可以在里面记录是否通过安全访问,当前诊断会话类型等信息,handler函数需要返回一个DoIP数据包,作为对这次UDS诊断的回复。
def my_securiy_access(pkt: doipserver.doip.DoIP, session: {}) -> doipserver.doip.DoIP:
pass
ecu1.add_uds_handler(doipserver.uds.UDS_SA, my_securiy_access)
可以参考安全访问服务的默认实现进行修改,只要在产生随机数种子的代码块中加入对请求次数的限制即可,如下所示
...
if sat % 2 == 1:
# 如果是请求种子
# 产生随机数种子
session["seed"] = random.randbytes(session["seed_len"])
# 递增请求次数
session["request_times"] = session.get("request_times", 0) + 1
if session.get("request_times", 0) > 3:
# 如果请求次数超过3次, 返回 UDS_NR, 错误码为0x36, 超过最大请求限制
resp = doip.DoIP(payload_type=0x8001, source_address=ta, target_address=sa) / uds.UDS() / uds.UDS_NR(
requestServiceId=pkt[1].service,
negativeResponseCode=0x36
)
return resp
# 返回种子
resp = doip.DoIP(payload_type=0x8001, source_address=ta, target_address=sa) / uds.UDS() / uds.UDS_SAPR(
securityAccessType=pkt[2].securityAccessType,
securitySeed=session["seed"]
)
...
修改后,请求4次种子,如下所示,发现在第4次请求时返回错误超过最大请求限制。
# 开始尝试诊断仿真的网关
ds = DoIPSocket(source_address=0x1010, target_address=0x0e80)
uds = UdsOverDoIP(ds)
uds.open_extended_session()
for i in range(4):
seed, code = uds.request_seed()
if code == 0:
print("第 %d 次请求成功, %s" % (i, str(seed)))
else:
print("第 %d 次请求失败, %s" % (i, uds.negativeResponseCodes[code]))
uds.exit_extended_session()
开源代码
https://github.com/ddddhm1234/DoIPSimulator
原文始发于微信公众号(网络空间威胁观察):汽车安全之DoIP协议仿真靶场