APC篇——APC挂入

博客 分享
0 214
张三
张三 2022-01-28 16:54:40
悬赏:0 积分 收藏

APC 篇—— APC 挂入

APC 篇之 APC 挂入,详细介绍与 APC 挂入相关的基础知识和简单分析流程。

写在前面

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

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

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

?


?? 华丽的分割线 ??


?

NtReadVirtualMemory 分析

??由于是仅仅分析挂靠时该函数是如何备份和恢复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备份恢复的都是在进程挂靠相关函数上:KeStackAttachProcessKeUnstackDetachProcess。我们先看看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;   }

??分析至此,本题就结束了。

QueueUserAPC 引发的血案

??还记着 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了,也就是我在参考中给的效果图:

??原因将会在本篇后部分进行揭晓。

KAPC

??无论是正常状态还是挂靠状态,都有两个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

Type

??指明结构体的类型,APC类型为0x12

Size

??该结构体的大小,值为0x30

Thread

??指向目标线程的线程结构体的指针,因为任何一个APC都是让目标线程进行完成。

ApcListEntry

??APC队列挂的位置。

KernelRoutine

??指向一个函数,调用ExFreePoolWithTag释放APC

NormalRoutine

??存储着用户APC总入口或真正的内核APC函数地址,里面具体的细节将会在后面的文章进行介绍。

NormalContext

??当为内核APC,该成员存储着NULL;如果为用户APC,则为真正的APC函数。

SystemArgument1

??APC函数的参数。

SystemArgument2

??APC函数的参数。

ApcStateIndex

??挂哪个队列,有四个值:0、1、2、3,里面的细节将在后面进行介绍。

ApcMode

??指示该APC是内核APC还是用户APC

Inserted

??表示本APC是否已挂入队列。挂入前值为0,挂入后值为1。

挂入流程

??为了方便理解,我们先撸一下函数大体调用流程:

graph TD QueueUserAPC--用户层调用--> NtQueueApcThread -.->KeInitializeApc -.->KeInsertQueueApc-.->KiInsertQueueApc

??其中QueueUserAPC这个函数位于kernel32.dll,它会调用内核模块的NtQueueApcThread进行实现,经历过重重调用,使用KeInitializeApcAPC结构体分配内存并进行初始化,调用KeInsertQueueApc进行插入到指定队列,而插入最终由KiInsertQueueApc实现。

KeInitializeApc 函数说明

??为了做好本篇练习,我们先过一下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函数) 

ApcStateIndex 详解

??该成员与KTHREAD + 0x165偏移处的属性同名,但含义不一样。该ApcStateIndex有四个值,如下面表格所示:

含义
0原始环境
1挂靠环境
2当前环境
3插入APC时的当前环境

??前两个值挺好理解,当值为0时,就是指线程的“亲生父母”;如果值为1时,就是指自己的“养父母”。后面的两个值比较绕,下面将会详细解释一下:

??上一篇我们说过,线程在正常情况下ApcStatePointer[0]指向ApcStateApcStatePointer[1]指向SavedApcState;而在挂靠情况下ApcStatePointer[0]指向SavedApcStateApcStatePointer[1]指向ApcState。当值为2的时候,插入的是当前进程的队列。什么是当前队列,是我不管你环境是挂靠还是不挂靠,我就插入当前进程的APC队列里面,以初始化APC的时候为基准。还剩下最玄学的一个值,当值为3时,插入的是当前进程的APC队列,此时有修复ApcStateIndex的操作,以插入APC的时候为基准。

KiInsertQueueApc 调用流程

??为了降低本篇思考题难度,我把该函数的调用流程说一下:

  1. 根据KAPC结构中的ApcStateIndex找到对应的APC队列
  2. 再根据KAPC结构中的ApcMode确定是用户队列还是内核队列
  3. KAPC挂到对应的队列中(挂到KAPCApcListEntry处)
  4. 再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态
  5. 修改KAPC_STATE结构中的KernelApcPending/UserApcPending

Alertable 详解

??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 执行

知识共享许可协议
知识共享许可协议
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
本文来自博客园,作者:寂静的羽夏 ,一个热爱计算机技术的菜鸟
转载请注明原文链接:https://www.cnblogs.com/wingsummer/p/15853079.html
posted @ 2022-01-28 16:28 寂静的羽夏 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    张三

    张三 (王者 段位)

    821 积分 (2)粉丝 (41)源码

     

    温馨提示

    亦奇源码

    最新会员