本页面包含 RAMN 参与过的历届 CTF 题解。您可从 GitHub 仓库的 misc 文件夹下载历届 CTF 固件及挑战题目。欢迎随时联系我们,以添加或移除您自己的题解。
参赛者题解:
标志格式为 flag{xxxx}。在 CHV 现场的桌子上提供了十块 RAMN 板。明确指出每块 ECU 都配备了不同的诊断接口(USB、UDS、KWP2000 或 XCP)。同时提醒,Flash 地址范围为0x08000000-0x08040000,RAM 地址范围为 0x20000000-0x20040000。
本页面包含简单的简要说明。带有详细解决方案的 Jupyter Notebook 可在 misc 文件夹中获取。
可以立即确定,具有 USB 诊断接口的正是 ECU A,因为它是唯一配备 USB 接口的设备。通过浏览各个屏幕,我们可以发现存在一个等待输入秘密代码的调试界面。

我们可以将提供的固件文件加载到 Ghidra 中。由于这是一个带有调试符号的.elf 文件,因此很容易进行逆向工程。
使用“窗口 > 已定义字符串”功能,查找“Awaiting secret code”字符串,并跟踪其引用,我们可以确定当 DEBUG_MODE_UNLOCKED 变量被设置为 1 时,调试模式即被解锁。
通过跟踪 WRITE 引用,我们可以发现,如果 verify_secret_input 函数返回 1,则该变量会被设置为 1。

通过分析 verify_secret_input 函数,我们可以确定哪些 RAMN 输入会触发调试模式的解锁。

借助 cansniffer(或 RAMN 的 CAN RX MONITOR 界面),我们可以通过实际操作控制装置并观察相应变化的 CAN ID 及其有效载荷,来识别每个控制对应的 CAN ID。随后,我们可以按照 verify_secret_input 函数所指定的方式设置 RAMN 的输入。

调试界面现在显示,通过输入“#”可以访问一个新的 USB 命令接口,并且当前用户名的 CRC 校验值不正确。

通过在 USB 接口中输入“#”后跟“help”,我们发现存在一个名为“username”的命令,该命令接受一个参数。
通过将“default_user”输入到 https://crccalc.com/ 并查找生成 C862ED4F 的算法,我们确定所使用的 CRC 算法为 CRC-32/ISO-HDLC。只需输入一个 CRC 值为 0xDA5D344D 的用户名,即可获取到 flag,这一 CRC 值可使用 CRC RevEng 或 crchack 等工具计算得出。


通过使用诸如 caringcaribou 之类的扫描工具,可以确认 ECU B 具有一个活跃的 XCP 接口(位于 CAN ID 0x552/0x553):

进一步使用 caringcaribou 进行探测后发现,CAL/PAG 资源是可用的,但需要身份验证。

命令 caringcaribou xcp commands 0x552 0x553 可用于识别当前可用的命令包括:GET_STATUS、SYNCH、GET_SEED、UNLOCK、SET_MTA、UPLOAD 和 DOWNLOAD。
从提示信息以及目前收集到的数据可以推断,如果使用 DOWNLOAD 命令(0xF0),标志将通过 CAN ID 0x777 传输——但该命令需要身份验证(通过 GET_SEED 和 UNLOCK)。
我们可以利用 SET_MTA 和 UPLOAD 命令从 ECU 中转储内存。通过 caringcaribou xcp dump 0x552 0x553 0x08000000 256 可以观察到,闪存范围无法访问,但 RAM 范围是可以访问的(例如,通过 caringcaribou xcp dump 0x552 0x553 0x20000000 256 )。需要注意的是,某些版本的 caringcaribou 可能存在漏洞,导致该命令无法成功执行。
Caringcaribou 不支持 XCP 身份验证,因此后续步骤应直接按照 XCP 规范发送 CAN 报文来完成。种子可以通过以下命令请求:cansend can0 552#f80001。

这表明种子为6字节。 我们可以使用 dump 命令转储完整的 RAM 地址范围(0x20000000-0x20040000),并在其中搜索种子。 Caringcaribou 不应未经修改直接使用,因为它会在每次执行命令时重置 XCP 连接,从而导致种子被重置。种子可能位于多个位置(例如,在 TRNG 缓冲区中),因此应检查所有可能的位置。
要通过 XCP 转储 RAM,我们可以使用 SET_MTA(0xF6)和 UPLOAD(0xF5)。例如,要从 0x20000000 转储 6 个字节,我们使用:
每次调用 UPLOAD 都会转储下一个地址(例如,对于上述示例,地址为 0x20000006)。通过对不同种子的整个 RAM 进行转储,我们可以确定种子始终位于 0x20033f50 附近。此外还可以观察到,在其紧邻位置存在另一个 6 字节的变量,该变量会随之变化。这很可能就是我们期望找到的种子答案。由于没有身份验证尝试次数限制,我们可以自由地尝试不同的排列组合。

可以通过请求种子、转储 RAM 以读取预期答案、利用该答案解锁 ECU,然后使用 DOWNLOAD 命令让 ECU 传输标志来读取标志。

挑战提示表明 ECU C 使用了 KWP2000 协议。我们可以使用 caringcaribou 工具来查找使用 UDS 和 KWP2000 协议的 ECU:

根据 RAMN 的文档,我们可以确定 7e3 对应于 ECU D 的 UDS 接口,而 7e6 对应于 ECU C 的 KWP2000 接口(这一点可以通过 ReadDataByIdentifier 读取信息来确认)。在本挑战中,我们重点关注 7e6/7ee(即 ECU C 的 KWP2000 接口)。
通过 caringcaribou 的服务发现功能,我们可以看到有许多可用的服务:

服务 0x1a 的存在表明该接口是 KWP2000 协议,而非 UDS 协议。服务 0x29 并不对应于认证(AUTHENTICATION),因为这不是一个 UDS 接口(从技术上讲,我们也不应该用 caringcaribou 来处理它)。
尝试使用 ReadDataByIdentifier 读取所有 DID 时发现,DID 0x0000 返回“安全访问被拒绝”。由此可以推断,本挑战的目标就是绕过这一安全访问限制。

尝试使用默认会话请求种子时,将返回错误代码 0x80,对于 KWP2000 而言,这意味着当前会话不支持该服务。我们可以通过暴力破解所有会话来观察到,会话 0x92(KWP2000 扩展会话)是可用的。然而,在请求安全访问种子时,我们可能会遇到其他错误:要么是“无需时间延迟”(意味着我们必须等待暴力破解保护计时器到期),要么是“子功能不支持”。
通过暴力破解所有安全等级,我们观察到安全等级 0x05 存在,并返回一个 16 位的种子:

我们可以请求任意数量的种子,这些种子似乎是随机的。由于种子长度仅为 16 位,因此暴力破解似乎是最简单的方法。然而,ECU 会限制尝试次数:

幸运的是,通过进一步探索可以发现,每当调用“诊断会话控制”服务以请求新会话时,ECU 都会重置尝试次数,这使我们能够随意尝试任意多次,而无需重启 ECU。
因此,我们可以使用以下脚本,该脚本会反复请求新的种子,并尝试答案“1234”(一旦最终成功解锁 ECU,应立即停止脚本)。
(请注意,此脚本速度非常慢但功能正常;我们原本期望参赛者编写一个更高效的脚本。)几分钟后,ECU 应该会被解锁,并且可以通过 ReadDataByIdentifier 服务,使用 DID 0x0000 读取标志位:

通过之前的挑战,我们了解到 ECU D 在 7e3/7eb 处存在 UDS 接口。我们可以使用 caringcaribou 来扫描可用的服务:

我们可以使用 dump_dids 模块来读取所有 DID:

我们可以观察到,WriteDataByIdentifier 处于激活状态,并且唯一可写入的 DID 是 0x0207,其值似乎指向 RAM 中的一个地址。我们尝试对该值进行轻微修改,结果发现 RAMN 上的 LED 随之发生变化。由于 DID 0x206 的描述为“LED 控制指针”,并且提示和标题均表明与 LED 有关,因此我们可以推断,该 DID 用于指定内存中显示在 LED 上的地址。此外,我们还可以观察到,该值也可以指向闪存地址。
因此,我们可以预期能够将标志位的值逐字节地显示在 RAMN 的 LED 上。然而,我们目前仍不清楚标志位的具体地址。
我们可以观察到,REQUEST_UPLOAD 和 TRANSFER_DATA 处于激活状态,这使我们能够转储固件(参见 请求上传 (0x35))。挑战提示中已明确指出固件的大小为 0x0c548 字节。
转储固件后,我们可以在 Ghidra 中打开它(使用与 ECUA_REDACTED.elf 相同的设置)。通过搜索“flag”,我们可以找到字符串 Loaded FLAG from private flash at address %p ,其中 %p 被替换为“0x0803e000”。因此,我们可以得出结论:标志位于 0x0803e000 处,我们只需利用按标识符写入数据服务逐字节转储即可(文档中可立即识别出哪个 LED 对应哪个位,详见 正文 )。
标志:flag{BEST_LIGHT_SHOW_IN_VEGAS}
参赛者题解:
Flag 格式为 bh{xxxx}。六支队伍每队均获得两套带有 CTF 固件的 RAMN 设备,此外还有一套配备标准固件的参考 RAMN 设备供参赛者共享使用。题目标题中括号内的字母表示 flag 所在的 ECU。
预计难度:简单。标签:取证。
提供的文件是一个逻辑分析仪捕获数据(来自 Scanaquad SQ200)。
它可以在诸如 PulseView 之类的工具中加载并进行解码(应在导入选项中输入正确的 CSV 格式)。加载完成后,信号即可通过 SWD 协议分析器进行解码(正如标题所提示的那样)。

解码后的数据可以导出为文本文件。只需在小端序(7B6862)中搜索字符串“bh{”(十六进制为 62 68 7B),即可直接显示出明文形式的标志。

标志:bh{an4lyst_s3ssION_Ro4d}。
预期难度:非常困难。标签:取证、逆向。
本挑战延续 SWD 1。第一步是从逻辑分析仪捕获中提取完整的固件。这可以通过查找“W AP4”命令(指示地址)以及“W APc”命令(指示要写入该地址的数据)来实现。选手需编写脚本,以重建固件的二进制文件(代码 FLASH 起始于 0x08000000;数据以 32 位小端字节序分块写入)。

固件文件重构完成后,可在 Ghidra 中加载。为便于分析,需根据提示设置内存映射。

搜索 0x12345678 可发现发送标志的函数。由此可知,第二个标志(未混淆时)位于 0x20030020。

仅存在另一处对 0x20030020 的引用——由此可推断,该函数正是用于加载其中标志的函数。

这揭示了用于对标志进行去混淆处理的函数。

其核心部分最初是用 C 语言编写的:
剩下的唯一一步是确定混淆标志位于何处。根据之前的步骤可知,0x20037750 是混淆标志在 RAM 中的位置。挑战提示中提供了 Reset_Handler() 函数的地址:

可以推断,RAM 的默认值是从 FLASH 的 0x0800ab94 加载到 RAM 的 0x20037750(混淆标志恰好位于.data 节的第一个地址,紧挨着 SWD1 标志之前):

以上步骤可用于对标志进行去混淆处理,得到 bh{pr0duct_AMB1tion}。
预期难度:简单。标签:硬件。
LED 由 ECU D 的 SPI 接口控制,且 SPI 信号在车身 PCB 上有明确标记的探针。使用逻辑分析仪观察 SPI 信号可以发现,ECU D 正常情况下每 10 毫秒更新一次 LED 的状态。

当发动机钥匙处于 IGN 位置时,可以看到在传输 LED 状态之前有一段突发的数据——这是以明文 ASCII 格式的标志位。

标志:bh{TREE_FORMS_WIND}。
预期难度:中等。标签:硬件。
本挑战要求玩家阅读 STM32L5x2 数据手册 ,并确定 I2C2 端口的可能引脚。
SDA 可能位于 PF0、PB11 或 PB14;SCL 可能位于 PF1、PB10 或 PB13。PB13 已被“Follow Me”挑战中的 SPI 接口占用,而 PF0/PF1 在 RAMN 使用的 48 引脚封装上不可用。因此,板上可供尝试的配置仅剩下 SDA:PB11/SCL:PB10 和 SDA:PB14/SCL:PB10(其中前者为正确配置)。可以使用任何 I2C 工具进行尝试,例如配置为 I2C 模式的 FT2232H 板。
这将触发标志的传输。

标志:bh{INFAMOUS_REMAKE}。
预计难度:中/难。标签:CAN,硬件。
挑战提示表明,存在一个“被遗忘的字段”,大多数 CAN 工具(如 candump)并未显示该字段。快速查阅维基百科上的 CAN 页面后发现,这很可能是指 CRC 字段(提示中的“check some”进一步暗示了这一点)。
因此,解决方案是查看 ID 为 0x607 的 CAN 帧的 CRC 字段。最简单的方法是使用逻辑分析仪观察 CAN 帧(如果关闭其他 ECU,并直接观察 ECU D 的 TX 引脚而非 CANH/CANL,则更为方便)。另一种解决方法是根据 candump 的数据重建 CAN 帧(注意,在计算 CAN 协议的 CRC15 之前,必须重现位填充过程)。

flag 即为 CAN 帧的 CRC 值(每帧一个字节):bh{LAGGING_BEHIND}。

预计难度:简单。标签:CAN,硬件。
正如标题所示,本挑战是对 CVE-2017-14937 的简单复现。CVE-2017-14937 详细描述了如何利用 ECU 的安全访问服务来解锁 ECU。一旦 ECU 被解锁,玩家只需使用 WriteDataByIdentifier 服务,在 DID 0x1111 处写入任意数据,即可通过 ReadDataByIdentifier 服务,利用 DID 0x0000 读取标志。
详细的参赛者报告可在此处查看。
Flag: bh{SUP3RS0NIc}。
预计难度:中等。标签:CAN,硬件。
本挑战以附件形式提供了 ECU B 的固件(其中包含已屏蔽的标志)。该固件采用 ELF 格式,并带有调试符号,便于进行逆向工程。(另一个以 ELF 格式提供固件的原因是,在仅提供.HEX 文件的挑战中,这样可以更轻松地确定正确的 Ghidra 设置。)
通过搜索标志位可以发现,它能够使用 ReadDataByIdentifier UDS 服务,并以 DID 0x0001(由于 ARM32 的字节序问题,在 Ghidra 中显示为 0x100)进行读取。

然而,正如挑战提示中所说明的,存在一个全局变量 UDS_ENABLE,当其值为 0 时,会阻止玩家使用 UDS。

玩家应注意,该变量的默认值为 1(表示 UDS 可用),但在启动过程中被设置为 0。

由于该值是在 CAN 外设被激活之后才被设置为 0,因此存在一个 10 毫秒的窗口期,在此期间可以使用 UDS。因此,解决方案是在 ECU 启动时持续发送请求。

标志:bh{an4lyst_s3ssION_Ro4d}。
预期难度:中等。标签:CAN、USB。
ramn_utils.c 中的 ascii_hashmap 表(其代码可在 github 上获取)用于将 ASCII 十六进制字符串转换为字节。由于十六进制字符仅由“0 到 9”、“A 到 F”以及“a 到 f”组成,因此该表大部分区域均填充了 0x00。
挑战提示表明,flag 位于该表中。通过阅读源代码可知,在使用 slcan 接口请求发送 CAN 消息时,slcan 协议的‘t’命令会调用 ASCIItoUint8 函数。
发送 SLCAN 命令的格式为 t<id><dlc><data>。一种从表中转储单个字节的简单方法是执行 SLCAN 命令 t00210 <index>,以强制 ECU A 通过 CAN ID 0x002 发送位于<index>处的字节。通过重复执行该命令并同时观察 CAN 总线(使用外部 CAN 适配器),我们可以转储整个表格——其中就包含该标志位。

标志:bh{B4RK_B0RK_bOrK}。
预计难度:中/难。标签:UDS。
挑战提示给出了标志的地址和大小。扫描 ECU C 的 UDS 服务发现,DynamicallyDefineDataIdentifier 服务处于激活状态。因此,可以利用该服务在 0x0803e000 处定义一个动态 DID(根据 UDS 标准,该 DID 应位于 0xF300-0xF3FF 范围内),大小为 26。随后,通过使用 ReadDataByIdentifier 读取该 DID,即可获取标志。
Flag: bh{TAKE_THE_LONG_WAY_HOME}。
参赛者 报告请见此处 。
预计难度:中等/困难。标签:CAN,硬件。
屏幕上显示了一个“拉面点击器”游戏,每次按下 SHIFT 操纵杆的中心按钮时,计数器就会加一。提示表明,当计数器超过 0x9000 时,即可显示 flag。

通过观察 CAN 总线,可以发现 ECU A 与 ECU C 之间不存在任何身份验证机制,因此伪造操纵杆状态轻而易举。利用 cansniffer 工具,我们可以观察到 045#0106 对应“操纵杆按下”,而 045#0101 对应“操纵杆松开”(第一个字节表示档位状态,可忽略不计)。
然而,尝试伪造这些消息将触发以下画面:

反作弊系统并不会进行惩罚,游戏无需断电重启即可重新开始。由于未提供固件,目前尚不清楚究竟是什么触发了反作弊系统。不过,由于缺乏适当的身份验证机制,我们可以确定,只要伪装不被轻易识破,就应当有可能冒充 ECU C。
第一步是将 ECU C(通常负责传输操纵杆消息)从 CAN 总线上移除,例如可以通过以下方式实现:
此后,玩家只需发送045#0106和045#0101即可增加点击次数。
注意:当 ECU A 超过 500 毫秒未收到 CAN ID 为 045 的消息时,或当 ECU A 接收到 ID 为 001 的消息时(即 ECU C 在自身接收到 ID 为 045 的消息后,会发送 ID 为 001 的消息以警告 ECU A,从而得知有人正在作弊),反作弊系统将被触发。系统并未对消息频率进行检查。玩家无需完全了解这些具体条件,只需在正常流量与被篡改流量之间尝试实现相对平滑的过渡即可。
另一种解决方案是物理上按下该按钮 0x9000 次。
Flag: bh{N1NN1KUM4SHIMA5HI}.
预计难度:非常困难。标签:逆向、UDS、硬件。
所附文件为一个.hex 文件,不含调试符号,因此逆向工程难度稍大。这种.hex 文件在“Security Access 1”和“Security Access 2”挑战中较为常见。使用 Ghidra 进行初步分析(以 ARM v8 LE 模式加载)后发现,正如标题所示,在完成安全访问认证后(分别针对级别 0x01 和 0x03),可通过 ReadDataByIdentifier(DID 0x0001 和 DID 0x0002)读取标志位。
参考以下内容可引导我们找到安全访问算法。

“安全访问 1”的安全访问检查由 FUN_0900be24 执行,通过 ChatGPT 等 AI 工具可识别其为“memcmp”。由此可以推断,08002310h 处存储的是预期的 16 字节(静态)密码的地址。

08002310h 包含 0BF974C0h,但该地址在固件文件中无法找到。
根据参考手册中的地址映射,我们可以观察到 0BF974C0h 位于系统内存引导加载程序区域(位于 ROM 中)。诀窍在于识别出该值位于系统内存中,因此对所有 STM32L552 微控制器(至少是同一批次的)都是通用的,从而可以从另一台 ECU 中读取。因此,密码可以被读取:
借助 ECU C 的 UDS:

在参考 RAMN 的 ECU D 上启用 JTAG:

通过发送该密码并读取 DID 0x0001,即可获取标志。

标志:bh{an4lyst_s3ssION_Ro4d}。
预计难度:非常困难。标签:逆向、UDS。
按照与“安全访问 1”相同的步骤,我们可以识别出用于检查密码的函数。

该函数将提供的密码与四个 32 位值进行比较,其依据是一个函数,该函数以字符串“HAPPY HAPPY HAPPY HAPPY”、“HAPPY HAPPY HAPPY”、“HAPPY HAPPY”和“HAPPY”(及其相应长度)作为参数。
参考相关资料,并借助 ChatGPT 的帮助,我们可以确定:
因此,密码是以 32 位块的形式从 40023000h 处读取的。再次查阅参考手册 ,我们可以确认该地址对应于 CRC 引擎外设的一个特殊功能寄存器。尽管我们可以通过参考文献逆向工程该引擎的参数(这些参数在 FUN_08003580 中被初始化),但由于没有尝试次数限制,我们也可以简单地尝试所有常见的 CRC32 算法(采用不同的字节序)。
我们可以使用 https://crccalc.com/ 并采用默认的 STM32 CRC 引擎算法(CRC-32/MPEG-2),这将为我们提供 0x14b311c9、0x6442CA33、0xC25DE077 和 0x6DA5F0C1,它们对应正确的密码。

Flag: bh{Thanks_P3riPH3Rals!}。