windows10下的堆結構及unlink分析

Editor發表於2018-09-13

前言

最近除錯windows下堆的cve的時候發現對windows下的堆管理理解不夠,對javascript堆分配和利用基礎不夠,由於windows下沒有原始碼可以看,只能通過網上的部落格和偵錯程式自己學習。windows堆管理在不斷更新,部落格內容會有所偏差,接下來的筆記是windows10上的堆結構。


堆函式

函式宣告等來源為百度百科。


HeapCreate

這個函式建立一個只有呼叫程式才能訪問的私有堆。程式從虛擬地址空間裡保留出一個連續的塊,並且為這個塊特定的初始部分分配物理空間。

HANDLE HeapCreate(DWORD flOptions ,DWORD dwInitialSize , DWORD dwMaxmumSize);


引數:

flOptions:堆的可選屬性。這些標記影響以後對這個堆的函式操作,函式有—HeapAlloc , HeapFree , HeapReAlloc 和 HeapSize 。


下面給出在此可以指定的標記:

HEAP_NO_SERIALIAZE:指定當函式從堆裡分配和釋放空間時不互斥(不使用互斥鎖)。當不指定該標記時預設為使用互斥。序列化允許多個執行緒操作同一個堆而不會錯誤。這個標記是可忽略的。

HEAP_SHARED_READONLY:這個標記指定這個堆只能由建立它的程式進行寫操作,對其他程式是隻讀的。如果呼叫者不是可靠的,呼叫將會失敗,錯誤程式碼ERROR_ACCESS_DENIDE 。


註解:

為了使用標記為HEAP_SHARED_READONLY的堆,執行在kernel mode(核心狀態)是必須的。

dwInitialSize:堆的初始大小,單位為Bytes。這個值決定了分配給堆的初始物理空間大小。這個值將向上舍入知道下個page boundary(頁界)。若需得到主機的頁大小,使用GetSystemInfo 函式。

dwMaxmumSize:如果該引數是一個非零的值,它指定了這個堆的最大大小,單位為Bytes。該函式會向上舍入該值直到下個頁界,然後為這個堆在程式的虛擬地址裡保留舍入後大小的塊。如果函式 HeapAlloc 和 HeapReAlloc 要求分配的空間超過引數 dwInitialSize 指定的大小,系統會分配額外的空間給該堆直到這個堆的最大大小。

If dwMaximumSize is nonzero, the heap cannot grow and an absolute limitation arises where all allocations are fulfilled within the specified heap unless there is not enough free space. (如果該引數非零,除非沒有足夠的空間,這個堆總可以增長到該大小)。如果該引數為零,那麼該堆大小的唯一限制是可用的記憶體空間。分配大小超過 0x0018000 Bytes的空間總會失敗,因為獲得這麼大的空間需要系統呼叫 VirtualAlloc 函式。需要使用大空間的應用,應該把該引數設定為零。


返回值:

成功:一個指向新建立的堆的指標。

失敗:NULL

呼叫函式 GetLastError 獲得更多的錯誤資訊。


附註:

這個函式在呼叫程式裡建立一個私有堆,程式可呼叫 HeapAlloc 函式分配記憶體空間。這些頁在程式的虛擬空間內建立了一個塊,在那裡堆可以增長。

如果 HeapAlloc 函式請求的空間超過了現有的頁大小,物理空間足夠的話,額外的空間將會從已保留的空間裡附加。

只有建立私有堆的程式才可以訪問私有堆。

如果一個DLL(動態連結庫)建立了一個私有堆,那麼這個私有堆是在呼叫該DLL的程式的地址空間內,且僅該程式可訪問。

系統會使用私有堆的一部分空間去儲存堆的結構資訊,所以,不是所有的堆內空間對程式來說是可用的。例如:HeapAlloc函式從一個最大大小為 64KB 的堆裡申請 64KB 的空間,由於系統佔用了一部分空間,這個請求通常會失敗。


HeapAlloc

LPVOID HeapAlloc(

HANDLE hHeap,

DWORD dwFlags,

SIZE_T dwBytes,

);

hHeap:要分配堆的控制程式碼,可以通過HeapCreate()函式或GetProcessHeap()函式獲得。

dwFlags:堆分配時的可選引數,其值可以為以下的一種或多種。

  • HEAP_GENERATE_EXCEPTIONS:如果分配錯誤將會丟擲異常,而不是返回NULL。異常值可能是STATUS_NO_MEMORY, 表示獲得的記憶體容量不足,或是STATUS_ACCESS_VIOLATION,表示存取不合法。
  • HEAP_NO_SERIALIZE:不使用連續存取。
  • HEAP_ZERO_MEMORY:將分配的記憶體全部清零。

dwBytes:要分配堆的位元組數。


HeapFree

BOOL HeapFree(

HANDLE hHeap,

DWORD dwFlags,

LPVOID lpMem

);

Heap Entry

Heap Entry類似於linux下的chunk。

前八個位元組儲存結構資訊,類似chunk頭,但是windows為了安全性,對前八個位元組進行了加密,加密方式:與HEAP結構0x50偏移處八個位元組異或(ps:此處HEAP 結構先理解為arena,後續再說),可以有效防止堆溢位。

由於被加密,windbg顯示混亂:


windows10下的堆結構及unlink分析

於是通過x32dbg進行除錯過程:

windows10下的堆結構及unlink分析

先把解密金鑰提取出來,為:56 8B 12 F9 5B 64 00 00。

下面進行一次alloc堆的分配觀察,Heap Entry結構:

windows10下的堆結構及unlink分析


charp = (char)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);

提取頭部資訊,異或解密得到eb 03 07 ef 90 00 00 18。

分配的大小為8000位元組,得到頭部資訊大小為0x3eb,大小以8位元組為單位,所以真實大小應該為0x3eb8=8024位元組。

大小包含頭部資訊及最後16位的填充:


windows10下的堆結構及unlink分析

分配出來的堆塊地址為0x019C0480:

windows10下的堆結構及unlink分析


相鄰下一堆塊地址為分配出來的堆塊,地址為0x019C023d8。

驗證發現0x019C023d8-0x019C0480=8024。

此時確定前兩位元組為size,通過查詢第三位元組為flag。

flag標誌位

  • 0×01 該塊處於佔用狀態
  • 0×02 該塊存在額外描述
  • 0×04 使用固定模式填充堆塊
  • 0×08 虛擬分配
  • 0×10 該段最後一個堆塊

此處分配的flag為07,表示該塊處於佔用狀態,該塊存在額外描述,使用固定模式填充堆塊。

下一處空閒的堆塊flag為0x4,使用固定模式填充堆塊。

第四位元組用於檢測堆有效性的cookie。

提取相鄰堆塊解密頭部為81 01 04 84 eb 03。

第5-6位元組為pre_size,上一堆塊的大小。驗證,分配堆塊的pre_size=0x90,0x908=0x480。0x019C0480-0x480=HeapCreate返回地基址。

第7位元組堆塊所在段的序號,未驗證。

第8位元組為UnusedBytes。未用到的位元組,如分配出來的堆塊頭部及最後的16位元組填充未使用,故第八位元組為0x18。


相比較linux的堆漏洞利用,windows要多出一步資訊洩露,下面是UAF的一個演示。通過堆的讀和寫得到加密金鑰:

nt main()

{

HANDLE hHeap = HeapCreate(HEAP_NO_SERIALIZE, 10000, 40000);char * p = (char *)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);

bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);

HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);

void * pp = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);

DWORD z = *(((DWORD *)p) + 0x112)^ 0xE20404E2;

DWORD z1 = *(((DWORD *)p) + 0x113)^ 0x45;

return 0;

}

HEAP_SEGMENT

一個heap結構有多個heap_segment,當堆的大小不足以分配時,建立新的堆段分配新的空間。堆段通過連結串列進行連線:


windows10下的堆結構及unlink分析

heap_segment分配在堆裡,所以整體也算是一個HEAP_ENTRY,前8個位元組為HEAP_ENTRY頭部。

堆段的簽名為0Xffeeffee;

偏移為0x10為堆段的連結串列;

0x18為堆段隸屬的heap;

0x20為堆塊分配的頁的數目。


當需要分配的空間大小>這個數目*頁時,會建立一個新的heap_segment並鏈入連結串列中。

FirstEntry,LastValidEntry 分別為堆段中第一個,及最後一個HEAP_ENTRY結構。

堆段中主要儲存堆段的起始及範圍、堆段隸屬的heap和堆段的連結串列。


HEAP 結構

每個HEAP有一個HEAP結構,一個heap結構有多個heap_segment。


windows10下的堆結構及unlink分析

heap結構{

heap_segment

heap頭部

}

windows10下的堆結構及unlink分析

heap結構在每個堆的起始地址,由每個堆的0號堆段和一個特殊結構拼接而成。由圖可見堆段頭部大小為0x40,前0x40位元組屬於0號堆段,0x40之後的heap結構用來儲存堆的資產及必要資訊。

+0x040 Flags : 2

+0x044 ForceFlags : 0

+0x048 CompatibilityFlags : 0

+0x04c EncodeFlagMask : 0x100000

+0x050 Encoding : _HEAP_ENTRY

+0x058 Interceptor : 0

+0x05c VirtualMemoryThreshold : 0xfe00

+0x060 Signature : 0xeeffeeff

+0x064 SegmentReserve : 0x1fd0000

+0x068 SegmentCommit : 0x2000

