譯者:知道創宇404實驗室翻譯組
原文鏈接:https://0x434b.dev/breaking-the-d-link-dir3060-firmware-encryption-static-analysis-of-the-decryption-routine-part-2-1/
前言
在第一篇中,我們突出了相關偵察步驟!在本文中,我們深入研究了IDA歷險,更好地了解imgdecrypt如何操作,以確保最新路由器型號的固件完整性。

將二進制文件加載到IDA中時,將會出現一個功能列表。我們已經發現二進制代碼應該是從調試符號中剝離出來的,使得調試整個代碼變得較為困難,但從IDA提供給我們的方式來看,它還是相當不錯的:

1.共有104個公認功能。
2.只有16個函數不能與任何庫函數(或類似的函數)匹配,該程序很可能包含由D-Link生成的自定義解/加密程序。
3.即使二進制文件被稱為imgdecrypt,主要入口點顯示它顯然也具有加密功能。

這里的要點是,為了進入二進制文件的解密部分,我們的**argv參數列表必須包含子字符串“decrypt”。如果不是這種情況,則char *strstr(const char *haystack, const char *needle)會返回NULL,因為它在haystack (argv[0] == "imgdecrypt\0")中找不到"decrypt"。如果返回NULL,則beqz $v0, loc_402AE0指令將計算正確,并將控制流重定向到loc_402AE0,這是二進制文件的加密部分。不理解的話,建議您仔細閱讀本系列的第1部分。
因為我們正在分析的二進制文件imgdecrypt被調用,從argv空間的開頭進行搜索會找到途徑進入解密例程。為了能夠輸入加密例程,我們需要重命名二進制文件。
所以現在我們知道了如何到達存放解密固件decrypt_firmware的基本塊。在輸入之前,應該仔細查看該函數是否帶有參數以及使用哪個參數。從帶注釋的版本中可以看到,argc被加載到$a0中,argv被加載到$a1。根據MIPS32 ABI,這兩個寄存器保存了前兩個函數參數!
crypto_firmware

從IDA如何在圖形視圖中分組基本塊進入decrypt_firmware函數后,我們可以肯定兩件事:從IDA如何
1.解密有兩個明顯的途徑
2.存在某種形式的循環。

開頭的少數幾個lw和sw指令是在適當的位置設置堆棧框架和函數參數,還記得第1部分中的/etc_ro/public.pem嗎?在這里的函數序言中,還為以后的使用設置了證書。除此之外,argc被加載到$v0中,通過slti $v0, 2和2進行比較,并將其與下一條指令beqz $v0, loc_402670轉換為以下樣式代碼:
if(argc < 2) {
...
} else {
goto loc_402670
}
這意味著要正確地調用imgdecrypt,我們至少還需要一個參數(因為./imgdecrypt意味著argc為1)。這是有道理的,因為如果不提供至少一個加密的固件映像,調用此二進制文件將不會獲得任何收益!讓我們先檢查一下要避免的錯誤路徑:

正如預期的那樣,二進制文件接受一個輸入文件,將其命名為sourceFile,二進制操作的模式可以是解密或加密。回到我們想要遵循的控制流,一旦確定argc至少是2,就需要對argc進行另外的檢查:
lw $v0, 0x34+argc($sp)
nop
slti $v0, 3
bnez $v0, loc_402698
直接轉換為:
if(argc < 3) {
// $v0 == 1
goto loc_402698
} else {
// $v0 == 0
goto loadUserPem
}
我所說的loadUserPem允許用戶certificate.pem在調用時自定義,然后將其存儲在默認值所在的內存位置/etc_ro/public.pem。由于這不是我們目前的著重點,就此忽略,并繼續loc_402698。我們直接設置了一個函數調用,將它重命名為check_cert,將參數分別加載到$a0和中$a1:check_cert(pemFile, 0)
check_cert
簡單的是,它僅利用了一些庫功能。

