ESP32 Reversing

說是筆記,但感覺還是偏流水帳。高二還是高三的時候有兄弟買了個手台,看到上面標著ESP32閒著沒事把flash dump出來,但後來又因為科大特選這類事情鴿了很久,前幾天在雲端硬碟挖到才想起來。

RECON

回顧過程,先把flash整個dump出來

esptool.py -p /dev/ttyUSB0 -b 460800 read_flash 0 0x400000 dump.bin

先利用 esp32_image_parser.py 把程式的分區從dump出來的檔案分離出來

shell> python esp32_image_parser.py show_partitions dump.bin
reading partition table...
entry 0:
  label      : nvs
  offset     : 0x9000
  length     : 20480
  type       : 1 [DATA]
  sub type   : 2 [WIFI]

entry 1:
  label      : otadata
  offset     : 0xe000
  length     : 8192
  type       : 1 [DATA]
  sub type   : 0 [OTA]

entry 2:
  label      : app0
  offset     : 0x10000
  length     : 1310720
  type       : 0 [APP]
  sub type   : 16 [ota_0]

entry 3:
  label      : app1
  offset     : 0x150000
  length     : 1310720
  type       : 0 [APP]
  sub type   : 17 [ota_1]

entry 4:
  label      : spiffs
  offset     : 0x290000
  length     : 1507328
  type       : 1 [DATA]
  sub type   : 130 [unknown]

MD5sum:
28f14c0945017760a107065db97a2507
Done

然後把app0分離出來

python esp32_image_parser.py dump_partition -partition app0 dump.bin

先直接strings看看有沒有什麼線索,最後找到這幾個關鍵字

{"LOOP_DELAY_TIME":2000,"ENCODR_THRESHOLD":1,"MOUSE_ENCODER_DISTANCE":1,"MOUSE_KEYBOARD_DISTANCE":1,"ENCODER_L_CW_MODE":0,"ENCODER_L_CW_CODE":1,"ENCODER_L_CCW_MODE":0,"ENCODER_L_CCW_CODE":0,"ENCODER_R_CW_MODE":0,"ENCODER_R_CW_CODE":3,"ENCODER_R_CCW_MODE":0,"ENCODER_R_CCW_CODE":2,"BTN_START_MODE":1,"BTN_START_CODE":22,"BTN_A_MODE":1,"BTN_A_CODE":4,"BTN_B_MODE":1,"BTN_B_CODE":5,"BTN_C_MODE":1,"BTN_C_CODE":6,"BTN_D_MODE":1,"BTN_D_CODE":7,"BTN_L_MODE":1,"BTN_L_CODE":15,"BTN_R_MODE":1,"BTN_R_CODE":21,"FXL_MODE":2,"FXL_BRIGHT":15,"FXL_COLOR":"00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF","FXL_B_FRQ":35,"FXL_B_MOV":35,"FXL_R_SPEED":40,"FXL_R_DIR":4,"FXR_MODE":2,"FXR_BRIGHT":15,"FXR_COLOR":"00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF","FXR_B_FRQ":35,"FXR_B_MOV":35,"FXR_R_SPEED":40,"FXR_R_DIR":4,"VOL_MODE":2,"VOL_BRIGHT":15,"VOL_COLOR":"00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF,00FFFF","VOL_B_FRQ":10,"VOL_B_MOV":0,"VOL_R_SPEED":30,"VOL_R_DIR":4,"BTN_MODE":3,"BTN_BRIGHT":95,"BTN_COLOR":"FFFF00,00FFFF,00FFFF,00FFFF,00FFFF,FF0000,FF0000","BTN_B_FRQ":1,"BTN_B_MOV":0,"BTN_R_SPEED":35,"BTN_R_DIR":0,"LOGO_MODE":2,"LOGO_BRIGHT":90,"LOGO_COLOR":"00FFFF,00FFFF,FF00FF,00FFFF,00FFFF,00FFFF,00FFFF","LOGO_B_FRQ":35,"LOGO_B_MOV":35,"LOGO_R_SPEED":20,"LOGO_R_DIR":3,"OTHER_MODE":1,"OTHER_BRIGHT":100,"OTHER_COLOR":"00B5FF,00B5FF,D50505,FF0000","OTHER_B_FRQ":1,"OTHER_B_MOV":35,"OTHER_R_SPEED":35,"OTHER_R_DIR":0}

找entry

裝個插件然後丟進Ghidra,很輕鬆找到ROM引導bootloader call的main fucntion,但啟動流程並不是這樣

  1. ROM Bootloader:晶片通電,執行ROM裡面的程式
  2. Second Stage Bootloader:載入Partitions Table,決定啟動哪個Partition
  3. Application Startup:Allocate CPU, RAM, 最後jump到執行task的程式 最後目標應該是找到第三階段把控制權給 app 的那一段

那我們就從main function中尋找把控制權交給app的邏輯。通常bootloader會將app的entry address載入到一個特定的RAM和他的位置,然後透過函數指針呼叫。 在decompile window中,注意到了這樣一段code

// psuedo code at main (0x4000f6c4)
void main(void) {
    // hardware initialization...

    code *user_entry = (code *)Ram40024598;

    iVar3 = (*user_entry)();

    // ...
}

0x40024598是一個data section的位置。Ghidra的decompiler雖然顯示呼叫了它,但無法直接告訴我們這個指針指向哪裡,因為那是在runtime決定的。 但是我們可以直接看那個address的初始值。

  1. 在Ghidra跳轉到0x40024598
  2. 查看該地址儲存的資料
  3. xtensa是Little Endian,00 aa 08 40對應的是0x4008aa00

然後我們跳轉到0x4008aa00

void app_main(void) {
    // Core system initialization
    func_0x40026c94();           // System core init
    FUN_4008bfe8();              // Memory/heap initialization
    FUN_4008d6fc();              // Device initialization
    FUN_4008d878();              // Data structures setup

    // Conditional USB initialization
    if (*_LAB_4008081c != '\0') {
        iVar4 = FUN_4008d380();  // Initialize USB hardware
        if (iVar4 != 0) {
            FUN_4002d354();       // Error handler
        }
        FUN_4008bf08(_LAB_400800bc);  // Configure USB descriptors
    }

    // Arduino setup() equivalent
    FUN_4008aba8();              // User setup code

    // Timer and interrupt setup
    FUN_4008da2c();              // Timer/interrupt initialization

    // Task creation
    FUN_4008bbf8();              // Create tasks

    piVar1 = _LAB_40080820;
    func_0x40027c00(*_LAB_40080820);

    // Create 3 FreeRTOS tasks
    uVar5 = FUN_4009a0f4(_LAB_40080828, _DAT_40080824);
    *(iVar4 + 4) = uVar5;

    uVar6 = FUN_4009a0f4(uVar7, _DAT_4008082c);
    *(iVar10 + 8) = uVar6;

    uVar7 = FUN_4009a0f4(uVar7, uVar5);
    *(iVar4 + 0xc) = uVar7;

    // Hardware peripheral initialization
    func_0x40089d80();           // Initialize encoders
    func_0x40088780();           // Initialize LEDs (WS2812)
    FUN_400259a0(_LAB_40080830);
    func_0x40088814();           // Initialize buttons
    FUN_400887b8();              // Additional hardware setup

    // Execute C++ global constructors
    puVar8 = _DAT_40080834;      // Constructor array at 0x3F005DE0
    while (puVar2 <= puVar8) {
        (*(code *)*puVar8)();     // Call each constructor
        puVar8 = puVar8 + -1;
    }

    // Execute additional constructors with flags
    for (; puVar3 <= puVar9; puVar9 = puVar9 + -2) {
        if ((puVar9[1] & 1) != 0) {
            (*(code *)*puVar9)();
        }
    }

    // Start system timer
    local_30 = 0;
    uStack_2c = _LAB_4008076c;
    FUN_4002c640(&local_30);
    FUN_4002c6a8(&local_30);
    FUN_4002c790(&local_30);
    FUN_400a09d4();

    // Enter infinite loop - scheduler takes over
    do { } while(true);
}

注意到符合以下特徵:

  1. Hardware peripheral initialization
  2. Execute C++ global constructors
  3. 3 FreeRTOS Tasks has been created

