65.9K
CodeProject 正在变化。 阅读更多。
Home

从内核模式启动进程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (17投票s)

2006年3月25日

CPOL

5分钟阅读

viewsIcon

170536

downloadIcon

5322

如何在驱动程序内部启动 Win32 进程

Sample Image - KernelExec.jpg

引言

在多次不成功的尝试寻找从内核模式启动一个可工作的Win32进程的方法后,我终于偶然发现了一段有创意且新颖的代码(注意:这个想法属于Valerino)。

不幸的是,那段代码在我机器上似乎无法正常工作,最终总是导致系统崩溃,或者(在最幸运的情况下)只是一个进程崩溃。因此,我决定以自己的方式重新实现Mighty Valerino的这个想法(尽管代码结构基本相同)。

那么,我们开始吧。首先,您必须了解...

APC

APC,即异步过程调用,代表一个内核过程,它被排队到特定线程以便执行。换句话说,它是在线程上下文中强制执行的代码(这种方法主要由I/O管理器使用)。这是我能给出的最简单的解释,也是您目前需要知道的全部。APC有三种类型:

  1. 内核APC - 它们可以排队到任何内核线程,并且只有在指定线程没有执行内核APC时才会执行。
  2. 特殊内核APC - 基本与上述相同。它们在APC_LEVEL的IRQL下运行,并且只能通过在更高的IRQL下运行来阻止。它们总是可以抢占普通内核APC。
  3. 用户APC - 这些是可以排队到UserMode线程的APC,但有一个前提:该线程必须先前调用了等待服务,例如将Alertable字段设置为TRUEWaitForSingleObject。下次线程从内核模式返回时,将调用APC。这是我们接下来要处理的APC类型。

说得够多了。让我们进入有趣的部分。

运行该进程

启动Win32进程的思想简要描述如下:

  1. 我们遍历正在运行的进程列表,直到找到Explorer.exe。为什么是explorer.exe?因为它是一个与桌面交互的服务(我尝试从WinLogon.exe弹出消息框,但我只能听到它的声音)。它还有许多等待的线程(包括可通知和不可通知的),所以它与这段代码配合得最好。
  2. 一旦找到Explorer.exe,我们就遍历它的线程,寻找一个可通知的线程。如果没有找到这样的线程,我们就简单地保存一个指向非可通知线程的指针,并将其ApcState.UserApcPending设置为TRUE,从而使其可通知(注意:在这种情况下,线程通常需要几秒钟才能返回内核模式)。
  3. 现在我们有了Explorer.exePEPROCESS以及它的一个PETHREAD。接下来,我们排队我们的APC对象(其中包含将在用户模式下执行的代码),当它完成后,我们就释放之前为其分配的内存。就是这样。

实现

主程序是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是一个LIST_ENTRY字段,包含指向计算机上运行的其他进程(PEPROCESS)的链接(指针)。让我们搜索Explorer.exe,并保存指向它及其一个线程的指针。(注意:您可以将APC排队到任何进程,包括CSRSSSVCHOST,但系统很可能会崩溃。)一旦我们获得了指向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);
}

InstallUserModeApc具有以下原型...

NTSTATUS
InstallUserModeApc(
                   IN LPSTR lpProcess,
                   IN PKTHREAD pTargetThread,
                   IN PEPROCESS pTargetProcess);

...其中pTargetProcess指向Explorer.exePEPROCESS,而pTargetThread是APC将被排队的PKTHREAD。现在,让我们为该APC以及用于映射我们用户模式代码的MDL(内存描述符列表)分配一些内存。

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现在是有效的,并且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的地址移入eax(在WinXP SP2上,其地址是0x7C86114D),我们将1推入栈(SW_SHOWNORMAL),然后推入… 0xabcd,然后调用WinExec。您可能会问,为什么是0xabcd?嗯,push 0xabcdWinExec的第一个参数,它指向要执行的应用程序的路径。但这意味着0xabcd不可能一直指向路径!

为什么不直接从RunProcess(LPSTR lpProcess)push lpProcess呢?答案是 - 因为WinExec将无法访问它,并且会抛出“访问冲突”错误!您不能从用户模式访问内核内存,还记得吗?相反,在我们将代码映射到用户模式内存后,我们将路径复制到第一个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并将其排队到线程。我将不解释KeInitializeApcKeInsertQueueApc的工作原理,因为Tim Deveaux已经在这里进行了说明。

  //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**。或者在Microsoft Visual Studio 6中按**F7**键。

现在将KernelExec.sys复制到您的C:\目录,运行Dbgview查看驱动程序的输出。然后双击Start_KE_Driver.exe来安装并启动驱动程序。搞定!RawWrite.exe的窗口应该现在就在您面前了!

附注:请确保您首先将一个名为RawWrite.exe的应用程序放在您的c:\目录中,因为这是驱动程序尝试运行的。

© . All rights reserved.