进程线程篇——总结与提升(上)

写在前面

  此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我

你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。

  看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?系统调用篇学会了吗?练习做完了吗?没有的话就不要继续了。


🔒 华丽的分割线 🔒


模拟线程切换分析

  之前我们详细分析了模拟线程切换的本质,里面模拟了两处线程切换,一处模拟了时钟切换,一处模拟了主动切换,也就是通过API的方式进行的切换。
  我们来看一下模拟时钟切换的部分:

while(TRUE) {
    Sleep(20);
    Scheduling();
}

  上面的Sleep(20)就是代表我每20ms继续调用Scheduling()来实现线程切换,也就是说时钟周期是20ms
  模拟通过API的方式进行的主动切换的就是如下函数:

void GMSleep(int MilliSeconds)
{
   GMThread_t* GMThreadp;
   GMThreadp = &GMThreadList[CurrentThreadIndex];
   if (GMThreadp->Flags != 0) {
      GMThreadp->Flags = GMTHREAD_SLEEP;
      GMThreadp->SleepMillsecondDot = GetTickCount() + MilliSeconds;
   }

   Scheduling();
   return;
}

  而我们的每一个线程,都会调用GMSleep这个函数,这个函数模拟的就是模拟调用WinAPI

void Thread1(void*) {
    while(1){
        printf("Thread1\n");
        GMSleep(500);
    }
}

  我假设上面的你都搞明白了。那么,接着上篇的课后练习,怎样实现线程的挂起和恢复呢?这个问题你思考了吗?没思考的话就不要继续了。

线程挂起恢复分析实现

  为了实现线程的挂起恢复函数,我们应该把思路放在它是如何实现线程调度上。让线程挂起,无非就是不让给这个线程CPU时间了,看看下面负责调度的函数是怎样找到线程的:

void Scheduling(void)
{
    int i;
    int TickCount;
    GMThread_t* SrcGMThreadp;
    GMThread_t* DstGMThreadp;
    TickCount = GetTickCount();
    SrcGMThreadp = &GMThreadList[CurrentThreadIndex];
    DstGMThreadp = &GMThreadList[0];    
    for (i = 1; GMThreadList[i].name; i++) {
        if (GMThreadList[i].Flags & GMTHREAD_SLEEP) {
            if (TickCount > GMThreadList[i].SleepMillsecondDot) {
                GMThreadList[i].Flags = GMTHREAD_READY;
            }
        }
        if (GMThreadList[i].Flags & GMTHREAD_READY) {
            DstGMThreadp = &GMThreadList[i];
            break;
        }
    }   
    CurrentThreadIndex = DstGMThreadp - GMThreadList;
    SwitchContext(SrcGMThreadp, DstGMThreadp);
    return;
}

  可以看到,只要模拟线程是GMTHREAD_READY状态,它就会调换线程,也就是说我们改一改这个标志线程状态的参数一改,就能实现我想要的挂起和恢复,我们来实现它。
  首先,我们在ThreadSwitch.h来添加两处声明:

bool SyspendThread(char* Name);
bool ResumeThread(char* Name);

  然后我们对它进行实现:

bool SyspendThread(char* Name)
{
    for (int i=1;i<MAXGMTHREAD;i++)
    {
        if (!strcmp(Name,GMThreadList[i].name))
        {
            GMThreadList[i].Flags = GMTHREAD_EXIT;
            return true;
        }
    }
    return false;
}

bool ResumeThread(char* Name)
{
    for (int i=1;i<MAXGMTHREAD;i++)
    {
        if (!strcmp(Name,GMThreadList[i].name))
        {
            GMThreadList[i].Flags = GMTHREAD_READY;
            return true;
        }
    }
    return false;
}

  为了方便观察,我们只保留了Thread1Thread2,最终main.cpp的内容如下:

#include "stdafx.h"
#include <windows.h>
#include "ThreadSwitch.h"
 
extern int CurrentThreadIndex;
 
extern GMThread_t GMThreadList[MAXGMTHREAD];
void Thread1(void*) {
    while(1){
        printf("Thread1\n");
        GMSleep(500);
    }
}
void Thread2(void*) {
    while (1) {
        printf("Thread2\n");
        GMSleep(500);
    }
}

int main()
{
    RegisterGMThread("Thread1", Thread1, NULL);
    RegisterGMThread("Thread2", Thread2, NULL);

    //SyspendThread("Thread2");
    //ResumeThread("Thread2");

    while(TRUE) {
        Sleep(20);
        Scheduling();
    }

    return 0;
}

  我们接下来以动图的形式进行演示:

  上一篇留下的第0题到此就解决完毕了。

SwapContext 分析

  本分析对于逆向水平有一定的要求,如果不行的话建议做多一些有关IDACrakeMe练习,分析流程以做熟悉和练习。不过没有经验也无所谓,仔细看看本部分,回去重新做一遍。
  当你找到这个函数的是哦胡,你看到的应该是下面的情况:

SwapContext     proc near               ; CODE XREF: KiUnlockDispatcherDatabase(x)+72↑p
                                        ; KiSwapContext(x)+29↑p ...
                or      cl, cl
                mov     byte ptr es:[esi+2Dh], 2
                pushf

loc_46A8E8:                             ; CODE XREF: KiIdleLoop()+5A↓j
                mov     ecx, [ebx]      ; ebx = KPCR
                cmp     dword ptr [ebx+994h], 0
                push    ecx
                jnz     loc_46AA2D
                cmp     ds:_PPerfGlobalGroupMask, 0
                jnz     loc_46AA04

loc_46A905:                             ; CODE XREF: SwapContext+12C↓j
                                        ; SwapContext+13D↓j ...
                mov     ebp, cr0
                mov     edx, ebp
                mov     cl, [esi+2Ch]
                mov     [ebx+50h], cl

  既然分析它的参数是什么,首先我们得知道它们是如何传参的,传的是什么,这样我们才能解决上一篇留下的思考题。
  然后我们找到它的来自KiSwapContext的一个引用,结果如下:

; __fastcall KiSwapContext(x)
@KiSwapContext@4 proc near              ; CODE XREF: KiSwapThread()+41↑p

var_200FE4      = dword ptr -200FE4h
var_10          = dword ptr -10h
var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4

                sub     esp, 10h
                mov     [esp+10h+var_4], ebx
                mov     [esp+10h+var_8], esi
                mov     [esp+10h+var_C], edi
                mov     [esp+10h+var_10], ebp
                mov     ebx, ds:0FFDFF01Ch ; ebx = &_KPCR
                mov     esi, ecx        ; esi = ecx = NextReadyThread
                mov     edi, [ebx+124h]
                mov     [ebx+124h], esi
                mov     cl, [edi+58h]
                call    SwapContext
                mov     ebp, [esp+10h+var_10]
                mov     edi, [esp+10h+var_C]
                mov     esi, [esp+10h+var_8]
                mov     ebx, [esp+10h+var_4]
                add     esp, 10h
                retn
@KiSwapContext@4 endp

  但是这样也看不出来参数是什么,我们再往上找一级:

@KiSwapThread@0 proc near               ; CODE XREF: KiAttachProcess(x,x,x,x)+F2↑p
                                        ; KeDelayExecutionThread(x,x,x):loc_42279A↑p ...
                mov     edi, edi
                push    esi
                push    edi
                db      3Eh
                mov     eax, ds:0FFDFF020h
                mov     esi, eax

  我们找到了KiSwapThread这个函数,明显望文生义就是用来切换线程用的内核函数。前面我们知道0xFFDFF000这个地址存放的就是KPCR结构体的地址,那么根据结构体可以知道0xFFDFF020存放的就是KPRCB这个结构体的首地址,最终分析得到如下结果:

@KiSwapThread@0 proc near               ; CODE XREF: KiAttachProcess(x,x,x,x)+F2↑p
                                        ; KeDelayExecutionThread(x,x,x):loc_42279A↑p ...
                mov     edi, edi
                push    esi
                push    edi
                db      3Eh
                mov     eax, ds:0FFDFF020h
                mov     esi, eax
                mov     eax, [esi+_KPRCB.NextThread] ; eax = NextThread
                test    eax, eax        ; 测一测有没有
                mov     edi, [esi+_KPRCB.CurrentThread] ; edi = CurrentThread
                jz      short loc_429CAC ; 没有 NextThread 就跳
                and     [esi+_KPRCB.NextThread], 0 ; 把 NextThread 清零
                jmp     short loc_429CCF
; ---------------------------------------------------------------------------

loc_429CAC:                             ; CODE XREF: KiSwapThread()+14↑j
                push    ebx
                movsx   ebx, [esi+_KPRCB.Number]
                xor     edx, edx
                mov     ecx, ebx
                call    @KiFindReadyThread@8 ; KiFindReadyThread(x,x)
                test    eax, eax
                jnz     short loc_429CCE ; 如果找到了下一个线程就跳走
                mov     eax, [esi+_KPRCB.IdleThread]
                xor     edx, edx
                inc     edx
                mov     ecx, ebx
                shl     edx, cl
                or      _KiIdleSummary, edx

loc_429CCE:                             ; CODE XREF: KiSwapThread()+2C↑j
                pop     ebx

loc_429CCF:                             ; CODE XREF: KiSwapThread()+1A↑j
                mov     ecx, eax        
                call    @KiSwapContext@4 ; KiSwapContext(x)

  经过简单的分析,我们很容易地判断出执行到KiSwapContext前的ecx为一个线程结构体,如下图所示:

  根据符号显示KiSwapContext是只有一个参数,经过简单的分析可以得到下面的结果:

; __fastcall KiSwapContext(x)
@KiSwapContext@4 proc near              ; CODE XREF: KiSwapThread()+41↑p

var_200FE4      = dword ptr -200FE4h
var_10          = dword ptr -10h
var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4

                sub     esp, 10h
                mov     [esp+10h+var_4], ebx
                mov     [esp+10h+var_8], esi
                mov     [esp+10h+var_C], edi
                mov     [esp+10h+var_10], ebp
                mov     ebx, ds:0FFDFF01Ch ; ebx = &_KPCR
                mov     esi, ecx        ; esi = ecx = NextReadyThread
                mov     edi, [ebx+_KPCR.PrcbData.CurrentThread]
                mov     [ebx+_KPCR.PrcbData.CurrentThread], esi
                mov     cl, [edi+_KTHREAD.WaitIrql]
                call    SwapContext
                mov     ebp, [esp+10h+var_10]
                mov     edi, [esp+10h+var_C]
                mov     esi, [esp+10h+var_8]
                mov     ebx, [esp+10h+var_4]
                add     esp, 10h
                retn
@KiSwapContext@4 endp

  执行到SwapContext这个函数前,esi成了下一个切换新线程,而edi成了需要被切换的老线程,而ebxKPCR结构体,也就是说,改函数一共有3个参数,每一个参数的含义我们都已经知道了,我们利用IDAF5也可以得到一定的验证:

char __usercall SwapContext@<al>(int *a1@<ebx>, int a2@<edi>, int a3@<esi>)

  接下来我们分析一下在哪里实现了线程切换。既然操作系统线程切换是基于堆栈的,esp换了,必然导致线程的切换,我们很容易跟到下面的汇编:

loc_46A94C:                             ; CODE XREF: SwapContext+67↑j
                mov     ecx, [ebx+_KPCR.TSS]
                mov     [ecx+_KTSS.Esp0], eax
                mov     esp, [esi+_KTHREAD.KernelStack]
                mov     eax, [esi+_KTHREAD.Teb]
                mov     [ebx+_KPCR.NtTib.Self], eax

  经历过堆栈的弹出恢复操作,再调用retn,即可完成线程的切换。接下来我们看看什么时候切换的CR3

                mov     eax, [edi+_KTHREAD.ApcState.Process]
                cmp     eax, [esi+_KTHREAD.ApcState.Process]
                mov     [edi+_KTHREAD.IdleSwapBlock], 0
                jz      short loc_46A994
                mov     edi, [esi+_KTHREAD.ApcState.Process]
                test    word ptr [edi+_KTHREAD.Teb], 0FFFFh
                jnz     short loc_46A9CE
                xor     eax, eax

loc_46A975:                             ; CODE XREF: SwapContext+117↓j
                lldt    ax
                xor     eax, eax
                mov     gs, eax
                assume gs:GAP
                mov     eax, [edi+_EPROCESS.Pcb.DirectoryTableBase]
                mov     ebp, [ebx+_KPCR.TSS]
                mov     ecx, dword ptr [edi+_KTHREAD.Iopl]
                mov     [ebp+_KTSS.CR3], eax
                mov     cr3, eax
                mov     [ebp+_KTSS.IoMapBase], cx
                jmp     short loc_46A994
; ---------------------------------------------------------------------------
                align 4

loc_46A994:                             ; CODE XREF: SwapContext+86↑j
                                        ; SwapContext+AF↑j
                mov     eax, [ebx+_KPCR.NtTib.Self]

  经过分析,切换CR3是需要条件的,它会判断新的线程和老的线程的Process是不是一样的,然后决定是否处理:

mov     eax, [edi+_KTHREAD.ApcState.Process]
cmp     eax, [esi+_KTHREAD.ApcState.Process]
mov     [edi+_KTHREAD.IdleSwapBlock], 0
jz      short loc_46A994

  如果不相同的话,就会执行下面的代码切换CR3

mov     eax, [edi+_EPROCESS.Pcb.DirectoryTableBase]
mov     ebp, [ebx+_KPCR.TSS]
mov     ecx, dword ptr [edi+_KTHREAD.Iopl]
mov     [ebp+_KTSS.CR3], eax
mov     cr3, eax

  看明白到这个地方的时候,第2题就解决了。一个CPU一套寄存器,也就说明里面只能存储一个TSS地址的寄存器,那么中断门提权时TSS中存储的一定是当前线程的ESP0SS0吗?我们接下来分析一下:

                mov     eax, [esi+_KTHREAD.InitialStack]
                mov     ecx, [esi+_KTHREAD.StackLimit]
                sub     eax, 210h
                mov     [ebx+_KPCR.NtTib.StackLimit], ecx
                mov     [ebx+_KPCR.NtTib.StackBase], eax
                xor     ecx, ecx
                mov     cl, [esi+_KTHREAD.NpxState]
                and     edx, 0FFFFFFF1h
                or      ecx, edx
                or      ecx, [eax+20Ch]
                cmp     ebp, ecx
                jnz     loc_46A9FC
                lea     ecx, [ecx+0]

loc_46A940:                             ; CODE XREF: SwapContext+11F↓j
                test    dword ptr [eax-1Ch], 20000h ; 检查是否为虚拟8086模式
                jnz     short loc_46A94C
                sub     eax, 10h

loc_46A94C:                             ; CODE XREF: SwapContext+67↑j
                mov     ecx, [ebx+_KPCR.TSS]
                mov     [ecx+_KTSS.Esp0], eax ; 将修正好的栈顶放入到 TSS 中
                mov     esp, [esi+_KTHREAD.KernelStack]
                mov     eax, [esi+_KTHREAD.Teb]

  可以看出在mov esp, [esi+_KTHREAD.KernelStack]之前,上面的代码已经处理好并修正ESP0,所以中断门提权时TSS中存储的一定是当前线程的ESP0SS0。至此,第3题解答完毕。
  在我测试的虚拟机中,fs的段选择子都是0x3B,但为什么不同的线程段选择子指向的TEB却不一样呢?是因为它直接修改了GDT表的内容,使它指向的地址是我们现在线程的TEB,代码如下所示:

mov     eax, [ebx+_KPCR.NtTib.Self]
mov     ecx, [ebx+_KPCR.GDT]
mov     [ecx+3Ah], ax
shr     eax, 10h
mov     [ecx+3Ch], al
mov     [ecx+3Fh], ah

  至此,第4题解答完毕。我们来看看0环的ExceptionList在哪里备份的:

pop     ecx
mov     [ebx+_KPCR.NtTib.ExceptionList], ecx

  第5题也就解决了,它把新线程的ExceptionList存于KPCR中。下面我们来看看IdleThread如何查找:
  我们知道IdleThread存储于KPCR之中,我们通过结构体的方式进行查询,找到该成员存储的地址。

kd> dt _KPCR  0xffdff000 
nt!_KPCR
   +0x000 NtTib            : _NT_TIB
   +0x01c SelfPcr          : 0xffdff000 _KPCR
   +0x020 Prcb             : 0xffdff120 _KPRCB
   +0x024 Irql             : 0x1c ''
   +0x028 IRR              : 4
   +0x02c IrrActive        : 0
   +0x030 IDR              : 0xffff20f8
   +0x034 KdVersionBlock   : 0x80546ab8 Void
   +0x038 IDT              : 0x8003f400 _KIDTENTRY
   +0x03c GDT              : 0x8003f000 _KGDTENTRY
   +0x040 TSS              : 0x80042000 _KTSS
   +0x044 MajorVersion     : 1
   +0x046 MinorVersion     : 1
   +0x048 SetMember        : 1
   +0x04c StallScaleFactor : 0x64
   +0x050 DebugActive      : 0 ''
   +0x051 Number           : 0 ''
   +0x052 Spare0           : 0 ''
   +0x053 SecondLevelCacheAssociativity : 0 ''
   +0x054 VdmAlert         : 0
   +0x058 KernelReserved   : [14] 0
   +0x090 SecondLevelCacheSize : 0
   +0x094 HalReserved      : [16] 0
   +0x0d4 InterruptMode    : 0
   +0x0d8 Spare1           : 0 ''
   +0x0dc KernelReserved2  : [17] 0
   +0x120 PrcbData         : _KPRCB

kd> dx -id 0,0,805539a0 -r1 ((ntkrnlpa!_KPRCB *)0xffdff120)
((ntkrnlpa!_KPRCB *)0xffdff120)                 : 0xffdff120 [Type: _KPRCB *]
    [+0x000] MinorVersion     : 0x1 [Type: unsigned short]
    [+0x002] MajorVersion     : 0x1 [Type: unsigned short]
    [+0x004] CurrentThread    : 0x80553740 [Type: _KTHREAD *]
    [+0x008] NextThread       : 0x0 [Type: _KTHREAD *]
    [+0x00c] IdleThread       : 0x80553740 [Type: _KTHREAD *]
    [+0x010] Number           : 0 [Type: char]
    [+0x011] Reserved         : 0 [Type: char]

  然后我们dt一下这个结构体:

kd> dt _ETHREAD 0x80553740
ntdll!_ETHREAD
   +0x000 Tcb              : _KTHREAD
   +0x1c0 CreateTime       : _LARGE_INTEGER 0x0
   +0x1c0 NestedFaultCount : 0y00
   +0x1c0 ApcNeeded        : 0y0
   +0x1c8 ExitTime         : _LARGE_INTEGER 0x0
   +0x1c8 LpcReplyChain    : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x1c8 KeyedWaitChain   : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x1d0 ExitStatus       : 0n0
   +0x1d0 OfsChain         : (null) 
   +0x1d4 PostBlockList    : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x1dc TerminationPort  : (null) 
   +0x1dc ReaperLink       : (null) 
   +0x1dc KeyedWaitValue   : (null) 
   +0x1e0 ActiveTimerListLock : 0
   +0x1e4 ActiveTimerListHead : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x1ec Cid              : _CLIENT_ID
   +0x1f4 LpcReplySemaphore : _KSEMAPHORE
   +0x1f4 KeyedWaitSemaphore : _KSEMAPHORE
   +0x208 LpcReplyMessage  : (null) 
   +0x208 LpcWaitingOnPort : (null) 
   +0x20c ImpersonationInfo : (null) 
   +0x210 IrpList          : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x218 TopLevelIrp      : 0
   +0x21c DeviceToVerify   : (null) 
   +0x220 ThreadsProcess   : (null) 
   +0x224 StartAddress     : (null) 
   +0x228 Win32StartAddress : (null) 
   +0x228 LpcReceivedMessageId : 0
   +0x22c ThreadListEntry  : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x234 RundownProtect   : _EX_RUNDOWN_REF
   +0x238 ThreadLock       : _EX_PUSH_LOCK
   +0x23c LpcReplyMessageId : 0
   +0x240 ReadClusterSize  : 0
   +0x244 GrantedAccess    : 0x1f03ff
   +0x248 CrossThreadFlags : 0
   +0x248 Terminated       : 0y0
   +0x248 DeadThread       : 0y0
   +0x248 HideFromDebugger : 0y0
   +0x248 ActiveImpersonationInfo : 0y0
   +0x248 SystemThread     : 0y0
   +0x248 HardErrorsAreDisabled : 0y0
   +0x248 BreakOnTermination : 0y0
   +0x248 SkipCreationMsg  : 0y0
   +0x248 SkipTerminationMsg : 0y0
   +0x24c SameThreadPassiveFlags : 0
   +0x24c ActiveExWorker   : 0y0
   +0x24c ExWorkerCanWaitUser : 0y0
   +0x24c MemoryMaker      : 0y0
   +0x250 SameThreadApcFlags : 0
   +0x250 LpcReceivedMsgIdValid : 0y0
   +0x250 LpcExitThreadCalled : 0y0
   +0x250 AddressSpaceOwner : 0y0
   +0x254 ForwardClusterOnly : 0 ''
   +0x255 DisablePageFaultClustering : 0 ''

  在上面的结构体中+0x224 StartAddress存储的就是线程开始执行的地址,也就是我们在3环调用CreateThread传递的函数地址。但是对于这个线程比较特殊,直接全为空,那么我们如何找到函数地址呢?
  程序执行的时候,一定会用到堆栈。我们可以通过堆栈就可以定位程序的行为。我们把关注点放到KTHREADKernelStack上。涉及该成员的操作存在于线程切换中,我们来看看与堆栈操作相关的局部汇编代码:

pushf
mov     ecx, [ebx]      ; ebx = KPCR
cmp     [ebx+_KPCR.PrcbData.DpcRoutineActive], 0
push    ecx ;KPCR.NtTib.ExceptionList
……
mov     esp, [esi+_KTHREAD.KernelStack]
……
pop     ecx
xor     eax, eax
retn

  先看看堆栈地址的地址是啥:

kd> dt _KTHREAD 0x80553740
ntdll!_KTHREAD
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListHead   : _LIST_ENTRY [ 0x80553750 - 0x80553750 ]
   +0x018 InitialStack     : 0x8054af00 Void
   +0x01c StackLimit       : 0x80547f00 Void
   +0x020 Teb              : (null) 
   +0x024 TlsArray         : (null) 
   +0x028 KernelStack      : 0x8054ac4c Void
   +0x02c DebugActive      : 0 ''
   +0x02d State            : 0x2 ''
   +0x02e Alerted          : [2]  ""
   +0x030 Iopl             : 0 ''
   +0x031 NpxState         : 0xa ''
   +0x032 Saturation       : 0 ''
   +0x033 Priority         : 16 ''
   +0x034 ApcState         : _KAPC_STATE
   +0x04c ContextSwitches  : 0x1736
   +0x050 IdleSwapBlock    : 0 ''
   +0x051 Spare0           : [3]  ""
   +0x054 WaitStatus       : 0n0
   +0x058 WaitIrql         : 0x2 ''
   +0x059 WaitMode         : 0 ''
   +0x05a WaitNext         : 0 ''
   +0x05b WaitReason       : 0 ''
   +0x05c WaitBlockList    : 0x805537b0 _KWAIT_BLOCK
   +0x060 WaitListEntry    : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x060 SwapListEntry    : _SINGLE_LIST_ENTRY
   +0x068 WaitTime         : 0x555
   +0x06c BasePriority     : 0 ''
   +0x06d DecrementCount   : 0 ''
   +0x06e PriorityDecrement : 0 ''
   +0x06f Quantum          : -17 ''
   +0x070 WaitBlock        : [4] _KWAIT_BLOCK
   +0x0d0 LegoData         : (null) 
   +0x0d4 KernelApcDisable : 0
   +0x0d8 UserAffinity     : 0xffffffff
   +0x0dc SystemAffinityActive : 0 ''
   +0x0dd PowerState       : 0 ''
   +0x0de NpxIrql          : 0 ''
   +0x0df InitialNode      : 0 ''
   +0x0e0 ServiceTable     : 0x80553fa0 Void
   +0x0e4 Queue            : (null) 
   +0x0e8 ApcQueueLock     : 0
   +0x0f0 Timer            : _KTIMER
   +0x118 QueueListEntry   : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x120 SoftAffinity     : 1
   +0x124 Affinity         : 1
   +0x128 Preempted        : 0 ''
   +0x129 ProcessReadyQueue : 0 ''
   +0x12a KernelStackResident : 0x1 ''
   +0x12b NextProcessor    : 0 ''
   +0x12c CallbackStack    : (null) 
   +0x130 Win32Thread      : (null) 
   +0x134 TrapFrame        : (null) 
   +0x138 ApcStatePointer  : [2] 0x80553774 _KAPC_STATE
   +0x140 PreviousMode     : 0 ''
   +0x141 EnableStackSwap  : 0x1 ''
   +0x142 LargeStack       : 0 ''
   +0x143 ResourceIndex    : 0 ''
   +0x144 KernelTime       : 0x2d1a
   +0x148 UserTime         : 0
   +0x14c SavedApcState    : _KAPC_STATE
   +0x164 Alertable        : 0 ''
   +0x165 ApcStateIndex    : 0 ''
   +0x166 ApcQueueable     : 0x1 ''
   +0x167 AutoAlignment    : 0 ''
   +0x168 StackBase        : 0x8054af00 Void
   +0x16c SuspendApc       : _KAPC
   +0x19c SuspendSemaphore : _KSEMAPHORE
   +0x1b0 ThreadListEntry  : _LIST_ENTRY [ 0x805539f0 - 0x805539f0 ]
   +0x1b8 FreezeCount      : 0 ''
   +0x1b9 SuspendCount     : 0 ''
   +0x1ba IdealProcessor   : 0 ''
   +0x1bb DisableBoost     : 0 ''

  再看看堆栈长什么样子:

