WebSpellChecker 堆栈缓冲区溢出
发现 WebSpellChecker 中的漏洞
在今天的博客文章中,我们将讨论 ReLabs 团队如何在 WebSpellChecker 中发现一个栈溢出漏洞。
WebSpellChecker 是 Salesforce 平台使用的一个第三方服务,通过 Web API 提供拼写检查功能。该软件有两个版本:托管版或授权版。托管版运行在 WebSpellChecker 的服务器上,而授权版需要运行在您自己组织的服务器上。授权版支持不同的平台:Windows(32位或64位)和 Linux(32位或64位)。本文将讨论在所有平台的授权版中发现的一个栈缓冲区溢出。
该软件包含多个组件,如下图所示:
[按回车键或单击以查看完整尺寸的图片]
位于中心的 SSRV 组件是一个二进制程序,一个在服务器中运行的 CGI。AppServer 是另一个通常与 CGI 运行在同一服务器上的二进制程序。AppServer 是真正执行拼写检查的应用程序,而 CGI 则负责验证、解析和解码由 Web 组件发送的参数。CGI 可以从互联网访问,并且可以由用户提供所需的参数。这一点,再加上它是一个在服务器上运行的二进制程序,使其成为寻找漏洞的最佳切入点。
模糊测试
第一种方法是模糊测试 CGI,试图找到漏洞。我们选择在 64 位 Linux 版本上测试 CGI,因为这是我们在 Salesforce 上运行的版本。首先,我们需要了解用户向 CGI 发送输入的方式以及程序期望的输入类型。为了完成这个任务,我们使用 IDA Pro 进行了静态分析。下图红色下划线标出了命令 spell、sc 和 check_spelling:
[按回车键或单击以查看完整尺寸的图片]
我们使用完整的命令和参数列表构建了一个自定义的模糊测试框架,以发现软件中的漏洞。
模糊测试结果
使用我们的模糊测试框架,我们在 Linux 64 位系统中发现了不同的访问违规异常。在下面的分析中,我们检查这些异常及其来源,以了解用户是否可以利用它们来控制执行流程。结果显示异常发生时的 RIP 地址以及该地址的汇编指令(如果有的话):
-
0000000000000000 | 无 当使用 "user_dictionary" 命令且没有提供 action 参数或使用了无效的 action 时,会发生此异常。程序使用 "action" 参数来映射并调用一个函数。但当没有有效的 action 时,这个函数指针为空,它会执行一个
CALL 0x0000000000000000。无法利用这一点来实现代码执行。以下参数会触发此异常:cmd=user_dictionary&name=test -
00000000004E1695 | jmp r11 当执行函数
void atexit_event()时,会引发此异常。在该函数内部有一个函数指针 (jmp r11)。但这个指针是无效的。用户无法控制该指针的值。以下参数会触发此异常:cmd=ospsave&dname=a -
000003999AB6672 | lock xadd DWORD PTR [rdi],eax 当调用 sprintf 后,栈上的一个局部变量被覆盖时,会引发此异常。在调用 sprintf 之后,并且在函数执行到 RET 指令之前,会执行以下函数:
__gnu_cxx::__exchange_and_add该函数接收一个指针变量并向该变量添加一个给定值。当栈被覆盖时,局部变量也会被覆盖。然后,当调用此函数时,指针变量不再指向一个有效地址,并且因为指令试图写入一个无效位置而生成异常。以下参数会触发此异常:cmd=user_dictionary&name=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=&action=create
最后一个异常非常有趣:栈被用户控制的输入数据(参数 name)溢出了,但在程序执行到 RET 指令之前就发生了异常,应用程序终止了执行。
发现的漏洞
如前所述,使用模糊测试器,我们能够在 WebSpellChecker 使用 sprintf 的方式中发现一个漏洞。Sprintf 不会限制在组合格式字符串后写入的字节数。这允许用户溢出保存最终字符串的变量。通过手动分析,我们识别出 CGI 中另外十二个以同样不安全的方式使用 sprintf 的位置(显示的地址来自 Windows 32 位版本):
0x0043FED0 sprintf(&v193, "{dname: \"%s\", action: \"%s\"}", v50, v49);
0x0044015A sprintf(&v193, "{dname: \"%s\", action: \"%s\"}", v71, v70);
0x00440596 sprintf(&v193, "{dname: \"%s\", action: \"%s\"}", v101, v100);
0x00440859 sprintf(&v193, "{dname: \"%s\", action: \"%s\"}", v119, v118);
0x004409A0 sprintf(&v193, "{dname: \"%s\", action: \"%s\"}", v129, v130);
0x00440CC5 sprintf(&v193, "{word: \"%s\", action: \"%s\"}", v138, v137);
0x00440F62 sprintf((char *)(a1 - 276), "{text: \"%s\", type: \"%d\", error: true, dname: \"%s\"}", v10, v9, v8);
0x00443D0C sprintf(&v29, (const char *)v6, v7, v5);
0x0044394F sprintf(&v41, (const char *)v5, v6, v20);
0x004432DC sprintf(&v24, (const char *)v4, v5, v6);
0x0044287C sprintf(&v29, (const char *)v6, v7, v5);
0x00442FA4 sprintf(&v42, (const char *)v14, v15, v17);
这些 sprintf 调用由以下命令触发,每条命令对应不同的 action:
cmd=dictionary&action=create&ud=1&udn=1&c=a&callback=a&format=xml&view=true&wordlist=aaa&dname=AAAAAA...["A"*1024]
cmd=dictionary&action=delete&ud=1&udn=1&c=a&callback=a&format=xml&view=true&wordlist=aaa&dname=AAAAAA...["A"*1024]
cmd=dictionary&action=rename&ud=1&udn=1&c=a&callback=a&format=xml&view=true&wordlist=aaa&dname=AAAAAA...["A"*1024]
cmd=dictionary&action=restore&ud=1&udn=1&c=a&callback=a&format=xml&view=true&wordlist=aaa&dname=AAAAAA...["A"*1024]
cmd=dictionary&action=getname&ud=1&udn=1&c=a&callback=a&format=xml&view=true&wordlist=aaa&dname=AAAAAA...["A"*1024]
cmd=dictionary&action=addword&ud=1&udn=1&c=a&callback=a&format=xml&view=true&wordlist=aaa&dname=AAAAAA...["A"*1024]
cmd=ospsave&text=create&&udn=bbbbbb&dname=AAAAAA...["A"*1024]
cmd=user_dictionary&action=setdict&name=AAAAAA...["A"*1024]
cmd=user_dictionary&action=getdic&name=AAAAAA...["A"*1024]
cmd=user_dictionary&action=check&name=AAAAAA...["A"*1024]
cmd=user_dictionary&action=rename&name=AAAAAA...["A"*1024]
注意: 字符串 AAAAAA...["A"*1024] 代表 1024 个 "A" 字符。
漏洞利用尝试
我们尝试利用栈溢出来执行代码,但遇到了一些困难。在上述指出的几乎所有 sprintf 调用中,一旦发生溢出,就会抛出异常。所述异常发生在 RET 指令执行之前,从而阻止了对执行流程的控制。
下图显示了 sprintf 调用,以及在到达返回点之前如何抛出异常。
[按回车键或单击以查看完整尺寸的图片]
一个有趣的命令是 "cmd=dictionary&action=delete"。该命令本身不会抛出代码异常,但是当一个局部变量(一个指针)被覆盖并解引用时,会引发访问违规异常。
解决这个问题的一种方法是覆盖异常处理程序(SEH),这样当异常被引发时,我们就可以控制执行流程的去向。此选项仅适用于 Windows,因为 SEH 存储在栈中。
一个问题在于,空字符串字符 0x00 不能用作输入的一部分。此字符会终止字符串,因此不能用于利用目的。
另一个问题是,所有模块都是用 SafeSEH 编译的,因此不可能用属于这些模块之一的地址覆盖 SEH。不过,如果 DEP 保护被禁用,我们仍然可以用属于堆或栈的地址覆盖 SEH。但通常栈和堆地址位于内存的低地址部分,因此它们至少包含一个 0x00 字节:0x00xxxxxx。
在某些情况下,可以部分覆盖 SEH,只替换前三个字节,并使用字符串结束符 0x00 作为第四字节。但在这种情况下,这是不可能的,因为在用户控制的字符串 %s 之后,格式字符串中还有额外的字符("})。
"{text: \"%s\", type: \"%d\", error: true, dname: \"%s\"}"
在下一节中,我们将看到 WebSpellChecker 中用于转义 0x00 字符的编码函数。
编码
发送给 WebSpellChecker CGI 的参数使用 UrlEncoding 算法进行编码。所以我们尝试使用编码为 %00 的字符 0x00,但它没有起作用。
我们识别出了执行解码的函数,试图理解为什么会发生这种情况,以及是否有可能使用其他方式在参数中写入 0x00 字符。
执行解码的函数如下:
__int64 __fastcall TextUtils::escaped2plain(char *original_string, const std::string *transformed_string, std::string *a3)
该函数解析输入,查找字符 %:
if ( (_BYTE)v10 != '%' )
找到这个字符后,函数会取接下来的两个字符,并使用函数 "sscanf" 以十六进制数的格式扫描它们:
std::string::string((std::string *)&two_chars_hex, (const std::string *)original_string, v5, 2uLL);
sscanf(two_chars_hex, "%x", &number);
之后,函数代码检查结果数字是否为 0:
if ( number )
当这个条件不满足时(number == 0),函数直接使用 % 后的第一个字符。因此无法转义 NULL 字符,因为 %00 被转换为字符 "0"(0x30)。
结论
发现了多个漏洞(包括栈缓冲区溢出),但由于不同的问题,无法利用这些漏洞来执行代码。漏洞研究员的生活并非总是充满乐趣和利润。
报告时间线:
- 首次联系供应商:2015年12月9日
- 供应商首次回应:2015年12月9日
- 供应商确认漏洞:2016年1月13日
- 供应商提供修复:2016年4月5日
(此文最初发表于 2016年11月22日) CSD0tFqvECLokhw9aBeRqm74RFNV+leWU52R650h3ngCYauDwNe/3O51To2Gim0WwtMwGEI91J9RF5TP4WRuG9IqUrqLcrEmPo2VktGy5EUNJmd4qLe/scE8nDYMuaZ6