路由器无限重启救砖之旅

路由器无限重启救砖之旅

挖掘路由器漏洞时,发现了一个命令执行漏洞,正好有重启设备的需求,于是通过漏洞传递 reboot 命令重启,没想到的是设备变砖了。分析变砖原因,发现传递的参数会保存 NVRAM 中,开机的时候会读取并执行,于是造成了设备的拒绝服务。手头这个设备也是好不容易淘来的,这可怎么是好。开始救砖吧!

尝试一——恢复记忆

由于 reboot 命令存储到了路由器的 NVRAM 配置中,将路由器恢复出厂设置还原 NVRAM 中的值理论上可行。正好路由器上有 reset 按键,“摁”住它一段时间,测试了好几次没反应。这种方法就不回来。猜测是负责恢复出厂设置程序在设备自动重启之间没有启动。

尝试二——争分夺秒

软的不行来硬的,直接拆。拆了发现预留有串口。通过串口进入系统 shell,与自动重启争分夺秒。希望web服务启动能有点延时,在重启之前,删除 reboot。

1
nvram set name=""

输入了用户名密码,还没进入shell 就重启了。这个方法也不行,时间来不及。

image-20210217162552577

尝试三——紧急救援

进入单用户模式,修改启动参数进入单用户模式,U-Boot 没有 saveenv 命令,使用 setenv 修改启动参数后无法保存。这种方法也受阻了,如果能进入单用户模式,可以恢复参数进行救砖。

image-20210203132138347

尝试四——内存修改

首先提取出固件定位到问题所在,然后修改好固件后,最后在 U-Boot 中直接刷写。

1. 提取固件

读取内存:Flash 在内存的地址使用命令 bdinfo 查看,。

1
2
3
4
5
6
7
8
9
10
ath> bdinfo
boot_params = 0x87F77FB0
memstart = 0x80000000
memsize = 0x08000000
flashstart = 0x9F000000
flashsize = 0x01000000
flashoffset = 0x0002BD20
ethaddr = 00:AA:BB:CC:DD:EE
ip_addr = 10.10.10.123
baudrate = 115200 bps

Flash 的起始地址为 0x9F000000,大小为 0x01000000(16M)。16M = 16777216 Bit,16777216/32 = 524288=0x80000。启动xshell 的日志记录功能。等待两个小时左右,完成读取。

1
2
3
4
5
6
ath> md 0x9F000000 80000
9f000000: 100000ff 00000000 100000fd 00000000 ................
9f000010: 10000175 00000000 10000173 00000000 ...u.......s....
9f000020: 10000171 00000000 1000016f 00000000 ...q.......o....
9f000030: 1000016d 00000000 1000016b 00000000 ...m.......k....
9f000040: 10000169 00000000 10000167 00000000 ...i.......g....

然后把日志中的内容转化为二进制。

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
import re

pre_index = 0
index = 0
#检查日志中的内容是否完整
with open(r"md_flash.log".encode(),encoding="utf-8") as log:
for line in log.readlines():
index = int(line.split(":")[0],16)
if pre_index == 0:
pre_index = index
continue
if index - pre_index != 0x10:
print(hex(pre_index)," to ",hex(index),"data absence")
break
else:
pre_index = index

with open(r"md_flash.log".encode(),encoding="utf-8") as log:
data = log.read()

#末尾加一个换行符,方便正常统一处理
data = data+"\n"

# 去掉字符
data = re.sub(r"\s\s\s\s.*\n","",data)
# 去掉地址
data = re.sub(r"[0-9a-zA-Z]{8,8}:\s","",data)
# 去掉空格
data = re.sub(r"\s","",data)

with open("flash.bin","bw") as f:
f.write(bytes.fromhex(data))

2. 分析固件

binwalk 分析对固件的分析结果。

image-20210217162647088

现在手里有串口,当然得参考串口日志信息。根据串口打印的 Flash 中配置分区的信息。

1
2
[    9.465000] Creating 9 MTD partitions on "spi0.0":
[ 9.519000] 0x000000fe0000-0x000000ff0000 : "Config"

使用 dd 拆分出各模块,下面是可能与 NVRAM 有关的模块。

1
2
3
4
root@kali#dd if=flash.bin of=config.bin bs=1 count=65535 skip=16646144
65535+0 records in
65535+0 records out
65535 bytes (66 kB, 64 KiB) copied, 1.46555 s, 44.7 kB/s

binwalk 分析出 config 中的内容是经过 gzip压缩的。

image-20210217162738728

binwalk 提取出配置文件,里面是明文存储着的配置数据。

image-20210217162440505

找到了罪魁祸首。

image-20210217162915051

修改好之后,进行压缩。

1
root@kali:~# gzip C  -9 -c > config_m.bin 

最后加上加上分区头,就重构了配置分区,接下来就要想办法写入到 Flash 中。

3. 修改 Flash

搞 IOT 安全的并不是都熟悉嵌入式开发,搞嵌入式开发的不一定理解我们的需求,问了搞嵌入式的朋友他会不会。我是第一次通过 U-Boot 刷写固件,由于缺乏基础知识走了不少弯路,写这篇文章的目的也是帮助有同样需求的人少走弯路。U-Boot 固件刷写不能直接写入到 Flash 中,需要按照下图的流程操作。

1
2
3
4
graph LR;
上传固件到内存中 --> 擦除Flash;
擦除Flash --> 关闭Flash保护;
关闭Flash保护 --> 从内存中拷贝固件到Flash中;

