UDS诊断27服务:NRC 35/36/37三道关卡排查全解析 📅 发布时间:2026/9/2 19:45:37 👁 浏览次数: 开发UDS诊断时最让人头疼的往往不是协议本身而是 ECU 冷不丁回一个7F 27 36。你翻遍协议栈代码觉得请求格式完全正确你对比了 seed/key 算法觉得结果也没有问题但 ECU 就是不肯放行。最近我帮一个团队排查刷写流程自动化脚本连续跑了三天每天都会在 27 服务上卡住。日志里一会儿是NRC 0x35一会儿是NRC 0x36偶尔还冒出NRC 0x37。刚开始大家默认这是同一个问题的三种表现于是反复调 seed/key 算法结果毫无变化。后来把日志按时间顺序摊开才发现这三个 NRC 根本不是同一层的问题——它们分别对应安全访问流程里三个完全不同的检查关卡。这篇文章就围绕 27 服务的 NRC 35/36/37讲清楚它们各自的触发机制、排查思路和工程落地时的避坑经验。1. 27 服务到底在做什么NRC 在哪个环节生效1.1 27 服务不是“密钥计算服务”而是访问控制服务27 服务全称 Security Access翻译过来叫安全访问。它解决的根本问题不是“怎么生成 key”而是“ECU 怎么允许诊断仪访问受保护的功能”。通常情况下刷写、标定、特定数据写入这类操作都不会直接开放。诊断仪要先向 ECU 请求一个 seed然后根据 seed 算出一个 key再把这个 key 发回给 ECU。ECU 校验通过后会把当前会话的安全等级置为“已解锁”后续那些受保护的诊断服务才允许被调用。所以 27 服务本质上是一个流程控制服务不是纯计算服务。这也是很多人容易误解的地方——只盯着 seed/key 算法忽略了整个流程的时序、次数、会话状态和权限管理。一个完整的 27 服务访问流程通常是这样诊断仪发送27 01请求安全种子。ECU 返回67 01附带一串 seed 数据。诊断仪根据 seed 计算出 key。诊断仪发送27 02附带 key 数据。ECU 校验 key。通过则返回67 02失败则返回7F 27 37。后续发送受保护的服务请求时ECU 会先检查当前安全等级是否已解锁。注意第 2 步和第 5 步之间以及第 5 步之后都有可能有额外的状态检查。NRC 35、36、37 就是在这条链路的不同节点上触发的。1.2 NRC 不是报错信息而是状态机给出的“拒绝理由”NRC 全称 Negative Response Code也就是负响应码。ECU 收到一个请求后如果不能满足会返回7F SID NRC告诉诊断仪“这个请求我没法接受原因是 X”。在 ISO 14229 的定义里NRC 有很多个覆盖了格式错误、不支持、长度错误、条件不满足、安全校验失败等场景。27 服务相关的三个高频 NRC 是NRC 值名称字面含义在 27 服务里的真实含义0x35requestOutOfRange请求超出范围请求参数格式、子功能、seed/key 长度不合法0x36securityAccessDenied安全访问被拒绝访问被时序、次数、状态机规则拒绝0x37invalidKey密钥无效seed/key 校验过程中 key 不匹配这三个 NRC 虽然经常同时出现在日志里但它们的位置完全不同。用一句话概括主判断NRC 0x35 查格式NRC 0x36 查状态NRC 0x37 查算法。排查时先确定报错发生在哪一关再决定改哪里。这句话看起来简单实际调试时非常有用。接下来逐个拆开讲。2. NRC 0x35第一个关卡先查请求本身是否合法2.1 0x35 不一定是“参数超范围”更准确说是“请求没通过格式检查”0x35 requestOutOfRange在文档里的定位是请求参数超出范围或者请求操作在当前会话/当前状态下不被允许。但实际调试时27 服务返回0x35最常见的情况有三类第一子功能参数不对。ECU 只支持01/02这一组安全级别你却发送了03/04或者使用了不规范的自定义子功能值。有些 ECU 会把这种请求直接认定为“请求超出范围”。第二请求长度不对。27 服务请求 seed 时标准请求长度就是27 01两个字节。发送 key 时需要27 02 keykey 的长度要和 ECU 定义的 seed/key 长度匹配。如果你发送的 key 长度不对ECU 可能返回0x35也可能返回0x13报文长度错误。具体返回哪一个取决于 ECU 的协议栈实现顺序。第三seed 或 key 的数据长度与算法定义不匹配。比如算法定义 seed 是 8 字节你按 4 字节去解析或者反过来。这个问题经常出现在“同一个平台不同项目”的场景里因为不同车型可能对 seed 长度定义不同但测试脚本沿用了上一版。2.2 看到 0x35先按这四个方向排查建议按下面这个顺序检查子功能值是否在 ECU 支持的安全级别列表里。请求 SID 和子功能之后的数据长度是否和 ECU 协议栈规定一致。seed/key 的长度定义是否匹配。查看诊断调查表Diagnostic Specification或者 ECU 软件设计文档里的 Security Access 配置。当前会话模式是否允许执行 27 服务。有些 ECU 在默认会话下不允许做安全访问必须先切换到扩展会话或编程会话。这里有个容易忽略的点如果 ECU 在默认会话下收到27 01有的会直接返回0x35或0x22条件不满足而不是0x7F的子功能不支持。所以看到0x35时不要第一反应就去翻算法先确认请求报文本身有没有问题。2.3 一个容易误判的 0x35多级别安全访问参数错位很多 ECU 支持多级别安全访问比如级别 1 用于标定级别 2 用于刷写。常见请求形式是27 01请求级别 1 的 seed27 02发送级别 1 的 key27 03请求级别 2 的 seed27 04发送级别 2 的 key但如果 ECU 设计成“一个子功能对应固定访问级别”也就是27 01请求访问级别 1、27 02请求访问级别 1 的 key那么你发送27 03时ECU 可能直接返回0x35表示“这个子功能在我这里不存在”。这时候如果你还在纠结 key 算法方向就错了。排查提示把 27 服务当成一个普通诊断服务来理解。先确认你的请求能通过格式检查再谈后面的事。3. NRC 0x36第二个关卡访问被时序和状态机挡住3.1 0x36 的本质ECU 认为你没有“资格”执行这一步0x36 securityAccessDenied是一个状态管理层面的拒绝。它跟 seed/key 算法无关跟请求格式也无关。它表示你的请求格式合法但你访问的时机、频率、状态不满足 ECU 的安全策略。最常见触发场景包括连续输错 key 达到最大次数限制。很多 ECU 默认允许 3 次或 5 次失败超过后进入延时锁定。锁定期间继续尝试。ECU 会在一段时间内拒绝所有 key 请求常见锁定时间是 10 分钟也有更长的。解锁成功后没有及时使用超时后安全状态自动关闭。此时再发送 key 请求也会被拒绝。解锁成功后进行了会话切换ECU 的安全状态被重置。此时如果继续沿用旧的安全状态去请求受保护服务也会遇到0x36。这类问题的核心是“状态”不是“数据”。3.2 处理 0x36 的正确顺序不要急着重试遇到0x36最忌讳的就是马上重试。因为0x36一旦触发往往意味着失败计数器已经累加继续重试只会让锁定时间更长。正确顺序是确认当前 ECU 是否处于锁定状态。读一下 ECU 的诊断会话状态、安全等级状态或者等待一段时间后重新发送27 01。检查日志中的时间戳。如果在短时间内连续发送了多次无效 key那基本就是触发了失败计数。检查是否刚做过会话切换。如果从扩展会话切回默认会话再切回扩展会话安全状态通常会被清掉。如果自动化流程里包含了安全访问建议在会话切换后重新执行完整的“请求 seed 发送 key”流程而不是直接复用之前解锁状态。如果 ECU 支持可以尝试通过下电重启来清除锁定状态仅测试阶段可用量产环境要按规范来。3.3 0.36 和 0.37 的先后关系先查状态再查算法这里有一个很有用的判断逻辑。ECU 收到 27 服务请求时通常会按下面这个顺序做检查格式检查子功能、长度、参数范围。不通过 → 返回0x35。状态检查当前安全等级、锁定状态、失败计数、时序。不通过 → 返回0x36。算法校验seed/key 计算比对。不通过 → 返回0x37。也就是说在大多数实现里0x36的优先级高于0x37。如果你的 key 算法完全正确但当前已经因为失败次数过多被锁了ECU 会在发送27 02后直接返回0x36让你误以为是 key 算错了。这时候一味去改算法只会把问题越带越偏。建议每次调试 27 服务日志里至少记录四样东西请求时间、请求子功能、seed 值、返回 NRC。有这四样才能判断是状态问题还是算法问题。4. NRC 0x37第三个关卡key 校验失败才轮到算法出场4.1 0x37 的含义你已经通过格式检查和状态检查就差 key 比对0x37 invalidKey是三个 NRC 里最容易被理解的一个ECU 已经给了你 seed你也发回了 key但 key 和 ECU 内部计算的结果不一致。此时ECU 的安全状态不会变成“已解锁”通常失败计数器也会加一。连续失败多次就会触发0x36的锁定。所以看到0x37时可以确认两件事你的请求格式、会话模式、子功能是正确的。你的 seed 已经被 ECU 接受问题出在“从 seed 到 key”这一步。4.2 排查 0x37 的关键维度seed/key 算法各家差异很大但排查方向基本是固定的seed 值是否完整、准确。注意大小端和字节序。key 的输出长度是否和 ECU 定义一致。算法中间步骤是否引入了额外参数。比如 GPIO 状态、定时器值、VIN 码、随机数、固定密钥表。是否使用了正确的密钥版本。很多 ECU 在不同项目里用不同密钥表同一个 seed不同项目算出的 key 完全不同。是否有加盐、掩码、校验和等额外处理。这些维度里字节序和掩码是最高频的坑。举个例子seed 是A1 B2 C3 D4有的算法直接把这四个字节查表运算有的算法要先做字节反转变成D4 C3 B2 A1还有的算法会把每个字节和一个固定数做异或后再查表。表面看算法版本一样但细节差了十万八千里。4.3 实际调试中最有效的方法先找一组标准样本对在没有确定性依据的情况下不要用穷举法去试 key效率极低。建议先做两件事找 ECU 供应商或开发团队要一组“固定 seed 对应固定 key”的验证样本。用 Python 或脚本写一个算法验证函数先把样本对跑通再接入真实动态 seed。下面是一个示意结构具体算法以 ECU 厂商定义为准def compute_key(seed: bytes) - bytes: # 这是一个示例结构不是通用算法。 # 真实实现可能是查表、字节置换、CRC、AES、自定义流式算法等。 # 在没拿到算法定义前先用样本对验证函数是否和 ECU 一致。 key b # TODO: 在这里完成从 seed 到 key 的转换 return key seed bytes.fromhex(A1B2C3D4) expected_key bytes.fromhex(8E7D6C5B) result compute_key(seed) if result.hex().upper() expected_key.hex().upper(): print(验证通过) else: print(算法实现与样本不一致先检查字节序和掩码逻辑)这段代码本身没有任何实际算法但调试流程值得借鉴先用固定样本对锁定算法的确定性再去看动态逻辑。如果连样本对都拿不到就只能靠日志回溯 seed再手工验证算法分支过程会非常痛苦。4.4 0x37 之后如果又出现 0x36说明失败计数已经在累加很多项目里的自动化脚本会这样写发送27 02收到0x37后重新获取 seed再继续尝试。如果循环写得比较粗暴连续三次失败后下一条请求就会收到0x36。这时候日志里会出现非常典型的现象0x37和0x36交替出现。刚开始看到这种日志会以为是状态机问题但实际上前面几个0x37已经定义了失败次数后续的0x36只是延时锁定的表现。两者不是独立问题是同一个问题链。5. 一套可复用的排查流程三个 NRC 对应三个检查关卡5.1 先确定问题在哪一级可以按下面这张判断图来定位收到0x35→ 查请求格式、子功能、长度、会话模式。收到0x36→ 查状态机、失败计数、时序、会话切换。收到0x37→ 查 seed/key 算法、字节序、密钥表、掩码。注意这种对应关系在大多数 ECU 上是成立的但不是绝对标准。因为不同厂商的协议栈实现顺序不同某些 ECU 会把参数错误统一返回0x31或0x22。所以这不是法条而是高效定位的起点。5.2 用日志回溯建立完整事件链建议在日志里至少记录这些字段字段说明时间戳判断是否触发延时等待诊断会话当前是默认会话、扩展会话还是编程会话请求 SID27 服务子功能01/02/03/04 等seed 值ECU 返回的原始 seedkey 值诊断仪发送的 keyNRC返回的负响应码连续失败次数用于判断是否触发锁定有了这张表排查顺序就很清晰了看 NRC 出现的位置。看连续失败次数。看两次 27 服务请求之间的时间间隔。看是否在会话切换后立即出现。大多数 27 服务问题在这一步就能定位到 70% 以上。5.3 三个 NRC 对应的边界条件清单在实际项目中建议按下面这些边界条件逐项验证安全级别支持列表确认 ECU 支持哪些子功能。seed/key 长度确认是 4 字节、8 字节还是其他长度。算法版本不同项目、不同硬件版本可能有不同密钥表。失败计数上限确认是 3 次还是 5 次。锁定时间确认锁定后是等待 10 分钟还是需要重新上电。会话切换行为确认安全状态是否会随会话切换被清空。解锁超时时间确认解锁后多久会自动关闭安全状态。这些参数不是每个 ECU 都会公开但至少要在测试前向供应商确认关键几项尤其是安全访问失败后的恢复方式。6. 长期来看27 服务调试需要沉淀什么6.1 不要把一个 NRC 当成独立错误处理要当成流程关卡很多团队在排查 27 服务时习惯用“NRC 对照表”来做定位。看到0x37就搜“invalidKey”然后开始查算法。这种做法的最大问题是忽略了 NRC 背后还有状态机和时序约束。更好的做法是把 27 服务理解成一个三层关卡格式关请求合法吗状态关当前状态下允许访问吗算法关key 对得上吗三个 NRC 分别对应这三关的失败结果。用这种框架去排查方向不会走偏而且能很快区分“是脚本问题”“是状态机问题”还是“是算法问题”。6.2 自动化刷写/标定流程里27 服务要单独封装在实际工程里不建议到处散落 27 服务的发送代码。建议把它封装成一个独立模块对外只暴露三个接口request_seed(level)send_key(key)unlock(level, key_calculator)这样做的原因是27 服务的异常处理逻辑非常集中失败计数、延时等待、会话切换、重新解锁都应该在同一个模块里统一管理。如果每个脚本自己写一遍很容易出现不同脚本对0x36的处理方式不一致导致量产现场表现不可控。6.3 给新同学的建议先跑通最小样本再进入复杂流程如果是刚开始接触 27 服务不要直接冲进自动化刷写流程。建议先做这几步手动发送27 01确认能拿到 seed。用一组已知样本对确认 key 计算正确。连续发送错误 key观察 NRC 从0x37变到0x36的过程。等待锁定解除后用正确 key 解锁确认后续受保护服务可以正常访问。把以上步骤固化成脚本再接入刷写流程。这个路径看起来很慢但能帮你建立对 27 服务状态机的直觉。否则直接在自动化脚本里排查日志一多反而分不清当前处于哪个状态。回到最初的主判断三个 NRC 不是三个并列的错误码它们对应安全访问流程的三道关卡。格式、状态、算法每一关的失败原因完全不一样排查方法也完全不同。下一次再看到7F 27 36先别急着改算法。看一下时间戳数一下失败次数再瞄一眼当前会话状态——大概率问题不在 key 上而在你比 ECU 更着急。