📄
字号:
Status = ObReferenceObjectByHandle(ProcessHandle,
PROCESS_VM_WRITE,
NULL,
UserMode,
(PVOID*)(&Process),
NULL);
. . . . . .
/* We have to make sure the target memory is writable.
*
* I am not sure if it is correct to do this in any case, but it has to be
* done at least in some cases because you can use WriteProcessMemory to
* write into the .text section of a module where memcpy() would crash.
* -blight (2005/01/09)
*/
ProtectBaseAddress = BaseAddress;
ProtectNumberOfBytes = NumberOfBytesToWrite;
/* Write memory */
if (Process == PsGetCurrentProcess())
{
/* 目标进程就是本进程 */
. . . . . .
memcpy(BaseAddress, Buffer, NumberOfBytesToWrite);
}
else
{
/* Create MDL describing the source buffer. */
Mdl = MmCreateMdl(NULL, Buffer, NumberOfBytesToWrite);
. . . . . .
/* Make the target area writable. */
Status = MiProtectVirtualMemory(Process,
&ProtectBaseAddress,
&ProtectNumberOfBytes,
PAGE_READWRITE,
&OldProtection);
. . . . . .
/* Map the MDL. */
MmProbeAndLockPages(Mdl,
UserMode,
IoReadAccess);
/* Copy memory from the mapped MDL into the target buffer. */
KeAttachProcess(&Process->Pcb);
SystemAddress = MmGetSystemAddressForMdl(Mdl);
memcpy(BaseAddress, SystemAddress, NumberOfBytesToWrite);
KeDetachProcess();
/* Free the MDL. */
if (Mdl->MappedSystemVa != NULL)
{
MmUnmapLockedPages(Mdl->MappedSystemVa, Mdl);
}
MmUnlockPages(Mdl);
ExFreePool(Mdl);
}
/* Reset the protection of the target memory. */
Status = MiProtectVirtualMemory(Process,
&ProtectBaseAddress,
&ProtectNumberOfBytes,
OldProtection,
&OldProtection);
. . . . . .
ObDereferenceObject(Process);
if (NumberOfBytesWritten != NULL)
MmCopyToCaller(NumberOfBytesWritten, &NumberOfBytesToWrite,
sizeof(ULONG));
return(STATUS_SUCCESS);
}[/code]
首先当然要找到目标进程的进程控制块。如果目标进程即为当前进程,那就是同一用户空间的拷贝,这当然很简单,调用一下memcpy()就可以了。我们在这里只关心跨进程的拷贝。由于是跨进程的拷贝,这里有个如何处理源端数据的问题。显然,源端的数据是在当前进程的用户空间,而目标端是在另一个进程的用户空间,这不是简单的通过memcpy()就可以完成的操作。怎么办呢?方法之一是分两步走,先在内核中分配一块足够大的缓冲区,从当前进程的用户空间把数据拷贝到这个缓冲区中,然后再从这个缓冲区拷贝到目标进程的用户空间。这样当然也是可以的,但是多了一次拷贝,降低了效率。另一个方法是先把源端数据所在的(物理)页面映射到内核里面、即系统空间。这样,同一个物理页面就有了两个映射,从而有了两个虚拟地址,一个在用户空间,另一个在系统空间。于是从其在系统空间的映像拷贝到目标进程的用户空间就行了,这样可以省去一次拷贝。Windows在与设备驱动有关(包括文件操作)的系统调用中都提供了这样的手段,称为MDL,这里就用上了。对于MDL将来我在谈到设备驱动框架时还会介绍,在这里读者只要知道有这么一回事就行了。
代码中先通过MmCreateMdl()和MmProbeAndLockPages()把源端的物理页面映射到内核里面,并加以验证。同时又通过MiProtectVirtualMemory()把目标端页面的保护模式设置成PAGE_READWRITE。这就为拷贝做好了准备。
接着就是所谓进程挂靠、即通过KeAttachProcess()切换到目标进程的用户空间了。一旦切换到目标进程的用户空间,memcpy()就有了用武之地。然后又通过KeDetachProcess()切换回原来的用户空间。
完成了拷贝以后,又通过MiProtectVirtualMemory()恢复目标区间原有的页面保护。
最后的MmCopyToCaller()只是把一个无符号长整数、即实际写入目标进程用户空间的长度复制到用户空间,作为系统调用的返回值。
现在,离开“阴谋”的实现只有一步之遥了,下一步就是在目标进程中为刚才拷贝过去的可执行代码创建一个线程,这是通过NtCreateThread()实现的。
[code]NTSTATUS STDCALL
NtCreateThread(OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle,
OUT PCLIENT_ID ClientId,
IN PCONTEXT ThreadContext,
IN PINITIAL_TEB InitialTeb,
IN BOOLEAN CreateSuspended)
{
. . . . . .
PEPROCESS Process;
PETHREAD Thread;
. . . . . .
Status = ObReferenceObjectByHandle(ProcessHandle, PROCESS_CREATE_THREAD,
PsProcessType, PreviousMode, (PVOID*)&Process, NULL);
. . . . . .
Status = PsInitializeThread(Process, &Thread, ObjectAttributes, PreviousMode, FALSE);
ObDereferenceObject(Process);
. . . . . .
/* create a client id handle */
Status = PsCreateCidHandle(Thread, PsThreadType, &Thread->Cid.UniqueThread);
. . . . . .
Status = KiArchInitThreadWithContext(&Thread->Tcb, ThreadContext);
. . . . . .
Status = PsCreateTeb(ProcessHandle, &TebBase, Thread, InitialTeb);
. . . . . .
Thread->Tcb.Teb = TebBase;
Thread->StartAddress = NULL;
. . . . . .
/*
* Queue an APC to the thread that will execute the ntdll startup
* routine.
*/
LdrInitApc = ExAllocatePool(NonPagedPool, sizeof(KAPC));
KeInitializeApc(LdrInitApc, &Thread->Tcb, OriginalApcEnvironment,
LdrInitApcKernelRoutine, LdrInitApcRundownRoutine,
LdrpGetSystemDllEntryPoint(), UserMode, NULL);
KeInsertQueueApc(LdrInitApc, NULL, NULL, IO_NO_INCREMENT);
/*
* The thread is non-alertable, so the APC we added did not set UserApcPending to TRUE.
* We must do this manually. Do NOT attempt to set the Thread to Alertable before the call,
* doing so is a blatant and erronous hack.
*/
Thread->Tcb.ApcState.UserApcPending = TRUE;
Thread->Tcb.Alerted[KernelMode] = TRUE;
oldIrql = KeAcquireDispatcherDatabaseLock ();
PsUnblockThread(Thread, NULL, 0);
KeReleaseDispatcherDatabaseLock(oldIrql);
Status = ObInsertObject((PVOID)Thread, NULL, DesiredAccess, 0, NULL, &hThread);
. . . . . .
. . . . . .
return Status;
}[/code]
参数ThreadContext指向一个PCONTEXT数据结构。这个数据结构因CPU而不同,对于X86是CONTEXT_X86,其内容是要求新建线程开始运行时各个寄存器的初值。另一个参数InitialTeb指向一个“初始TEB”,主要是给定了新建线程的堆栈位置。参数ClientId用来返回一个“客户标识”CLIENT_ID,实质上是返回客户标识中的线程号。CreateSuspended则表明是否要求新建线程一创建就被挂起。其余的参数就不言自明了。
首先当然还是找到目标进程的进程控制块。然后调用PsInitializeThread(),这个函数虽然名为InitializeThread,实际上却包括了创建线程、对线程的ETHREAD数据结构进行初始化、并将其挂入目标进程的线程队列等操作。注意对ETHREAD数据结构的初始化并不等同于对整个线程的初始化,因为ETHREAD并不代表着一个线程的全部,堆栈也是线程的一部分。
接着是PsCreateCidHandle()。如前所述,一个CID是由两个Handle构成的,其一是进程控制块的Handle,其二是线程控制块的Handle。这里要做的就是为目标线程的控制块(ETHREAD)创建一个全局的线程Handle,并把它填写在Thread->Cid中。
下面的KiArchInitThreadWithContext()是个宏操作,因CPU的不同而定义为不同的函数,对于X86处理器定义为Ke386InitThreadWithContext()。这个函数在目标线程的系统堆栈中伪造出一个中断现场,使得当目标进程被调度运行而返回用户空间时正好具有通过参数ThreadContext给定的上下文、即各寄存器的值。至于目标线程在用户空间的程序入口,则就是ThreadContext中寄存器Eip的值,这是必须在调用NtCreateThread()之前设置好的。注意这与APC函数是两码事。
PsCreateTeb()根据参数InitialTeb在用户空间创建一个TEB。TEB的位置在用户空间顶部,PEB的下面。由于一个进程可以有多个线程,在PEB下面实际上是一个TEB数组。每创建一个新的线程,就通过ZwAllocateVirtualMemory()为其分配一个TEB页面,然后就通过NtWriteVirtualMemory()填写其初始内容,内容主要来自InitialTeb和Thread->Cid。
如果参数CreateSuspended非0,表示新建线程应该一创建即被挂起,那么这里就是地方了,PsSuspendThread()将目标线程挂起。被挂起的线程将不会被调度运行。
再往下就是为新建线程准备并挂入APC函数了。这里通过LdrpGetSystemDllEntryPoint()获取的还是指向LdrInitializeThunk()的指针。我们知道,这个函数的主要功能是DLL的装入和动态连接,按理说只有目标进程中的第一个线程才需要执行这个函数。但是读者不妨回过去(漫谈十一)看一下__true_LdrInitializeThunk()的代码,DLL的装入和动态连接只是在第一次进入这个函数时才执行,以后就跳过去了。而LdrInitializeThunk()的“次要”功能,即对于LdrpAttachThread()的调用,却是每一个线程都要执行的。特别是这里面还有对TLS、即“线程本地存储(Thread Local Storage)”的初始化,所以每一个新建的线程都要到这个APC函数去转一下。
接着的PsUnblockThread()又是关键。新建的线程至此还是被“阻塞(blocked)”的,其ETHREAD数据结构尚未被挂入调度队列。而PsUnblockThread()的作用就是解除其阻塞并将其ETHREAD数据结构挂入调度队列。在这个操作的过程中当然不能允许发生调度,所以要用KeAcquireDispatcherDatabaseLock ()和KeReleaseDispatcherDatabaseLock把这个过程保护起来。完成了这个过程以后,在发生调度的时候,新建的线程就有机会被调度运行了。
最后通过ObInsertObject()创建一个Handle表项,并返回相应的Handle,这就是目标线程的Handle。
至于新建线程被调度运行时的流程,读者在以前就已经看到过的了。
显然,目前的ReactOS对这整个过程是“不设防”的,尚未实现理应与主流功能配套的安全措施,与Windows的代码应该还有很大的差距(有幸看到Windows源代码的人不妨重点考察一下有关的代码)。特别地,对于跨进程操作的安全性而言,需要有严密的“对象保护”机制。以后我们再来讨论这个问题。
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -