从内核模式启动进程

 从内核模式启动进程


我想很多人都会有这样的问题,能否从内核驱动中启动用户应用程序呢?在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]

你可能感兴趣的:(从内核模式启动进程)