|
三、 控制进程创建
我们的解决方案由一个内核模式驱动程序和一个用户模式应用程序组成。为了开始监视进程创建,我们的应用程序要把服务索引(相应于NtCreateSection())以及交换缓冲区的地址传递到我们的驱动程序。这是由下列代码所完成的:
//打开设备 device=CreateFile("\\.\PROTECTOR",GENERIC_READ|GENERIC_WRITE, 0,0,OPEN_EXISTING, FILE_ATTRIBUTE_SYSTEM,0); //得到NtCreateSection的索引并把它连同输出缓冲区的地址传递给设备 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字节长的指令,以MOV EAX操作码作第一字节,服务索引作为留下的4字节。因此,为了得到相应于一些特别的本机API函数的服务索引,所有你要做的是从该地址读取4个字节,-位于从这个代理开始1字节距离的地方。
现在让我们看一下我们的驱动程序做什么,当它收到来自我们的应用程序的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; //钩住服务调度表 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()来把交换缓冲区映射到内核中,另外把我们的代理函数的地址写到服务表(当然,我们这是在把实际的服务执行的地址保存到全局变量RealCallee以后这样做的)。为了改写服务表的适当入口,我们通过MmMapIoSpace()来映射目标地址。为什么我们要这样做?不管怎么说,我们已经可以存取服务表了,不是吗?问题是,服务表可能驻留在一段只读内存中。因此,我们必须检查一下是否我们有对目标空间写的权限,而如果我们没有这个权限,那么在改写服务表之前,我们必须改变页面保护。你不认为这样以来工作太多了吗?因此,我们仅用MmMapIoSpace()来映射我们的目标地址,这样以来,我们就不必担心任何的页面保护问题了-从现在开始,我们假定已有到目标页面写的权限了。现在让我们看一下我们的代理函数:
//这个函数用来确定是否我们应该允许NtCreateSection()调用成功 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; //经由文件句柄得到文件名 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); //如果它是不可执行的,这也不要紧 //返回1 if(_stricmp(&buff[a],"exe")){RtlFreeAnsiString(&str);return 1;} //现在,我们要询问用户的选择。 //把文件名写入缓冲区,并等待直到用户显示响应 //(第一个DWORD为1意味着我们可以继续) //同步存取该缓冲区 KeWaitForSingleObject(&event,Executive,KernelMode,0,0); //把缓冲区的前两个DWORD置为0, //把字符串复制到该缓冲区中,并循环下去,直到用户把每一个 //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; } //仅保存执行上下文并调用check() _declspec(naked) Proxy() { _asm{ //保存执行上下文并调用check() //-后面的依赖于check()所返回的值 // 如果返回值是1,继续实际的调用。 //否则,返回STATUS_ACCESS_DENIED pushfd pushad mov ebx,esp add ebx,40 push ebx call check cmp eax,1 jne block //继续实际的调用 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()保存寄存器和标志,把一个指向服务参数的指针压入栈中并调用check()。其它的依赖于check()所返回的值。如果check()返回TRUE(也就是,我们想要继续请求),那么,Proxy()将恢复寄存器和标志,并且把控制权交给服务实现部分。否则,Proxy()将把STATUS_ACCESS_DENIED写入EAX,恢复ESP并返回-从调用者的观点来看,这就象对NtCreateSection()的调用失败一样-以错误状态STATUS_ACCESS_DENIED返回。
|
|
|