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看看有沒有什麼線索,最後找到這幾個關鍵字
loopTask:TinyUSB HID:/spiffs:- 一些Arduino相關字串:
https://espressif.github.io/arduino-esp32/webusb.html,arduino_usb_events然後spiffs分離出來有個 config.json 內容如下
{"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,但啟動流程並不是這樣
- ROM Bootloader:晶片通電,執行ROM裡面的程式
- Second Stage Bootloader:載入Partitions Table,決定啟動哪個Partition
- 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的初始值。
- 在Ghidra跳轉到0x40024598
- 查看該地址儲存的資料
- 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);
}
注意到符合以下特徵:
- Hardware peripheral initialization
- Execute C++ global constructors
- 3 FreeRTOS Tasks has been created
但是看Create Tasks的參數指向的是一些ESP-IDF服務的字串,而不是真正的邏輯。 回顧前面的Recon階段,有個loopTask字串很有可能是。雖然字串存在,找不到直接引用的。 那我們思考一下這個手台原本的邏輯,一個遊戲控制器就必須做兩件事:掃描按鍵和發送USB HID。
我們先定位TinyUSB最頂層的發送函式 usb_dc_ep_write。這是物理上把資料丟給USB硬體的函式,所有更上層的呼叫都得經過它。
調用他的函式並沒有特別多,就一個一個看。發現有個函式 FUN_400843bc 符合以下特徵
- while(true): 通常任務函式都是這樣
- 週期性呼叫某個函式: 點進去看裡面有一段 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"