发新话题
打印

[转载]NT 下动态切换进程分析笔记

[转载]NT 下动态切换进程分析笔记

文章作者:sinister
信息来源:白细胞

Author:  sinister
Email:  sinister@whitecell.org
Homepage:http://www.whitecell.org
Date:   2005-11-16


2005-2-22

  在应用层想要动态嵌入其他进程内存空间,我们可以调用 CreateRemoteThread()
等相关函数来实现。那么在内核态想要动态嵌入其他进程内存空间又如何做呢?这时
我们需要调用一个未公开的内核函数 KeAttachProcess(),利用这个函数我们可以在
内核态实现将代码插入到其他进程地址空间中。下面是一段演示代码。


NTSTATUS PsLookupProcessByProcessId( IN ULONG ulProcId, OUT PEPROCESS * pEProcess);
VOID KeAttachProcess ( IN PEPROCESS pEProcess );
VOID KeDetachProcess ( VOID );


NTSTATUS KeSwitchProcess(ULONG uPID)
{
   PEPROCESS  EProcess;
   LPTSTR    lpCurProc;
   DWORD     dwCR3;
   NTSTATUS   ntStatus;
   
   ntStatus = PsLookupProcessByProcessId(uPID, &EProcess);

      if (!NT_SUCCESS( ntStatus ))
   {

    DbgPrint("PsLookupProcessByProcessId()\n");
          return ntStatus;
   }

      KeAttachProcess(EProcess);
   __asm{
    mov eax,cr3
    mov dwCR3,eax
   }

     lpCurProc = (LPTSTR)EProcess;
     lpCurProc = lpCurProc + ProcessNameOffset;
     DbgPrint("CR3 = %x, PROCESS = PROCESS NAME: %s, PROCESS ID: %d, PROCESS ADDRESS %x:\n",
        dwCR3,
           lpCurProc,
           uPID,
           EProcess
           );

     KeDetachProcess();

     return STATUS_SUCCESS;

}

NTSTATUS KeEnumProcess(VOID)
{
   ULONG nextoffset;
   NTSTATUS rc;
   ULONG ReturnLength = 0;
   SYSTEM_PROCESSES *curr = NULL;
   PVOID SystemInformationBuffer = ExAllocatePool(PagedPool, 255*sizeof(SYSTEM_PROCESSES));
   

   rc = ZwQuerySystemInformation(5, SystemInformationBuffer, 255*sizeof(SYSTEM_PROCESSES), &ReturnLength );

   if(NT_SUCCESS(rc))
   {
     curr = (SYSTEM_PROCESSES *)SystemInformationBuffer;
  
      while(curr)
      {

        rc = SwitchProcess(curr->ProcessId);
        if (!NT_SUCCESS( rc ))
    {
           DbgPrint("AttachProcess() to failed!\n");
    }

        nextoffset = curr->NextEntryDelta;
    if (nextoffset) {
       curr = (SYSTEM_PROCESSES *)((PCHAR)curr + nextoffset);
    }
    else curr = NULL;
     }
   }

   ExFreePool(SystemInformationBuffer);
   return rc;
}


  为了调试方便,我枚举系统中所有进程,并依次调用 KeAttachProcess(),KeDetachP
rocess()  函数动态嵌入和退出,通过打印 CR3 的值我们可以看到确实是实现了动态嵌
入的目的,在这里我们并不想介绍嵌入函数的用法,而是想要了解动态切换进程内存空
间的实现方法。首先我们来分析一下 KeAttachProcess() 函数的流程,当调用此函数时,
首先从 KPCR 结构中得到当前线程 ETHREAD 结构,并提升当前 IRQL 为 DISPATCH_LEVEL
(防止在进程切换中被其他软件列程所中断),并从形参中取出要切换进程的 EPROCESS
结构与当前进程 EPROCESS 结构进行比较,如果是当前进程的话,则降低当前 IRQL 为
初始值,函数返回。如果要进行切换的进程不是当前进程的话,先要判断当前线程是否
处于 APC 与 DPC 活动状态(ETHREAD->ApcStateIndex,KPCR->DpcRoutineActive),
如果是则不允许动态切换进程,跳转到蓝屏处,系统崩溃。(微软规定在进行动态切换
时不允许 APC ,DPC 列程运行,否则将系统崩溃,这应该不是必须的)如果不是则从形
参中取当前线程(ETHREAD)、要切换的进程(EPROCESS)初始的 IRQL 值和当前线程的
APC 保存状态 (ETHREAD->SavedApcState) 做为参数,来调用 KiAttachProcess()函
数完成具体的切换。


  从上面可以看出 KeAttachProcess() 函数只是进行了一些相关参数的设置和系统环
