深入Windows APC

本文深入探讨Windows操作系统中的异步过程调用(APC)机制,涵盖了APC的内部实现、类型、内核与用户模式的使用细节,包括线程挂靠、APC的内存管理问题、内核与用户模式APC的执行特点。文章通过案例分析,揭示了早期注入Kernel32.dll的陷阱,并讨论了APC在不同Windows版本中的实现差异,对于理解和避免APC使用中的潜在问题提供了宝贵的洞见。
摘要由CSDN通过智能技术生成

Python微信订餐小程序课程视频

https://edu.csdn.net/course/detail/36074

Python实战量化交易理财系统

https://edu.csdn.net/course/detail/35475
  本篇原文为 寂静的羽夏(wingsummer) 中文翻译,非机翻,著作权归原作者 RbmmDennis A. Babkin 所有。
  由于原文十分冗长,也十分干货,采用机翻辅助,人工阅读比对修改的方式进行,如有翻译不得当的地方,欢迎批评指正。翻译不易,如有闲钱,欢迎支持。注意在转载文章时注意保留原文的作者链接,我(译者)的相关信息。话不多说,正文开始:

本篇文章包含一些没有被原厂家(指微软)进行文档化的功能和特色。在阅读本篇文章的相关内容和建议之后,你应该为自己的所作所为负责。展示在本文的方法依赖于内部实现并且可能以后不再有效。

介绍

在我们的第一篇关于错综复杂的用户APC的博文发布之后,我们决定从细节的深度拓展该主题,将会介绍有关异步过程调用(APC)在Windows操作系统的内部实现。
  那我们就开始吧,这里介绍没有什么特别的先后顺序。

目录

以下所有主题之间的联系并不是特别紧密,所以你或许想要一个可以更容易查阅的目录表:

APC 内部实现概要

为了在后面的文章——《伸入 NT 内核的异步过程调用内幕》,能够更好让大家更深入的理解内核 APC 的内部实现,我们不会重复原来已经讲过的了,而是多多陈述一些其他且鲜为人知的 APC 相关的细节。

为了更简洁的描述,在技术上说 APC 就是一堆在内存里存储的二进制字节,也就是所谓的 KAPC 结构体:

typedef struct \_KAPC {
  UCHAR Type;
  UCHAR SpareByte0;
  UCHAR Size;
  UCHAR SpareByte1;
  ULONG SpareLong0;
  _KTHREAD * Thread;
  _LIST_ENTRY ApcListEntry;
  void (* KernelRoutine)( _KAPC * , void (* * )( void * , void * , void * ), void * * , void * * , void * * );
  void (* RundownRoutine)( _KAPC * );
  void (* NormalRoutine)( void * , void * , void * );
  void * Reserved[0x3];
  void * NormalContext;
  void * SystemArgument1;
  void * SystemArgument2;
  CHAR ApcStateIndex;
  CHAR ApcMode;
  UCHAR Inserted;
}KAPC, *PKAPC;

上述结构体是在KAPC_STATE结构体里面的双向链表一部分:

typedef struct \_KAPC\_STATE {
  _LIST_ENTRY ApcListHead[0x2];
  _KPROCESS * Process;
  UCHAR InProgressFlags;
  UCHAR KernelApcInProgress : 01; // 0x01;
  UCHAR SpecialApcInProgress : 01; // 0x02;
  UCHAR KernelApcPending;
  UCHAR UserApcPendingAll;
  UCHAR SpecialUserApcPending : 01; // 0x01;
  UCHAR UserApcPending : 01; // 0x02;
}KAPC_STATE, *PKAPC_STATE;

并且KAPC_STATE自身也是线程对象的一部分,存储在内核里的KTHREAD结构体中:

🔒 点击查看 KTHREAD 🔒

typedef struct \_KTHREAD {
  _DISPATCHER_HEADER Header;
  void * SListFaultAddress;
  ULONGLONG QuantumTarget;
  void * InitialStack;
  void * volatile StackLimit;
  void * StackBase;
  ULONGLONG ThreadLock;
  ULONGLONG volatile CycleTime;
  ULONG CurrentRunTime;
  ULONG ExpectedRunTime;
  void * KernelStack;
  _XSAVE_FORMAT * StateSaveArea;
  _KSCHEDULING_GROUP * volatile SchedulingGroup;
  _KWAIT_STATUS_REGISTER WaitRegister;
  UCHAR volatile Running;
  UCHAR Alerted[0x2];
  ULONG AutoBoostActive : 01; // 0x00000001;
  ULONG ReadyTransition : 01; // 0x00000002;
  ULONG WaitNext : 01; // 0x00000004;
  ULONG SystemAffinityActive : 01; // 0x00000008;
  ULONG Alertable : 01; // 0x00000010;
  ULONG UserStackWalkActive : 01; // 0x00000020;
  ULONG ApcInterruptRequest : 01; // 0x00000040;
  ULONG QuantumEndMigrate : 01; // 0x00000080;
  ULONG UmsDirectedSwitchEnable : 01; // 0x00000100;
  ULONG TimerActive : 01; // 0x00000200;
  ULONG SystemThread : 01; // 0x00000400;
  ULONG ProcessDetachActive : 01; // 0x00000800;
  ULONG CalloutActive : 01; // 0x00001000;
  ULONG ScbReadyQueue : 01; // 0x00002000;
  ULONG ApcQueueable : 01; // 0x00004000;
  ULONG ReservedStackInUse : 01; // 0x00008000;
  ULONG UmsPerformingSyscall : 01; // 0x00010000;
  ULONG TimerSuspended : 01; // 0x00020000;
  ULONG SuspendedWaitMode : 01; // 0x00040000;
  ULONG SuspendSchedulerApcWait : 01; // 0x00080000;
  ULONG CetUserShadowStack : 01; // 0x00100000;
  ULONG BypassProcessFreeze : 01; // 0x00200000;
  ULONG Reserved : 10; // 0xffc00000;
  LONG MiscFlags;
  ULONG BamQosLevel : 02; // 0x00000003;
  ULONG AutoAlignment : 01; // 0x00000004;
  ULONG DisableBoost : 01; // 0x00000008;
  ULONG AlertedByThreadId : 01; // 0x00000010;
  ULONG QuantumDonation : 01; // 0x00000020;
  ULONG EnableStackSwap : 01; // 0x00000040;
  ULONG GuiThread : 01; // 0x00000080;
  ULONG DisableQuantum : 01; // 0x00000100;
  ULONG ChargeOnlySchedulingGroup : 01; // 0x00000200;
  ULONG DeferPreemption : 01; // 0x00000400;
  ULONG QueueDeferPreemption : 01; // 0x00000800;
  ULONG ForceDeferSchedule : 01; // 0x00001000;
  ULONG SharedReadyQueueAffinity : 01; // 0x00002000;
  ULONG FreezeCount : 01; // 0x00004000;
  ULONG TerminationApcRequest : 01; // 0x00008000;
  ULONG AutoBoostEntriesExhausted : 01; // 0x00010000;
  ULONG KernelStackResident : 01; // 0x00020000;
  ULONG TerminateRequestReason : 02; // 0x000c0000;
  ULONG ProcessStackCountDecremented : 01; // 0x00100000;
  ULONG RestrictedGuiThread : 01; // 0x00200000;
  ULONG VpBackingThread : 01; // 0x00400000;
  ULONG ThreadFlagsSpare : 01; // 0x00800000;
  ULONG EtwStackTraceApcInserted : 08; // 0xff000000;
  LONG volatile ThreadFlags;
  UCHAR volatile Tag;
  UCHAR SystemHeteroCpuPolicy;
  UCHAR UserHeteroCpuPolicy : 07; // 0x7f;
  UCHAR ExplicitSystemHeteroCpuPolicy : 01; // 0x80;
  UCHAR RunningNonRetpolineCode : 01; // 0x01;
  UCHAR SpecCtrlSpare : 07; // 0xfe;
  UCHAR SpecCtrl;
  ULONG SystemCallNumber;
  ULONG ReadyTime;
  void * FirstArgument;
  _KTRAP_FRAME * TrapFrame;
  _KAPC_STATE ApcState;
  UCHAR ApcStateFill[0x2b];
  CHAR Priority;
  ULONG UserIdealProcessor;
  LONGLONG volatile WaitStatus;
  _KWAIT_BLOCK * WaitBlockList;
  _LIST_ENTRY WaitListEntry;
  _SINGLE_LIST_ENTRY SwapListEntry;
  _DISPATCHER_HEADER * volatile Queue;
  void * Teb;
  ULONGLONG RelativeTimerBias;
  _KTIMER Timer;
  _KWAIT_BLOCK WaitBlock[0x4];
  UCHAR WaitBlockFill4[0x14];
  ULONG ContextSwitches;
  UCHAR WaitBlockFill5[0x44];
  UCHAR volatile State;
  CHAR Spare13;
  UCHAR WaitIrql;
  CHAR WaitMode;
  UCHAR WaitBlockFill6[0x74];
  ULONG WaitTime;
  UCHAR WaitBlockFill7[0xa4];
  SHORT KernelApcDisable;
  SHORT SpecialApcDisable;
  ULONG CombinedApcDisable;
  UCHAR WaitBlockFill8[0x28];
  _KTHREAD_COUNTERS * ThreadCounters;
  UCHAR WaitBlockFill9[0x58];
  _XSTATE_SAVE * XStateSave;
  UCHAR WaitBlockFill10[0x88];
  void * volatile Win32Thread;
  UCHAR WaitBlockFill11[0xb0];
  _UMS_CONTROL_BLOCK * Ucb;
  _KUMS_CONTEXT_HEADER * volatile Uch;
  void * Spare21;
  _LIST_ENTRY QueueListEntry;
  ULONG volatile NextProcessor;
  ULONG NextProcessorNumber : 31; // 0x7fffffff;
  ULONG SharedReadyQueue : 01; // 0x80000000;
  LONG QueuePriority;
  _KPROCESS * Process;
  _GROUP_AFFINITY UserAffinity;
  UCHAR UserAffinityFill[0xa];
  CHAR PreviousMode;
  CHAR BasePriority;
  CHAR PriorityDecrement;
  UCHAR ForegroundBoost : 04; // 0x0f;
  UCHAR UnusualBoost : 04; // 0xf0;
  UCHAR Preempted;
  UCHAR AdjustReason;
  CHAR AdjustIncrement;
  ULONGLONG AffinityVersion;
  _GROUP_AFFINITY Affinity;
  UCHAR AffinityFill[0xa];
  UCHAR ApcStateIndex;
  UCHAR WaitBlockCount;
  ULONG IdealProcessor;
  ULONGLONG NpxState;
  _KAPC_STATE SavedApcState;
  UCHAR SavedApcStateFill[0x2b];
  UCHAR WaitReason;
  CHAR SuspendCount;
  CHAR Saturation;
  USHORT SListFaultCount;
  _KAPC SchedulerApc;
  UCHAR SchedulerApcFill0[0x1];
  UCHAR ResourceIndex;
  UCHAR SchedulerApcFill1[0x3];
  UCHAR QuantumReset;
  UCHAR SchedulerApcFill2[0x4];
  ULONG KernelTime;
  UCHAR SchedulerApcFill3[0x40];
  _KPRCB * volatile WaitPrcb;
  UCHAR SchedulerApcFill4[0x48];
  void * LegoData;
  UCHAR SchedulerApcFill5[0x53];
  UCHAR CallbackNestingLevel;
  ULONG UserTime;
  _KEVENT SuspendEvent;
  _LIST_ENTRY ThreadListEntry;
  _LIST_ENTRY MutantListHead;
  UCHAR AbEntrySummary;
  UCHAR AbWaitEntryCount;
  UCHAR AbAllocationRegionCount;
  CHAR SystemPriority;
  ULONG SecureThreadCookie;
  _KLOCK_ENTRY LockEntries[0x6];
  _SINGLE_LIST_ENTRY PropagateBoostsEntry;
  _SINGLE_LIST_ENTRY IoSelfBoostsEntry;
  UCHAR PriorityFloorCounts[0x10];
  ULONG PriorityFloorSummary;
  LONG volatile AbCompletedIoBoostCount;
  LONG volatile AbCompletedIoQoSBoostCount;
  SHORT volatile KeReferenceCount;
  UCHAR AbOrphanedEntrySummary;
  UCHAR AbOwnedEntryCount;
  ULONG ForegroundLossTime;
  _LIST_ENTRY GlobalForegroundListEntry;
  _SINGLE_LIST_ENTRY ForegroundDpcStackListEntry;
  ULONGLONG InGlobalForegroundList;
  LONGLONG ReadOperationCount;
  LONGLONG WriteOperationCount;
  LONGLONG OtherOperationCount;
  LONGLONG ReadTransferCount;
  LONGLONG WriteTransferCount;
  LONGLONG OtherTransferCount;
  _KSCB * QueuedScb;
  ULONG volatile ThreadTimerDelay;
  LONG volatile ThreadFlags2;
  ULONG PpmPolicy : 02; // 0x00000003;
  ULONG ThreadFlags2Reserved : 30; // 0xfffffffc;
  ULONGLONG TracingPrivate[0x1];
  void * SchedulerAssist;
  void * volatile AbWaitObject;
}KTHREAD, *PKTHREAD;

将线程挂靠到另一个进程

值得注意的一点就是任何一个线程都可以通过调用KeStackAttachProcess(该函数会接收KAPC_STATE对象,并查看它的ApcState参数)临时地挂靠到另一个进程上,也可以通过调用KeUnstackDetachProcess函数脱离进程。但是这会有会导致问题一点点的可能性,所以开发者需要把注意力放到上面。
  因此,有一个十分重要的事情去理解,我们需要通过使用一个未被文档化但是导出的KeInitializeApc调用初始化一个APC对象:

VOID KeInitializeApc(
  IN PRKAPC Apc,          //pointer to KAPC
  IN PKTHREAD Thread,
  IN KAPC_ENVIRONMENT Environment,
  IN PKKERNEL_ROUTINE KernelRoutine,
  IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
  IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,
  IN KPROCESSOR_MODE ApcMode,
  IN PVOID NormalContext
);

我们使用该函数需要提供KAPC_ENVIRONMENT类型的参数,它的枚举如下:

typedef enum \_KAPC\_ENVIRONMENT {
  OriginalApcEnvironment,
  AttachedApcEnvironment,
  CurrentApcEnvironment
} KAPC_ENVIRONMENT;

这个参数指定了APC环境,换句话说,当我们插入一个APC,我们会告诉系统是应该为当前线程激活它,还是应该在线程挂靠到另一个进程之前为保存的状态(KTHREAD::SavedApcState)激活它。该参数将会在后面保存到KAPC::ApcStateIndex成员当中。
  为了更好的说明这个概念,让我们回顾如下的KiInsertQueueApc代码:

// KiInsertQueueApc() excerpt:

Thread = Apc->Thread;
PKAPC_STATE ApcState;

if (Apc->ApcStateIndex == 0 && Thread->ApcStateIndex != 0)
{
  ApcState = &Thread->SavedApcState;
}
else
{
  Apc->ApcStateIndex = Thread->ApcStateIndex;
  ApcState = &Thread->ApcState;
}

所以本质上KAPC::ApcStateIndex是一个布尔值:

  • 非0:指示APC插入到当前线程中,话句话说,APC应该执行在当前进程的上下文环境中,也就是线程当前运行的环境。
  • 0:指示当前APC应该仅仅在源进程的环境中运行,或者在线程在进程挂靠之前的进程环境中。

KeStackAttachProcess函数中,有如下逻辑:

// KeStackAttachProcess() excerpt:

if (Thread->ApcStateIndex != 0)
{
  KiAttachProcess(Thread, Process, &LockHandle, ApcState);
}
else
{
  KiAttachProcess(Thread, Process, &LockHandle, &Thread->SavedApcState);
  ApcState->Process = NULL;
}

也就是意味着,当我们第一次挂靠到另一个进程,打个比方:如果它的KAPC::ApcStateIndex值为0,当前的KTHREAD::ApcState存储在KTHREAD::SavedApcState当中,并且以前的ApcState不会被使用,除非设置KAPC_STATE::Process为0表示这个状态存储在KTHREAD::SavedApcState
  但是如果我们递归式挂靠,或当一个线程已经挂靠到另一个进程时我们已经调用了KeStackAttachProcess,在那种情况下APC的状态被保存在ApcState对象中,被作为参数传递到函数当中。
  这种逻辑处理是为了让系统始终可以访问线程的原始APC状态。这可以用于将APC插入原始线程,或通过调用KeUnstackDetachProcess将线程脱离原进程。

APC 的类型

APC有两个基础类型:内核APC和用户APC。内核APC给予了开发者更多便利来处理APC排列和处理(我们在本篇博文已讨论过用户APC)。内核APC不向用户层开发者们开放能够直接访问的权限。
  KAPC_STATE::ApcListHead里面包含了两个链表用来存放内核APC和用户APC。这两个链表分别有APC排队等待线程处理:

typedef enum \_MODE {
  KernelMode = 0x0,
  UserMode = 0x1,
  MaximumMode = 0x2
}MODE;

内核使用这些列表来维护每种类型的APC的状态。当APC排队或调用KeInsertQueueApc处理时,KAPC::ApcMode用作KAPC_STATE::ApcListHead的索引:

NTSTATUS NtQueueApcThread(
  IN HANDLE Thread,
  IN PKNORMAL_ROUTINE NormalRoutine,
  IN PVOID NormalContext,
  IN PVOID SystemArgument1,
  IN PVOID SystemArgument2
);

内核 APC 的使用内存的易错点

许多内核开发新手犯了一个错误:为内核模式APC指定了错误的内存类型。认识到这一点很重要,以防止各种意外的蓝屏死机(BSOD)。
  这是一定要记住经验,KAPC结构体只能使用从非分页内存分配的内存(或者从类似NonPagedPool*类型分配)。即使在PASSIVE_LEVELIRQL初始化并插入APC,这也是没问题的。
  为什么要有这样的内存类型限制呢?其他一些APC也可以插入到运行在更高调度级别IRQL的同一线程中。在插入双链接APC列表期间,系统将尝试访问列表中已经存在的其他KAPC结构。因此,如果其中任何一个使用的是从分页内存分配的内存,你将会从DISPATCH_LEVEL间接访问分页内存,这也是一种会导致蓝屏保护原因。
  比较棘手的是,我在描述如上的情况非常少见,在开发和测试阶段可能不会出现。这将很难在生产代码中进行诊断,正如我在上面解释的,可能过一段时间之后,会在你无法控制的环境中发生蓝屏。

中断和阻塞内核 APC

关于内核模式APC,需要记住的重要一点是,它通过中断实现,这意味着它可以发生在代码中的任意两个CPU指令之间。
  内核开发允许我们阻止APC的执行。只有在代码的某些特殊部分起作用:将IRQL提升到APC_LEVEL或更高级别或将写的代码放在KeEnterCriticalRegionKeLeaveCriticalRegion的调用之间。(请注意,这些函数不会阻止所谓的特殊内核APC的执行,只有提高IRQL级别才能阻止这些APC的执行)。
  关于我在上面展示的IRQL条件限制,一个有趣的事实是,如果APC到达关键区域,它不会丢失,稍后将在以下任一函数中处理:KeLeaveGuardedRegionKeLeaveCriticalRegionKeLowerIrql或者在临界区的结尾。

RundownRoutine 细节

如果我再次引用这篇博文:

简单点说,任何一种APC都可以定义一个有效的 RundownRoutine 。此例程必须驻留在内核内存中,并且仅在系统需要丢弃 APC 队列的内容时(例如线程退出时)调用。在这种情况下,既不执行 KernelRoutine ࿰

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值