CrewCTF2024 WP(Partial)
1926172928836009 发表于 广东 CTF 583浏览 · 2024-08-20 16:25

又是酣畅淋漓奋斗(摸鱼)的国际赛,这个比赛感觉这几题比较有意思,记录复现学习一下

Unfare

题目描述如下

Someone was reading cards on my computer! Lucky I was listening...

The flag is in this format: crew{[a-z0-9!_]+} .

Author : sealldev

附件是一个 pcapng 流量,然后对流量的数据解密发现是 Proxmark3 的流量(看着是经典 M1 卡),和 d3f4ult 师傅讨论了很久但是可惜思路都不大对,后面找老外问了下思路

于是就搜索 MIFARE Classic,发现新的文档如下
https://github.com/RfidResearchGroup/proxmark3/blob/master/doc/new_frame_format.md

数据包的结构组成如下

commands being sent to the Proxmark3:
[magic] [length+ng] [bool ng:1] [cmd] [data] [crc]
PM3a                                         crc14a/a3

responses from the Proxmark3:
[magic] [length+ng] [bool ng:1] [status] [cmd] [data] [crc]
PM3b                                                  crc14a/a3

一开始的想法是把发送的数据和返回的数据进行解密但是发现这是不行的,难绷的是后面问完发现其实这题其实很猜谜,一些解法是
1.找到在改变那个字节转换成整型然后对数据排序(需要找到的数据不被分割,这题是找首字节)
2.猜对所有字节偏移量(看来国外取证高手也是多啊)
那我写写第三种,也是我复现的方法,其实读取卡的过程(包括数据的发送和响应)就是以随机顺序读取具有固定键的块(脚本会贴在下面)

这里大家先简单的了解一下存储结构:
1.M1 卡分为 16 个扇区,每个扇区由 4 块(块0,块1,块2,块3)组成,我们也将 16 个扇区的 64 个块按绝对地址编号为 0~63
2.第 0 扇区的块 0(即绝对地址 0 块),它用于存放厂商代码,已经固化,不可更改。
3.每个扇区的块 0、块 1、块 2 为数据块,可用于存贮数据。
数据块可作两种应用:
(1)用作一般的数据保存,可以进行读、写操作。
(2)用作数据值,可以进行初始化值、加值、减值、读值操作。
4.每个扇区的块 3 为控制块,包括了密码 A、存取控制、密码 B

于是最后的思路就是我们先把传输的数据提取出来转换为字节格式,然后根据文档中的代码进行解析并识别特定命令,按照 blocknokeytypekey 的顺序从请求数据中提取相应字段,并对响应数据进行处理和存储并打印每个数据块的首字节
https://github.com/RfidResearchGroup/proxmark3/blob/ee8b9ca74b10536db246286179183e0f1d89770e/include/pm3_cmd.h#L289
重要的代码部分如下

typedef struct {
    uint8_t blockno;//要读取的具体块
    uint8_t keytype;//密钥类型
    uint8_t key[6];//6字节的密钥
} PACKED mf_readblock_t;

提取数据直接用 tshark 就可以

tshark -r traffic.pcapng -T fields -e usb.capdata

解密的脚本如下

from struct import unpack

