??此系列是本人一个字一个字码出来的,包括示例和实验截图。由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新。 如有好的建议,欢迎反馈。码字不易,如果本篇文章有帮助你的,如有闲钱,可以打赏支持我的创作。如想转载,请把我的转载信息附在文章后面,并声明我的个人信息和本人博客地址即可,但必须事先通知我。
你如果是从中间插过来看的,请仔细阅读 羽夏看Win系统内核——简述 ,方便学习本教程。
??看此教程之前,问几个问题,基础知识储备好了吗?保护模式篇学会了吗?练习做完了吗?没有的话就不要继续了。
?
?? 华丽的分割线 ??
?
??由于是仅仅分析挂靠时该函数是如何备份和恢复APC队列的,为了缩短篇幅增加可读性,我会尽可能使用IDA翻译的伪代码,你的伪代码结果应该和我的不一样,因为我进行了一些重命名操作。我们先定位到NtReadVirtualMemory这个伪代码:
NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead){ _KTHREAD *v5; // edi PSIZE_T v6; // ebx int v8; // [esp+10h] [ebp-28h] BYREF PVOID Object; // [esp+14h] [ebp-24h] BYREF KPROCESSOR_MODE AccessMode[4]; // [esp+18h] [ebp-20h] NTSTATUS v11; // [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; } v8 = 0; v11 = 0; if ( NumberOfBytesToRead ) { v11 = ObReferenceObjectByHandle(ProcessHandle, 0x10u, PsProcessType, AccessMode[0], &Object, 0); if ( !v11 ) { v11 = MmCopyVirtualMemory( Object, BaseAddress, v5->ApcState.Process, Buffer, NumberOfBytesToRead, AccessMode[0], &v8); ObfDereferenceObject(Object); } } if ( v6 ) { *v6 = v8; ms_exc.registration.TryLevel = -1; } return v11;}??我们可以看到,该函数实现内存拷贝是通过MmCopyVirtualMemory这个函数实现的,我们点击去看看:
NTSTATUS __stdcall MmCopyVirtualMemory(PEX_RUNDOWN_REF RunRef, int a2, PRKPROCESS KPROCESS, volatile void *Address, SIZE_T Length, KPROCESSOR_MODE AccessMode, int a7){ struct _KPROCESS *v8; // ebx PRKPROCESS kprocess; // ecx NTSTATUS res; // esi struct _EX_RUNDOWN_REF *RunRefa; // [esp+8h] [ebp+8h] if ( !Length ) return 0; v8 = RunRef; kprocess = RunRef; if ( RunRef == KeGetCurrentThread()->ApcState.Process ) kprocess = KPROCESS; RunRefa = &kprocess[1].ProfileListHead.Blink; if ( !ExAcquireRundownProtection(&kprocess[1].ProfileListHead.Blink) ) return STATUS_PROCESS_IS_TERMINATING; if ( Length <= 0x1FF ) goto LABEL_10; res = MiDoMappedCopy(v8, a2, KPROCESS, Address, Length, AccessMode, a7); if ( res == STATUS_WORKING_SET_QUOTA ) { *a7 = 0;LABEL_10: res = MiDoPoolCopy(v8, a2, KPROCESS, Address, Length, AccessMode, a7); } ExReleaseRundownProtection(RunRefa); return res;}??你可能看到一个新奇的函数ExAcquireRundownProtection,这个函数是申请一个锁,从网上查阅翻译过来是停运保护(RundownProtection)锁,名字怪怪的听起来怪怪的。
??这个不涉及我们的核心,我们继续分析,发现它内部又是通过MiDoMappedCopy实现进程内存读取的:
NTSTATUS __stdcall MiDoMappedCopy(PRKPROCESS PROCESS, int a2, PRKPROCESS a3, volatile void *Address, SIZE_T Length, KPROCESSOR_MODE AccessMode, int a7){ // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] v13 = 0; v22 = a2; v17 = Address; 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 ( v22 == a2 && AccessMode ) { v20 = 1; if ( Length && (a2 + Length < a2 || a2 + Length > MmUserProbeAddress) ) ExRaiseAccessViolation(); v20 = 0; } MemoryDescriptorList.Next = 0; MemoryDescriptorList.Size = 4 * (((v22 & 0xFFF) + v19 + 0xFFF) >> 12) + 28; MemoryDescriptorList.MdlFlags = 0; MemoryDescriptorList.StartVa = (v22 & 0xFFFFF000); MemoryDescriptorList.ByteOffset = v22 & 0xFFF; MemoryDescriptorList.ByteCount = v19; MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess); v12 = 1; BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0, 0, 0x20u); if ( !BaseAddress ) { v13 = 1; ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES); } KeUnstackDetachProcess(&ApcState); KeStackAttachProcess(a3, &ApcState); if ( v22 == a2 ) { if ( AccessMode ) { v20 = 1; ProbeForWrite(Address, Length, 1u); v20 = 0; } } v11 = 1; qmemcpy(v17, BaseAddress, v19); ms_exc.registration.TryLevel = -1; KeUnstackDetachProcess(&ApcState); MmUnmapLockedPages(BaseAddress, &MemoryDescriptorList); MmUnlockPages(&MemoryDescriptorList); Length_1 -= v19; v22 += v19; v17 += v19; } *a7 = Length; return STATUS_SUCCESS;}??经过分析,发现与APC备份恢复的都是在进程挂靠相关函数上:KeStackAttachProcess和KeUnstackDetachProcess。我们先看看KeStackAttachProcess:
void __stdcall KeStackAttachProcess(PRKPROCESS PROCESS, PRKAPC_STATE ApcState){ _KTHREAD *CurrentThread; // esi char PROCESSa; // [esp+10h] [ebp+8h] CurrentThread = KeGetCurrentThread(); if ( KeGetPcr()->PrcbData.DpcRoutineActive ) KeBugCheckEx( 5u, PROCESS, CurrentThread->ApcState.Process, CurrentThread->ApcStateIndex, KeGetPcr()->PrcbData.DpcRoutineActive); if ( CurrentThread->ApcState.Process == PROCESS ) { ApcState->Process = 1; } else { PROCESSa = KeRaiseIrqlToDpcLevel(); if ( CurrentThread->ApcStateIndex ) { KiAttachProcess(CurrentThread, PROCESS, PROCESSa, ApcState); } else { KiAttachProcess(CurrentThread, PROCESS, PROCESSa, &CurrentThread->SavedApcState); ApcState->Process = 0; } }}??重点我们来看看ApcStateIndex,上一篇我们讲过,当正常状态为0,挂靠状态为1.也就是说,他将会走如下代码:
KiAttachProcess(CurrentThread, PROCESS, PROCESSa, ApcState);??点击去看看里面有啥代码:
void __stdcall KiAttachProcess(_KTHREAD *thread, PRKPROCESS Process, KIRQL irql, PRKAPC_STATE ApcState){ // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] ++Process->StackCount; KiMoveApcState(&thread->ApcState, ApcState); InitializeListHead(thread->ApcState.ApcListHead); InitializeListHead(&thread->ApcState.ApcListHead[1]); thread->ApcState.Process = Process; thread->ApcState.KernelApcInProgress = 0; thread->ApcState.KernelApcPending = 0; thread->ApcState.UserApcPending = 0; if ( ApcState == &thread->SavedApcState ) { thread->ApcStatePointer[0] = &thread->SavedApcState; thread->ApcStatePointer[1] = &thread->ApcState; thread->ApcStateIndex = 1; } if ( Process->State ) { thread->State = 1; thread->ProcessReadyQueue = 1; v9 = Process->ReadyListHead.Blink; thread->WaitListEntry.Flink = &Process->ReadyListHead; thread->WaitListEntry.Blink = v9; v9->Flink = &thread->WaitListEntry; Process->ReadyListHead.Blink = &thread->WaitListEntry; if ( Process->State == 1 ) { Process->State = 2; v10 = KiProcessInSwapListHead; v11 = &Process->SwapListEntry; Processa = &Process->SwapListEntry; ApcStatea = KiProcessInSwapListHead; do { v11->Next = v10; v12 = v10; v10 = ApcStatea; _ECX = &KiProcessInSwapListHead; _EDX = Processa; __asm { cmpxchg [ecx], edx } } while ( ApcStatea != v12 ); KiSetSwapEvent(); } thread->WaitIrql = irql; KiSwapThread(); } else { v4 = &Process->ReadyListHead; while ( 1 ) { v8 = v4->Flink; if ( v4->Flink == v4 ) break; v5 = v8->Flink; v6 = v8 - 12; v7 = v8->Blink; v7->Flink = v5; v5->Blink = v7; BYTE1(v6[37].Flink) = 0; KiReadyThread(v6); } KiSwapProcess(Process, ApcState->Process); KiUnlockDispatcherDatabase(irql); }}??我们就可以看到里面与APC备份相关操作了:
if ( ApcState == &thread->SavedApcState ) { thread->ApcStatePointer[0] = &thread->SavedApcState; thread->ApcStatePointer[1] = &thread->ApcState; thread->ApcStateIndex = 1; }??我们再来看看KeUnstackDetachProcess这个函数:
void __stdcall KeUnstackDetachProcess(PRKAPC_STATE ApcState){ PRKAPC_STATE v1; // ebx _KTHREAD *CurrentThread; // esi _KPROCESS *CurrentProcess; // edi int v4; // eax int v7; // ecx _KAPC_STATE *v8; // [esp-Ch] [ebp-20h] int v9; // [esp+4h] [ebp-10h] int v10; // [esp+Ch] [ebp-8h] signed __int8 v11; // [esp+13h] [ebp-1h] v1 = ApcState; if ( ApcState->Process != 1 ) { CurrentThread = KeGetCurrentThread(); v11 = KeRaiseIrqlToDpcLevel(); if ( !CurrentThread->ApcStateIndex || CurrentThread->ApcState.KernelApcInProgress || CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->ApcState || CurrentThread->ApcState.ApcListHead[1].Flink != &CurrentThread->ApcState.ApcListHead[1] ) { KeBugCheck(6u); } CurrentProcess = CurrentThread->ApcState.Process; if ( !--CurrentProcess->StackCount && CurrentProcess->ThreadListHead.Flink != &CurrentProcess->ThreadListHead ) { CurrentProcess->State = 3; v4 = KiProcessOutSwapListHead; v10 = KiProcessOutSwapListHead; do { CurrentProcess->SwapListEntry.Next = v4; v9 = v4; v4 = v10; _ECX = &KiProcessOutSwapListHead; _EDX = &CurrentProcess->SwapListEntry; __asm { cmpxchg [ecx], edx } } while ( v10 != v9 ); KiSetSwapEvent(); v1 = ApcState; } v8 = &CurrentThread->ApcState; if ( v1->Process ) { KiMoveApcState(v1, v8); } else { KiMoveApcState(&CurrentThread->SavedApcState, v8); CurrentThread->SavedApcState.Process = 0; CurrentThread->ApcStatePointer[0] = &CurrentThread->ApcState; CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState; CurrentThread->ApcStateIndex = 0; } if ( CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->ApcState ) { LOBYTE(v7) = 1; CurrentThread->ApcState.KernelApcPending = 1; HalRequestSoftwareInterrupt(v7); } KiSwapProcess(CurrentThread->ApcState.Process, CurrentProcess); KiUnlockDispatcherDatabase(v11); }}??我们很快找到了与APC恢复相关的代码:
if ( v1->Process ) { KiMoveApcState(v1, v8); } else { KiMoveApcState(&CurrentThread->SavedApcState, v8); CurrentThread->SavedApcState.Process = 0; CurrentThread->ApcStatePointer[0] = &CurrentThread->ApcState; CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState; CurrentThread->ApcStateIndex = 0; }??分析至此,本题就结束了。
??还记着 APC 篇——备用 APC 队列 提供的第一题的参考代码中的一行注释了吗?
DWORD WINAPI ThreadProc(VOID* Param){ for (int i =0 ;i<100;i++) { SleepEx(1000,TRUE); //思考为什么? //Sleep(1000); printf("Running\n"); } return 0;}??为什么我用SleepEx函数而不是用Sleep吗?你思考这个问题了吗?我们来看看下面几个图:
??我们将SleepEx函数用Sleep替换,并注释掉主函数的Sleep看看效果:

??APC正常被执行,接下来我们去掉注释掉主函数的Sleep,继续运行看看:

??这次竟然发现APC没有执行,到底是为什么呢?我们改回原答案,就可以正常执行APC了,也就是我在参考中给的效果图:

??原因将会在本篇后部分进行揭晓。
??无论是正常状态还是挂靠状态,都有两个APC队列,一个内核队列,一个用户队列。每当要挂入一个APC函数时,不管是内核APC还是用户APC,内核都要准备一个KAPC的数据结构,并且将这个KAPC结构挂到相应的APC队列中。现在我们看看KAPC的结构:
kd> dt _KAPCntdll!_KAPC +0x000 Type : Int2B +0x002 Size : Int2B +0x004 Spare0 : Uint4B +0x008 Thread : Ptr32 _KTHREAD +0x00c ApcListEntry : _LIST_ENTRY +0x014 KernelRoutine : Ptr32 void +0x018 RundownRoutine : Ptr32 void +0x01c NormalRoutine : Ptr32 void +0x020 NormalContext : Ptr32 Void +0x024 SystemArgument1 : Ptr32 Void +0x028 SystemArgument2 : Ptr32 Void +0x02c ApcStateIndex : Char +0x02d ApcMode : Char +0x02e Inserted : UChar??指明结构体的类型,APC类型为0x12。
??该结构体的大小,值为0x30。
??指向目标线程的线程结构体的指针,因为任何一个APC都是让目标线程进行完成。
??APC队列挂的位置。
??指向一个函数,调用ExFreePoolWithTag释放APC。
??存储着用户APC总入口或真正的内核APC函数地址,里面具体的细节将会在后面的文章进行介绍。
??当为内核APC,该成员存储着NULL;如果为用户APC,则为真正的APC函数。
??APC函数的参数。
??APC函数的参数。
??挂哪个队列,有四个值:0、1、2、3,里面的细节将在后面进行介绍。
??指示该APC是内核APC还是用户APC。
??表示本APC是否已挂入队列。挂入前值为0,挂入后值为1。
??为了方便理解,我们先撸一下函数大体调用流程:
??其中QueueUserAPC这个函数位于kernel32.dll,它会调用内核模块的NtQueueApcThread进行实现,经历过重重调用,使用KeInitializeApc为APC结构体分配内存并进行初始化,调用KeInsertQueueApc进行插入到指定队列,而插入最终由KiInsertQueueApc实现。
??为了做好本篇练习,我们先过一下KeInitializeApc的相关说明:
VOID KeInitializeApc( IN PKAPC Apc, //KAPC 指针 IN PKTHREAD Thread, //目标线程 IN KAPC_ENVIRONMENT TargetEnvironment, //四种状态 IN PKKERNEL_ROUTINE KernelRoutine, //销毁 KAPC 的函数地址 IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL, IN PKNORMAL_ROUTINE NormalRoutine, //用户 APC 总入口或者内核 APC 函数 IN KPROCESSOR_MODE Mode,//要插入用户 APC 队列还是内核 APC 队列 IN PVOID Context//内核APC:NULL,用户APC:真正的APC函数) ??该成员与KTHREAD + 0x165偏移处的属性同名,但含义不一样。该ApcStateIndex有四个值,如下面表格所示:
| 值 | 含义 |
|---|---|
| 0 | 原始环境 |
| 1 | 挂靠环境 |
| 2 | 当前环境 |
| 3 | 插入APC时的当前环境 |
??前两个值挺好理解,当值为0时,就是指线程的“亲生父母”;如果值为1时,就是指自己的“养父母”。后面的两个值比较绕,下面将会详细解释一下:
??上一篇我们说过,线程在正常情况下ApcStatePointer[0]指向ApcState,ApcStatePointer[1]指向SavedApcState;而在挂靠情况下ApcStatePointer[0]指向SavedApcState,ApcStatePointer[1]指向ApcState。当值为2的时候,插入的是当前进程的队列。什么是当前队列,是我不管你环境是挂靠还是不挂靠,我就插入当前进程的APC队列里面,以初始化APC的时候为基准。还剩下最玄学的一个值,当值为3时,插入的是当前进程的APC队列,此时有修复ApcStateIndex的操作,以插入APC的时候为基准。
??为了降低本篇思考题难度,我把该函数的调用流程说一下:
KAPC结构中的ApcStateIndex找到对应的APC队列KAPC结构中的ApcMode确定是用户队列还是内核队列KAPC挂到对应的队列中(挂到KAPC的ApcListEntry处)KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态KAPC_STATE结构中的KernelApcPending/UserApcPending??Alertable属性位于KTHREAD当中,如下所示:
kd> dt _KTHREADntdll!_KTHREAD ... +0x164 Alertable : UChar ...??我们可以发现很多与线程相关的结尾带Ex的函数的参数都会有一个bAlertable,举例如下:
DWORD SleepEx( DWORD dwMilliseconds, // time-out interval BOOL bAlertable // early completion option);DWORD WaitForSingleObjectEx( HANDLE hHandle, // handle to object DWORD dwMilliseconds, // time-out interval BOOL bAlertable // alertable option);??该值指示线程是否运行被APC吵醒,我们开头说QueueUserAPC 引发的血案解决办法就是由该属性捣的鬼。当该属性为0时,当前插入的用户APC函数未必有机会执当UserApcPending = 0时就会无法执行插入的APC,如果Alertable = 1 ,就会使UserApcPending = 1,从而将目标线程唤醒,从等待链表中被摘出来,并挂到调度链表当中执行。
本节的答案将会在下一节进行讲解,务必把本节练习做完后看下一个讲解内容。不要偷懒,实验是学习本教程的捷径。
??俗话说得好,光说不练假把式,如下是本节相关的练习。如果练习没做好,就不要看下一节教程了,越到后面,不做练习的话容易夹生了,开始还明白,后来就真的一点都不明白了。本节练习不多,请保质保量的完成,本篇参考将会在正文给出。
1?? 逆向分析QueueUserAPC完整的调用流程。
2?? 如果在一个无法被唤醒的线程插入一个APC,然后紧接又插入一个,如果设置线程可被唤醒,那么它会执行几个APC呢?请用代码论证。
??APC 篇—— APC 执行
