Featured image of post ESP32 IoT CTF 清华校赛版wp

ESP32 IoT CTF 清华校赛版wp

学习iot安全的入门题目

环境搭建

  • 需要准备 ESP32、杜邦线、USB 转 TTL 模块、支持监听模式的无线网卡

  • 克隆源码

1
git clone https://github.com/xuanxuanblingbling/esp32ctf_thu.git

安装好后打开ESP-IDF v4.3 PowerShell

1
2
cd esp32ctf_thu/thuctf/
idf.py menuconfig 
  • 设置
    • Serial flasher config ---> Flash size (4 MB)
    • Partition Table ---> Partition Table (Custom partition table CSV)

  • 编译,烧录,监听串口输出
1
2
3
idf.py build 
idf.py flash 
idf.py monitor
  • esp_no_flag_source目录中是删掉flag的源码,可以用来分析,但不能烧录

  • 上电前连接GND与23号引脚进入硬件、网络、蓝牙题目,否则默认进入MQTT题目,已供电时可按 EN 键重启使设置生效。

硬件

task1

将GPIO18抬高,持续3s即可获得flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define GPIO_INPUT_IO_0     18
void hardware_task1(){
    int hit = 0;
    while(1) {
        printf("[+] hardware task I : hit %d\n",hit);
        if(gpio_get_level(GPIO_INPUT_IO_0)){ //判断gpio18是否为高电平
            hit ++ ;
        }else{
            hit = 0;
        }
        if(hit>3){
            printf("[+] hardware task I : %s\n",hardware_flag_1);
            break;
        }
        vTaskDelay(1000 / portTICK_RATE_MS); //这个任务每秒检测一次 GPIO 状态
    }
}
  • 题目意思是GPIO18 引脚的电平保持为高电平(也就是接 3.3V或5v),时间要持续大于 3 秒,程序就会打印出 flag。

GPIO(General Purpose Input/Output)是通用输入输出引脚。可以配置成读取外部电平(输入模式)或控制外设(输出模式)。

ESP32 上的 GPIO 是多功能的,每个引脚都可以自由配置成输入、输出或其他特殊用途。

用杜邦线连接3v3与GPIO18,持续3s即可

此时hit增加,获得flag

THUCTF{Ev3ryth1ng_st4rt_fr0m_GPIO_!!!}

task2

在GPIO18处构造出1w个上升沿

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#define GPIO_INPUT_IO_0     18
#define GPIO_INPUT_PIN_SEL  ((1ULL<<GPIO_INPUT_IO_0) )

static void IRAM_ATTR gpio_isr_handler(void* arg){
    trigger++; // 每次触发中断时,trigger增加
}

void hardware_gpio_setup(){
    gpio_config_t io_conf;
    io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL; // 绑定 GPIO18
    io_conf.mode = GPIO_MODE_INPUT; // 设置为输入模式
    io_conf.intr_type = GPIO_INTR_POSEDGE; // 上升沿中断
    io_conf.pull_up_en = 0; // 关闭上拉,初始状态是低电平
    gpio_config(&io_conf);
    gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT); // 开启中断服务
    gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0); // 注册中断处理函数
}

void hardware_task2(){
    trigger = 0;
    while(1){
        printf("[+] hardware task II : trigger %d\n",trigger);
        if(trigger > 10000){ // 触发次数超过10000,输出flag
            printf("[+] hardware task II : %s\n",hardware_flag_2);
            break;
        }
        vTaskDelay(1000 / portTICK_RATE_MS);
    }
}
  • 题目意思是要让 GPIO18 不断从低电平跳到高电平(上升沿),当次数大于 10000,程序就会打印出 flag

上升沿是电平从 0 变成 1 的瞬间。

ESP32 默认的 TX 引脚是持续发送数据的,它的电平会反复在高低之间切换。

用杜邦线连接TX引脚与GPIO18,直到trigger>10000

THUCTF{AuT0++_is_th3_r1ght_w4y_hhhhhh}

  • 本质上任何能控制输出高低电平切换的引脚都可以用,甚至可以手动连接并断开GPIO18和其他引脚10000次(除了GND),还可以写一个任务让GPIO不断切换输出电平

task3

在另一个串口处寻找第三个flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define ECHO_TEST_TXD  (GPIO_NUM_4) // UART1 的 TX(发送)引脚是 GPIO4
#define ECHO_TEST_RXD  (GPIO_NUM_5) // UART1 的 RX(接收)引脚是 GPIO5