首先尝试用 FTP、loady、loads 上传文件,实际使用时选择一种即可。最后重新刷写配置分区,救活路由器。

1.1) 使用 FTP 上传固件

printenv 命令查看环境变量可知预设服务器的地址为10.10.10.3。

image-20210201145923744

找一根网线连接路由器和电脑,然后给电脑设置静态 IP 地址 10.10.10.3,掩码 255.255.255.0。然后,测试一下连通性。在路由器串口中 ping 电脑。

image-20210201153709706

host 10.10.10.3 存活说明连接成功。另外,还准备一个 TFTP 服务器,刚开始用的 FileZilla,不行发现还需要 DHCP 服务器,有找个dhcpsrv ,最后发现还是 TFTPD比较好用。打开就能用,配置也很简单,FTP 设置一下路径,DHCP 需要设置IP等,如下图。

image-20210202112524412

参考环境变量中的la,如果环境变量中没有可以参考的,那就需要在内存中寻找一块足够大的空白区域。

1
la=tftp 0x80060000 data.bin&&erase 0x9fff0000 +$filesize&&protect off all&&cp.b $fileaddr 0x9fff0000 $filesize

将固件加载到地址 0x80060000。

image-20210203171118562

1.2) 使用 loady 上传固件

命令格式为 loady address,然后使用 YMODEM 发送修改后的文件。

image-20210204111142453

image-20210217171100965

1.3) 使用 loads 上传固件

首先将二进制文件转为S-Record文件。

image-20210204112130922

​ 使用默认配置,导出即可。

image-20210204112818418

S-Record文件中的数据都是以ASCII码的格式存储,正好使用 Xshell ASCII 字符传输修改后的文件。命令格式为 loads address

image-20210204114159142

选择文件,等待片刻写入完成。

image-20210204114321805

2) 刷写配置分区

刷血配置分区,加载到内存的数据拷贝到 Flash 中。

  1. 擦除 Config 分区

    image-20210203170300054

  2. 关闭 flash 保护

    image-20210203171244274

  3. 从Momory 中复制到 Flash 中。

    image-20210203172021000

  4. 查看写入的数据

    image-20210203172703641

4. 重启

输入 reset 重启,路由器正常运行,救砖成功。

尝试五——物理伤害

除了U-Boot 刷写还可以直接物理操作,使用 FT232H 连接 SOP8 封装的 SPI Flash,用 Flashrom 刷入固件。

压缩修改后的配置文件。

1
root@kali:~# gzip C  -9 -c > config_m.bin 

截取分区头。

1
2
3
4
root@kali:~# dd if=../config.bin of=header.bin bs=1 count=12
12+0 records in
12+0 records out
12 bytes copied, 0.000132176 s, 90.8 kB/s

计算需要填充的0xFF的大小,然后使用 dd 创建填充块。

1
2
3
4
5
6
7
8
9
10
root@kali:~# ls -l config_m.bin 
-rw-r--r-- 1 root root 34289 Jan 31 23:08 config_m.bin
root@kali:~# ls -l config.bin
-rwxrw-rw- 1 root root 65535 Jan 27 03:15 config.bin
root@kali:~# python3 -c "print(65535-12-34289)"
31246
root@kali:~# dd if=/dev/zero bs=1 count=31234 | tr "\000" "\377" >ff.bin
31234+0 records in
31234+0 records out
31234 bytes (31 kB, 31 KiB) copied, 0.0801646 s, 390 kB/s

最后,将所有的分区拼接起来形成完整的固件。

1
2
3
root@kali:~# cat header.bin config_m.bin ff.bin  > config_new.bin
root@kali:~# ls -l config_new.bin
-rw-r--r-- 1 root root 65535 Jan 31 23:24 config_new.bin

以上操作完成了固件的重打包,然后就可以使用 Flashrom 写入新固件。

SOP 夹子夹不稳,只能手扶着。

image-20210217165552187

写入时间还是比较长,手有点受不了,可能导致接触不良,写入失败。

image-20210202172331541

然后,就使用芯片测试夹连接,这样稳一下。

image-20210217165714575

此时写入成功,但是验证失败。

image-20210202173126095

上电,并双手合十,登录界面维持住了,Web 管理端也能访问了,救砖成功。

image-20210217163301489

其他方法

除以上 5 种方法,还有更简单粗暴的方法,但需要一个正常的设备,或之前备份过的正常完整 Flash。操作比较简单直接读取正常设备的 Flash 并写入变砖的设备中即可。

总结

救砖,首先需要分析搬砖的原因,然后对症下药。我这次是配置参数引起的,所以我首先想到的是恢复出厂设置,但恢复出厂模式需要长按数秒,此时设备已经重启,这种方法就不行了;然后想到进入 shell 快速还原配置参数修复,结果是还没进入shell,设备就重启了;设备重启是 Web 服务启动时触发,只要不启动 Web 服务设备就不会重启,于是就尝试了一下进入单用户模式,然而 U-Boot中修改后的启动参数却无法保存; 从系统层的修复已经无计可施只能从底层出发,于是对固件进行提取分析,找到问题点修复后重新刷写,刷写尝试了不同的方法,一是通过U-Boot刷写,另外就是直接操作 Flash。

此次救砖还是花了不少时间的,通过实践了解了 NVRAM 的物理存储方式,还尝试了 U-Boot 中多种固件刷写方法。

参考

本文首发于安全客