CrewCTF 2024 硬件题目
这两道题共用一个压缩包附件,需要找到在键盘上输入的密码和输完密码在屏幕上显示的内容
通过阅读 README 得到的信息如下:他用逻辑分析仪嗅探了设备的通信过程,来看一张全局的图,里面除了树莓派和逻辑分析仪其他两个不知道是啥,看看别的图找线索
通过这张图搜索发现是个电子墨水屏:
https://learn.pimoroni.com/article/getting-started-with-inky-phat
通过谷歌搜索键盘上的型号得知通信接口是 IIC:
https://docs.m5stack.com/en/unit/cardkb
Sniff One
首先来看键盘输入的信息,用逻辑分析仪对应的软件打开给的嗅探文件,嚯,这么多,来了个大活...
这得确认一下哪根线是哪个啊,在 README 中已经给出了,根据这个描述去找接线图,键盘的黑是 GND,红是供电,那么白色和黄色是 IIC,对应了其他地方的灰色和紫色
找到逻辑分析仪上的灰色和紫色,通过这张图可以看出来是通道 0 和通道 1
那么在逻辑分析仪软件中把通道 0 和通道 1 设置成 IIC 解析一下,这软件搜索功能有点不太行啊,导出来搜索一下
证据确凿,就是你了
过滤一下没用的信息
得到 flag{717f7532}
Sniff Two
这个屏和树莓派之间的关系应该是树莓派上装个上位机软件,就能控制这个屏显示图像,然后搜了搜这个屏的引脚定义如下图,估计要用剩下的 SPI 的数据了(后面看源码也能确定是 SPI)
https://pinout.vvzero.com/pinout/inky_phat
但是正常使用应该是要盖在树莓派上的,因此引脚定义和实际接线图是对着的,要自己理解一下这个关系
因此按照接线图来看一下
绿色是 BUSY 对应通道 2
黄色是 Reset 对应通道 3
橙色是 Data/Command 对应通道 4
红色是 MOSI 对应通道 5
棕色是 SCLK 对应通道 6
蓝色是 Chip Select 对应通道 7
然而打开逻辑分析仪我傻眼了,这名字不一样啊,MOSI 和 SCLK 肯定一一对应,Chip Select 是片选,可以对应到 Enable 上
一个合理的解释:对于 LCD 控制器来说,MOSI 引脚用于接收数据和命令,并且 D/C 由主机设置低电平时表示写入命令;高电平时表示写入数据/参数
(https://forums.adafruit.com/viewtopic.php?t=51949)
因此可以直接将 D/C 引脚设置为 MISO,这样解析为 0x00 就表示是低电平写入命令,解析为 0xFF 就表示高电平写入数据
这样就能从逻辑分析仪里面解析出来 SPI 发了啥,但是怎么从发出去的数据中恢复图像,还得分析源码... 这是它的源码:
https://github.com/pimoroni/inky/blob/main/inky/inky.py
set_image 函数用来拷贝要显示的图像到 buffer 中;show 函数根据 buf 进行拆分,拆分成黑色的 buf_a 和红色的 buf_b;最后调用 _update 函数在屏幕上显示像素,里面有个 for 循环,0x24 时表示发送的是 buf_a,0x26 时发送的是 buf_b
在逻辑分析仪里面搜索一下,发现有两个 0x24 和 0x26,可能是传输了两张图片,正好 Data/Command 也有两段不同的波动
放大细节看一下,还是可以很好的区分 cmd 和 data 的
接下来就是对这些数据进行处理了,可以用逻辑分析仪将这段导出为 csv
然后根据 0x24 和 0x26 分割处理一下数据,分别保存到 buf_a 和 buf_b 中,再根据他源码中的 set_image 和 show 函数来往回倒腾一下数据,先看看源码里都做了啥
show 函数把黑色像素点设置为0其余为1,红色像素点设置为1其余为0,然后将 bit 打包成NumPy 数组,转成 Python 列表
buf_a = numpy.packbits(numpy.where(region == BLACK, 0, 1)).tolist()
buf_b = numpy.packbits(numpy.where(region == RED, 1, 0)).tolist()
对应的,我们已经有 buf_a 和 buf_b 的列表了,只需要用 unpackbits 把它先还原回 NumPy 数组
buf_a_unpacked = np.unpackbits(np.array(buf_a, dtype=np.uint8))
buf_b_unpacked = np.unpackbits(np.array(buf_b, dtype=np.uint8))
set_image 函数是根据图像的大小存到了 numpy 的数组中,因此接下来需要考虑一个对于图像来说比较重要的参数:长和宽,根据源码中的定义,设置这些长宽时的 command 分别为 0x44 和 0x45
因此在逻辑分析仪中找到这些值,宽就是 (0x10+1) * 8 = 136,计算高的时候要注意小端序计数,因此高为 0x00F9 = 249
使用 reshape 函数对长宽进行调整
width = 136
height = 249
buf_a = buf_a_unpacked[:height * width].reshape((height, width))
buf_b = buf_b_unpacked[:height * width].reshape((height, width))
最后再根据颜色往一个新的 buf 里面填充数值,最后创建一个图像数组根据 buf 将对应的坐标点改为不同的颜色
color_mapping = {
0:(255,255,255), # 白色
1:(0,0,0), # 黑色
2:(255,0,0) # 红色
}
buf = np.zeros((height,width), dtype=np.uint8)
for i in range(height):
for j in range(width):
if buf_a[i,j] == 0: # 黑色
buf[i,j] = 1
elif buf_b[i,j] == 1: # 红色
buf[i,j] = 2
else:
buf[i,j] = 0 # 白色
image_array = np.zeros((height, width, 3), dtype=np.uint8)
for y in range(height):
for x in range(width):
image_array[y,x] = color_mapping[buf[y,x]] # 修改对应位置的颜色
image = Image.fromarray(image_array,'RGB')
image.show()
最终可以打印出来两张图片,得到 flag{ec9cf2b7}
参考
https://mwlik.github.io/2024-08-05-crewctf-2024-sniff-challenge/
https://xz.aliyun.com/t/15357
完整脚本:
import csv
import time
import numpy as np
from PIL import Image
def display(buf_a, buf_b):
width = 136
height = 249
color_mapping = {
0:(255,255,255), # 白色
1:(0,0,0), # 黑色
2:(255,0,0) # 红色
}
buf_a_unpacked = np.unpackbits(np.array(buf_a, dtype=np.uint8))
buf_b_unpacked = np.unpackbits(np.array(buf_b, dtype=np.uint8))
buf_a = buf_a_unpacked[:height*width].reshape((height,width))
buf_b = buf_b_unpacked[:height*width].reshape((height,width))
buf = np.zeros((height,width), dtype=np.uint8)
for i in range(height):
for j in range(width):
if buf_a[i,j] == 0:
buf[i,j] = 1
elif buf_b[i,j] == 1:
buf[i,j] = 2
else:
buf[i,j] = 0
image_array = np.zeros((height, width, 3), dtype=np.uint8)
for y in range(height):
for x in range(width):
image_array[y,x] = color_mapping[buf[y,x]]
image = Image.fromarray(image_array,'RGB')
image.show()
with open("./capture.csv") as file:
buf_a = []
buf_b = []
in_packet = False
in_buf_a = False
in_buf_b = False
my_table = csv.reader(file)
header = next(my_table) # 跳过第一行的标题
for line in my_table:
MOSI = int(line[2],16)
D_C = int(line[3],16)
if D_C == 0x00: # 如果是command
if MOSI == 0x01: # command 是 0x01 的话表示是数据包
in_packet = True # 表示这是一张图片
elif MOSI == 0x20: # command 是 0x20 的话表示是图片传输完了
in_packet = False
display(buf_a, buf_b) # 展示图片
buf_a = []
buf_b = []
elif MOSI == 0x24: # 如果是 0x24 的话表示的 buf_a 的内容
in_buf_a = True
elif MOSI == 0x26: # 如果是 0x24 的话表示的 buf_b 的内容
in_buf_b = True
in_buf_a = False
elif D_C == 0Xff: # 如果是 data
if in_packet: # 先判断是不是数据包
if in_buf_a: # 是 buf_a 的内容的话就往 buf_a 里面添加
buf_a.append(MOSI)
elif in_buf_b: # 否则是 buf_b 的内容的话就往 buf_b 里面添加
buf_b.append(MOSI)