4. 研究 Xtensa 架構特性
現在我們已經将所有段加載到适當的地址,我們可以開始逆向工程了。
但爲了高效地做到這一點,我們需要更多地了解 Xtensa 架構,包括:
1. 指令中的參數順序
2. 條件跳轉的執行細節
3. 編譯器調用約定
4. 堆棧組織
首先要探索的是指令中的參數順序。例如:MOV R1, R2. 您可以在所有架構中找到此類指令,但這可能意味着将 R1 複制到 R2 或将 R2 複制到 R1。因此,了解指令中源代碼的位置以及目标寄存器的位置至關重要。您可以在 GitHub 上找到 Xtensa 指令集描述。
至于該 MOV 指令,在 Xtensa 中,表示将 R2 複制到 R1。因此,第一個參數将是大多數簡單指令(例如數學相關指令)中的目的地。例如,指令 addi a14, a1, 0x38 意味着 a14 = a1 + 0x38。
但對于存儲數據的指令,情況則相反。例如,該指令 s32i.n a5, a1, 0x10 意味着 的值 a5 必須存儲在地址 處 ( a1 + 0x10 ) 。
要學習的第二件事是條件跳轉的完成方式。有兩種方法可以做到這一點:
1. 使用專用指令進行比較操作,設置标志寄存器,然後進行條件跳轉。
2. 使用一條指令一次性執行所有這些操作。
Xtensa 執行後者:beqz a10, loc_400E1C54
使用單個指令來檢查是否 a10 等于零,然後它要麽跳轉到 loc_400E1C54,要麽不跳轉。
第三步是檢查編譯器使用的調用約定:将參數傳遞給函數的方式以及如何返回值。
Xtensa 以一種非常不尋常的方式傳遞參數。參數在調用指令之前放入寄存器中。但它們在函數中出現的寄存器與調用之前所在的寄存器不同:
以下是如何在彙編程序級别将參數傳遞給函數的示例:
這裏我們有三個論據:
a10 是目的地址
a11 是源地址
a12 是要複印的尺寸
然而,一旦代碼進入 memcpy 函數,這些值就會自動分别傳輸到 a2、a3 和 a4 寄存器中。
同樣的技巧也用于返回值。在 memcpy 函數内部,該值存儲在寄存器中 a2,但從函數返回後,該值出現在 a10.
返回的樣子如下 0:
這就是檢查返回值的樣子:
benz.na10 在從調用返回時檢查寄存器的值。
最後,有必要了解堆棧是如何組織的。
Xtensa 使用 a1 寄存器來創建堆棧幀。每個函數都以入口指令開始:entry a1,0xC0,其中 0xC0 是堆棧幀的大小,即函數需要用于堆棧變量的堆棧量。
通常,這些函數從初始化堆棧變量開始:
寄存器中的零值 a5 被寫入基于 a1 寄存器的堆棧變量中。
在獲得有關 Xtensa 架構的所有必要知識後,我們終于可以開始逆向其代碼了。
5. 在 IDA 中對 Xtensa 代碼進行逆向工程
與 ARM、MIPS 和 PowerPC 相比,Xtensa 不是最流行的架構,并且沒有完整的功能列表。因此,IDA 處理器模塊會存在一些我們需要克服的限制。
IDA 中 Xtensa 處理器模塊的主要限制是:
函數參數沒有自動注釋
堆棧幀不會自動創建
一些 ESP32 函數屬于 IROM,因此存在對硬編碼地址的調用
部分 Xtensa 指令未反彙編
讓我們讨論一些克服這些挑戰的技巧。
5.1. 函數參數的類型系統和注釋
從 IDA 7.7 開始提供 Xtensa 類型系統。在 IDA 中擁有可用的類型系統非常重要,因爲它使逆向變得方便。特别是,它允許您導入 C 結構的定義并指定 IDA 使用的函數原型,以便在傳輸函數參數的指令附近放置自動注釋。
但是,如果您沒有類型系統,還有一個解決方法。
首先,讓我們看看有類型系統時函數是什麽樣子的:
屏幕截圖 13. 當有可用的類型系統時函數的外觀
函數原型設置有參數的名稱和類型,以便 IDA 可以使用此信息在調用站點注釋參數:
屏幕截圖 14. 函數原型
但 Xtensa 不會有這樣的事情。另一種方法是使用 IDA 中的可重複注釋功能。如果您在函數的開頭設置可重複的注釋,它将顯示在所有調用站點上。
屏幕截圖 15. 設置可重複注釋
屏幕截圖 16. 可重複的注釋顯示在所有調用站點上
因此,我們可以使用此功能來定義函數參數:
屏幕截圖 17. 使用可重複注釋功能定義函數參數
調用站點将如下所示:
屏幕截圖 18. 調用站點
您可以在注釋中選擇寄存器名稱,IDA 會在代碼中突出顯示它。因此,您可以輕松找到參數值。
5.2. 恢複堆棧幀
要恢複堆棧幀,您需要手動指定堆棧大小,然後通過在每個與堆棧一起使用的指令處按 K 鍵來顯示 IDA 的使用位置。
讓我們探索一下 config_router_safe 函數,例如:
屏幕截圖 19. config_router_safe 函數
很明顯這裏的棧幀大小是 0xC0。我們在函數的堆棧設置中使用該值 ( Alt+P ) :
屏幕截圖 20. 使用 0xC0 值(堆棧幀大小)
從視覺上看,什麽也不會發生,但是如果您通過按 Ctrl+K 轉到該函數的堆棧幀,您會注意到堆棧空間現在已分配:
屏幕截圖 21. 分配堆棧空間
接下來要做的是使用 entry 指令指定堆棧移位。在此之前,我們建議啓用堆棧指針可視化,如下面的屏幕截圖所示:
屏幕截圖 22. 啓用堆棧指針可視化
現在,代碼應該如下所示:
屏幕截圖 23. 啓用堆棧指針可視化後的代碼
000 是當前堆棧指針移位值,我們需要将其移位 0xC0。爲此,請将光标置于入口指令處,然後按 Alt+K 以查看以下窗口,您可以在其中指定新舊堆棧指針之間所需的差異:
屏幕截圖 24. 将當前堆棧指針值移動 0xC0
作爲此操作的結果,代碼将如下所示:
屏幕截圖 25. 移動當前堆棧指針移位值後的代碼
現在,如果您開始在與寄存器一起使用的每條指令處按 Ka1,IDA 将創建堆棧變量:
屏幕截圖 26.IDA 創建新的堆棧變量
還可以編寫 IDA 腳本來自動執行這些操作。
5.3. 調用 IROM
調用位于 CPU 的 IROM 部分而不是固件中的某些低級 API 的情況并不少見。在這種情況下,固件僅與包含定義的 IROM 函數地址的特殊鏈接器定義文件鏈接。
在逆向期間,IROM 函數調用如下所示:
屏幕截圖 27. IROM 函數調用
40058E4C 是 IROM 内的地址。但不可能知道固件調用了哪個函數。因此有必要檢查 ESP32 工具鏈以查找鏈接器定義。
ESP32 芯片的 IDE 是 Espressif IDE。在 IDE 文件中搜索 IROM 地址,我們會找到:C:Espressifframeworksesp-idf-v4.4.2componentsesptool_pyesptoolflasher_stubldrom_32.ld
屏幕截圖 28. ESP32 ROM 地址表
這些值可以輕松轉換爲枚舉數據類型:
屏幕截圖 29. 将值轉換爲枚舉數據類型
然後,我們需要導入 IDA,以便将 enum 應用于 IROM 地址值:
屏幕截圖 30. 将枚舉應用于 IROM 地址值
如果我們在 IROM 地址附近添加可重複的注釋,它将使所有内容更容易閱讀:
屏幕截圖 31. 在 IROM 地址附近添加可重複注釋後的代碼
5.4. 無法識别的指令
經常發生的情況是,處理器模塊已針對指令集的某些特定變體實現。然後制造商制造出具有 99% 兼容指令集的新 CPU,其中包含 10 多個新指令,這是最初沒人想到的。因此 IDA、Ghidra 和 Radare 等工具可能無法反彙編一些新指令。
克服這一挑戰的正确方法是擴展處理器模塊并添加對新指令的支持。這需要對反彙編器 API 有深入的了解,而這些 API 并不那麽容易理解。
讓我們讨論一種可能的解決方法,用于解決以下情況:盡管存在一些無法識别的指令,但您隻想讓 IDA 創建函數。假設 IDA 不知道 RER 指令,并且在包含 RER 操作碼的情況下無法創建該函數:
屏幕截圖 32. 如果函數包含 RER 操作碼,IDA 無法創建該函數
您可以按 P 多次。除了控制台窗口中出現錯誤外,不會發生任何事情:
屏幕截圖 33. 控制台窗口中的錯誤
但是,這并不意味着 IDA 無法創建遵循 RER 指令的指令。您可以跳過 RER 指令的三個字節,然後創建代碼:
屏幕截圖 34. 跳過 RER 指令的三個字節後創建代碼
接下來,您可以選擇從輸入到最後的整段代碼 retw.n,然後按 P:
屏幕截圖 35. 選擇從 Entry 到 retw.n 的整段代碼
之後,IDA 将創建該函數:
屏幕截圖 36.IDA 創建一個函數
通常,反彙編程序無法識别的擴展指令在逆向過程中不會産生太大差異。可能導緻問題的是執行調用、跳轉或加載 / 存儲等操作的新指令,因爲代碼流丢失并且對數據的引用丢失。
結論
對于涉及逆向工程物聯網固件的項目來說,在轉向業務邏輯之前研究未知的硬件架構至關重要。盡管逆向工程師可能需要幾周的時間來學習該架構,但從長遠來看,這種深入的研究有助于提高進一步工作的速度。