0%

IOT - 某路由器mips固件解密脚本复现

期中终于结束了😭复现一下b站Wker666的固件解密

第一次分析IOT固件,不当之处请指出😭

0xFF 前置知识

  1. 多数情况下,路由器固件解包以后我们会拿到一个类似于 Linux 文件系统的文件夹,这个文件系统会跑一些三环程序,而三环程序有非常多的系统调用,因此我们希望通过这些三环程序来提权。

  2. 现在很多厂商会认为,自己的路由器能被解包太不安全了,因此他们会通过加密,但是每次更新换代时,后一代的固件包都是通过前一代的固件中的某个解密算法来解密的。因此,我们在挖掘 IOT 漏洞时,会先找到比较老的一些版本,分析解密算法,进而自己写出解密脚本来解密新一代的固件包。

  3. (建议看到0x01后再来阅读本条)我们在路由器的 web user 上发送一个更新请求的时候,客户端会先发给服务器一个 http 请求,然后被服务器的 httpd 二进制文件接收,httpd 会 fork 一个新的进程来启用二进制文件 cgibin ,并把环境变量和 http 的请求发给 cgi , cgi 完成处理以后,把内容输出到标准输出流之类的东西,并返回客户

  4. IDA 反编译 Mips 文件会有很多“无用操作”,就是两个变量来回赋值之类的,这是由于 Mips 有分支延迟的特性

0x00 准备工作

lto8e.png
上图中间有这样一句话Upgrade to Firmware V2.10 and then instantly go back into the web user interface and upgrade to Firmware V2.20

它告诉我们,从web user更新到2.2版本时,需要先从官网下载2.1的固件并更新到2.1才行

也就是说,2.2大概率是一个加密固件包,我们无法通过binwalk直接解包,但是2.1可以

我们download两个包到本地binwalk解包一下看看

ltiAU.png

ltjx0.png

我们可以看到,确实像我们猜想的那样,V2.1未加密,V2.2加密

0x01 定位解密逻辑

通过 0xFF.3 我们知道要分析cgibin和httpd文件,所以我们用IDA打开看一下

1
2
3
cgibin : /_DIR850LB1_FW210WWb03.bin.extracted/squashfs-root/htdocs

httpd : /_DIR850LB1_FW210WWb03.bin.extracted/squashfs-root/sbin

我们先来分析一下在本文中不那么重要的 httpd 文件,IDA7.6 以上的版本是可以反编译 Mips 的
l12OW.png
我们在 function 中搜索 cgi,找到process_cgi函数,如上图,可以看到里面有很多环境变量如GATEWAY_INTERFACECONTENT_LENGTH等,在设置完环境变量后,我们可以看到它调用了spawn函数,我们跟进去看一下
l1ZZe.png

l1lK3.png
可以看到,在 Line20 的位置,该进程是被fork起来的,而在 Line46 的位置,我们执行了 execve 系统调用,而我们知道 execve 的第一个参数是文件路径,第二个参数是argv,第三个参数是环境变量,因此我们回溯一下

而在调用 spawn 的图中,我们可以看到,spawn 的第一个参数是*v77,v77,ptr,这里的 v77 就是 file_path,而 ptr 就是 env_ptr,我们接着溯源(溯源的时候我们会发现 file_path 的调用处非常少,这是因为 ida 的反编译并没有将 file_path 的数据类型正确处理,导致 file_path 下面的一些变量,其实可能就是 file_path,但因为处理错了,所以被命名为 v78 v79 …)

如下图,我们挨个分析,而当我们看到 v80 时,可以看到对 v80 被做了手脚,Wker666 说这里是对 cgi 进行一些选择,但我 STFW 以后也没找到原因,先搁置一下吧

l1tB2.png

至此,httpd 就分析完了,httpd主要是做前端用的,它本身也有一些漏洞,不过这里就不再分析了,我们直接看cgibin


