从内核模式启动进程
我想很多人都会有这样的问题,能否从内核驱动中启动用户应用程序呢?在Win9x(win95, 98, ME)中有专门的导出函数ShellVxd_ShellExecute,此函数能够启动应用程序。而在NT系统中没有专门的导出函数。
单数不存在导出函数决不意味这种想法不可能实现。恰恰相反,这种想法是完全可能实现的。当然,您可能会觉得这有如恶梦一般,分析一大堆系统对象,仔细的研究它们的属性与方法等等。不用紧张,事实上要简单得多。来,让我们放松一下,并用几种非标准的观点看一下这个问题。
我们先推开现实不管。仔细的查看模块ntoskrnl.exe的导出函数,我们没有找到能直接赋予新进程生命的函数。DDK对此也保持沉默。但是我们知道,进程是由用户模块kernel32.dll的导出函数kernel32.dll:CreateProcessA, CreateProcessW, CreateProcessInternalA, CreateProcessInternalW创建的,对于这些函数的描述以及参数可以很容易的在PSDK的文档中找到。当然,绕过kernel32.dll,尝试自己模拟这些函数是可以成功的。对这种方法的讲解和相应的代码可以在Garry Nebetta的WinNT Native Api指南中找到。实际上这也没有什么不可思议的。进程仍然从用户模式启动,只不过是直接调用了ntdll.dll模块里的导出函数。但是我们的问题依然没有答案,内核中的哪些东东能实现同样的效果呢?
其实,我所给出的方法也没有显出什么太古怪的地方来。我们不会自己构造内核对象并靠自己的力量和智慧来模拟出相应的内核行为和机制。我们知道,没有人能比操作系统做得更好,而且内核恐怕也不会对我们的“自己动手”表示赞赏。但是,我们还是要在内核模式驱动中把应用程序启动起来,只是……是在用户模式下。下面我们就来开始吧。
--------------------------------------------------------------------------------
方法很简单。现在我对机制进行一点解释,然后继续对内核代码进行分析。主要的思想就是,要“抓住”某个进程的用户线程,并将其与我们自己的条件“捆绑”,也就是说修改执行的代码并让它转向到预先准备好的用户地址空间中的代码中,这段代码调用函数kernel32.dll:CreateProcessA。
怎样才能做到呢?首先需要找到线程从内核模式转向用户模式的地方。进程是如何从用户模式切换到内核模式再切换回来的呢?这是通过中断门/快速系统调用接口进行的。对于本例,经过思考,我们没有找到比INT 0x2E/SYSENTER更好的地方,而正是在这里线程改变了自己的优先级。
当然,如果有其它类似的服务,比如调试用的int 0x2d,或是timer int 0x2a。但是我们需要稳定的方法,对大多数进程能够使用的,可以频繁使用的方法。这样,在一番寻找之后,最好的候选方法就是系统调用门接口INT 0x2E/SYSENTER。我们如何才能使用这个后门呢?无疑需要知道,在中断调用时会发生向段寄存器重新加载事先保存在TSS中的选择子,主要是CS和SS。在通过门到达内核后,在堆栈中保存着中断的返回地址。使用指令sysenter(winXP+)的情况类似。这个地址是我们所需要的,而取得它完全不是问题。之后,我们必需拦截函数_KiSystemService,向其中插入我们处理程序的代码。问题又来了,我们如何找到_KiSystemService函数呢?
有一点是显然的,中断向量INT 0x2E指向这个函数。但是,在WinXP+中情况并非只此一种。由于最新处理器的新特性,新版本的系统都使用了快速系统调用指令SYSENTER/SYSEXIT。SYSENTER指令与INT指令类似,但有一点区别,就是它调用的代码的地址位于MSR寄存器中的某一个里。为什么这种情况下我们需要多余的查找呢?另外,我们还知道,MSR的内容是经常变化的。尽管如此,IDT表中的中断向量int 0x2e不管怎样都还是指向_KiSystemService的起始代码。尽管int 0x2e没有被使用,但找到它显然不是问题。
害怕失望——您错了。拦截掉INT 0x2E会怎样呢?例如,ntice就好干这个。在代码调试时我们没有可能“亲眼”见到这个函数。除此之外,在ntoskrnl.exe的导出中,此函数也没有被提及。但是,还不是太坏。我们来想一想,如何能找到它。扫描全部内存查找含有_KiSystemService函数的一组指令没有什么意义。我们这里的解决方案十分简单。我们知道,_KiSystemService是转向内核函数的入口,所以在函数的内部有直接调用内核服务的代码。我们来看下面的代码。
0008:804DA113 5A POP EDX
0008:804DA114 FF0538F6DFFF INC DWORD PTR [FFDFF638]
0008:804DA11A 8BF2 MOV ESI,EDX
0008:804DA11C 8B5F0C MOV EBX,[EDI+0C]
0008:804DA11F 33C9 XOR ECX,ECX
0008:804DA121 8A0C18 MOV CL,[EBX+EAX]
0008:804DA124 8B3F MOV EDI,[EDI]
0008:804DA126 8B1C87 MOV EBX,[EAX*4+EDI]
0008:804DA129 2BE1 SUB ESP,ECX
0008:804DA12B C1E902 SHR ECX,02
0008:804DA12E 8BFC MOV EDI,ESP
0008:804DA130 3B35D4C75480 CMP ESI,[ntoskrnl!MmUserProbeAddress]
0008:804DA136 0F83A1010000 JAE 804DA2DD
0008:804DA13C F3A5 REPZ MOVSD
0008:804DA13E FFD3 CALL EBX
0008:804DA140 8BE5 MOV ESP,EBP
我们看到了_KiSystemService的一段代码,指令CALL EBX调用了某个内核函数,这个函数是预先从服务表中选出的,然后在EBX寄存器中加载这个函数的地址(详细过程可以阅读我前一篇文章《跟踪Native Api函数调用》)。
函数_KiSystemService并没有在ntoskrnl.exe的导出表中,但是我们有指向表KeServiceDescriptorTable的指针,从这个表中可以取出所要的Native Api函数的地址,比如说,NtReadFile。接下来,函数将在发出CALL EBX指令的进程上下文中调用并返回至_KiSystemService的下一条指令。
前一篇文章中有个问题涉及到了拦截NativeApi函数,所用的方法是将自己的处理程序添加到服务表中替换原来的函数,然后依次调用这两个函数。这里使用的也是这种办法。比如,我们拦截了NtReadFile这个最常调用的函数,然后,在我们的处理程序内部的堆栈中有函数返回到_KiSystemService的地址。以下是演示代码:
DWORD FindKiSystemServiceOriginalEntryPoint()
{
KPRIORITY CurrentThreadPriority;
// 拦截 NtReadFile这个最常调用的函数
CurrentThreadPriority = KeQueryPriorityThread(KeGetCurrentThread());
// so that the system not degraded
KeSetPriorityThread(KeGetCurrentThread(),1);
if( *NtBuildNumber == 2195 ) OriginalNtReadFile = *KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2195];
if( *NtBuildNumber == 2600 ) OriginalNtReadFile = *KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2600];
disableinterruptions
clearwp
if( *NtBuildNumber == 2195 ) KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2195] = ArtificialNtReadFile;
if( *NtBuildNumber == 2600 ) KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2600] = ArtificialNtReadFile;
restorewp
enableinterruptions
while( !FindingForWhile );
// wait, NtReadFile will be called, надеюсь не вечно :)
// 相似的, 地址被找到, 取下ArtificialNtReadFile()处理程序
disableinterruptions
clearwp
if( *NtBuildNumber == 2195 )
KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2195] =
*OriginalNtReadFile;
if( *NtBuildNumber == 2600 )
KeServiceDescriptorTable->NtoskrnlTable.ServiceTable[_NtReadFile_2600] =
*OriginalNtReadFile;
restorewp
enableinterruptions
KeSetPriorityThread(KeGetCurrentThread(),CurrentThreadPriority);
// 恢复原先状态
// 现在计算KiSystemService原来的入口点
return NtReadFileReturnAddress - 0xC4;
// KiSystemService内部入口点与返回点的差
}
NtReadFile的处理程序:
__declspec(naked) ArtificialNtReadFile()
{ __asm
{
push dword ptr [esp] // !!!
pop NtReadFileReturnAddress
inc dword ptr FindingForWhile // 找到所需函数的地址
jmp dword ptr OriginalNtReadFile
}
}
这段代码的含义您可能已经猜到了。对不同版本Win NT的_KiSystemService函数的分析显示,除了WinXPSP2和2K3,在所有NT内核中这个函数都是相同的(为了保证兼容性,在驱动的代码中有某些不大的改动)。这些都可以利用起来。我想现在大家都会明白,如果我们找到了某个内部函数的地址,而函数又有确定的大小,就不难找到它的startup。call ebx后面一条指令的地址我们可在处理程序的堆栈中找到,而从这条指令到startup间的距离是0xC4字节(Win XP, 2K)。现在_KiSystemService的起始地址就不难计算了。
// такова разница м/д точкой входа и точкой возврата внутри KiSystemService
return NtReadFileReturnAddress - 0xC4;
由此我们就得到了_KiSystemService的原始入口点。
这样,第一步就走完了,我们继续向下进行。下面向实现目标迈出的这一大步就应该是找到放置调用kernel32:CreateProcessA的代码的位置。自然,我们可以将代码数据只放在用户地址空间中,而且是放在大多数进程都能访问到的地方。对此没什么太好的办法,我们所能做的就是利用修正后的kernel32.dll模块第一页中的空闲空间,这块空间紧挨着首部。在被加载到内存并进行修正后,从0x1000字节PE首部占据了某些地方的0x400字节,剩余的地方都由0填充。这些地方完全可以为我们所用。但是,这还是个次要的问题,主要的问题是要在内核模式下找到kernel32.dll的基地址。
这个问题也并不困难。实际上,我们所需要的一切都能在进程PEB指向的结构体中找到。有些结构体的描述可以在Свен Шрайбер的那本关于undocunmented win nt的书中找到。有个结构体指向了进程加载的模块的列表,但是我没有在此书中找到具体的对此结构体的描述,所以只好祭起调试器来小小的挖掘一下。看过PEB,我们立即被pPeb->ProcessModuleInfo->ModuleHeader.List3域所吸引。书中所提到的正是这个成员(List3)。经过试验我明白了,这个ListEntry类型的结构体大概是这个样子的(与MS的源码可能会有区别):
typedef struct _KMODULEINFOLISTENTRY {
DWORD Flink;
DWORD Blink;
DWORD ModuleIBase;
DWORD DllEntryPoint;
DWORD Unknown2;
DWORD Unknown3;
PWCHAR ModuleName;
} KMODULEINFOLISTENTRY, *PKMODULEINFOLISTENTRY, **PPKMODULEINFOLISTENTRY;
某些域对于我来说还是个迷,实际上我们对这些域并不感兴趣,其余的我想也不需要解释了。这样,使用这个结构体,我们就可以用
DWORD kwsGetModule(PWCHAR ModuleNameW)函数找到kernel32.dll的基地址,此函数可参见驱动源代码。实际上,这个函数可以用来查找进程中的各个模块。
现在我们回到查找最适宜放置调用kernel32:CreateProcessA代码的位置这个问题上来。我们前边提到,代码将位于kernel32.dll模块首部后紧接着的第一页中。这样我们一箭双雕:一是嵌入这个位置中的代码能被所有的进程看到,二是我们有足够的空间来放置植入的CreateProcessA调用代码。别的倒也没什么,只是模块的PE的第一页在用户空间中是只读的,属性为readonly。为了详细的了解此问题,我们来打开PSDK文档并对CreateProcess函数作更细致的研究:
BOOL CreateProcess(
LPCTSTR lpApplicationName, // name of executable module
LPTSTR lpCommandLine, // command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
LPSECURITY_ATTRIBUTES lpThreadAttributes, // SD
BOOL bInheritHandles, // handle inheritance option
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // new environment block
LPCTSTR lpCurrentDirectory, // current directory name
LPSTARTUPINFO lpStartupInfo, // startup information
LPPROCESS_INFORMATION lpProcessInformation // process information
);
typedef struct _STARTUPINFO {
DWORD cb;
LPTSTR lpReserved;
LPTSTR lpDesktop;
LPTSTR lpTitle;
DWORD dwX;
DWORD dwY;
DWORD dwXSize;
DWORD dwYSize;
DWORD dwXCountChars;
DWORD dwYCountChars;
DWORD dwFillAttribute;
DWORD dwFlags;
WORD wShowWindow;
WORD cbReserved2;
LPBYTE lpReserved2;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION;
我们看到,函数在堆栈中接收10个双字,多数都是指针。其中某些并不是必需的,可以传递NULL,而例如lpStartupInfo,lpProcessInformation和ProgExeNameAddrinUser则是必需要确定的(关于调用权限和传递参数更详细的信息我这里就不给出了,对此可参阅MSDN)。相应的需要为这些参数分配空间。但这只是问题的一部分。
问题的另一部分在于,除了要为结构体提供空间之外,CreateProcess还应该向它们的域进行写入。我们这里不能出现这种情况,因为我们的页是只读的,并且任何向其中写入的尝试都会引起异常,这个异常会关掉当前进程。为了消除在用户模式下向此页写入的可能性,我们应该修改该页PTE的属性。对此有两种办法:一是在内核模式下调用NtProtectVirtualMemory,这时应考虑到这个函数没有导出,需要找到SST表的入口点;二是直接找到该页的descriptor(PTE)并修正这个恼人的“错误”。我们实际上调用了专门为此而写的函数PageAccessProp(…),在堆栈中接收4个参数,这些参数我想就不必解释了。
除此之外,此函数还可以用在如下情况,比如必须在UserMode下操作系统内存页的时候。为此需要相应的翻转U位,但是如果内核使用的是4K字节的页,则在descriptor里还有一个G位。此位的意思是这一页是全局的,而且在任务切换或重置CR3时不应在TLB中更新或替换此PTE。也就是说,如果翻转U位,关掉G位,在用户模式下向高2G空间中写入,就会引起异常。一定要关掉CR4中的PGE位,然后进行类似的操作并向系统页进行写入。除此之外,不要忘了CR0中的WP位。从原理上讲会出现的问题到这里就讲完了。
接下来还有一个有趣的任务。我们必须找到导出函数kernel32:CreateProcessA。在我们的驱动中这项工作是由函数GetExportedFuncAddr(DWORD ModuleImageBase,PCHAR FuncName)来负责的,此函数的参数是模块在内存中的地址和所要找函数的名字。若找到,则返回其在内存中的基地址。代码我就不解释了,更详细的信息请参见PE的相关文档,同时还可以直接研究GetExportedFuncAddr。驱动中用到的所有结构体都位于struc.h中。一般来讲,在跟踪CreateImplant()函数时,前面讲到的各个部分会按顺序出现。
前面我们讨论了所有所需的辅助,相关的函数,并被引导到了神秘的目标面前。现在我们从大体上来依次研究驱动的算法。
驱动中最主要的函数就是ReplaceKiSystemServiceCode()。此函数首先用前面讲到的方法找到startup _KiSystemService,然后直接切入代码的核心来取得控制权。为此我们必须:第一,取得_KiSystemService返回时的返回点来分析地址并为后面替换此地址提供方便,而替换后的地址就指向我们植入kernel32.dll第一页中的代码;第二,处理程序将植入代码注入到用户模式内存中。总共会有两处拦截。
现在来更详细的看一下第一处拦截。我们来看代码:
0008:804DA07C 6A00 PUSH 00
0008:804DA07E 55 PUSH EBP
0008:804DA07F 53 PUSH EBX
0008:804DA080 56 PUSH ESI
0008:804DA081 57 PUSH EDI
0008:804DA082 0FA0 PUSH FS
0008:804DA084 BB30000000 MOV EBX,00000030
0008:804DA089 668EE3 MOV FS,BX
0008:804DA08C FF3500F0DFFF PUSH DWORD PTR [FFDFF000]
0008:804DA092 C70500F0DFFFFFFFFFFF MOV DWORD PTR [FFDFF000],FFFFFFFF
0008:804DA09C 8B3524F1DFFF MOV ESI,[FFDFF124]
0008:804DA0A2 FFB640010000 PUSH DWORD PTR [ESI+00000140]
0008:804DA0A8 83EC48 SUB ESP,48
原始的 startup _KiSystemService.
我们直接在代码起始处注入第一个处理程序,方法就是splicing,保存原始地址,之后将其恢复。这样就得到了下面的代码:
0008:804DA07C FF25008578FC JMP [ArtificialKiSystemService]
0008:804DA082 0FA0 PUSH FS
0008:804DA084 BB30000000 MOV EBX,00000030
0008:804DA089 668EE3 MOV FS,BX
0008:804DA08C FF3500F0DFFF PUSH DWORD PTR [FFDFF000]
0008:804DA092 C70500F0DFFFFFFFFFFF MOV DWORD PTR [FFDFF000],FFFFFFFF
0008:804DA09C 8B3524F1DFFF MOV ESI,[FFDFF124]
0008:804DA0A2 FFB640010000 PUSH DWORD PTR [ESI+00000140]
0008:804DA0A8 83EC48 SUB ESP,48
下面是处理程序KiSystemServiceHandler(),这个函数的地址就保存在ArtificialKiSystemService里。
__declspec(naked) KiSystemServiceHandler()
{
SaveKISSRetAddr // 保存由系统服务向用户模式返回的返回点
_asm{
OriginalKiSystemServiceInlineStartUpCode // 恢复原始代码
push dword ptr [OriginalKiSystemService]
add dword ptr [esp],OriginalKiSystemServiceStartUpCodeSize
ret
}
}
我希望您能明白,为什么拦截被实现在startup代码的起始处。正是在这里我们能从线程堆栈中取出所需的地址而不用多费力气。但是我不主张在KiSystemServiceHandler()处理程序内部进行更为复杂的操作。这段代码是在关中断的情况下执行的,也就是说IRQL处于最高级,且除此之外fs也不要动。处理程序中不当的行为必然会引起系统崩溃。现在来看第二处拦截,代码如下:
0008:804DA07C FF25008578FC JMP [ArtificialKiSystemService]
0008:804DA082 0FA0 PUSHFS
0008:804DA084 BB30000000 MOV EBX,00000030
0008:804DA089 668EE3 MOV FS,BX
0008:804DA08C FF3500F0DFFF PUSH DWORD PTR [FFDFF000]
0008:804DA092 C70500F0DFFFFFFFFFFF MOV DWORD PTR [FFDFF000],FFFFFFFF
0008:804DA09C 8B3524F1DFFF MOV ESI,[FFDFF124]
0008:804DA0A2 FFB640010000 PUSH DWORD PTR [ESI+00000140]
0008:804DA0A8 83EC48 SUB ESP,48
0008:804DA0AB 8B5C246C MOV EBX,[ESP+6C]
0008:804DA0AF 83E301 AND EBX,01
0008:804DA0B2 889E40010000 MOV [ESI+00000140],BL
0008:804DA0B8 8BEC MOV EBP,ESP
0008:804DA0BA 8B9E34010000 MOV EBX,[ESI+00000134]
0008:804DA0C0 895D3C MOV [EBP+3C],EBX
0008:804DA0C3 89AE34010000 MOV [ESI+00000134],EBP
0008:804DA0C9 FC CLD
0008:804DA0CA F6462CFF TEST BYTE PTR [ESI+2C],FF
0008:804DA0CE 0F85D6FEFFFF JNZ 804D9FAA
0008:804DA0D4 FB STI // 降IRQL
0008:804DA0D5 FF25F08478FC JMP [ArtificialKiSystemServiceSafedCode]
0008:804DA0DB CC INT 3
0008:804DA0DC CC INT 3
0008:804DA0DD 8BCF MOV ECX,EDI
现在我们来看处理程序KiSystemServiceHandler2(),其地址就在ArtificialKiSystemServiceSafedCode:
__declspec(naked)KiSystemServiceHandler2()
{
__asm mov CanUnload,FALSE // 设置不能卸载驱动的标志
saveregisters // 保护寄存器
if (!isP)
{
isP++; // 建立不可多次进入的标志
CreateImplant(); // 注入植入代码
}
restoreregisters // 恢复寄存器
__asm
{
OriginalKiSystemServiceInLineSafedCode
push dword ptr [KiSystemServiceSafedCode]
add dword ptr [esp],OriginalAfterStiCodeSize
mov CanUnload,TRUE // 现在可以放心的卸载驱动了:)
ret
}
}
在这个处理程序中我们可以做所有我们想到的事情。这里的IRQL是最低的,所以我们在这里调用CreateImplant()函数。这个函数完成前面讲过的操作,包括注入植入代码。在此之后,在_KiSystemService返回后将调用植入代码,在其处理程序之后线程重新返回到这个中断之前,更精确讲,就是返回到ntdll.dll内部。下面是CreateImplant()函数中创建植入代码部分的代码。
// 将从KiSystemService返回时的返回点
// 改为用户模式下植入代码的地址
mov eax,ImpStartAddr
mov ebx,[KISS_SP]
// 将参数由后向前塞入堆栈
mov [ebx],eax
// pushad
mov bl,0x60
mov [eax],bl
inc eax
mov bl, 0xb8
mov [eax], bl
inc eax
mov ebx,pUprocessInformation // PI
mov [eax],ebx // mov eax,PI
add eax,4
mov [eax],0x50 // push eax
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,pUstartUpInfo
mov [eax],ebx // mov eax,SI
add eax,4
mov [eax],0x50 // push eax
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
mov [eax],ebx // mov eax,0
add eax,4
mov [eax],0x50 // push eax
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
mov [eax],ebx // mov eax,0
add eax,4
mov [eax],0x50 // push eax
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0x04000000
// mov eax,0x04000000 = Create_default_error_mode
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,ProgExeNameAddrinUser
// mov eax,ProgExeNameAddrinUser
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,0
// mov eax,0
mov [eax],ebx
add eax,4
// push eax
mov [eax],0x50
inc eax
// 现在是过程调用本身
mov bl,0xb8
mov [eax],bl
inc eax
mov ebx,CreateProcessA_OEP
// mov eax,CreateProcessA_OEP
mov [eax],ebx
add eax,4
mov bx,0xD0FF
mov [eax], bx
// call eax
add eax,2
// popad
mov bl,0x61
mov [eax],bl
inc eax
mov bl,0xbb
mov [eax],bl
inc eax
mov ebx,KiSystemServiceReturnAddress
// mov ebx,KiSystemServiceReturnAddress
mov [eax],ebx
add eax,4
mov bx,0xE3FF
// jmp ebx ... 现在重新恢复
// KiSystemService的原始返回点
mov [eax], bx
我们看到,借助于数十行的汇编指令,我们在kernel32.dll的首部中建立起了植入代码,这段代码看上去如下:
图中所示为kernel32.dll模块的一段内存dump。红框内的是代码,而蓝框内的是结构体USTARTUPINFO、UPROCESS_INFORMATION和ProgExeName,对应于CreateProcessA中相应的结构体。红框中的代码的实际意义如下:
0010:77E6047A 60 PUSHAD
0010:77E6047B B84C04E677 MOV EAX,77E6044C - UPROCESS_INFORMATION
0010:77E60480 50 PUSH EAX
0010:77E60481 B80804E677 MOV EAX,77E60408 - USTARTUPINFO
0010:77E60486 50 PUSH EAX
0010:77E60487 B800000000 MOV EAX,00000000
0010:77E6048C 50 PUSH EAX
0010:77E6048D B800000000 MOV EAX,00000000
0010:77E60492 50 PUSH EAX
0010:77E60493 B800000004 MOV EAX,04000000 - Create_default_error_mode
0010:77E60498 50 PUSH EAX
0010:77E60499 B800000000 MOV EAX,00000000
0010:77E6049E 50 PUSH EAX
0010:77E6049F B800000000 MOV EAX,00000000
0010:77E604A4 50 PUSH EAX
0010:77E604A5 B800000000 MOV EAX,00000000
0010:77E604AA 50 PUSH EAX
0010:77E604AB B85C04E677 MOV EAX,77E6045C - ProgExeNameAddrinUser
0010:77E604B0 50 PUSH EAX
0010:77E604B1 B800000000 MOV EAX,00000000
0010:77E604B6 50 PUSH EAX
0010:77E604B7 B8BC1BE677 MOV EAX,KERNEL32!CreateProcessA
0010:77E604BC FFD0 CALL EAX
0010:77E604BE 61 POPAD
0010:77E604BF BB0403FE7F MOV EBX,7FFE0304 - 向ntdll.dll中返回的地址
0010:77E604C4 FFE3 JMP EBX
总的来说都比较简单。在线程用指令ret/sysexit离开_KiSystemService时,通过改换返回线程内核堆栈的地址,就去执行前面编写的汇编植入代码。这段代码在CreateProcessA中调用,之后JMP EBX指令重新返回到ntdll.dll。由此,如果一切顺利,就会调用创建进程的植入代码。
之后从类似于Mark Russinovich的Process Explorer这样的程序中就可以看到所创建的进程。如果,比如说,我们创建了一个GUI进程,则在某些情况下看不到应用程序窗口,尽管Process Explorer能正常显示其存在。不用担心,事实上,在某些我所不了解的情况下进程没有连接上WindowStation并获得比如说Desktop,这会发生在父进程成为Services进程时。
例如,我们看到,“产生”在WinAmp内部的CMD.EXE进程,取得了Desktop并在“表面”上可见。
这又是另一种情况了。
我想就不必多说了。
通过本文所讲,我想我们已经体会到,我们实现目的的方法是多么的简单。说起来,从内核模式启动用户进程不算是什么鬼把戏。现在在读过所有说明与补充之后,您可以着手研究驱动代码了。加载驱动可以用Four-F的KmdKit里的KmdManager。其实,要感谢他帮助解决了文末的一个问题,同时还要感谢lial先生对本文的热情帮助。
批评建议请发至
[email protected]。