設置完堆棧框架后,將通過執行FILE *fopen(const char *pathname, const char *mode)來檢查提供的證書位置是否有效,失敗時將返回一個NULL指針。如果是這樣,beqz $v0, early_return它將評估為true,控制流將采用early_return路徑,最終將-1從函數返回,如同lw $v0, RSA_cert; beqz $v0, bad_end評估為true一樣,RSA_cert尚未初始化為它擁有任何數據的地步以通過對0*的檢查。
在這種情況下,成功打開文件后,RSA *RSA_new(void)和RSA *PEM_read_RSAPublicKey(FILE *fp, RSA **x, pem_password_cb \*cb, void \*u*)將用于填充RSA *RSA_struct。
該結構具有以下字段:
struct {
BIGNUM *n; // public modulus
BIGNUM *e; // public exponent
BIGNUM *d; // private exponent
BIGNUM *p; // secret prime factor
BIGNUM *q; // secret prime factor
BIGNUM *dmp1; // d mod (p-1)
BIGNUM *dmq1; // d mod (q-1)
BIGNUM *iqmp; // q^-1 mod p
// ...
}; RSA
// In public keys, the private exponent and the related secret values are NULL.
最后,這些值(也稱為公共密鑰)將通過sw $v1, RSA_cert指令存儲在RSA_cert中。接下來,函數被拆解,比較early_return得到一個值!= 0,我們的函數將在good_end基本塊中將返回值設置為0 move $v0, $zero。
在decrypt_firmware中,check_cert的返回值被放置到內存中(我將它重新標記為loop_ctr,因為它稍后將被重用),并將其與0進行比較。只有在滿足該條件時,控制流才會繼續深入到程序中以檢查_cert_succ。在這里,我們直接將控制流重定向到call_aes_cbc_encrypt(),并使用key_0作為其第一個參數。

call_aes_cbc_encrypt
該函數本身僅充當包裝器,因為它直接調用帶有5個參數的aes_cbc_encrypt(),前四個寄存器為$a0 - $a3,第五個在堆棧上。

五個自變量中的四個被硬編碼到此二進制文件中,并通過以下方式從內存中加載:加載內存基地址(lw $v0, offset_crypto_material)并向其添加偏移量(addiu $a0, $v0, offset),直接一個接一個地放置:
offset_crypto_material + 0x20→C8D32F409CACB347C8D26FDCB9090B3C(in)offset_crypto_material + 0x10→358790034519F8C8235DB6492839A73F(userKey)offset_crypto_material→98C9D8F0133D0695E2A709C8B69682D4(IVEC)0x10→密鑰長度
這基本上轉換為帶有以下簽名的函數調用:aes_cbc_encrypt(*ptrTo_C8D32F409CACB347C8D26FDCB9090B3C, 0x10, *ptrTo_358790034519F8C8235DB6492839A73F, *ptrTo_98C9D8F0133D0695E2A709C8B69682D4, *key_copy_stack。重命名key_copy_stack,實際上它只是一個16字節的緩沖區。
aes_cbc_encrypt
該函數的前三分之一是常見的堆棧框架設置,因為它需要正確處理5個函數參數。

此外,AES_KEY定義了一個如下結構:
#define AES_MAXNR 14
// [...]
struct aes_key_st {
#ifdef AES_LONG
unsigned long rd_key[4 *(AES_MAXNR + 1)];
#else
unsigned int rd_key[4 *(AES_MAXNR + 1)];
#endif
int rounds;
};
typedef struct aes_key_st AES_KEY;
在第一次庫調用AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key)時需要這個函數,它將key配置為使用 bits-bit密鑰解密userKey。在這個特殊的例子中,鍵的大小是0x80(128位== 16字節)。最后,調用AES_cbc_encrypt(const uint8_t *in, uint8_t *out, size_t len, const AES_KEY *key, uint8_t *ivec, const int enc),這個函數從in到out加密(如果enc == 0即解密) len字節。由于out是從call_aes_cbc_encrypt中外部提供的內存地址(key_copy_stack,即16字節緩沖區),AES_cbc_encrypt直接存儲在內存中,而不是用作此函數的專用返回值,而是返回move $v0, $zero。
注意:對于想知道這些lwl并lwr在其中做什么的人……它們顯示未對齊的內存訪問,看起來ivec是像數組一樣在訪問,但之后從未使用過。
無論如何,這個函數實際上所做的是設置來自硬編碼組件的解密密鑰。因此,“生成的”解密密鑰每次都是相同的,我們可以很容易地編寫這個行為:
from Crypto.Cipher import AES
from binascii import b2a_hex
inFile = bytes.fromhex('C8D32F409CACB347C8D26FDCB9090B3C')
userKey = bytes.fromhex('358790034519F8C8235DB6492839A73F')
ivec = bytes.fromhex('98C9D8F0133D0695E2A709C8B69682D4')
cipher = AES.new(userKey, AES.MODE_CBC, ivec)
b2a_hex(cipher.decrypt(inFile)).upper()
# b'C05FBF1936C99429CE2A0781F08D6AD8'
我們現在又重新獲得decrypt_firmware有關靜態解密密鑰的新知識:
crypto_firmware

