加密韌體之依據老韌體進行解密

IoT漏洞分析最為重要的環節之一就是獲取韌體以及韌體中的檔案系統。韌體獲取的方式也五花八門,硬核派有直接將flash拆下來到程式設計器讀取,透過硬體偵錯程式UART/SPI、JTAG/SWD獲取到控制檯訪問;網路派有中間人攻擊攔截OTA升級,從製造商的網頁進行下載;社工派有假裝研究者(學生)直接向客服索要,上某魚進行PY。有時候千辛萬苦獲取到韌體了,開開心心地使用

binwalk -Me

一把梭,卻發現,韌體被加密了,驚不驚喜,刺不刺激。

如下就是針對如何對加密韌體進行解密的其中一個方法:回溯未加密的老韌體,從中找到負責對韌體進行解密的程式,然後解密最新的加密韌體。此處做示範使用的裝置是前幾天爆出存在漏洞的路由器D-Link DIR 3040 US,韌體使用的最新加密版本

1。13B03

,老韌體使用的是已經解密韌體版本

1。13B02

判斷韌體是否已經被加密

一般從官網下載到韌體的時候,是先以zip等格式進行了一次壓縮的,通常可以先正常解壓一波。

$ tree -L 1。├── DIR3040A1_FW112B01_middle。bin├── DIR3040A1_FW113B03。bin└── DIR-3040_REVA_RELEASE_NOTES_v1。13B03。pdf

使用binwalk檢視一下韌體的資訊,如果是未加密的韌體,通常可以掃描出來使用了何種壓縮演算法。以常見的嵌入式檔案系統squash-fs為例,比較常見的有LZMA、LZO、LAMA2這些。如下是使用binwalk分別檢視一個未加密韌體(netgear)和加密韌體(DIR 3040)資訊。

$ binwalk GS108Tv3_GS110TPv3_GS110TPP_V7。0。6。3。bix DECIMAL HEXADECIMAL DESCRIPTION————————————————————————————————————————64 0x40 LZMA compressed data, properties: 0x5D, dictionary size: 67108864 bytes, uncompressed size: -1 bytes

$ binwalk DIR3040A1_FW113B03。bin DECIMAL HEXADECIMAL DESCRIPTION————————————————————————————————————————

還有一種方式就是檢視韌體的熵值。熵值是用來衡量不確定性,熵值越大則說明韌體越有可能被加密或者壓縮了。這個地方說的是被加密或者壓縮了,被壓縮的情況也是會讓熵值變高或者接近1的,如下是使用

binwalk -E

檢視一個未加密韌體(RAX200)和加密韌體(DIR 3040)。可以看到,RAX200和DIR 3040相對比,不像後者那樣直接全部是接近1了。

加密韌體之依據老韌體進行解密

加密韌體之依據老韌體進行解密

找到負責解密的可執行檔案

接下來是進入正軌了。首先是尋找到老韌體中負責解密的可執行檔案。基本邏輯是先從HTML檔案中找到顯示升級的頁面,然後在伺服器程式例如此處使用的是lighttpd中去找到何處進行了呼叫可執行檔案下載新韌體、解密新韌體,這一步也可能是發生在呼叫的CGI中。

使用find命令定位和升級相關的頁面。

$ find 。 -name “*htm*” | grep -i “firmware”。/etc_ro/lighttpd/www/web/MobileUpdateFirmware。html。/etc_ro/lighttpd/www/web/UpdateFirmware。html。/etc_ro/lighttpd/www/web/UpdateFirmware_e。html。/etc_ro/lighttpd/www/web/UpdateFirmware_Multi。html。/etc_ro/lighttpd/www/web/UpdateFirmware_Simple。html

然後現在後端lighttpd中去找相關字串,似乎沒有結果呢,那麼猜測可能發生在CGI中。

$ find 。 -name “*httpd*” | xargs strings | grep “firm”strings: Warning: ‘。/etc_ro/lighttpd’ is a directory

從CGI程式中查詢,似乎運氣不錯,,,直接就定位到了,結果過多就只展示了最有可能的結果。Bingo!似乎已經得到了解密韌體的程式,img、decrypt。

$ find 。 -name “*cgi*” | xargs strings | grep -i “firm”/bin/imgdecrypt /tmp/firmware。img

