作者:OneShell@知道創宇404實驗室
時間:2021年7月27日
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/imgdecrypt
bin/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/sh
BusyBox 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.bin
key:C05FBF1936C99429CE2A0781F08D6AD8
/ # ls -a tmp/
.. .firmware.orig . DIR3040A1_FW113B03.bin
/ #
那么就解壓出來了,解壓到了tmp文件夾中,.firmware.orig文件。這個時候使用binwalk再次進行查看,可以看到已經被成功解密了。
$ binwalk .firmware.orig
DECIMAL 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 bytes
1810550 0x1BA076 PGP RSA encrypted session key - keyid: 12A6E329 67B9887A RSA (Encrypt or Sign) 1024b
14275307 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 <sourceFile>\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 | 4 | 解密固件的大小,帶填充 |
| 8:0x8 | 4 | 解密固件的大小,不帶填充 |
| 12:0xC | 16 | AES_128_CBC解密密鑰 |
| 28:0x1C | 64 | 解密后固件+KEY的SHA512消息摘要 |
| 92:0x5C | 64 | 解密后固件的SHA512消息摘要 |
| 156:0x9C | 64 | 加密固件的SHA512消息摘要 |
| 220:0xDC | 512 | 未使用 |
| 732:0x2DC | 512 | 解密后固件消息摘要的數字簽名 |
| 1244:0x4DC | 512 | 加密后固件消息摘要的數字簽名 |
結合上面的加密固件文件結構,再次概述一下解密邏輯:
-
判斷加密固件是否以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年的分析思路,結合之前看過的關于可信計算相關的一些知識點,簡單敘述了固件安全性,然后重點分析了解密驗證邏輯如上。
關于漏洞分析,感興趣的師傅可以看一下我的這篇分析文章。
參考鏈接
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.jmbmsq.com/1651/
暫無評論