我们用 IDA 打开 cgibin 文件,往下找到 seamacgi_main (这里没找到 seama 到底是什么意思,只是因为在 function 中搜索 enc 可以找到 encrypt_main ,溯源分析就能找到 semacgi_main 了)
l111j.png

跟进 semacgi_main ,可以找到 encrypt_main 函数,这就是我们需要的解密函数了

l1UfV.png

0x02 解密逻辑分析

被传入的参数

我们查看 encrypt_main 函数的引用,可以在 sub_407664+668 处找到一处调用,我们可以看到第一个参数是6,第二个参数被赋值了很多类似于-i,-d之类的东西,结合 encrypt_main 函数是个 main,我们猜测第一个参数是 argc,第二个参数是argv

但是这里的赋值方式很奇怪,v116 是struct stat,是一个结构体,我们再去别的引用处看一下,可以在 encode_file_check 函数中发现这些参数其实是一个 char 数组,这就符合我们对 argv 的认知了。

我们按 y 键将 sub_407664 中的 v116 的数据类型修改成 char* 即可

l1iQJ.png

接下来分析 argv 都是些什么东西

  • 第一个参数: encimg
  • 第二个参数: -i (将 v116 数据类型修改后,可以很明显的看到”-i”是 v117,而全局变量 ptr 才是 v118)
  • 第三个参数: ptr(在 encode_file_check 函数中我们可以看到,ptr 与 “/var/firmware.seama” 作了比较,因此我们猜测ptr可能是指向文件名字符串的指针)
  • 第四个参数: -s
  • 第五个参数: byte_43CDB0(在 encode_file_check 函数中我们可以看到,byte_43CDB0 是从 /etc/config/image_sign 文件读出了128个字符,我们去看一下这个文件,发现里面是wrgac25_dlink.2013gui_dir850l)
  • 第六个参数: -d

因此,我们传入的参数是encimg -i file -s wrgac25_dlink.2013gui_dir850l -d

这也符合 argc = 6 的要求

参数功能

我们知道,一般来说 -h 代表的是 help,所以我们查看一下 encrypt_main 函数的case h:会打印什么东西

1
2
3
4
5
6
7
8
9
10
int sub_408F8C()
{
printf("Usage: %s {OPTIONS}\n", "encimg");
puts(" -h : show this message.");
puts(" -v : Verbose mode.");
puts(" -i {input image file} : input image file.");
puts(" -e : encode file.");
puts(" -d : decode file.");
return puts(" -s : signature.");
}

而经过 switch-case 后,在 Line57 判断 dword_43CE40 也就是 signature 是否存在,这个是 -s 参数做的事情,而在 Line59 处判断 file 是否存在,如果两个都通过,就会调用 sub_4090E0 函数,我们继续跟进

l1qvD.png

在 sub_4090E0 函数的前一部分中,做了文件校验等不是很重要的操作,而重要的解密操作从 Line108 开始,也就是下图位置,我已经加好注释了

lJCoX.png

Line130-Line137 是设置AES加解密密钥user_key,而 Line108-Line129 是用_____progs_board_fw_sign_data初始化user_key,接着用 encrypt 函数对user_key做一些处理,我们跟进一下 encrypt 函数

lJP5U.png

而设置好密钥以后,Line158 处调用 AES_cbc_encrypt 函数(如下图),但我们通过OpenSSL-AES这篇文章知道,AES 不可能只有这么点参数,并且根据分析, v32 和 mmap_file_ptr 都是 mmap_file ,这肯定不对。

lJLr0.png

所以我们按 y 键,将AES_cbc_encrypt修改为void AES_cbc_encrypt(const unsigned char *in, unsigned char *out, size_t length, const AES_KEY *key, unsigned char *ivec, const int enc);

1
2
3
4
5
6
7
8
9
10
11
in: 需要加密/解密的数据;

out: 计算后输出的数据;

length: 数据长度(这里不包含初始向量数据长度)

key:密钥