模擬並解密韌體

拿到了解密程式,也知道解密程式是怎麼輸入引數執行的,這個時候可以嘗試對直接使用qemu模擬解密程式跑起來,直接對韌體進行解密。最好保持解密可執行檔案在老版本韌體檔案系統的位置不變,因為不確定是否使用相對或者絕對路徑引用了什麼檔案,例如解密公私鑰。

先檢視可執行檔案的執行架構,然後選擇對應qemu進行模擬。

$ file bin/imgdecryptbin/imgdecrypt: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc。so。0, stripped$ cp $(which qemu-mipsel-static) 。/usr/bin$ sudo mount -t proc /proc proc/$ sudo mount ——rbind /sys sys/$ sudo mount ——rbind /dev/ dev/

$ sudo chroot 。 qemu-mipsel-static /bin/shBusyBox v1。22。1 (2020-05-09 10:44:01 CST) built-in shell (ash)Enter ‘help’ for a list of built-in commands。/ # /bin/imgdecrypt tmp/DIR3040A1_FW113B03。binkey:C05FBF1936C99429CE2A0781F08D6AD8/ # ls -a tmp/。。 。firmware。orig 。 DIR3040A1_FW113B03。bin/ #

那麼就解壓出來了,解壓到了tmp資料夾中,。firmware。orig檔案。這個時候使用binwalk再次進行檢視,可以看到已經被成功解密了。

$ binwalk 。firmware。origDECIMAL HEXADECIMAL DESCRIPTION————————————————————————————————————————0 0x0 uImage header, header size: 64 bytes, header CRC: 0x7EA490A0, created: 2020-08-14 10:42:39, image size: 17648005 bytes, Data Address: 0x81001000, Entry Point: 0x81637600, data CRC: 0xAEF2B79F, OS: Linux, CPU: MIPS, image type: OS Kernel Image, compression type: lzma, image name: “Linux Kernel Image”160 0xA0 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 23083456 bytes1810550 0x1BA076 PGP RSA encrypted session key - keyid: 12A6E329 67B9887A RSA (Encrypt or Sign) 1024b14275307 0xD9D2EB Cisco IOS microcode, for “z”

加解密邏輯分析(重點)

關於韌體安全開發到釋出的一般流程

如果要考慮到韌體的安全性,需要解決的一些痛點基本上是:

機密性:透過類似官網的公開渠道獲取到解密後的韌體

完整性:攻擊者劫持升級渠道,或者直接將修改後的韌體上傳到裝置,使韌體升級

對於機密性,從韌體的源頭、傳輸渠道到裝置三個點來分析。首先在源頭,官網上或者官方TFP可以提供已經被加密的韌體,裝置自動或手動檢查更新並從源頭下載,下載到裝置上後進行解密。其次是渠道,可以採用類似於HTTPS的加密傳輸方式來對韌體進行傳輸。但是前面兩種方式終歸是要將韌體下載到裝置中。

如果是進行簡單的加密,很常見的一種方式,尤其是對於一些低端嵌入式韌體,通常使用了硬編碼的對稱加密方式,例如AES、DES之類的,還可以基於硬編碼的字串進行一些資料計算,然後作為解密金鑰。這次分析的DIR 3040就是採用的這種方式。

對於完整性,開發者在一開始可以透過基於自簽名證書來實現對韌體完整性的校驗。開發者使用私鑰對韌體進行簽名,並把簽名附加到韌體中。裝置在接受安裝時使用提前預裝的公鑰進行驗證,如果檢測到裝置完整性受損,那麼就拒絕韌體升級。簽名的流程一般不直接對韌體本身的內容進行簽名,首先計算韌體的HASH值,然後開發者使用私鑰對韌體HASH進行簽名,將簽名附加到韌體中。裝置在出廠時檔案系統中就被預裝了公鑰,升級透過公鑰驗證簽名是否正確。

加密韌體之依據老韌體進行解密

加解密邏輯分析

既然到這個地方了,那麼順便進去看一看解密程式是如何進行運作的。從IDA的符號表中可以看到,使用到了對稱加密AES、非對稱加密RSA和雜湊SHA512,是不是對比上面提到的韌體安全開發到釋出的流程,心中大概有個數了。

