首页
社区
课程
招聘
[原创]使用车载DoIP协议仿真框架零基础入门汽车诊断协议
发表于: 2024-6-1 12:57 3844

[原创]使用车载DoIP协议仿真框架零基础入门汽车诊断协议

2024-6-1 12:57
3844

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种类。

1
|协议版本号(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正确收到,或者收到但不能正常解释等情况的。
Payload类型

使用DoIP仿真靶场

以下几行代码就能仿真一个DoIP网络,DoIP网关运行在0.0.0.0:13400地址上,网络中有两个DoIP节点,逻辑地址0x0e80的DoIP节点的安全访问服务的pincode值为b"2345",逻辑地址0x1010的DoIP节点的安全访问服务的pincode值为"4321",因为没有显示指定密钥算法,它们的安全访问密钥算法都是默认的DoIPNode.calc_key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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数据项是否被成功写入。完整的运行输出如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 开始尝试诊断仿真的网关
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 枚举 $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'
  • 逻辑地址扫描
1
2
3
4
5
6
7
8
9
10
11
12
# 扫描逻辑地址
# 不自动进行路由激活
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
  • 固件刷写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import doipsimu.doipserver as doipserver
from doipsimu.doipclient import DoIPSocket, UdsOverDoIP
 
def my_callback(code, seq, buf, cur, block_len):
    # 刷写过程的回调函数, 每传输完成一个数据块就会调用一次该回调函数
    # code: 为0表示正常, 非0表示本次刷写失败; seq: 块编号; buf: 本次传输块的数据内容; cur: 本次写入目标内存地址; block_len: 最大块长度
    if code == 0:
        print(f"[+] 第 {seq} 轮写入成功")
    else:
        print(f"[+] 第 {seq} 轮写入失败")
 
# 创建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()
 
 
# 开始尝试诊断仿真的网关
ds = DoIPSocket(source_address=0x1010, target_address=0x0e80)
uds = UdsOverDoIP(ds)
 
# 进入扩展会话
uds.open_extended_session(diagnostic_session_type=2)
 
# 请求种子
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 (code := uds.erase_memory(0x1000, 0x3000)) == 0:
        print("[+] 成功擦除内存 0x1000 - 0x3000 的内存")
    else:
        print("[+] 擦除内存失败", uds.negativeResponseCodes[code])
 
     
    if (code := uds.reprogramming(0x1000, 0x3000, b"\x00" * 0x2000, my_callback)) == 0:
        print("[+] 向0x1000 - 0x3000刷写全0成功")
    else:
        print("[+] 向0x1000 - 0x3000刷写全0失败")
 
    if (code := uds.reset(1)) == 0:
        print("[+] 重置ECU成功")
    else:
        print("[+] 重置ECU失败")
 
    ret = uds.get_flash(0x1000, 0x2000)
    if isinstance(ret, bytes) and all(b == 0 for b in ret):
        print("[+] 成功读取0x1000 - 0x3000内存, 且为全0, 证明成功刷写")
    else:
        print("[-] 内存写入失败或读取失败")
 
 
else:
    print("[-]进入27服务失败")
 
# 退出扩展会话
uds.exit_extended_session()
 
# 停止网关仿真
gw.stop()

  • WireShark抓包学习UDS协议
    在仿真时使用WireShark抓本地Loopback网卡的包即可
    图片描述

二次开发扩展仿真能力

为安全访问服务增加请求次数限制

默认的安全访问服务仿真没有对请求次数限制,可以重新实现安全访问服务增加随机数种子请求次数限制。第一步是在创建DoIP节点后调用add_uds_handler方法,使用自定义handler处理安全访问服务。每个handler的函数原型是固定的,pkt参数是接收到的DoIP数据包,可以在pkt中解析UDS诊断参数等,session是当前UDS诊断会话的上下文,可以在里面记录是否通过安全访问,当前诊断会话类型等信息,handler函数需要返回一个DoIP数据包,作为对这次UDS诊断的回复。

1
2
3
4
def my_securiy_access(pkt: doipserver.doip.DoIP, session: {}) -> doipserver.doip.DoIP:
    pass
 
ecu1.add_uds_handler(doipserver.uds.UDS_SA, my_securiy_access)

可以参考安全访问服务的默认实现进行修改,只要在产生随机数种子的代码块中加入对请求次数的限制即可,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
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次请求时返回错误超过最大请求限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 开始尝试诊断仿真的网关
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


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 2
支持
分享
最新回复 (1)
雪    币: 41
活跃值: (823)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
感谢大神分享
2024-6-3 08:58
0
游客
登录 | 注册 方可回帖
返回
//