❝
UDS 诊断协议提供了 RequestDownload 与 RequestUpload 服务用于刷写和读取 UDS 固件。DoIP 仿真靶场的本次更新增加了对以上服务以及其它若干服务的仿真支持,用户可以尝试在靶场中进行固件刷写与读取。
❞
问题说明
上次发布的 DoIP 协议仿真靶场其实是残血状态,因为不支持固件刷写等行为的仿真。本次更新增加了这一能力。
UDS 刷写简明教程
在本章节中,我们简单介绍一下 UDS 刷写的流程。
刷写固件
参考文献[1]提供了一个比较易懂的刷写流程图,但是这个流程图还是有一些问题,后文加粗的步骤都是我认为这个图存在问题的地方。如下图所示。图中流程可以分为以下三个步骤:
-
「预编程阶段」从#22 服务到#28 服务都是预编程阶段。顾名思义,这个步骤还没有进入刷写流程,只是在为刷写做准备,详细描述如下:
-
「我认为图中的#22 服务应该没什么意义」 -
通过#10 服务进入 03 号扩展会话 -
「通过#31 服务执行 Routing 检查 ECU 是否处于可刷写状态」。注意图中给出的 Routing 的 ID 号是 0x0203,实际上这个 ID 号可能随着车型变化而变化。参考文献[2]给出了 Routing 的 ID 号的命名规范,其中 0x0200 - 0xDFFF 是由设备制造商指定的 -
通过#85 服务与#28 服务关闭 DTC 和非诊断报文,使整个 CAN 网络处于安静状态,这一步的作用应该是降低 CAN 网络通信开销。
-
「主编程阶段」
-
通过#10 服务切换到 02 号编程会话,刷写只能在编程会话中进行 -
通过#27 服务通过安全验证 -
通过#2E 服务写入一些刷写记录,比如刷写人信息。这一步也是由设备制造商自定义的,不通用 -
通过#31 服务调用 0xFF00 号 Routing 擦除 FLASH 内容。参考文献[2]指出 0xFF00 号 Routing 必须是擦除 FLASH 内容的 Routing -
通过#34 服务,要求 ECU 从 Tester(也就是诊断仪,我方)下载内容到 FLASH 上指定位置 -
通过#36 服务,我方将下载内容发送给 ECU -
通过#37 服务,指示传输过程结束,结束下载 -
「通过#31 服务调用 0x0202 号 Routing 检查内存」。参考文献[2]指出 0x0202 号 Routing 由设备制造商指定,这一步由设备制造商自定义的,不通用 -
通过#31 服务调用 0xFF01 号 Routing 检查传输内容完整性。参考文献[2]指出 0xFF01 号 Routing 必须是检查完整性的 -
通过#11 服务硬重置 ECU,ECU 执行刷入的固件
-
「后编程阶段」
-
通过#28 与#85 服务打开预编程阶段关闭的 DTC 和非诊断报文 -
通过#10 服务进入 01 号会话,也就是正常会话,刷写结束
接下来我们深入这些步骤,解读一些服务的细节
-
「#31 RoutingControl 服务」#31 服务的实现细节可见参考文献[3]。我简单介绍一些关键的信息。#31 服务用以控制/读取一些 Routing 的状态,可以理解为一种远程调用协议,Tester(我方)可以通过#31 服务执行,停止一些 Routing,并读取这些 Routing 的状态信息。#31 服务的数据格式如下,执行/停止 Routing 对应的 subFunction 值为 01/02,读取 Routing 状态对应的 subFunction 值为 03,附加信息是由设备制造商自定义的,可以理解为调用 Routing 时提供的参数等。
0x31 subFunction(Byte) Routing ID(Short) 附加信息(自定义结构)
在主编程阶段需要调用 0xFF00 号 Routing 擦除 FLASH 内容,这个 Routing 的附加信息一般就是两个参数,即内存地址与长度。附加信息的结构还是由设备制造商自定义的,一般可能使用这种结构:「内存地址(32 位整数) 长度(16 位整数)」,如果是 16 位 CPU 的 ECU,可能类型从 32 整数位变成 16 位整数。在主编程阶段还需要调用 0xFF01 号 Routing 检查传输内容完整性,这个 Routing 没有附加信息。
-
「#34 RequestDownload 服务」RequestDownload 服务的命名规则有点奇怪,我的理解是,Tester(我方)请求 ECU 从 Tester 下载一段内容到指定内存位置。这个服务的详细说明可见参考文献[4]。我简单介绍一些关键的信息。#34 服务的数据格式如下,其中 dataFormatIdentifier 字段指示了传输数据时的格式,为 0 则表示没有压缩也没有加密,这个字段的低 4 位指示数据加密类型,高 4 位指示数据压缩类型,取值的含义也是由设备制造商自定义的,所以本文只讨论该字段为 0 的情况。Address & Length FormatIndetifier 字段指示了内存地址与长度这两个字段的字节数,低 4 位指示内存地址字段的字节数,高 4 位指示长度字段的字节数,比方说该值为 0x44,那么内存地址与长度这两个字段都是 32 位整数。
0x34 dataFormatIdentifier(Byte) Address & Length FormatIdentifier(Byte) 内存地址 长度
这个服务的 PositiveRespone 回应也需要注意,scapy 库中关于它的回应的定义如下,memorySizeLen 字段的命令是有问题的,因为它的值实际上等于 maxNumberOfBlockLength 字段的字节数,maxNumberOfBlockLength 字段表示在通过#36 TransferData 服务进行数据传输时,一次能传输的最大字节数,当要传输的数据超过这个上限时,就需要多次调用 TrasnferData 服务分块传输。
class UDS_RDPR(Packet):
name = 'RequestDownloadPositiveResponse'
fields_desc = [
BitField('memorySizeLen', 0, 4),
BitField('reserved', 0, 4),
StrField('maxNumberOfBlockLength', b"", fmt="B"),
]
-
「#36 TransferData 服务」TransferData 服务很有意思,#34 RequestDownload 服务是从 Tester(我方)传输数据到 ECU,#35 RequestUpload 服务是从 ECU 传输数据到 Tester(我方),这两个服务的数据传输都是通过 TransferData 服务完成的,也就是说,TransferData 服务能完成双向的数据传输。scapy 中,TransferData 服务与对应的 PositiveResponse 响应的数据格式定义如下,注意到它们都含有完全相同的字段,blockSequenceCounter 字段是块编号,每传输 1 次递增 1,指示当前传输的是第几个编号,它的取值范围是 00-FF,「但是它的初始值是从 1 开始的」,transferResponseParameterRecord 字段是块的内容。理解了这些,双向传输数据的实现就容易理解了。从 Tester 传输数据到 ECU 时,Tester 向 ECU 发送 TransferData 服务数据包,其中 transferRequestParameterRecord 字段是本次传输的内容;从 ECU 传输数据到 Tester 时,Tester 向 ECU 发送 TransferData 服务数据包,transferRequestParameterRecord 字段为空,ECU 向 Tester 发送 PositiveResponse,其中 transferResponseParameterRecord 字段就是本次传输的内容。
class UDS_TD(Packet):
name = 'TransferData'
fields_desc = [
ByteField('blockSequenceCounter', 0),
StrField('transferRequestParameterRecord', b"", fmt="B")
]
class UDS_TDPR(Packet):
name = 'TransferDataPositiveResponse'
fields_desc = [
ByteField('blockSequenceCounter', 0),
StrField('transferResponseParameterRecord', b"", fmt="B")
]
-
「#35 RequestUpload 服务」故名思意,Tester 请求 ECU 上传特定内存位置的内容到 Tester。这个服务与#34 服务的数据结构以及 PostiveResponse 的数据结构都完全一样,只是传输数据的流向不同,具体细节就请见#35 服务的论述了。
-
「#37 RequestTransferExit 服务」当传输完成后,调用该服务指示传输完成。
读取固件
读取固件与刷写固件基本相同,只是从调用#34 RequestDownload 服务变为调用#35 RequestUplaod 服务,以及调用#36 TransferData 服务时的传输流向不同。在此就不再赘述了。
UDS 刷写仿真实现细节
预编程阶段的仿真没有什么意义,此处只讨论主编程阶段的仿真实现细节。
#34 RequestDownload 服务仿真
#34 RequestDownload 服务的仿真实现细节如下所示,首先检查当前是否处于 02 编程会话,以及是否通过安全访问,接下来检查数据格式是否为无加密无压缩,以及要刷写的目标地址范围是否合法,刷写的目标地址范围是否已经擦除过了,如果这些检查都通过了,记录一些信息以供 TransferData 服务使用。
def request_download(self, pkt: doip.DoIP, session: Dict) -> doip.DoIP:
if session.get("session_type", -1) != 2 or session.get("session_deadline", -1) <= time.time():
# 刷写模式只能在02编程会话中执行, 返回0x7F ServiceNotSupportedInActiveSession
return self.mk_nr(pkt, 0x7F)
# 如果没有进入安全访问, 或者进入了安全访问但会话过期了
if session.get("sa_type", -1) == -1 or session.get("session_deadline", -1) < time.time():
# 返回 0x33 SecurityAccess Denied
return self.mk_nr(pkt, 0x33)
# 检查数据格式是否为无加密无压缩
if pkt[2].dataFormatIdentifier == 0:
# 检查长度和地址的长度是否为4字节
if pkt[2].memorySizeLen != 4 or pkt[2].memoryAddressLen != 4:
# 返回0x22 ConditionNotCorrect
return self.mk_nr(pkt, 0x22)
# 要写入的目标内存地址与长度
addr = pkt[2].memoryAddress4
size = pkt[2].memorySize4
# 检查目标内存是否在flash_mem范围内
if addr + size >= len(self.flash_mem):
# 不在返回0x31 RequestOutOfRange
return self.mk_nr(pkt, 0x31)
buf = self.flash_mem[addr : addr + size]
# 如果全为x00, 说明已经被擦除过了, 可以写入
if all(b == 0 for b in buf):
# 在当前会话中记录要下载的地址与长度
session["request_addr"] = addr
session["request_cur"] = addr
session["request_size"] = size
session["request_seq"] = 0
session["request"] = "download"
# 最大Block长度
return self.mk_pr(pkt, uds.UDS_RDPR(memorySizeLen=4) / int.to_bytes(self.flash_max_blocklen, byteorder="big", length=4))
else:
# 否则, 说明这段内存还没有被擦除, 返回0x70
return self.mk_nr(pkt, 0x70)
else:
# 否则, 返回0x22 ConditionNotCorrect
return self.mk_nr(pkt, 0x22)
#36 TransferData 服务仿真
TransferData 服务的仿真需要注意数据双向传输的问题,此处只展示 Tester 传输到 ECU 的实现。首先还是检查是否处于编程会话,是否通过安全访问,接下来读取 Tester 本次发送的数据块,写入当前 FLASH。
def transfer_data(self, pkt: doip.DoIP, session: Dict) -> doip.DoIP:
# 如果没有进入安全访问, 或者进入了安全访问但会话过期了
if session.get("sa_type", -1) == -1 or session.get("session_deadline", -1) < time.time():
# 返回 0x33 SecurityAccess Denied
return self.mk_nr(pkt, 0x33)
if session.get("request", "") not in ["download", "upload"]:
# 还没有请求下载或者上传, 返回0x70 uploadDownloadNotAccepted
return self.mk_nr(pkt, 0x70)
block = pkt.transferRequestParameterRecord
# 检查block长度是否超过最大长度
if len(block) > self.flash_max_blocklen:
# 返回0x31 Request out of range
return self.mk_nr(pkt, 0x31)
if session["request"] == "download":
seq = session["request_seq"]
seq += 1
seq %= 0xFF
# 检查 block序号是否连续
if seq == pkt[2].blockSequenceCounter:
begin_addr = session["request_cur"]
# 检查是否超过写入范围
if begin_addr + len(block) - session["request_addr"] > session["request_size"]:
# 返回0x31 roor
return self.mk_nr(pkt, 0x31)
# 写入内存
self.flash_mem = self.flash_mem[ : begin_addr] + block + self.flash_mem[begin_addr + len(block) : ]
# 递增当前写入指针
session["request_cur"] += len(block)
# 更新block序号
session["request_seq"] = seq
return self.mk_pr(pkt, uds.UDS_TDPR(blockSequenceCounter=seq))
else:
# 返回0x24 request sequence error
return self.mk_nr(pkt, 0x24)
使用演示
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
)
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()
其运行输出如下所示:
开源代码
DoIPSimulator: https://github.com/ddddhm1234/DoIPSimulator
参考文献
[1] UDS 刷写流程图 https://piembsystech.com/application-of-uds-flash-programming/
[2] Routing ID 号命名规范 https://piembsystech.com/routineidentifier-rid-in-uds-protocol/
[3] #31 RoutingControl 服务 https://piembsystech.com/routinecontrol-0x31-service-uds-protocol/
[4] #34 #35 #36 服务 https://embetronicx.com/tutorials/automotive/uds-protocol/upload-download-function-unit-in-uds-protocol/
原文始发于微信公众号(中机博也车联网安全):DoIP仿真靶场更新:新增UDS刷写服务仿真支持
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论