Windows11_22H2下的APC机制分析

APC简介

APC即异步过程调用(Asyncroneous Procedure Call),是Windows系统中一种基于线程的机制,用于将要在特定线程环境下进行的操作排队,可以实现各种异步操作和多任务处理。

APC是基于线程异步调用,所以可以用来执行处于某特定线程环境才能做的操作。
例如,文件IO操作,某个线程正在读取一个文件,此时可以使用APC向特定线程插入一个中断APC,IRQL=1,在这个APC中可以完成已经读写成功的操作。

APC与DPC的区别

相关结构与成员

APC既然是基于线程的,那么在线程结构ETHREAD上就会有与其相关的成员。

_KAPC_STATE(ApcState)

表示APC状态。
位置:

nt!_KTHREAD
  ...
  +0x098 ApcState         : _KAPC_STATE
  ....

结构

nt!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY     //核APC在索引为0的链表里,用户APC在索引为1的链表里。
   +0x020 Process          : Ptr64 _KPROCESS     //当前进程。
   +0x028 InProgressFlags  : UChar               //APC是否正在执行,派遣APC时将此位置1,防止重入。
   +0x028 KernelApcInProgress : Pos 0, 1 Bit     
   +0x028 SpecialApcInProgress : Pos 1, 1 Bit
   +0x029 KernelApcPending : UChar               //是否有正在等待执行的内核APC。
   +0x02a UserApcPendingAll : UChar              //是否有正在等待执行的用户APC。
   +0x02a SpecialUserApcPending : Pos 0, 1 Bit   //特殊用户APC无论是否可以唤醒(alterable是否为1),都能打断 (XP,Win7,Win8没有此成员,从Win10_1809开始增加的)
   +0x02a UserApcPending   : Pos 1, 1 Bit

如果想让线程执行某些操作,可以将例程挂到ApcListHead链表里。

_KAPC_STATE(SaveApcState)

备用ApcState,在_KTHREAD + 0x258处。

nt!_KTHREAD
  ...
  +0x258 SavedApcState    : _KAPC_STATE
  ....

当线程挂靠时会用到此成员。
举个栗子,P1进程的T1线程,想要访问P2进程的内存,可以使用挂靠来进行切换T1线程所属进程为P2,当T1挂靠到P2后,ApcState.ApcListHead中存储的仍然是原来的APC,为了避免混淆,当T1挂靠P2后,会将ApcState中的值存储到SaveApcState中,等回到原进程P1时,再将ApcState恢复。

_KTHREAD的ApcStateIndex成员

用来标识当前线程处于什么状态:0:正常状态,1:挂靠状态。

_KAPC

APC结构,_KAPC_STATE.ApcListHead 链表就是把一个个此结构穿起来。
当要挂入一个APC函数时,内核需要准备一个_KAPC结构,并将其挂入到相应的链表中。

nt!_KAPC
   +0x000 Type             : UChar               //不同版本可能不同
   +0x001 AllFlags         : UChar
   +0x001 CallbackDataContext : Pos 0, 1 Bit
   +0x001 Unused           : Pos 1, 7 Bits
   +0x002 Size             : UChar               //不同版本可能不同
   +0x003 SpareByte1       : UChar 
   +0x004 SpareLong0       : Uint4B
   +0x008 Thread           : Ptr64 _KTHREAD
   +0x010 ApcListEntry     : _LIST_ENTRY         //_KAPC_STATE.ApcListHead链表挂入的地方。
   +0x020 KernelRoutine    : Ptr64     void      //内核APC
   +0x028 RundownRoutine   : Ptr64     void      //APC执行完之后执行此例程,一般用来做收尾工作
   +0x030 NormalRoutine    : Ptr64     void      //正常执行例程,并不完全等于APC函数地址。一般情况下,内核态下如果没有那么就是特殊内核APC(中断APC),用户态必须要有。
   +0x020 Reserved         : [3] Ptr64 Void
   +0x038 NormalContext    : Ptr64 Void
   +0x040 SystemArgument1  : Ptr64 Void
   +0x048 SystemArgument2  : Ptr64 Void
   +0x050 ApcStateIndex    : Char               // 0:原始环境;1:挂靠环境;2:当前环境,初始化APC时,判断KTHREAD.ApcStateIndex,如果为0,插入原始环境,如果为1,插入挂靠环境;3:插入APC时的当前环境,判断方法与2一样。
   +0x051 ApcMode          : Char
   +0x052 Inserted         : UChar             //当前APC是否已经插入到APC队列中

_KTHREAD的Alerted与Alertable成员

位置

