Python微信订餐小程序课程视频
https://edu.csdn.net/course/detail/36074
Python实战量化交易理财系统
https://edu.csdn.net/course/detail/35475
本篇原文为 寂静的羽夏(wingsummer) 中文翻译,非机翻,著作权归原作者 Rbmm 和 Dennis A. Babkin 所有。
由于原文十分冗长,也十分干货,采用机翻辅助,人工阅读比对修改的方式进行,如有翻译不得当的地方,欢迎批评指正。翻译不易,如有闲钱,欢迎支持。注意在转载文章时注意保留原文的作者链接,我(译者)的相关信息。话不多说,正文开始:
本篇文章包含一些没有被原厂家(指微软)进行文档化的功能和特色。在阅读本篇文章的相关内容和建议之后,你应该为自己的所作所为负责。展示在本文的方法依赖于内部实现并且可能以后不再有效。
介绍
在我们的第一篇关于错综复杂的用户APC
的博文发布之后,我们决定从细节的深度拓展该主题,将会介绍有关异步过程调用(APC
)在Windows
操作系统的内部实现。
那我们就开始吧,这里介绍没有什么特别的先后顺序。
目录
以下所有主题之间的联系并不是特别紧密,所以你或许想要一个可以更容易查阅的目录表:
- 介绍
- 目录
- APC 内部实现概要
- 来自内核的用户 APC
- Windows XP 中漏洞百出的用户模式 APC 实现
- 错综复杂的用户模式 APC 实现 DLL 注入
- ZwQueueApcThread 与 QueueUserAPC 孰优孰劣
- 用户 APC 演示代码
- 32 位进程中的 64 位用户 APC
- 后记
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_LEVEL
的IRQL
初始化并插入APC
,这也是没问题的。
为什么要有这样的内存类型限制呢?其他一些APC
也可以插入到运行在更高调度级别IRQL
的同一线程中。在插入双链接APC
列表期间,系统将尝试访问列表中已经存在的其他KAPC
结构。因此,如果其中任何一个使用的是从分页内存分配的内存,你将会从DISPATCH_LEVEL
间接访问分页内存,这也是一种会导致蓝屏保护原因。
比较棘手的是,我在描述如上的情况非常少见,在开发和测试阶段可能不会出现。这将很难在生产代码中进行诊断,正如我在上面解释的,可能过一段时间之后,会在你无法控制的环境中发生蓝屏。
中断和阻塞内核 APC
关于内核模式APC
,需要记住的重要一点是,它通过中断实现,这意味着它可以发生在代码中的任意两个CPU
指令之间。
内核开发允许我们阻止APC
的执行。只有在代码的某些特殊部分起作用:将IRQL
提升到APC_LEVEL
或更高级别或将写的代码放在KeEnterCriticalRegion
和KeLeaveCriticalRegion
的调用之间。(请注意,这些函数不会阻止所谓的特殊内核APC
的执行,只有提高IRQL
级别才能阻止这些APC
的执行)。
关于我在上面展示的IRQL
条件限制,一个有趣的事实是,如果APC
到达关键区域,它不会丢失,稍后将在以下任一函数中处理:KeLeaveGuardedRegion
、 KeLeaveCriticalRegion
、KeLowerIrql
或者在临界区的结尾。
RundownRoutine 细节
如果我再次引用这篇博文:
简单点说,任何一种APC都可以定义一个有效的 RundownRoutine 。此例程必须驻留在内核内存中,并且仅在系统需要丢弃 APC 队列的内容时(例如线程退出时)调用。在这种情况下,既不执行 KernelRoutine