全球资讯

深入分析NSA用了5年的IIS漏洞

1.1 漏洞简介

2017年3月27日,来自华南理工大学的 Zhiniang Peng 和 Chen Wu 在 GitHub [1] 上公开了一份 IIS 6.0 的漏洞利用代码,并指明其可能于 2016 年 7 月份或 8 月份被用于黑客攻击活动。

该漏洞的编号为 CVE-2017-7269 [2],由恶意的 PROPFIND 请求所引起:当 If 字段包含形如 <http://localhost/xxxx> 的超长URL时,可导致缓冲区溢出(包括栈溢出和堆溢出)。

微软从 2015 年 7 月 14 日开始停止对 Windows Server 2003 的支持,所以这个漏洞也没有官方补丁,0patch [3] 提供了一个临时的解决方案。

无独有偶,Shadow Brokers 在2017年4月14日公布了一批新的 NSA 黑客工具,笔者分析后确认其中的 Explodingcan 便是 CVE-2017-7269 的漏洞利用程序,而且两个 Exploit 的写法如出一辙,有理由认为两者出自同一团队之手:

  • 两个 Exploit 的结构基本一致;
  • 都将 Payload 数据填充到地址 0x680312c0
  • 都基于 KiFastSystemCall / NtProtectVirtualMemory 绕过 DEP;

本文以 3 月份公布的 Exploit 为基础,详细分析该漏洞的基本原理和利用技巧。

1.2 原理概述

  • CStackBuffer 既可以将栈设置为存储区(少量数据)、也可以将堆设置为存储区(大量数据);
  • CStackBuffer 分配存储空间时,误将 字符数 当做 字节数 使用,此为漏洞的根本原因;
  • 因为栈上存在 cookie,不能直接覆盖返回地址;
  • 触发溢出时,改写 CStackBuffer 对象的内存,使之使用地址 0x680312c0 作为存储区;
  • 将 Payload 数据填充到 0x680312c0
  • 程序存在另一处类似的漏洞,同理溢出后覆盖了栈上的一个指针使之指向 0x680313c0
  • 0x680313c0 将被当做一个对象的起始地址,调用虚函数时将接管控制权;
  • 基于 SharedUserData 调用 KiFastSystemCall 绕过 DEP;
  • URL 会从 UTF-8 转为 UNICODE 形式;
  • Shellcode 使用 Alphanumeric 形式编码(UNICODE);

2. 漏洞原理

2.1 环境配置

在 Windows Server 2003 R2 Standard Edition SP2 上安装 IIS 并为其启用 WebDAV 特性即可。
为IIS启用WebDAV特性

修改 Exploit 的目标地址,执行后可以看到 svchost.exe 启动 w3wp.exe 子进程,后者以 NETWORK SERVICE 的身份启动了 calc.exe 进程 。
CVE-2017-7269 IIS远程代码执行漏洞exploit

2.2 初步调试

首先,为进程 w3wp.exe 启用 PageHeap 选项;其次,修改 Exploit 的代码,去掉其中的 Shellcode,使之仅发送超长字符串。

执行之后 IIS 服务器上会启动 w3wp.exe 进程(并不会崩溃),此时将 WinDbg 附加到该进程并再次执行测试代码,即可在调试器中捕获到 first chance 异常,可以得到以下信息:

  • httpext!ScStoragePathFromUrl+0x360 处复制内存时产生了堆溢出;
  • 溢出的内容和大小看起来是可控的;
  • 被溢出的堆块在 httpext!HrCheckIfHeader+0x0000013c 处分配;
  • 崩溃所在位置也是从函数 httpext!HrCheckIfHeader 执行过来的;
  • 进程带有异常处理,因此不会崩溃;

2.3 CStackBuffer

崩溃所在模块 httpext.dll 会多次使用一个名为 CStackBuffer 的模板,笔者写了一份类似的代码,以辅助对漏洞原理的理解。为了简单起见,默认存储类型为 unsigned char,因此省略了模板参数 typename T

CStackBuffer 的相关特性如下:

  • 默认使用栈作为存储空间,大小由模板参数 SIZE 决定;
  • 通过 resize 可以将堆设置为存储空间;
  • 通过 fake_heap_size 的最低位标识存储空间的类型;
  • 通过 release 释放存储空间;
  • 对象的内存布局依次为:栈存储空间、堆块大小成员、存储空间指针;

CStackBuffer 的源码如下:

2.4 漏洞调试

根据之前的简单分析,可知 HrCheckIfHeader 是一个关键函数,因为:

  • 目标堆块是在这个函数中动态分配的;
  • 从这里可以执行到触发异常的函数 ScStoragePathFromUrl

函数 HrCheckIfHeader 简化后的伪代码如下所示:

可以看出这里的关键函数为 CMethUtil::ScStoragePathFromUrl,该函数会将请求转发给 ScStoragePathFromUrl,后者简化后的伪代码如下所示:

函数 HrCheckIfHeader 会调用 ScStoragePathFromUrl 两次,在第一次调用 ScStoragePathFromUrl 时,会执行如下的关键代码:

这里得到 v18 的值为 0x281c,而 *length 的值由参数传递,实际由 CStackBuffer::resize 计算得到,最终的值为 0x82,计算公式为:

显然有 0x82 < 0x281c,所以函数 ScStoragePathFromUrl*length 填充为 0x281c 并返回 1。实际上,这个值代表的是真实物理路径的字符个数

