
作為會議的一部分 0x0A DC7831 16月XNUMX日,我們做了關於二進位程式碼模擬基本原理和我們自己開發的硬體平台模擬器的報告 .
在本文中,我們將介紹如何在模擬器中啟動裝置的韌體,示範與偵錯器的交互,並對韌體執行小型動態分析。
底
很久以前,在一個遙遠的星系
幾年前,我們的實驗室需要檢查一個設備的韌體。使用引導程式對韌體進行壓縮和解壓。他以一種非常複雜的方式做到了這一點,在內存中多次移動數據。然後韌體本身就會主動與週邊設備互動。所有這些都在 MIPS 核心上。
由於客觀原因,現有的模擬器不適合我們,但我們仍然希望運行程式碼。然後我們決定製作自己的模擬器,它可以完成最低限度的功能並允許我們解壓縮主韌體。我們嘗試了並且成功了。我們想,如果我們添加外圍設備來運行主韌體會怎麼樣。它並不太痛苦,而且也很有效。我們再次考慮並決定製作一個功能齊全的模擬器。
其結果是一個計算系統的模擬器。 .

為什麼選擇 Kopycat?
這是一個文字遊戲。
- 山寨 (英語,名詞 [ˈkɒpɪkæt]) - 模仿者,模仿者
- 貓 (英語,名詞 [ˈkæt])— 一隻貓,一隻公貓 — 該計畫的一位創始人最喜歡的動物
- 字母「K」來自 Kotlin 程式語言
科皮貓
在建立模擬器時,我們設定了非常具體的目標:
- 快速創建新的周邊設備、模組或處理器核心的能力;
- 從各種模組組裝虛擬設備的能力;
- 將任何二進位資料(韌體)載入到虛擬設備記憶體的能力;
- 使用快照(系統狀態快照)的能力;
- 透過內建調試器與模擬器互動的能力;
- 非常適合開發的現代語言。
因此,我們選擇了 Kotlin 來實現,總線架構(即模組透過虛擬資料匯流排相互通訊),JSON 作為設備描述格式,GDB RSP 作為與偵錯器互動的協定。
開發工作已進行了兩年多,並且仍在積極繼續。在此期間,MIPS、x86、V850ES、ARM 和 PowerPC 處理器核心得到了實作。
該項目正在不斷發展,現在是時候將其展示給更廣泛的公眾了。我們稍後會詳細描述該項目,但現在讓我們集中討論如何使用 Kopycat。
對於最不耐煩的人,可以在以下網址下載該模擬器的促銷版本: .
模擬器中的 Rhino
讓我們回想一下,早些時候,為SMARTRHINO-2018會議創建了一個測試設備“Rhino”,用於教授逆向工程技能。靜態韌體分析的過程描述於 .
現在讓我們嘗試添加“揚聲器”並在模擬器中運行韌體。
我們需要:
1)Java 1.8
2)Python 和模組 在模擬器內部使用 Python。 WHL 建構的 Jep 模組位於 Windows 人們可以 .
為 Windows:
1)
2)
為 Linux:
1)socat
您可以使用 Eclipse、IDA Pro 或 radare2 作為 GDB 用戶端。
它是如何工作的呢?
為了在模擬器中刷新固件,需要「組裝」一個虛擬設備,它是真實設備的模擬。
實際設備(「犀牛」)的結構圖如下:

