从内核模式启动进程
如何在驱动程序内部启动 Win32 进程
引言
在多次不成功的尝试寻找从内核模式启动一个可工作的Win32进程的方法后,我终于偶然发现了一段有创意且新颖的代码(注意:这个想法属于Valerino)。
不幸的是,那段代码在我机器上似乎无法正常工作,最终总是导致系统崩溃,或者(在最幸运的情况下)只是一个进程崩溃。因此,我决定以自己的方式重新实现Mighty Valerino的这个想法(尽管代码结构基本相同)。
那么,我们开始吧。首先,您必须了解...
APC
APC,即异步过程调用,代表一个内核过程,它被排队到特定线程以便执行。换句话说,它是在线程上下文中强制执行的代码(这种方法主要由I/O管理器使用)。这是我能给出的最简单的解释,也是您目前需要知道的全部。APC有三种类型:
- 内核APC - 它们可以排队到任何内核线程,并且只有在指定线程没有执行内核APC时才会执行。
- 特殊内核APC - 基本与上述相同。它们在
APC_LEVEL
的IRQL下运行,并且只能通过在更高的IRQL下运行来阻止。它们总是可以抢占普通内核APC。 - 用户APC - 这些是可以排队到
UserMode
线程的APC,但有一个前提:该线程必须先前调用了等待服务,例如将Alertable
字段设置为TRUE
的WaitForSingleObject
。下次线程从内核模式返回时,将调用APC。这是我们接下来要处理的APC类型。
说得够多了。让我们进入有趣的部分。
运行该进程
启动Win32进程的思想简要描述如下:
- 我们遍历正在运行的进程列表,直到找到Explorer.exe。为什么是explorer.exe?因为它是一个与桌面交互的服务(我尝试从WinLogon.exe弹出消息框,但我只能听到它的声音)。它还有许多等待的线程(包括可通知和不可通知的),所以它与这段代码配合得最好。
- 一旦找到Explorer.exe,我们就遍历它的线程,寻找一个可通知的线程。如果没有找到这样的线程,我们就简单地保存一个指向非可通知线程的指针,并将其
ApcState.UserApcPending
设置为TRUE
,从而使其可通知(注意:在这种情况下,线程通常需要几秒钟才能返回内核模式)。 - 现在我们有了Explorer.exe的
PEPROCESS
以及它的一个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排队到任何进程,包括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);
}
InstallUserModeApc
具有以下原型...
NTSTATUS
InstallUserModeApc(
IN LPSTR lpProcess,
IN PKTHREAD pTargetThread,
IN PEPROCESS pTargetProcess);
...其中pTargetProcess
指向Explorer.exe的PEPROCESS
,而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 0xabcd
是WinExec
的第一个参数,它指向要执行的应用程序的路径。但这意味着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并将其排队到线程。我将不解释KeInitializeApc
和KeInsertQueueApc
的工作原理,因为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:\目录中,因为这是驱动程序尝试运行的。