HrCheckIfHeader 第二次调用 ScStoragePathFromUrl 之前,将根据 length 的值设置 CStackBuffer 缓冲区的大小。然而,这里设置的大小是字符个数,并不是字节数,所以第二次调用 ScStoragePathFromUrl 时会导致缓冲区溢出。实际上,调用 CStackBuffer::resize 的位置就是 httpext!HrCheckIfHeader+0x0000013c,也就是堆溢出发生时通过 !heap -p -a edi 命令得到的栈帧。

小结:

  • 函数 ScStoragePathFromUrl 负责将 URL 请求中的文件路径转换为实际的物理路径,函数的名字也印证了这一猜想;
  • 第一次调用此函数时,由于缓冲区大小不够,返回实际物理路径的字符个数;
  • 第二次调用此函数之前先调整缓冲区的大小;
  • 由于缓冲区的大小设置成了字符个数,而不是字节数,因此导致缓冲区溢出;
  • 两次调用同一个 API 很符合微软的风格(第一次得到所需的空间大小,调整缓冲区大小后再次调用);

3. 漏洞利用

3.1 URL 解码

在函数 HrCheckIfHeader 中,首先调用 CRequest::LpwszGetHeader 来获取 HTTP 头中的特定字段的值,该函数简化后的伪代码如下所示:

可以看出这里通过 CHeaderCache 建立缓存机制,此外获取到的值会通过调用 ScConvertToWide 来进行转换操作。事实上,ScConvertToWide 会调用 MultiByteToWideChar 对字符串进行转换。

由于存在编码转换操作,Exploit 中的 Payload 需要先进行编码,这样才能保证解码后得到正常的 Payload。字符串转换的调试日志如下所示:

3.2 栈溢出

根据前面的分析,可以知道当字符串超长时是可以导致堆溢出的,但问题是堆块的基地址并不是固定的。实际上,当 CStackBuffer 使用栈作为存储空间时,也可以触发栈溢出,原理和堆溢出是一样的。

当然,这里不是通过栈溢出来执行代码,因为栈上有 cookie

在函数 HrCheckIfHeader 中存在两个 CStackBuffer 实例:

基于前面对 CStackBuffer 内存布局的分析,可以知道这里栈空间的分布为:

下面要重点分析的代码片段为:

(1) HrCheckIfHeader 第一次调用 ScStoragePathFromUrl 时传递的参数分析如下(函数返回值为 1,长度设置为 0xaa):

(2) 因为 ScStoragePathFromUrl 返回 0xaa,所以 buffer1.resize(0xaa) 并不会在堆上分配空间,而是直接使用栈上的 buffer

(3) 第二次调用 ScStoragePathFromUrl 时会导致栈溢出,实际结果是 CStackBuffer1.fake_heap_size 被改写为 0x02020202CStackBuffer1.heap_buffer 被改写为 0x680312c0

3.3 填充数据

通过!address 命令可知地址 0x680312c0 位于 rsaenh 模块中,具备 PAGE_READWRITE 属性。

在解析 http://localhost/bbbbbbb...... 时,数据将被直接填充到地址 0x680312c0。此时,由于 CStackBuffer1 的长度已经 足够大ScStoragePathFromUrl 只会被调用一次。

3.4 控制 EIP

在函数 HrCheckIfHeader 返回后,后面会跳转到 CParseLockTokenHeader::HrGetLockIdForPath 中去执行,而后者也会多次调用 CMethUtil::ScStoragePathFromUrl 这个函数。同样,解析 URL 第一部分(http://localhost/aaaaaaa....)时完成栈溢出,此时会覆盖到一个引用 CMethUtil 对象的局部变量;在解析 URL 第二部分(http://localhost/bbbbbbb....)时,因为 CMethUtil 已经伪造好,其成员 IEcb 实例同样完成伪造,最后在 ScStripAndCheckHttpPrefix 中实现 EIP 的控制。

(1) FGetLockHandle 分析
函数 FGetLockHandle 里面构造了一个 CParseLockTokenHeader 对象,存储于栈上的一个局部变量引用了这个对象 (这一点很重要),调用该对象的成员函数 HrGetLockIdForPath 进入下一阶段。

(2) HrGetLockIdForPath 分析
HrGetLockIdForPathHrCheckIfHeader 有点类似,同样存在两个 CStackBuffer 变量。不同的是,v22.HighPart 指向父级函数 HrGetLockIdForPath 中引用 CParseLockTokenHeader 对象的局部变量,而且这里也会将其转换为 CMethUtil 类型使用。

在解析 URL 第一部分(http://localhost/aaaaaaa....)时,通过栈溢出可以覆盖引用 CParseLockTokenHeader 对象的局部变量,栈布局如下所示。

栈上的数据分布如下所示:

分析栈的布局可以知道,在复制 260+12*4=308 字节数据后,后续的 4 字节数据将覆盖引用 CParseLockTokenHeader 对象的局部变量。需要注意的是,这里所说的 308 字节,是 URL 转变成物理路径后的前 308 字节。执行完 CMethUtil::ScStoragePathFromUrl 之后,680313c0 被填充到父级函数中引用 CParseLockTokenHeader 对象所在的局部变量。

(3) ScStripAndCheckHttpPrefix 分析
在解析 URL 第二部分(http://localhost/bbbbbbb....)时,由于引用 CParseLockTokenHeader 对象的局部变量的值已经被修改,所以会使用伪造的对象,最终在函数 ScStripAndCheckHttpPrefix 中完成控制权的转移。