+0x06c DeCommitFreeBlockThreshold : 0x800

+0x070 DeCommitTotalFreeThreshold : 0x2000

+0x074 TotalFreeSize : 0x3c440

+0x078 MaximumAllocationSize : 0xfffdefff

+0x07c ProcessHeapsListIndex : 1

+0x07e HeaderValidateLength : 0x248

+0x080 HeaderValidateCopy : (null)

+0x084 NextAvailableTagIndex : 0

+0x086 MaximumTagIndex : 0

+0x088 TagEntries : (null)

+0x08c UCRList : _LIST_ENTRY [ 0x5a4ffe8 - 0x5a4ffe8 ]

+0x094 AlignRound : 0xf

+0x098 AlignMask : 0xfffffff8

+0x09c VirtualAllocdBlocks : _LIST_ENTRY [ 0x71009c - 0x71009c ]

+0x0a4 SegmentList : _LIST_ENTRY [ 0x710010 - 0x5760010 ]

+0x0ac AllocatorBackTraceIndex : 0

+0x0b0 NonDedicatedListLength : 0

+0x0b4 BlocksIndex : 0x00710260 Void

+0x0b8 UCRIndex : (null)

+0x0bc PseudoTagEntries : (null)

+0x0c0 FreeLists : _LIST_ENTRY [ 0x3c2b140 - 0x59ed748 ]

+0x0c8 LockVariable : 0x00710248 _HEAP_LOCK

+0x0cc CommitRoutine : 0x1bb2ba92 long +1bb2ba92

+0x0d0 StackTraceInitVar : _RTL_RUN_ONCE

+0x0d4 FrontEndHeap : 0x00150000 Void

+0x0d8 FrontHeapLockCount : 0

+0x0da FrontEndHeapType : 0x2 ‘’

+0x0db RequestedFrontEndHeapType : 0x2 ‘’

+0x0dc FrontEndHeapUsageData : 0x007161b0 -> 0

+0x0e0 FrontEndHeapMaximumIndex : 0x802

+0x0e2 FrontEndHeapStatusBitmap : [257] “???”

+0x1e4 Counters : _HEAP_COUNTERS

+0x240 TuningParameters : _HEAP_TUNING_PARAMETERS

+0x040 Flags 堆的flag由heapcreate時的flag引數控制,其中HEAP_GROWABLE(0x2)屬性是預設的。且私有堆的flag要 or 0x1000。


windows10下的堆結構及unlink分析


windows10下的堆結構及unlink分析


windows10下的堆結構及unlink分析


windows10下的堆結構及unlink分析

建立時引數為4,flag為 2or 4 or 0x1000=0x1006

+0x050為之後解密HEAP_ENTRY頭部的金鑰;

+0x060 Signature : 0xeeffeeff heap結構頭簽名;

+0x078 MaximumAllocationSize 允許分配的最大空間,由於heapcreate時引數選擇了0,這裡允許分配整個地址空間大小;

+0x07c heap在peb堆陣列中的索引;

+0x074 TotalFreeSize : 0x3c440 全部free堆塊的總大小;

+0x07e HeaderValidateLength heap結構頭的大小;

+0x0a4 SegmentList 堆段的連結串列,前向指標指向0號堆段,後向指標指向最後一個堆段;

+0x0c0 FreeLists 儲存整個堆的空閒堆塊,所有堆塊的空閒堆塊的集合。


unlink實驗前言

實驗嘗試做windows下的unlink攻擊,windows下有safeunlink保護,和glibc一樣,若free x發生了unlink,要求x->fd->bk=x=x->bk->fd。

於是想進行一次類似於linux unlink的攻擊,中間產生了異常事件。於是對windows的free函式進行了逆向分析。


unlink實驗

#include "stdafx.h"

#include <Windows.h>

void *pp;

void try1() {

  HANDLE hHeap = HeapCreate(HEAP_GROWABLE, 1024, 10000);

  DWORD zz = 1;

  char *p = (char *)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);

  bool bRetVal = HeapFree(hHeap, HEAP_NO_SERIALIZE, p);

  HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);

  void * ppp = HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);

    DWORD z = *(((DWORD *)p) + 0x112) ^ 0xDF0404DF;

  DWORD z1 = *(((DWORD *)p) + 0x113) ^ 0x45;

  pp=HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);

  printf("%x", pp);

  HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 528);

  HeapFree(hHeap, HEAP_NO_SERIALIZE, pp);

  *(((DWORD *)p) + 0x112) = 0x41040045 ^ z;

  *(((DWORD *)p) + 0x113) = 0x45 ^ z1;

  *(((DWORD *)p) + 0x114) = DWORD(&pp -0x1);

  *(((DWORD *)p) + 0x115) = DWORD(&pp);

  HeapFree(hHeap, HEAP_NO_SERIALIZE, ppp);

}

