硬件安全攻防实战:Off-By-One会议徽章CTF挑战全解析

本文详细解析了Off-By-One会议徽章的硬件设计与CTF挑战,涵盖USB描述符分析、固件提取、硬件随机数生成、I2C通信、时序攻击和电压毛刺攻击等嵌入式系统安全技术。

#BadgeLife @ Off-By-One Conference 2024 | STAR Labs

引言

按照承诺,我们在活动结束约一个月后发布了Off-By-One徽章的固件和本文,为感兴趣的参与者提供探索机会。我们在首届Off-By-One Conference 2024上推出了Octopus徽章,该徽章是会议的亮点之一,包含了以硬件为核心的CTF挑战。本文将探讨徽章的概念和设计过程,并讨论解决挑战所需的概念。

硬件设计

艺术作品由Sarah Tan设计,特色是一个带有转动眼睛的可爱章鱼。经过头脑风暴多种设计后,我们决定为眼睛配备两个独立的圆形显示屏。以下是早期原型的一张草图:

将概念转化为电路设计,徽章围绕ESP32-S3主处理器构建,驱动一对GC9A01 OLED显示屏。用户可以通过按键和方向摇杆与徽章交互。此外,一个小型协处理器ATmega328P通过I2C协议进行通信。

电子设计在KiCad中创建,以下是3D渲染图。徽章有三种颜色变体,用于区分不同人群,如参与者、工作人员和志愿者。

最初计划包括一个可充电的LiPo电池,以在整个会议期间持续使用。但由于供应链困难,我们改用AAA电池。希望明年能在徽章中加入LiPo电池。

最终,实际徽章的外观如下!

硬件CTF挑战

与任何会议徽章一样,我们的徽章也包含CTF挑战。本节将解释这些挑战的灵感来源和预期解决方案。

嵌入式系统与完整的计算机非常不同,它最初是为资源受限的应用设计的。例如,ESP32-S3处理器没有内存管理单元(MMU)。这意味着嵌入式工程师编写代码的方式与软件工程师非常不同。

我们的目标是让参与者接触硬件黑客技术,而不仅仅是在便携式硬件设备中提供软件挑战。我们也在此过程中学习了如何改进我们的电子徽章。

1. USB字符串描述符

第一步是确定设备类型。因此,欢迎标志隐藏在USB字符串描述符中。

USB描述符会告诉我们设备的来源,如供应商和产品标识符,也被您的PC用于确定加载哪些驱动程序。

在Linux中,您可以使用dmesg打印内核调试消息。也可以在Windows中查看设备管理器。

1
2
3
4
5
6
$ dmesg -w
[3240249.488872] usb 3-3.2: New USB device found, idVendor=303a, idProduct=4001, bcdDevice= 1.00
[3240249.488883] usb 3-3.2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[3240249.488887] usb 3-3.2: Product: #BadgeLife
[3240249.488889] usb 3-3.2: Manufacturer: STAR LABS SG
[3240249.488892] usb 3-3.2: SerialNumber: {Welcome_To_OffByOne_2024}

或者,您也可以使用lsusb打印连接到PC的所有设备。

1
2
3
4
$ lsusb -vd 303a:
iManufacturer           1 STAR LABS SG
iProduct                2 #BadgeLife
iSerial                 3 {Welcome_To_OffByOne_2024}

2. C编译的内部库

下一个标志隐藏在一个名为flaglib的库中。这可以通过MicroPython REPL显示所有模块看到。

1
2
3
4
5
6
>>> help('modules')
[...] flaglib [...]

>>> import flaglib
>>> dir(flaglib)
['__class__', '__name__', '__dict__', 'getflag']

简单的解决方案是编写一个脚本通过暴力破解逐个字符提取。

1
2
3
4
5
6
7
>>> flaglib.getflag("")
''
>>> flaglib.getflag("{____________________________}")
'{??_????????_??????_??????????'

>>> flaglib.getflag("{my_compiled_python_library}")
'{my_compiled_python_library}'

然而,知道这是一个C编译的内部库,与固件捆绑在一起,这意味着如果转储闪存,可以检索其内容。特别是在低成本系统中,加密资源消耗大,我们可能通过转储固件或闪存发现以明文保存的密码或密钥。

尝试转储固件时,首先识别设备类型。从电路板上的标签,我们看到它是ESP32-S3-WROOM-1-N4。我们可以搜索数据手册,得知它有4MiB的闪存。

可以使用esptool.py包从ESP32-S3转储固件。按住BOOT按钮并按下RESET按钮将其置于引导加载程序模式。运行以下命令将完整内容保存到文件中:

1
$ esptool.py --baud 115200 --port /dev/serial/by-id/usb-** read_flash 0x0 0x400000 fw-backup-4M.bin

随后,通常会对IoT设备的转储固件进行简单的静态分析。

1
2
$ strings fw-backup-4M_black.bin | strings | grep {
{my_compiled_python_library}

3. 硬件随机数生成器

通过显示菜单,显示了一个损坏的轮盘游戏。许多人通过逆向从设备提取的MicroPython编译库roulette.mpy解决了它。文件可以轻松通过MicroPython IDE(如Thonny或Mu Editor)提取。

1
2
3
>>> from starlabs import roulette
>>> roulette.roulette()
([1, 0, 1, 2, 1, 2, 2, 1, 2, 2], None)

尽管如此,我们的预期解决方案是理解朴素的RNG方法是使用模数转换器(ADC)产生的噪声。这对于缺乏硬件RNG外设的较旧微控制器尤其相关。

模数转换器(ADC)常用于将来自外部传感器的模拟信号转换为处理器可以在数字域中使用的数字格式。

这是周围噪声的可视化示例,可以作为随机数采样的来源。

正确的引脚可以通过硬件模糊测试找到。在将引脚短路或焊接到地后,我们可以控制数字生成,标志将打印出来。这可以使用电阻完成,但裸线也足够。

4. Arduino协处理器

电路板还有一个使用Arduino平台构建的协处理器。

协处理器在许多IoT应用中很常见,例如具有外部安全元素或处理单元(用于加密、证书存储或神经网络处理器)。如果通信协议未加密,可以进行物理中间人攻击。未来,可以进行重放或欺骗攻击以控制主处理器的行为。

从我们在硬件介绍指南中提供的提示,用户可以访问I2C接口与Arduino通信。通过扫描I2C总线,用户可以发现总线上的地址。

1
2
>>> arduino.i2c.scan()
[48, 49]

内部集成电路(I2C)是一种在PCB上多个设备(微控制器、传感器或其他外部设备)之间常用的通信协议。多个设备连接到I2C总线,数据可以通过寻址方案交换。

这里我们看到两个I2C外设地址,分别是十六进制的0x30和0x31。通过从I2C外设执行读取请求,可以找到标志。

1
2
>>> arduino.i2c.readfrom(0x30, 100).rstrip(b'\xff')
b'Welcome to STAR LABS CTF. Your first flag is starlabs{i2c_flag_1}'

5. 时序攻击

如果我们从第二个地址执行读取请求,我们会收到一条消息,指示我们应该重启并更早重试。换句话说,该挑战指示参与者根据系统运行时间快速读取标志。

1
2
>>> arduino.i2c.readfrom(0x31, 200).rstrip(b'\xff')
b'The early bird catches the worm. System uptime: 221. You are too late. Reboot the arduino and try again.'

这给我们呈现了一种情况,在启动序列期间,某些信息目标设备可能在内存中存在很短的时间。我们可以通过重置MCU,在已知时间量后从内存读取来执行"时序攻击"。

注意:由于小的时序变化(我们谈论的是毫秒级),可能需要重复该过程直到获得相应的字符。

这是一个解决挑战的示例实现。

1
2
3
4
5
6
7
8
9
def derp(x):
  global arduino
  arduino.off()
  time.sleep(1) # 关闭arduino
  arduino.on()
  time.sleep(x); # 打开并等待已知时间量
  return arduino.i2c.readfrom(0x31, 200).rstrip(b'\xff') # 立即读取

for i in range(200, 500): print(derp(0.01*i)) # 按顺序重复

标志的每个字符如下检索:

1
2
3
4
5
6
b'The early bird catches the worm. System uptime: 197. You too early, wait a little longer!'
b'The early bird catches the worm. System uptime: 198. You too early, wait a little longer!'
b'The early bird catches the worm. System uptime: 199. You too early, wait a little longer!'
b'The early bird catches the worm. System uptime: 200. You are an early bird, here is your gift: s'
b'The early bird catches the worm. System uptime: 201. You are an early bird, here is your gift: t'
[...]

6. 电压毛刺攻击

要访问此挑战,我们必须从Arduino的UART串行端口读取。USB串行适配器的示例包括CH340串行适配器或FT232串行适配器。

有方便的引脚供我们使用。这是如何连接它的方法。

您将在新连接的串行端口上看到此消息。

1
2
3
4
Flag is Locked! Please help me to jailbreak it.
int i = 0, k = 0;
for (i = 0; i < 123456; i++) k++;
if (k != 123456) { unlock(); } else { lock(); }

在正常操作中,k == 123456将始终满足。因此,我们可以推断标志只能通过未定义操作获得。这是通过毛刺跳过if语句完成的。

电压毛刺涉及在处理器处于未定义状态的短暂时刻切断电源。此时,我们快速恢复电源,使其继续正常操作而不触发重启。重复此操作,希望在该未定义状态期间发生毛刺。

从MicroPython REPL中,已经为我们编写了一些代码,可以通过简单的函数调用打开和关闭Arduino。

1
2
3
4
>>> arduino
<MyArduino object at 3fcaac10>
>>> arduino.off();
>>> arduino.on();

为了执行毛刺,我们快速切换电源。当电压降低并快速连续升高时,可能发生毛刺。

1
>>> arduino.on(); arduino.off(); arduino.on();

我们可以使用示波器,这是一种允许我们实时查看电压波形的工具(这不是解决挑战所必需的,但它帮助我们可视化正在发生的事情)。下图描绘了从引出引脚测量的Arduino电压。

最后,如果我们幸运,会看到标志弹出:

1
2
3
4
5
6
7
Flag is Locked! Please help me to jailbreak it.
int i = 0, k = 0;
for (i = 0; i < 123456; i++) k++;
if (k != 123456) { unlock(); } else { lock(); }

This should not happen! k=123271
Unlocked. Here is your flag starlabs{voltage_glitching_is_cool}

完善毛刺: 由于制造差异,可能不容易复现,因为我们看到切换可能导致Arduino完全重启而不是毛刺。这是因为电源可能消耗太快(即毛刺"太强")。

通过识别附近组件(Qx1:MOSFET),我们理解电源是通过MOSFET晶体管切换的。还有一个"Glitcher"测试点,连接在MOSFET晶体管两端。我们可以通过在MOSFET晶体管两端焊接电容器或电阻器使毛刺更可复现。

结论

感谢所有参加会议并接受CTF挑战的人!徽章设计成功促进了对嵌入式系统的更深入理解以及处理硬件设备所需的技能。我们希望徽章能在会议结束后很长时间内激发进一步兴趣,并为探索和学习提供有价值的平台。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计