物聯網(IoT) 設備已經成為我們日常生活、工作環境、醫院、政府設施和車隊的重要組成部分。比如:Wi-Fi打印機、智能門鎖、報警系統等等。 2020 年,美國居民平均擁有十多個聯網設備。但出於實用性而選擇物聯網設備的用戶還需要確保這些設備的安全。
由於物聯網設備通常連接到內部家庭或公司網絡,因此破壞此類設備可以為犯罪分子提供對整個系統的訪問權限。 2021 年前六個月,智能設備遭受了約15 億次攻擊,攻擊者試圖竊取數據、挖掘加密貨幣或構建殭屍網絡。
確保物聯網設備良好安全性的一種方法是執行逆向工程活動,這將幫助您更好地了解特定設備的構建方式,並允許您對設備及其固件進行進一步分析。
在本文中,我們展示了智能空氣淨化器逆向工程固件的實際示例,強調了研究其架構的重要性。本文對於致力於網絡安全項目、想要了解逆向工程物聯網設備的細微差別和步驟的開發團隊很有幫助。
研究固件架構的重要性逆向工程物聯網固件的過程因所研究的設備而異。
物聯網設備發展得非常快,市場的主導架構一直在變化。不到十年前,最流行的選擇主要是x86 或ARM,不太可能是MIPS 或PowerPC。但現在,您需要了解多種微控制器架構來對嵌入式設備進行逆向工程:Tricore、rh850、i8051、PowerPC VLE等。
深入學習單一架構不足以在物聯網逆向工程中取得成功。如果開發人員有必要盡快開始逆向工程,他們應該從學習固件架構和結構的基礎知識開始。
這正是我們想要在本文中描述的:逆向工程師研究他們以前從未見過的新架構和固件格式的方式。
在本文中,我們使用了小米空氣淨化器3H 的固件轉儲。我們選擇它是因為它是ESP32 CPU(即Tensilica Xtensa 架構)的固件轉儲。這是一種相當奇特的架構選擇,但在需要Wi-Fi 通信的物聯網設備中很常見。您可以在此GitHub 頁面上找到我們將作為本文示例進行逆向工程的固件(ESP-32FW.bin)。
這種情況的挑戰是,沒有針對固件架構的現有反編譯器,並且反彙編器幾乎不支持它。然而,這是逆向工程師當今面臨的一個非常準確的例子。
物聯網固件逆向工程過程由以下五個階段組成:
1.確定架構在對物聯網設備進行逆向工程之前要問的第一個問題是如何了解逆向工程所需的固件的架構。
最直接的找出方法是閱讀CPU 的數據表並從中了解答案。但在某些情況下,您所擁有的只是固件本身。在這種情況下,您可以使用以下兩個選項之一:
1. 字符串搜索可能允許您找到一些剩餘的編譯字符串,其中包含有關編譯器名稱和體系結構的信息。
2. 二進制模式搜索要求您了解不同類型的微控制器架構中經常使用的指令。您可以在固件中搜索特定架構常見的二進制模式,然後嘗試將固件加載到支持此類架構的反彙編程序中以驗證您的猜測。
一旦確定了架構類型,您就可以開始選擇用於進一步逆向的工具集。對於ESP-32FW.bin,我們已經知道它將是Tensilica Xtensa 架構,因此我們需要選擇要用於研究的反彙編程序。
2.選擇反彙編工具在研究了可以支持Xtensa 的適當反彙編程序後,我們最終得到了三個選項:IDA、Ghidra和Radare。
我們決定首先嘗試使用Ghidra 和IDA,因為我們已經擁有將這些工具成功應用於不同逆向工程項目的豐富經驗。由於IDA 沒有用於Xtensa 的反編譯器,只有用於反彙編器的CPU 模塊,因此我們決定首先嘗試使用Ghidra(我們使用的是10.0 版本)。
Ghidra 默認不支持Xtensa,因此我們需要先為Ghidra 安裝Tensilica Xtensa 模塊。
Xtensa 的反彙編程序可以工作,但反編譯程序存在一些問題,如下面的屏幕截圖所示:
屏幕截圖1. 有關Ghidra 反編譯器中未實現指令的警告
經過一段時間的反彙編,我們意識到Xtensa 的Ghidra 處理器模塊在多種情況下難以確定指令長度。因此,我們放棄了Ghidra,轉而使用IDA(我們使用的是7.7 版本)。
起初在處理器模塊列表中找到Xtensa 很困難,但最終我們在這裡找到了它:
屏幕截圖2. IDA 處理器模塊列表中的Xtensa
IDA 中的處理器模塊看起來足夠穩定,因此我們決定堅持使用IDA。
3. 加載固件第一步是將固件加載到正確的映像基地址,以便將所有全局變量指針解析為有效地址。為此,有必要了解代碼在二進製文件中的位置。
我們首先在基地址加載固件0,並嘗試標記盡可能多的代碼。為了能夠在IDA 中正確標記代碼,我們需要學習Xtensa 固件常見的典型指令序列。為了找出在函數序言中使用哪些指令,我們從GitHub 中獲取了一個示例:esp8266/Arduino:適用於Arduino 的ESP8266 核心。
編譯器似乎使用了以下指令:entry a1, XX
該指令根據參數的值轉換為字節序列,例如36 41 00/36 61 00/36 81 00XX。
通過實現一個簡單的IDA 腳本來搜索此類模式,可以標記大約90% 的代碼:
屏幕截圖3. 在IDA 中標記代碼的結果
一旦我們找到了代碼,就該探索並看看它看起來是否正確。
看看下面的截圖,很明顯有問題。字符串資源被正確引用,但call8指令指向字符串,而不是代碼。並且有些call8指令指向不存在的地址。通常這意味著映像基址錯誤,固件必須加載到其他基址,而不是0.
屏幕截圖4. 發現call8 指令指向字符串和不存在的地址
確定基地址的常見方法是:
1.選擇一個字符串。
2.使用該字符串地址的低位部分查找引用它的代碼。
3.找出真實的字符串地址和我們在代碼中看到的地址之間的差異。因此,我們可以理解如何移動代碼的地址以匹配字符串的當前地址。
在這種情況下,我們發現基地址一定是0x3F3F0000,但即使使用它,call8指令仍然無效。這可能意味著二進制數據被分段,並且閃存中的代碼被分段映射到RAM。因此,有必要將固件分割成多個片段,並將這些片段加載到IDA 的適當段中。
我們查看了固件中的字符串,發現它確實是分段的:
屏幕截圖5. 固件分段證明
經過進一步研究,我們發現了ESP IDF 框架。由於我們的目標固件包含該框架的某些版本,因此我們可以嘗試使用其源代碼來了解固件結構。
我們在ESP IDF 內的bootloader_utility.c 源代碼文件中發現了一個有趣的bootloader_utility_load_partition_table()函數,這意味著固件必須包含分區表。
屏幕截圖6. bootloader_utility_load_partition_table() 函數顯示固件必須包含分區表
為了識別分區表,我們繼續探索源代碼,最終找到了esp_partition_table_verify()函數,該函數由bootloader_utility_load_partition_table()函數調用:
屏幕截圖7. 發現esp_partition_table_verify() 函數
所以一定有ESP_PARTITION_MAGIC和ESP_PARTITION_MAGIC_MD5:
二分搜索AA 50給了我們很好的結果:
屏幕截圖8. AA 50 的二分搜索成功結果
兩者ESP_PARTITION_MAGIC都ESP_PARTITION_MAGIC_MD5可以在附近看到。最有可能的sub_3F3F4848 是esp_partition_table_verify()。
由於我們已經知道esp_partition_table_verify函數在哪裡,因此我們能夠找到bootloader_utility_load_partition_table函數和ESP_PARTITION_TABLE_OFFSET 文件偏移量:
屏幕截圖9. 查找bootloader_utility_load_partition_table 和ESP_PARTITION_TABLE_OFFSET
屏幕截圖10. 查找偏移值
ESP_PARTITION_TABLE_OFFSET 是ESP32-FW.bin 文件中的文件偏移量。現在我們只需要知道分區表條目的結構。 ESP IDF框架的源代碼再次幫助了我們:
我們已將這些結構導入IDA 並將其應用到分區表數據中:
屏幕截圖11. 將結構導入IDA 並將其應用到分區表數據
如您所見,esp_partition_pos_t.offset 是每個分區的文件偏移量,我們現在可以將ESP32-FW.bin 拆分為分區。
但是我們如何才能將每個分區加載到適當的地址呢?似乎有一個image_load()函數負責將固件分區映射到地址空間:
屏幕截圖12. 將固件分區映射到地址空間
接下來,每個分區被分成段。在標題之後,您可以看到一個結構,後面是實際數據:
這裡,esp_image_segment_header_t.load_addr是CPU 地址空間中段數據的虛擬地址。
分區內的段如下所示:
現在,有了有關段的完整信息,我們可以將分區拆分為段並將它們加載到IDA 中的適當地址。我們可以手動完成此提取工作,也可以嘗試通過IDA 加載器插件將其自動化。
儘管如此,Ghidra 似乎已經實現了這樣的加載器。
Recommended Comments