針對進程行為的監控需求,以往很多安全軟件都是採用的Hook技術攔截關鍵的系統調用,來實現對惡意軟件進程創建的攔截。但在x64架構下,系統內核做了很多安全檢測措施,特別是類似於KDP這樣的技術,使得Hook方法不再有效。為此OS推出了基於回調實現的行為監控方案。本文藉助IDA逆向分析該技術的實現原理並給出了關鍵數據結構及調用鏈,通過雙機內核調試驗證了該數據結構以及調用鏈的正確性。
涉及到的內容如下:
1、內核對象及內核對象管理;2、進程回調;3、內核調試;4、Windbg雙擊調試;
0 引言近年來,各種惡意軟件新變種層出不窮,攻擊方法、手段多種多樣,造成了巨大的經濟損失。作為防守的第一個環節就是能夠識別出惡意進程創建的動作,而進程創建監控技術是為了能夠讓安全軟件有機會攔截到此動作的技術。安全軟件根據匹配算法判斷是否准許該進程創建,以此達到保護用户數據安全的目的。x86架構下的實現方案多為Hook技術,通過攔截內核中進程創建的關鍵API如nt!NtCreateProcess或nt!NtCreateProcessEx,通過堆棧來回溯到關鍵參數,如待創建進程的exe全路徑、父進程信息,然後根據獲取到的全路徑檢測exe磁盤文件,同時也可以分析進程鏈最終確定是否放行該動作。但這種技術方案存在一些缺陷,一方面其破壞了內核的完整性,導致系統的穩定性下降;另一方面,這些API很多都是未公開的,也就意味着需要通過逆向工程等技術手段來分析OS內核鏡像文件,定位到關鍵的API。但如果系統升級了,該API可能就不存在了,這也導致安全軟件的兼容性特別差;最重要的是各個安全廠家的實現方案不一樣,掛鈎的點也不同,很容易出現相互競爭的情況,極有可能會導致BSoD(Blue Screen of Death)。另一種傳統的基於特徵碼的攔截方式,也同樣存在類似的問題。需要為每個子版本的系統關鍵API做逆向分析,取出特徵碼,當系統更新或者打補丁,則需要再次逆向分析取出特徵碼。工作量巨大,效率低下,適配性很低,如果沒有及時更新特徵碼,很可能會使得監控失效,情況糟糕的時候會直接導致BSoD。為此,在x64架構下,內核一方面為了保護關鍵數據的完整性,另一方面也為了提高內核程序自身的穩定性,推出了諸如KDP(Kernel Data Protection)、PG等安全措施,使得傳統的 Hook技術失效;同時OS為了規範化安全相關信息的獲取,使得安全軟件能夠在內核可控的情況下提供安全服務,Windows系統層面提供了一種基於回調的方式來通知安全軟件註冊的內核回調例程。這種方式優點是方便高效,可移植性好,穩定性高,且各個安全廠商之間也不會出現競爭的關係。
本文基於逆向工程及內核調試技術,分析了該技術的具體實現及系統額外增加的數據檢測機制。藉助逆向工具IDA靜態逆向分析了系統關鍵API的內部動作及具體的實現,相關的數據結構,得到該技術實際觸發的調用源以及整個調用鏈。藉助VMWare搭建雙機調試環境,利用Windbg動態調試系統內核,查看系統中所涉及到的關鍵數據,並與PCHunter給出的數據做對比分析,驗證了分析結論的正確性。此外還通過對調用鏈中的關鍵函數下斷點,通過棧回溯技術,動態觀察了整個調用鏈及觸發時間。分析得到的關鍵數據結構和系統對數據做的檢測校驗算法可用於檢測病毒木馬等軟件惡意構造的表項,且還可以應用到安全廠商對抗惡意代碼時,自動構造表項來檢測系統行為,完全脱離系統提供的註冊卸載API。
1 進程回調原理分析1.1 安裝與卸載逆向分析根據微軟官方技術文檔MSDN上的説明,通過PsSetCreateProcessNotifyRoutine、PsSetCreateProcessNotifyRoutineEx和PsSetCreateProcessNotifyRoutineEx2這三API來安裝一個進程創建、退出通知回調例程,當有進程創建或者退出時,系統會回調參數中指定的函數。以PsSetCreateProcessNotifyRoutine為例子,基於IDA逆向分析該API的具體實現。如圖1所示,由圖可知,該API內部僅僅是簡單的調用另一個函數,其自身僅僅是一個stub,具體的實現在PspSetCreateProcessNotifyRoutine中,此函數的安裝回調例程的關鍵實現如圖所示。
調用ExAllocateCallBack,創建出了一個回調對象,並將pNotifyRoutine和bRemovel作為參數傳入,以初始化該回調對象,代碼如圖所示;其中pNotifyRoutine即是需要被回調的函數例程,此處的bRemovel為false,表示當前是安裝回調例程。
緊接着調用ExCompareExchangeCallBack將初始化好的CallBack對象添加到PspCreateProcessNotifyRoutine所維護的全局數組中。值得注意的是,ExCompareExchangeCallBack中在安裝回調例程時,對回調例程有一個特殊的操作如圖所示。
與0x0F做了或操作,等價於將低4位全部置1;若ExCompareExchangeCallBack執行失敗,則接着下一輪循環繼續執行。由圖2中第66行代碼可知,循環的最大次數是0x40次。如果一直失敗,可調用ExFreePoolWithTag釋放掉pCallBack所佔用的內存,且返回0xC000000D錯誤碼。
然後根據v3的值判斷是通過上述三個API中的哪個安裝的回調,來更新相應的全局變量。其中PspCreateProcessNotifyRoutineExCount和PspCreateProcessNotifyRoutineCount分別記錄當前通過PsSetCreateProcessNotifyRoutineEx和PsSetCreateProcessNotifyRoutine安裝回調例程的個數。PspNotifyEnableMask用以表徵當前數組中是否安裝了回調例程,該值在系統遍歷回調數組執行回調例程時,用以判斷數組是否為空,加快程序的執行效率。
除了能夠安裝回調例程,這三個API也能卸載指定的回調例程。以PsSetCreateProcessNotifyRoutine為例,分析其實現的關鍵部分,如圖所示。
通過一個while循環遍歷PspCreateProcessNotifyRoutine數組,調用ExReferenceCallBackBlock取出數組中的每一項,該API內部會做一些檢驗動作且對返回的數據也做了特殊處理,如圖所示。圖6中*pCallBackObj即是取出回調對象中的回調例程的函數地址,通過判斷其低4位是否為1來做一些數據的校驗,如17行所示。系統做這個處理也是起到保護作用,防止惡意構造數據填入表中,劫持正常的系統調用流程。此外,圖中第33行處的代碼,在將回調例程返回給父調用時,也將回調例程的低4位全部清零,否則返回的地址是錯誤的,調用立馬觸發CPU異常。
ExReferenceCallBackBlock成功返回後,調用ExGetCallBackBlockRoutine從返回的回調對象中取出回調例程,並判斷取出的是否為當前指定需要卸載的項,如果是則調用ExDereferenceCallBackBlock遞減引用計數,接着調用ExFreePoolWithTag釋放掉Callback所佔用的內存。期間也會更新PspCreateProcessNotifyRoutineExCount或PspCreateProcessNotifyRoutineCount的值。根據源碼還可以得知,該數組總計64項,也即只能安裝64個回調例程。如果遍歷完數組的64項依舊沒有找到,則返回0xC000007A錯誤碼。
1.2 OS執行回調例程分析回調例程安裝完之後,如果有新的進程創建或退出,內核則便會遍歷該數組來執行其中安裝的每一項回調例程。通過IDA的交叉引用功能,可分析出內核其他地方對PspCreateProcessNotifyRoutine的交叉引用,如圖所示,
共計5個地方涉及到此變量。其中PspCallProcessNotifyRoutines是直接調用回調例程的函數,該函數的關鍵部分如圖所示。
通過while循環,遍歷PspCreateProcessNotifyRoutine數組中安裝的所有回調例程,依次執行。PspNotifyEnableMask & 2的操作即為判斷當前數組中是否安裝有回調例程,加快程序的執行效率,這個變量的值在PsSetCreateProcessNotifyRoutine中安裝回調例程時設置。bRemove & 2這個if分支,是用來判斷當前的回調例程是通過PsSetCreateProcessNotifyRoutine還是PsSetCreateProcessNotifyRoutineEx安裝,因為這兩個API安裝的回調例程的原型不同,在實際調用時傳入的參數也不同。兩者的回調例程原分別為:void PcreateProcessNotifyRoutine(HANDLE ParentId,HANDLE ProcessId,BOOLEAN Create)和void PcreateProcessNotifyRoutineEx(PEPROCESS Process,HANDLE ProcessId,PPS_CREATE_NOTIFY_INFO CreateInfo)。此外,圖8中IDA給出的偽C代碼RoutineFun((unsigned __int64)RoutineFun)明顯不對,因為回調例程的參數個數是3個,而IDA分析出的參數只有1個,顯然有問題。直接看下反彙編代碼即可得知,如圖所示,
根據x64下的調用約定可知,函數的前4個參數是通過rcx、rdx、r8和r9這四個寄存器傳遞,圖給出的正是回調例程的前三個參數,_guard_dispatch_icall內部會直接取rax的值調用過去,而rax的值正是ExGetCallBackBlockRoutine調用返回的回調例程函數地址。
上圖中的第二個涉及到PspCreateProcessNotifyRoutine數組的是PspEnumerateCallback函數,該函數是系統內部函數,沒有導出,其具體實現如圖所示。
該函數根據dwEnumType來判斷想要枚舉的是哪個數組,由代碼分析可知,系統內核維護了三個回調相關的數組,分別為鏡像加載回調數組,進程創建退出回調數組,線程創建退出數組。類似之前的函數校驗,這裏也檢測了索引是否超過0x40,超過了則返回0,以示失敗。
1.3 觸發調用的調用鏈分析上節分析了回調例程的直接調用上級函數,本節分析整個調用鏈,主要分析調用源及調用過程中涉及到的關鍵函數。根據IDA給出的交叉引用圖如圖所示。
涉及到的函數調用非常多,很多不相關的也被包含進來,不便於分析。經手動分析整理後的調用鏈,其鏈路中的關鍵API如圖所示。
虛線以上部分為用户態程序,虛線以下為內核態程序,紅色標註的都是標準導出的API。根據圖12可知,當用户態進程調用RtlCreateUserProcess、RtlCreateUserProcesersEx或RtlExitUserProcess時,內核都會去遍歷PspCreateProcessNotifyRoutine數組,依次執行回調例程,通知給驅動程序做相應的處理。驅動接管之後,可以做安全校驗處理,分析進程的父進程或者進一步分析進程鏈,此外還可以對即將被拉起的子進程做特徵碼匹配,PE指紋識別,導入表檢測等防禦手段。這種方式不需要去Hook任何API,也無需做特徵碼定位等重複繁瑣的工作,完全基於系統提供的回調機制,且在Windows系統中都可以無縫銜接。且各個安全廠家之間也不存在相互競爭,大大縮小了系統藍屏的風險。圖12中NtCreateUserProcess調用PspInsertThread的原因是創建進程的API內部會創建該進程的主線程。將遍歷回調例程數組的工作統一到PspInsertThread中,由其去調用下層的PspCallProcessNotifyRoutines更為合理。
2 實驗2.1 觀察系統中已安裝的回調例程實驗環境如表1所示,藉助於VMWare進行雙機調試。
Guest OS Build 10.0.16299.125Host OS Build 10.0.17134.885Windbg版本 10.0.17134.1VMWare 14.1.1 build-7528167PCHunter V1.56
在Windbg中觀察PspCreateProcessNotifyRoutine數組,共計14項有效數據,如下所示;