境的判断后简单的调用了 KiAttachProcess() 函数,那么我们需要对 KiAttachProcess()
流程做一下分析。当调用此函数时先把要切换的进程的内核堆栈数加一(EPROCESS->Stack
Count),并从形参中取得当前线程保存 APC 状态的位置,(ETHREAD->SaveApcState)
得到当前线程 APC 状态(ETHREAD->ApcState),来调用 KiMoveApcState()  函数,将当
前线程APC 状态 (ETHREAD->ApcState) 复制到当前线程 SavedApcState 处保存。然后
将当前线程内核 APC 的 Progress 状态、内核模式 APC 的 Pending 状态、用户模式
APC 的 Pending 态均设置为 FALSE(ETHREAD->KernelApcInProgress、ETHREAD->Kernel
ApcPending、ETHREAD->UserApcPending),并初始化当前线程内核模式与用户模式 APC
状态链(ETHREAD->>ApcState.ApcListHead[KernelMode]、ETHREAD->ApcState.ApcList
Head[UserMode]),然后将当前线程所在进程(ETHREAD->ApcState->EPROCESS)设置为
要切换的进程(EPROCESS),比较当前线程保存 APC 状态(ETHREAD->SavedApcState)
是否与形参中(SavedApcState)要输出的保存 APC 态相等,如相等则需要将当前线程
APC 状态与保存 APC 状态(ETHREAD->ApcState、ETHREAD->SavedApcState)分别赋与
当前线程的 ApcStatePointer[0] 与 ApcStatePointer[1],(ApcStatePointer 是
KAPC_STATE 结构数组而 KAPC_STATE 又是由 LIST_ENTRY 链表描述的)然后在设置当前
线程 APC 状态索引为 1(ETHREAD->ApcStateIndex)。这里需要重点讲解一下,上述基
本都是处理与 APC 相关的操作,(APC 即“异步过程调用” )为什么要做这些操作呢?
在进程调度中怎么没有看到相关的操作?那是因为进行强行切换时需要中断当前线程运
行切换到新的进程中,这种切换又跟时钟中断所产生的进程调度不一样,时钟中断产生
的进程调度会根据时间片与线程优先级等让被中断的线程 APC 重新得以运行,而这种强
行进程切换如果不调用退出函数,是不会让原线程 APC 运行的,所以首先要保留当前线
程的 APC 状态,状态保存后为了不让当前 APC 状态影响要切换的新进程,则需要把相关
APC 位清0。为了更加明确,我这里配合 KeDetachProcess() 函数深入讲解一下,当调用
KeDetachProcess() 函数退出时,首先得到当前线程所指象的进程(ETHREAD->ApcState-
>EPROCESS),这里要注意,当前运行环境是已被嵌入的新进程,所以得到的是被嵌入的
进程,退出函数的下一步就是调用KiMoveApcState 来恢复 SavedApcState 到当前线程
APC 状态(ETHREAD->ApcState),
这时的 ETHREAD->ApcState->EPROCESS 是原始进程和刚才第一次调用得到的被嵌入的新
进程是两个 EPROCESS 结构地址了,待做一些相关操作后调用 KiAttachProcess() 来切
换回到原始进程。在段落开头时讲过 KiAttachProcess() 调用会先判断要进行嵌入的是
否为同一进程,如果是则退出,经过上述对线程相关的 APC 操作后,现在是完全不同的
两个进程,所以可以顺利的切换回原始进程。想象一下我们经常通过一个线程(ETHREAD)
的偏移 0x44 处得到它所属的进程(EPROCESS)而这个进程正是在当前线程的 APC 状态
中的(ETHREAD->ApcState->EPROCESS)通过上面的分析,可以看出微软把对 APC 的处
理与ETHREAD,EPROCESS 结构之间的连接安排的非常巧妙,充分考虑了各种环境应用。
好了,当相关 APC 设置完成后,我们需要比较要切换的进程是否在内存中?(EPROCESS
->State)(因为有可能进程在等待或挂起时相关页面已交换到硬盘页面文件中)如果不
存在则不能马上进行切换,要将当前线程等待链(ETHREAD->WaitListEntry)插入到要
切换进程的就绪链中(EPROCESS->ReadyListHead),并设置当前线程为就绪状态(ETHR
EAD->State)和当前线程链所指向的进程就绪队列(ETHREAD->ProcessReadyQueue)为
TRUE。然后判断要切换的进程状态(EPROCESS->State)是否已经交换出当前内存,如果
为其他状态则直接调用 KiSwapThread() 函数来完成切换。如果已交换出当前内存状态
则将要切换的进程状态(EPROCESS->State)设置为 Transition 并将要切换进程的交换
链(EPROCESS->SwapListEntry)插入到全局进程输入交换链 KiProcessInSwapListHead
中继续设置全局交换事件状态(KiSwapEvent.Header.SignalState),判断全局交换事
件等待链头(KiSwapEvent.Header.WaitListHead)是否为空如果不为空则需要调用 KiW
aitTest() 函数来测试全局交换事件 (KiSwapEvent)余额。最后调用 KiSwapThread()
来完成最终切换并跳转到函数结束处。如果存在当前内存中,则遍历要切换进程的就绪
链(EPROCESS->ReadyListHead),如果要切换进程的下一节点没有就绪,则从线程等待
链里得到一个线程(ETHREAD)并移除刚刚得到的要切换进程的节点,设置得到的线程链
所指向的进程就绪队列(ETHREAD->ProcessReadyQueue)为 FALSE,然后调用 KiReady
Thread() 来就绪得到的线程(ETHREAD),并得到要切换进程就绪链的下一节点,重复
上述步骤直到要切换进程的下一节点就绪,这时调用 KiSwapProcess() 函数来完成最终
的切换工作,切换完成后从形参中得到初始 IRQL 值,调用 KfLowerIrql() 来降低当前
IRQL 为初始值,函数结束。这里可能会有疑问,既然当前进程在内存中直接调用 KiSw
apProcess() 来切换不就行了?为什么还需要做那么多的设置?那是因为必须是有一个
线程在你要切换进程的就绪队列中,这个时候才可以实现强行切换。


  这里对最终切换进行一个分析,上面提到了两种切换方式,一种是 KiSwapThread(),
在这份笔记里我不打算记录它,我想在下一份笔记里将线程优先级分析等结合在一起来
讲。这份笔记里仅仅记录对 KiSwapProcess() 的分析,其实 KiSwapProcess()和《NT
内核的进程调度分析笔记》中写到的 SwapContext() 函数大致一样,而且还比他简单了
许多,因为要被嵌入的进程许多环境和参数都已设置好了。函数实现就是,先得到要切换
进程的 LDT 描述符(EPROCESS->LdtDescriptor)与页目录表(EPROCESS->DirectoryTa
bleBase),判断要切换的进程 LDT 不为空,如果不为空则从 KPCR 中取得 KGDT 的位
置,并从 KGDT 中索引到 LDT,把要切换的进程(EPROCESS->LdtDescriptor)中的 LDT
赋与 KPCR 中的LDT。再得到 KPCR 中 IDT的位置,把当前进程(EPROCESS->Int21Descr
iptor)中的 INT 21中断赋与 KPCR 中 KIDT中的相应位置,使当前进程可以调用 INT
21。最后跳转到调用 LLDT 使当前所有设置生效。(按理说 NT 内核中 32 位应用程序
是不使用 LDT 的但为什么在线程切换中会有设置LDT的部分呢?这是为了向下兼容 16
位的应用程序,当调度到一个 16 位的应用程时则会特意为它分配 LDT 并且使 IDT 中的
INT 21有效,玩过 DOS 的人都知道 INT 21 是 DOS 下的系统调用,可以试着运行一个
16 位的 DOS 程序,然后观察下 IDT 表就会发现,原来没有用到的 INT 21 会被设置成
一个 16 位的 TrapGate),否则用要切换进程的页目录来刷新 KTSS 中的 CR3 值与当前
CR3 值,并用要切换进程的 IOPM 来填充 KTSS 中的 IOPM。完成切换,函数返回。