但是看Create Tasks的參數指向的是一些ESP-IDF服務的字串,而不是真正的邏輯。 回顧前面的Recon階段,有個loopTask字串很有可能是。雖然字串存在,找不到直接引用的。 那我們思考一下這個手台原本的邏輯,一個遊戲控制器就必須做兩件事:掃描按鍵和發送USB HID。

我們先定位TinyUSB最頂層的發送函式 usb_dc_ep_write。這是物理上把資料丟給USB硬體的函式,所有更上層的呼叫都得經過它。 調用他的函式並沒有特別多,就一個一個看。發現有個函式 FUN_400843bc 符合以下特徵

  1. while(true): 通常任務函式都是這樣
  2. 週期性呼叫某個函式: 點進去看裡面有一段 for(i=1; i<7; i++) 的循環 正好對應硬體的七個按鍵
void controller_main_loop(void) {
    static bool initialized = false;
    static uint8_t sequence_number = 0;
    uint8_t hid_report[32];

    // Stack canary for security
    char *stack_guard = (char *)*_DAT_400800a4;

    // ONE-TIME INITIALIZATION
    if (!initialized) {
        initialized = true;
        FUN_40085f68();      // Initialize encoders
        FUN_400861dc();      // Initialize buttons
    }

    // INFINITE MAIN LOOP
    while(true) {
        // Check stack canary (detect buffer overflows)
        memw();
        if (stack_guard != (char *)*_DAT_400800a4) {
            return;  // Stack corruption - exit
        }

        // Read button state (returns 0 if no button pressed)
        int button = get_pressed_button();  // FUN_40086228

        if (button != 0) {
            // BUILD 32-BYTE HID REPORT
            hid_report[0] = 9;      // Report descriptor field
            hid_report[1] = 4;      // Report descriptor field
            hid_report[2] = sequence_number++;  // Increment sequence
            hid_report[3] = 2;
            // ... encoder values (left rotation)
            hid_report[10] = 0x21;
            hid_report[11] = 9;
            hid_report[12] = 0x11;
            hid_report[13] = 5;
            hid_report[14] = 5;
            hid_report[15] = button;  // Pressed button number (1-7)
            hid_report[16] = 3;
            hid_report[17] = 0x40;
            // ... encoder values (right rotation)
            hid_report[20] = 0x22;
            hid_report[21] = 0x40;
            hid_report[22] = 7;
            hid_report[23] = 7;
            hid_report[24] = 0x80;  // Control byte
            hid_report[25] = 3;
            hid_report[26] = 0x40;
            // ... rest of report

            // SEND USB HID REPORT (32 bytes)
            send_usb_hid_report(hid_report, 0x20);  // Call via function pointer
        }
    }
}
uint get_pressed_button(void) {
    ushort current_state;
    ushort previous_state;
    uint button_num;

    button_num = 1;
    current_state = button_state[1];      // Current button states
    previous_state = button_state[0];     // Previous button states

    // Loop through buttons 1-6
    do {
        uint button_bit = 1 << button_num;

        // Check for rising edge (was not pressed, now pressed)
        bool was_not_pressed = (current_state & button_bit) == 0;
        bool is_now_pressed = (previous_state & button_bit) != 0;

        if (was_not_pressed && is_now_pressed) {
            // Button was just pressed - update state and return
            button_state[1] |= button_bit;
            return button_num;
        }

        button_num++;
    } while (button_num != 7);

    // Check button 7 separately
    button_num = 1;
    while ((current_state & (1 << button_num)) != 0) {
        button_num++;
        if (button_num == 7) {
            return 0;  // All buttons released
        }
    }

    // Update state and return button 7
    button_state[1] |= (1 << button_num);
    return button_num;
}

番外:ESP32-S2模擬器

最後因為不支援Flash模擬就放棄了,這邊紀錄一下編譯參數之類的

../qemu-xtensa-esp32s2/configure --target-list=xtensa-softmmu,riscv32-softmmu --enable-debug --enable-sanitizers --disable-strip --disable-capstone --disable-vnc --disable-seccomp --disable-werror --disable-virtiofsd --disable-linux-io-uring --disable-libnfs --extra-cflags="-I/usr/include" --extra-ldflags="-L/usr/lib -lgcrypt"