int main()

{

  try1();

  printf("%x", pp);

  return 0;

}

windows10下的堆結構及unlink分析

在實驗過程中heapfree會呼叫一次raiseexception函式。若為偵錯程式執行,則上面的unlink會攻擊成功;若無偵錯程式則不會成功。

得出結論,這樣的unlink攻擊不可利用,於是對windows下rtlfreeheap進行了逆向分析。

經過ida分析,得出真正的free過程在rtlpfreeheap函式中,但是此函式被保護不能直接f5。


rtlpfreeheap分析

第一步:解密heap_entry

這個函式就是windows下的堆free函式。

單步跟蹤到此函式,對我們free heap的第一個記憶體操作。

windows10下的堆結構及unlink分析

edi暫存器儲存的為我們heap結構的地址。

對heap偏移0x4c與0進行比較。

+0x04c EncodeFlagMask ,即判斷此堆塊是否進行過加密操作。若flag不為0,則進行下面的解密操作。

從偏移0x50處取出Encoding金鑰異或解密heap_entry頭部,未加密則直接跳轉至解密後流程。


第二步:heap_entry檢驗

mov al,byte ptr ds:[ebx+2]

xor al,byte ptr ds:[ebx+1]

xor al,byte ptr ds:[ebx]

cmp byte ptr ds:[ebx+3],al

heap_entry偏移0,1,2相互異或操作,與偏移3進行對比。可得出偏移3為防止heap_entry被修改的cookie值。

防止off-by-one單位元組修改heap_entry。


第三步:判斷上一相鄰堆塊

通過自己pre_size找到上一堆塊位置,計算過程:自己的地址-pre_size<<3

windows10下的堆結構及unlink分析

判斷前一堆塊是否為自己,即自己是否為堆中第一個堆塊,若為自己直接轉至第五步。


第四步:判斷上一堆塊是否free

windows10下的堆結構及unlink分析

判斷過程:encodeflag>>14 and encode金鑰 xor

若heap_entry未被加密 encodeflag>>14=0,0 and 金鑰=0,0 xor flag不變。

若heap_entry被加密 encodeflag>>14=1,1 and 金鑰=一位金鑰。一位金鑰 xor flag,flag最低位被解密。

通過test flag最低一位和1進行判斷堆塊是否free。

free則unlink,否則轉第五步。


第五步:判斷下一相鄰堆塊

windows10下的堆結構及unlink分析

通過自己size找到下一堆塊位置,計算過程:自己的地址+size*8。

解密heap_entry,進行堆塊有效性校驗(ps:對於上一相鄰堆塊不進行解密和校驗,應該是防止堆溢位)。

雖然校驗過程與第一步程式碼不同,但根據單步除錯。校驗的實際效果是一樣的。

heap_entry偏移0,1,2相互異或操作,與偏移3進行對比。


第六步:判斷下一堆塊是否freeHeapAlloc

windows10下的堆結構及unlink分析

判斷過程同第四步一致,但是若可以進行unlink,還要再次對堆塊進行校驗(unlink前都會進行校驗,不論前後)。


unlink過程

safe unlink

windows10下的堆結構及unlink分析

safe unlink的程式碼同linux下的保護如出一轍。

要求x->fd->bk=x=x->bk->fd。


失敗原因

經過一段根據free堆塊大小的判斷及合併後大小的計算,來到失敗處。

其實攻擊失敗原因很簡單,unlink後會再次對相鄰堆塊進行完整性校驗。這裡我們的fd指標被修改成我們程式中記憶體一部分,並不能完成完成性校驗,會呼叫raiseexcition(校驗過程和上方一致故不貼出)。

此步正是unlink記憶體寫入最後一個校驗(如果堆塊未啟動加密的話,偽造heap_entry為0x0000000即可通過校驗):


windows10下的堆結構及unlink分析


總結

經過unlink後會進行flag更新,連結串列更新。將堆塊內容填充,最後加密heap_entry等步驟(這裡就不繼續跟蹤了,最後的流程不長)。

經過分析可以得知,只有在堆塊未加密情況下,且能偽造heap_entry為0x0000000才能進行利用(感嘆windows的嚴謹和glibc的粗糙)。




原文作者:sixty的夢想(看雪ID)

原文連結:https://bbs.pediy.com/thread-246570.htm

轉載請註明:轉自看雪論壇



看雪閱讀推薦:

1、[原創]關於手遊王朝崛起除錯破解心得分享

2、[翻譯]加密聊天室:第二部分

3、[原創]移動樣本之初學脫殼

4、[原創]黑蘋果Hackintosh 10.13.5 High Sierra i7 8700k z370

5、[原創] 第九題 PWN-羞恥Player WriteUp


相關文章