kd> dd 0x8054ac4c
8054ac4c  00000000 ffdff980 80542af0 00000000
8054ac5c  0000000e 00000000 00000000 00000000
8054ac6c  00000000 00000000 00000000 00000000
8054ac7c  00000000 00000000 00000000 00000000
8054ac8c  00000000 00000000 00000000 00000000
8054ac9c  00000000 00000000 00000000 00000000
8054acac  00000000 00000000 00000000 00000000
8054acbc  00000000 00000000 00000000 00000000

  也就是说,第一个就是ExceptionList,第二个就是Eflag,第三个就是切换线程后跳转的地址,在这里也就是IdleThread继续走的地址。这个地址肯定就在IdleThread当中。我们u一下:

kd> u 80542af0
ReadVirtual: 80542af0 not properly sign extended
80542af0 fb              sti
ReadVirtual: 80542b00 not properly sign extended
80542af1 90              nop
ReadVirtual: 80542b01 not properly sign extended
80542af2 90              nop
ReadVirtual: 80542b02 not properly sign extended
80542af3 fa              cli
ReadVirtual: 80542b03 not properly sign extended
80542af4 3b6d00          cmp     ebp,dword ptr [ebp]
80542af7 740d            je      nt!KiIdleLoop+0x26 (80542b06)
80542af9 b102            mov     cl,2
80542afb ff15a8864d80    call    dword ptr [nt!_imp_HalClearSoftwareInterrupt (804d86a8)]

  也就是函数是KiIdleLoop,我们通过IDA看看该函数:

; _DWORD __cdecl KiIdleLoop()
@KiIdleLoop@0   proc near               ; CODE XREF: KiSystemStartup(x)+2E2↓j
                lea     ebp, [ebx+980h]
                jmp     short loc_46AAF0
; ---------------------------------------------------------------------------

loc_46AAE8:                             ; CODE XREF: KiIdleLoop()+2D↓j
                lea     ecx, [ebx+0C50h]
                call    dword ptr [ecx]

loc_46AAF0:                             ; CODE XREF: KiIdleLoop()+6↑j
                                        ; KiIdleLoop()+65↓j
                sti
                nop
                nop
                cli
                cmp     ebp, [ebp+0]
                jz      short loc_46AB06
                mov     cl, 2
                call    ds:__imp_@HalClearSoftwareInterrupt@4 ; HalClearSoftwareInterrupt(x)
                call    KiRetireDpcList

loc_46AB06:                             ; CODE XREF: KiIdleLoop()+17↑j
                cmp     dword ptr [ebx+128h], 0
                jz      short loc_46AAE8
                sti
                mov     esi, [ebx+128h]
                mov     edi, [ebx+124h]
                or      ecx, 1
                mov     [ebx+124h], esi
                mov     byte ptr es:[esi+2Dh], 2
                mov     dword ptr [ebx+128h], 0
                push    offset loc_46AB3F
                pushf
                jmp     loc_46A8E8
; ---------------------------------------------------------------------------

loc_46AB3F:                             ; DATA XREF: KiIdleLoop()+54↑o
                lea     ebp, [ebx+980h]
                jmp     short loc_46AAF0
@KiIdleLoop@0   endp

  这个函数没有任何意义,就是让CPU别闲着,执行一波无任何意义的代码。至此第6题解决。
  KiFindReadyThread分析这块涉及算法,经查阅是通过二分法进行查找的。算法实现原理是我的知识盲区,我仅把流程说一下:首先该函数会解析KiReadySummary,找到从左起第一个为1的位数,再用该位获取从KiDispatchReadListHead中的第一个_KTHREAD线程,将其从链表中摘除再判断如果摘除后该链表为空,则找到相应的KiReadySummary位将其置0,然后将对应找到的线程结构体返回。至于其中的详细细节,可以参考这位博友的分析:KiFindReadyThread分析 - 查找下一个就绪线程 。如下是IDA的伪C代码,仅供参考:

PETHREAD __fastcall KiFindReadyThread(ULONG ProcessorNumber, KPRIORITY LowPriority)
{
  int v2; // ecx
  unsigned int v3; // eax
  unsigned int v4; // edx
  int v5; // edx
  int v6; // eax
  _LIST_ENTRY *v7; // esi
  PETHREAD ReadyThread; // eax
  _LIST_ENTRY *v9; // ecx
  _LIST_ENTRY *v10; // edi

  v2 = 16;
  v3 = KiReadySummary & ~((1 << LowPriority) - 1);
  v4 = HIWORD(v3);
  if ( !HIWORD(v3) )
  {
    v2 = 0;
    v4 = v3;
  }
  if ( (v4 & 0xFFFFFF00) != 0 )
    v2 += 8;
  v5 = v2 + KiFindFirstSetLeft[v3 >> v2];
  v6 = v3 << (31 - v5);
  v7 = &KiDispatcherReadyListHead[2 * v5];
  if ( !v6 )
    return 0;
  while ( v6 >= 0 )
  {
    LOBYTE(v5) = v5 - 1;
    --v7;
    v6 *= 2;
    if ( !v6 )
      return 0;
  }
  v9 = v7->Flink->Flink;
  ReadyThread = &v7->Flink[-12];
  v10 = ReadyThread->WaitListEntry.Blink;
  v10->Flink = v9;
  v9->Blink = v10;
  if ( IsListEmpty(v7) )
    KiReadySummary &= ~(1 << v5);
  return ReadyThread;
}

  至此第7题解决。
  模拟线程切换与Windows的线程切换有哪些区别?真正的线程有两个栈,一个内核0环的栈,一个是3环的栈,发生线程切换在0环;模拟线程切换没用到FS、异常列表之类的东西,其他的区别可以自行总结。
  接下来是最后一题,我们走一下时钟中断的流程,中断都是在IDT表中的,首先我们跟着走一下,首先定位该表,只需g_IDT,效果如下所示:

_IDT            dd offset _KiTrap00     ; DATA XREF: KiSystemStartup(x)+1D5↑o
                db 0, 8Eh
word_5B8B02     dw 8                    ; DATA XREF: KiSwapIDT()↓o
                dd offset _KiTrap01
                dd 88E00h
                dd offset _KiTrap02
                dd 88E00h
                dd offset _KiTrap03
                dd 8EE00h
                dd offset _KiTrap04
                dd 8EE00h
                dd offset _KiTrap05
                dd 88E00h
                dd offset _KiTrap06
                dd 88E00h
                dd offset _KiTrap07
                dd 88E00h
                dd offset _KiTrap08
                dd 88E00h
                dd offset _KiTrap09

  时钟中断的中断号是0x30,我们定位到这个函数,如何定位呢?看下面的图:

  然后跳转到这个函数:

; _DWORD __stdcall KiStartUnexpectedRange()
_KiStartUnexpectedRange@0 proc near     ; DATA XREF: KiGetVectorInfo(x,x)+68↑o
                                        ; INIT:005B8C7C↓o
                push    30h ; '0'
                jmp     _KiEndUnexpectedRange@0 ; KiEndUnexpectedRange()
_KiStartUnexpectedRange@0 endp

  我们看看跳转到哪里:

; _DWORD __stdcall KiEndUnexpectedRange()
_KiEndUnexpectedRange@0 proc near       ; CODE XREF: KiStartUnexpectedRange()+5↑j
                                        ; _KiUnexpectedInterrupt1+5↑j ...
                jmp     cs:off_46632E
_KiEndUnexpectedRange@0 endp

; ---------------------------------------------------------------------------
off_46632E      dd offset _KiUnexpectedInterruptTail
                                        ; DATA XREF: KiEndUnexpectedRange()↑r

  继续跟着,为了节约篇幅,只保留调用流程部分:

_KiUnexpectedInterruptTail proc near    ; CODE XREF: KiEndUnexpectedRange()↑j
                                        ; DATA XREF: .text:off_46632E↑o

……

loc_466E7E:                             ; CODE XREF: Dr_kui_a+10↑j
                                        ; Dr_kui_a+7C↑j
                inc     dword ptr ds:0FFDFF5C4h
                mov     ebx, [esp+68h+var_68]
                sub     esp, 4
                push    esp
                push    ebx
                push    1Fh
                call    ds:__imp__HalBeginSystemInterrupt@12 ; HalBeginSystemInterrupt(x,x,x)
                or      eax, eax
                jnz     short loc_466E9D
                add     esp, 8
                jmp     short loc_466EEC
; ---------------------------------------------------------------------------

loc_466E9D:                             ; CODE XREF: _KiUnexpectedInterruptTail+BF↑j
                cli
                call    ds:__imp__HalEndSystemInterrupt@8 ; HalEndSystemInterrupt(x,x)
                jmp     short Kei386EoiHelper@0 ; Kei386EoiHelper()
_KiUnexpectedInterruptTail endp
                public HalEndSystemInterrupt
HalEndSystemInterrupt proc near         ; CODE XREF: sub_80010EF0+E8↑p
                                        ; sub_80017144+B3↓p
                                        ; DATA XREF: ...

arg_0           = byte ptr  4

                movzx   ecx, [esp+arg_0]
                cmp     byte ptr ds:0FFDFF024h, 2
                jbe     short loc_8001123E
                mov     eax, ds:dword_800176EC[ecx*4]
                or      eax, ds:0FFDFF030h
                out     21h, al         ; Interrupt controller, 8259A.
                shr     eax, 8
                out     0A1h, al        ; Interrupt Controller #2, 8259A

loc_8001123E:                           ; CODE XREF: HalEndSystemInterrupt+C↑j
                mov     ds:0FFDFF024h, cl
                mov     eax, ds:0FFDFF028h
                mov     al, ds:byte_80017784[eax]
                cmp     al, cl
                ja      short loc_80011256
                retn    8
; ---------------------------------------------------------------------------

loc_80011256:                           ; CODE XREF: HalEndSystemInterrupt+35↑j
                add     esp, 0Ch
                jmp     ds:pKiUnexpectedInterrupt[eax*4]
HalEndSystemInterrupt endp ; sp-analysis failed
pKiUnexpectedInterrupt dd offset KiUnexpectedInterrupt
                                        ; DATA XREF: HalEndSystemInterrupt+3D↑r
                                        ; sub_80011260+3D↑r
                dd offset sub_80016BDD
                dd offset sub_80016A45
sub_80016A45    proc near               ; CODE XREF: KfLowerIrql:loc_800110AC↑p
                                        ; KfReleaseSpinLock:loc_8001111C↑p ...
                push    dword ptr ds:0FFDFF024h
                mov     byte ptr ds:0FFDFF024h, 2
                and     dword ptr ds:0FFDFF028h, 0FFFFFFFBh
                sti
                call    ds:KiDispatchInterrupt
                cli
                call    sub_80011260
                jmp     ds:Kei386EoiHelper