該模擬器具有模組化結構,最終的虛擬設備可以用 JSON 檔案描述。
JSON 105 行
{
"top": true,
// Plugin name should be the same as file name (or full path from library start)
"plugin": "rhino",
// Directory where plugin places
"library": "user",
// Plugin parameters (constructor parameters if jar-plugin version)
"params": [
{ "name": "tty_dbg", "type": "String"},
{ "name": "tty_bt", "type": "String"},
{ "name": "firmware", "type": "String", "default": "NUL"}
],
// Plugin outer ports
"ports": [ ],
// Plugin internal buses
"buses": [
{ "name": "mem", "size": "BUS30" },
{ "name": "nand", "size": "4" },
{ "name": "gpio", "size": "BUS32" }
],
// Plugin internal components
"modules": [
{
"name": "u1_stm32",
"plugin": "STM32F042",
"library": "mcu",
"params": {
"firmware:String": "params.firmware"
}
},
{
"name": "usart_debug",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_dbg"
}
},
{
"name": "term_bt",
"plugin": "UartSerialTerminal",
"library": "terminals",
"params": {
"tty": "params.tty_bt"
}
},
{
"name": "bluetooth",
"plugin": "BT",
"library": "mcu"
},
{ "name": "led_0", "plugin": "LED", "library": "mcu" },
{ "name": "led_1", "plugin": "LED", "library": "mcu" },
{ "name": "led_2", "plugin": "LED", "library": "mcu" },
{ "name": "led_3", "plugin": "LED", "library": "mcu" },
{ "name": "led_4", "plugin": "LED", "library": "mcu" },
{ "name": "led_5", "plugin": "LED", "library": "mcu" },
{ "name": "led_6", "plugin": "LED", "library": "mcu" },
{ "name": "led_7", "plugin": "LED", "library": "mcu" },
{ "name": "led_8", "plugin": "LED", "library": "mcu" },
{ "name": "led_9", "plugin": "LED", "library": "mcu" },
{ "name": "led_10", "plugin": "LED", "library": "mcu" },
{ "name": "led_11", "plugin": "LED", "library": "mcu" },
{ "name": "led_12", "plugin": "LED", "library": "mcu" },
{ "name": "led_13", "plugin": "LED", "library": "mcu" },
{ "name": "led_14", "plugin": "LED", "library": "mcu" },
{ "name": "led_15", "plugin": "LED", "library": "mcu" }
],
// Plugin connection between components
"connections": [
[ "u1_stm32.ports.usart1_m", "usart_debug.ports.term_s"],
[ "u1_stm32.ports.usart1_s", "usart_debug.ports.term_m"],
[ "u1_stm32.ports.usart2_m", "bluetooth.ports.usart_m"],
[ "u1_stm32.ports.usart2_s", "bluetooth.ports.usart_s"],
[ "bluetooth.ports.bt_s", "term_bt.ports.term_m"],
[ "bluetooth.ports.bt_m", "term_bt.ports.term_s"],
[ "led_0.ports.pin", "u1_stm32.buses.pin_output_a", "0x00"],
[ "led_1.ports.pin", "u1_stm32.buses.pin_output_a", "0x01"],
[ "led_2.ports.pin", "u1_stm32.buses.pin_output_a", "0x02"],
[ "led_3.ports.pin", "u1_stm32.buses.pin_output_a", "0x03"],
[ "led_4.ports.pin", "u1_stm32.buses.pin_output_a", "0x04"],
[ "led_5.ports.pin", "u1_stm32.buses.pin_output_a", "0x05"],
[ "led_6.ports.pin", "u1_stm32.buses.pin_output_a", "0x06"],
[ "led_7.ports.pin", "u1_stm32.buses.pin_output_a", "0x07"],
[ "led_8.ports.pin", "u1_stm32.buses.pin_output_a", "0x08"],
[ "led_9.ports.pin", "u1_stm32.buses.pin_output_a", "0x09"],
[ "led_10.ports.pin", "u1_stm32.buses.pin_output_a", "0x0A"],
[ "led_11.ports.pin", "u1_stm32.buses.pin_output_a", "0x0B"],
[ "led_12.ports.pin", "u1_stm32.buses.pin_output_a", "0x0C"],
[ "led_13.ports.pin", "u1_stm32.buses.pin_output_a", "0x0D"],
[ "led_14.ports.pin", "u1_stm32.buses.pin_output_a", "0x0E"],
[ "led_15.ports.pin", "u1_stm32.buses.pin_output_a", "0x0F"]
]
}注意參數 固件 部分 PARAMS — 是可以作為韌體載入到虛擬設備中的檔案的名稱。
虛擬設備及其與主作業系統的互動可以用下圖來表示:

模擬器的目前測試實例涉及與主作業系統的 COM 連接埠(調試 UART 和藍牙模組的 UART)的交互作用。這些可以是設備連接的真實連接埠或虛擬 COM 連接埠(這是您需要的 com0com/socat).
目前從外部與模擬器互動的方式主要有兩種:
- GDB RSP協定(分別支援該協定的工具有Eclipse/IDA/radare2);
- 內部模擬器命令列(Argparse 或 Python)。
虛擬 COM 連接埠
為了透過終端與本機上的虛擬設備的 UART 進行交互,需要建立一對連結的虛擬 COM 連接埠。在我們的例子中,一個連接埠由模擬器使用,第二個連接埠由終端程式(PuTTY 或螢幕)使用:

使用 com0com
虛擬 COM 連接埠使用 com0com 套件中的設定公用程式進行設定(控制台版本 - C:\Program Files (x86)\com0\comsetupс.exe, 或 GUI 版本 - C:\Program Files (x86)\com0\comsetupg.exe):

應勾選以下複選框 啟用緩衝區溢出 對於所有建立的虛擬端口,否則模擬器將等待來自 COM 端口的回應。
使用 socat
在 UNIX 系統上,模擬器使用 socat 公用程式自動建立虛擬 COM 連接埠。為此,只需在啟動模擬器時指定連接埠名稱中的前綴。 socat:.
內部命令列介面(Argparse 或 Python)
由於 Kopycat 是一個控制台應用程序,因此該模擬器提供了兩個用於與其物件和變數交互的命令列介面選項:Argparse 和 Python。
Argparse 是 Kopycat 內建的 CLI,可供所有人使用。
另一個 CLI 是 Python 解釋器。要使用它,您需要安裝 Jep Python 模組並配置模擬器以與 Python 一起工作(將使用安裝在使用者主系統上的 Python 解釋器)。
安裝 Jep Python 模組
下 Linux Jep 可以透過 pip 安裝:
pip install jep安裝 Jep Windows 必須預先安裝 Windows SDK 和對應的 Microsoft Visual Studio。我們為您簡化了一些操作。 適用於目前 Python 版本的 JEP Windows因此,該模組可以透過檔案進行安裝:
pip install jep-3.8.2-cp27-cp27m-win_amd64.whl要檢查 Jep 的安裝,需要在命令列中執行:
python -c "import jep"作為回應,應該收到以下訊息:
ImportError: Jep is not supported in standalone Python, it must be embedded in Java.在您的系統的模擬器命令檔中(複製貓腳本 - 為了 Windows, 科皮貓 - 為了 Linux)到參數列表 DEFAULT_JVM_OPTS 新增附加參數 Djava.library.path - 它必須包含已安裝的 Jep 模組的路徑。
因此, Windows 你應該會看到類似這樣的訊息:
set DEFAULT_JVM_OPTS="-XX:MaxMetaspaceSize=256m" "-XX:+UseParallelGC" "-XX:SurvivorRatio=6" "-XX:-UseGCOverheadLimit" "-Djava.library.path=C:/Python27/Lib/site-packages/jep"啟動 Kopycat
該模擬器是一個控制台 JVM 應用程式。啟動是透過作業系統命令列腳本(sh/cmd)進行的。
運行命令 Windows:
binkopycat -g 23946 -n rhino -l user -y library -p firmware=firmwarerhino_pass.bin,tty_dbg=COM26,tty_bt=COM28運行命令 Linux 使用 socat 工具:
./bin/kopycat -g 23946 -n rhino -l user -y library -p firmware=./firmware/rhino_pass.bin, tty_dbg=socat:./COM26,tty_bt=socat:./COM28-g 23646— 將開啟用於存取 GDB 伺服器的 TCP 連接埠;-n rhino— 系統主模組(組裝設備)的名稱;-l user— 要搜尋主模組的庫的名稱;-y library— 搜尋設備中所包含的模組的路徑;firmwarerhino_pass.bin— 韌體檔案的路徑;- COM26 和 COM28 是虛擬 COM 連接埠。
結果將顯示邀請。 Python > (或者 Argparse >):
18:07:59 INFO [eFactoryBuilder.create ]: Module top successfully created as top
18:07:59 INFO [ Module.initializeAndRes]: Setup core to top.u1_stm32.cortexm0.arm for top
18:07:59 INFO [ Module.initializeAndRes]: Setup debugger to top.u1_stm32.dbg for top
18:07:59 WARN [ Module.initializeAndRes]: Tracer wasn't found in top...
18:07:59 INFO [ Module.initializeAndRes]: Initializing ports and buses...
18:07:59 WARN [ Module.initializePortsA]: ATTENTION: Some ports has warning use printModulesPortsWarnings to see it...
18:07:59 FINE [ ARMv6CPU.reset ]: Set entry point address to 08006A75
18:07:59 INFO [ Module.initializeAndRes]: Module top is successfully initialized and reset as a top cell!
18:07:59 INFO [ Kopycat.open ]: Starting virtualization of board top[rhino] with arm[ARMv6Core]
18:07:59 INFO [ GDBServer.debuggerModule ]: Set new debugger module top.u1_stm32.dbg for GDB_SERVER(port=23946,alive=true)
Python >與 IDA Pro 交互
作為在 IDA 中進行分析的來源文件,為了簡化測試,我們使用以下形式的 Rhino 韌體 (元資訊儲存在那裡)。
您也可以使用沒有元資訊的基礎韌體。
在 IDA Pro 中啟動 Kopycat 後,在調試器選單中,轉到“切換調試器...“並選擇”遠端 GDB 偵錯工具「接下來,我們設定連線:選單 調試器 - 處理選項...
我們設定以下值:
- 應用程式-任意值
- 主機名稱:127.0.0.1(或執行 Kopycat 的遠端機器的 IP 位址)
- 端口:23946的

