📄
字号:
漫谈兼容内核之十四:Windows的跨进程操作
毛德操
Jeffrey Richter在他的“Advanced Windows”一书第18章“打破进程壁垒(Breaking Through Process Boundary Walls)”中讲述了一个有趣的实验,就是利用OpenProcess()、CreateRemoteThread()、VirtualAllocEx()、WriteProcessMemory()等等Win32 API函数从一个进程向另一个进程的用户空间“注入(Inject)”一个DLL。其过程大致如下:
● 给定目标进程的进程号PID,通过OpenProcess()“打开”这个进程,得到代表着目标进程“对象”的Handle。OpenProcess()的基础是系统调用NtOpenProcess()。
● 通过VirtualAllocEx()在目标进程的用户空间分配一块内存。VirtualAllocEx()的基础是系统调用NtAllocateVirtualMemory()。
● 通过WriteProcessMemory()把一些代码和数据拷贝到目标进程用户空间中刚分配的那块内存中,这些代码的入口为ThreadFunc()。WriteProcessMemory()的基础是系统调用NtWriteVirtualMemory()。
● 通过CreateRemoteThread()在目标进程中创建一个以ThreadFunc()为入口的线程。CreateRemoteThread()的基础是系统调用NtCreateThread()。
● 当目标进程中的线程ThreadFunc()受调度运行时,就把预定的DLL装入目标进程的用户空间。
● 然后线程ThreadFunc()退出运行而不复存在,但是所装入(并连接)的DLL却留在了目标进程的用户空间。
当然,这是个很有趣的实验,利用这个实验所揭示的特点也许可以开发出某些很好的应用。但是问题也随之而生:要是ThreadFunc()是一段木马程序呢?比方说,要是这里的目标进程是网络浏览器,而ThreadFunc()每当受调度运行时就把本地的某些信息发送给某个网站,然后睡眠一段时间,如此周而复始呢?显然,只要那个被ThreadFunc()“附体”的网络浏览器进程还在运行,这段木马程序就可周期性地得到执行,而很难被察觉。
笔者在以前的漫谈中曾经讲过,Windows与Linux的一个很明显、很重要的区别就是:在Windows中一个进程可以越俎代庖地替别的进程做好多事,其中就包括上面讲到的几项跨进程操作。我们在创建Windows进程、启动PE映像执行的过程中也看到过一些跨进程的操作,例如把可执行映像映射到子进程的用户空间、在子进程的映像中寻找函数入口、为子进程创建线程等等。除直接的跨进程操作外,还可以跨进程复制已打开对象的Handle。而Linux,则是不允许、或者说不提供此类跨进程操作的。而且,正是这方面的差异使得Wine的“核内差异核外补”策略难以有效实施。
相比之下,Linux进程是“独立自主”的。当然,Linux也有进程间通信,但那只是通信而已。在进程间通信的基础上,一个进程也可以应另一个进程的请求而在其自身的上下文中执行某些操作。但是那些操作都是预定的、预先就安排在这个进程的代码中的,所反映的是程序设计者的意志。从这个意义上说,除非程序中有错误(bug),Linux进程的行为是可预测的。而Windows进程则有可能发生不可预测的行为,因为别的进程居然可以把一段程序“注入”其空间并使之成为一个线程而得到执行。
可想而知,要是允许这样的跨进程操作不受限制地进行,对于系统的安全性是影响极大的,所以必定要有安全措施配套才行。
对于这么重要的问题,我们当然希望能了解Windows的跨进程操作和相应的安全措施是怎么实现的。但是,遗憾的是:一方面是微软不向公众公开Windows内核的代码,另一方面是ReactOS尚未实现有关的安全措施。这样,我们现在能做的就只能是先通过ReactOS的代码了解跨进程操作的实现,而看不到有关安全措施的实现。所以下面我们只能在代码中看到“矛”的一面,而看不到“盾”的一面。
下面的代码仍取自ReactOS的0.2.6版本,不过这个版本的ReactOS尚未实现配套的安全措施,所以只能借此了解一下有关跨进程操作的实现。
先看NtOpenProcess()的实现,因为所有的跨进程操作都是从这里开始的。
[code]NTSTATUS STDCALL
NtOpenProcess(OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId)
{
. . . . . .
if (ObjectAttributes != NULL && ObjectAttributes->ObjectName != NULL &&
ObjectAttributes->ObjectName->Buffer != NULL)
{
NTSTATUS Status;
PEPROCESS Process;
Status = ObReferenceObjectByName(ObjectAttributes->ObjectName,
ObjectAttributes->Attributes, NULL, DesiredAccess,
PsProcessType, UserMode, NULL, (PVOID*)&Process);
. . . . . .
Status = ObCreateHandle(PsGetCurrentProcess(),Process, DesiredAccess,
FALSE, ProcessHandle);
ObDereferenceObject(Process);
return(Status);
}
else
{
PLIST_ENTRY current_entry;
PEPROCESS current;
NTSTATUS Status;
ExAcquireFastMutex(&PspActiveProcessMutex);
current_entry = PsActiveProcessHead.Flink;
while (current_entry != &PsActiveProcessHead)
{
current = CONTAINING_RECORD(current_entry, EPROCESS,
ProcessListEntry);
if (current->UniqueProcessId == ClientId->UniqueProcess)
{
if (current->Pcb.State == PROCESS_STATE_TERMINATED)
{
Status = STATUS_PROCESS_IS_TERMINATING;
}
else
{
Status = ObReferenceObjectByPointer(current,
DesiredAccess,
PsProcessType,
UserMode);
}
ExReleaseFastMutex(&PspActiveProcessMutex);
if (NT_SUCCESS(Status))
{
Status = ObCreateHandle(PsGetCurrentProcess(),
current,
DesiredAccess,
FALSE,
ProcessHandle);
ObDereferenceObject(current);
. . . . . .
}
return(Status);
}
current_entry = current_entry->Flink;
}
ExReleaseFastMutex(&PspActiveProcessMutex);
DPRINT("NtOpenProcess() = STATUS_UNSUCCESSFUL\n");
return(STATUS_UNSUCCESSFUL);
}
return(STATUS_UNSUCCESSFUL);
}[/code]
像别的对象一样,进程对象也可以有个对象名(注意进程的对象名与所执行的映像文件名是两码事)。同时,进程又有进程号。要打开一个进程对象时,既可以按对象名打开,也可以按进程号打开。如果是对象名打开,就把对象名填写在作为参数的OBJECT_ATTRIBUTES数据结构中,就是这里的参数ObjectAttributes。如果是按进程号打开,则把进程号填写在也是作为参数的“客户标识”CLIENT_ID数据结构中,就是这里的参数ClientId。严格地说CLIENT_ID数据结构是进程Handle和线程Handle的组合,用来唯一地标识一个线程。之所以叫“客户”,可能是对服务进程csrss而言。但是Handle在本质上是数组下标,所以进程Handle其实也就是进程号。至于线程Handle,则此刻不在关心之列,所以设置成0就可以了。注意CLIENT_ID中的进程Handle不同于打开一个进程以后所得到的Handle,前者是全局的,内核中单独有个Cid对象表PspCidTable;而后者是局部的,作用于当前进程的打开对象表中。
看一下ReactOS的Win32 API函数OpenProcess()的代码,就可以明白应该怎样使用CLIENT_ID于NtOpenProcess():
[code]HANDLE STDCALL
OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId)
{
. . . . . .
ClientId.UniqueProcess = (HANDLE)dwProcessId;
ClientId.UniqueThread = 0;
. . . . . .
errCode = NtOpenProcess(&ProcessHandle,…, &ClientId);
. . . . . .
return ProcessHandle;
}
可见,这里只是把进程号dwProcessId的类型转换成了HANDLE,而数值并未改变。
NtOpenProcess()的另一个参数DesiredAccess则是以标志位的形式说明打开这进程对象的目的,例如:
#define PROCESS_TERMINATE 1
#define PROCESS_CREATE_THREAD 2
#define PROCESS_SET_SESSIONID 4
#define PROCESS_VM_OPERATION 8
#define PROCESS_VM_READ 16
#define PROCESS_VM_WRITE 32
#define PROCESS_DUP_HANDLE 64
#define PROCESS_CREATE_PROCESS 128
#define PROCESS_SET_QUOTA 256
#define PROCESS_SET_INFORMATION 512
#define PROCESS_QUERY_INFORMATION 1024[/code]
这些标志位的作用与打开文件时所使用的可读、可写等等相似,一方面是为以后对这个已打开映像的访问设置一个范围,一方面是让内核可以针对所要求的操作实施权限检查。
如果打开成功,NtOpenProcess()就通过参数ProcessHandle返回已打开对象的Handle。
所以,如果是按对象名打开,代码中就通过ObReferenceObjectByName()在内核中比对寻找同名的对象,找到后就返回目标进程的进程控制块指针,然后在当前进程的打开对象表中加入一个表项,并返回其Handle。按理说在这个过程中应该检查当前进程的权限,但在ReactOS的0.2.6版中尚未实现。
而如果是按进程号打开,那就要扫描(while循环)当前的进程队列,找出进程号与ClientId->UniqueProcess相符的进程控制块,然后通过ObReferenceObjectByPointer()找到相应的对象,再往下就都一样了。
打开进程是这样,打开线程也是差不多。
打开了目标进程以后,就可以对其实施跨进程操作了。允许跨进程操作的Windows系统调用有很多,这些系统调用一般都以ProcessHandle为参数。跨线程的操作也可以间接地认为是跨进程操作,因为目标线程可以是别的进程中的线程。这样,直接或间接意义上的跨进程操作就数量不小了:
[code] NtAllocateVirtualMemory()
NtFreeVirtualMemory()
NtQueryVirtualMemory()
NtLockVirtualMemory()
NtUnlockVirtualMemory()
NtReadVirtualMemory()
NtWriteVirtualMemory()
NtProtectVirtualMemory()
NtFlushVirtualMemory()
NtAllocateUserPhysicalPages()
NtFreeUserPhysicalPages()
NtMapUserPhysicalPages()
NtMapUserPhysicalPagesScatter()
NtGetWriteWatch()
NtResetWriteWatch()
NtMapViewOfSection()
NtUnmapViewOfSection()
NtCreateThread()
NtOpenThread()
NtTerminateThread()
NtQueryInformationThread()
NtSetInformationThread()
NtResumeThread()
NtGetContextThread()
NtSetContextThread()
NtQueueAPCThread()
NtAlertThread()
NtAlertResumeThread()
NtRegisterThreadTerminatePort()
NtImpersonateThread()
NtImpersonateAnonymousThread()
NtTerminateProcess()
NtQueryInformationProcess()
NtSetInformationProcess()
NtAssignProcessToJobObject()
NtOpenProcessToken()
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -