环境搭建
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
|
硬件
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

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 11111111,a & 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 题解-腾讯云开发者社区-腾讯云