無論出于何種原因,二進制文件現在都會進入一個循環結構,該結構會打印出先前計算出的解密密鑰,綠色標記的基本塊大致轉換為以下代碼:
int ctr = 0;
while(ctr <= 0x10 ) {
printf("%02X", *(key + ctr));
ctr += 1;
}
我的假設是,它可能用于內部調試,所以當改變ivec時,仍可以很快得到新的解密密鑰……一旦將解密密鑰打印到標準輸出上,循環條件就會將控制流重定向到標記為path_to_dec的基本塊,其中actual_decryption(argv[1], "/tmp/.firmware.orig", *key)正在準備。
完成控制流和參數后,調用標記為actual_decryption的函數。
actual_decryption
該功能是將這種解密方案結合在一起的主體。

第一部分通過初始化所有0來準備兩個存儲位置void *memset(void *s, int c, size_t n)。我將這些區域表示為buf[68]和buf[0x98] statbuf_[98]。該函數通過調用int stat(const char *pathname, struct stat *statbuf)來檢查argv[1]中提供的sourceFile是否確實存在,結果存儲在一個stat結構中,如下所示:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
/* Since Linux 2.6, the kernel supports nanosecond
precision for the following timestamp fields.
For the details before Linux 2.6, see NOTES. */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
如果成功(意味著路徑名存在),stat返回0;當bnez $v0失敗,stat_fail會跟隨到stat_fail的分支。所以我們要確保$v0是0,才能正常繼續,所需控制流如下:

在這里,除了保存一些局部變量外,sourceFile還以只讀模式打開,由0x0提供給的標志指示open(const char *pathname, int flags)。該調用的結果/返回的文件描述符保存到0x128+fd_enc。與stat例程類似,如之前檢查stat例程是否open(sourceFile, O_RDONLY) 成功bltz $v0, open_enc_fail。該分支open_enc_fail只采取了$v0 < 0因此,假設打開調用成功,我們將通過$v0持有打開文件描述符進入下一部分:

