跳转至

Esp32ctf thu学习

esp32ctf_thu学习

环境搭建

硬件

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

软件

这是源码:GitHub - xuanxuanblingbling/esp32ctf_thu

esp32的windows烧录环境:Windows 平台工具链的标准设置 - ESP32 - — ESP-IDF 编程指南 latest 文档,直接点进来下载离线安装包

1663401939422-92048e0d-64e1-4672-8e71-5fc4562289c9.png

我下载的是这个,无脑安装

1663401961582-a5a645d1-4c58-4b25-93d6-aae72bcb94d6.png

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

1663402036469-21523c33-b291-43f4-a5cf-8b255b41da21.png

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

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

1663402294888-caf41bc4-481f-439a-95c5-21ca9c949f04.png

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

1663402370898-3eed033c-1e8b-4151-8aeb-329cc5b97174.png

选完之后 Q 保存退出

然后idf.py build编译代码

1663402432662-1bb995b5-25a9-4615-9330-b03932686ade.png

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

1663402583249-266c8ec6-d36d-44c5-80e2-1aa415357bcf.png

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

1663402993713-7632bd14-b14b-4b96-bf4e-fedb58296b73.png

注意

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

开搞

咱从头开始,先把 GND 和 23 号引脚连起来,如果前面已经供电了在连 GND 和 23 引脚需要断电重新供电,或者摁一下板子上的 EN 摁扭才能切换到硬件的题目这一方向

1663422356545-ded8b28f-9077-42fd-8162-2a3977d4728a.png

硬件题目

task1

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

#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)){   //gpio_get_level是获取GPIO电平值,低电平是0,高电平是1
            hit ++ ;
        }else{
            hit = 0;
        }
        if(hit>3){
            printf("[+] hardware task I : %s\n",hardware_flag_1);
            break;
        }
        vTaskDelay(1000 / portTICK_RATE_MS);
    }
}

此时日志如下

1663422439837-27234f7a-a502-485d-bae9-74120af6b489.png

GPIO 是指板子上的一组引脚。这些引脚可以发送或接收电信号,但它们不是为任何特定目的而设计的,可以由我们通过编程来实现任意功能。这就是为什么它们被称为通用 IO(General-purpose input/output)

抬高就是给它供电,把板子上的 3.3V 或 5V 与他接起来就行了

1663423088413-9806b8b3-cf2d-44bb-9758-60662c9c7749.png

task2

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

#define GPIO_INPUT_IO_0     18
#define GPIO_INPUT_PIN_SEL  ((1ULL<<GPIO_INPUT_IO_0) )
#define ESP_INTR_FLAG_DEFAULT 0

static void IRAM_ATTR gpio_isr_handler(void* arg){  //GPIO中断处理程序,中断了就执行这个
    trigger++;
}

void hardware_gpio_setup(){
    gpio_config_t io_conf;  //GPIO的配置参数
    io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL; //要配置的GPIO引脚
    io_conf.mode = GPIO_MODE_INPUT;   //仅输入
    io_conf.intr_type = GPIO_INTR_POSEDGE;  //GPIO中断类型,上升沿
    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);  //注册GPIO_INPUT_IO_0,也就是GPIO18中断处理程序
}

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

上升沿指的是数字电路中数字电平从低电平(数字0)到高电平(数字1)的一瞬间,下降沿同理

借助一个 TX 的引脚会一直输出这一特点来与 GPIO18 连起来,这样就可以啦

1663424431142-ac0514c9-56ba-49a9-8921-25ec638ce677.png

补充:上下拉是给 IO 一个默认的状态,上拉和下拉是指 GPIO 输出高电位(上拉)还是低电位(下拉),从程序设计的角度讲,上拉就是如果没有输入信号则此时 I/O 状态为 1,下拉相反

关于上下拉电阻看一下这个

试着理解一下代码的意思,给 GPIO18 注册了一个上升沿中断处理函数,函数的功能是 trigger+1,同时把 GPIO18 的上拉关掉,这样没有输入时候的 I/O 状态就不是 1,有输入的时候就会触发上升沿,这样 trigger 就会增加了

task3

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

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

被晃了呜呜呜,这个板子上有个TX2 我以为是这个呐,结果等了半天啥也没有,这个是让你分析代码,看一下用的哪一个 GPIO 作为 TX,通过 define 可以看到,TXD 是 GPIO4,那就把 GPIO4 接到 USB->TTL 的 RX 上就可以看到了

1663464167141-2542de13-242f-4e65-9e8a-9b32caf1c3c1.png

1663464321221-1b358f19-fddc-47b9-8b5c-89564481dc14.png

网络题目

task1

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

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

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

这里得往上翻日志了,他随机指定了一个 wifi 名和密码,会去连接那个密码,用手机开个热点即可

ssid: kbmxet , password svtujgjb

1663468246528-b7658834-cd5c-4f3a-a095-5e77c4bba78f.png

1663468313965-ffdc9a21-bc2c-4d4a-9a65-e76b825ce593.png

日志里给出了 IP 和端口,用电脑也连接上开的热点

1663468509188-0bc598ff-f205-45b7-8c28-cc55d347644b.png

nc 一下,连上之后根据源码里的逻辑,发送 getflag 即可

1663468674689-64674887-08ac-473f-889f-421499447354.png

task2

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

此部分代码不完善,可能会因死循环爆栈导致重启,请见谅...(确实容易重启🤣)

改为用电脑开这个热点,然后直接抓取网卡的流量,嗯,,他好像不会切换,得重新做一遍

1663469043873-d25f2250-0618-4456-8c39-855556eb1593.png

电脑开启热点后会有一个新的网卡,就抓这个网卡即可

1663469130367-202f4c8c-6b5c-4ec9-bc62-41b45b28c461.png