首先我們進入main函式,可以知道,這個解密程式imgdecrypt實際上也是具有加密功能的。這裡提一下,因為想要把整個解密韌體的邏輯都看一看,可能會在文章裡面貼出很多的具體函式分析,那麼文章篇幅就會有點長,不過最後會進行一個流程的小總結,希望看的師傅不用覺得囉嗦。

int __cdecl main(int argc, const char **argv, const char **envp){ int result; // $v0 if ( strstr(*argv, “decrypt”, envp) ) result = decrypt_firmare(argc, (int)argv); else result = encrypt_firmare(argc, argv); return result;}

下一步繼續進入到函式decrypt_firmare中,這個地方結合之前模擬可以知道:argc=2,argv=引數字串地址。首先是進行一些引數的初始化,例如aes_key、公鑰的儲存地址pubkey_loc。

接下來是對輸入引數數量和引數字串的判定,輸入引數數量從2開始判定,結合之前的模擬,那麼argc=2,第一個是程式名,第二個是已加密韌體地址。

然後在004021AC地址處的函式check_rsa_cert,該函式內部邏輯也非常簡單,基本就是呼叫RSA相關的庫函式,讀取公鑰並判定公鑰是否有效,有效則將讀取到的RSA物件儲存在dword_413220。檢查成功後,就進入到004025A4地址處的函式aes_cbc_crypt中。這個函式的主要作用就是根據一個固定字串0123456789ABCDEF生成金鑰,是根據硬編碼生成的解密金鑰,因此每次生成並打印出來的金鑰是相同的,此處金鑰用變數aes_key表示。

int __fastcall decrypt_firmare(int argc, int argv){ int result; // $v0 const char *pubkey_loc; // [sp+18h] [-1Ch] int i; // [sp+1Ch] [-18h] int aes_key[5]; // [sp+20h] [-14h] BYREF qmemcpy(aes_key, “0123456789ABCDEF”, 16); pubkey_loc = “/etc_ro/public。pem”; i = -1; if ( argc >= 2 ) { if ( argc >= 3 ) pubkey_loc = *(const char **)(argv + 8); if ( check_rsa_cert((int)pubkey_loc, 0) ) // 讀取公鑰並進行儲存RSA物件到dword_413220中 { result = -1; } else { aes_cbc_crypt((int)aes_key); // 生成aes_key printf(“key:”); for ( i = 0; i < 16; ++i ) printf(“%02X”, *((unsigned __int8 *)aes_key + i));// 打印出key puts(“\r”); i = actual_decrypt(*(_DWORD *)(argv + 4), (int)“/tmp/。firmware。orig”, (int)aes_key); if ( !i ) { unlink(*(_DWORD *)(argv + 4)); rename(“/tmp/。firmware。orig”, *(_DWORD *)(argv + 4)); } RSA_free(dword_413220); result = i; } } else { printf(“%s \r\n”, *(const char **)argv); result = -1; } return result;}

接下來就是真正的負責解密和驗證韌體的函式actual_decrypt,位於地址00401770處。在分析這個函式的時候,我發現IDA的MIPS32在反編譯處理函式的輸入引數的時候,似乎會把數值給弄錯了,,,比如fun(a + 10),可能會反編譯成fun(a + 12)。已經修正過函式引數數值的反編譯程式碼就放在下面,程式碼分析也全部直接放在註釋中了。

int __fastcall actual_decrypt(int img_loc, int out_image_loc, int aes_key){ int image_fp; // [sp+20h] [-108h] int v5; // [sp+24h] [-104h] _DWORD *MEM; // [sp+28h] [-100h] int OUT_MEM; // [sp+2Ch] [-FCh] int file_blocks; // [sp+30h] [-F8h] int v9; // [sp+34h] [-F4h] int i; // [sp+38h] [-F0h] int out_image_fp; // [sp+3Ch] [-ECh] int data1_len; // [sp+40h] [-E8h] int data2_len; // [sp+44h] [-E4h] _DWORD *IN_MEM; // [sp+48h] [-E0h] char hash_buf[68]; // [sp+4Ch] [-DCh] BYREF int image_info[38]; // [sp+90h] [-98h] BYREF image_fp = -1; out_image_fp = -1; v5 = -1; MEM = 0; OUT_MEM = 0; file_blocks = -1; v9 = -1; // 這個hashbuf用於儲存SHA512的計算結果,在後面比較會一直被使用到 memset(hash_buf, 0, 64); data1_len = 0; data2_len = 0; memset(image_info, 0, sizeof(image_info)); IN_MEM = 0; // 透過stat函式讀取加密韌體的相關資訊寫入結構體到image_info,最重要的是檔案大小 if ( !stat(img_loc, image_info) ) { // 獲取檔案大小 file_blocks = image_info[13]; // 以只讀開啟加密韌體 image_fp = open(img_loc, 0); if ( image_fp >= 0 ) { // 將加密韌體對映到記憶體中 MEM = (_DWORD *)mmap(0, file_blocks, 1, 1, image_fp, 0); if ( MEM ) { // 以O_RDWR | O_NOCTTY獲得解密後韌體應該存放的檔案描述符 out_image_fp = open(out_image_loc, 258); if ( out_image_fp >= 0 ) { v9 = file_blocks; // 比較寫入到記憶體的大小和韌體的真實大小是否相同 if ( file_blocks - 1 == lseek(out_image_fp, file_blocks - 1, 0) ) { write(out_image_fp, &unk_402EDC, 1); close(out_image_fp); out_image_fp = open(out_image_loc, 258); // 以加密韌體的檔案大小,將待解密的韌體對映到記憶體中,返回記憶體地址OUT_MEM OUT_MEM = mmap(0, v9, 3, 1, out_image_fp, 0); if ( OUT_MEM ) { IN_MEM = MEM; // 重新賦值指標 // 檢查韌體的Magic,透過檢視HEX可以看到加密韌體的開頭有SHRS魔數 if ( check_magic((int)MEM) ) // 比較讀取到的韌體資訊中含有SHRS { // 獲得解密後韌體的大小 data1_len = htonl(IN_MEM[2]); data2_len = htonl(IN_MEM[1]); // 從加密韌體的1756地址起,計算data1_len個位元組的SHA512,也就是解密後韌體大小的訊息摘要,並儲存到hash_buf sub_400C84((int)(IN_MEM + 0x6dc), data1_len, (int)hash_buf); // 比較原始韌體從156地址起,64個位元組大小,和hash_buf中的值進行比較,也就是和加密韌體頭中預儲存的真實加密韌體大小的訊息摘要比較 if ( !memcmp(hash_buf, IN_MEM + 0x9c, 64) ) { // AES對加密韌體進行解密,並輸出到OUT_MEM中 // 這個地方也可以看出從加密韌體的1756地址起就是真正被加密的韌體資料,前面都是一些頭部資訊 // 函式邏輯比較簡單,就是AES加解密相關,從儲存在韌體頭IN_MEM + 0xc獲取解密金鑰 sub_40107C((int)(IN_MEM + 0x6dc), data1_len, aes_key, IN_MEM + 0xc, OUT_MEM); // 計算解密後韌體的SHA_512訊息摘要 sub_400C84(OUT_MEM, data2_len, (int)hash_buf); // 和儲存在原始加密韌體頭,從92地址開始、64位元組的SHA512進行比較 if ( !memcmp(hash_buf, IN_MEM + 0x5c, 64) ) { // 獲取解密韌體+aes_key的SHA512 sub_400D24(OUT_MEM, data2_len, aes_key, (int)hash_buf); // 和儲存在原始韌體頭,從28地址開始、64位元組的SHA512進行比較 if ( !memcmp(hash_buf, IN_MEM + 0x1c, 64) ) { // 使用當前檔案系統內的公鑰,透過RSA驗證訊息摘要和簽名是否匹配 if ( sub_400E78((int)(IN_MEM + 0x5c), 64, (int)(IN_MEM + 0x2dc), 0x200) == 1 ) { if ( sub_400E78((int)(IN_MEM + 0x9c), 64, (int)(IN_MEM + 0x4dc), 0x200) == 1 ) v5 = 0; else v5 = -1; } else { v5 = -1; } } else { puts(“check sha512 vendor failed\r”); } } else { printf(“check sha512 before failed %d %d\r\n”, data2_len, data1_len); for ( i = 0; i < 64; ++i ) printf(“%02X”, (unsigned __int8)hash_buf[i]); puts(“\r”); for ( i = 0; i < 64; ++i ) printf(“%02X”, *((unsigned __int8 *)IN_MEM + i + 92)); puts(“\r”); } } else { puts(“check sha512 post failed\r”); } } else { puts(“no image matic found\r”); } } } } } } } if ( MEM ) munmap(MEM, file_blocks); if ( OUT_MEM ) munmap(OUT_MEM, v9); if ( image_fp >= 0 ) close(image_fp); if ( image_fp >= 0 ) close(image_fp); return v5;}

概述DIR 3040的韌體組成以及解密驗證邏輯

從上面最關鍵的解密函式邏輯分析中,可以知道如果僅僅是解密相關,實際上只用到了AES解密,而且還是使用的硬編碼金鑰(通過了一些計算)。只是看上面的解密+驗證邏輯分析,對整個流程可能還是會有點混亂,下面就說一下加密韌體的檔案結構和總結一下上面的解密+驗證邏輯。

先直接給出加密韌體檔案結構的結論,只展現出重要的Header內容,大小1756位元組,其後全部是真正的被加密韌體資料。

起始地址

長度(Bytes)

作用

0:0x00

4

魔數:SHRS

4:0x4

解密韌體的大小,帶填充

8:0x8

解密韌體的大小,不帶填充

12:0xC

16

AES_128_CBC解密金鑰

28:0x1C

64

解密後韌體+KEY的SHA512訊息摘要

92:0x5C

解密後韌體的SHA512訊息摘要

156:0x9C

加密韌體的SHA512訊息摘要

220:0xDC

512

未使用

732:0x2DC

解密後韌體訊息摘要的數字簽名

1244:0x4DC

加密後韌體訊息摘要的數字簽名

結合上面的加密韌體檔案結構,再次概述一下解密邏輯:

判斷加密韌體是否以Magic Number:SHRS開始。

判斷(加密韌體中存放的,真正被加密的韌體資料大小的SHA512訊息摘要),和,(去除Header之後,資料的SHA512訊息摘要)。

這一步是透過驗證韌體的檔案大小,判定是否有人篡改過韌體,如果被篡改,解密失敗。

讀取儲存在Header中的AES解密金鑰,對加密韌體資料進行解密

計算(解密後韌體資料的SHA512訊息摘要),和(預先儲存在Header中的、解密後韌體SHA512訊息摘要)進行對比

計算(解密韌體資料+解密金鑰的、SHA512訊息摘要),和(預先儲存在Header中的、解密後韌體資料+解密金鑰的、SHA512訊息摘要)進行對比

使用儲存在當前檔案系統中的RSA公鑰,驗證解密後韌體的訊息摘要和其簽名是否匹配

使用儲存在當前檔案系統中的RSA公鑰,驗證加密後韌體的訊息摘要和其簽名是否匹配

小結

這篇文章主要是以DIR 3040韌體為例,說明如何從未加密的老韌體中去尋找負責解密的可執行檔案,用於解密新版的加密韌體。先說明拿到一個韌體後如何判斷已經被加密,然後說明如何去找到負責解密的可執行檔案,再透過qemu模擬去執行解密程式,將韌體解密,最後簡單說了下韌體完整性相關的知識,並重點分析瞭解密程式的解密+驗證邏輯。

這次對於DIR 3040的漏洞分析和韌體解密驗證過程分析還是花費了不少的時間。首先是韌體的獲取,從官網下載到的韌體是加密的,然後看到一篇文章簡單說了下基於未加密韌體版本對加密韌體進行解密,也是DIR 3040相關的。但是我在官網上沒有找到未加密的韌體,全部是被加密的韌體。又在資訊蒐集的過程中,發現了原來在Github上有一個比較通用的、針對D-Link系列的

韌體解密指令碼

。原來,Dlink近兩年使用的加密、驗證程式imgdecrypt基本上都是一個套路,於是我參考瞭解密指令碼開發者在2020年的分析思路,結合之前看過的關於可信計算相關的一些知識點,簡單敘述了韌體安全性,然後重點分析瞭解密驗證邏輯如上。

關於漏洞分析,感興趣的師傅可以看一下我的這篇

分析文章

作者:OneShell@知道創宇404實驗室

本文作者:知道創宇404實驗室,

轉載來自

FreeBuf