:u KeAttachProcess l 200
ntoskrnl!KeAttachProcess
0008:8042BE1A  PUSH    EBP
0008:8042BE1B  MOV     EBP,ESP
0008:8042BE1D  PUSH    ECX
0008:8042BE1E  PUSH    ESI
0008:8042BE1F  MOV     EAX,FS:[00000124]  
0008:8042BE25  MOV     ESI,EAX  注释:ESI = 当前线程(ETHREAD)
0008:8042BE27  CALL    [HAL!KeRaiseIrqlToDpcLevel]
0008:8042BE2D  MOV     ECX,[EBP+08]  注释:ECX = 要切换进程的(EPROCESS)
0008:8042BE30  MOV     [EBP-04],AL  注释:保存初始 IRQL 值到临时变量
0008:8042BE33  CMP     [ESI+44],ECX  
0008:8042BE36  JNZ    8042BE46
0008:8042BE38  MOV     CL,AL   
0008:8042BE3A  CALL    80403FD0  注释:降低当前 IRQL(CL=初始IRQL值)
0008:8042BE3F  POP     ESI
0008:8042BE40  LEAVE
0008:8042BE41  RET     0004  注释:KeAttachProcess() 函数调用完毕,返回。

从 KPCR 结构中得到当前线程 ETHREAD 结构,并提升当前 IRQL 为 DISPATCH_LEVEL 并
从形参中取出要切换进程的 EPROCESS 结构与当前进程 EPROCESS 结构进行比较,如果
要进行切换的进程不是当前进程的话,则跳转到具体切换地址进行处理。如果是当前进
程的话,则降低当前 IRQL 为初始值,函数返回。

0008:8042BE44  JMP    8042BE3F

跳转函数完成地址。

0008:8042BE46  CMP     BYTE PTR [ESI+00000159],00
0008:8042BE4D  JNZ    8042BE6C

判断如果当前线程(ETHREAD->ApcStateIndex)处于 APC 运行状态,则不允许动态切换
进程,跳转到蓝屏处,系统崩溃。

0008:8042BE4F  MOV     EAX,FS:[0000080C]
0008:8042BE55  TEST    EAX,EAX
0008:8042BE57  JNZ    8042BE6C

判断如果当前 有 DCP 列程运行 (KPCR->DpcRoutineActive) 则不允许动态切换进程,
跳转到蓝屏处,系统崩溃。

0008:8042BE59  LEA     EAX,[ESI+00000140] 注释:当前线程的 APC 状态(ETHREAD->SavedApcState)
0008:8042BE5F  PUSH    EAX
0008:8042BE60  PUSH    DWORD PTR [EBP-04] 注释:初始的 IRQL 值
0008:8042BE63  PUSH    ECX           注释:要切换的进程(EPROCESS)
0008:8042BE64  PUSH    ESI           注释:当前线程(ETHREAD)
0008:8042BE65  CALL    8042C2F2        注释:调用 KiAttachProcess() 函数  

如果当前没有 APC,DPC 等调用处理,则取当前线程(ETHREAD)、要切换的进程(EPROC
ESS)初始的 IRQL 值和当前线程的 APC 保存状态 (ETHREAD->SavedApcState) 做为参
数,来调用KiAttachProcess() 函数完成具体的切换。

0008:8042BE6A  JMP    8042BE3F

进程切换完成后,直接跳转函数完成地址。

0008:8042BE6C  MOV     EAX,FS:[0000080C]
0008:8042BE72  PUSH    EAX
0008:8042BE73  MOVZX    EAX,BYTE PTR [ESI+00000159]
0008:8042BE7A  PUSH    EAX
0008:8042BE7B  PUSH    DWORD PTR [ESI+44]
0008:8042BE7E  PUSH    ECX
0008:8042BE7F  PUSH    05
0008:8042BE81  CALL    ntoskrnl!KeBugCheckEx

调用 KeBugCheckEx() 蓝屏,系统崩溃,并显示错误信息(当前进程,要切换到的目标进
程、APC,DPC 状态等)


_______________________________________________________________________________________



下面是 KiAttachProcess() 实现细节

:u 8042c2f2 l 200

0008:8042C2F2  PUSH    EBP
0008:8042C2F3  MOV     EBP,ESP
0008:8042C2F5  PUSH    EBX
0008:8042C2F6  PUSH    ESI
0008:8042C2F7  MOV     ESI,[EBP+08]    注释:ESI = 当前线程(ETHREAD)
0008:8042C2FA  PUSH    EDI
0008:8042C2FB  PUSH    DWORD PTR [EBP+14] 注释:形参 SavedApcState
0008:8042C2FE  MOV     EDI,[EBP+0C]    注释:EDI = 要切换进程的(EPROCESS)
0008:8042C301  LEA     EBX,[ESI+34]    注释:EBX = 当前线程APC状态(ETHREAD->ApcState)

0008:8042C304  INC     WORD PTR [EDI+60]
0008:8042C308  PUSH    EBX
0008:8042C309  CALL    8042C3FC       注释:调用 KiMoveApcState() 函数

