當你手上有一個固件?或許是從晶片上提取出來的亦或是從網上下載的更新文件。接下來你會怎麼做呢?
獲取到固件,然後提取出其中的有效信息,這可不是一個簡單輕而易舉的過程。有時,你不得不面對一些專有的文件格式(幾乎沒有任何參考文檔),奇怪的原始數據 quirks,甚至是加密的數據。
接下來,就讓我們來了解了解從固件中獲取有效信息的一些策略。
1、結合使用場景
充分了解當前正在處理的文件的使用場景(環境),對後續的分析是很有幫助的。你需要搞清楚支持該固件運行的晶片型號,晶片架構,大端模式還是小端模式,固件上運行的系統是 RTOS 還是 Linux ?或者是純裸機?等等問題。
Context will help 。。。
2、Binary還是ASCII
So,文件裡面到底是 ASCII 字符串還是二進位數據呢?用 head,cat,hexdump這些命令亦或是你最喜歡的 GUI 文本編輯器去查看這些文件的類型。
如果你的目標設備是運行在裸機上的,那麼你可能會看到這些固件文件分散分布在眾多的十六進位文本中(也經常前綴有區分碼或者偏移地址/絕對地址,同時有可能在每一行後綴有校驗和)。在你對這些文件進行進一步分析前,首先需要將他們整理成二進位文件格式。接下來,我們來學習了解一些常見的二進位文件格式。
>>>>2.1 Motorola S-Record
通常,該文件格式也稱為 SREC。所有 S-record 文件,每行都是大寫字母S開頭。關於該二進位格式的詳細信息,參考[連結]。
>>>>2.2 Intel HEX
類似 SREC , [ Intel HEX ] 每行都是以一個冒號(:)開頭。
>>>>2.3 TI-TXT
TI-TXT 是德州儀器(Texas Instruments)公司開發的格式,常用於 MSP430 系列單片機。該格式以符號@作為地址前綴,而數據是以十六進位存儲的,整個文件的看起來如下:
所有 Motorola S-record,Intel HEX 和 TI-TXT 文件都可以用[ bincopy python library ]轉換成二進位文件。
>>>>2.4 Raw NAND dumps
咋一看,數據是以一種奇怪的方式存儲在 NAND 晶片上的。可是,當你想在對晶片上的數據進行預處理時,你會稱讚該設計的智慧和實用。
從上圖可以看到,Out-of-band(OOB)「空閒」區塊被插入到每頁數據的末端,或者是每塊數據塊的末端。這些空白的存儲區塊被主控用於跟蹤壞塊,擦除計數等等。所以,當你整塊晶片地 dump 原始數據的時候,這些「空白的」區塊也會 dump 下來。
這意味著你需要將所有這些「空閒」區塊剔除掉才能從晶片的 dump 中獲得連續的真實文件。這樣,你就可以開始用下述的策略對這些二進位文件進行深入地分析了。
3、你提取到了二進位文件?這並不值得十分興奮
一旦你得到了二進位格式的固件,你就可以嘗試從中分析出一些有趣的信息。
再次提醒,多結合固件的使用場景對進一步分析是十分有益的。如果你了解到該固件是用在某個具體型號的 MCU 上,那麼你可能會查找對應的 datasheet 然後直接將固件載入 IDA 進行分析。如果你知道該固件是用在裸機晶片上的,但是你沒有該晶片的 datasheet,那麼你可能會做一些位元組級別的分析工作。如果該固件是用在一個更加複雜的系統,例如像 Linux 這樣的作業系統,那麼你很可能需要將文件提取出來。
無論如何,下面的這些工具還是很好用的。
>>>>3.1 strings
strings是提取文件初始底層數據的強有力工具。它返回的是一列由null字符結尾的可列印字符串。strings命令遠比大多數人了解到的強悍。一條基本的命令strings file.bin將返回 ASCII/ISO 4 字符串。稍微複雜一點的是:
strings -n16 file.bin
默認的字符串最小長度為 4 。-n選項用於申明返回的字符串的具體長度。上面例子將輸出長度不短於 16 的字符串到 stdout 。
strings -el file.bin
strings的 -e選項明確了字符編碼。-el指定字符編碼為小端模式,16 位寬。如果二進位文件是大端編碼的,那麼用 -eb 。16-bit 位寬編碼常見於嵌入式視窗系統的固件中。
strings -tx file.bin
-t選項用於返迴文件中字符串的偏移。-tx則返回十六進位格式,-to則返回八進位(octal),-td則返回十進位(decimal)。這將非常有用當你在 十六進位編輯器中跨越引用時,或者你只是簡單地像知道字符串在文件中的位置。
配合 -n 和 -t 選項,strings 命令的輸出效果類似於下圖:
$ strings -n16 -tx file.bin
de1d73 vl1T-W4m% ]e7 ^")
14b3b12 K,E>$r!!qxc` a~S
15715a8 hX3Y@<-Gb$r+G9[j
19717f0 hg9Dfs[31+.|~#y*4
3a223b5 v?_-=jO ?0n>#@[D
417fec4 s]pD(6X#_tD&-NN-
47667a1 dAsMJjD#=+x'LG44d55401 =GKw]I6VCDuTGvsv
511ad94 HelloFirmwareFans
53ef9cc %z.rkn'-z:gVUUl1-i
548b9e0 oelinux123
5d1c7cf P~7^SLD0njEo:ALa+
字符串的偏移,是以十六進位格式顯示在每行的行首。
>>>>3.2 hexdump
用 hexdump對二進位文件進行分析,該命令將會向 stdout 輸出該文件中每一位元組的十六進位形式。所以,該命令其實就是一個 hex(十六進位) dump 工具。
據我所知(了解),大多數人常用 -C 選項,該選項的結果是:以單位元組方式返迴文件數據,同時,添加一行顯示可列印字符(若為不可列印字符,則代之以 stop 字符)。因此,藉助 hexdump可以很方便地獲取二進位文件中的 字符串以及對該文件的總體輪廓。
hexdump還通過 * 來指代重複行。但是,假如你希望清楚地看到每一行輸出的內容,那麼可以通過 -v 選項來取消這一項功能。
-n選項用來限制返回的位元組數目。在下述例子中,hexdump 的-C 選項返回 file.bin 文件 0x200 位元組的數據。
>>>>3.3 file
用 file 命令檢測提取出的固件(包括 binwalk 、 dd 提取出的任何文件)往往能收到不錯的效果。file 是通過檢測文件頭部(即所給定文件的前面幾個位元組)的 magic bytes 工作的。
無法界定的文件類型會被標記為「data(數據)」。任何可區別的文件都會被標記為 file認定的文件格式,同時, file 也會給出相關元數據。下面的示例是 file 對一幅 JPEG 圖片的解析:
提取出來的固件有時候是包含大量各種類型文件的不規則文件塊。亦或是加密的以完全隨機的位元組序列開始,這可能使得它的 magic bytes 與合法文件格式相符或不符。在這種情況下,我們得到的結果將會是個錯誤的方向。
PDP-11 UNIX/RT ldp 是什麼文件格式?我真的不清楚,不過可以肯定的是,這不是固件。這個結果反映的只是該文件的前幾個位元組與 PDP-11 UNIX/RT ldp 文件相符。
在文件夾目錄下運行 file * 是一個快速而高效的做法。舉例來說,在 binwalk 輸出目錄下運行該命令可以快速看到我們將要處理的文件類型。例如, binwalk 可能找到(並成功提取出)了一個 JFFS2 文件系統,以及其他一些東西。運行該命令, binwalk 輸出目錄的內容可能如下:
$ file *
2042C4: data
800000.jffs2: Linux jffs2 filesystem data little endian
jffs2-root: directory
>>>>3.4 binwalk
binwalk 是一款可靠且很受歡迎的針對運行有作業系統的設備的固件分析工具。關於這方面,網上的討論不計其數。不過,更為重要的是,並不要僅僅認為 binwalk 只是徹頭徹腦的一個固件分析工具而已;這只不過是它極其有用而又簡單的一方面功能。
從一個更高層次來說,在默認情況下 binwalk 循環疊代地搜索整個二進位文件,檢索 magic bytes。如果找到,就把它以表格的形式通過 stdout 列印出來。
binwalk 可以分塊的形式提取出被分析的二進位文件數據,這樣的話,就可以獨立單個地研究這些數據塊了。 - e 選項聲明將數據提取出來,而非輸出到 stdout 。提取出來的文件存放於 _filename.extracted ( 或_filename-[int].extracted ,如果前者已存在)。
由於 binwalk的本質特徵,你很有可能會進入錯誤的方向。分析的文件大小越大,越有可能出錯。這是因為,在偶然情況下,一個普通的文件也可能會有 binwalk 能解析的 magic bytes 。在這種情況下,就意味著 binwalk 報告的結果是無效的。
所以,當你在用 binwalk 分析文件時,你應該大概清楚它可能的結果。舉例來說,如果你分析的設備運行的是嵌入式 Linux 系統,那麼它的 ROM 文件系統不外乎 squashfs,cramfs 或 jffs2 這些。zImage 或 uImage 也是有可能的。另外,也可能是 bootloader 鏡像。
下面是運行 binwalk 分析一個大的加密固件的例子。因為是加密文件,這種簡單的方式本來是不可能分析出有價值的信息來的,但 binwalk 卻返回了一些可能的結果。
很顯然,這是錯誤的。返回這個結果,純粹是加密文件的某些位元組序列與這些文件類型的 magic bytes 碰巧相符而已。另外,這些「文件」的偏移地址隨意分布,使得他們看起來更不像是合法的文件類型。
binwalk 正常的、有意義的輸出結果大概是這樣的:
從結果可以看到,該文件包含一個 uImage(大小、名稱與 entry point 基本一致,) 和 JFFS2 文件系統。因為內核通常是經過 gzip 壓縮的,所以緊接著內核頭之後的 gzip magic bytes 也說得通。內核和文件系統兩者都是啟動嵌入式 linux 系統必須的。更加巧妙的是,這兩者的偏移地址( 0x200000 和 0x800000 )都十分整齊。儘管在實際情況中,這基本不可能,但是,在這裡也只是為了強調效果。
>>>>3.5 fdisk
有時,碰到的設備是相當高端的產品,而且它的固件文件很大 -- 往往是整個的驅動鏡像。這通常是以為該設備具有豐富的資源,處於向桌面平台發展的過程中。試試 fdisk -l 或 fdisk -lu ( -u 選項指定以 segment(段) 標記分區大小,而不是默認的 「cylinders(柱面)」)。
這是一個不含有效磁碟鏡像的文件的 fdisk 輸出:
$ fdisk -l file.bin
Disk file.bin: 104.3 MiB, 109407232 bytes, 213686 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
而包含有效文件系統的輸出則與下面相似:
$ fdisk -l file.bin
Disk file.bin: 2.6 GiB, 2751447040 bytes, 5373920 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xcd42b400
Device Boot Start End Sectors Size Id Type
file.binp1 3892371390 4109164418 216793029 103.4G 72 unknown
file.binp2 3287936629 3304577640 16641012 8G 6 FAT16
從這裡可以看到,一個 FAT16 鏡像位於 3287936629到 3304577640 之間。鏡像大小是 8 GB。
利用這些數據,我們可以用 dd (稍後會詳細講解)從固件把該文件系統提取出來。一旦將文件系統提取出來了,就可以用合適 mount 命令將它恰當地掛載到系統上。掛載文件系統是另一門額外的高深學問 ,另寫一整篇文章來討論它也不為過。不過,根據 fdisk 判定的文件系統類型,結合網絡,搜索到合適的掛載命令/工具,成功掛載文件系統也不是很難的事情。
另外,特別說明一下。有些情況下,在一個非原生系統上你需要加載內核模塊(QNX 系統尤其要注意)。
如果得到的文件包含多種文件系統,想要把這些文件一次性提取出來,可以用下面一行命令:
fdisk -lu file.bin | egrep -i
'file.bin[0-9]' | sed 's/ */
/g' | while read line; do dd
if=file.bin of=$(echo $line|
cut -d' ' -f1) skip=$(echo
$line | cut -d' ' -f2)
count=$(echo $line | cut -d'
' -f4); done
>>>>3.6 dd
說到 dd 命令,很多人可能害怕它。的確,使用不當的話確實有可能把磁碟廢掉。不過,這也僅僅是當你把 of 設置成錯誤的路徑了(譯者註:dd 一般的用法是提取某處的文件,輸出到另外一個位置。of 是指 output file,輸出路徑,如果把 of =/boot 就會將提取的文件輸出到 /boot 下,從而覆蓋系統原來的啟動文件,導致無法正常啟動)。使用中絕對要小心。
即便如此, dd 依然是一個極其簡單高效的位元組級複製工具。尤其是在支持「dollar bracket bracket」語法的 Shell 中。這在將十六進位轉化成十進位以及在處理不同文件塊大小等基本算術運算中十分有用。
dd 的幾個必須知道的關鍵參數有:
if = [ FILE ]
dd 的輸入文件從這裡讀取。
of = [ FILE ]
dd 的文件輸出位置。
bs = [ NUMBER ]
塊大小。dd 所有的數值參數都將以該大小的倍數計算。默認的塊大小為 512 。如果你不介意運行的緩慢,煩人的數學運算的話,設置成 bs=1 也是可以。
skip = [ NUMBER ]
讀取輸入文件前跳過的塊。
count = [ NUMBER ]
從輸入文件拷貝到輸出的總的塊數目。
所以,當我們想從 firmware.bin 中提取 0x200 到 0x400 這一塊時:
記住, dd 是對按塊對文件操作的。因此,在計算具體的塊大小,塊數目時,需要特別精確。如果你想確保安全,可以設置塊大小 bs = 1 。
4、GUI hex 文本編輯器
當事情更進一步時,為了更具可視化,你可能需要一款 GUI hex 文本編輯器來幫助固件分析。到目前為止,我還沒有找到一款完全符合我需求的編輯器。我理想中的 hex 文本編輯器應該是這樣的:能夠可視化二進位文件,辨別出常見文件中的有效位元組,可視化信息熵,包括位元組碼。支持文本標記和高亮,列出字符串等等。不過,不管怎樣,適合自己的就是最好的。
日復一日,我傾向於用 HxD 做基本的單調性工作,wxHexEditor 來做文本標記(note-taking)和高亮顯示任務。例如:
除了這兩款外,還有眾多的 hex 文本編輯器,所以選擇那一款只是個人喜好問題。不妨多試試,體驗一下它們的差別。
(上篇 完)