void hardware_task3(){
    printf("[+] hardware task III : find the third flag in another UART\n");
    while (1) {
        // 向 UART1 发送一串字符串(hardware_flag_3),这个串口不会在 log 输出看到
        uart_write_bytes(UART_NUM_1, hardware_flag_3, strlen(hardware_flag_3)); // 不断往 UART1 输出 flag
        vTaskDelay(1000 / portTICK_RATE_MS);
    }
}
  • 题目意思是要监听 UART1 的串口输出,即可直接读取 flag

UART(通用异步收发器)是串口通信的一种协议,本质就是用两根线(TX 和 RX)来做点对点的串行通信。ESP32 上有多个 UART 通道。默认我们用的是 UART0,它用于 log 输出。但这里题目用的是 UART1。

将USB 转 TTL 模块的RX引脚连接ESP的GPIO4引脚,gnd也对应连接

用串口工具监听USB 转 TTL 模块

THUCTF{UART_15_v3ry_imp0r7ant_1n_i0T}

网络

task1

连接板子目标端口,尝试获得flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void network_init(){
    char ssid[0x10] = {0};
    char pass[0x10] = {0};
    get_random(ssid,6); //随机生成WiFi名称
    get_random(pass,8); //随机生成密码
    printf("[+] network task I: I will connect a wifi -> ssid: %s , password %s \n",ssid,pass);
    connect_wifi(ssid,pass); //尝试连接到刚刚生成的WiFi
}

static void network_tcp()
{
    
    char addr_str[128];
    struct sockaddr_in dest_addr;

    dest_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 接收任意IP
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = htons(3333); // 监听3333端口

    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP); // 创建 TCP socket

    ESP_LOGI(TAG, "Socket created");

    bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); // 绑定 socket
    ESP_LOGI(TAG, "Socket bound, port %d", 3333);

    listen(listen_sock, 1); // 开始监听
    while (1) {

        ESP_LOGI(TAG, "Socket listening");
        struct sockaddr_storage source_addr;
        socklen_t addr_len = sizeof(source_addr);
        int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len); // 等待连接
        inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
        ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str);
        char buffer[100];
        while(recv(sock,buffer,0x10,0)){ // 接收数据
            if(strstr(buffer,"getflag")){ //如果内容包含getflag
                send(sock, network_flag_1, strlen(network_flag_1), 0); // 返回flag
                break;
            }else{
                send(sock, "error\n", strlen("error\n"), 0);
            }
            vTaskDelay(1000 / portTICK_RATE_MS);
        }
        open_next_tasks = 1;
        shutdown(sock, 0);
        close(sock);
    }
}
  • 题目意思是ESP32 板子会随机生成一个WiFi 名(SSID)和密码并尝试连接。连上之后,它会在本地开启一个 TCP Server,监听端口 3333。需要在网络中找到并连接它,发送 getflag,它就会返回 flag。

STA 模式(Station):ESP32 作为 WiFi 客户端接入热点。

Socket 是一对网络通信端点,由 IP 地址 + 端口号组成。通过 socket 函数可以建立 TCP 或 UDP 连接,发送和接收数据。

ESP32 成功连接热点后,就成了局域网的一员,它的 3333 端口会接受来自其他设备的 TCP 连接。

netcat作用是快速建立 TCP 或 UDP 连接,用来读写数据。

修改电脑的热点名称和密码与串口打印的相同

esp32连接成功,获得ip地址192.168.137.188

nc一下,输入nc 192.168.137.188 3333

getflag

THUCTF{M4k3_A_w1rele55_h0t5p0ts}

task2

你知道他发给百度的flag么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void network_http()
{
    char  fmt[]  = "GET / HTTP/1.0\r\n"
                        "Host: www.baidu.com:80\r\n"
                        "User-Agent: esp-idf/1.0 esp32\r\n"
                        "flag: %s\r\n"
                        "\r\n";
    
    char request[200];
    sprintf(request,fmt,network_flag_2); // 构造带有 flag 的 HTTP 请求头

    const struct addrinfo hints = {
        .ai_family = AF_INET,
        .ai_socktype = SOCK_STREAM,
    };
    struct addrinfo *res;
    struct in_addr *addr;
    int s;

    while(1) {
        if(open_next_tasks){
            printf("[+] network task II : send the second flag to baidu\n");
            getaddrinfo("www.baidu.com", "80", &hints, &res); // DNS 解析
            addr = &((struct sockaddr_in *)res->ai_addr)->sin_addr;
            ESP_LOGI("network", "DNS lookup succeeded. IP=%s", inet_ntoa(*addr));
            s = socket(res->ai_family, res->ai_socktype, 0);
            connect(s, res->ai_addr, res->ai_addrlen); // 建立 TCP 连接
            freeaddrinfo(res);
            write(s, request, strlen(request)); // 发送 HTTP 请求
            close(s);
        }
        vTaskDelay(10000 / portTICK_PERIOD_MS);
    }
}
  • 题目意思是板子把 network_flag_2 写在 HTTP 请求头里,然后发给百度的 80 端口,要拦截 ESP32 向百度服务器发送的 HTTP 请求,提取其中携带的 flag。

