作者:Anton Bassov
原文地址:http://www.codeproject.com/KB/system...protector.aspx
作者:LeoF
时间:2010-12-15
简介
最近我偶然看到一款叫作Sanctuary的安全产品的介绍,这个产品非常有趣.它可以阻止任何程序的的运行,只要在特定机器的"允许运行软件列表"中没有这个软件.因此,PC用户就可以对抗间谍插件,蠕虫和木马--即使一些恶意软件可以存在于电脑中,但是它却没有机会来运行,所以也没有机会来对机器造成破坏.当然,我发现这个功能非常有趣,一点思考之后自己把它做了出来.因此,该文介绍如何通过挂钩native API来在系统范围内用程序监控进程的创建.
该文做了一个大胆的假设,假设目标进程在用户模式下被创建(shell函数,CreateProcess(),手动进程创建作为native API调用的一个顺序等等).尽管理论上一个进程可以在内核模式下被创建,这种可能是有特定目的的,可以忽略不计,不用去担心它.为什么???试着想一下-要在内核模式下创建一个进程,一个程序必须要加载一个驱动,这样的话就意味着首先要执行一些用户模式的代码.因此,要阻止未授权的程序运行,我们可以安全地缩小范围到在系统内控制用户模式的创建就可以了.
制定我们的策略
首先,我们要决定欲在系统内监控一个进程创建我们必须做什么.
进程创建是一件相当复杂的事情,它牵扯到大量的工作(如果你不信我,可以反汇编CreateProcess(),你可以亲眼看到).为了启动一个进程,要做以下的步骤:
1.可执行文件被打开(FILE_EXECUTE访问权限)
2.可执行映像(Executable image)被加载到内存中
3.建立进程执行对象(Process Executive Object,EPROCESS,KPROCESS和PEB结构)
4.为新创建的进程分配地址空间
5.建立进程的主线程的线程执行对象(Thread Executive Object,ETHREAD,KTHREAD和TEB结构)
6.建立主线程的堆栈
7.建立主线程的执行上下文(Execution context)
8.通知Win32子系统新进程信息
对于这些步骤中任何一步的成功执行,所有之前的步骤都必须被成功完成(你不可以还没有一个 executable section的句柄就建立Executive Process Object;你也不可以还没有一个文件句柄就要映射executable section等等).因此,如果我们决定终止这些步骤中的任何一步,接下来的操作也同样会失败,这样进程创建就会被终止.所有这些步骤都要调用对应的native API函数,这是很容易理解的.所以,为了监控进程创建,我们需要做的就是挂钩这些API函数不让启动一个新进程的代码通过.
我们需要挂钩哪个native API函数?尽管NtCreateProcess()似乎是最明显的答案,但是它确实是错的--不通过这个函数创建一个进程是有可能的.比如说,CreateProcess()建立一个进程相关的内核模式结构而不通过NtCreateProcess().所以挂钩NtCreateProcess()对我们来说没用.
为了监视进程创建,我们可以挂钩NtCreateFile()和NtOpenFile(),也可以挂钩NtCreateSection()--一个可执行文件运行而不调用这些函数是绝不可能的.如果我们决定监视NtCreateFile()和NtOpenFile(),我们不得不区分进程创建和正常的文件IO操作.这个任务不怎么容易.例如,如果一个可执行文件用FILE_ALL_ACCESS属性来打开,我们应该怎么办???它是一个IO操作呢还是它是进程创建的一部分???在这点上面很难做判断--我们需要看看调用线程下一步将要做什么.因此,挂钩NtCreateFile()和NtOpenFile()不是最好的选择.
挂钩NtCreateSection()是更合理的--如果有一个映射可执行文件作为映像(SEC_IMAGE属性)的请求,我们拦截了这个请求对NtCreateSection()调用,再结合具有可执行属性的页的请求,我们可以确定进程是将要被启动.在这个时候我们可以做决定,如果我们不希望进程被创建,那就让NtCreateSection()返回STATUS_ACCESS_DENIED.因此,为了获得在机器上面全面监控进程创建的权力,我们要做的就是在系统上挂钩NtCreateSection().
和其他的函数(stub这里翻译成了函数)一样,NtCreateSection()把函数的服务索引号放进EAX中,让EDX指向函数的参数,然后执行转到内核函数KiDispatchService()(这一步在windows NT/2000下通过INT 0x2E指令,在windows XP下是SYSENTER实现).在检查过参数的有效性之后,KiDispatchService()把执行权转到实际的服务实现处,在SSDT(System Service Descriptor Table)中这个服务的地址是有效的(ntoskrnl.exe导出的KeServiceDescriptorTable变量指向这个表,因此对于内核驱动它可以使用).SSDT描述见下面的结构:
struct SYS_SERVICE_TABLE {
void **ServiceTable;
unsigned long CounterTable;
unsigned long ServiceLimit;
void **ArgumentsTable;
};
看起来我们已经了解了要在系统中监控进程创建所有我们需要知道的东西了.开始进行实际工作吧!
控制进程创建
我们的解决方案由一个内核模式的驱动和一个用户模式的应用程序组成.为了开始监视进程创建,我们的应用程序根据NtCreateSection()传递服务索引值和通信缓冲区的地址给我们的驱动.以下的代码做了这个工作:
//open device
device=CreateFile("\\\\.\\PROTECTOR",GENERIC_READ|GENERIC_WRITE,
0,0,OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM,0);
// get index of NtCreateSection, and pass it to the driver, along with the
//address of output buffer
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);
现在我们看一下当驱动接收到我们应用程序的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;
// hook service dispatch table
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;
}
//this function decides whether we should
//allow NtCreateSection() call to be successfull
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;
//check the flags. If PAGE_EXECUTE access to the section is not requested,
//it does not make sense to be bothered about it
if((arg[4]&0xf0)==0)return 1;
if((arg[5]&0x01000000)==0)return 1;
//get the file name via the file handle
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);
//if it is not executable, it does not make sense to be bothered about it
//return 1
if(_stricmp(&buff[a],"exe")){RtlFreeAnsiString(&str);return 1;}
//now we are going to ask user's opinion.
//Write file name to the buffer, and wait until
//the user indicates the response
//(1 as a first DWORD means we can proceed)
//synchronize access to the buffer
KeWaitForSingleObject(&event,Executive,KernelMode,0,0);
// set first 2 DWORD of a buffer to zero,
// copy the string into the buffer, and loop
// until the user sets first DWORD to 1.
// The value of the second DWORD indicates user's
//response
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;
}
//just saves execution contect and calls check()
_declspec(naked) Proxy()
{
_asm{
//save execution contect and calls check()
//-the rest depends upon the value check() returns
// if it is 1, proceed to the actual callee.
//Otherwise,return STATUS_ACCESS_DENIED
pushfd
pushad
mov ebx,esp
add ebx,40
push ebx
call check
cmp eax,1
jne block
//proceed to the actual callee
popad
popfd
jmp RealCallee
//return STATUS_ACCESS_DENIED
block:popad
mov ebx, dword ptr[esp+8]
mov dword ptr[ebx],0
mov eax,0xC0000022L
popfd
ret 32
}
}
check()怎么做决定?一旦它接收到一个指向服务参数的指针作为实参,它能检查这些参数.首先,它检查标志和属性--如果一个段(section)没有请求被映射为一个可执行映像(image),或者如果请求的页面保护属性没有允许可执行,我们可以确定这次NtCreateSection()调用没有与进程创建相关.这种情况check()直接返回TRUE.否则,它检查文件的扩展名--毕竟,SEC_IMAGE属性和页面保护属性允许可执行也可能是映射一些DLL文件.如果这个文件不是.exe文件,check()返回TRUE.否则,它给用户模式的代码一个机会来做决定.因此,它只是把文件名称和路径写进通信缓冲区里面,然后不断测试它直到得到回应.
再打开我们的驱动之前,我们的应用程序创建了一个线程来运行下面这个函数:
void thread()
{
DWORD a,x; char msgbuff[512];
while(1)
{
memmove(&a,&outputbuff[0],4);
//if nothing is there, Sleep() 10 ms and check again
if(!a){Sleep(10);continue;}
// looks like our permission is asked. If the file
// in question is already in the white list,
// give a positive response
char*name=(char*)&outputbuff[8];
for(x=0;x<stringcount;x++)
{
if(!stricmp(name,strings[x])){a=1;goto skip;}
}
// ask user's permission to run the program
strcpy(msgbuff, "Do you want to run ");
strcat(msgbuff,&outputbuff[8]);
// if user's reply is positive, add the program to the white list
if(IDYES==MessageBox(0, msgbuff,"WARNING",
MB_YESNO|MB_ICONQUESTION|0x00200000L))
{a=1; strings[stringcount]=_strdup(name);stringcount++;}
else a=0;
// write response to the buffer, and driver will get it
skip:memmove(&outputbuff[4],&a,4);
//tell the driver to go ahead
a=0;
memmove(&outputbuff[0],&a,4);
}
}
你可以看到,我们让内核模式的代码等待用户的回答.这是一个明智的做法吗?为了回答这个问题,你必须要问问自己是否你正在阻止系统临界资源--所有的都要看情况(而定).我们这里的情况是所有的操作都发生在IRQL PASSIVE_LEVEL级别,没有涉及到处理IRPs,等待用户回应的线程也不是非常重要.所以,我们这种情况一切运行正常.然而,这个例子只是为了演示目的而写的.为了让它有实际的用处,重写我们的应用程序作为一个自动启动的服务是非常有意义的.这种情况下,我觉得我们应该对LocalSystem账户做一个解除,如果NtCreateSection()是在LocalSystem账户(这个账户可以在MSDN上查到说明:http://msdn.microsoft.com/en-us/libr...90(VS.85).aspx)的一个线程上下文中被调用的,在这种情况下,不需要做任何检查(直接)转到真正的服务执行处--毕竟,LocalSystem账户仅仅加载注册表指定的可执行文件.所以,这个的解除操作不会威胁到我们的安全.
结论
在结论中我必须要说挂钩native API是现存的最强的编程技术之一.这篇文章仅仅给了你一个例子来演示通过挂钩native API可以达到什么目的--你可以看到,我们通过挂钩了一个(!!!)native API函数就做到了阻止未授权程序的运行.你可以进一步的扩展这个方法来获取硬件设备的全部控制权,文件IO操作,网络流量等等.然而,我们目前的解决方案不适合内核API来调用(被挂钩的函数)--一旦内核模式的代码被允许直接调用ntoskrnl.exe的导出(函数),这些调用不需要经过SSDT.因此,在我的下一篇文章里面我们要挂钩ntoskrnl.exe本身.
这个例子已经在windows XP SP2的一些机器上测试成功.尽管我还没有在其他环境下做测试,我相信它在哪里都会运行正常的(这句话有点吹牛了,我直接下载的代码,直接运行就有问题)--毕竟,它没有用到任何可能和系统相关的结构.要运行这个例子,你需要做的就是把protector.exe和protector.sys放到同一个目录下面,然后运行protector.exe.在protector.exe的应用程序窗口关闭前,你每次试图运行任何的可执行文件时都会被提示(已经被允许运行的从第二次开始就不会再被提示,这点老外没表达清楚).
如果你给我发邮件告诉我你的评论和建议,我将十分感谢.
---------------------------------------------END--------------------------------------
译者的话:请在虚拟机下运行本程序!!!XP SP2下没有进行测试,XP SP3下有问题,需要在驱动protector.c的76行的while循环内首先对li.QuadPart进行重新赋值(加上li.QuadPart=-10000;这句代码即可),否则会死机.为了保证文章和代码的完整性,这点需要读者自行修改.