从核心模式启动一个进程----怎样从内部的驱动程序启动一个Win32进程

来源:互联网 发布:多用户微商城源码下载 编辑:程序博客网 时间:2024/05/02 01:02
从核心模式启动一个进程
----怎样从内部的驱动程序启动一个Win32进程
[Romania]Stan Alex著   [China]rxxi 译

 

文章原址:http://www.codeproject.com/useritems/KernelExec.asp
  • Download source files - 55 Kb
  • Download demo project - 27 Kb

Sample Image - KernelExec.jpg


介绍
在许多不成功的尝试来试图找到一种方式来以核心模式(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:/目录,因为那是驱动程序尝试去运行的目录。


译者注

1.翻译水平有限望多多指教。
2.2006/06/30