ivec: 初始向量(一般为16字节全0

enc: AES_ENCRYPT(1) 代表加密, AES_DECRYPT(0) 代表解密;

lJXIC.png

现在我们可以看出,Line131-Line177 就是很标准的一个 AES 加解密

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from Crypto.Cipher import AES

def aes_decrypt_file(key,iv,input_file,output_file):
cipher = AES.new(bytes(key),AES.MODE_CBC,bytes(iv))
with open(input_file,'rb') as infile, open(output_file,'wb') as outfile:
while True:
chunk = infile.read(16)
if len(chunk) == 0:
break
elif len(chunk) % 16 != 0:
raise ValueError("aes error")
decrypted_chunk = cipher.decrypt(chunk)
outfile.write(decrypted_chunk)


______progs_board_fw_sign_data =[0x6B, 0x35, 0x4E, 0x49, 0x31, 0x2B, 0x62, 0x76, 0x57, 0x45,
0x66, 0x5A, 0x36, 0x6F, 0x68, 0x74, 0x70, 0x55, 0x4F, 0x77,
0x79, 0x6E, 0x4F, 0x64, 0x55, 0x63, 0x69, 0x76, 0x71, 0x77,
0x45, 0x5A, 0x71, 0x51, 0x65, 0x68, 0x48, 0x4D, 0x45, 0x6D,
0x45, 0x50, 0x51, 0x35, 0x69, 0x7A, 0x4C, 0x2B, 0x63, 0x61,
0x62, 0x6E, 0x38, 0x62, 0x4E, 0x48, 0x5A, 0x58, 0x48, 0x6A,
0x6B, 0x70, 0x36, 0x57, 0x43, 0x6C, 0x39, 0x79, 0x6E, 0x39,
0x43, 0x49, 0x6B, 0x69, 0x49, 0x31, 0x0A, 0x6D, 0x54, 0x46,
0x75, 0x32, 0x31, 0x54, 0x45, 0x45, 0x50, 0x6F, 0x36, 0x36,
0x4A, 0x42, 0x46, 0x76, 0x39, 0x42, 0x4D, 0x6D, 0x62, 0x2B,
0x49, 0x4B, 0x51, 0x67, 0x6E, 0x4F, 0x38, 0x4F, 0x75, 0x46,
0x34, 0x62, 0x7A, 0x34, 0x66, 0x72, 0x47, 0x50, 0x64, 0x4E,
0x36, 0x37, 0x67, 0x59, 0x4C, 0x75, 0x4F, 0x73]

def dec_val_by_sig(dec_sig_val,dec_len,sig): # encrypt 函数 python表示
sig_len = len(sig)
loop_sig_idx = 0
loop_dec_idx = 1
idx = 0
while True:
cur_sig = sig[loop_sig_idx]
loop_sig_idx+=1
if(idx>=dec_len):
break
if(loop_sig_idx>=sig_len):
loop_sig_idx=0

dec_sig_val[idx] = loop_dec_idx ^ dec_sig_val[idx] ^ ord(cur_sig)
loop_dec_idx+=1
idx+=1
if loop_dec_idx >= 252:
loop_dec_idx = 1

aes_key = []

for i in range(0,32):
aes_key.append(______progs_board_fw_sign_data[i+32])

iv = []

for i in range(0,16):
iv.append(______progs_board_fw_sign_data[i+96])

print('iv before dec: ',iv)
print('aes_key before dec: ',aes_key)
dec_val_by_sig(iv,16,'wrgac25_dlink.2013gui_dir850l')
dec_val_by_sig(aes_key,32,'wrgac25_dlink.2013gui_dir850l')
print('iv before dec: ',iv)
print('aes_key before dec: ',aes_key)

aes_decrypt_file(aes_key,iv,'/mnt/e/EdgeDownload/IOT/DIR850LB1_FW220WWb03.bin','out.bin')

解密后即可解包

lJZRR.png

-------------文章就到这里啦!感谢您的阅读XD-------------