這基本上是嘗試使用void mmap(void addr, size_t length, int prot, int flags, int fd, off_t offset)將剛打開的文件映射到內核選擇的***addr == 0共享(只讀)的內存區域。
這樣的標志可以很容易地從系統文件中提取出來,如下所示:
> egrep -i '(PROT_|MAP_)' /usr/include/x86_64-linux-gnu/bits/mman-linux.h
implementation does not necessarily support PROT_EXEC or PROT_WRITE
without PROT_READ. The only guarantees are that no writing will be
allowed without PROT_WRITE and no access will be allowed for PROT_NONE. */
#define PROT_READ 0x1 /* Page can be read. */
#define PROT_WRITE 0x2 /* Page can be written. */
#define PROT_EXEC 0x4 /* Page can be executed. */
#define PROT_NONE 0x0 /* Page can not be accessed. */
#define PROT_GROWSDOWN 0x01000000 /* Extend change to start of
#define PROT_GROWSUP 0x02000000 /* Extend change to start of
#define MAP_SHARED 0x01 /* Share changes. */
#define MAP_PRIVATE 0x02 /* Changes are private. */
# define MAP_SHARED_VALIDATE 0x03 /* Share changes and validate
# define MAP_TYPE 0x0f /* Mask for type of mapping. */
#define MAP_FIXED 0x10 /* Interpret addr exactly. */
# define MAP_FILE 0
# ifdef __MAP_ANONYMOUS
# define MAP_ANONYMOUS __MAP_ANONYMOUS /* Don't use a file. */
# define MAP_ANONYMOUS 0x20 /* Don't use a file. */
# define MAP_ANON MAP_ANONYMOUS
/* When MAP_HUGETLB is set bits [26:31] encode the log2 of the huge page size. */
# define MAP_HUGE_SHIFT 26
# define MAP_HUGE_MASK 0x3f
在這種情況下,先前的stat調用再次派上用場,因為它不僅用于驗證argv [1]中提供的文件是否確實存在,而且statStruct還包含st_blocks可用于填充所需size_t length參數的成員。mmap的返回值存儲在中0x128+mmap_enc_fw($sp),'if'條件類型分支檢查內存映射是否成功。成功時,mmap返回一個指向映射區域的指針,并在beqz $v0上進行分支,mmap_fail不會出現,因為$v0包含一個值!= 0。以下是對open的最后一次調用:

這只是嘗試將預定義的路徑(“ /tmp/.firmware.orig”)以讀寫方式打開,并將新文件描述符保存在0x128+fd_tmp($sp)。如果打開失敗,則分支到該函數的失敗部分;如果成功后,這將引導我們進行最后的步驟:

1.我們準備在/tmp/位置中設置新打開的文件的正確大小,首先通過調用lseek來查找stat.st_blocks -1的偏移量(fd_tmp, stat.st_blocks -1)。
2.當lseek操作成功時,我們向該偏移位置的文件寫入一個0,這使得我們可以輕松快速地創建一個“空”文件,而不需要寫入N個字節(其中N==所需的文件大小,以字節為單位)。最后,關閉、重新打開并使用新的權限重新映射文件。
總結
到目前為止,我們還沒有深入挖掘解密例程,這篇文章即將發表的第二部分將只關注D-Link所利用的方案加密方面。
如果您始終無法正確操作,可以在此處找到到目前為止的完整源代碼。
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <openssl/aes.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static RSA *grsa_struct = NULL;
static unsigned char iv[] = {0x98, 0xC9, 0xD8, 0xF0, 0x13, 0x3D, 0x06, 0x95,
0xE2, 0xA7, 0x09, 0xC8, 0xB6, 0x96, 0x82, 0xD4};
static unsigned char aes_in[] = {0xC8, 0xD3, 0x2F, 0x40, 0x9C, 0xAC,
0xB3, 0x47, 0xC8, 0xD2, 0x6F, 0xDC,
0xB9, 0x09, 0x0B, 0x3C};
static unsigned char aes_key[] = {0x35, 0x87, 0x90, 0x03, 0x45, 0x19,
0xF8, 0xC8, 0x23, 0x5D, 0xB6, 0x49,
0x28, 0x39, 0xA7, 0x3F};
unsigned char out[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
int check_cert(char *pem, void *n) {
OPENSSL_add_all_algorithms_noconf();
FILE *pem_fd = fopen(pem, "r");
if (pem_fd != NULL) {
RSA *lrsa_struct[2];
*lrsa_struct = RSA_new();
if (!PEM_read_RSAPublicKey(pem_fd, lrsa_struct, NULL, n)) {
RSA_free(*lrsa_struct);
puts("Read RSA private key failed, maybe the password is incorrect.");
} else {
grsa_struct = *lrsa_struct;
}
fclose(pem_fd);
}
if (grsa_struct != NULL) {
return 0;
} else {
return -1;
}
}
int aes_cbc_encrypt(size_t length, unsigned char *key) {
AES_KEY dec_key;
AES_set_decrypt_key(aes_key, sizeof(aes_key) * 8, &dec_key);
AES_cbc_encrypt(aes_in, key, length, &dec_key, iv, AES_DECRYPT);
return 0;
}
int call_aes_cbc_encrypt(unsigned char *key) {
aes_cbc_encrypt(0x10, key);
return 0;
}
int actual_decryption(char *sourceFile, char *tmpDecPath, unsigned char *key) {
int ret_val = -1;
size_t st_blocks = -1;
struct stat statStruct;
int fd = -1;
int fd2 = -1;
void *ROM = 0;
int *RWMEM;
off_t seek_off;
unsigned char buf_68[68];
int st;
memset(&buf_68, 0, 0x40);
memset(&statStruct, 0, 0x90);
st = stat(sourceFile, &statStruct);
if (st == 0) {
fd = open(sourceFile, O_RDONLY);
st_blocks = statStruct.st_blocks;
if (((-1 < fd) &&
(ROM = mmap(0, statStruct.st_blocks, 1, MAP_SHARED, fd, 0),
ROM != 0)) &&
(fd2 = open(tmpDecPath, O_RDWR | O_NOCTTY, 0x180), -1 < fd2)) {
seek_off = lseek(fd2, statStruct.st_blocks - 1, 0);
if (seek_off == statStruct.st_blocks - 1) {
write(fd2, 0, 1);
close(fd2);
fd2 = open(tmpDecPath, O_RDWR | O_NOCTTY, 0x180);
RWMEM = mmap(0, statStruct.st_blocks, PROT_EXEC | PROT_WRITE,
MAP_SHARED, fd2, 0);
if (RWMEM != NULL) {
ret_val = 0;
}
}
}
}
puts("EOF part 2.1!\n");
return ret_val;
}
int decrypt_firmware(int argc, char **argv) {
int ret;
unsigned char key[] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46};
char *ppem = "/tmp/public.pem";
int loopCtr = 0;
if (argc < 2) {
printf("%s <sourceFile>\r\n", argv[0]);
ret = -1;
} else {
if (2 < argc) {
ppem = (char *)argv[2];
}
int cc = check_cert(ppem, (void *)0);
if (cc == 0) {
call_aes_cbc_encrypt((unsigned char *)&key);
printf("key: ");
while (loopCtr < 0x10) {
printf("%02X", *(key + loopCtr) & 0xff);
loopCtr += 1;
}
puts("\r");
ret = actual_decryption((char *)argv[1], "/tmp/.firmware.orig",
(unsigned char *)&key);
if (ret == 0) {
unlink(argv[1]);
rename("/tmp/.firmware.orig", argv[1]);
}
RSA_free(grsa_struct);
} else {
ret = -1;
}
}
return ret;
}
int encrypt_firmware(int argc, char **argv) { return 0; }
int main(int argc, char **argv) {
int ret;
char *str_f = strstr(*argv, "decrypt");
if (str_f != NULL) {
ret = decrypt_firmware(argc, argv);
} else {
ret = encrypt_firmware(argc, argv);
}
return ret;
}
> ./imgdecrypt
./imgdecrypt <sourceFile>
> ./imgdecrypt testFile
key: C05FBF1936C99429CE2A0781F08D6AD8
EOF part 2.1!
本文的下篇不久后將發布,敬請期待!
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1524/
暫無評論