- 开发工具
- 逆向工程
- 嵌入式
【免费下载链接】SavvyCAN
QT based cross platform canbus tool
UDS(Unified Diagnostic Services,统一诊断服务)是 ISO 14229 定义的车载诊断协议,现代车辆的 ECU 大多通过 ISO-TP(ISO 15765)在 CAN 总线上承载 UDS 请求与响应。SavvyCAN 的 UDS 扫描窗口(UDS Scan Window)是一个专门面向 UDS 的“特殊模糊测试工具”:它既可以在一片 CAN 地址区间内搜索符合(或近似符合)UDS 规范的节点,也可以针对已知 UDS 节点批量探测其支持的服务、子功能和数据项。阅读本文后,你将掌握如何配置多组扫描任务、逐项理解 Tester Present、Session Control、Security Access、Read By ID 等扫描类型的含义与底层服务码,并能结合 SavvyCAN 的源码与脚本接口把扫描能力进一步延伸到自动化诊断分析中。
窗口定位:一个“特殊”的 CAN 模糊测试器
SavvyCAN 中 UDS 扫描窗口本质上属于 CAN 模糊测试(fuzzing)家族的一员,但它的目标高度专一:在总线上搜索 UDS 合规(或近合规)的节点,或在已知节点上做服务、子功能与数据项的全量探测。它由主窗口菜单中的 UDS Scanner 动作触发(见 mainwindow.cpp 中的showUDSScanWindow())。
与通用模糊测试窗口(对应文档 fuzzingwindow.md)不同,UDS 扫描窗口发出的每条报文都被组织成规范的 UDS 服务请求——第一个字节是服务 ID(Service ID),其后是定长的子功能字节,最终由底层 ISO-TP 处理器封装成 CAN 帧发送。从源码结构看(udsscanwindow.cpp 的setupScan()),每条扫描请求都是先构造UDS_MESSAGE,再交由UDS_HANDLER::sendUDSFrame()发出,与手工逐条发送相比,它把“按区间、按步进、按类型”批量生成测试流的过程完全自动化了。
UDS 扫描窗口主界面:左侧为待执行扫描任务列表,右侧为参数配置区与树形结果展示区
界面布局与任务列表
窗口采用左右分栏布局(对应 udsscanwindow.ui):
- 左侧 “Scans To Run”:以列表形式保存全部扫描任务,支持 Add(新增)、Delete(删除)、Load / Save(载入 / 保存任务组)。
- 右侧上半部:当前选中任务的各项参数(ID 范围、总线、回复偏移、扫描类型、子功能/服务范围等)。
- 右侧下半部:树形结果视图(Results)、进度条,以及 Run All Scans / Run Selected Scan / Save Scan Results 三个执行按钮。
每个扫描任务都由一个ScanEntry结构描述(见 udsscanwindow.h),其关键字段如下:
| 字段 | 含义 | 默认值(由addNewScan()设定) |
|---|---|---|
startID/endID | 扫描的起始 / 结束 CAN ID | 0x7E0/0x7E7 |
idOffset | 期望的回复地址偏移 | 8 |
bAdaptiveOffset | 是否允许自适应回复偏移 | false(UI 中复选框默认勾选) |
bShowNoReplies | 是否在结果中显示无回复项 | false(UI 中默认勾选) |
busToScan | 扫描的总线编号 | 0 |
maxWaitTime | 每条请求的最大等待时间(毫秒) | 100 |
scanType | 扫描类型(SCAN_TYPE枚举) | ST_TESTER_PRESENT |
sessType | 测试前切换的会话类型 | 0(不切换) |
subfunctLen | 子功能(Subfunction)字节数 | 1 |
subfunctLower/subfunctUpper | 子功能下界 / 上界 | 0/0 |
subfunctIncrement | 子功能步进 | 1 |
serviceLower/serviceUpper | 服务字节下界 / 上界(Custom 类型使用) | 1/1 |
任务按列表顺序依次执行,每个任务可以拥有完全独立的参数。例如:任务 A 扫描0x7E0到0x7E7,任务 B 只扫0x600一个地址,两者互不影响。任务描述会以(0x起始ID - 0x结束ID) 类型缩写的形式实时刷新在列表中(缩写对应关系见generateListDesc(),如 TP= Tester Present、SESS= Session Control、RID= Read By ID 等)。
任务组可以保存为*.uds文件(版本号为 1),Load 时若文件版本不符会弹出警告并拒绝加载(见 udsscanwindow.cpp 的loadScans()/saveScans())。
总线与回复匹配配置
扫描开始前需要设置四个与“怎么发、怎么收”相关的参数:
Starting ID 与 Ending ID
UDS 请求按Starting ID到Ending ID逐地址发送。通常 UDS 合规的 ECU 会响应0x7E0~0x7E7(物理请求地址区间),因此这也是默认值。但部分车辆在其它 ID 上使用“类 UDS”协议,此时可将区间设为目标地址。两个输入框均为 16 进制显示(displayIntegerBase=16),最大值可达536870911(即 0x1FFFFFFF)。若起始 ID 大于结束 ID,对应输入框背景会变红提示无效(checkIDRange()中的setControlState()逻辑)。
Bus to Scan 与 Maximum reply delay
- Bus to scan:选择在哪个 CAN 总线上执行扫描。可用总线来自
CANConManager运行时枚举(udsscanwindow.cpp)。 - Maximum reply delay (ms):每条请求发出后等待回复的最长时间,范围 50~10000 ms,步进 10 ms,默认 100 ms。等待超时后窗口会按“无回复”处理并发送下一条(
timeOut()槽函数)。
Reply Offset 与 Allow adaptive reply offset
UDS 节点通常以比请求 ID 大 8 的地址回复,因此程序默认将回复偏移(Reply Offset)设为 8。但存在两类例外:
- 部分节点使用偏移 16 的回复地址;
- 少数节点的回复地址与请求地址毫无关联。
针对这些情况,勾选“Allow adaptive reply offset”后,无论回复来自哪个地址都会被接受;取消勾选则只接受“恰好等于 请求 ID + 偏移”的回复。当自适应模式开启时,Reply Offset 输入框自动禁用(见adaptiveToggled(),udsscanwindow.cpp)。Reply Offset 的可调范围为 -128~128,这为处理“回复地址反而小于请求地址”的少见情况也留出了余地。
Show “No Reply”
勾选该选项后,结果树中会为每个“超时无回复”的探测项生成一条灰色NO REPLY记录;不勾选则直接跳过无回复项,减少列表干扰。有些 ECU 对不认识的报文会直接忽略,是否在结果中保留这些“被忽略”痕迹属于个人偏好。在源码中,无回复项通过哨兵 ID0xDEAD5EA1标记,再以NO REPLY文本呈现(udsscanwindow.cpp)。
扫描类型详解
每个任务只能选择一种扫描类型,但可以自由创建任意多个任务组合成一套完整的探测方案。窗口共支持 13 种扫描类型(SCAN_TYPE枚举,见 udsscanwindow.h),按 UI 顺序依次为:Tester Present、Session Control、Communication Control、ECU Reset、Clear DTCs、Read DTCs、Security Access、Read By ID、Read By Address、Read Scaling Data By ID、IO Control、Routine Control、Custom UDS。切换类型时,窗口会按类型自动启用或禁用对应的参数控件(changedScanType()中的分支逻辑)。
Tester Present(0x3E)
发送 Tester Present 请求(子功能 0x00),探测目标地址是否“有人”。这是最基础的存活探测:正常情况下,诊断仪连接期间会周期性地发送 Tester Present 告知 ECU “测试仪仍然在线”。在扫描场景下,它最适合用来扫宽泛的地址区间以快速缩小需要深入扫描的地址列表——正如原文档所述,该服务几乎必然被支持,因此命中率极高。其服务码0x3E与说明 “Tester is present” 可在 uds_handler.h 的UDS_SERVICES枚举及 uds_handler.cpp 的服务描述表中确认。
Session Control(0x10 / DIAG_CONTROL)
UDS 定义了若干会话类型(Session Type),常见的包括normal(默认)、extended diagnostics(扩展诊断)、programming(编程)。Session Control 扫描会逐一尝试这些会话类型,并反馈设备实际支持哪些。需要注意:即便收到“条件不满足”或“访问被拒绝”的负响应,也很可能意味着该模式本身受支持,只是需要先完成前置操作——这依然是有价值的信息。窗口实现中,会话扫描遍历类型 1~3(for (int typ = 1; typ < 4; typ++)),对应UDS_DIAG_CTRL_SUB表中的 DFLT_SESS(1)、PROG_SESS(2)、EXT_SESS(3)(见 uds_handler.cpp)。扫描窗口本身太简单,不适合进一步“破解”这类受保护功能——原文档明确建议:此时应改用 SavvyCAN 的脚本接口(详见下文“脚本扩展”一节)。
Communication Control(0x28)
发送 Communication Control 请求(子功能 2 字节,值为0x100,总线上的含义为“00 01 = 启用 Rx/Tx”),探测 ECU 对通信控制服务的支持情况。服务描述表中的子功能还包括 COMM_DIS_TX(1)、COMM_DIS_ALL(3) 等,但扫描窗口只发送启用类型,属于较安全的探测。
ECU Reset(0x11)
ECU Reset 扫描尝试各种复位类型并查看哪些受支持。窗口遍历类型 1~3,对应UDS_ECU_RESET_SUB表中的 HARD_RESET(1)、KEYOFFON_RESET(2)、SOFT_RESET(3)(uds_handler.cpp)。注意:复位类操作会影响目标 ECU 的运行状态,应在理解后果的前提下使用。
Clear DTCs(0x14)与 Read DTCs(0x19)
这两个类型用于故障码(DTC)管理:
- Clear DTCs:发送
CLEAR_DIAG服务,子功能 3 字节取0xFFFFFF——按源码注释为“clear everything!”,即清除全部 DTC 组。 - Read DTCs:发送
READ_DTC服务,子功能 2 字节取0x02FF——即“按掩码读取 DTC,掩码为 FF”。
两者均以最激进的参数探测 ECU 是否支持故障码清除 / 读取能力,属于“能力确认型”扫描。
Security Access(0x27)
设备刚启动并正常运行期间,通常不允许执行下载固件、修改参数等危险或敏感操作,必须先进入更高的安全级别,这一机制即 Security Access。本扫描类型会尝试找出 ECU 支持的安全级别并判断其是否受保护。源码中遍历奇数子功能 1~0x41(typ = typ + 2,步进 2,for (int typ = 1; typ < 0x42; ...)),因为按 UDS 约定,奇数子功能用于请求种子(seed),偶数子功能用于发送密钥(key)。
原文档对此做了非常务实的提醒:
- 大多数情况下,高安全级别由 challenge/response(挑战/应答)机制保护——ECU 下发一串随机字节作为挑战,你必须返回正确的应答;猜错次数过多会被锁定(负响应码
0x36 EXCEED_ATTEMPTS、0x37 TIMEDELAY等正是描述此类锁定的错误码,见 uds_handler.cpp 的负响应码表)。 - 偶尔 C/R 是固定的,抓一次合法流量就能永久复用;
- 偶尔 C/R 简单到只有几种可能答案;
- 而如果 ECU 开发者认真实现了安全机制,这个扫描类型只能确认级别存在,基本无法真正解锁。
因此本类型用于探测哪些安全级别存在以及是否受保护,而不是用来破解。值得注意的是,UDS_HANDLER::getDetailedMessageAnalysis()对 Security Access 做了精细解码(区分 seed 请求/seed 响应/key 发送/解锁成功,uds_handler.cpp),配合 ISO-TP 解释窗口可深入观察这类交互。
Read By ID(0x22)
UDS 允许通过一个 ID 编号读取 ECU 数据。大量 ID 并未在标准中定义,属于各 ECU 私有定义,因此可以用本类型扫描一段 ID 区间,看哪些 ID 有响应。ID 的字节数没有统一标准——可能是 1、2、3 或 4 字节,通常为 2 字节。操作方式:
- 用Subfunct Len(子功能字节数)设定 ID 大小,可选范围 1~4;
- 设置Lower Bound / Upper Bound(上下界,会随字节数自动扩展到对应上限);
- 可选设置Increment(步进)。
步进上限为 1024(0x400),对宽区间扫描很有用。该类型同时启用 “Run test in session type” 选项,因为某些数据 ID 仅在特定会话下可读。
Read By Address(0x23)
与 Read By ID 类似,但改为按内存地址读取。地址长度同样随硬件不同而不同,配置方式与 Read By ID 完全一致。源码实现中二者共用一套子功能范围遍历逻辑(ST_READ_ID与ST_READ_ADDR分支结构相同,仅服务码不同,udsscanwindow.cpp)。
Read Scaling Data By ID(0x24)
读取 ID 对应的缩放/换算数据(如传感器量程、分辨率等)。源码强制subFuncLen = 2,并按子功能区间 + 步进遍历。它比普通 Read By ID 更“温和”,常用于确认 ECU 对数据定义类服务的支持。
IO Control(0x2F)
发送 IO Control 请求(子功能 3 字节,高字节为 0,即 “Return control to ECU”——将控制权交还给 ECU)。窗口遍历各 IO 控制参数区间,探测 ECU 支持哪些 IO 控制项。源码注释强调:这是相对安全的选择,因为并没有真正强制改变 IO 状态。
Routine Control(0x31)
发送 Routine Control 请求,子功能 3 字节按(subf << 8) + 3构造——只请求例程结果(GET_ROUTINE_RESULTS),这是最安全的做法。源码注释明确警告:“启动或停止任意例程极度危险,除非你非常清楚自己在做什么,否则可能发生非常疯狂的事情”(udsscanwindow.cpp)。因此该类型仅探测“有哪些例程 ID 存在并返回结果”。
Custom UDS(通配符扫描,文档中称 Wildcard)
这是最具模糊测试色彩的类型,对应ST_CUSTOM。你可以同时设定:
- Lower Service / Upper Service:服务字节的下界与上界;
- Subfunct Len、Lower/Upper Bound、Increment:子功能的字节数与遍历区间。
它会以“双重循环”方式把服务字节与子功能逐一组合发送(for service ... for subFunc ...),全方位扫出 ECU 支持和不支持的内容。如果区间设置不当,这一测试可能耗时极长,请务必控制范围。
Run test in session type
部分扫描类型支持“在指定会话类型中执行测试”,选项包括:No Change(不切换)、Default、Programming、Extended Diag、Safety Sys Diag(见 udsscanwindow.cpp)。勾选后,程序会在正式测试前先向目标发送一条DIAG_CONTROL(0x10)请求切换会话(setupScan()中sessType > 0的分支),再执行当前任务。这对“仅在诊断模式或编程模式下才可用”的功能探测很有价值。不过请注意:进入编程模式通常需要先提升安全级别,而这是本程序不负责的专有过程,所以切换可能失败——此时负响应结果同样能提供线索。
执行扫描与解读结果
点击Run All Scans(运行全部任务,按列表顺序展开所有请求)或Run Selected Scan(仅运行当前选中任务)即可开始。执行流程如下(对应startScan()/sendNextMsg(),udsscanwindow.cpp):
- 清空结果树,启动 UDS 接收(
setReception(true))、全 ID 处理(setProcessAllIDs(true))与流控(setFlowCtrl(true)); - 按任务生成
UDS_MESSAGE队列(sendingFrames),进度条以队列长度为上限逐条推进; - 每条请求发出后启动等待定时器,收到匹配回复或超时后立即发送下一条。
结果以树形结构呈现,层次为:请求 ID → 回复 ID(或无回复)→ 服务名称 → 子功能值 → 具体回复内容(setupNodes()构建,udsscanwindow.cpp)。关键判定逻辑在gotUDSReply()中:
- 若回复服务为
0x40 + 请求服务,判为正响应(POSITIVE),以深绿色显示,并附上全部返回数据字节; - 若
msg.isErrorReply为真(即收到0x7F负响应帧),判为负响应(NEGATIVE),以深红色显示,并直接翻译负响应码的短名称(如SERVICE_NOTSUPP、SECURITY_DENIED、RESP_PENDING等,对应 uds_handler.cpp 的UDS_NEG_RESPONSE表); - 超时且勾选了 Show “No Reply” 时,生成灰色NO REPLY节点。
响应匹配的前提是回复地址满足偏移规则(id == sentFrame.frameId() + offset或已勾选自适应偏移)。底层判定依赖UDS_HANDLER::tryISOtoUDS()(uds_handler.cpp)把 ISO-TP 消息解析为UDS_MESSAGE:首字节为服务码,0x7F开头则标记为错误回复并把真正的服务码取回。扫描完成后,点击Save Scan Results可将结果树以缩进文本形式导出为.txt文件(dumpNode()递归写出,udsscanwindow.cpp)。
底层原理:UDS 请求如何在 CAN 上落地
了解底层链路有助于正确解释扫描结果。SavvyCAN 的 UDS 栈分三层:
- UDS_HANDLER(bus_protocols/uds_handler.h):负责服务码 / 子功能语义。
sendUDSFrame()将service与按subFuncLen大端展开的子功能字节拼入数据区,再交给 ISO-TP 层(uds_handler.cpp)。 - ISOTP_HANDLER(bus_protocols/isotp_handler.cpp):当单帧数据不足 8 字节时直接以单帧发送;数据较长时按 ISO 15765 发送首帧(
0x10+ 长度)并等待流控帧,随后以连续帧(0x20 + 序号)完成传输。接收侧同理,将分散的 CAN 帧重组为完整 ISO-TP 消息。 - CANConManager:把最终 CAN 帧送上所选总线。
这也解释了为什么扫描窗口同时开启了流控(Flow Control)与全 ID 接收:UDS 响应经常是多帧 ISO-TP 消息,必须正确处理流控与重组;而“全 ID 处理”保证了无论 ECU 用什么地址回复(配合自适应偏移)都能被捕获。
脚本扩展:把扫描能力带向自动化
原文档明确指出,UDS 扫描窗口适合“确认能力”,而更深入的交互(如完成安全解锁流程)应交给脚本接口。SavvyCAN 的脚本容器为 JS 提供了UDSScriptHelper(scriptcontainer.cpp),可直接调用:
uds.setFilter(id, mask, bus):设置 UDS 接收过滤器;uds.sendUDS(bus, id, service, sublen, subFunc, length, dataBytes):按参数构造并发送 UDS 消息(对应UDS_HANDLER::sendUDSFrame的同名流程);- 在 JS 中定义
gotUDSMessage(bus, id, service, subFunc, len, data)回调函数即可接收解析后的 UDS 回复。
仓库自带的示例脚本 CaptureAllMsgTypes.js 展示了这种用法——它先发送一条 Tester Present(uds.sendUDS(0, 0x604, 0x3E, 1, 0, 0, 0)),再通过gotUDSMessage回调打印捕获到的 UDS 总线、ID 与服务码。这正是把“扫描窗口发现的能力”转化为“自定义自动化诊断流程”的推荐路径。
小结与安全提醒
SavvyCAN 的 UDS 扫描窗口是一个介于“CAN 模糊测试”与“诊断协议分析器”之间的实用工具:用 Tester Present 快速圈定节点地址,用 Session Control / Security Access / Read By ID 等类型逐项确认 ECU 的能力边界,用 Custom UDS 做全量探测,最后把结果保存为文本或交给脚本接口做进一步交互。
最后重申原文档与源码反复强调的安全边界:ECU Reset 会重置目标设备,Routine Control 可能触发不可预知的后果,Security Access 的暴力试探会触发 ECU 的锁定机制。所有扫描都应在合法授权、可承受风险的测试环境中进行——本工具的价值在于把“探测”做得清晰、可控、可复现,而不是替代对协议与目标的审慎判断。
- 开发工具
- 逆向工程
- 嵌入式
【免费下载链接】SavvyCAN
QT based cross platform canbus tool
相关推荐
SavvyCAN 脚本接口完全指南:用 JavaScript 的 host/can/isotp/uds 对象操控 CAN 总线
SavvyCAN 脚本接口完全指南:用 JavaScript 的 host/can/isotp/uds 对象操控 CAN 总线 SavvyCAN 内置一套基于
开发工具逆向工程嵌入式SavvyCAN 时间窗口(Temporal Window)实战指南:用密度热图定位 CAN 总线高流量时段与活跃 ID
SavvyCAN 时间窗口(Temporal Window)实战指南:用密度热图定位 CAN 总线高流量时段与活跃 ID Temporal Window 是 S
开发工具逆向工程嵌入式SavvyCAN Fuzzing Window 实战指南:用 CAN 总线模糊测试探测报文 ID 与数据位
SavvyCAN Fuzzing Window 实战指南:用 CAN 总线模糊测试探测报文 ID 与数据位 本篇技术指南以 SavvyCAN 内置的 Fuzzi
开发工具逆向工程嵌入式
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考