作者: 360漏洞研究院 李雙
原文鏈接:https://vul.360.net/archives/648

背景

Foxit Reader(舊名:Foxit PDF Reader),是一套用來閱讀PDF格式文件的軟件,由福建福昕軟件所研發。

在 Adobe Reader 以及舊版本的 Foxit Reader 中,通常會利用 JS 的 ArrayBuffer 來布局內存并最終實現任意代碼執行。然而,最新版本 Foxit Reader 中的 ArrayBuffer 已經被禁用,這導致漏洞利用的難度很大,目前還未見到網上有公開的漏洞利用方案。

這篇文章主要總結我在研究 Foxit Reader 漏洞利用的過程中積累的一些經驗,從 Foxit Reader 的一套內存管理機制的角度出發,探索潛在的漏洞利用方式,為大家提供一些可能的思路。

內存管理

Foxit Reader 中可能存在有多套內存管理機制,其在文件解析中自己實現了一個內存池堆分配器來管理內存。

每個使用 malloc 分配的 chunk 都取自一個固定 0x1000 大小的池中。每個池都會分割成若干個指定 size 的 chunk,擁有相同的 chunk size 的池之間通過指針構成雙向鏈表。所有的雙向鏈表的頭指針都保存在一個全局數組變量中。

+--------+
| 0x8    |--------> +--------+        +--------+
+--------+          | head   | <====> | head   | <====> ...
| 0x10   |------+   +--------+        +--------+
+--------+      |   | chunk  |        | chunk  |
| 0x18   |----+ |   +--------+        +--------+
+--------+    | |   | chunk  |        | chunk  |
| ...    |    | |   +--------+        +--------+
              | |   | ...    |        | ...    |
              | |
              | +-> +--------+        +--------+
              |     | head   | <====> | head   | <====> ...
              |     +--------+        +--------+
              |     | chunk  |        | chunk  |
              |     +--------+        +--------+
              |     | chunk  |        | chunk  |
              |     +--------+        +--------+
              |     | ...    |        | ...    |
              |
              +---> +--------+        +--------+
                    | head   | <====> | head   | <====> ...
                    +--------+        +--------+
                    | chunk  |        | chunk  |
                    +--------+        +--------+
                    | chunk  |        | chunk  |
                    +--------+        +--------+
                    | ...    |        | ...    |

當通過 malloc 分配小于 0x400 size 的 chunk 時,直接從全局數組變量 poolheadarray 中取得對應 size 的池,然后取得它的 nextfreechunk 返回。若 nextfreechunk 不存在,則分配一個新的 chunk。

//...
  if ( size > 0x400 )
    return sub_1D92C70(a1, size);
  idx = (size - 1) >> 3;
  pool = poolheadarray[2 * idx];
  if ( pool == pool->nextpool )
    return sub_1D92D60(a1, (size - 1) >> 3);
  v5 = pool->nextfreechunk;
  ++pool->ref;
  v6 = *v5;
  pool->nextfreechunk = *v5;
  if ( !v6 )
  {
    sub_1D93260(pool, idx);
    return v5;
  }
//...

當通過 free 釋放時,將 chunk 鏈接到 nextfreechunk 下。

  v4 = *((a2 & 0xFFFFF000) + 4);
  *a2 = v4;
  v5 = pool->ref - 1;
  *((a2 & 0xFFFFF000) + 4) = a2;
  pool->ref = v5;

池的結構

池總是固定為 0x1000 大小,其前 0x20 字節保存了池的相關信息:

base +--------+---------------+----------+-----------+
     |        | nextfreechunk | prevpool | nextpool  |
     +--------+---------------+----------+-----------+
     |        | sizeidx       | offset   | maxoffset |
     +--------+---------------+----------+-----------+
     |        |               |          |           |

相關成員如下

  • nextfreechunk

所有被釋放掉的空閑 chunk 構成一個單向鏈表,nextfreechunk 指向鏈表頭。分配內存時總是從 nextfreechunk 首先獲取。

nextfreechunk 為空,則存在兩種情況:所有 chunk 已經全部分配或者還有從未分配過的 chunk。

若所有 chunk 已經全部分配則會取下一個池,然后重復上述步驟。

  • prevpoolnextpool

