通過對PsSetCreateProcessNotifyRoutineEx的逆向分析得出的結果來實現反進程監控
前言
關于如何使用PsSetCreateProcessNotifyRoutineEx來實現進程監控,請看這篇文章:
通過PsSetCreateProcessNotifyRoutineEx和PsSetCreateThreadNotifyRoutine實現進程與線程監控(https://bbs.pediy.com/thread-269312.htm)
這篇主要是講解通過逆向PsSetCreateProcessNotifyRoutineEx來查看這個API如何實現的進程監控并以此來實現反進程監控。
這次的實驗是在Win7 x86系統上進行,所以如果是其他版本的Windows系統,里面的具體實現會有不同。
逆向分析PsSetCreateProcessNotifyRoutine
首先看下PsSetCreateProcessNotifyRoutine的反匯編結果。
PAGE:005947DE ; NTSTATUS __stdcall PsSetCreateProcessNotifyRoutine(PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, BOOLEAN Remove)PAGE:005947DE public PsSetCreateProcessNotifyRoutinePAGE:005947DE PsSetCreateProcessNotifyRoutine proc nearPAGE:005947DE ; CODE XREF: sub_6FC83F+2B↓pPAGE:005947DE ; sub_798D02+22↓pPAGE:005947DEPAGE:005947DE NotifyRoutine = dword ptr 8PAGE:005947DE Remove = byte ptr 0ChPAGE:005947DEPAGE:005947DE mov edi, ediPAGE:005947E0 push ebpPAGE:005947E1 mov ebp, espPAGE:005947E3 push 0 ; 將0壓入棧中PAGE:005947E5 push dword ptr [ebp+Remove] ; 將Remove壓入棧中PAGE:005947E8 push [ebp+NotifyRoutine] ; 將Routine壓入棧中PAGE:005947EB call PspSetCreateProcessNotifyRoutinePAGE:005947F0 pop ebpPAGE:005947F1 retn 8PAGE:005947F1 PsSetCreateProcessNotifyRoutine endp
可以看到PsSetCreateProcessNotifyRoutine就干了一件事,就是調用PspSetCreateProcessNotifyRoutine。在對它的調用時候,壓入的兩個參數除了NotifyRoutine和Remove以外,還壓入了一個0。事實上PsSetCreateProcessNotifyRoutine和PsSetCreateProcessNotifyRoutineEx的唯一區別就是,在壓入第三個參數的時候,PsSetCreateProcessNotifyRoutine壓入的是0,而PsSetCreateProcessNotifyRoutineEx壓入的是1。
接著跟進PspSetCreateProcessNotifyRoutine。
PspSetCreateProcessNotifyRoutine proc nearPAGE:005947F9 ; CODE XREF: PsSetCreateProcessNotifyRoutineEx+D↑pPAGE:005947F9 ; PsSetCreateProcessNotifyRoutine+D↑pPAGE:005947F9PAGE:005947F9 arg_Routine = dword ptr 8PAGE:005947F9 arg_Remove = dword ptr 0ChPAGE:005947F9 arg_Zero = byte ptr 10hPAGE:005947F9PAGE:005947F9 mov edi, ediPAGE:005947FB push ebpPAGE:005947FC mov ebp, espPAGE:005947FE cmp byte ptr [ebp+arg_Remove], 0 ; 判斷Remove是否為0PAGE:00594802 push ebxPAGE:00594803 push esiPAGE:00594804 push ediPAGE:00594805 jz loc_594905 ; 為0則跳轉,當要增加回調的時候,會傳入FALSE,所以這里會跳轉
函數首先判斷壓入的Remove參數是否為0,是的話則跳轉。由于在增加回調的時候,Remove參數會使用FALSE,所以這個時候這個跳轉會成立。接著跟進loc_594905看看。
PAGE:00594905 loc_594905: ; CODE XREF: PspSetCreateProcessNotifyRoutine+C↑jPAGE:00594905 cmp [ebp+arg_Zero], 0 ; 判斷第三個參數是否為0PAGE:00594909 jz short loc_59491E PAGE:0059490B push [ebp+arg_Routine]PAGE:0059490E call sub_56F869PAGE:00594913 test eax, eaxPAGE:00594915 jnz short loc_59491E PAGE:00594917 mov eax, STATUS_ACCESS_DENIEDPAGE:0059491C jmp short loc_59496B
在loc_594905中,函數會判斷傳進的第三個參數是否為0。上面提到過,這個參數其實是區別你調用的是PsSetCreateProcessNotifyRoutine還是PsSetCreateProcessNotifyRoutineEx。如果是后者,這個參數就是1,前者這個參數是0。而兩者的區別只是后者會調用sub_56F869。但由于這個函數的作用對今天的主題而言沒什么意義,所以這里不分析,直接接著看loc_59491E的內容。
PAGE:0059491E loc_59491E: ; CODE XREF: PspSetCreateProcessNotifyRoutine+110↑jPAGE:0059491E ; PspSetCreateProcessNotifyRoutine+11C↑jPAGE:0059491E xor eax, eax ; 將eax清0PAGE:00594920 cmp [ebp+arg_Zero], al ; 將傳入的第三個參數與al做比較PAGE:00594923 setnz al ; 將zf位取反然后賦值給alPAGE:00594926 push eax ; 將eax入棧PAGE:00594927 push [ebp+arg_Routine] ; 將要設置的回調函數地址入棧PAGE:0059492A call AllocateAssignPAGE:0059492F mov ebx, eax ; 將分配的內存的地址賦給ebxPAGE:00594931 test ebx, ebx ; 判斷ebx是否為0PAGE:00594931 ; 為0則為eax賦值內存不夠的返回值并跳到函數結束PAGE:00594933 jnz short loc_59493CPAGE:00594935 mov eax, STATUS_INSUFFICIENT_RESOURCESPAGE:0059493A jmp short loc_59496B
函數將要設置的回調函數的地址作為第一個參數,根據調用是否帶有Ex的PsSetCreateProcessNotifyRoutine來傳入第二個參數。
這里主要是調用AllocateAssign這個函數,當然這個函數名是我重命名以后的函數名。因為這個函數就是為保存回調函數地址申請一塊空間,如果申請到了那么會跳轉繼續執行,如果沒申請到就會設置返回值為沒有足夠的資源并且跳到函數結束執行,接下來看看AllocateAssign的反匯編結果。
PAGE:005946D7 AllocateAssign proc near ; CODE XREF: PsSetLoadImageNotifyRoutine+E↑pPAGE:005946D7 ; DbgkLkmdRegisterCallback+53↑p ...PAGE:005946D7PAGE:005946D7 arg_NotifyRoutine= dword ptr 8PAGE:005946D7 arg_Zero = dword ptr 0ChPAGE:005946D7PAGE:005946D7 mov edi, ediPAGE:005946D9 push ebpPAGE:005946DA mov ebp, espPAGE:005946DC push 'brbC' ; TagPAGE:005946E1 push 0Ch ; NumberOfBytesPAGE:005946E3 push PagedPool ; PoolTypePAGE:005946E5 call ExAllocatePoolWithTagPAGE:005946EA test eax, eaxPAGE:005946EC jz short loc_5946FDPAGE:005946EE mov ecx, [ebp+arg_NotifyRoutine]PAGE:005946F1 and dword ptr [eax], 0 ; 分配到的內存低4位清0PAGE:005946F4 mov [eax+4], ecx ; 分配到的內存的中間4位賦值為函數地址PAGE:005946F7 mov ecx, [ebp+arg_Zero]PAGE:005946FA mov [eax+8], ecx ; 分配到的內存的最高4位賦值為傳入的參數,此時為0PAGE:005946FDPAGE:005946FD loc_5946FD: ; CODE XREF: AllocateAssign+15↑jPAGE:005946FD pop ebpPAGE:005946FE retn 8PAGE:005946FE AllocateAssign endp
可以看到這個函數中,首先是申請0xC大小的內存空間,然后將低4為清0,中間4位賦值為要設置的回調函數的地址,最高4位根據傳入的參數來設置。
由此我們可以知道,在PsSetCreateProcessNotifyRoutine函數中,系統會分配0xC大小的內存,而內存的中間4位保存的就是要設置的回調函數的地址。根據調用的是PsSetCreateProcessNotifyRoutine還是PsSetCreateProcessNotifyRoutineEx,系統會設置最高的4位的值,如果是前者會設置位0,后者設置位1。
接著看內存看內存分配并且賦值成功以后,會執行的代碼,也就是loc_59493C的代碼。
PAGE:0059493C loc_59493C: ; CODE XREF: PspSetCreateProcessNotifyRoutine+13A↑jPAGE:0059493C mov esi, offset ProcessFuncArray ; 將進程數組地址賦給esiPAGE:00594941 xor edi, edi ; 為edi清0PAGE:00594943PAGE:00594943 loc_594943: ; CODE XREF: PspSetCreateProcessNotifyRoutine+165↓jPAGE:00594943 push 0 ; 將0壓入棧中PAGE:00594945 mov ecx, ebx ; 分配到的內存的地址賦值給ecxPAGE:00594947 mov eax, esi ; 數組地址賦值給eaxPAGE:00594949 call SetArrayPAGE:0059494E test al, alPAGE:00594950 jnz short loc_594972 ; 函數返回值不為0,說明設置成功PAGE:00594952 add edi, 4 ; 將edi加4PAGE:00594955 add esi, 4 ; 數組的地址加4,也就是獲得下一個元素的地址PAGE:00594958 cmp edi, 100h ; 判斷是否小于0x100,小于的話。使用新地址來調用上面的函數繼續設置PAGE:0059495E jb short loc_594943 ; 這里可以得出結論,這個數組一共0x100 % 4 = 0x40PAGE:0059495E ; 也就是說,最多可以設置64個進程創建的回調函數PAGE:00594960 push ebx ; BufferPAGE:00594961 call FreeAllocatePAGE:00594966 mov eax, STATUS_INVALID_PARAMETER ; 如果執行到這里說明賦值失敗PAGE:00594966 ; 接下來就釋放內存,返回值設為失敗PAGE:0059496BPAGE:0059496B loc_59496B: ; CODE XREF: PspSetCreateProcessNotifyRoutine+A8↑jPAGE:0059496B ; PspSetCreateProcessNotifyRoutine+10A↑j ...PAGE:0059496B pop ediPAGE:0059496C pop esiPAGE:0059496D pop ebxPAGE:0059496E pop ebpPAGE:0059496F retn 0Ch
可以看到,函數首先將一個數組地址賦給esi,然后將0壓入棧中并且調用SetArray這個函數,在調用這個函數之前還把esi的地址,也就是這個數組的地址賦給了eax,把ebx的值賦給ecx,這個ebx的值就是在上面調用完AllocateAssign函數以后得到的分配和賦值的內存地址。因為在調用AllocateAssign函數后的返回值eax賦值給了ebx。
調用完SetArray函數以后,會判斷返回值是否為0,不為0那就是調用成功,也就是回調函數設置成功,就會跳轉到函數調用成功的退出代碼執行。如果為0說明并沒有設置完成,隨后會對edi和esi進行加4的操作并判斷edi是否小于0x100,如果小于就跳上去繼續執行SetArray函數。由于此時esi指向的是ProcessFuncArray數組的地址,所以加4就是指向數組中下一個元素,edi就是為了避免數組溢出而設置,根據值的大小可以猜測這個數組一共0x40個大小。
分析到這里就可以得出結論,PsSetCreateProcessNotifyRoutine函數設置進程監控的回調函數的辦法就是首先申請一塊內存來保存要設置的回調函數的地址,然后將這個申請到的內存的地址賦到ProcessFuncArray數組中的某個元素。
那么它是怎么判斷要賦值到哪個元素呢,就要繼續跟進SetArray函數。
PAGE:00594706 SetArray proc near ; CODE XREF: IoRegisterPriorityCallback+5B↑pPAGE:00594706 ; IoUnregisterPriorityCallback+4C↑p ...PAGE:00594706PAGE:00594706 var_8 = dword ptr -8PAGE:00594706 var_4 = dword ptr -4PAGE:00594706 arg_Zero = dword ptr 8PAGE:00594706PAGE:00594706 mov edi, ediPAGE:00594708 push ebpPAGE:00594709 mov ebp, espPAGE:0059470B push ecx ; 此時ecx是分配的地址,eax是數組的地址PAGE:0059470C push ecxPAGE:0059470D push ebxPAGE:0059470E push esiPAGE:0059470F push ediPAGE:00594710 mov esi, eax ; 將數組地址賦給esiPAGE:00594712 test ecx, ecx ; 判斷分配地址是否為0PAGE:00594714 jz short loc_594726 PAGE:00594716 push 8PAGE:00594718 pop edxPAGE:00594719 call ExAcquireRundownProtectionEx ; 為了讓調用者安全訪問對象,這里不是重點,過PAGE:0059471E test al, alPAGE:00594720 jz loc_5947D0 ; 返回為0就代表函數運行失敗,會退出函數PAGE:00594726PAGE:00594726 loc_594726: ; CODE XREF: SetArray+E↑jPAGE:00594726 mov ebx, [esi] ; 將數組中的內容取出賦給ebxPAGE:00594728 mov eax, ebx ; 將內容賦給eaxPAGE:0059472A jmp short loc_594749
函數將數組中的對應的內容賦給eax以后跳轉到loc_594759執行,接著看loc_594749處的代碼。
PAGE:00594749 loc_594749: ; CODE XREF: SetArray+24↑jPAGE:00594749 xor eax, [ebp+arg_Zero] ; 將得到的值與0進行異或,與7做比較,小于7則跳轉.PAGE:0059474C cmp eax, 7 ; 所以這兩條合起來看的話,就是除了低3位,其他都為0,否則就跳轉PAGE:0059474F jbe short loc_59472C ; 可以得出結論,數組中元素高29位用來判斷是否還未使用。PAGE:0059474F ; 如果為0,說明還未使用
在這里程序將得到的數組中對應的元素內容和傳入的參數進行異或,然后判斷是否小于7,如果小于7就跳轉到對數組進行賦值的代碼進行執行。而0x7對應的二進制是高29位是0,低3位是1,在跟進得到的數組內容要和0進行異或可以知道,這里是判斷得到的數組中的內容高29位是不是0。如果高29位中有1個是1,那么異或以后得到的值就會大于7。
接著看loc_59472C中的代碼,也就是對數組進行賦值的代碼。
PAGE:0059472C loc_59472C: ; CODE XREF: SetArray+49↓jPAGE:0059472C test ecx, ecx ; ecx是申請的地址,判斷是否為0,為0則跳轉PAGE:0059472E jz short loc_594737PAGE:00594730 mov eax, ecx ; 將ecx賦給eaxPAGE:00594732 or eax, 7 ; 將低三位置1,這里要注意,在將申請到的內存地址復制到數組之前,會將地址的低3位置1PAGE:00594735 jmp short loc_594739 PAGE:00594737 ; ---------------------------------------------------------------------------PAGE:00594737PAGE:00594737 loc_594737: ; CODE XREF: SetArray+28↑jPAGE:00594737 xor eax, eax ; 如果ecx是0,那就會跳轉到這里執行,也就是eax會被變成0PAGE:00594739PAGE:00594739 loc_594739: ; CODE XREF: SetArray+2F↑jPAGE:00594739 mov edx, eax ; 將或運算得到的值賦給edxPAGE:0059473B mov edi, esi ; 取出數組對應元素地址給ediPAGE:0059473D mov eax, ebx ; ebx保存的是數組中的內容,賦給eaxPAGE:0059473F lock cmpxchg [edi], edx ; edi是數組地址,而eax是數組元素,所以eax==[edi]PAGE:0059473F ; 那么此時,edx,也就是計算以后的地址會賦值給[edi],也就是賦值到數組中PAGE:00594743 cmp eax, ebx ; 由于上面的賦值操作,這里會成立跳轉PAGE:00594745 jz short loc_594751 PAGE:00594747 mov ebx, eax
這里的ecx的值就是前面用來保存回調函數地址所調用的AllocateAssign函數得到的分配的內存地址。因為上面申請到內存以后,將內存地址先賦給eax在賦給ecx,隨后的代碼中都沒對ecx進行改變。
將這個地址賦給eax并且與7異或,那就是將低3位置1,接著就跳轉到loc_594739處執行。執行的時候,這個esi就是要賦值的數組的對應元素的地址,因為在進入SetArray函數前,將數組的對應元素的地址賦給了esi后就沒在改變了。而根據cmpxchg的功能可以知道,這里就是將AllocateAssign分配到的內存地址和7或運算以后的地址復制到數組中,隨后跳轉到loc_594751執行。那么繼續看loc_594751執行的代碼。
PAGE:00594751 loc_594751: ; CODE XREF: SetArray+3F↑jPAGE:00594751 mov edi, ebx ; ebx中是數組中原來的內容,將他賦值給ediPAGE:00594753 and edi, 0FFFFFFF8h ; 將低3位清0PAGE:00594756 cmp edi, [ebp+arg_Zero] ; 判斷是否為0,不為0則跳轉退出函數,函數執行失敗,返回值為0PAGE:00594759 jnz short loc_5947C4 ; 這里就是判斷原來的高29位是不是0,不是的話,函數執行失敗PAGE:0059475B test edi, edi ;判斷edi是否位0PAGE:0059475D jz short loc_5947C0 ; 為0則跳轉,函數執行成功,返回值eax=1PAGE:0059475F mov esi, large fs:124h ; 接下來的內容和APC相關,這里不討論PAGE:00594766 dec word ptr [esi+86h]PAGE:0059476D xchg eax, [ebp+var_4]PAGE:00594770 mov ecx, dword_53C7D8PAGE:00594776 and ecx, 1PAGE:00594779 xchg eax, [ebp+var_8]PAGE:0059477C test ecx, ecxPAGE:0059477E jz short loc_594794PAGE:00594780 mov ecx, offset dword_53C7D8PAGE:00594785 call ExfAcquirePushLockExclusivePAGE:0059478A mov ecx, offset dword_53C7D8PAGE:0059478F call ExfReleasePushLockExclusive
在這里,程序首先將原來數組中保存的內容的低三位清0,接著判斷得到的數據是否為0。也就是判斷原來數組中的內容的高29位是不是0,如果是0說明上面的賦值成功了,程序就會跳轉到函數執行成功的地方,也就是loc_5947C0處繼續執行,在loc_5947C0,程序會將eax賦值為1,也就是說返回值是1。如果高29位不是0,那上面的賦值就會失敗,程序就會跳轉到loc_5947C4的地址繼續執行,在這個地址中,程序會將eax賦值為0,也就是說返回值是0。
綜上,可以得出以下的結論:
- 程序會申請一塊0xC大小的內存,內存中低4為是0,中間4為是要設置的回調函數的地址,高4為根據是否是Ex函數來設置為是0還是1。
- 從整型數組ProcessArray中按順序依次查看對應的數組的元素的位置是不是可以用來保存分配的內存地址。
- 而這個位置能不能存儲這個地址,取決于這個位置中保存的內容的高29位是不是0,如果是0就用來保存申請到的地址。
反進程監控的實現
根據上面的分析,可以得出結論,設置這些回調函數的時候,操作系統首先會申請一個0xC大小的內存,然后將內存地址的中間4位設置為回調函數的地址。隨后就會在ArrayProcess數組中存放這個分配的內存地址,當要注意這個內存地址的低3位在復制到數組之前會被置1。
那也就是說當有進程的狀態發生變化的時候,操作系統就會去ProcessArray數組中遍歷,如果發現對應的元素的高29位不是0就把內容取出并且將低3位置0(因為在復制到數組之中去的時候,分配的內存的低3位會被置為1)。置0以后得到的地址在+4的地方保存的就是回調函數的地址,系統就會根據這個地址在調用相應的回調函數。
所以要進行反進程監控,只需要找到這個數組,并且將這個數組中的回調函數的地址取出然后通過PsSetCreateProcessNotifyRoutine函數刪除這個回調函數就可以了。
要找到這個數組首先需要找到PspSetCreateProcessNotifyRoutine函數地址,因為在這個函數中才有對這個數組進行使用,才可以找到數組地址。但因為這個函數并沒有導出,所以需要通過PsSetCreateProcessNotifyRoutine函數來找到它,因為這個函數是導出函數,可以直接獲取到地址。
在PsSetCreateProcessNotifyRoutine中對PspSetCreateProcessNotifyRoutine的調用如下:

可以看到是調用地址處偏移為9的地方就是PspSetCreateProcessNotifyRoutine函數的地址,所以可以將0xE8作為特征碼來查找函數地址。
得到PspSetCreateProcessNotifyRoutine函數地址以后,可以在對esi賦值為函數地址的地方找到ProcessFuncArray數組的地址如下:

所以得到PspSetCreateProcessNotifyRoutine函數地址以后,可以根據0xEB,0x2F和0xBE這三個特征碼來定位進程數組的地址(當然這樣有點太少,可以弄的多一點,這里就偷了個懶)。
根據以上內容就可以寫出獲取ProcessFuncArray地址的代碼如下:
PUINT32 SearchProcessArray(){ UNICODE_STRING uStrFuncName; PUCHAR pPsSetCreateProessNotifyRoutine = NULL; PUCHAR pPspCreateProcessNotifyRoutine = NULL; PUINT32 pProcessArray = NULL; //獲取PsSetCreateProcessNotifyRoutine函數地址 RtlInitUnicodeString(&uStrFuncName, L"PsSetCreateProcessNotifyRoutine"); pPsSetCreateProessNotifyRoutine = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName); //獲取PspCreateProcessNotifyRoutine函數的地址,由于程序執行到最后的ret語句會使用0xC2,所以這里用它作為結束 while (pPsSetCreateProessNotifyRoutine && *pPsSetCreateProessNotifyRoutine != 0xC2) { //根據0xE8來獲取函數地址 if (*pPsSetCreateProessNotifyRoutine == 0xE8) { pPspCreateProcessNotifyRoutine = pPsSetCreateProessNotifyRoutine + 5 + *(PUINT32)(pPsSetCreateProessNotifyRoutine + 1); break; } pPsSetCreateProessNotifyRoutine++; } //獲取回調函數數組的地址,由于程序執行到最后的ret語句會使用0xC2,所以這里用它作為結束 while (pPspCreateProcessNotifyRoutine && *pPspCreateProcessNotifyRoutine != 0xC2) { //根據特征碼來獲取數組的地址 if (*pPspCreateProcessNotifyRoutine == 0xEB && *(pPspCreateProcessNotifyRoutine + 1) == 0x2F && *(pPspCreateProcessNotifyRoutine + 2) == 0xBE) { pProcessArray = (PUINT32)*(PUINT32)(pPspCreateProcessNotifyRoutine + 3); break; } pPspCreateProcessNotifyRoutine++; } return pProcessArray;}
獲取到函數地址以后,只需要遍歷數組并查看相應的高29位是不是0,如果不是0說明里面保存了地址,取出這個地址將低3位置0以后在加4就得到了保存回調函數地址的地址,將這個回調函數地址取出就可以調用PsSetCreateProcessNotifyRoutine函數來進行回調函數的刪除,代碼如下:
NTSTATUS status = STATUS_SUCCESS;PUINT32 pProcessArray = NULL, pFuncAddr = NULL;UINT32 i = 0; pProcessArray = SearchProcessArray();if (pProcessArray != NULL){ for (i = 0; i < 0x40; i++) { if (pProcessArray[i] & ~0x7) { pFuncAddr = (PUINT32)(pProcessArray[i] & ~0x7 + 4); status = PsSetCreateProcessNotifyRoutineEx((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)*pFuncAddr, TRUE); if (NT_SUCCESS(status)) { DbgPrint("成功刪除進程的回調函數, 函數地址0x%X\r", *pFuncAddr); } } }}else DbgPrint("沒有找到進程數組\r");
運行結果
首先是開啟進程監控以后,可以看到所有進程的創建都被監控到了,而且demo.exe進程的創建也被攔截了。

當運行程序卸載掉回調函數以后,在創建進程就沒有被監控到,并且demo.exe進程也可以成功創建。
