windows10下的堆結構及unlink分析
前言
最近除錯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顯示混亂:
於是通過x32dbg進行除錯過程:
先把解密金鑰提取出來,為:56 8B 12 F9 5B 64 00 00。
下面進行一次alloc堆的分配觀察,Heap Entry結構:
charp = (char)HeapAlloc(hHeap, HEAP_NO_SERIALIZE, 8000);
提取頭部資訊,異或解密得到eb 03 07 ef 90 00 00 18。
分配的大小為8000位元組,得到頭部資訊大小為0x3eb,大小以8位元組為單位,所以真實大小應該為0x3eb8=8024位元組。
大小包含頭部資訊及最後16位的填充:
分配出來的堆塊地址為0x019C0480:
相鄰下一堆塊地址為分配出來的堆塊,地址為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,當堆的大小不足以分配時,建立新的堆段分配新的空間。堆段通過連結串列進行連線:
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。
heap結構{ heap_segment heap頭部 }
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。
建立時引數為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; }
在實驗過程中heapfree會呼叫一次raiseexception函式。若為偵錯程式執行,則上面的unlink會攻擊成功;若無偵錯程式則不會成功。
得出結論,這樣的unlink攻擊不可利用,於是對windows下rtlfreeheap進行了逆向分析。
經過ida分析,得出真正的free過程在rtlpfreeheap函式中,但是此函式被保護不能直接f5。
rtlpfreeheap分析
第一步:解密heap_entry
這個函式就是windows下的堆free函式。
單步跟蹤到此函式,對我們free heap的第一個記憶體操作。
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
判斷前一堆塊是否為自己,即自己是否為堆中第一個堆塊,若為自己直接轉至第五步。
第四步:判斷上一堆塊是否free
判斷過程: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,否則轉第五步。
第五步:判斷下一相鄰堆塊
通過自己size找到下一堆塊位置,計算過程:自己的地址+size*8。
解密heap_entry,進行堆塊有效性校驗(ps:對於上一相鄰堆塊不進行解密和校驗,應該是防止堆溢位)。
雖然校驗過程與第一步程式碼不同,但根據單步除錯。校驗的實際效果是一樣的。
heap_entry偏移0,1,2相互異或操作,與偏移3進行對比。
第六步:判斷下一堆塊是否freeHeapAlloc
判斷過程同第四步一致,但是若可以進行unlink,還要再次對堆塊進行校驗(unlink前都會進行校驗,不論前後)。
unlink過程
safe unlink
safe unlink的程式碼同linux下的保護如出一轍。
要求x->fd->bk=x=x->bk->fd。
失敗原因
經過一段根據free堆塊大小的判斷及合併後大小的計算,來到失敗處。
其實攻擊失敗原因很簡單,unlink後會再次對相鄰堆塊進行完整性校驗。這裡我們的fd指標被修改成我們程式中記憶體一部分,並不能完成完成性校驗,會呼叫raiseexcition(校驗過程和上方一致故不貼出)。
此步正是unlink記憶體寫入最後一個校驗(如果堆塊未啟動加密的話,偽造heap_entry為0x0000000即可通過校驗):
總結
經過unlink後會進行flag更新,連結串列更新。將堆塊內容填充,最後加密heap_entry等步驟(這裡就不繼續跟蹤了,最後的流程不長)。
經過分析可以得知,只有在堆塊未加密情況下,且能偽造heap_entry為0x0000000才能進行利用(感嘆windows的嚴謹和glibc的粗糙)。
原文作者:sixty的夢想(看雪ID)
原文連結:https://bbs.pediy.com/thread-246570.htm
轉載請註明:轉自看雪論壇
看雪閱讀推薦:
4、[原創]黑蘋果Hackintosh 10.13.5 High Sierra i7 8700k z370
5、[原創] 第九題 PWN-羞恥Player WriteUp
相關文章
- 堆溢位的unlink利用方法2020-08-19
- ctf pwn中的unlink exploit(堆利用)2018-03-05
- Linux堆溢位漏洞利用之unlink2020-08-19Linux
- 堆結構2024-12-08
- 資料結構-堆2020-01-31資料結構
- 資料結構 - 堆2024-10-25資料結構
- [資料結構]堆2024-07-07資料結構
- 資料結構——堆2022-04-09資料結構
- 資料結構的概念、堆疊2024-12-07資料結構
- 資料結構 - 堆(Heap)2020-09-26資料結構
- 加強堆結構說明2022-11-29
- 資料結構之堆(Heap)2021-03-27資料結構
- JAVA常用資料結構及原理分析2020-11-09Java資料結構
- 資料結構之索引堆(IndexHeap)2021-05-11資料結構索引Index
- 資料結構之堆(c++)2021-03-27資料結構C++
- 資料結構 9 基礎資料結構 二叉堆 瞭解二叉堆的元素插入、刪除、構建二叉堆的程式碼方式2020-06-01資料結構
- Java堆疊的深度分析及記憶體管理技巧2024-08-08Java記憶體
- class檔案的基本結構及proxy原始碼分析二2020-07-10原始碼
- 資料結構與演算法-堆2019-01-05資料結構演算法
- 演算法(4)資料結構:堆2019-04-08演算法資料結構
- 資料結構和演算法-堆2020-06-15資料結構演算法
- 高階資料結構-可並堆2024-07-14資料結構
- 堆疊的應用——用JavaScript描述資料結構2018-08-10JavaScript資料結構
- libevent原始碼初識及目錄結構分析2021-09-09原始碼
- Java Class檔案結構例項分析(下)2018-12-18Java
- Linux系統下ifconfig命令使用及結果分析2019-01-08Linux
- 資料結構-二叉樹、堆、圖2024-07-28資料結構二叉樹
- 使用C#實現資料結構堆2021-02-03C#資料結構
- Redis 5種資料結構 及使用場景分析2020-06-01Redis資料結構
- CTF 中 glibc堆利用 及 IO_FILE 總結2022-03-30
- openGauss lo_unlink2024-05-16
- URL 結構分析2019-03-22
- 結構化分析2020-12-22
- 資料結構&堆&heap&priority_queue&實現2018-09-21資料結構
- 高階資料結構---堆樹和堆排序2020-05-02資料結構排序
- 使用加強堆結構解決topK問題2022-04-10TopK
- iOS探索 類的結構分析2020-01-21iOS
- 堆的基本操作及堆排序2022-01-23排序