重載內核實現繞過一切鈎子


很久不玩PE格式了,這次由於要恢復SSDT表的緣故+一個忽然興起的念頭,導致我花了一個小午寫了個運行在Ring0的簡單PE加載器,並且有意外的收獲。

恢復SSDT表手段很多,基本上都是直接從文件中依賴重定位表獲取對應數據,重定位后得到相對當前內核加載位置的正確調用地址。大部分的實現代碼比較粗糙,因為要不停的在文件地址和內存地址之間進行轉換,因此代碼繁瑣不易讀懂。

我在考慮解放方案的時候有這樣一個念頭:將內核在Ring0重新加載一份,但是在重定位時,不要針對剛剛加載的基址,而是針對內核真實的加載基址來重定位。本來這個念頭只是一下閃過,但是我隱約覺着這樣是可行的,而且如果成功,那么就不只是恢復SSDT那么簡單了。

試驗過來,確實可行。重定位結束后,在內存中有了2個內核,一份是原來的(方便起見成為FirstKernel,簡寫FK),一份是我剛剛加載起來的,在新加載的這份內核中(方便起見稱為SecondKernel,簡寫SK),所有的函數代碼都是全新的,干凈的,未曾被HOOK代碼污染過的,安全的,而所有針對數據的引用,則都因為以FK加載基址為目標重定向的緣故,被正確的指向了FK所在的內存區域,這樣一來,SK中的所有導出函數都是可以直接進行調用的!只要通過函數在FK中的偏移就可以簡單的得到其在SK中的調用地址,相當於我們擁有了一份完全干凈的內核函數集,此時恢復SSDT無論是表項還是代碼HOOK,都很容易了,因為我已經有了一份完全干凈的內核,我稱之為Kernel In Kernel(其名靈感來自 Windows On Windows ),無論在安全還是反安全方面, KIK都有不小的意義!

這樣一來SK的加載者就可以使用SK中的內核函數集來進行各種功能調用,從而繞開FK內核中已經存在的各種各樣的鈎子。 如果再做一些簡單處理,就可以輕松的將SSDT表導出變量也指向FK內核,這樣的話,所有在內核中存在在的HOOK鈎子就徹底的失效了(之所以還要進行一些簡單處理,原因在於 SSDT的初始化有一部分是在內核引導時完成的,而不是在編譯時完成的,可參考wrk中的內核函數 KiInitSystem(void))。

原理已經給出來了,詳細代碼我不想貼了,流程很簡單,首先讀取DosHeader和NtHeader,計算出展開后需要的內存字節數,分配足夠的內存,讀取SectionHeader,按照SectionHeader中記錄的數據,將每一個Seciton從文件中讀入內存對應的展開地址處(就是由File::PointerToRawData 讀入到 Mem::Base + VirtualAddress處),最后進行重定位處理即可。由於我這里目標在於加載第二個內核(KIK),因此輸入輸出表之類的不需要我來處理,重定位的過程中,會自動指向到FK內核,而FK早就搞定這些了…… 下面記錄的是下代碼調試過程中遇到的一些問題,僅供參考,錯誤之處,請務必拍磚,在下感激不盡:-)

1. PE展開在內存后所需要的實際內存字節數計算方法:
直接讀NtHeader中的SizeOfImage可以的,但是還有個計算方法:
首先遍歷所有的SectionHeader,記錄下具有最大VirtualAddress的節,將該節的VirtualAddress + Aligne(VirtualSize, NtHeader::SectionAligenment) 得到的數值就是PE文件展開后實際所需要的內存字節數

2. 區段展開時到底應該讀取多少個字節?
這個問題取決於該區段的 SizeOfRawData和VirtualSize 這兩個字段,應該取值min(SizeOfRawData, VirtualSize)。如果 VirtualSize > SizeOfRawData,那么多余的部分用0填充(全局變量們為啥為0?:-) )。如果 VirtualSize < SizeOfRawData,那么讀入正確的長度即可,這是因為NtHeader::FileAlignement域中記錄的對齊數值引起的文件對齊空洞。

3. 對重定位表結束點的判定:
國內耳熟能詳的《加密解密》中提到,會以一個VirtualAddress為0的塊結束所有的重定位塊。這個值得商榷,Win2k的內核 NTOSKRNL.EXE,由於第一個.text區段是從0x540加載,導致第一個重定位塊的VirtualAddress直接為0(每個重定位塊負責4K的區域),進而使得重定位操作整體被跨越了(可能很多PE工具也有類似的問題,比如PETOOLS就看不到2k內核的重定位表)。
於是我改為同時針對SizeOfBlock和VirtualAddress進行判斷,但是在Vista的內核 NTKRNLPA.EXE 中遇到了反問題,該內核的最后一個重定位表的VIrtualAddress不為0但是SizeOfBlock為0,導致重定位操作溢出越界。
總結了一下,發現最可靠的是SizeOfBlock域,這個域本身的含義就是指明自身塊的尺寸(雖然嚴格按照定義其實這個域該永不為0),因此作為是否最后一個塊的判斷是比較准確的。改為針對該數值判斷后,在2k到Win7(均為32位)的內核上,測試均通過。
因此同重定位表中的 SizeOfBlock進行比較判斷,是判斷重定位表是否結束的較佳方法。當然也可以通過判斷操作內存指針是否越過了重定位區塊的結尾來判斷,但是較之前一個方法,我個人以為不夠優雅。

以上,Kernel In Kernel, Good JOB!! :-)

 

內核自古乃兵家必爭之地,某些流氓,某些小人,總喜歡在內核做一堆的patch,你patch就算了,你還去patch那些未導出的危險函數,讓機器BSOD, 這也算了,你還加個檢測,我勒個去,是可忍孰不可忍!

兔子逼急了還要咬人,哥逼急了直接滅了你

算了,其實很簡單的兩步。。。。好吧,我承認我是來騙kx的

1:將內核文件映射到我內核空間去;
2:path KiFastCallEntry重定向

一:加載內核

1:關於加載
我代碼里用了兩種方法,一種被我注釋了的:
a:使用ZwCreateSection + ZwMapVieOfSection 結合 SEC_IMAGE 標志來加載,此時加載的地址必定是位於user-mode空間,可以簡單的用MDL鎖定,然后映射這些頁面到kernel-mode空間,也可以直接ExAllocatePool 然后 copy加載的鏡像
b : 直接ReadFile啦,對PE格式了解點點,很簡單的,不多說,看代碼吧

2:關於修正鏡像
1:重定位
這個是必須的,但是要注意,必須以原先系統的內核為基址重定位,因為你想自己初始化一些東西如對象頭,調度鏈之類的已經不可能了,你必須讓新內核的數據指向老內核

2:修正SDT
這個不需要多說,算算RVA,四則運算一做就好啦


參考資料: 網易博客一篇好像叫《kernel in kernel》的文章……具體我也不清楚了,調試這個驅動時發現的一篇的文章,寫的很好

 

繞過文件下載:https://files.cnblogs.com/files/csnd/kernel_reload.zip


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM