文章原址:
http://www.codeproject.com/useritems/KernelExec.asp
Download source files - 55 Kb
Download demo project - 27 Kb
介绍
在许多不成功的尝试来试图找到一种方式来以核心模式(KernelMode)来启动可运行的Win32进程后,我最终意外的发现了一段有希望的代码,这些代码既有独创性又有革新性(注意:这种想法来源于Valerino)
不幸的是,那些代码看上去并非能在我的机器上正确的运行,并且它总是以崩溃而结束,或者仅仅(在众多幸运的例子中)是一个进程。因此,我决定以自己的方式(尽管代码的结构是相同的)重新实现伟大的Valerino's的想法.
那么咱们就开始吧。首先,你必须知道...
APC
APC, 即异步程序调用, 表示核心程序列入到一个独特的线程队列等待执行.其他意思方面,它就是一段代码被强制在一个线程上下文(thread's context)中执行(这种方法通常被一些I/O管理者员所采用)。这就是我所能够提供给你的最简单的解释了,并且这是你现在需要了解的全部了。
APC有三种方式:
Kernel APC's -- 他们能被列入任何核心线程(kernel thread)队中并且它们将会被执行倘若既定的线程已经没有执行一个核心的APC.
Special Kernel APC's -- 基本上与上面相同。他们运行在中断请求(IRQL)APC_LEVEL级别并且不能被锁定除非运行在一个提升的中断请求级别(IRQL)。他们总是能抢先于正常的核心APC而运行.
User APC's -- 这些APC's是这样的:它们能被列入用户模式(UserMode)线程队列,但是这有个条件:这个线程以前一定调用了一个等待的服务就像WaitForSingleObject一样其
Alertable 域被设置成TRUE。这个APC将会在下次这个线程从核心模式(KernelMode)返回后被调用。这就是我们将会从现在处理的这种APC。
说得够多了吧。咱们开始这个有趣的部分吧:)
运行进程
在开始一个Win32进程之前这种想法的简短描述如下:
1.我们循环查找运行的进程链表,直到找到Explorer.exe。为什么要Explorer.exe呢?因为它是桌面交互的服务(我试图从WinLogon.exe弹出一个对话框并且我只能听到它弹出时的声音)它也有很多等待线程(都是
alertable 或non-
alertable ),因此它能以这种代码运行得很好。
2.一旦我们找到Explorer.exe我们通过它的线程循环查找来找一个
alertable 的线程.如果这样的线程不能被找到,我们仅需保存指向一个非
alertable 线程的指针,并且把它的ApcState.UserApcPending设置成TRUE,因此这就使这个non-
alertable 线程变成
alertable (注意:在这种情况下它通常要花费几秒钟才能将这个线程返回到核心模式(KernelMode))。
3.现在我们已经有了Explorer.exe的PEPROCESS和他的一个PETHREAD。下面我们把我们的APC对象列入队列(这将包含在用户模式(UserMode)中被执行的代码)并且当列队完毕,我们只需释放掉以前为其分配的内存,这就是整个的过程。
实现
主要的程序就是RunProcess(LPSTR lpProcess),其lpProcess必须是将要被运行的应用程序的全路径(我们的例子中是'c:/RawWrite.exe')。
void RunProcess(LPSTR lpProcess)
{
PEPROCESS pTargetProcess = NULL;//self explanatory
PKTHREAD pTargetThread = NULL; //thread that can be either
//
alertable or not
PKTHREAD pNotAlertableThread = NULL;//non-
alertable thread
PEPROCESS pSystemProcess = NULL; //May not necessarily be the
//'System' process
PETHREAD pTempThread = NULL;
PLIST_ENTRY pNextEntry, pListHead, pThNextEntry;
//...
}
我们以回收指向System进程的指针来开始:
pSystemProcess = PsGetCurrentProcess();
//make sure you are running at IRQL PASSIVE_LEVEL
pSystemProcess ->ActiveProcessLinks是一个包含链接(指针)到运行在机器上的其他进程(PEPROCESS)的LIST_ENTRY域。咱们 搜索一下Explorer.exe并且保存指向它及指向它的一个线程的指针。(注意:你能把APC列入任何进程的队列,即使是 CSRSS或SVCHOST,但是系统可能会崩溃)。一旦我们得到了一个指向Explorer.exe并且指向他的一个线程(这里我将不解释如何那样做) 的指针那么是把我们的APC列入那样的线程的队列的时候了:
if(!pTargetThread)
{
//No
alertable thread was found, so let's hope
//we've at least got a non-
alertable one
pTargetThread = pNotAlertableThread;
}
if(pTargetThread)
{
DbgPrint("KernelExec -> Targeted thread: 0x%p",
pTargetThread);
//We have a thread, now install the APC
InstallUserModeApc(lpProcess,
pTargetThread,
pTargetProcess);
}
这 里 pTargetProcess指向Explorer.exe的PEPROCESS并且pTargetThread指向APC将列入队列的 PKTHREAD。咱们现在为APC分配一些内存及把MDL(Memory Descriptor List)映射到我们的用户模式代码上:
PRKAPC pApc = NULL;
PMDL pMdl = NULL;
ULONG dwSize = 0; //Size of code to be executed in Explorer's address space
pApc = ExAllocatePool (NonPagedPool,sizeof (KAPC));
dwSize = (unsigned char*)ApcCreateProcessEnd-
(unsigned char*)ApcCreateProcess;
pMdl = IoAllocateMdl (ApcCreateProcess, dwSize, FALSE,FALSE,NULL);
//Probe the pages for Write access and make them memory resident
MmProbeAndLockPages (pMdl,KernelMode,IoWriteAccess);
安装用户模式的APC是下面的函数原型:
NTSTATUS
InstallUserModeApc(
IN LPSTR lpProcess,
IN PKTHREAD pTargetThread,
IN PEPROCESS pTargetProcess);
我 们的APC现在是有效的及pMdl是驻留内存的并且映射到了我们的用户模式代码上(ApcCreateProcess() 正是如此。我们后面将会看到)。那么现在做什么呢?我们应该把我们的APC传递给这个线程然后观察我们的Win32进程运行吗?不不不...用不着这么 快!:)
如果Explorer.exe的线程并不能存取核心内存那么它应该怎样调用我们的APC例程呢?它不能够那样做!那么好得很,咱们把我们的APC代码映射到用户模式的内存吧:
KAPC_STATE ApcState;
//Attach to the Explorer's address space
KeStackAttachProcess(&(pTargetProcess->Pcb),&ApcState);
//Now map the physical pages (our code) described by pMdl
pMappedAddress =
MmMapLockedPagesSpecifyCache(pMdl,
UserMode,
MmCached,
NULL,FALSE,
NormalPagePriority);
继续,首先我必须给你展示ApcCreateProcess(这个代码被映射到用户模式的内存,进入Explorer的地址空间)怎样运行的:
__declspec(naked)
void ApcCreateProcess(
PVOID NormalContext,
PVOID SystemArgument1,
PVOID SystemArgument2)
{
__asm
{
mov eax,0x7C86114D
push 1
nop
push 0xabcd
call eax
jmp end
nop
nop
//...about 400 nop's here
end:
nop
ret 0x0c
}
}
void ApcCreateProcessEnd(){}
//Used only to calculate the size of the code above
我 们把WinExec的地址move入eax(在WinXP SP2上0x7C86114D是其地址),我们把1push入栈(SW_SHOWNORMAL)并且然后在调用WinExec之前我们把0xabcd push入栈.你可能问为什么是0xabcd?哦,push 0xabcd是WinExec的第一个参数,它指向被执行的应用程序的路径。但是这意味着0xabcd并不可能一直指向此路径。
那么你为什么不仅 仅把RunProcess(LPSTR lpProcess)的lpProcess push入栈呢?答案是 -- 因为WinExec将并不能访问lpProcess,并且这将会给你抛出一个Access Violation的异常!你并不能从用户模式访问核心内存(Kernel memory),记住了吗?相反,我们应该立刻把我们的代码映射成用户模式的内存,我们把这个路径拷贝给此位置的随后就是第一个nop指令(这就是为什么 这里有如此之多的nop指令),然后我们修改0xabcd来指向此位置。代码如下:
ULONG *data_addr=0; //just a helper to change the address of the 'push' instruction
//in the ApcCreateProcess routine
ULONG dwMappedAddress = 0; //same as above
pMappedAddress =
MmMapLockedPagesSpecifyCache(pMdl,
UserMode,
MmCached,
NULL,FALSE,
NormalPagePriority);
dwMappedAddress = (ULONG)pMappedAddress;
//zero everything out except our assembler code
memset ((unsigned char*)pMappedAddress + 0x14, 0, 300);
//copy the path to the executable
memcpy ((unsigned char*)pMappedAddress + 0x14,
lpProcess,
strlen (lpProcess));
data_addr = (ULONG*)((char*)pMappedAddress+0x9);//address pushed on the stack
//(originally 0xabcd)...
*data_addr = dwMappedAddress+0x14; //gets changed to point to our exe's path
//all done, detach now
KeUnstackDetachProcess (&ApcState);
现在留下的就是初始化APC然后把其列入线程的队列。我不解释KeInitializeApc 和 KeInsertQueueApc是怎样运行的,这里这正如像Tim Deveaux已经完成的那样:(rxxi注:这句不知翻译的正确与否。)
//Initialize the APC...
KeInitializeApc(pApc,
pTargetThread,
OriginalApcEnvironment,
&ApcKernelRoutine, //this will fire after
//the APC has returned
NULL,
pMappedAddress,
UserMode,
NULL);
//...and queue it
KeInsertQueueApc(pApc,0,NULL,0);
//is this a non-
alertable thread?
if(!pTargetThread->ApcState.UserApcPending)
{
//if yes then alert it
pTargetThread->ApcState.UserApcPending = TRUE;
}
return STATUS_SUCCESS;
}
编译代码
这很简单 -- 执行cd sys_path 命令,sys_path是驱动程序项目的路径,然后运行build -ceZ。或仅仅在MS Visual Studio 6中按F7。:)
现在拷贝KernelExec.sys到你的C:/目录下,运行Dbgview看看驱动程序的输出结果,然后双击Start_KE_Driver.exe进行安装和开始执行驱动,对了!RawWrite.exe的窗口就现在展现在你的屏幕上了!:-)
后记:确认你首先把被应用程序调用的RawWrite.exe放入C:/目录,因为那是驱动程序尝试去运行的目录。