data = """
504d336120800901000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f6133
504d3362208000000901000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f6233
504d3361008012016133
504d33620d80000012010600000000f09c0000d2ff1fe86233
504d3361008007016133
504d3362fa8100000701400a0b27a3e70400ee010000205b201b5b33336d41524d1b5b306d205d0a2020626f6f74726f6d3a204963656d616e2f6d61737465722f76342e31373134302d3133392d673532363633323139382d64697274792d7375737065637420323032332d31302d31372031393a32323a3336203864326539376263310a202020202020206f733a204963656d616e2f6d61737465722f76342e31373134302d3133392d673532363633323139382d64697274792d7375737065637420323032332d31302d31372031393a32323a3439203864326539376263310a2020636f6d70696c65642077697468204743432031302e332e31203230323130383234202872656c65617365290a0a205b201b5b33336d465047411b5b306d205d200a2020667067615f706d335f6c662e6e636420696d6167652032733330767131303020323032332d30382d32392031363a34343a30370a2020667067615f706d335f68662e6e636420696d6167652032733330767131303020323032332d30382d32392031363a34343a31390a2020667067615f706d335f66656c6963612e6e636420696d6167652032733330767131303020323032332d30382d32392031363a34343a34330a2020667067615f706d335f68665f31352e6e636420696d6167652032733330767131303020323032332d30382d32392031363a34343a3331006233
504d3361088020060800ffffffffffff6133
504d3362108000002006337688b61c3b5ce5f2091b6d8032d9956233
504d3361088020061100ffffffffffff6133
504d3362108000002006338bddfb4e37559b9aaea49b6a0dafaa6233
504d3361088020063d00ffffffffffff6133
504d33621080000020062107aded393150de1ce3325a13b8abf96233
504d3361088020061900ffffffffffff6133
504d33621080000020065fd92dcf98d59d29487b5dee39995fa16233
504d3361088020060e00ffffffffffff6133
504d336210800000200661c9877232b9e783e430bc2ecbf604616233
504d3361088020062400ffffffffffff6133
504d33621080000020065fadbb39be53fef32f00fa4f924863786233
504d3361088020061000ffffffffffff6133
504d33621080000020066b0d1458ca1abba068d4664163fcf15b6233
504d3361088020063800ffffffffffff6133
504d3362108000002006309c09062bb43e149155ae3392b810556233
504d3361088020062900ffffffffffff6133
504d336210800000200633b0c80585b9abea6e1743c18d14303d6233
504d3361088020060900ffffffffffff6133
504d336210800000200661452ec13b44b7b484eded217e55f2406233
504d3361088020063200ffffffffffff6133
504d3362108000002006336ca7bd28b6b614a8396a173b4304006233
504d3361088020063000ffffffffffff6133
504d336210800000200633344e2317595123e4b8d93914fce0a86233
504d3361088020063500ffffffffffff6133
504d33621080000020066223f2995a6f92e6b44b17262662568c6233
504d3361088020062e00ffffffffffff6133
504d33621080000020066800806fbc568fa6369a1defea517ac96233
504d3361088020060d00ffffffffffff6133
504d33621080000020066d86fa8014aec637ee3d7ad0975af16e6233
504d3361088020063a00ffffffffffff6133
504d33621080000020066bde66d8b65fc6e2dbfcd9e1d197e1a16233
504d3361088020062d00ffffffffffff6133
504d33621080000020067426ebbe0ba633697dd4e64fe9d751c76233
504d3361088020060200ffffffffffff6133
504d336210800000200665e86000c44c11b2c374155e60e49dac6233
504d3361088020062800ffffffffffff6133
504d336210800000200676dc21177f0d9a91594cc627467ca3e96233
504d3361088020061c00ffffffffffff6133
504d33621080000020063098ba30d2b5cc31dbf757f917fd577e6233
504d3361088020062c00ffffffffffff6133
504d33621080000020065fda2fcecf43661edfa58b7a8af34dac6233
504d3361088020062600ffffffffffff6133
504d3362108000002006300db215b54333058397fcf55863b1be6233
504d3361088020063e00ffffffffffff6133
504d33621080000020067d8e7982f641919e39cef1cc357773726233
504d3361088020062500ffffffffffff6133
504d336210800000200663445e9fb246648a21b2e54d4cb0c22d6233
504d3361088020061d00ffffffffffff6133
504d33621080000020066e6bf5bf7f79fe2c49089dabd1d8c23c6233
504d3361088020063400ffffffffffff6133
504d33621080000020065fcf5431b327c0ee0bed0ade1e1c353f6233
504d3361088020062a00ffffffffffff6133
504d336210800000200672907eae7510d8a6b8053aa843a6a70e6233
504d3361088020060000ffffffffffff6133
504d336210800000200663de34ee6708040002b21e2423271e1d6233
504d3361088020063c00ffffffffffff6133
504d33621080000020067371ba89b526281359e5e86dcefc5bb96233
504d3361088020061800ffffffffffff6133
504d33621080000020066705867aec3f3743ca1672d0850dff8b6233
504d3361088020060100ffffffffffff6133
504d3362108000002006720ed035962d2203b06bff6a4b79b8f36233
504d3361088020063600ffffffffffff6133
504d33621080000020066c613dc76ce6b225e48035a521bd2b776233
504d3361088020062200ffffffffffff6133
504d3362108000002006307c0012ca0c1babd0e2e8eaf7af2df36233
504d3361088020060600ffffffffffff6133
504d336210800000200673be1e531874b4419db2c26b5e1766906233
504d3361088020060400ffffffffffff6133
504d336210800000200677a1215c3bb5ef8c5270a10b94c681ba6233
504d3361088020063900ffffffffffff6133
504d336210800000200663776fff6a190f37b4c713915c83c3516233
504d3361088020061600ffffffffffff6133
504d336210800000200661ba52c76c145fe2591c120136e5a32b6233
504d3361088020061400ffffffffffff6133
504d3362108000002006665536c1ddb8536a420724d0551b8fb56233
504d3361088020062100ffffffffffff6133
504d3362108000002006740c759833093fc8ed15adffd8d31bc86233
504d3361088020060a00ffffffffffff6133
504d33621080000020066c5ae3fc47a105af8bb753ce72b5a6996233
504d3361088020061500ffffffffffff6133
504d33621080000020066c350643893fa9b3ab89125711ad2d6f6233
504d3361088020060c00ffffffffffff6133
504d33621080000020065f072d8eedef594206b77515846390a46233
504d3361088020063100ffffffffffff6133
504d3362108000002006738dfd0c15bd36f37f333cd5428039116233
504d3361088020061e00ffffffffffff6133
504d3362108000002006676d2b2f6de30795afc423c5fa5013d46233
504d3361088020061a00ffffffffffff6133
504d33621080000020066c1d25b571a7fbe75e14992cebd243436233
504d3361088020062000ffffffffffff6133
504d33621080000020065fa8aa559b800204129bad721b8e977a6233
504d3361088020061200ffffffffffff6133
504d33621080000020065ffc75e4ca64dddcba0f16ce6d2ea3d76233
504d3361088020060500ffffffffffff6133
504d33621080000020067b2d8b2170503fe263721a8ca2f380286233
""".strip()

out = [b'\x00' * 16] * 64
lines = data.splitlines()
txs = [(lines[i], lines[i + 1]) for i in range(0, len(lines), 2)]
for req, res in txs:
    req = bytes.fromhex(req)
    res = bytes.fromhex(res)

    magic = req[:4]
    ln = unpack('H', req[4:6])[0] & 0x7fff
    cmd = unpack('H', req[6:8])[0]
    cmd_data = req[8:]
    if cmd == 0x112:
        print('Requested capabilities')
    elif cmd == 0x107:
        print('Requested version')
    elif cmd == 0x109:
        print('Ping')
    elif cmd == 0x620:
        blockno, keytype, key = cmd_data[0], cmd_data[1], cmd_data[2:-2]
        print('MIFARE RDBL', blockno)
        # print(blockno, keytype, key)
        out[blockno] = res[10:-2]
    else:
        print(hex(cmd), ln, len(cmd_data) - 2, cmd_data)
with open('card.bin', 'wb') as f:
    f.write(b''.join(out))
print(b''.join(a[0:1] for a in out).replace(b'\x00', b''))

crew{s3al_mak3_flag_l0ng_t0_c0v3r_th3s3_bl0cks!}

最后给大家贴个链接是关于 M1 卡的 AES 认证机制的 CTF 题(得知道 M1 卡的加密原理),,也挺有意思的可以去写看看(DCTF2022)
https://writeup.enj.oye.rs/posts/dctf_2022/securecard/
https://cloud.tencent.com/developer/article/2309777

Sniff one && Sniff two

典型的 hardware 开局,之前没学过这次正好看看,Sniff one 题目描述如下

A two part challenge.

See the README in the download for details.

Author : Oshawk

The first (easier) is the password that was typed on the keyboard.

下面那题和这题是一样的附件,附件里面有一个 README.pdf 和一个 capture.sal 文件,设备图片如下

This challenge has two flags in the flag{} format:
  - The first (easier) is the password that was typed on the keyboard.
  - The second (significantly harder) is what was displayed to the screen after the password was entered.

看到题目描述后发现是 Sniff one 的 flag 是找到键盘输入的内容,通过查找所使用的键盘之后,发现了这篇文章,并且发现了发现通信方法是 I2C 和 I2C 的地址是 0x5F,一些链接如下
https://theembeddedsystem.readthedocs.io/en/latest/c5_ec_i2c_if/i2c.html
https://www.youtube.com/watch?v=CAvawEcxoPU
https://www.youtube.com/watch?v=XGxE4FJH5kI
I2C 是一种两线式串行总线,用于连接微控制器及其外围设备,它是半双工通信方式(输入输出数据均使用一根线),通常有两个通道 SDA (负责传输数据的一个), SCL (同步时钟的时钟)
然后 sal 文件可以用 Saleae Logic Analyzers 进行分析,seleae 可以解析 I2C、SPI、MDIO、SMBus 协议,通过图形或者数据统计化的方式展示采集的信号
首先我们先用 seleae 打开 sal 文件

然后我们可以根据信号波形来区分 SCL 和 SDA(也可以查看接线图片)
SCL(时钟线):时钟信号通常是一个连续的方波,代表时钟的节拍。时钟信号会一直在高电平和低电平之间切换,频率恒定。
SDA(数据线):数据线的波形会根据传输的数据内容而变化。当时钟线处于高电平时,数据线可以改变电平;而当时钟线处于低电平时,数据线的电平应该保持稳定。这是 I2C 协议的特点。

所以 D0 和 D1 分别是 SDA 和 SCL,那我们选择 I2C 的分析器并把数据导出为 csv

现在该解码这些信号,搜索后发现有一个库 https://github.com/ian-antking/cardkb
也可以直接写脚本解答(感谢 luoingly 师傅的脚本),这里的数据有一堆 0,但这可能是键盘报告"此时没有按键,请继续等待",于是脚本里记得过滤掉 0

source = './keyboard.csv'

import pandas as pd
import numpy as np

raw = pd.read_csv(source)

reading = False
address = None
data = []
key_data = []

for i, row in raw.iterrows():
    if row['type'] == 'start':
        reading = True
        address = None
        data = []
    if row['type'] == 'address':
        address = row['address']
    if row['type'] == 'data':
        data.append(row['data'])
    if row['type'] == 'stop':
        reading = False
        if address == '0x5F':
            key_data.append(int(data[0][2:], 16))

print("".join([chr(c) for c in key_data if c > 0x01])) 

flag{717f7532}

Sniff two 题目描述如下

A two part challenge.

See the README in the download for details. ( same attachment as sniff one )

Author : Oshawk

The second (significantly harder) is what was displayed to the screen after the password was entered.

看得出 flag2 和屏幕显示的有关,通过看设备图看到是一个 Pimoroni Inky pHAT,首先第一步肯定是要先理线,可以看这个文档,然后 github 也有一个专属的仓库,后面也会用到里面的脚本,
首先根据图片能很轻易的分别出哪个引脚是哪个接口

CS   -> blue   -> G7 //片选信号
CLK  -> brown  -> G6 //同步数据传输
DIN  -> red    -> G5 //数据输入信号
DC   -> orange -> G4 //(Data/Command)数据/命令控制信号
RST  -> yellow -> G3 //复位信号
BUSY -> pastel green -> G2 //表示设备当前是否忙碌

时序:CPHL=0,CPOL=0(即 SPI 模式0)
然后我们可以在库里找到这个 py 文件,查看里面的代码部分中有设置,传输和分割图片三个函数,将发送的信号与库代码进行比较之后可以了解它实际上在做什么
设置图像
代码链接,查看下面的函数

def set_image(self, image):
        """Copy an image to the buffer.

        The dimensions of `image` should match the dimensions of the display being used.

        :param image: Image to copy.
        :type image: :class:`PIL.Image.Image` or :class:`numpy.ndarray` or list
        """
        if self.rotation % 180 == 0:
            self.buf = numpy.array(image, dtype=numpy.uint8).reshape((self.width, self.height))
        else:
            self.buf = numpy.array(image, dtype=numpy.uint8).reshape((self.height, self.width))

这里是负责设置表示图像的 buf 变量
分割图像
代码链接,查看下面的函数

def show(self, busy_wait=True):
        """Show buffer on display.

        :param bool busy_wait: If True, wait for display update to finish before returning, default: `True`.
        """
        region = self.buf

        if self.v_flip:
            region = numpy.fliplr(region)

        if self.h_flip:
            region = numpy.flipud(region)

        if self.rotation:
            region = numpy.rot90(region, self.rotation // 90)

        buf_a = numpy.packbits(numpy.where(region == BLACK, 0, 1)).tolist()
        buf_b = numpy.packbits(numpy.where(region == RED, 1, 0)).tolist()

        self._update(buf_a, buf_b, busy_wait=busy_wait)

这里将图像(buf 数组)分割为代表图像黑色部分的 buf_a 和代表图像红色/黄色等部分的 buf_b
发送图像
代码链接,查看下面的函数

def _update(self, buf_a, buf_b, busy_wait=True):
        """Update display.

        :param buf_a: Black/White pixels
        :param buf_b: Yellow/Red pixels

        """
        self.setup()

        packed_height = list(struct.pack('<H', self.rows))

        if isinstance(packed_height[0], str):
            packed_height = map(ord, packed_height)

        self._send_command(0x74, 0x54)  # Set Analog Block Control
        self._send_command(0x7e, 0x3b)  # Set Digital Block Control

        self._send_command(0x01, packed_height + [0x00])  # Gate setting

        self._send_command(0x03, 0x17)  # Gate Driving Voltage
        self._send_command(0x04, [0x41, 0xAC, 0x32])  # Source Driving Voltage

        self._send_command(0x3a, 0x07)  # Dummy line period
        self._send_command(0x3b, 0x04)  # Gate line width
        self._send_command(0x11, 0x03)  # Data entry mode setting 0x03 = X/Y increment

        self._send_command(0x2c, 0x3c)  # VCOM Register, 0x3c = -1.5v?

        self._send_command(0x3c, 0b00000000)
        if self.border_colour == self.BLACK:
            self._send_command(0x3c, 0b00000000)  # GS Transition Define A + VSS + LUT0
        elif self.border_colour == self.RED and self.colour == 'red':
            self._send_command(0x3c, 0b01110011)  # Fix Level Define A + VSH2 + LUT3
        elif self.border_colour == self.YELLOW and self.colour == 'yellow':
            self._send_command(0x3c, 0b00110011)  # GS Transition Define A + VSH2 + LUT3
        elif self.border_colour == self.WHITE:
            self._send_command(0x3c, 0b00110001)  # GS Transition Define A + VSH2 + LUT1

        if self.colour == 'yellow':
            self._send_command(0x04, [0x07, 0xAC, 0x32])  # Set voltage of VSH and VSL
        if self.colour == 'red' and self.resolution == (400, 300):
            self._send_command(0x04, [0x30, 0xAC, 0x22])

        self._send_command(0x32, self._luts[self.lut])  # Set LUTs

        self._send_command(0x44, [0x00, (self.cols // 8) - 1])  # Set RAM X Start/End
        self._send_command(0x45, [0x00, 0x00] + packed_height)  # Set RAM Y Start/End

        # 0x24 == RAM B/W, 0x26 == RAM Red/Yellow/etc
        for data in ((0x24, buf_a), (0x26, buf_b)):
            cmd, buf = data
            self._send_command(0x4e, 0x00)  # Set RAM X Pointer Start
            self._send_command(0x4f, [0x00, 0x00])  # Set RAM Y Pointer Start
            self._send_command(cmd, buf)

        self._send_command(0x22, 0xC7)  # Display Update Sequence
        self._send_command(0x20)  # Trigger Display Update
        time.sleep(0.05)

        if busy_wait:
            self._busy_wait()
            self._send_command(0x10, 0x01)  # Enter Deep Sleep

这里是两个缓冲区(代表整个图像)发送到设备

其实我们可以在 channel4区分两张图片何时写入屏幕

于是来分析波形和数据,前面提到了用 SPI 模式0(CPOL 和 CPHA 为 0) 进行分析

在 SPI 协议上通道的选择如下

MOSI(Master Out Slave In):DIN G5 主设备输出,从设备输入
MISO(Master In Slave Out):G4 主设备输入、从设备输出,选择额外的通道观察其变化(可留空)
Clock(CLK):G6 时钟信号
Enable(CS/Chip Select):G7 选择哪个从设备参与通信
Bit order:MSB First(默认大端优先)
CPOL和CPHA为 0,即 SPI 模式0

这里我们可以看到它正在发送高度 0x00F9 + 0x00

数据 0xF90x00 是一组 16 位(2 个字节)的数据,高位在前,低位在后,SPI 传输中的数据顺序是小端序,所以是 0xF9 发送在前,然后这部分操作对应函数来说是最重要的,因为它将 buf_a , buf_b 缓冲区(我们的原始图像)发送到设备

# 0x24 == RAM B/W, 0x26 == RAM Red/Yellow/etc
for data in ((0x24, buf_a), (0x26, buf_b)):
    cmd, buf = data
    self._send_command(0x4e, 0x00)  # Set RAM X Pointer Start
    self._send_command(0x4f, [0x00, 0x00])  # Set RAM Y Pointer Start
    self._send_command(cmd, buf)

发送 buf_a

发送 buf_b (就是一张图片的发送开始到结束发送后的就是 buf_b)
那于是脚本如下 https://github.com/mwlik/inky-phat-signals-to-frames

with open("./inky_spi.csv") as file:
    reader = csv.reader(file)
    header = next(reader)
    in_packet = False
    in_b_w = False
    in_y = False
    b_w = []
    y = []
    for row in reader:
        record = [int(row[2], 16), int(row[3], 16)]
        is_command = record[1] == 0x00
        is_data = not is_command
        if is_command:
            command = record[0]
            if command == 0x01:
                in_packet = True
            elif command == 0x20:
                in_packet = False
                display(b_w, y)
                b_w = []
                y = []
            elif command == 0x24:
                in_b_w = True
            elif command == 0x26:
                in_y = True
                in_b_w = False
        elif is_data:
            if in_packet:
                data = record[0]
                if in_b_w:
                    b_w.append(data)
                elif in_y:
                    y.append(data)

这部分仅用于获取 buf_a , buf_b 数组,将它们发送到执行实际工作的 display 函数

def display(buf_a, buf_b):
    BLACK = 1
    RED = 2

    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))

    width = 136
    height = 250

    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] = BLACK
            elif buf_b[i, j] == 1:
                buf[i, j] = RED
            else:
                buf[i, j] = 0

    height, width = buf.shape
    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 的 e 是会看不到的),最后得到的 flag 如下(传输了两张图不用急着看到第一张不是 flag 就关了)

flag{ec9cf27b}

参考链接:
https://mwlik.github.io/2024-08-05-crewctf-2024-sniff-challenge/
https://discord.gg/SnCgFWdv (CrewCTF 的 DC 邀请链接)

附件:
0 条评论
某人
表情
可输入 255