追踪 HTTP 流发现 flag

1663469182870-f53830fb-a789-4c0d-b323-aadcc23789a0.png

task3

题目:flag在空中

同时日志如下:

1663469414936-904aa8a7-418e-44d1-9908-f4091aea9dd3.png

把无线网卡插到 kali 里面,把无线网卡设置为监听模式,然后开启抓包

airmon-ng start wlan0
airodump-ng wlan0mon

然后打开 wireshark 选择网卡为 wlan0mon 进行抓包

1678027749145-7acde137-a82b-4dc0-8f33-dbb5f82d21a9.png

蓝牙题目

task1

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

也是刚开始的日志中随机指定了蓝牙设备的名字

1663471371588-bd267804-0dc5-45da-a875-1b80fc966eff.png

直接改手机的名字就行了,现在手机好像默认不被发现?在手机上点击扫描周围设备就可以了

1663471536239-76f1b565-aaf4-44c2-bdc3-637effd17748.png

1663471496530-e47c613b-7194-4cbd-b11c-ad09575bac65.png

task2

题目:flag在空中

那就抓包吧,开启 Hollong,直接全选广播包中就有,同时也确定了他的 MAC 地址和设备名

1663471771481-66cee620-36a1-4ae4-9f07-fdfce90abba2.png

也可以用 nRF Connect 直接看到广播包

1669170878486-406afd07-13cd-436c-a3e7-53d7ad3c9ac9.png

task3

题目:分析GATT业务并获得flag

用 nRF Connect 连接,一开始读,只有DEEDBEEF

1663472681416-6ca6f906-9e63-44b0-ad41-0f5a5d59b6d4.png

搜索源码里的 [+] bluetooth task III 定位到这里,我们写入的值与 flag2 进行了对比,通过则 open_task3 = 1

1663472758689-93f20f90-6f37-4edc-b93d-fc1929d0874f.png

只有当 open_task3 = 1 时才会把真正的 flag 拷贝过去

1663472824539-fe259d58-8b23-49d4-b77f-857a6735103d.png

发送 task2 的 flag

1663472524087-2f203cb1-b446-4466-808a-a8e2156e8535.png

再次读取,转成 ASCII码即可 THUCTF{WrItE_4_gA7T}

1663472536660-885fccaa-01f1-4b7a-83fc-396256a37f79.png

MQTT题目

这里有些问题,原版的域名到期了,你需要在自己的服务器上拉起来一个 Docker

docker build -t esp32ctf .

然后别忘了把服务器的防火墙打开 1883 端口,运行命令把 Docker 启动起来

docker run -d -p 1883:1883 esp32ctf

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

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

然后需要开一个热点:名称:THUCTFIOT;密码:mqttwifi@123

设备连接上之后 esp32 会连接我们搭建的 MQTT broker

task1

题目:你知道MQTT的上帝是谁么

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

1669194495711-d7b7d5f2-da07-4e60-bd52-906de352d67a.png

也可以参考这个用 python 调用 https://www.yuque.com/hxfqg9/iot/pqfymw#r2zRT

task2

题目:你能欺骗订阅者么

当 esp32 接收到数据时,会进入到 MQTT_EVENT_DATA 中,在代码中看到会去调用 mqtt_data_hander

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;

mqtt_data_hander 这一段代码是 task2 和 task3 通用的,传进去的参数一个是数据长度,一个是数据,会先通过数据中的 ? 进行分割,前面的是 ip,后面的是长度,flag2 定义的长度是 46,也就是变量 a,如果通过 ? 能找到长度,就会把 a 覆盖掉,否则就直接用 a 的值,因此在 task2 中,我们只需要传入 IP 即可

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);//把flag2、tag3和flag3都放在flagdata里面

    int a = 46;

    char * p = strnstr(data,"?",length);//在data中查找"?",如果找到了就返回位置
    if(p){
        int data_length = p - data;//数据长度 等于 p的地址 - 数据的地址
        snprintf(l,length - data_length,"%s",p+1);//这个l就是打印flag的长度,就是传入的data中?后面的数,但是是字符串的形式
        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 ) ){//判断a的大小,以整数的形式进行比较,正常来说肯定是通过的,因为mqtt_flag_2+tag3-1长度大于a
        memcpy(out,flagdata,a & 0xff);//这样out就是flagdata中以a&0xff为长度的
        sprintf(httpdata,fmt,out);
        http_get_task(url,httpdata);//带上数据去请求
    }
}

在 MQTTX 中给主题 /topic/flag2/bayyqa 发送 ip 后 esp32 就会按照 a 的长度从 flagdata 中取出值来请求 ip

1669194579526-109ac397-c377-4ebb-bef5-bee9bccf090d.png

在远程服务器上监听一个 80 端口,得到了 task2 的 flag

1669193639985-46244871-a34e-47bf-8a29-27b28a8d52dd.png

task3

题目:这是个内存破坏的前戏

如果想要得到 task3 的 flag 需要使 a 的足够大,这样 memcpy 的时候才会把 flagdata 全部包括进来,但是前面有个判断,只有当 ( a < (int)(sizeof(mqtt_flag_2) + sizeof(tag3) - 1 ) ) 时才可以,这里判断长度时有符号,但下面使用时和 0xff 进行了与运算 memcpy(out,flagdata,a & 0xff),如果设置长度为 -1 ,与 0xff 与运算将会得到一个很大的数,这样既可以绕过大小限制,又可以完整的拷贝 flagdata 带出位于 flag2 后的 flag3

1669195008238-0ef5c66b-000e-43f6-9779-958e456a5373.png

1669193666814-8d06a945-9467-4711-811b-30db48a7be54.png

原文: https://www.yuque.com/hxfqg9/iot/oqmpwi