05年的老文章了,今天才看到,一直想做而沒有做出來的東西,差距啊...
修正原文一些翻譯不恰當?shù)牡胤?br>
原英文地址:
http://www.codeproject.com/KB/system/soviet_protector.aspxDownload source files - 10.8 Kb Download demo project - 12.1 Kb 一、 簡介
最近,我了解到一個叫做Sanctuary的相當有趣的安全產(chǎn)品。它能夠阻止任何程序的運行-這些程序沒有顯示在軟件列表中-該表中的程序被允許在一個特定的機器上運行。結(jié)果,PC用戶得到保護而免于各種插件間諜軟件、蠕蟲和特洛伊木馬的侵襲-就算能夠進入他/她的計算機,它們也沒有機會執(zhí)行,并因此沒有機會對該機器造成任何損害。當然,我覺得這個特征相當有趣;并且,在稍作思考以后,我就有了一個自己的實現(xiàn)。因此,本文將描述如何通過鉤住本機API的方式來實現(xiàn)監(jiān)控一個進程的創(chuàng)建并在系統(tǒng)級上對之進行控制。
本文大膽假設(shè),目標進程是以一種用戶模式(外殼函數(shù),CreateProcess(),用一系列的本機API調(diào)用的手工的進程創(chuàng)建,等等)創(chuàng)建的。盡管從理論上,一個進程能夠以內(nèi)核方式啟動;不過從實際來看,如此的可能性是可以忽略不計的,因此我們不必為此擔心。為什么?請邏輯地思考一下-為了以內(nèi)核方式啟動一個進程,用戶必須裝載一個驅(qū)動程序,該驅(qū)動程序反過來首先要暗示某種用戶模式代碼的執(zhí)行。因此,為了防止未被授權(quán)程序的執(zhí)行,我們可以安全地在系統(tǒng)級上以用戶模式限制我們自己控制的進程的創(chuàng)建。
二、 定義策略
首先讓我們明確,之所以這樣做的目的是為了在系統(tǒng)級上監(jiān)視和控制進程創(chuàng)建。
進程創(chuàng)建是一件相當復雜的事情-它包含相當多的工作(如果你不相信我,可以反匯編CreateProcess(),這樣你就會親眼看到這點)。為了啟動一個進程,可以使用下列步驟:
1.可執(zhí)行文件必須被以FILE_EXECUTE存取方式打開。
2.可執(zhí)行映像必須被裝載進RAM。
3.必須建立進程執(zhí)行對象(EPROCESS,KPROCESS和PEB結(jié)構(gòu))。
4.必須為新建進程分配地址空間。
5.必須建立進程的主線程的線程執(zhí)行對象(ETHREAD,KTHREAD和TEBstructures)。
6.必須為主線程分配堆棧。
7.必須建立進程的主線程的執(zhí)行上下文。
8.必須通知Win32子系統(tǒng)有關(guān)該新進程的創(chuàng)建情況。
為確保這些步驟中的任何一步的成功,所有其前面的步驟必須是成功執(zhí)行的(你不能夠在沒有一個可執(zhí)行區(qū)句柄的情況下建立一個可執(zhí)行進程對象;沒有文件句柄的情況下你無法映射一個可執(zhí)行區(qū),等等)。因此,如果我們決定退出任何這些步驟,所有后面的步驟也會失敗,以至于整個進程創(chuàng)建會失敗。上面所有的步驟都可以通過調(diào)用某些本機API函數(shù)的方式來實現(xiàn),這是可以理解的。因此,為了監(jiān)視和控制進程創(chuàng)建,我們所有要做的就是鉤住這些API函數(shù)-它們無法旁路掉要創(chuàng)建一新進程所要執(zhí)行的代碼。
我們應該鉤住哪些本機API函數(shù)呢?盡管NtCreateProcess()似乎是問題的最顯然的答案,但是,這個答案是錯誤的-有可能不需要調(diào)用這個函數(shù)也可以創(chuàng)建一個新的進程。例如,CreateProcess()可以在不調(diào)用NtCreateProcess()的情況下創(chuàng)建與進程相關(guān)的內(nèi)核模式結(jié)構(gòu).因此,這樣以來鉤住NtCreateProcess()對我們毫無幫助。
為了監(jiān)視進程的創(chuàng)建,我們必須鉤住NtCreateFile()和NtOpenFile(),或者NtCreateSection()之中的一個,-不經(jīng)調(diào)用這些API是絕對無法運行任何可執(zhí)行文件的。如果我們決定監(jiān)視對NtCreateFile()和NtOpenFile()的調(diào)用,那么我們必須區(qū)別開進程創(chuàng)建和常規(guī)的文件IO操作。這項任務并不總是那么容易。例如,如果一些可執(zhí)行文件正在被以FILE_ALL_ACCESS存取方式打開,我們該怎么辦?這僅是一個IO操作還是一個進程創(chuàng)建的一部分?在這點上,是很難判斷的-我們需要了解調(diào)用線程下一步要干什么。因此,鉤住NtCreateFile()和NtOpenFile()可能不是最好的選擇。
鉤住NtCreateSection()是更為合理的-如果我們在發(fā)生把可執(zhí)行文件映射為映像(SEC_IMAGE 屬性)的請求發(fā)生時攔截對NtCreateSection()的調(diào)用,結(jié)合允許執(zhí)行頁面保護的請求;那么,我們可以確信該進程將要被啟動。在這一點上,我們是能夠作出決定,如果我們不想該進程被創(chuàng)建,可以讓NtCreateSection()返回STATUS_ACCESS_DENIED。因此,為了完全控制目標機器上的進程創(chuàng)建,所有我們要做的是在系統(tǒng)級上鉤住NtCreateSection()。
象來自于ntdll.dll中的任何其它代理一樣,NtCreateSection()用服務索引加載EAX,使EDX指向函數(shù)參數(shù),并且把執(zhí)行權(quán)傳遞到KiDispatchService()內(nèi)核模式例程(這是通過Windows NT/2000中的INT 0x2E指令或者Windows XP下的SYSENTER指令實現(xiàn)的)。在校驗完函數(shù)參數(shù)之后,KiDispatchService()把執(zhí)行權(quán)傳遞到服務的實際實現(xiàn)部分-它的地址可用于服務描述表(指向這個表的指針由ntoskrnl.exe作為KeServiceDescriptorTable變量所輸出,所以它對于內(nèi)核模式驅(qū)動程序是可用的)中。服務描述表通過下列結(jié)構(gòu)所描述:
struct SYS_SERVICE_TABLE {
void **ServiceTable;
unsigned long CounterTable;
unsigned long ServiceLimit;
void **ArgumentsTable;
};
這個結(jié)構(gòu)中的ServiceTable字段指向一個數(shù)組-它擁有所有實現(xiàn)系統(tǒng)服務的函數(shù)的地址。因此,為了在系統(tǒng)級上鉤住任何本機API函數(shù),所有我們必須做的是把我們的代理函數(shù)的地址寫入被KeServiceDescriptorTable的ServiceTable字段所指向的數(shù)組的第i個入口(i是服務索引)。
至此,看起來我們已了解了在系統(tǒng)級上監(jiān)視和控制進程創(chuàng)建的一切。現(xiàn)在讓我們開始實際的工作。
三、 控制進程創(chuàng)建
我們的解決方案由一個內(nèi)核模式驅(qū)動程序和一個用戶模式應用程序組成。為了開始監(jiān)視進程創(chuàng)建,我們的應用程序要把服務索引(相應于NtCreateSection())以及交換緩沖區(qū)的地址傳遞到我們的驅(qū)動程序。這是由下列代碼所完成的:
//打開設(shè)備 device=CreateFile("\\.\PROTECTOR",GENERIC_READ|GENERIC_WRITE, 0,0,OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM,0); //得到NtCreateSection的索引并把它連同輸出緩沖區(qū)的地址傳遞給設(shè)備 DWORD * addr=(DWORD *) (1+(DWORD)GetProcAddress(GetModuleHandle("ntdll.dll"),"NtCreateSection")); ZeroMemory(outputbuff,256); controlbuff[0]=addr[0]; controlbuff[1]=(DWORD)&outputbuff[0]; DeviceIoControl(device,1000,controlbuff,256,controlbuff,256,&dw,0); |
此代碼是顯然的-唯一需要注意的是我們得到服務索引的方式。所有來自于ntdll.dll的代理都從一行代碼MOV EAX,ServiceIndex開始-它可以適用于任何版本和風味的Windows NT。這是一條5字節(jié)長的指令,以MOV EAX操作碼作第一字節(jié),服務索引作為留下的4字節(jié)。因此,為了得到相應于一些特別的本機API函數(shù)的服務索引,所有你要做的是從該地址讀取4個字節(jié),-位于從這個代理開始1字節(jié)距離的地方。
現(xiàn)在讓我們看一下我們的驅(qū)動程序做什么,當它收到來自我們的應用程序的IOCTL時:
NTSTATUS DrvDispatch(IN PDEVICE_OBJECT device,IN PIRP Irp) { UCHAR*buff=0; ULONG a,base; PIO_STACK_LOCATION loc=IoGetCurrentIrpStackLocation(Irp); if(loc->Parameters.DeviceIoControl.IoControlCode==1000) { buff=(UCHAR*)Irp->AssociatedIrp.SystemBuffer; //鉤住服務調(diào)度表 memmove(&Index,buff,4); a=4*Index+(ULONG)KeServiceDescriptorTable->ServiceTable; base=(ULONG)MmMapIoSpace(MmGetPhysicalAddress((void*)a),4,0); a=(ULONG)&Proxy; _asm { mov eax,base mov ebx,dword ptr[eax] mov RealCallee,ebx mov ebx,a mov dword ptr[eax],ebx } MmUnmapIoSpace(base,4); memmove(&a,&buff[4],4); output=(char*)MmMapIoSpace(MmGetPhysicalAddress((void*)a),256,0); } Irp->IoStatus.Status=0; IoCompleteRequest(Irp,IO_NO_INCREMENT); return 0; } |
正如你所見,這里沒有什么特別的-我們只是通過MmMapIoSpace()來把交換緩沖區(qū)映射到內(nèi)核中,另外把我們的代理函數(shù)的地址寫到服務表(當然,我們這是在把實際的服務執(zhí)行的地址保存到全局變量RealCallee以后這樣做的)。為了改寫服務表的適當入口,我們通過MmMapIoSpace()來映射目標地址。為什么我們要這樣做?不管怎么說,我們已經(jīng)可以存取服務表了,不是嗎?問題是,服務表可能駐留在一段只讀內(nèi)存中。因此,我們必須檢查一下是否我們有對目標空間寫的權(quán)限,而如果我們沒有這個權(quán)限,那么在改寫服務表之前,我們必須改變頁面保護。你不認為這樣以來工作太多了嗎?因此,我們僅用MmMapIoSpace()來映射我們的目標地址,這樣以來,我們就不必擔心任何的頁面保護問題了-從現(xiàn)在開始,我們假定已有到目標頁面寫的權(quán)限了。現(xiàn)在讓我們看一下我們的代理函數(shù):
//這個函數(shù)用來確定是否我們應該允許NtCreateSection()調(diào)用成功 ULONG __stdcall check(PULONG arg) { HANDLE hand=0;PFILE_OBJECT file=0; POBJECT_HANDLE_INFORMATION info;ULONG a;char*buff; ANSI_STRING str; LARGE_INTEGER li;li.QuadPart=-10000; //檢查標志。如果所要求的存取方式不是PAGE_EXECUTE, //這并不要緊 if((arg[4]&0xf0)==0)return 1; if((arg[5]&0x01000000)==0)return 1; //經(jīng)由文件句柄得到文件名 hand=(HANDLE)arg[6]; ObReferenceObjectByHandle(hand,0,0,KernelMode,&file,&info); if(!file)return 1; RtlUnicodeStringToAnsiString(&str,&file->FileName,1); a=str.Length;buff=str.Buffer; while(1) { if(buff[a]=='.'){a++;break;} a--; } ObDereferenceObject(file); //如果它是不可執(zhí)行的,這也不要緊 //返回1 if(_stricmp(&buff[a],"exe")){RtlFreeAnsiString(&str);return 1;} //現(xiàn)在,我們要詢問用戶的選擇。 //把文件名寫入緩沖區(qū),并等待直到用戶顯示響應 //(第一個DWORD為1意味著我們可以繼續(xù)) //同步存取該緩沖區(qū) KeWaitForSingleObject(&event,Executive,KernelMode,0,0); //把緩沖區(qū)的前兩個DWORD置為0, //把字符串復制到該緩沖區(qū)中,并循環(huán)下去,直到用戶把每一個 //DWORD置為1. //第二個DWORD的值指明用戶的響應 strcpy(&output[8],buff); RtlFreeAnsiString(&str); a=1; memmove(&output[0],&a,4); while(1) { KeDelayExecutionThread(KernelMode,0,&li); memmove(&a,&output[0],4); if(!a)break; } memmove(&a,&output[4],4); KeSetEvent(&event,0,0); return a; } //僅保存執(zhí)行上下文并調(diào)用check() _declspec(naked) Proxy() { _asm{ //保存執(zhí)行上下文并調(diào)用check() //-后面的依賴于check()所返回的值 // 如果返回值是1,繼續(xù)實際的調(diào)用。 //否則,返回STATUS_ACCESS_DENIED pushfd pushad mov ebx,esp add ebx,40 push ebx call check cmp eax,1 jne block //繼續(xù)實際的調(diào)用 popad popfd jmp RealCallee //返回STATUS_ACCESS_DENIED block:popad mov ebx, dword ptr[esp+8] mov dword ptr[ebx],0 mov eax,0xC0000022L popfd ret 32 } } |
Proxy()保存寄存器和標志,把一個指向服務參數(shù)的指針壓入棧中并調(diào)用check()。其它的依賴于check()所返回的值。如果check()返回TRUE(也就是,我們想要繼續(xù)請求),那么,Proxy()將恢復寄存器和標志,并且把控制權(quán)交給服務實現(xiàn)部分。否則,Proxy()將把STATUS_ACCESS_DENIED寫入EAX,恢復ESP并返回-從調(diào)用者的觀點來看,這就象對NtCreateSection()的調(diào)用失敗一樣-以錯誤狀態(tài)STATUS_ACCESS_DENIED返回。
check()函數(shù)是怎樣做出決定的?一旦它收到一個指向服務參數(shù)的指針參數(shù),它就可以檢查這些參數(shù)。首先,它檢查標志和屬性-如果有一部分沒有被要求作為一個可執(zhí)行映像映射,或如果要求的頁面保護不允許執(zhí)行,那么我們可以確定NtCreateSection()調(diào)用與進程創(chuàng)建毫無關(guān)系。在這種情況下,check()直接返回TRUE。否則,它將檢查該潛在文件的擴展-畢竟,SEC_IMAGE屬性和允許執(zhí)行的頁面保護可能被要求來映射某個DLL文件。如果該潛在文件不是一個.exe文件,那么,check()將返回TRUE。否則,它給用戶模式代碼一個作出決定的機會。因此,它僅把文件名和路徑寫到交換緩沖區(qū),并且對它循環(huán)查詢,直到它得到響應為止。
在打開我們的驅(qū)動程序前,我們的應用程序創(chuàng)建一個運行下面函數(shù)的線程:
void thread() { DWORD a,x; char msgbuff[512]; while(1) { memmove(&a,&outputbuff[0],4); //如果什么也沒有,Sleep() 10毫秒并再檢查 if(!a){Sleep(10);continue;} //看起來象我們的權(quán)限被詢問。 //如果被懷疑的文件已經(jīng)存在于空白列表中, // 則給出一個積極的響應。 char*name=(char*)&outputbuff[8]; for(x=0;x<stringcount;x++) { if(!stricmp(name,strings[x])){a=1;goto skip;} } //要求用戶允許運行該程序 strcpy(msgbuff, "Do you want to run "); strcat(msgbuff,&outputbuff[8]); //如果用戶的答復是積極的,那么把這個程序添加到空白列表中 if(IDYES==MessageBox(0, msgbuff,"WARNING",MB_YESNO|MB_ICONQUESTION|0x00200000L)) {a=1; strings[stringcount]=_strdup(name);stringcount++;} else a=0; // 把響應寫入緩沖區(qū)中,而由驅(qū)動程序之后取回它 skip:memmove(&outputbuff[4],&a,4); //告訴驅(qū)動程序繼續(xù) a=0; memmove(&outputbuff[0],&a,4); } } |
這段代碼是顯然的-我們的線程每10毫秒查詢交換緩沖區(qū)。如果它發(fā)現(xiàn)我們的驅(qū)動程序已經(jīng)把它的請求寄到了該緩沖區(qū)中,它就檢查被允許在本機上運行的程序列表中的文件的文件名和路徑。如果發(fā)現(xiàn)匹配,它直接給出一個OK響應。否則,它顯示一個消息窗口,詢問用戶是否允許有問題的程序執(zhí)行。如果響應是積極的,我們就把有問題的程序添加到允許在本機上運行的軟件列表中。最后,我們把用戶響應寫入緩沖區(qū),也就是說,把它傳遞到我們的驅(qū)動程序。因此,該用戶就能完全控制它的PC上的進程的創(chuàng)建-只要我們的程序運行,在沒有用戶所給予權(quán)限的情況下,絕對沒有辦法來啟動該PC上的任何進程。
正如你所見,我們讓內(nèi)核方式代碼等待用戶反應。這是否是一種聰明的舉措呢?為了回答這個問題,你必須問你自己你是否正在堵住任何關(guān)鍵的系統(tǒng)資源-一切都依賴于具體的情況。在我們的情況下,一切發(fā)生在IRQLPASSIVE_LEVEL級上,并沒有包含對IRPs的處理,并且必須等待用戶響應的線程并不十分重要。因此,在我們的情況下,一切工作正常。然而,本例僅為演示之目的而編寫。為了實際地使用它,以一個自動啟動的服務的方式來重寫我們的應用程序是很重要的。在這種情況下,我建議我們解除LocalSystem帳戶,并且,在NtCreateSection()被用LocalSystem帳戶特權(quán)在一個線程的上下文中調(diào)用的情況下,可以繼續(xù)實際的服務實現(xiàn)而不施行任何檢查-不管怎么說,LocalSystem帳戶僅運行那些在注冊表中指定的可執(zhí)行程序。因此,這樣的一種解除不會是與我們的安全相妥協(xié)的。
四、 結(jié)論 最后,我必須指出,鉤住本機API很明顯是現(xiàn)已存在的最強有力的編程技術(shù)之一。本文通過一個例子向你展示通過鉤住本機API可以實現(xiàn)的能力-正如你所見,我們已設(shè)法防止未被授權(quán)的程序的執(zhí)行-這可以通過鉤住單一的本機API函數(shù)來實現(xiàn)。你可以進一步擴展這個方法,并且獲得對硬件設(shè)備、文件IO操作、網(wǎng)絡流量等等的完全控制。然而,我們現(xiàn)在的解決方案并不是準備為內(nèi)核模式API調(diào)用者所用-一旦內(nèi)核模式代碼被允許直接調(diào)用ntoskrnl.exe的輸出,則這些調(diào)用就不需要經(jīng)由系統(tǒng)服務發(fā)送者進行了。
本文源碼在運行Windows XP SP2的若干機器上成功地測試過。盡管我還沒在任何另外的環(huán)境下面測試它,我相信它應該到處工作正常-不管怎么說,它從未使用任何系統(tǒng)特定的結(jié)構(gòu)。為了運行這個示例,所有你要做的是放置protector.exe和protector.sys到相同的目錄下,并且運行protector.exe。直到protector.exe的應用程序窗口被關(guān)閉為止;否則,每次你都會被提示你試圖運行任何可執(zhí)行程序。