分析了一下360安全衛士的HOOK(一)
Posted on 2009-10-18 18:02 S.l.e!ep.¢% 閱讀(3510) 評論(1) 編輯 收藏 引用 所屬分類: Windows WDM
分析了一下360安全衛士的HOOK(一)
分析了一下360的HOOK,通過直接hook KiFastCallEntry實現對所有系統調用的過濾。
我分析的版本如下:
主程序版本: 6.0.1.1003
HookPort.sys版本: 1, 0, 0, 1005
HookPort.sys的TimeStamp: 4A8D4AB8
簡單說明:360把所有被hook的系統服務的過濾函數放在了一個表里,索引即對應的系統服務在該過濾函數表中的索引。
所有列出來的函數都會被hook掉的,是否處理指某個系統服務有沒有相應的過濾函數進行處理,拒絕還是放行就是在過濾函數中完成判斷的。
不處理的系統服務,將會直接調用原始服務例程。
函數如下:
服務名稱? ? ? ? ? ? ? ? ? ? 索引? ? 是否處理? ? 備注
==============================================================================
NtCreateKey? ? ? ? ? ? ? ? ? ? 0x00? ? 否
NtQueryValueKey? ? ? ? ? ? ? ? 0x01? ? 是
NtDeleteKey? ? ? ? ? ? ? ? ? ? 0x02? ? 是
NtDeleteValueKey? ? ? ? ? ? 0x03? ? 是
NtRenameKey? ? ? ? ? ? ? ? ? ? 0x04? ? 是
NtReplaceKey? ? ? ? ? ? ? ? 0x05? ? 是
NtRestoreKey? ? ? ? ? ? ? ? 0x06? ? 是
NtSetValueKey? ? ? ? ? ? ? ? 0x07? ? 是
NtCreateFile? ? ? ? ? ? ? ? 0x08? ? 是
NtFsControl? ? ? ? ? ? ? ? ? ? 0x09? ? 是
NtSetInformationFile ? ? ? ? 0x0A? ? 是
NtWriteFile? ? ? ? ? ? ? ? ? ? 0x0B? ? 是
NtWriteFileGather? ? ? ? ? ? 0x0B? ? 是? ? ? ? //和NtWriteFile共用一個過濾函數
NtCreateProcess? ? ? ? ? ? ? ? 0x0D? ? 是
NtCreateProcessEx? ? ? ? ? ? 0x0E? ? 是
NtCreateUserProcess? ? ? ? ? ? 0x0F? ? 是? ? ? ? //Only on Vista or later
NtCreateThread? ? ? ? ? ? ? ? 0x10? ? 是
NtCreateThreadEx? ? ? ? ? ? 0x10? ? 是? ? ? ? //和NtCreateThread共用一個過濾函數,for vista or later
NtOpenThread? ? ? ? ? ? ? ? 0x11? ? 是
NtDeleteFile? ? ? ? ? ? ? ? 0x12? ? 是
NtOpenFile? ? ? ? ? ? ? ? ? ? 0x13? ? 是
NtReadVirtualMemory? ? ? ? ? ? 0x14? ? 否
NtTerminateProcess? ? ? ? ? ? 0x15? ? 是
NtQueueApcThread? ? ? ? ? ? 0x16? ? 是
NtSetContextThread? ? ? ? ? ? 0x17? ? 是
NtSetInformationThread? ? ? ? 0x18? ? 否
NtProtectVirtualMemory? ? ? ? 0x19? ? 否
NtWriteVirtualMemory? ? ? ? 0x1A? ? 是
NtAdjustGroupToken? ? ? ? ? ? 0x1B? ? 否
NtAdjustPrivilegesToken ? ? 0x1C? ? 否
NtRequestWaitReplyPort? ? ? ? 0x1D? ? 是
NtCreateSection? ? ? ? ? ? ? ? 0x1E? ? 是
NtOpenSecton? ? ? ? ? ? ? ? 0x1F? ? 是
NtCreateSymbolicLinkObject? ? 0x20? ? 是
NtOpenSymbolicLinkObject? ? 0x21? ? 否
NtLoadDriver? ? ? ? ? ? ? ? 0x22? ? 是
NtUnloadDriver? ? ? ? ? ? ? ? 0x22? ? 是? ? ? ? //和NtLoadDriver共用一個過濾函數
NtQuerySystemInformation? ? 0x23? ? 是
NtSetSystemTime? ? ? ? ? ? ? ? 0x25? ? 否
NtSystemDebugControl? ? ? ? 0x26? ? 是
NtUserBuildHwndList? ? ? ? ? ? 0x27? ? 是
NtUserQueryWindow? ? ? ? ? ? 0x28? ? 是
NtUserFindWindowEx? ? ? ? ? ? 0x29? ? 是
NtUserWindowFromPoint? ? ? ? 0x2A? ? 是
NtUserMessageCall? ? ? ? ? ? 0x2B? ? 是
NtUserPostMessage? ? ? ? ? ? 0x2C? ? 是
NtUserSetWindowsHookEx? ? ? ? 0x2D? ? 是
NtUserPostThreadMessage? ? ? ? 0x2E? ? 是
NtOpenProcess? ? ? ? ? ? ? ? 0x2F? ? 是
NtDeviceIoControlFile? ? ? ? 0x30? ? 是
NtUserSetParent? ? ? ? ? ? ? ? 0x31? ? 是
NtOpenKey? ? ? ? ? ? ? ? ? ? 0x32? ? 是
NtDuplicateObject? ? ? ? ? ? 0x33? ? 是
NtResumeThread? ? ? ? ? ? ? ? 0x34? ? 否
NtUserChildWindowFromPointEx 0x35? ? 是
NtUserDestroyWindow? ? ? ? ? ? 0x36? ? 是
NtUserInternalGetWindowText? ? 0x37? ? 否
NtUserMoveWindow? ? ? ? ? ? 0x38? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserRealChildWindowFromPoint 0x39 是? ? ? ? //和NtUserChildWindowFromPointEx共用一個過濾函數
NtUserSetInformationThread? ? 0x3A? ? 否
NtUserSetInternalWindowPos? ? 0x3B? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserSetWindowLong? ? ? ? ? ? 0x3C? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserSetWindowPlacement? ? 0x3D? ? 是? ? ? ? //和NtSetParent共用一個過濾函數? ? ? ?
NtUserSetWindowPos? ? ? ? ? ? 0x3E? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserSetWindowRgn? ? ? ? ? ? 0x3F? ? 是? ? ? ? //和NtSetParent共用一個過濾函數? ? ? ?
NtUserShowWindow? ? ? ? ? ? 0x40? ? 是
NtUserShowWindowAsync? ? ? ? 0x41? ? 是? ? ? ? //和NtUserShowWindow共用一個過濾函數
NtQueryAttributesFile? ? ? ? 0x42? ? 否
NtUserSendInput? ? ? ? ? ? ? ? 0x43? ? 否
NtAlpcSendWaitReceivePort? ? 0x44? ? 是? ? ? ? //for vista or later
NtUnmapViewOfSection? ? ? ? 0x46? ? 是
NtUserSetWinEventHook? ? ? ? 0x47? ? 否
NtSetSecurityObject? ? ? ? ? ? 0x48? ? 是
NtUserCallHwndParamLock? ? ? ? 0x49? ? 是
NtUserRegisterUserApiHok? ? 0x4A? ? 否
分析了一下360安全衛士的HOOK(二)
上一篇的分析中漏掉了三個函數,現補上:
NtSetSystemInformation? ? ? ? 0x24? ?
ProcessNotify? ? ? ? ? ? ? ? 0x45 //這個并非Hook,只是HookPort安裝的一個Notify
KeUserModeCallback? ? ? ? ? ? 0x4B
這樣一共是從0到0x4B,共0x4C個過濾函數,齊了~~
上次先列出了360所hook的系統服務,讓大家對它做了什么有了一些了解。這次分析的重點是360的KiFastCallEntry鉤子安裝全過程,更為重點的是360這樣一個安全軟件,是如何理好地處理好這眾全多的Hook,即我所謂的“架構”問題。
一、準備工作
(1)準備要Hook的系統服務的服務號ServiceIndex,對于導出的服務,采用獲取Zw*函數地址后再取服務號的方法,這個想必大家都很熟悉。未導出的,則根據不同系統版本,采用硬編碼的方法。
(2)準備緩沖區,存放原始服務例程地址、過濾開關、代理函數地址等,記為ServiceFilterInfoTable,這些都是HOOK中要使用到的數據。
HookPort.sys申請了一塊很大的內存用于存放這些數據,該內存大小為0x5DDC=6007*4=(6006+1)*4,為什么這么寫呢?因為它實際上是下面一個結構:
?
考慮到服務數最多的就是Win7的ShadowSSDT服務了,共827個,所以這里使用1001個項。顯然,每個表之間都有空隙,每個表內部也有大量空隙,覺得浪費內存了是吧?確實是這樣的,但是不要忘了有一條法則,叫做“以時間換空間,以空間換時間”,這里雖然浪費了內存,但是保證了效率最高,因為被Hook的地方是調用非常非常頻繁的KiFastCallEntry.
(3)準備緩沖區,存放過濾函數的地址和規則表,記為FilterFunRuleTable。這個部分的實現雖然是在HookPort.sys中,但是它對外留出了這個接口,實際上卻是由360SelfProtect.sys調用完成的。
這個緩沖區的結構如下:
?
這些只是一些準備工作,具體有些是什么時候完成的,在后面會講到。
?
二、KiFastCallEntry的Hook
(1)目標KiFastCallEntry,但具體Hook在哪里最好?
KiFastCallEntry是ring3經sysenter進入內核后的第一個必經之地(不考慮sysenter hook),但是KiFastCallEntry所作的工作也很多,比如設置ds,es,fs段寄存器的值,從ETHREAD中取ServiceTable,判斷ServiceIndex是否合法,若合法則判斷應使用哪張表(SSDT還是ShadowSSDT),然后從表中取出服務例程地址,從用戶棧復制參數到內核棧,然后調用服務例程,調用完之后再做一點準備工作然后就由KiServiceExit再飛回ring3.大致的流程就是這樣的.我貼一下關鍵的一段:
?
我們不能hook在開頭,這樣太多的準備工作需要自己來完成,而且涉及到的操作太多的話,兼容性和穩定性就很成問題。顯然我們又不能hook在call服務例程之后,這時該辦的事都辦完了,我們再接手就已經晚了,所以必須在call服務例程之前。KiFastCallEntry中的指令,真是有點寸士寸金的感覺,寄存器也不能隨意改變,挑哪兒下手呢?來看看360是怎么做的:
Hook之后:
?
這個Hook的位置確實選得非常好啊,自己省去了很多準備工作,都由系統準備好了,此時edi指向服務表的基址(KiServiceTable或W32pServiceTable),ebx是剛剛取出的原始服務例程的地址,eax是服務號,這時再做處理不是就很容易了嘛,重要的幾個信息都有了。
來看看跳轉的地址,又是個跳轉,看來這里只是個中間跳
?
f867b9a2這里才是真正的目的地,在HookPort.sys中.
?
(2)360是如何具體地去安裝這個Hook的?
360首先獲取ZwSetEvent的服務號,然后安裝了一個SSDT Hook,目標就是NtSetEvent.
然后,自己調用了一下ZwSetEvent,觸發自己安裝的Hook,代碼如下:
?
?
0x288C58F1,好奇怪,你看過誰家的句柄長成這個樣子嘛,顯然這是個假句柄了,它的作用就是在hook函數Fake_NtSetEvent中辨別一下是不是自己人,充當了暗號。
Fake_NtSetEvent中確實也是這樣寫的:
?
?
不是自己人就調用原函數去了,等對上暗號,是自己人了,才真正開始干事.
先申請一個5字節的buffer,寫入一個跳轉指令,這個就是上面看到的中間跳,這里還有一個細節,判斷了一下系統版本,Vista前后處理上有稍許不同,根據當前系統版本選擇合適的JmpStub.
再準備另外一個跳轉指令,跳轉目標就是剛才準備的中間跳的地址,這個跳轉指令將要被寫入KiFastCallEntry中。那么360是如何找到要寫入的位置呢?繼續看.
跳轉指令準備好之后,先還原剛才SSDT中安裝的NtSetEvent鉤子,然后從棧中回溯返回地址,也就是取[ebp+4]的值,并保存起來.上面對KiFastCallEntry的分析知道,SSDT中的函數都是從KiFastCallEntry中call過來的,那么返回地址肯定也在KiFastCallEntry中,具體地講,就是8053d806處call ebx的下一條指令處。取得這個地址后,往上匹配尋找
?
8053d7f3 c1e902????????shr? ? ecx,2
8053d7f6 8bfc? ? ? ? ? ? mov? ? edi,esp
找到之后,就確定了要Hook的位置。接下來怎么做寫過hook的都知道,就是關寫保護然后把剛才準備好的跳轉指令寫入再打開保護而已,不多說。
(3)KiFastCallEntry被hook后,360是如何處理的?
KiFastCallEntry被hook后,經二級跳跳到了JmpStub里。來看看:
?
?
可以看到,edi,ebx,eax三個重要數據入棧后,調用了另一個判斷函數。該函數逆向如下:
?
?
結合上面逆出來的代碼和數據結構,相信不難看懂。對于一個調用,通過判斷ServiceTable確定是SSDT調用還是Shadow調用,兩者過程基本一樣,以SSDT為例:
首先根據ServiceIndex判斷SwitchTable中的Hook開關是否打開,是則調用HookOrNot函數根據FilterFunRuleTable表中的Rule((根據PreviousMode有進一步判斷))來判斷是否需要Hook。經過這兩重檢查和判斷,最終若需要Hook,就保存原始服務例程地址并返回我們的代理函數,若不需要hook,就直接返回原始例程。由于使用了良好的數據結構,這里的效率是非常高的。
KiFastCallEntryFilterFunc的返回結果,要么是原始例程,要么是代理函數,返回至Jmpstub后,這個結果被保存在了[esp+0x10]處。不要忘了剛才有個pushad,所以[esp+0x10]處實際保存的是ebx的值,這樣修改了棧中的ebx的值,再popad時,ebx的值就被修改為了KiFastCallEntryFilterFunc的返回值,再一個popfd恢復剛才保存的標志寄存器,然后執行被jmp指令覆蓋掉的那兩句指令,最后將剛才回溯到的返回地址壓棧,一個ret就又飛回到了KiFastCallEntry中。若已Hook,此時ebx的值就已經被修改了,再下來call ebx時調用的就是剛才返回的代理函數了。很巧妙的處理啊。
?
三、代理函數如何處理?
Hook的細節搞清楚了,本來就已經差不多了,但是看了360代理函數中的處理,又發現一些有趣的東西。
每一個代理函數,都會首先調用一個CallFilterFunByIndex,調用時傳入的第一個參數就是過濾函數在過濾函數表中的索引,也就是我上一篇文章中所列出來的那些函數名稱后面的索引。第二個參數則是棧中的參數數組,相當于一下把所有參數都傳過去了。
?
CallFilterFunByIndex會先根據FilterFunRuleTable->IsFilterFunFilledReady判斷該表是否已經準備好(這個過濾函數表實際上是由360SelfProtect.sys調用HookPort.sys提供的接口填充的,所有過濾函數的實現也都在360SelfProtect.sys中),根據傳入的過濾函數的索引在FilterFunRuleTable表(也就是我前面提到的第二張表)中查找對應的過濾函數,若過濾函數存在,就調用過濾函數,傳入的參數同樣有過濾函數索引和參數數組,在這個過濾函數中才真正實現了對參數的判斷。
判斷完畢之后,若檢查通過,就予以放行,此時再調用JmpStub中保存的原始服務例程的地址,調用完原始例程之后,若調用成功,還會有一個循環的檢查,檢查目標是調用原始服務例程后返回的結果。這些循環檢查的函數哪兒來的呢?就是在CallFilterFunByIndex調用過濾函數時返回的,由于返回的這些函數是在調用原始服務例程之后對結果進行檢查,所以稱之為CheckResult系列函數,而過濾函數表中的那些函數則稱為CheckArguments函數。
為什么要有CheckArguments函數,又要有CheckResult函數?
這里來個小小的科普,hook某函數后,檢查參數的時機是怎么樣的呢?參數是分IN、OUT的,一般來說,傳入的參數要在調用原始函數前檢查,傳出的參數要在調用原始函數后檢查,而有些則在調用前后檢查都可以,但效果不同。大致可以分為四種情況:
第一種以(Nt)TerminateProcess為例,它的原型是這樣的:
NTSTATUS
NtTerminateProcess(
? ? __in_opt HANDLE ProcessHandle,
? ? __in NTSTATUS ExitStatus
? ? );
除了狀態碼,它沒有有效的返回值,它的作用更多的在于結束進程這個“過程”,等該函數返回的時候,進程已經被結束掉了,再來檢查有個P用。所以這種強調過程而且重點不在返回值(即使它有)的函數,必須要在調用原函數前檢查。
第二種,一個例子是recv,原型如下:
int recv (
? SOCKET s,? ? ?
? char FAR* buf,?
? int len,? ? ? ?
? int flags? ? ?
);
顯然該函數的重點在于第二個參數中返回的數據,但是在調用原始函數之前,緩沖區里什么都沒有,檢查,怎么檢查?像這種函數也是過程性的,但是它的第二個參數是OUT型的,就必須在調用原函數之后檢查。
第三類,比如CreateFile,OpenProcess,OpenThread,OpenEvent等函數,它們有一個共同的特征,就是傳入特性相關的數據(文件名,pid等),返回一個句柄,重點不在過程而在于返回值。這類函數即可以在調用原函數前檢查傳入的參數,也可以在調用原函數后檢查返回的句柄。但是一個是對象的名稱等外在信息,一個直接指向對象,顯然檢查后者更為可靠一些,因為它更貼近對象本身。
對于那些異步方式調用的函數更要注意了,比如異步方式調用的ReadFile或ReadFileEx,調用原函數之前顯然緩沖區沒有數據,調用了原函數緩沖區也不見得有數據,但是異步有個特征就是它的通知機制,可能是Apc,也可能是Event,這時就必須替換或其它方式處理它的通知機制才能在合適的時候拿到數據。
科普就先到這兒。現在來解釋為什么要有CheckResult系列函數的存在,相信大家就該明白了。以NtOpenSection這個服務的hook為例,我們知道有一個重點照顧對象叫做\Device\PhysicalMemory,經常被大家用于各種XX中,一段典型的打開該對象的代碼是:
?
?
怎么檢查?檢查第三個參數attributes.ObjectName是否是\\Device\\PhysicalMemory??顯然是不行的,看看MJ的《續PhysicalMemory攻擊》你就知道檢查這個參數有多困難。顯然NtOpenSection屬于我上面提到的第三類函數,看看360是怎么做的:
代理函數Proxy_NtOpenSection首先調用CallFilterFunByIndex,CallFilterFunByIndex根據所傳入的過濾函數索引在FilterFunRuleTable中找到Fake_NtOpenSection并調用之,然而Fake_NtOpenSection除了簡單判斷下調用者之外不做任何檢查,只是返回了一個函數的地址給CallFilterFunIndex,記為CheckResult_After_NtOpenSection,CallfilterFunIndex再把這個地址返回給代理函數Proxy_NtOpenSection,此時Proxy_NtOpenSection調用原函數NtOpenSection,若返回不成功,就直接返回這個狀態值,不成功當然就不管啦。若成功,就會調用剛才返回的CheckResult_After_NtOpenSection來檢查返回的句柄所指向的對象是不是\Device\PhysicalMemory,若是,就關掉該句柄,并返回一個禁止的狀態碼(STATUS_ACCESS_DENIED),若不是,綠燈大開,放行之.有時,FakeXXX函數返回的CheckResultXXX函數可能不止一個,此時ProxyXXX會根據返回的函數個數循環調用這些CheckResultXXX,有一個不通過即為不通過,只有所有CheckResultXXX都檢查通過了,ProxyXXX函數才會返回原始結果給調用者,真是把關極嚴啊。
值得一提的是,所有的過濾函數對于來自KernelMode的調用都不做處理,所以使用驅動來破壞360是輕而易舉的,但是完全沒有意義。按照MJ一貫的理念,如果你都能加載驅動了,那么所有ring0的保護也就失去了意義,再來保護完全是無用功,事實上也根本起不到保護效果了。
?
四、總結回顧
現在讓我們站得高一點,略去一些細節,來總結一下360整個hook架構:
1.HookPort.sys準備了ServiceFilterInfoTable(上面的表一),里面保存了SSDT函數的代理函數地址、原始例程地址、Hook開關,ShadowSSDT也是。
2.HookPort.sys對外留出了三個接口(保存在了DEVICE_EXTENSION中),第一個用于準備FilterFunRuleTable(上面的表二),第二個用于向FilterFunRuleTable中注冊過濾函數,第三個用于設置FilterFunRuleTable中的過濾規則。
3.HookPort.sys安裝KiFastCallEntry hook.
4.HookPort.sys!KiFastCallEntryFilterFunc根據Hook開關和RuleTable中的過濾規則來決定某個系統服務是否會被Hook,被hook后將會調用Proxy函數。
5.360SelfProtect.sys使用HookPort.sys提供的接口初始化RuleTable,并向RuleTable中注冊過濾函數、設置過濾規則。
6.每個Proxy函數使用CallFilterFunByIndex調用RuleTable中相應的FilterFun進行參數檢查,通過后調用原函數,若調用成功再調用FilterFun函數提供的CheckResult函數檢查結果(非SSDT、ShadowSSDT函數的Hook也采用同樣結構)。
很顯然地:
如果要增加一個系統服務的Hook,只需要打開ServiceFilterInfoTable中該服務Index對應的開關,并提供一個過濾函數就可以了。
如果要去掉一個系統服務的hook,只需要關閉ServiceFilterInfoTable中該服務Index對應的開關,立刻生效,而無須其它改變。
如果要修改一個系統服務的過濾函數,只需要使用HookPort的接口設置新的過濾函數就可以了,無須其它改變。
所以說,這個架構設計非常好,易于修改,易于擴充,易于分工,無愧于優秀二字。
結束語:單獨寫一個或幾個函數的Hook,很多人都會,但是要實現這樣一個產品級的優秀架構,就不是誰都能完成的了。分析了360這樣一個安全產品所使用的Hook架構,讓我收獲頗多,也讓大家領略了360安全衛士的技術魅力。文中如有錯誤,還請MJ和大家指出。
分析了一下360安全衛士的HOOK(三)
分析了一下360的HOOK,通過直接hook KiFastCallEntry實現對所有系統調用的過濾。
我分析的版本如下:
主程序版本: 6.0.1.1003
HookPort.sys版本: 1, 0, 0, 1005
HookPort.sys的TimeStamp: 4A8D4AB8
簡單說明:360把所有被hook的系統服務的過濾函數放在了一個表里,索引即對應的系統服務在該過濾函數表中的索引。
所有列出來的函數都會被hook掉的,是否處理指某個系統服務有沒有相應的過濾函數進行處理,拒絕還是放行就是在過濾函數中完成判斷的。
不處理的系統服務,將會直接調用原始服務例程。
函數如下:
服務名稱? ? ? ? ? ? ? ? ? ? 索引? ? 是否處理? ? 備注
==============================================================================
NtCreateKey? ? ? ? ? ? ? ? ? ? 0x00? ? 否
NtQueryValueKey? ? ? ? ? ? ? ? 0x01? ? 是
NtDeleteKey? ? ? ? ? ? ? ? ? ? 0x02? ? 是
NtDeleteValueKey? ? ? ? ? ? 0x03? ? 是
NtRenameKey? ? ? ? ? ? ? ? ? ? 0x04? ? 是
NtReplaceKey? ? ? ? ? ? ? ? 0x05? ? 是
NtRestoreKey? ? ? ? ? ? ? ? 0x06? ? 是
NtSetValueKey? ? ? ? ? ? ? ? 0x07? ? 是
NtCreateFile? ? ? ? ? ? ? ? 0x08? ? 是
NtFsControl? ? ? ? ? ? ? ? ? ? 0x09? ? 是
NtSetInformationFile ? ? ? ? 0x0A? ? 是
NtWriteFile? ? ? ? ? ? ? ? ? ? 0x0B? ? 是
NtWriteFileGather? ? ? ? ? ? 0x0B? ? 是? ? ? ? //和NtWriteFile共用一個過濾函數
NtCreateProcess? ? ? ? ? ? ? ? 0x0D? ? 是
NtCreateProcessEx? ? ? ? ? ? 0x0E? ? 是
NtCreateUserProcess? ? ? ? ? ? 0x0F? ? 是? ? ? ? //Only on Vista or later
NtCreateThread? ? ? ? ? ? ? ? 0x10? ? 是
NtCreateThreadEx? ? ? ? ? ? 0x10? ? 是? ? ? ? //和NtCreateThread共用一個過濾函數,for vista or later
NtOpenThread? ? ? ? ? ? ? ? 0x11? ? 是
NtDeleteFile? ? ? ? ? ? ? ? 0x12? ? 是
NtOpenFile? ? ? ? ? ? ? ? ? ? 0x13? ? 是
NtReadVirtualMemory? ? ? ? ? ? 0x14? ? 否
NtTerminateProcess? ? ? ? ? ? 0x15? ? 是
NtQueueApcThread? ? ? ? ? ? 0x16? ? 是
NtSetContextThread? ? ? ? ? ? 0x17? ? 是
NtSetInformationThread? ? ? ? 0x18? ? 否
NtProtectVirtualMemory? ? ? ? 0x19? ? 否
NtWriteVirtualMemory? ? ? ? 0x1A? ? 是
NtAdjustGroupToken? ? ? ? ? ? 0x1B? ? 否
NtAdjustPrivilegesToken ? ? 0x1C? ? 否
NtRequestWaitReplyPort? ? ? ? 0x1D? ? 是
NtCreateSection? ? ? ? ? ? ? ? 0x1E? ? 是
NtOpenSecton? ? ? ? ? ? ? ? 0x1F? ? 是
NtCreateSymbolicLinkObject? ? 0x20? ? 是
NtOpenSymbolicLinkObject? ? 0x21? ? 否
NtLoadDriver? ? ? ? ? ? ? ? 0x22? ? 是
NtUnloadDriver? ? ? ? ? ? ? ? 0x22? ? 是? ? ? ? //和NtLoadDriver共用一個過濾函數
NtQuerySystemInformation? ? 0x23? ? 是
NtSetSystemTime? ? ? ? ? ? ? ? 0x25? ? 否
NtSystemDebugControl? ? ? ? 0x26? ? 是
NtUserBuildHwndList? ? ? ? ? ? 0x27? ? 是
NtUserQueryWindow? ? ? ? ? ? 0x28? ? 是
NtUserFindWindowEx? ? ? ? ? ? 0x29? ? 是
NtUserWindowFromPoint? ? ? ? 0x2A? ? 是
NtUserMessageCall? ? ? ? ? ? 0x2B? ? 是
NtUserPostMessage? ? ? ? ? ? 0x2C? ? 是
NtUserSetWindowsHookEx? ? ? ? 0x2D? ? 是
NtUserPostThreadMessage? ? ? ? 0x2E? ? 是
NtOpenProcess? ? ? ? ? ? ? ? 0x2F? ? 是
NtDeviceIoControlFile? ? ? ? 0x30? ? 是
NtUserSetParent? ? ? ? ? ? ? ? 0x31? ? 是
NtOpenKey? ? ? ? ? ? ? ? ? ? 0x32? ? 是
NtDuplicateObject? ? ? ? ? ? 0x33? ? 是
NtResumeThread? ? ? ? ? ? ? ? 0x34? ? 否
NtUserChildWindowFromPointEx 0x35? ? 是
NtUserDestroyWindow? ? ? ? ? ? 0x36? ? 是
NtUserInternalGetWindowText? ? 0x37? ? 否
NtUserMoveWindow? ? ? ? ? ? 0x38? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserRealChildWindowFromPoint 0x39 是? ? ? ? //和NtUserChildWindowFromPointEx共用一個過濾函數
NtUserSetInformationThread? ? 0x3A? ? 否
NtUserSetInternalWindowPos? ? 0x3B? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserSetWindowLong? ? ? ? ? ? 0x3C? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserSetWindowPlacement? ? 0x3D? ? 是? ? ? ? //和NtSetParent共用一個過濾函數? ? ? ?
NtUserSetWindowPos? ? ? ? ? ? 0x3E? ? 是? ? ? ? //和NtSetParent共用一個過濾函數
NtUserSetWindowRgn? ? ? ? ? ? 0x3F? ? 是? ? ? ? //和NtSetParent共用一個過濾函數? ? ? ?
NtUserShowWindow? ? ? ? ? ? 0x40? ? 是
NtUserShowWindowAsync? ? ? ? 0x41? ? 是? ? ? ? //和NtUserShowWindow共用一個過濾函數
NtQueryAttributesFile? ? ? ? 0x42? ? 否
NtUserSendInput? ? ? ? ? ? ? ? 0x43? ? 否
NtAlpcSendWaitReceivePort? ? 0x44? ? 是? ? ? ? //for vista or later
NtUnmapViewOfSection? ? ? ? 0x46? ? 是
NtUserSetWinEventHook? ? ? ? 0x47? ? 否
NtSetSecurityObject? ? ? ? ? ? 0x48? ? 是
NtUserCallHwndParamLock? ? ? ? 0x49? ? 是
NtUserRegisterUserApiHok? ? 0x4A? ? 否
分析了一下360安全衛士的HOOK(二)
上一篇的分析中漏掉了三個函數,現補上:
NtSetSystemInformation? ? ? ? 0x24? ?
ProcessNotify? ? ? ? ? ? ? ? 0x45 //這個并非Hook,只是HookPort安裝的一個Notify
KeUserModeCallback? ? ? ? ? ? 0x4B
這樣一共是從0到0x4B,共0x4C個過濾函數,齊了~~
上次先列出了360所hook的系統服務,讓大家對它做了什么有了一些了解。這次分析的重點是360的KiFastCallEntry鉤子安裝全過程,更為重點的是360這樣一個安全軟件,是如何理好地處理好這眾全多的Hook,即我所謂的“架構”問題。
一、準備工作
(1)準備要Hook的系統服務的服務號ServiceIndex,對于導出的服務,采用獲取Zw*函數地址后再取服務號的方法,這個想必大家都很熟悉。未導出的,則根據不同系統版本,采用硬編碼的方法。
(2)準備緩沖區,存放原始服務例程地址、過濾開關、代理函數地址等,記為ServiceFilterInfoTable,這些都是HOOK中要使用到的數據。
HookPort.sys申請了一塊很大的內存用于存放這些數據,該內存大小為0x5DDC=6007*4=(6006+1)*4,為什么這么寫呢?因為它實際上是下面一個結構:
|
?
考慮到服務數最多的就是Win7的ShadowSSDT服務了,共827個,所以這里使用1001個項。顯然,每個表之間都有空隙,每個表內部也有大量空隙,覺得浪費內存了是吧?確實是這樣的,但是不要忘了有一條法則,叫做“以時間換空間,以空間換時間”,這里雖然浪費了內存,但是保證了效率最高,因為被Hook的地方是調用非常非常頻繁的KiFastCallEntry.
(3)準備緩沖區,存放過濾函數的地址和規則表,記為FilterFunRuleTable。這個部分的實現雖然是在HookPort.sys中,但是它對外留出了這個接口,實際上卻是由360SelfProtect.sys調用完成的。
這個緩沖區的結構如下:
|
?
這些只是一些準備工作,具體有些是什么時候完成的,在后面會講到。
?
二、KiFastCallEntry的Hook
(1)目標KiFastCallEntry,但具體Hook在哪里最好?
KiFastCallEntry是ring3經sysenter進入內核后的第一個必經之地(不考慮sysenter hook),但是KiFastCallEntry所作的工作也很多,比如設置ds,es,fs段寄存器的值,從ETHREAD中取ServiceTable,判斷ServiceIndex是否合法,若合法則判斷應使用哪張表(SSDT還是ShadowSSDT),然后從表中取出服務例程地址,從用戶棧復制參數到內核棧,然后調用服務例程,調用完之后再做一點準備工作然后就由KiServiceExit再飛回ring3.大致的流程就是這樣的.我貼一下關鍵的一段:
|
?
我們不能hook在開頭,這樣太多的準備工作需要自己來完成,而且涉及到的操作太多的話,兼容性和穩定性就很成問題。顯然我們又不能hook在call服務例程之后,這時該辦的事都辦完了,我們再接手就已經晚了,所以必須在call服務例程之前。KiFastCallEntry中的指令,真是有點寸士寸金的感覺,寄存器也不能隨意改變,挑哪兒下手呢?來看看360是怎么做的:
Hook之后:
|
?
這個Hook的位置確實選得非常好啊,自己省去了很多準備工作,都由系統準備好了,此時edi指向服務表的基址(KiServiceTable或W32pServiceTable),ebx是剛剛取出的原始服務例程的地址,eax是服務號,這時再做處理不是就很容易了嘛,重要的幾個信息都有了。
來看看跳轉的地址,又是個跳轉,看來這里只是個中間跳
|
?
f867b9a2這里才是真正的目的地,在HookPort.sys中.
|
?
(2)360是如何具體地去安裝這個Hook的?
360首先獲取ZwSetEvent的服務號,然后安裝了一個SSDT Hook,目標就是NtSetEvent.
然后,自己調用了一下ZwSetEvent,觸發自己安裝的Hook,代碼如下:
?
|
?
0x288C58F1,好奇怪,你看過誰家的句柄長成這個樣子嘛,顯然這是個假句柄了,它的作用就是在hook函數Fake_NtSetEvent中辨別一下是不是自己人,充當了暗號。
Fake_NtSetEvent中確實也是這樣寫的:
?
|
?
不是自己人就調用原函數去了,等對上暗號,是自己人了,才真正開始干事.
先申請一個5字節的buffer,寫入一個跳轉指令,這個就是上面看到的中間跳,這里還有一個細節,判斷了一下系統版本,Vista前后處理上有稍許不同,根據當前系統版本選擇合適的JmpStub.
再準備另外一個跳轉指令,跳轉目標就是剛才準備的中間跳的地址,這個跳轉指令將要被寫入KiFastCallEntry中。那么360是如何找到要寫入的位置呢?繼續看.
跳轉指令準備好之后,先還原剛才SSDT中安裝的NtSetEvent鉤子,然后從棧中回溯返回地址,也就是取[ebp+4]的值,并保存起來.上面對KiFastCallEntry的分析知道,SSDT中的函數都是從KiFastCallEntry中call過來的,那么返回地址肯定也在KiFastCallEntry中,具體地講,就是8053d806處call ebx的下一條指令處。取得這個地址后,往上匹配尋找
?
8053d7f3 c1e902????????shr? ? ecx,2
8053d7f6 8bfc? ? ? ? ? ? mov? ? edi,esp
找到之后,就確定了要Hook的位置。接下來怎么做寫過hook的都知道,就是關寫保護然后把剛才準備好的跳轉指令寫入再打開保護而已,不多說。
(3)KiFastCallEntry被hook后,360是如何處理的?
KiFastCallEntry被hook后,經二級跳跳到了JmpStub里。來看看:
?
|
?
可以看到,edi,ebx,eax三個重要數據入棧后,調用了另一個判斷函數。該函數逆向如下:
?
|
?
結合上面逆出來的代碼和數據結構,相信不難看懂。對于一個調用,通過判斷ServiceTable確定是SSDT調用還是Shadow調用,兩者過程基本一樣,以SSDT為例:
首先根據ServiceIndex判斷SwitchTable中的Hook開關是否打開,是則調用HookOrNot函數根據FilterFunRuleTable表中的Rule((根據PreviousMode有進一步判斷))來判斷是否需要Hook。經過這兩重檢查和判斷,最終若需要Hook,就保存原始服務例程地址并返回我們的代理函數,若不需要hook,就直接返回原始例程。由于使用了良好的數據結構,這里的效率是非常高的。
KiFastCallEntryFilterFunc的返回結果,要么是原始例程,要么是代理函數,返回至Jmpstub后,這個結果被保存在了[esp+0x10]處。不要忘了剛才有個pushad,所以[esp+0x10]處實際保存的是ebx的值,這樣修改了棧中的ebx的值,再popad時,ebx的值就被修改為了KiFastCallEntryFilterFunc的返回值,再一個popfd恢復剛才保存的標志寄存器,然后執行被jmp指令覆蓋掉的那兩句指令,最后將剛才回溯到的返回地址壓棧,一個ret就又飛回到了KiFastCallEntry中。若已Hook,此時ebx的值就已經被修改了,再下來call ebx時調用的就是剛才返回的代理函數了。很巧妙的處理啊。
?
三、代理函數如何處理?
Hook的細節搞清楚了,本來就已經差不多了,但是看了360代理函數中的處理,又發現一些有趣的東西。
每一個代理函數,都會首先調用一個CallFilterFunByIndex,調用時傳入的第一個參數就是過濾函數在過濾函數表中的索引,也就是我上一篇文章中所列出來的那些函數名稱后面的索引。第二個參數則是棧中的參數數組,相當于一下把所有參數都傳過去了。
?
CallFilterFunByIndex會先根據FilterFunRuleTable->IsFilterFunFilledReady判斷該表是否已經準備好(這個過濾函數表實際上是由360SelfProtect.sys調用HookPort.sys提供的接口填充的,所有過濾函數的實現也都在360SelfProtect.sys中),根據傳入的過濾函數的索引在FilterFunRuleTable表(也就是我前面提到的第二張表)中查找對應的過濾函數,若過濾函數存在,就調用過濾函數,傳入的參數同樣有過濾函數索引和參數數組,在這個過濾函數中才真正實現了對參數的判斷。
判斷完畢之后,若檢查通過,就予以放行,此時再調用JmpStub中保存的原始服務例程的地址,調用完原始例程之后,若調用成功,還會有一個循環的檢查,檢查目標是調用原始服務例程后返回的結果。這些循環檢查的函數哪兒來的呢?就是在CallFilterFunByIndex調用過濾函數時返回的,由于返回的這些函數是在調用原始服務例程之后對結果進行檢查,所以稱之為CheckResult系列函數,而過濾函數表中的那些函數則稱為CheckArguments函數。
為什么要有CheckArguments函數,又要有CheckResult函數?
這里來個小小的科普,hook某函數后,檢查參數的時機是怎么樣的呢?參數是分IN、OUT的,一般來說,傳入的參數要在調用原始函數前檢查,傳出的參數要在調用原始函數后檢查,而有些則在調用前后檢查都可以,但效果不同。大致可以分為四種情況:
第一種以(Nt)TerminateProcess為例,它的原型是這樣的:
NTSTATUS
NtTerminateProcess(
? ? __in_opt HANDLE ProcessHandle,
? ? __in NTSTATUS ExitStatus
? ? );
除了狀態碼,它沒有有效的返回值,它的作用更多的在于結束進程這個“過程”,等該函數返回的時候,進程已經被結束掉了,再來檢查有個P用。所以這種強調過程而且重點不在返回值(即使它有)的函數,必須要在調用原函數前檢查。
第二種,一個例子是recv,原型如下:
int recv (
? SOCKET s,? ? ?
? char FAR* buf,?
? int len,? ? ? ?
? int flags? ? ?
);
顯然該函數的重點在于第二個參數中返回的數據,但是在調用原始函數之前,緩沖區里什么都沒有,檢查,怎么檢查?像這種函數也是過程性的,但是它的第二個參數是OUT型的,就必須在調用原函數之后檢查。
第三類,比如CreateFile,OpenProcess,OpenThread,OpenEvent等函數,它們有一個共同的特征,就是傳入特性相關的數據(文件名,pid等),返回一個句柄,重點不在過程而在于返回值。這類函數即可以在調用原函數前檢查傳入的參數,也可以在調用原函數后檢查返回的句柄。但是一個是對象的名稱等外在信息,一個直接指向對象,顯然檢查后者更為可靠一些,因為它更貼近對象本身。
對于那些異步方式調用的函數更要注意了,比如異步方式調用的ReadFile或ReadFileEx,調用原函數之前顯然緩沖區沒有數據,調用了原函數緩沖區也不見得有數據,但是異步有個特征就是它的通知機制,可能是Apc,也可能是Event,這時就必須替換或其它方式處理它的通知機制才能在合適的時候拿到數據。
科普就先到這兒。現在來解釋為什么要有CheckResult系列函數的存在,相信大家就該明白了。以NtOpenSection這個服務的hook為例,我們知道有一個重點照顧對象叫做\Device\PhysicalMemory,經常被大家用于各種XX中,一段典型的打開該對象的代碼是:
?
|
?
怎么檢查?檢查第三個參數attributes.ObjectName是否是\\Device\\PhysicalMemory??顯然是不行的,看看MJ的《續PhysicalMemory攻擊》你就知道檢查這個參數有多困難。顯然NtOpenSection屬于我上面提到的第三類函數,看看360是怎么做的:
代理函數Proxy_NtOpenSection首先調用CallFilterFunByIndex,CallFilterFunByIndex根據所傳入的過濾函數索引在FilterFunRuleTable中找到Fake_NtOpenSection并調用之,然而Fake_NtOpenSection除了簡單判斷下調用者之外不做任何檢查,只是返回了一個函數的地址給CallFilterFunIndex,記為CheckResult_After_NtOpenSection,CallfilterFunIndex再把這個地址返回給代理函數Proxy_NtOpenSection,此時Proxy_NtOpenSection調用原函數NtOpenSection,若返回不成功,就直接返回這個狀態值,不成功當然就不管啦。若成功,就會調用剛才返回的CheckResult_After_NtOpenSection來檢查返回的句柄所指向的對象是不是\Device\PhysicalMemory,若是,就關掉該句柄,并返回一個禁止的狀態碼(STATUS_ACCESS_DENIED),若不是,綠燈大開,放行之.有時,FakeXXX函數返回的CheckResultXXX函數可能不止一個,此時ProxyXXX會根據返回的函數個數循環調用這些CheckResultXXX,有一個不通過即為不通過,只有所有CheckResultXXX都檢查通過了,ProxyXXX函數才會返回原始結果給調用者,真是把關極嚴啊。
值得一提的是,所有的過濾函數對于來自KernelMode的調用都不做處理,所以使用驅動來破壞360是輕而易舉的,但是完全沒有意義。按照MJ一貫的理念,如果你都能加載驅動了,那么所有ring0的保護也就失去了意義,再來保護完全是無用功,事實上也根本起不到保護效果了。
?
四、總結回顧
現在讓我們站得高一點,略去一些細節,來總結一下360整個hook架構:
1.HookPort.sys準備了ServiceFilterInfoTable(上面的表一),里面保存了SSDT函數的代理函數地址、原始例程地址、Hook開關,ShadowSSDT也是。
2.HookPort.sys對外留出了三個接口(保存在了DEVICE_EXTENSION中),第一個用于準備FilterFunRuleTable(上面的表二),第二個用于向FilterFunRuleTable中注冊過濾函數,第三個用于設置FilterFunRuleTable中的過濾規則。
3.HookPort.sys安裝KiFastCallEntry hook.
4.HookPort.sys!KiFastCallEntryFilterFunc根據Hook開關和RuleTable中的過濾規則來決定某個系統服務是否會被Hook,被hook后將會調用Proxy函數。
5.360SelfProtect.sys使用HookPort.sys提供的接口初始化RuleTable,并向RuleTable中注冊過濾函數、設置過濾規則。
6.每個Proxy函數使用CallFilterFunByIndex調用RuleTable中相應的FilterFun進行參數檢查,通過后調用原函數,若調用成功再調用FilterFun函數提供的CheckResult函數檢查結果(非SSDT、ShadowSSDT函數的Hook也采用同樣結構)。
很顯然地:
如果要增加一個系統服務的Hook,只需要打開ServiceFilterInfoTable中該服務Index對應的開關,并提供一個過濾函數就可以了。
如果要去掉一個系統服務的hook,只需要關閉ServiceFilterInfoTable中該服務Index對應的開關,立刻生效,而無須其它改變。
如果要修改一個系統服務的過濾函數,只需要使用HookPort的接口設置新的過濾函數就可以了,無須其它改變。
所以說,這個架構設計非常好,易于修改,易于擴充,易于分工,無愧于優秀二字。
結束語:單獨寫一個或幾個函數的Hook,很多人都會,但是要實現這樣一個產品級的優秀架構,就不是誰都能完成的了。分析了360這樣一個安全產品所使用的Hook架構,讓我收獲頗多,也讓大家領略了360安全衛士的技術魅力。文中如有錯誤,還請MJ和大家指出。
分析了一下360安全衛士的HOOK(三)