現在調試開始按鈕可用(F9鍵):
![]()
我們按下它 - 就會與模擬器中的調試器模組建立連接。 IDA 進入偵錯模式,附加視窗變得可用:有關暫存器、有關堆疊的資訊。
現在我們可以使用調試器的所有標準功能:
- 逐步說明(步入 и 跨過 — 分別為鍵 F7 和 F8);
- 開始和停止執行;
- 在程式碼和資料上建立斷點(F2 鍵)。
連接到調試器並不意味著運行韌體程式碼。執行的目前位置必須是位址 0x08006A74 — 函數的開頭 重置處理程序。如果向下捲動列表,您可以看到函數調用 主。您可以將遊標放在此行(位址 0x08006ABE)並執行操作 運行直到遊標 (F4 鍵)。

接下來你可以按F7進入功能 主.
如果您運行命令 繼續該過程 (F9 鍵),然後會出現「請等待」窗口,其中有一個按鈕 暫停:

當您按下 暫停 韌體程式碼執行被暫停,並且可以從中斷的程式碼中的相同位址繼續執行。
如果繼續執行程式碼,您將在連接到虛擬 COM 連接埠的終端機中看到以下幾行:


「statebypass」這一行的存在表示虛擬藍牙模組已切換到從使用者的COM連接埠接收資料的模式。
現在您可以根據Rhino協定在藍牙終端機(圖中-COM29)輸入命令。例如命令“MEOW”會向藍牙終端返回字串“mur-mur”:

不完全模仿我
建立模擬器時,您可以選擇特定設備的細節/模擬等級。例如,藍牙模組可以透過不同的方式模擬:
- 該設備已完全模擬,並具有全套命令;
- 模擬AT指令,從主系統的COM口接收資料流;
- 虛擬設備提供資料到真實設備的完全重定向;
- 作為一個始終返回“OK”的簡單存根。
目前版本的模擬器採用第二種方式:虛擬藍牙模組進行配置,然後切換到從主系統的COM連接埠到模擬器的UART連接埠的資料「代理」模式。

讓我們考慮一下如果部分外圍設備未實現時進行簡單程式碼檢測的可能性。例如,如果沒有建立負責監控 DMA 資料傳輸的定時器(檢查在函數中執行) ws2812b_等待,位於 0x08006840),那麼韌體將一直等待標誌被重置 忙碌,位於地址 0x200004C4,顯示了 DMA 資料線的佔用情況:

我們可以透過手動重置標誌來解決這種情況。 忙碌 安裝後立即。在 IDA Pro 中,您可以建立一個 Python 函數並在斷點處呼叫它,而斷點本身則在將值 1 寫入標誌後放置在程式碼中 忙碌.
斷點處理程序
首先,讓我們在 IDA 中建立一個 Python 函數。選單 檔案 — 腳本指令...
在左側列表中新增新的程式碼片段,並為其命名(例如, BPT),
在右側的文字欄位中輸入功能代碼:
def skip_dma():
print "Skipping wait ws2812..."
value = Byte(0x200004C4)
if value == 1:
PatchDbgByte(0x200004C4, 0)
return False 
之後,按 運行 並關閉腳本視窗。
現在我們來看看地址處的程式碼 0x0800688A,設定斷點(F2 鍵),編輯它(上下文選單 編輯斷點...),別忘了將腳本類型設為Python:


如果標誌的當前值 忙碌 等於 1,則應該執行該函數 skip_dma 在腳本行中:

如果你運行固件,你可以在 IDA 視窗中看到斷點處理程式碼觸發 輸出 按行 Skipping wait ws2812...。現在韌體將不再等待標誌被重置 忙碌.
與模擬器交互
為了模仿而模仿不太可能帶來喜悅和歡樂。如果模擬器可以幫助研究人員查看記憶體中的資料或建立線程交互,那將會更有趣。
讓我們展示如何動態建立 RTOS 任務之間的互動。如果程式碼正在運行,您應該先暫停程式碼的執行。如果你去函數 藍牙任務入口 到“LED”指令的處理分支(地址 0x080057B8),然後你就可以看到首先創建了什麼,然後發送到系統隊列 ledControlQueueHandle 一些資訊。

應該在變數引用上設定斷點。 ledControlQueueHandle,位於 0x20000624 並繼續執行程式碼:

因此,停止將首先發生在地址 0x080057CA 在呼叫函數之前 osMailAlloc,然後 - 到地址 0x08005806 在呼叫函數之前 osMailPut,然後過一段時間 - 到地址 0x08005BD4 (在呼叫函數之前 osMailGet),屬於函數 leds_task_entry (LED 任務),也就是說,任務進行了切換,現在 LED 任務接收控制。

透過這種簡單的方法,您可以確定 RTOS 任務如何相互互動。
當然,在現實中,任務互動可能更加複雜,但使用模擬器可以減少追蹤這些互動的勞動強度。
您可以觀看模擬器啟動和與 IDA Pro 互動的簡短影片。
使用 Radare2 啟動
您不能忽視像 Radare2 這樣的通用工具。
要使用 r2 連接到模擬器,命令將如下所示:
radare2 -A -a arm -b 16 -d gdb://localhost:23946 rhino_fw42k6.elf現已推出 (dc) 並暫停執行 (Ctrl+C)。
不幸的是,目前 r2 的硬體 gdb 伺服器和記憶體佈局存在問題,因此斷點和步驟不起作用(命令 ds)。我們希望這個問題能盡快解決。
使用 Eclipse 啟動
使用模擬器的選項之一是調試正在開發的設備的韌體。為了清楚起見,我們還將使用 Rhino 韌體。您可以下載韌體原始碼 .
我們將使用 Eclipse 作為 IDE .
為了將直接在 Eclipse 中編譯的韌體載入到模擬器中,需要添加參數 firmware=null 在模擬器啟動命令中:
binkopycat -g 23946 -n rhino -l user -y modules -p firmware=null,tty_dbg=COM26,tty_bt=COM28設定調試配置
在 Eclipse 中,選擇選單 運行 - 調試配置... 在打開的視窗中,在部分 GDB硬體偵錯 您需要新增一個新的配置,然後在「Main」標籤上指定目前要偵錯的項目和應用程式:

在「偵錯器」標籤上,您需要指定 GDB 命令:
${openstm32_compiler_path}arm-none-eabi-gdb
並輸入連接GDB伺服器的參數(主機和連接埠):

在「啟動」標籤上,必須指定以下參數:
- 啟用複選框 載入圖片 (以便將編譯好的韌體映像載入到模擬器中);
- 啟用複選框 載入符號;
- 新增啟動命令:
set $pc = *0x08000004(將位址處記憶體中的值設定到 PC 暫存器0x08000004— 地址儲存在那裡 ResetHandler'a).
請注意,如果不想從Eclipse下載韌體文件,那麼參數 載入圖片 и 運行命令 無需指定。

按下“調試”後,您可以在調試器模式下工作:
- 一步步執行程式碼

- 與斷點交互

注意. Eclipse 有一些怪癖...你必須忍受它們。例如,如果啟動偵錯器時出現訊息“沒有可用於“0x0″的來源”,則執行 Step 命令(F5)

取而代之的是結論
模擬本機程式碼是一件非常有趣的事情。對於設備開發人員來說,無需真實設備即可調試韌體。對於研究人員來說,這是一個進行動態程式碼分析的機會,即使使用設備也不總是能夠實現。
我們希望為專家提供一種方便、適度簡單且不需要花費太多時間和精力來設定和啟動的工具。
在評論中寫下您使用硬體模擬器的經驗。我們邀請您參與討論,並很樂意回答您的問題。
只有註冊用戶才能參與調查。 , 請。
您使用模擬器來做什麼?
我開發(調試)韌體
我正在研究韌體
我推出遊戲(Dendi、Sega、PSP)
其他內容(寫在評論中)
7 位用戶投票。 2 名用戶棄權。
您使用什麼軟體來模擬本機程式碼?
QEMU
獨角獸引擎
變形
其他內容(寫在評論中)
6 位用戶投票。 2 名用戶棄權。
您希望改進所使用的模擬器的哪些方面?
我想要速度
我希望設定/啟動更加便捷
我想要更多與模擬器互動的選項(API、鉤子)
我對一切都很滿意
其他內容(寫在評論中)
8 位用戶投票。 1 位用戶棄權。
來源: www.habr.com