指向上一個池和下一個池。

  • sizeidx

當前池對應的 chunk 大小的索引,chunk 的大小 8 字節對齊。

  • offsetmaxoffset

在一個池初次創建時,所有的 chunk 都是空閑狀態,但是 nextfreechunk 并不會指向他們,只有所有已分配的 chunk 經歷過 free 之后才能通過 nextfreechunk 進行之后的分配。而第一次的分配就需要 offsetmaxoffset 來判斷下一個應該分配的 chunk。

offset 指向下一個尚未分配過的 chunk,maxoffset 指向池的末尾。當 offset < maxoffset 時,說明仍有尚未進行過分配的 chunk,若此時 nextfreechunk 為空,則返回 offset 處指向的 chunk。

free chunk 的結構

釋放后的 chunk 會被插入到空閑鏈表中, nextfreechunk 將指向該 chunk,而 nextfreechunk 原來所指向的 chunk 將放入該 chunk 中,其結構圖如下。

+---------------+--------+--------+--------+
| nextfreechunk |        |        |        |
+---------------+--------+--------+--------+
|               |        |        |        |

漏洞利用

通過上述對 Foxit Reader 內存管理的分析,我們會有一些可能存在的漏洞利用的想法。

由于當前版本的 Foxit Reader 沒有開啟 CFG 機制,所以我們將很容易通過劫持虛表之類的方法劫持控制流從而實現任意代碼執行。

Foxit Reader 的堆分配器主要通過一個空閑鏈表分配和釋放內存,該空閑鏈表是一個單向鏈表,并且缺少對鏈表完整性的檢驗,因此如果我們有機會劫持 nextfreechunk 指針,我們將有機會操縱任意的內存塊。

針對這種情況,我們可能需要有一個信息泄露和一個越界寫漏洞,通過越界寫覆蓋下一個 chunk 的 nextfreechunk 為我們想要的地址,然后進行后續利用。

并且不止于此,由于 nextfreechunk 通常與 C++ 對象的虛表指針處在同一位置,所以當存在一個 UAF 漏洞,并且在釋放后調用虛函數時,我們還有機會直接劫持控制流。

例如當前存在如下內存布局:

|          |          |          |          |
+----------+----------+----------+----------+ <- free chunk
| 0        | AAAAAAAA | AAAAAAAA | AAAAAAAA |
+----------+----------+----------+----------+ <- victim obj
| vtb      |          |          |          |
+----------+----------+----------+----------+
|          |          |          |          |

其中 free chunk 的第一個成員指向 0,表示它是空閑鏈表的最后一個 chunk。

victim obj 為 C++ 對象,其第一個成員為虛表指針。

若 victim obj 存在 uaf 漏洞,在釋放后,其第一個成員指向 free chunk。

    |          |          |          |          |
+-> +----------+----------+----------+----------+ <- free chunk
|   | 0        | AAAAAAAA | AAAAAAAA | AAAAAAAA |
|   +----------+----------+----------+----------+ <- victim obj
+-- | nextfree |          |          |          |
    +----------+----------+----------+----------+
    |          |          |          |          |

此后在調用虛函數 obj.vtb->field_4() 時,就會執行到我們提前在內存中布局的地址 0x41414141 處。

總結

Foxit Reader 實現的這套堆分配器在面對涉及大量的小塊內存的對象申請與釋放時將會有很高的效率,這可能也是 Foxit Reader 在很多方面的性能超過 Adobe Reader 的原因之一。然而這套堆分配器的特點也為漏洞利用提供了一些可能性。

這篇文章分享的漏洞利用思路還不太成熟,真正達到任意代碼執行的目的事實上也十分困難。因為實際上 Foxit Reader 的 JS 引擎并沒有使用這套堆分配器,盡管這樣的思路不需要很復雜的內存布局,但是如果無法通過 JS 或類似的方式操縱內存,仍然很難將內存布局成預期的效果。同樣的原因也使信息泄露比較困難,然而信息泄露對于這兩種思路又是十分必要的。另外,由于空閑鏈表是一條單向鏈表,所以需要保證指針的合法性或者置0,否則可能造成崩潰,這也給利用增加了難度。


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