微軟在2015年1月22日公布了windows10技術預覽版,Build號:9926。電腦管家反病毒實驗室第一時間對其引入的新安全特性進行了深入分析。
眾所周知,漏洞利用過程中攻擊者若要執行惡意代碼,需要破壞程序原有指令的的正常執行。執行流保護的作用就是在程序執行的過程中檢測指令流的正常性,當發生不符合預期的情況時,及時進行異常處理。業界針對執行流保護已經有一些相對成熟的技術方案,在微軟發布的windows10最新版本中,我們看到了這一防護思想的廣泛使用。
CFI即控制流完整性Control-Flow Integrity,主要是通過對二進制可執行文件的動態改寫,以此為其增加額外的安全性保障。
這是Mihai Budiu介紹CFI技術時使用的例子。這里通過對二進制可執行文件的改寫,對jmp的目的地址前插入一個在改寫時約定好的校驗ID,在jmp的時候看目的地址前的數據是不是我們約定好的校驗ID,如果不是則進入錯誤處理流程。
同理在call 和 ret的時候也可以進行改寫:
左半部分就是一個對call的改寫,右半部分是對ret的一個改寫,在call的目的地址和ret的返回地址之前插入校驗ID,然后改寫的call 和ret中增加了對校驗ID的檢查,如果不符合預期,進入錯誤處理流程,這個思路和上邊對jmp的處理是完全一樣的。
實現CFI需要在jmp、call 一個寄存器(或者使用寄存器間接尋址)的時候,目的地址有時必須通過動態獲得,且改寫的開銷又很大,這些都給CFI的實際應用造成了一定的困難。
微軟在最新的操作系統win10當中,對基于執行流防護的實際應用中采用了CFG技術。CFG是Control Flow Guard的縮寫,就是控制流保護,它是一種編譯器和操作系統相結合的防護手段,目的在于防止不可信的間接調用。
漏洞攻擊過程中,常見的利用手法是通過溢出覆蓋或者直接篡改某個寄存器的值,篡改間接調用的地址,進而控制了程序的執行流程。CFG通過在編譯和鏈接期間,記錄下所有的間接調用信息,并把他們記錄在最終的可執行文件中,并且在所有的間接調用之前插入額外的校驗,當間接調用的地址被篡改時,會觸發一個異常,操作系統介入處理。
以win10 preview 9926中IE11的Spartan html解析模塊為例,看一下CFG的具體情況:
這里就是被編譯器插入的CFG校驗函數
但是靜態情況下默認的檢測函數是一個直接return的空函數,是微軟在和我們開玩笑嗎?
通過動態調試看一下
從上圖我們可以看出,實際運行時的地址和我們通過IDA靜態看到的地址是不一樣的,這里就涉及到CFG和操作系統相關的那部分。支持CFG版本的操作系統加載器在加載支持CFG的模塊時,會把這個地址替換成ntdll中的一個函數地址。不支持CFG版本的操作系統不用理會這個檢測,程序執行時直接retn。
這是ntdll中的檢測函數
原理是在進入檢測函數之前,把即將call的寄存器值(或者是帶偏移的寄存器間接尋址)賦值給ecx,在檢測函數中通過編譯期間記錄的數據,來校驗這個值是否有效。
檢測過程如下:
首先從LdrSystemDllInitBlock+0x60處讀取一個位圖(bitmap),這個位圖表明了哪些函數地址是有效的,通過間接調用的函數地址的高3個字節作為一個索引,獲取該函數地址所在的位圖的一個DWORD值,一共32位,證明1位代表了8個字節,但一般來說間接調用的函數地址都是0x10對齊的,因此一般奇數位是不使用的。
通過函數地址的高3個字節作為索引拿到了一個所在的位圖的DWORD值,然后檢查低1字節的0-3位是否為0,如果為0,證明函數是0x10對齊的,則用3-7bit共5個bit就作為這個DWORD值的索引,這樣通過一個函數地址就能找到位圖中所對應的位了。如果置位了,表明函數地址有效,反之則會觸發異常。
這里有個有趣的東西,雖然使用test cl,0Fh檢測是否0x10對齊,如果對齊的話實際上用3-7位作為索引,也就是說第3位一定是0。但如果函數地址不是0x10對齊的話,則會對3-7位 or 1,然后再作為索引。這樣就有一個弊端,如果一個有效的間接調用的函數地址是8字節對齊的,那么其實是允許一個8字節的一個錯位調用的,這樣可能導致的結果就是可能造成雖然通過了校驗,但是實際調用的地址并不是原始記錄的函數地址。
還有一點,如果這時候漏洞觸發成功,間接調用的寄存器值已經被攻擊者修改了,這時候從bitmap中取值的時候可能造成內存訪問無效。請看LdrpValidateUserCallTargetBitMapCheck符
號處的這條指令:mov edx,dword ptr [edx+eax*4] edx是bitmap地址,eax是索引,但如果eax不可信了,這個很有可能,則會導致內存訪問異常,并且這個函數并沒有異常處理。這是因為微軟為了效率考慮(畢竟這個校驗函數的調用十分頻繁,一個開啟CFG的模塊可能會有上萬個調用處),微軟在ntdll! RtlDispatchException中對該地址發生的異常做了一個處理:
如果異常發生的地址命中LdrpValidateUserCallTargetBitMapCheck,則進行一個單獨處理,RtlpHandleInvalidUserCallTarget會校驗當前進程的DEP狀態和要間接調用的地址(ecx)的內存屬性,如果當前進程關閉了DEP并且要間接調用的地址有可執行屬性,則觸發CFG異常,否則通過修改pContext把EIP修正到ret返回處,并且表明異常已被處理。
最后再說下這個原始的bitmap,在系統初始化的時候,內存管理器初始化中會創建一個Section(MiCfgBitMapSection32),這個Section在Win8.1上的大小是通過MmSystemRangeStart(32位下是0x80000000)計算的,前面提到過bitmap里面1位代表8字節,計算完后正好是32MB
而在Win10上MiCfgBitMapSection32的大小有了變化,直接寫死成了0x3000000(48MB)
Section創建完成后在每個進程啟動的時候會映射進去
(NtCreateUserProcess-> PspAllocateProcess-> MmInitializeProcessAddressSpace-> MiMapProcessExecutable-> MiCfgInitializeProcess)
映射的時候作為shared view,除非某一個進程修改了這片內存。
在一個CFG模塊映射進來的時候,重定位過程中會重新解析PE文件LOADCONFIG中的Guard Function Table以重新計算該模塊對應的bitmap(MiParseImageCfgBits),最后更新到MiCfgBitMapSection32中去(MiUpdateCfgSystemWideBitmap)。
早些年的漏洞攻擊代碼可以直接在棧空間或堆空間執行指令,但近幾年,微軟操作系統在安全性方面逐漸加強,DEP、ASLR等防護手段的應用,使得攻擊者必須借助ROP等繞過手段來實現漏洞利用。在ROP利用中,棧交換指令Stack pivot必不可少。
針對ROP攻擊的防御長久以來是漏洞防御的一個難題,因為ROP指令在靜態層面分析與程序的正常指令流毫無差別,且運行時也是在合法模塊內執行,因此極難防御。
管家漏洞防御團隊針對ROP利用的特點,從整個程序的執行流層面進行分析,研究出在動態運行時區分是合法指令流還是異常指令流的方法,其思想與CFI不謀而合。
下邊就是一個由于錯位匯編形成的比較常用的棧交換指令
而實際正常的執行流程是這樣的
以上是沒有開啟XP防護的情況
開啟電腦管家XP防護之后:
此時如果攻擊者依靠靜態分析時得到棧交換指令位置來執行ROP攻擊的話,會被執行流保護邏輯發現異常,后續攻擊則無法實現。
CFG防護方法需要在編譯鏈接階段來完成準備工作,同時需要操作系統的支持。CFI無需編譯時的幫助,且不僅能夠防御call調用,能夠對全部執行流進行保護。但CFI需要插入大量的檢測點,并且在執行過程中檢測的頻率極高,難免對程序執行效率帶來影響。
電腦管家XP版的防御方法相比于前兩者,對性能的影響更小,但這種方法是針對舊版操作系統的緩解方案,通用性會打折扣。所以建議廣大windows用戶盡量升級到最新操作系統,享受全面的安全保護。而由于某些原因無法升級的用戶也不必擔心,管家XP版會繼續提供最高的安全防護能力。