把要切换的进程(EPROCESS->StackCount)的内核堆栈数加一,并从形参中取得当前线程
(ETHREAD->SaveApcState)保存APC状态的位置,得到当前线程(ETHREAD->ApcState)
APC 状态,来调用 KiMoveApcState()  函数。


0008:8042C30E  AND     BYTE PTR [ESI+48],00  
0008:8042C312  AND     BYTE PTR [ESI+49],00  
0008:8042C316  AND     BYTE PTR [ESI+4A],00  
0008:8042C31A  LEA     EAX,[ESI+3C]      
0008:8042C31D  MOV     [ESI+40],EAX
0008:8042C320  PUSH    01
0008:8042C322  MOV     [EAX],EAX
0008:8042C324  LEA     EAX,[ESI+00000140] 注释:EAX = 当前线程(ETHREAD->SavedApcState)保存 ACP 状态
0008:8042C32A  CMP     [EBP+14],EAX  注释:[EBP+14] = 形参 SavedApcState。
0008:8042C32D  MOV     [ESI+38],EBX
0008:8042C330  MOV     [EBX],EBX
0008:8042C332  MOV     [ESI+44],EDI  
0008:8042C335  POP     EDX      
0008:8042C336  JNZ    8042C34A

将当前线程(ETHREAD->KernelApcInProgress、ETHREAD->KernelApcPending、ETHREAD->
UserApcPending)内核 APC 的 Progress 状态、内核模式 APC 的 Pending 状态、用户
模式 APC 的 Pending 态均设置为 FALSE,并初始化当前线程ETHREAD->>ApcState.ApcL
istHead[KernelMode] ETHREAD->>ApcState.ApcListHead[UserMode])内核模式与用户模
式 APC 状态链,然后将当前线程(ETHREAD->ApcState->EPROCESS)所在进程设置为要切
换的进程(EPROCESS),比较当前线程(ETHREAD->SavedApcState)保存 APC 状态是否
与形参中(SavedApcState)要输出的保存 APC 态相等,如果不相等则直接跳转到比较
当前进程(EPROCESS->State)状态处。

0008:8042C338  MOV     [ESI+0000012C],EAX
0008:8042C33E  MOV     [ESI+00000130],EBX
0008:8042C344  MOV     [ESI+00000159],DL 注释:EDX = 1

如果当前线程(ETHREAD->SavedApcState)保存 APC 状态与形参中(SavedApcState)要
输出的保存 APC 状态相等则将当前线程(ETHREAD->ApcState、ETHREAD->SavedApcState)
APC 状态与保存 APC 状态分别赋与当前线程的 ApcStatePointer[0] 与 ApcStatePointer
[1],(ApcStatePointer 是 KAPC_STATE 结构数组而 KAPC_STATE 又是由 LIST_ENTRY 链
表描述的)然后在设置当前线程(ETHREAD->ApcStateIndex)APC 状态索引为 1。

0008:8042C34A  CMP     BYTE PTR [EDI+65],00  注释:要切换的进程(EPROCESS->State)状态
0008:8042C34E  JNZ    8042C391

比较要切换的进程(EPROCESS->State)是否在内存中?如果不存在则直接跳转到设置当前
线程(ETHREAD->State)状态与切换线程处执行。

0008:8042C350  LEA     ESI,[EDI+40] 注释:ESI = 要切换进程(EPROCESS->ReadyListHead)的就绪链表
0008:8042C353  MOV     EAX,[ESI]
0008:8042C355  CMP     EAX,ESI
0008:8042C357  JZ     8042C374
0008:8042C359  MOV     EDX,[EAX]
0008:8042C35B  LEA     ECX,[EAX-5C] 注释:ECX = 等待线程(ETHREAD)链
0008:8042C35E  MOV     EAX,[EAX+04]
0008:8042C361  MOV     [EAX],EDX
0008:8042C363  MOV     [EDX+04],EAX
0008:8042C366  AND     BYTE PTR [ECX+0000011D],00 注释:线程链所指向的进程就绪队列(ETHREAD->ProcessReadyQueue)
0008:8042C36D  CALL    8043150B   注释: 调用 KiReadyThread() 函数
0008:8042C372  JMP    8042C353      
0008:8042C374  MOV     EAX,[EBP+14]      注释:EAX = 形参 SavedApcState
0008:8042C377  PUSH    DWORD PTR [EAX+10]  注释:当前线程(ETHREAD->SavedApcState->EPROCESS)所在进程
0008:8042C37A  PUSH    EDI            注释:要切换的进程
0008:8042C37B  CALL    8040438C   注释:调用 KiSwapProcess() 函数
0008:8042C380  MOV     CL,[EBP+10]
0008:8042C383  CALL    80403FD0   注释:调用 KfLowerIrql() 函数


如果要切换的进程(EPROCESS)在内存中则遍历要切换进程(EPROCESS->ReadyListHead)
的就绪链,(ReadyListHead 是 LIST_ENTRY 结构),如果要切换进程的下一节点没有就
绪,则从线程等待链里得到一个线程(ETHREAD)并移除刚刚得到的要切换进程的节点,
设置得到的线程链所指向的进程就绪队列(ETHREAD->ProcessReadyQueue)为 FALSE,
然后调用 KiReadyThread() 来就绪得到的线程(ETHREAD),并得到要切换进程就绪链的
下一节点,重复上述步骤直到要切换进程的下一节点就绪,这时调用 KiSwapProcess()
函数来完成最终的切换工作,切换完成后从形参中得到初始 IRQL 值,调用 KfLowerIr
ql() 来降低当前 IRQL 为初始值。


0008:8042C388  POP     EDI
0008:8042C389  POP     ESI
0008:8042C38A  POP     EBX
0008:8042C38B  POP     EBP
0008:8042C38C  RET     0010

函数结束,返回。

0008:8042C38F  JMP    8042C388

跳转到函数结束。

0008:8042C391  LEA     EAX,[EDI+40] 注释:EAX = 要切换进程(EPROCESS->ReadyListHead)的就绪链表
0008:8042C394  MOV     [ESI+2D],DL  注释:[ESI+2D] = 当前线程(ETHREAD->State)状态
0008:8042C397  MOV     [ESI+0000011D],DL 注释:当前线程(ETHREAD->ProcessReadyQueue)链所指向的进程就绪队列
0008:8042C39D  LEA     ECX,[ESI+5C] 注释:ECX = 当前线程(ETHREAD->WaitListEntry)等待链
0008:8042C3A0  MOV     EBX,[EAX+04]
0008:8042C3A3  MOV     [ECX],EAX
0008:8042C3A5  MOV     [ESI+60],EBX
0008:8042C3A8  MOV     [EBX],ECX
0008:8042C3AA  MOV     [EAX+04],ECX
0008:8042C3AD  CMP     [EDI+65],DL  注释:要切换的进程(EPROCESS->State)状态
0008:8042C3B0  JNZ    8042C3EE
0008:8042C3B2  MOV     BYTE PTR [EDI+65],02
0008:8042C3B6  MOV     ECX,[80482714]
0008:8042C3BC  LEA     EAX,[EDI+48]  注释:EAX = 要切换进程(EPROCESS->SwapListEntry)的交换链
0008:8042C3BF  MOV     [EDI+4C],ECX
0008:8042C3C2  MOV     DWORD PTR [EAX],80482710 注释:80482710 = 全局进程输入交换链(KiProcessInSwapListHead)
0008:8042C3C8  MOV     [ECX],EAX
0008:8042C3CA  CMP     DWORD PTR [80482C98],80482C98 注释:80482C98 = 全局交换事件等待链(KiSwapEvent.Header.WaitListHead)
0008:8042C3D4  MOV     [80482714],EAX
0008:8042C3D9  MOV     [80482C94],EDX  注释:[80482C94] 全局交换事件状态(KiSwapEvent.Header.SignalState)
0008:8042C3DF  JZ     8042C3EE
0008:8042C3E1  PUSH    0A
0008:8042C3E3  MOV     ECX,80482C90 注释:80482C90 = 全局交换事件(KiSwapEvent)
0008:8042C3E8  POP     EDX
0008:8042C3E9  CALL    80432BF1    注释:调用 KiWaitTest() 函数
0008:8042C3EE  MOV     AL,[EBP+10]
0008:8042C3F1  MOV     [ESI+54],AL  注释:[ESI+54] = 当前线程(ETHREAD->WaitIrql)等待状态的 IRQL
0008:8042C3F4  CALL    80404080    注释:调用 KiSwapThread() 函数
0008:8042C3F9  JMP    8042C388   
0008:8042C3FB  INT     3

如果要切换的进程(EPROCESS)不在内存中则将当前线程(ETHREAD->WaitListEntry)等
待链插入到要切换进程(EPROCESS->ReadyListHead)的就绪链表中,并设置当前线程
(ETHREAD->State)为就绪状态和当前线程(ETHREAD->ProcessReadyQueue)链所指向的
进程就绪队列为 TRUE。然后判断要切换的进程(EPROCESS->State)状态是否已经交换出
当前内存,如果为其他状态则直接跳转到 KiSwapThread()函数来切换当前线程。如果已
交换出当前内存状态则将要切换的进程(EPROCESS->State)状态设置为Transition 并将
要切换进程(EPROCESS->SwapListEntry)的交换链(SwapListEntry 是 LIST_ENTRY 结
构)插入到全局进程输入交换链 KiProcessInSwapListHead 中 (KiProcessInSwapList
Head 是 LIST_ENTRY 结构),继续设置全局交换事件状态(KiSwapEvent.Header.Signal
State),判断全局交换事件等待链头(KiSwapEvent.Header.WaitListHead)是否为空如
果不为空则需要调用 KiWaitTest() 函数来测试全局交换事件 (KiSwapEvent)余额。最
后调用 KiSwapThread()  来完成线程切换并跳转到函数结束处。


_______________________________________________________________________________________



下面是 KiMoveApcState() 函数实现细节


0008:8042C3FC  MOV     EDX,[ESP+04]  
0008:8042C400  MOV     EAX,[ESP+08]
0008:8042C404  PUSH    ESI
0008:8042C405  PUSH    EDI
0008:8042C406  PUSH    06
0008:8042C408  MOV     ESI,EDX  注释:EDX,ESI = 当前线程(ETHREAD->ApcState)
0008:8042C40A  POP     ECX
0008:8042C40B  MOV     EDI,EAX  注释:EAX,EDI = 当前线程(ETHREAD->SavedApcState)
0008:8042C40D  REPZ MOVSD

当前线程的 ETHREAD->SavedApcState 与 ETHREAD->ApcState 都是 KAPC_STATE 结构,而
KAPC_STATE 结构又是一个双项链表 LIST_ENTRY 结构(LIST_ENTRY 结构在 DDK 头文件中
有定义)构成的数组 ApcListHead[2] ,数组织分别标识处了内核模式(ApcListHead[KernelMode]
)与用户模式(ApcListHead[UserMode])的 APC 链。以上代码是将 ETHREAD->ApcState 复制
到 ETHREAD->SavedApcState 中去。

0008:8042C40F  MOV     ESI,[EDX]
0008:8042C411  CMP     ESI,EDX  

0008:8042C413  JNZ    8042C431  

判断当前线程(ETHREAD->ApcState[KernelMode])内核模式的 APC 链是否为空,如果不为
空的话则跳转到保存内核模式 APC 双项链 ApcState[KernelMode] 到 SavedApcState[KernelMode]
中。

0008:8042C415  MOV     [EAX+04],EAX
0008:8042C418  MOV     [EAX],EAX

如果当前线程(ETHREAD->ApcState[KernelMode])内核模式的 APC 链为空,则初始化当
前线程(ETHREAD->SavedApcState[KernelMode])的 SavedApcState 链表保存内核模式的
APC 状态。

0008:8042C41A  MOV     ESI,[EDX+08]
0008:8042C41D  LEA     ECX,[EDX+08]
0008:8042C420  CMP     ESI,ECX
0008:8042C422  JNZ    8042C440     

继续比较当前线程(ETHREAD->ApcState[UserMode])用户模式的 APC 链是否为空,如果
不为空的话则跳转到保存用户模式 APC 双项链 ApcState[UserMode] 到 SavedApcState
[UserMode]中。

0008:8042C424  LEA     ECX,[EAX+08]  
0008:8042C427  MOV     [EAX+0C],ECX
0008:8042C42A  MOV     [ECX],ECX

如果当前线程(ETHREAD->ApcState[UserMode])用户模式的 APC 链为空,则初始化当前
线程(ETHREAD->SavedApcState[UserMode])的 SavedApcState 链表保存用户模式的 APC 状态。

0008:8042C42C  POP     EDI
0008:8042C42D  POP     ESI
0008:8042C42E  RET     0008

函数结束,返回。

0008:8042C431  MOV     ECX,[EDX+04]  
0008:8042C434  MOV     [EAX],ESI
0008:8042C436  MOV     [EAX+04],ECX
0008:8042C439  MOV     [ESI+04],EAX
0008:8042C43C  MOV     [ECX],EAX
0008:8042C43E  JMP    8042C41A   

设置内核模式 APC 双项链 ApcState[KernelMode].FLink 与 ApcState[KernelMode].BLink
到 SavedApcState[KernelMode].FLink 和 SavedApcState[KernelMode].BLink 中。然后
跳转到继续比较当前线程(ETHREAD->ApcState[UserMode])用户模式的 APC 链是否为空
处。


0008:8042C440  MOV     EDX,[EDX+0C]  
0008:8042C443  LEA     ECX,[EAX+08]
0008:8042C446  MOV     [EAX+0C],EDX
0008:8042C449  MOV     [ECX],ESI
0008:8042C44B  MOV     [ESI+04],ECX
0008:8042C44E  MOV     [EDX],ECX
0008:8042C450  JMP    8042C42C

设置用户模式 APC 双项链 ApcState[UserMode].FLink 与 ApcState[UserMode].BLink
到 SavedApcState[UserMode].FLink 和 SavedApcState[UserMode].BLink 中。然后跳转
到函数结束处。



_______________________________________________________________________________________



下面是 KiSwapProcess 函数实现细节

:u 8040438c l 100

0008:8040438C  MOV     EDX,[ESP+04]
0008:80404390  TEST    WORD PTR [EDX+20],FFFF 注释: EPROCESS->LdtDescriptor
0008:80404396  JNZ    804043BC
0008:80404398  XOR     EAX,EAX
0008:8040439A  LLDT    AX
0008:8040439D  MOV     ECX,[FFDFF040]  注释: ECX = KPCR->KTSS
0008:804043A3  XOR     EAX,EAX
0008:804043A5  MOV     GS,AX
0008:804043A8  MOV     EAX,[EDX+18]   注释:EAX = EPROCESS->DirectoryTableBase
0008:804043AB  MOV     [ECX+1C],EAX
0008:804043AE  MOV     CR3,EAX
0008:804043B1  MOV     AX,[EDX+30]
0008:804043B5  MOV     [ECX+66],AX
0008:804043B9  RET     0008

得到要切换进程的 LDT 描述符(EPROCESS->LdtDescriptor)与页目录表(EPROCESS->
DirectoryTableBase),判断要切换的进程 LDT 不为空,则跳转到设置 INT 21 处继续
运行,否则用要切换进程的页目录来刷新 KTSS 中的 CR3 值与当前 CR3 值,并用要切
换进程的 IOPM 来填充 KTSS 中的 IOPM。完成切换,函数返回。


0008:804043BC  MOV     ECX,[FFDFF03C]  注释:ECX = KPCR->KGDT
0008:804043C2  MOV     EAX,[EDX+20]
0008:804043C5  MOV     [ECX+48],EAX
0008:804043C8  MOV     EAX,[EDX+24]
0008:804043CB  MOV     [ECX+4C],EAX
0008:804043CE  MOV     ECX,[FFDFF038]  注释:ECX = KPCR->KIDT
0008:804043D4  MOV     EAX,[EDX+28]
0008:804043D7  MOV     [ECX+00000108],EAX
0008:804043DD  MOV     EAX,[EDX+2C]
0008:804043E0  MOV     [ECX+0000010C],EAX
0008:804043E6  MOV     EAX,00000048
0008:804043EB  JMP    8040439A

从 KPCR 中取得 KGDT 的位置,并从 KGDT 中索引到 LDT,把当前进程(EPROCESS)中的
LDT 赋与 KPCR 中的 LDT。再得到 KPCR 中 KIDT 的位置,把当前进程(EPROCESS)中的
INT 21 中断赋与 KPCR 中 KIDT 中的相应位置,并跳转到设置 LDT 处使当前进程可以使
用 INT 21 。


笔记是今年春节利用放假时间写的,当时分析到一半才发现原来 WIN2K 源代码中已
经包含了此部分,无奈已经把汇编进行了简单的注释,索性就这样写下去。错误之处再
所难免,还望得到您的指正。


参考资源: Windows 2000 源代码
感谢 FlashSky,SoBeIt 与我探讨。



WSS(Whitecell Security Systems),一个非营利性民间技术组织,致力于各种系统安全技术的研究。坚持传统的hacker精神,追求技术的精纯。
WSS 主页:http://www.whitecell.org/
WSS 论坛:http://www.whitecell.org/forums/
http://iittss.com/ kijs与牛人在一起不是有理由的让自己变懒,那是为了让视野更开阔

TOP

发新话题