nt!_KTHREAD
...
+0x072 Alerted          : [2] UChar
...
+0x074 Alertable        : Pos 4, 1 Bit
...

_KTHREAD.Alerted表示状态,即如果在等待时被唤醒了,那么就把此位置1。这个位影响了APC是否可以在线程等待的时候执行,如果使用WaitForSingleObjectEx,最后一个参数就是用来填写是否可以被唤醒。
_KTHREAD.Alertable表示是否能够唤醒,是否有唤醒的能力,在WaitForSingleObject中,是否可以被APC打断。

APC控制位

APC初始化分析

内核中使用KeInitializeApc初始化,

KeInitializeApc分析

函数声明:

VOID
KeInitializeApc (
    IN PRKAPC Apc,                                           //_KPAC结构指针
    IN PRKTHREAD Thread,                                     //目标线程
    IN KAPC_ENVIRONMENT Environment,                         //0、1、2、3四种状态,根据此值初始化_KAPC.ApcStateIndex
    IN PKKERNEL_ROUTINE KernelRoutine,                       //内核APC函数,初始化_KAPC.KernelRoutine
    IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,            // 初始化_KAPC.RundownRoutine 
    IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,              // 初始化_KAPC.NormalRoutine 
    IN KPROCESSOR_MODE ApcMode OPTIONAL,                     //要插入内核链表还是用户链表
    IN PVOID NormalContext OPTIONAL	                         //内核APC:NULL,用户APC函数
    )

顾名思义,就是初始化_KAPC结构。

_KAPC.Type_KAPC.Size 是操作系统规定好的,不同的版本可能不同。

如果参数Environment为2,则让Environment等于_KTHREAD.ApcStateIndex。根据_KTHREAD.ApcStateIndex的值去判断插入原始环境还是挂靠环境(0:原始环境,1:挂靠环境)。

如果参数NormalRoutine为0,那么_KAPC.ApcMode为0,如果参数NormalRoutine不为0的话,则_KAPC.ApcMode等于参数_ApcMode。

如果参数NormalRoutine为0,那么_KAPC.NormalContext就为0。

如果为内核模式,那么_KAPC.NormalContext为0。(这句话是从别的资料上看到的,但根据分析并不准确。)

_KAPC.Inserted设置为0,表示未插入状态。

_KAPC.AllFlags 设置为0。
伪代码如下:
KeInitializeApc伪代码

下面给出更详细的汇编分析。
KeInitializeApc汇编1
KeInitializeApc汇编2

APC插入分析

将APC挂入,通常调用KeInsertQueueApc。

KeInsertQueueApc

BOOLEAN
KeInsertQueueApc (
    __inout PRKAPC Apc,
    __in_opt PVOID SystemArgument1,
    __in_opt PVOID SystemArgument2,
    __in KPRIORITY Increment
    );

此函数主要做了:

加锁(好像是个自旋锁)。伪代码如下:
KeInsertQueueApc加锁

随后,如果_KTHREAD.MiscFlags&0x4000(ApcQueueable)不为0或参数Apc是未插入状态,
则将参数Apc设置为插入状态,填充Apc的两个参数指针,调用KiInsertQueueApc(插入APC的核心函数),调用KiSignalThreadForApc(根据APC的类型,检查是否应该向目标线程发送信号以及如何发送信号)。
伪代码如下:
KeInsertQueueApc2

之后,解锁,调用KiExitDispatcher(主要降低IRQL,也可以将线程上下文切换到其它线程,甚至可能是我们插入APC的线程,必须进行此调用,因为调度程序必须评估应该运行哪个线程,现在我们将一个APC排队给可能具有更高优先级的线程)。
随后,如果满足下面四个条件,则执行ETW日志记录,记录APC插入。
1.能够开启ETW。
2.APC插入线程的当前进程不是原始进程。
3._KAPC.ApcMode为用户模式或_KAPC.KernelRoutine==KeSpecialUserApcKernelRoutine
4._KTHREAD.u.ApcQueueable!=0(能够插入APC)或参数Apc是未插入状态。

之后,函数返回。

伪代码如下:
KeInsertQueueApc3

KiInsertQueueApc

此函数主要功能:将APC插入到_KTHREAD._KAPC_STATE

根据_KAPCApcStateIndexKTHREADApcStateIndex判断插入到ApcState还是SaveApcState
例如,如果_KAPC.ApcStateIndex为0(插入到原始环境),KTHREAD.ApcStateIndex为0(正常状态),则插入偏移为0x98,即KTHREAD.AcpState
KiInsertQueueApc1
如果_KAPC.NormalRoutine不为0。
如果为普通用户APC、ApcMode不为0、_KAPC.KernelRoutineKiSchedulerApcTerminate
设置ApcState的UserApcPending位为1,即设置有普通用户APC在等待执行。
这里ApcMode为1,将APC设置插入用户链表_KAPC_STATE.ApcListHead[1]
随后有个判断,如果头节点的下一个节点的上一个不是头节点,则跳转到LABEL_12处,调用 __fastfail快速失败机制退出。
头插法插入结点,然后函数返回。(这种APC与退出有关,所以挂入到头部。)
KiInsertQueueApc2

如果ApcMode=0或者为普通用户APC且_KAPC.KernelRoutine不为KeSpecialUserApcKernelRoutine(普通内核APC || 特殊用户APC || (普通用户APC && KernelRoutine != KeSpecialUserApcKernelRoutine) )。
ApcMode为0,APC插入_KAPC_STATE.ApcListHead[0]内核链表;ApcMode为1,则APC插入_KAPC_STATE.ApcListHead[1]用户链表。
这里也有个判断,跟上面那个差不多(如果头节点的上一个节点的下一个不是头节点,则跳转到LABEL_12处)。
尾插法插入结点,然后函数返回。
这里需要注意,用户特殊APC的ApcMode=0,插入的是内核链表。
KiInsertQueueApc3

到这里没有插入的话,遍历对应_APC_STATE.ApcListHead,将KTHREAD.ApcState的SpecialUserApcPending位设置为1,即有特殊用户APC在等待执行。
如果_KAPC.NormalRoutine不存在的话,遍历对应_APC_STATE.ApcListHead
KiInsertQueueApc4

然后,还会进行判断,跟上面的差不多(如果头节点的下一个节点的上一个不是当前节点,则跳转到LABEL_12处)。
再进行插入操作,头插法插入链表。(主要内核特殊APC
KiInsertQueueApc5

APC执行分析

通过函数KiDeliverApc执行APC。

KiDeliverApc

void __fastcall KiDeliverApc(char _PreviousMode, _KEXCEPTION_FRAME *_ExceptionFrame, _KTRAP_FRAME *_TrapFrame)

看一下此函数的交叉引用。
调用此函数挺频繁的。
KiDeliverApc交叉引用

如果参数_KTRAP_FRAME不为0的话,调用KiCheckForSListAddress(检查_KTRAP_FRAME的RIP是否在一定范围,保护栈的安全。这里不作详细解释,想了解具体可以去逆向,还是挺有意思的)。

KTHREADSpecialApcDisable位(+0x1E6处)为APC执行的开关。如果其为0,可以执行APC;如果其不为0,则不能执行APC。当然,KTHREAD.SpecialApcDisable==0 时是主要分析的。
将当前线程KTHREAD.TrapFrame设置为第三个参数的值。
将当前线程KTHREAD.ApcState.KernelApcPending设置为0,即没有内核APC在等待执行。
KiDeliverApc1

下面主要分析KTHREAD.SpecialApcDisable==0时(不为0时,做个检查就返回了。)。
有个while(1)循环来执行内核APC。
判断当前线程APC链表头的下一个结点是否为链表头(当前线程是否有需要执行的APC),如果有,继续往下执行;如果没有,则跳转至LABEL_15处(主要执行用户APC,后面会讲解)。
随后,提升当前IRQL到2,加锁,防止重入。
KiDeliverApc2

之后,再判断当前线程是否有需要执行的内核APC,如果没有的话,跳出while(1)循环;有的话,继续执行。
将当前线程KTHREAD.ApcState.KernelApcPending设置为0,即没有内核APC在等待执行(下面即将执行内核APC)。
调用_m_prefetchw预取至cache。
KiDeliverApc3

_KAPC.NormalRoutine不为0,是普通内核APC。 下面这段主要分析_KAPC.NormalRoutine不为0时。
如果当前线程有APC在执行或当前线程没有执行内核APC的能力,释放锁,降低IRQL至1,跳转到LABEL_16处。
如果当前线程没有APC在执行并且当前线程有执行内核APC的能力,继续执行。
判断链表连接是否错误,错误的话跳转至LABEL_95处,没错的话继续执行。
移除选中的APC,将APC设置为未插入状态,因为要执行这个APC了。
KiDeliverApc4

之后,释放锁,将IRQL设置为1,将当前线程设置为正在执行APC,然后执行KernelRoutine
KiDeliverApc5

随后呢,这个地方IDA翻译的伪代码应该有点儿问题,将IRQL设置为0,执行NormalRoutine,执行完之后再将IRQL设置为1。
将当前线程设置为APC没有在执行状态。
KiDeliverApc6

如果_KAPC.NormalRoutine为0,代表是特殊内核apc。
判断链表连接是否错误,如果错误,跳转到LABEL_95处;如果没错,继续执行。
从链表上移除选中的APC,将APC的插入状态设置为0,释放线程锁。
将IRQL设置为1。
将当前线程设置为特殊内核APC正在执行的状态。
执行KernelRoutine,执行完之后,将当前线程设置为特殊内核APC不在执行的状态。
while(1)循环体至此,不断循环,直到执行完需要执行的内核APC。
KiDeliverApc7

将IRQL设置为1,对两个局部变量赋值后来到LABEL_15处。

如果上下文环境是用户态并且用户APC链表不为空,代表可以去执行用户APC;如果不满足前面两个条件,则会执行到LABEL_16处。下面会主要分析满足条件时的情况。

将IRQL设置为2,加线程锁,防止重入。
KiDeliverApc8

将当前线程设置为没有等待执行的普通用户APC(因为下面有while循环去执行普通用户APC,暂且认为普通用户APC要没有了)。
判断用户APC链表是否为空,如果不为空,进入while(1)循环。

调用_m_prefetchw预取至cache。
判断是否是特殊用户APC,如果是,则跳出循环;如果不是,继续执行。
如果有普通用户APC在等待执行,并且不是特殊用户APC,跳转至LABEL_44处执行,如果是特殊用户APC则跳出循环。
如果没有普通用户APC在等待执行,获取下一个链表结点。如果结点是链表头,这时候说明没有用户APC可以执行了,跳到LABEL_49处,如果结点不是链表头,则继续在while(1)循环执行。
KiDeliverApc9

LABEL_44处,判断链表连接是否错误,如果错误就蓝屏(LABEL_95处);如果没错,就继续执行。
KiDeliverApc10

移除链表结点,将APC设置为未插入状态。
判断当前线程有没有特殊用户APC在等待执行。

  • 如果当前线程有特殊用户APC在等待执行,将当前线程设置为没有特殊用户APC在等待执行。判断当前线程用户APC链表是否为空,如果为空,执行到LABEL_49处;如果不为空,循环遍历用户APC链表,如果有特殊用户APC(KernelRoutine==KeSpecialUserApcKernelRoutine),则跳出循环并将当前线程设置为有特殊用户APC在等待执行,然后执行到LABEL_49处。
  • 如果当前线程没有特殊用户APC在等待执行,跳到LABEL_49处。

(因为写流程分析经常遇到“如果…如果…”,搞得分句分段什么的有点儿难受,感觉上面那种格式写流程判断还是挺不错的~)
KiDeliverApc11

LABEL_49处,释放线程锁,将IRQL设置为1。
判断是否有需要执行的用户APC,如果有(如果没有的话,就执行到LABEL_16处),执行KernelRoutine
KiDeliverApc12

如果有正在等待执行的普通用户APC,但是NormalRoutine==0,就会警醒该线程,随后跳转到LABEL_16处。
随后,调用KiInitializeUserApc(后面会讲解)。有无正在等待执行的普通用户APC会影响此函数的最后一个参数。
KiDeliverApc13

LABEL_16处,判断当前线程刚进此函数时的线程是否与当前线程相同,如果相同,设置当前线程的TrapFrame后函数返回;如果不同,蓝屏(蓝屏信息详见:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/bug-check-0x5–invalid-process-attach-attempt)。
KiDeliverApcLABEL_16

KiInitializeUserApc

unsigned __int64 __fastcall KiInitializeUserApc(
        _KEXCEPTION_FRAME *_ExceptionFrame_a,
        _KTRAP_FRAME *_TrapFrame_a,
        PVOID _NormalRoutine_a,
        PVOID _NormalContext_a,
        PVOID _arg_1,
        PVOID _arg_2,
        unsigned int a7)

此函数只看重要的部分。

线程进入内核态后,将大部分用户态的环境保存到_KTRAP_FRAME中,等要回到用户态时恢复,如果要返回用户态去处理用户APC,就要修改_KTRAP_FRAME结构体,并将原来的值备份下来。
通过KeContextFromKframes_KTRAP_FRAME_KEXCEPTION_FRAME的值复制到_CONTEXT中,以便恢复。
KiInitializeUserApc1

_CONTEXT.P1Home设置为NormalContext;将_CONTEXT.P2Home设置为_SystemArgument1;将_CONTEXT.P3Home设置为_SystemArgument2;将_CONTEXT.P4Home设置为NormalRoutine;将_CONTEXT.P5Home设置为a7(最后一个参数);
(当是普通用户APC时,a7=0;是特殊用户APC时,a7=1)

修改_TRAP_FRAME的RSP指向_CONTEXT结构的顶部;修改_TRAP_FRAME的RIP指向ntdll!KiUserApcDispatcher,修改_TRAP_FRAME的CS为0x33。
KiInitializeUserApc2
进入用户态时的栈为:
返回用户态时的栈

随后我们来看ntdll中的KiUserApcDispatcher

ntdll!KiUserApcDispatcher

通过对NormalRoutine解密判断是64位还是32位,如果是64位,调用KiUserCallForwarder;如果是32位调用Wow64ApcRoutine
KiUserApcDispatcher1

KiUserCallForwarder里,调用CFG(__guard_check_icall_fptr)以确保APC目标是CFG的有效函数。
随后调用NormalRoutine
KiUserCallForwarder

之后,调用ZwContinueEx,第一个参数是_CONTEXT结构,第二个参数是“a7”。
ZwContinueEx通过syscall进内核后会调用KiContinueEx。下面简要分析KiContinueEx

KiContinueEx

NTSTATUS
KiContinueEx(
IN PCONTEXT ContextRecord,
IN BOOLEAN  TestAlert,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame
)

调用此函数是为了将指定的_CONTEXT复制到指定的_KEXCEPTION_FRAME_KTRAP_FRAME,用于继续系统服务。
如果先前模式为用户模式,线程是可警醒的,用户APC可执行,指定的上下文记录先前由用户APC初始化例程放在堆栈上,然后绕过上下文记录复制到内核帧并再次返回。

如果当前线程可以被警醒并且有用户APC在等待执行,将上下文记录和异常帧地址保存在陷阱帧中,随后调用KiDeliverApc再次执行APC。(就不将_CONTEXT复制到_KTRAP_FRAME_KEXCEPTION_FRAME中了。)
KiContinueEx1

否则,根据提供的_CONTEXT还原_KTRAP_FRAME_KEXCEPTION_FRAME
KiContinueEx2

(其实这块代码可以结合WRK分析)

这样就可以遍历完所有的用户apc,形成一个闭环

  • 22
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: win11_22h2_chinese_simplified_x64v1.is 是一个特定版本的Windows 11的镜像文件。这个文件是一个可执行的镜像文件,它包含了安装Windows 11操作系统所需的所有文件和设置。该镜像文件是面向使用64位处理器的计算机的,并且适用于中文简体语言的用户。 关于该镜像文件的名称,其中的“win11_22h2”表示这是Windows 11的22H2版,指的是Windows 11的第二个半年度功能更新版本。而“chinese_simplified”表示该镜像文件支持中文简体语言。最后的“x64v1”表示该镜像文件适用于64位处理器的计算机。 安装Windows 11操作系统需要使用一种称为镜像文件的特殊文件来完成。以这种镜像文件的形式提供可以方便用户进行安装,因为它是一个自包含的文件,包含了操作系统的所有组件和设置。用户可以将该镜像文件烧录到光盘、制作成可启动的USB驱动器或者通过其他方法进行安装。安装过程中,用户需要提供适当的许可证密钥,并按照安装向导的指示进行操作。 总之,win11_22h2_chinese_simplified_x64v1.is 是一个Windows 11的特定版本镜像文件,适用于64位处理器的计算机,并支持中文简体语言。用户可以使用该镜像文件来安装Windows 11操作系统。 ### 回答2: win11_22h2_chinese_simplified_x64v1.is是指Windows 11的22H2版本简体中文64位操作系统镜像文件。这个镜像文件提供了Windows 11操作系统的安装文件,适用于使用64位处理器的计算机。 这个镜像文件是微软官方提供的,用于为用户提供新一代的Windows操作系统。Windows 11相比Windows 10有一些显著的改进和更新,包括新的图形用户界面、重新设计的开始菜单和任务栏、改进的多任务处理和窗口管理等。 通过使用win11_22h2_chinese_simplified_x64v1.is这个镜像文件,用户可以在自己的计算机上安装并体验全新的Windows 11操作系统。安装Windows 11需要一台计算机符合系统要求,包括64位处理器、4GB以上内存、64GB以上磁盘空间等。 用户可以将win11_22h2_chinese_simplified_x64v1.is镜像文件刻录到光盘或制作成可启动的USB安装盘,然后在计算机上启动并按照安装向导进行操作系统的安装。用户还可以将镜像文件挂载到一个虚拟光驱上,然后直接运行安装程序进行安装。 总之,win11_22h2_chinese_simplified_x64v1.is是Windows 11的22H2版本简体中文64位操作系统的镜像文件,通过它可以为用户提供Windows 11的安装文件,帮助用户在符合系统要求的计算机上安装和使用全新的Windows 11操作系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值