sub_80016A45    endp
; _DWORD __stdcall KiDispatchInterrupt()
                 public _KiDispatchInterrupt@0
 _KiDispatchInterrupt@0 proc near        ; DATA XREF: .edata:off_58D2A8↓o

 var_C           = dword ptr -0Ch
 var_8           = dword ptr -8
 var_4           = dword ptr -4

                 mov     ebx, ds:0FFDFF01Ch ; a1
                 lea     eax, [ebx+980h]
                 cli
                 cmp     eax, [eax]
                 jz      short loc_46A85E
                 push    ebp
                 push    dword ptr [ebx]
                 mov     dword ptr [ebx], 0FFFFFFFFh
                 mov     edx, esp
                 mov     esp, [ebx+988h]
                 push    edx
                 mov     ebp, eax
                 call    KiRetireDpcList
                 pop     esp
                 pop     dword ptr [ebx]
                 pop     ebp

 loc_46A85E:                             ; CODE XREF: KiDispatchInterrupt()+F↑j
                 sti
                 cmp     dword ptr [ebx+9ACh], 0
                 jnz     short loc_46A8BE
                 cmp     dword ptr [ebx+128h], 0
                 jz      short locret_46A8BD
                 mov     eax, [ebx+128h]

 loc_46A877:                             ; CODE XREF: KiDispatchInterrupt()+9F↓j
                 sub     esp, 0Ch
                 mov     [esp+0Ch+var_4], esi
                 mov     [esp+0Ch+var_8], edi
                 mov     [esp+0Ch+var_C], ebp
                 mov     esi, eax        ; NewThread
                 mov     edi, [ebx+124h] ; oldThread
                 mov     dword ptr [ebx+128h], 0
                 mov     [ebx+124h], esi
                 mov     ecx, edi
                 mov     byte ptr [edi+50h], 1
                 call    @KiReadyThread@4 ; KiReadyThread(x)
                 mov     cl, 1
                 call    SwapContext
                 mov     ebp, [esp+0Ch+var_C]
                 mov     edi, [esp+0Ch+var_8]
                 mov     esi, [esp+0Ch+var_4]
                 add     esp, 0Ch

 locret_46A8BD:                          ; CODE XREF: KiDispatchInterrupt()+3F↑j
                 retn
 ; ---------------------------------------------------------------------------

 loc_46A8BE:                             ; CODE XREF: KiDispatchInterrupt()+36↑j
                 mov     dword ptr [ebx+9ACh], 0
                 call    _KiQuantumEnd@0 ; KiQuantumEnd()
                 or      eax, eax
                 jnz     short loc_46A877
                 retn
 _KiDispatchInterrupt@0 endp

进程挂靠

  在讲编程的时候,我们都听过:一个进程可以包含多个线程,一个进程至少要有一个线程。进程为线程提供资源,也就是提供Cr3的值,Cr3中存储的是页目录表基址,Cr3确定了,线程能访问的内存也就确定了。
  对于这一行代码:mov eax,dword ptr ds:[0x12345678]CPU如何解析这个地址呢?CPU解析线性地址时要通过页目录表来找对应的物理页,页目录表基址存在Cr3寄存器中。当前的Cr3的值来源于当前的进程结构体的_KPROCESS.DirectoryTableBase当中。那么进程挂靠又是怎么回事呢?我们先来看个结构体:

kd> dt _KTHREAD
ntdll!_KTHREAD
   ……
   +0x032 Saturation       : Char
   +0x033 Priority         : Char
   +0x034 ApcState         : _KAPC_STATE
   +0x04c ContextSwitches  : Uint4B
   +0x050 IdleSwapBlock    : UChar
   ……

kd> dt _KAPC_STATE
ntdll!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY
   +0x010 Process          : Ptr32 _KPROCESS
   +0x014 KernelApcInProgress : UChar
   +0x015 KernelApcPending : UChar
   +0x016 UserApcPending   : UChar

  ApcState这个成员我们在逆向线程切换的时候遇到过,也就是解决我们第2题的时候,这个结构体的Process成员就是存储的进程挂靠上的进程CR3。可以打一个比方,EPROCESSDirectoryTableBase存储的是亲父母,而ApcState存储的是养父母,我想要资源时从养父母来拿。正常情况下,CR3的值是由养父母提供的,但CR3的值也可以改成和当前线程毫不相干的其他进程的DirectoryTableBase。将当前CR3的值改为其他进程,称为“进程挂靠”。

跨进程内存读写

  跨进程内存读写根据之前所学肯定必须切换CR3,并且读取内存肯定会落实到类似如下汇编:

mov eax,dword ptr ds:[0x12345678]
mov dword ptr ds:[0x00401234],eax

  我们自己实现一个跨进程内存读写一个int还好说,如果是一个指定长度的Buffer,那咋办呢?
  我们都知道,应用程序的高2G的内核空间是共用的,也就是说,无论是哪个应用程序,高2G的内容寻址都能寻到的。那么我把读取进程的内存写到高2G的空间,然后切换CR3回去,然后重新把高2G缓存的东西写到指定Buffer中,我们就完成了。上面的读的操作,写得操作也是类似的。

跨进程读

跨进程写

  我们下面来简单分析一下Windows实现的跨进程读内存的函数NtReadVirtualMemory和跨进程写NtWriteVirtualMemory的函数,NtWriteVirtualMemoryNtReadVirtualMemory实现十分相似,我就只分析前者,下面的自行分析。为什么说是浅析是因为里面有大量的其他前置知识,比如APC和内存管理。三环怎么进内核的我就不再赘述了,为了方便。为了缩短篇幅增加可读性,我会尽可能使用IDA翻译的伪代码,你的伪代码结果应该和我的不一样,因为我进行了一些重命名操作。

NtReadVirtualMemory 浅析

  我们先定位到NtReadVirtualMemory这个伪代码:

NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
{
  _KTHREAD *v5; // edi
  PSIZE_T v6; // ebx
  int a7; // [esp+10h] [ebp-28h] BYREF
  PRKPROCESS PROCESS; // [esp+14h] [ebp-24h] BYREF
  KPROCESSOR_MODE AccessMode[4]; // [esp+18h] [ebp-20h]
  NTSTATUS res; // [esp+1Ch] [ebp-1Ch]
  CPPEH_RECORD ms_exc; // [esp+20h] [ebp-18h]

  v5 = KeGetCurrentThread();
  AccessMode[0] = v5->PreviousMode;
  if ( AccessMode[0] )
  {
    if ( BaseAddress + NumberOfBytesToRead < BaseAddress
      || Buffer + NumberOfBytesToRead < Buffer
      || BaseAddress + NumberOfBytesToRead > MmHighestUserAddress
      || Buffer + NumberOfBytesToRead > MmHighestUserAddress )
    {
      return 0xC0000005;
    }
    v6 = NumberOfBytesRead;
    if ( NumberOfBytesRead )
    {
      ms_exc.registration.TryLevel = 0;
      if ( NumberOfBytesRead >= MmUserProbeAddress )
        *MmUserProbeAddress = 0;
      *NumberOfBytesRead = *NumberOfBytesRead;
      ms_exc.registration.TryLevel = -1;
    }
  }
  else
  {
    v6 = NumberOfBytesRead;
  }
  a7 = 0;
  res = 0;
  if ( NumberOfBytesToRead )
  {
    res = ObReferenceObjectByHandle(ProcessHandle, 0x10u, PsProcessType, AccessMode[0], &PROCESS, 0);
    if ( !res )
    {
      res = MmCopyVirtualMemory(
              PROCESS,
              BaseAddress,
              v5->ApcState.Process,
              Buffer,
              NumberOfBytesToRead,
              AccessMode[0],
              &a7);
      ObfDereferenceObject(PROCESS);
    }
  }
  if ( v6 )
  {
    *v6 = a7;
    ms_exc.registration.TryLevel = -1;
  }
  return res;
}

  我们可以看到,该函数实现内存拷贝是通过MmCopyVirtualMemory这个函数实现的,我们点击去看看:

NTSTATUS __stdcall MmCopyVirtualMemory(PRKPROCESS PROCESS, PVOID BaseAddress, PRKPROCESS KPROCESS, char *buffer, SIZE_T Length, KPROCESSOR_MODE AccessMode, int *a7)
{
  PRKPROCESS process; // ebx
  _EPROCESS *eprocess; // ecx
  NTSTATUS res; // esi
  _EX_RUNDOWN_REF *RunRefa; // [esp+8h] [ebp+8h]

  if ( !Length )
    return 0;
  process = PROCESS;
  eprocess = PROCESS;
  if ( PROCESS == KeGetCurrentThread()->ApcState.Process )
    eprocess = KPROCESS;
  RunRefa = &eprocess->RundownProtect;
  if ( !ExAcquireRundownProtection(&eprocess->RundownProtect) )
    return STATUS_PROCESS_IS_TERMINATING;
  if ( Length <= 0x1FF )
    goto LABEL_10;
  res = MiDoMappedCopy(process, BaseAddress, KPROCESS, buffer, Length, AccessMode, a7);
  if ( res == STATUS_WORKING_SET_QUOTA )
  {
    *a7 = 0;
LABEL_10:
    res = MiDoPoolCopy(process, BaseAddress, KPROCESS, buffer, Length, AccessMode, a7);
  }
  ExReleaseRundownProtection(RunRefa);
  return res;
}

  你可能看到一个新奇的函数ExAcquireRundownProtection,这个函数是申请一个锁,从网上查阅翻译过来是停运保护(RundownProtection)锁,名字怪怪的听起来怪怪的。
  这个不涉及我们的核心,我们继续分析,发现它内部又是通过MiDoMappedCopy实现进程内存读取的:

NTSTATUS __stdcall MiDoMappedCopy(PRKPROCESS PROCESS, char *src, PRKPROCESS process, char *buffer, SIZE_T Length, KPROCESSOR_MODE AccessMode, int *a7)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v13 = 0;
  src_0 = src;
  buffer_1 = buffer;
  v7 = 0xE000;
  if ( Length <= 0xE000 )
    v7 = Length;
  v16 = &MemoryDescriptorList;
  Length_1 = Length;
  v19 = v7;
  v20 = 0;
  v14 = 0;
  v15 = 0;
  while ( Length_1 )
  {
    if ( Length_1 < v19 )
      v19 = Length_1;
    KeStackAttachProcess(PROCESS, &ApcState);
    BaseAddress = 0;
    v12 = 0;
    v11 = 0;
    ms_exc.registration.TryLevel = 0;
    if ( src_0 == src && AccessMode )
    {
      v20 = 1;
      if ( Length && (&src[Length] < src || &src[Length] > MmUserProbeAddress) )
        ExRaiseAccessViolation();
      v20 = 0;
    }
    MemoryDescriptorList.Next = 0;
    MemoryDescriptorList.Size = 4 * (((src_0 & 0xFFF) + v19 + 0xFFF) >> 12) + 28;
    MemoryDescriptorList.MdlFlags = 0;
    MemoryDescriptorList.StartVa = (src_0 & 0xFFFFF000);
    MemoryDescriptorList.ByteOffset = src_0 & 0xFFF;
    MemoryDescriptorList.ByteCount = v19;
    MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess);
    v12 = 1;
    BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0u, 0u, 0x20u);
    if ( !BaseAddress )
    {
      v13 = 1;
      ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
    }
    KeUnstackDetachProcess(&ApcState);
    KeStackAttachProcess(process, &ApcState);
    if ( src_0 == src )
    {
      if ( AccessMode )
      {
        v20 = 1;
        ProbeForWrite(buffer, Length, 1u);
        v20 = 0;
      }
    }
    v11 = 1;
    qmemcpy(buffer_1, BaseAddress, v19);
    ms_exc.registration.TryLevel = -1;
    KeUnstackDetachProcess(&ApcState);
    MmUnmapLockedPages(BaseAddress, &MemoryDescriptorList);
    MmUnlockPages(&MemoryDescriptorList);
    Length_1 -= v19;
    src_0 += v19;
    buffer_1 += v19;
  }
  *a7 = Length;
  return STATUS_SUCCESS;
}

  KeStackAttachProcessKeUnstackDetachProcess这俩函数与APC相关,在这里你可以简单理解就是切换CR3,实现进程挂靠和解除挂靠。我们注意一下下面的伪代码:

MemoryDescriptorList.Next = 0;
MemoryDescriptorList.Size = 4 * (((src_0 & 0xFFF) + v19 + 0xFFF) >> 12) + 28;
MemoryDescriptorList.MdlFlags = 0;
MemoryDescriptorList.StartVa = (src_0 & 0xFFFFF000);
MemoryDescriptorList.ByteOffset = src_0 & 0xFFF;
MemoryDescriptorList.ByteCount = v19;
MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess);
v12 = 1;
BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0u, 0u, 0x20u);

  MmMapLockedPagesSpecifyCache这个函数就是映射里面描述的物理页,下面是微软对该函数的描述:

The MmMapLockedPagesSpecifyCache routine maps the physical pages that are described by an MDL to a virtual address, and enables the caller to specify the cache attribute that is used to create the mapping.

  上面的操作就算锁住物理页,并把它重新映射到高2G地址,我们直接写到里面,就少了重新把高2G的内容重新写到程序空间的步骤了。

未完待续

  由于减轻加载负担,所以将一半的内容放到下一篇,如果想继续学习请点击下一篇的链接。

下一篇

  进程线程篇——总结与提升(下)

posted @ 2022-02-03 15:02  寂静的羽夏  阅读(718)  评论(0编辑  收藏  举报