汽车安全之DoIP协议仿真靶场

admin 2024年2月7日22:52:15评论13 views字数 4888阅读16分17秒阅读模式

DoIP诊断协议是汽车安全领域的重要基础知识,在缺少整车情况下,已有的仿真方案都存在缺陷。基于开发板或CANOE的仿真方案需要硬件支持且环境配置复杂令人烦躁,基于软件的开源仿真方案对基于DoIP的UDS协议诊断实现都存在细节错误。为了解决这个问题,本文基于Python开发了一套高度可扩展的DoIP协议仿真靶场,并演示了如何在该靶场上完成基于路由激活的逻辑地址扫描,安全访问服务密钥爆破,读写服务数据项扫描等测试项,以及如何二次开发扩展仿真能力。相关代码已开源至GitHub

问题说明

问题的本质很简单,已有的DoIP协议仿真方案都存在问题,于是我自己实现了一套仿真方案,仿真方案包括DoIP协议仿真(doipserver),以及一个基于DoIP协议的UDS诊断模块(doipclient)。本文的组织结构如下:

  1. 简单介绍DoIP协议
  2. 如何使用DoIP仿真靶场
  3. 如何二次开发扩展仿真能力

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网络,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(0x1234b"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()

汽车安全之DoIP协议仿真靶场

诊断输出

下面是对UDS诊断模块使用的一些演示

  • 枚举22服务可以读取的DID
# 枚举 $22 服务可以读取的DID
for did in range(00xFFFF):
    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(00xFFFF):
    # 手动路由激活检查是否能激活成功
    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))

uds.exit_extended_session()

汽车安全之DoIP协议仿真靶场

请求失败

开源代码

https://github.com/ddddhm1234/DoIPSimulator

原文始发于微信公众号(中机博也车联网安全):汽车安全之DoIP协议仿真靶场

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月7日22:52:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   汽车安全之DoIP协议仿真靶场https://cn-sec.com/archives/2475785.html

发表评论

匿名网友 填写信息