ESP32 用 socket 构造 HTTP 报文并发送,请求并不返回给本地终端,需要抓包分析出站流量。板子的网络行为都会从电脑出去,所以可以抓到 HTTP 请求

电脑开启热点后,系统会新建一个虚拟网卡

用wireshark抓取这个网卡的流量,追踪http流,发现flag

THUCTF{Sn1ffer_N3tw0rk_TrAffic_In_7h4_Main_r0aD}

task3

flag在空中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void network_wifi()
{	
    // 定义一段固定的原始802.11数据包(PDU),是Wi-Fi协议的数据帧格式
    static const char ds2ds_pdu[] = {
    0x48, 0x03, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0xE8, 0x65, 0xD4, 0xCB, 0x74, 0x19, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
    0x60, 0x94, 0xE8, 0x65, 0xD4, 0xCB, 0x74, 0x1C, 0x26, 0xB9,
    0x0D, 0x02, 0x7D, 0x13, 0x00, 0x00, 0x01, 0xE8, 0x65, 0xD4, 0xCB, 0x74,
    0x1C, 0x00, 0x00, 0x26, 0xB9, 0x00, 0x00, 0x00, 0x00,
    };  

    char pdu[200]={0};
    // 在固定数据包后面拼接 flag 内容,形成完整的带 flag 的数据帧
    memcpy(pdu,ds2ds_pdu,sizeof(ds2ds_pdu));
    memcpy(pdu+sizeof(ds2ds_pdu),network_flag_3,sizeof(network_flag_3));

    while(1) {
        if(open_next_tasks){
            printf("[+] network task III : send raw 802.11 package contains the third flag\n");
            esp_wifi_80211_tx(ESP_IF_WIFI_STA, pdu, sizeof(ds2ds_pdu)+sizeof(network_flag_3), true);
            // 通过 esp_wifi_80211_tx 函数,直接发送底层802.11无线数据包(裸包),不经过协议栈
        }
        vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
}

  • 题目意思是ESP32 直接构造并发送了一个原始的802.11数据包,包含了第三个flag的数据。捕获周围无线信号里的所有802.11帧,包括ESP32发出的这段含flag的裸包,然后用Wireshark解析,就能看到里面隐藏的flag。

802.11数据帧(Wi-Fi裸包):无线局域网通信的底层数据单元,包含MAC地址、控制字段、负载等,不同于TCP/IP数据包。这种帧必须用支持监听模式的无线网卡开启监听模式后才能看到。

监听模式无线网卡:捕获所有经过空中的无线帧,包括不发给自己的。普通Wi-Fi连接只能看到TCP/IP层数据,监听模式才能看到MAC层及以上的帧内容。

把无线网卡插入kali,切换网卡到监听模式并开始监听

1
2
sudo airmon-ng start wlan0
sudo airodump-ng wlan0mon

wireshark抓wlan0mon网卡的包,在字节流中用字符串搜索CTF

1
sudo wireshark

THUCTF{YOu_cAn_s3nd_4nd_sNiff3r_802.11_r4w_pAckag3}

蓝牙

task1

修改蓝牙名称并设置可被发现即可获得flag

1
2
3
4
5
6
7
8
void check_name(char * a,char * b){
    if(!strcmp(a,b)){
        printf("bluetooth task I : %s\n",bt_flag_1);
        esp_bt_gap_cancel_discovery();
        scan = 0;
        next_task();
    }
}

字面意思,修改手机蓝牙名称与串口打印的相同,等待连接即可

THUCTF{b1u3t00th_n4me_a1s0_c4n_b3_An_aTT4ck_surfAce}

task2

flag在空中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void next_task(){
    unsigned char fmt[]= {0x06,0x09,0x68,0x65,0x6C,0x6C,0x6F,
                          sizeof(bt_flag_2),0xFD};

    char client_name[10]={};
    get_random(client_name,5);
    printf("[+] bluetooth task II : BLE device name is %s\n",client_name);// 生成随机 BLE 广播设备名
    printf("[+] bluetooth task II : Please find the second flag in the ADV package from this BLE device %s\n",client_name);
    unsigned char data[100];
    memcpy(data,fmt,sizeof(fmt));
    memcpy(data+2,client_name,5);
    memcpy(data+sizeof(fmt),bt_flag_2,sizeof(bt_flag_2));// 将第二个 flag 添加到广播包末尾

 // 配置 BLE 广播的原始数据,直接以二进制形式发送esp_ble_gap_config_adv_data_raw(data,sizeof(fmt)+sizeof(bt_flag_2));
    // 开始广播
    esp_ble_gap_start_advertising(&ble_adv_params);
    esp_ble_gatts_register_callback(gatts_event_handler);
    esp_ble_gatts_app_register(PROFILE_A_APP_ID);
    esp_ble_gatt_set_local_mtu(500);
}

  • 题目意思是ESP32 现在使用低功耗蓝牙广播模式,设备名是ivhti,flag 在 BLE 广播包里,通过监听广播拿到。

BLE 设备会周期性向外发送广播包,内容可以自定义。

nRF Connect可用于扫描,调试低功耗蓝牙设备。

用nRF Connect 抓取广播包,获得一串16进制数据5448554354467B416456443437617D

转成ascii,获得flag

THUCTF{AdVD47a}

task3

分析GATT业务并获得flag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
case ESP_GATTS_WRITE_EVT: { // 当手机等客户端向蓝牙设备写数据时,会触发这个事件
        ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, conn_id %d, trans_id %d, handle %d", param->write.conn_id, param->write.trans_id, param->write.handle);
        if (!param->write.is_prep){
            ESP_LOGI(GATTS_TAG, "GATT_WRITE_EVT, value len %d, value :", param->write.len);
            esp_log_buffer_hex(GATTS_TAG, param->write.value, param->write.len); // 打印写入的数据内容(十六进制)
            printf("[+] bluetooth task III : %s\n",param->write.value); // 打印收到的内容(字符串)
             // 检查写入的数据是否和第二个flag一样
            if(!strncmp(bt_flag_2,(char *)param->write.value,param->write.len)){
                printf("[+] bluetooth task III : you can read the third flag this time\n");
                open_task3 = 1;  // 如果正确,标记允许读第三个flag
            }
        }
        esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
        break;
    }
  • 题目意思是向 ESP32 的 GATT 服务写入flag2,如果写入正确,系统会提示你可以读第三个 flag 了,说明已经通过验证,然后就可以读取隐藏的 flag。

GATT(Generic Attribute Profile)是 BLE通信的一种通信规范,定义了BLE 设备之间如何组织数据和读写数据的规则。

通过手机写入广播获取的 flag,实现对 BLE 特征值的访问控制。

用nRF Connect连接上一题的BLE

Unknown Service>Unknown Characteristic中上传task2的flag

读取获得字符串54-48-55-43-54-46-7B-57-72-49-74-45- 5F-34-5F-67-41-37-54-7D-00

转ascii获得flag

THUCTF{WrItE_4_gA7T}

MQTT

题目的域名到期了,需要构建一个MQTT服务器,docker文件夹里有这题的dokerfile,起一个docker,然后打开服务器1833端口

1
2
docker build -t esp32ctf .
docker run -d -p 1883:1883 esp32ctf

手机热点改成如下名称和密码,电脑和ESP32连接热点

1
2
 // MQTT
        connect_wifi("THUCTFIOT","mqttwifi@123");

main.c这部分改成自己的域名,然后重新编译烧录

1
mqtt_app_start("mqtt://192.168.229.152");

日志看到连接成功,打开mqtt.fx,设置broker为自己的ip和端口号并连接

task1

你知道MQTT的上帝是谁么

1
2
3
4
5
6
 switch (event->event_id) {
        case MQTT_EVENT_CONNECTED:
            ESP_LOGI("mqtt", "MQTT_EVENT_CONNECTED");
         	// 向主题 "/topic/flag1" 发布一条消息,消息内容是 mqtt_flag_1
            msg_id = esp_mqtt_client_publish(client, "/topic/flag1", mqtt_flag_1, 0, 1, 0);
            printf("[+] MQTT task I: publish successful, msg_id=%d\n", msg_id);
  • 题目意思是ESP32 设备在连接 MQTT 成功后,会自动向 /topic/flag1 主题发送 flag,如果你作为 MQTT 客户端订阅了所有主题,就能收到这条 flag 消息

MQTT 协议是轻量级物联网通信协议,通过发布/订阅机制进行消息传递,适合嵌入式设备使用。

Broker是MQTT 网络中的核心服务器,负责接收、转发所有客户端的消息。客户端通过订阅主题来接收由其他客户端发布到这些主题的消息。

主题通配符:# 表示订阅所有主题,+ 表示匹配单层主题

subscribe输入#订阅所有主题,即可接收 broker 上发布的所有消息,看到flag

THUCTF{#_1s_God_in_MQTT}

task2

你能欺骗订阅者么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void mqtt_data_hander(int length,char * data){
    
    char l[10];
    char url[500] = {0};
    char out[500] = {0};
    char httpdata[500]={0};
    char flagdata[500]={0};
    char tag3[] = " [+] MQTT task III: ";
    sprintf(flagdata,"%s%s%s",mqtt_flag_2,tag3,mqtt_flag_3);  // 构造完整的 flag 内容:flag2 + 提示语 + flag3

    int a = 46; // 默认提取 flag 的长度

   // 从 MQTT 数据中查找 '?',分离 URL 与长度值
    char * p = strnstr(data,"?",length);
    if(p){
        int data_length = p - data; // URL 部分的长度
        snprintf(l,length - data_length,"%s",p+1); // 把 ? 后的内容存入 l
        a = atoi(l); // 把 ? 后的字符串转成整数 a
        length = data_length; // 更新长度,截掉问号部分
    }

    sprintf(url,"%.*s",length, data); // 提取 URL

    // 构造 HTTP 请求,请求头中包含 flag 前 a 字节内容
    char fmt[] = "GET / HTTP/1.0\r\n"
                 "User-Agent: esp-idf/1.0 esp32\r\n"
                 "flag: %s\r\n"
                 "\r\n";

    if( a < (int)(sizeof(mqtt_flag_2) + sizeof(tag3) - 1 ) ){
        memcpy(out,flagdata,a & 0xff);
        sprintf(httpdata,fmt,out); // 插入 a 长度的 flag 内容
        http_get_task(url,httpdata); // 向指定 URL 发送 HTTP 请求
    }          
}
case MQTT_EVENT_DATA:
            ESP_LOGI("mqtt", "MQTT_EVENT_DATA");
            printf("[+] MQTT task II: topic ->  %.*s\r\n", event->topic_len, event->topic);
            printf("[+] MQTT task II: data -> %.*s\r\n", event->data_len, event->data);
            mqtt_data_hander(event->data_len,event->data);
            break;
  • 题目意思是ESP32 作为 MQTT 客户端,订阅了主题,向该主题发送一条消息,只要内容是一个 IP 地址,ESP32 收到后会主动向这个 IP 发起 HTTP 请求,请求中带有部分 flag 内容,藏在 flag: 请求头中,截取长度默认46,如果消息中包含?数字,长度会被更改

攻击者通过发布特定消息控制设备发起 HTTP 请求,将 flag 内容泄露到外部服务器。

设备未验证 MQTT 消息合法性,导致攻击者可控制其对任意地址发起请求。

HTTP 默认使用 80端口

向flag2主题发送ip,nc监听80端口即可

nc -l -p 80

THUCTF{attAck_t0_th3_dev1ce_tcp_r3cV_ch4nnel}

task3

这是个内存破坏的前戏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    sprintf(url,"%.*s",length, data); // 提取 URL

    // 构造 HTTP 请求,请求头中包含 flag 前 a 字节内容
    char fmt[] = "GET / HTTP/1.0\r\n"
                 "User-Agent: esp-idf/1.0 esp32\r\n"
                 "flag: %s\r\n"
                 "\r\n";

    if( a < (int)(sizeof(mqtt_flag_2) + sizeof(tag3) - 1 ) ){
        memcpy(out,flagdata,a & 0xff);
        sprintf(httpdata,fmt,out); // 插入 a 长度的 flag 内容
        http_get_task(url,httpdata); // 向指定 URL 发送 HTTP 请求
    }          
}
  • 题目意思是需要让a的值大于字符串长度才可获得flag,但a < (int)(sizeof(mqtt_flag_2) + sizeof(tag3) - 1限制了a的大小,memcpy(out,flagdata,a & 0xff)是与运算,表示取a的二进制的最低8位。因此a=-1时,二进制是11111111 11111111 11111111 11111111a & 0xff = 00000000 00000000 00000000 11111111 = 255,且a=-1可通过长度判断

& 0xff 是按位与运算,常用来截取整数的二进制最低8位,对负数(补码全1)做 & 0xff 操作,得到的是255。

判断时没有验证符号导致安全漏洞。

向flag2主题发送ip?-1,nc监听80端口即可

THUCTF{0ver_the_Air_y0u_c4n_a77ack_t0_1ntranet_d3v1ce

参考

ESP32 IoT CTF 清华校赛版 Write Up | Clang裁缝店

ESP32 IoT CTF 题解-腾讯云开发者社区-腾讯云

Like 0
May you find your worth in the waking world.
使用 Hugo 构建
主题 StackJimmy 设计