在病毒、逆向中,关于APC最常见的应用就是APC注入的使用了,但是随着随APC的深入了解,发现不管是在内核状态进行APC注入的操作,还是在用户空间执行APC注入的操作。虽然调用的API不同,但是核心内容是不变的,就是往APC队列中插入回调函数,但是一直对于这个回调函数什么时候执行,是怎么执行的很费解.看了毛教授的文章《Windows的APC机制》受益匪浅,总结一点自己关于APC的思路:
之前在分析rootkit的时候,有注意到关于内核中文件的读写函数的API调用中存在APCRoutine的参数设定,内核文件写API函数ZwWriteFile的函数原型如下:但是驱动中用这些API的时候,相关的目标值设定都为0。
NTSYSAPI NTSTATUS ZwWriteFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
PLARGE_INTEGER ByteOffset,
PULONG Key
);
之所以存在这个APCRoutine函数指针,是因为,文件操作存在"同步"“异步"之分。所谓同步,异步的不同之处就是,是否等待返回结果,而且在等待过程中"不作为”,用一张图来解释,如下:
通过上边两幅图,不难看出文件在执行同步操作的时候,会影响当前程序的进程,转而去执行文件操作,当执行结束之后返回当前进程继续执行下一步操作,相较而言,异步操作在执行写操作的时候,不会等待返回结果,直接执行程序的下一步。那么在异步操作的时候,写文件操作的请求分发给谁了呢?这个时候之前的APCRoutine就派上用场了。在执行异步操作的时候,请求发送给内核,内核在使用设备驱动在真正执行相应的写操作的时候,调用者的线程就会去调用APCRoutine指向的APC函数,这个函数的调用如下:
typedef VOID (NTAPI *PIO_APC_ROUTINE)(IN PVOID ApcContext,IN PIO_STATUS_BLOCK IoStatusBlock,IN ULong Reserved)
IN 表示输入参数,参数列表中的ApcContext就是在调用ZwWriteFile的时候指定的ApcContext,当调用者的线程发出“APC请求”的时候,就会调用这个APCRoutine例程。从这个例子来看,APC请求是内核向用户线程发出的请求。但是实际上,别的线程甚至于目标线程自身也可以发出APC请求,微软也为程序提供了这样的接口,同时也是我们在实现APC注入的时候经常使用的API函数:QueueUserAPC(),该函数的原型如下:
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData
);
在RactOS上找到QueueUserAPC 函数的执行过程:
{
NTSTATUS Status;
ACTIVATION_CONTEXT_BASIC_INFORMATION ActCtxInfo;
/* Zero the activation context and query information on it */
RtlZeroMemory(&ActCtxInfo, sizeof(ActCtxInfo));//将内存空间清零
Status = RtlQueryInformationActivationContext(RTL_QUERY_ACTIVATION_CONTEXT_FLAG_USE_ACTIVE_ACTIVATION_CONTEXT,
NULL,
0,
ActivationContextBasicInformation,
&ActCtxInfo,
sizeof(ActCtxInfo),
NULL);//查询APC Context信息来填充数据结构
if (!NT_SUCCESS(Status))
{
/* Fail due to SxS */
DbgPrint("SXS: %s failing because RtlQueryInformationActivationContext()"
"returned status %08lx\n", __FUNCTION__, Status);
BaseSetLastNTError(Status);
return FALSE;
}
/* Queue the APC */
Status = NtQueueApcThread(hThread,
(PKNORMAL_ROUTINE)BaseDispatchApc,
pfnAPC,
(PVOID)dwData,
(ActCtxInfo.dwFlags & 1) ?
INVALID_ACTIVATION_CONTEXT : ActCtxInfo.hActCtx);//执行NtQueueApcThread
if (!NT_SUCCESS(Status))
{
BaseSetLastNTError(Status);
return FALSE;
}
/* All good */
return TRUE;
}
简单分析上边的代码,不难看出,真正执行APC查询操作的函数是,NtQueueApcThread函数,即:QueueUserAPC->NtQueueApcThread:
NtQueueApcThread函数如下:
NTSTATUS
NTAPI
NtQueueApcThread(IN HANDLE ThreadHandle,
IN PKNORMAL_ROUTINE ApcRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2)
{
return NtQueueApcThreadEx(ThreadHandle, NULL, ApcRoutine, NormalContext, SystemArgument1, SystemArgument2);
}
根据上述代码,NtQueueApcThread直接调用函数NtQueueApcThreadEx,即调用链如下:QueueUserAPC->NtQueueApcThread->NtQueueApcThreadEx
NtQueueApcThreadEx函数执行过程如下:
NTSTATUS
NTAPI
NtQueueApcThreadEx(IN HANDLE ThreadHandle,
IN OPTIONAL HANDLE UserApcReserveHandle,
IN PKNORMAL_ROUTINE ApcRoutine,
IN PVOID NormalContext,
IN OPTIONAL PVOID SystemArgument1,
IN OPTIONAL PVOID SystemArgument2)
{
PKAPC Apc;
PETHREAD Thread;
NTSTATUS Status = STATUS_SUCCESS;
PAGED_CODE();
/* Get ETHREAD from Handle */
Status = ObReferenceObjectByHandle(ThreadHandle,
THREAD_SET_CONTEXT,
PsThreadType,
ExGetPreviousMode(),
(PVOID)&Thread,
NULL);
if (!NT_SUCCESS(Status)) return Status;
/* Check if this is a System Thread */
if (Thread->SystemThread)
{
/* Fail */
Status = STATUS_INVALID_HANDLE;
goto Quit;
}
/* Allocate an APC */
Apc = ExAllocatePoolWithQuotaTag(NonPagedPool |
POOL_QUOTA_FAIL_INSTEAD_OF_RAISE,
sizeof(KAPC),
TAG_PS_APC);
if (!Apc)
{
/* Fail */
Status = STATUS_NO_MEMORY;
goto Quit;
}
/* Initialize the APC */
KeInitializeApc(Apc,
&Thread->Tcb,
OriginalApcEnvironment,
PspQueueApcSpecialApc,
NULL,
ApcRoutine,
UserMode,
NormalContext);
/* Queue it */
if (!KeInsertQueueApc(Apc,
SystemArgument1,
SystemArgument2,
IO_NO_INCREMENT))
{
/* We failed, free it */
ExFreePool(Apc);
Status = STATUS_UNSUCCESSFUL;
}
/* Dereference Thread and Return */
Quit:
ObDereferenceObject(Thread);
return Status;
}
通过对上述代码的简单分析,不难看出,在上述代码中完成的工作主要有:定义一个空的APC数据结构->初始化APC数据结构->插入APC队列。需要注意的是插入APC队列之后,当产生APC请求的时候,需要执行的对应的APC处理函数是什么,这个问题,还需要回到上边QueueUserAPC 的调用里边,在该函数的调用中已经将需要执行的APC例程传入,所对应的执行APC函数的代码如下:
VOID
NTAPI
BaseDispatchApc(IN PAPCFUNC ApcRoutine,
IN PVOID Data,
IN PACTIVATION_CONTEXT ActivationContext)
{
RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME ActivationFrame;
/* Setup the activation context */
ActivationFrame.Size = sizeof(ActivationFrame);
ActivationFrame.Format = RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_FORMAT_WHISTLER;
/* Check if caller wanted one */
if (ActivationContext == INVALID_ACTIVATION_CONTEXT)
{
/* Do the APC directly */
ApcRoutine((ULONG_PTR)Data);
return;
}
/* Then activate it */
RtlActivateActivationContextUnsafeFast(&ActivationFrame, ActivationContext);
/* Call the routine under SEH */
_SEH2_TRY
{
ApcRoutine((ULONG_PTR)Data);
}
_SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
{
}
_SEH2_END;
/* Now de-activate and release the activation context */
RtlDeactivateActivationContextUnsafeFast(&ActivationFrame);
RtlReleaseActivationContext(ActivationContext);
}
上边的代码也以参数的形式传递给NtQueueApcThreadEx,当产生APC请求的时候,执行对应函数进行处理。
通过上述过程的数里,不难发现,APC是针对具体到某一个线程而言的。在实际处理过程中需要有具体的线程加以执行的,所以每个线程都有自己所对应的APC队列。在内核中对应的APC结构体为:KAPC_STATE,对应的数据结构存储在代表线程的数据结构ETHREAD,ETHREAD->Tcb(KTHREAD数据结构)->ApcState(KAPC_STATE数据结构)
ntdll!_ETHREAD
+0x000 Tcb : _KTHREAD
+0x350 CreateTime : _LARGE_INTEGER
+0x358 ExitTime : _LARGE_INTEGER
+0x358 KeyedWaitChain : _LIST_ENTRY
+0x360 ChargeOnlySession : Ptr32 Void
+0x364 PostBlockList : _LIST_ENTRY
+0x364 ForwardLinkShadow : Ptr32 Void
+0x368 StartAddress : Ptr32 Void
+0x36c TerminationPort : Ptr32 _TERMINATION_PORT
+0x36c ReaperLink : Ptr32 _ETHREAD
+0x36c KeyedWaitValue : Ptr32 Void
+0x370 ActiveTimerListLock : Uint4B
+0x374 ActiveTimerListHead : _LIST_ENTRY
+0x37c Cid : _CLIENT_ID
+0x384 KeyedWaitSemaphore : _KSEMAPHORE
+0x384 AlpcWaitSemaphore : _KSEMAPHORE
+0x398 ClientSecurity : _PS_CLIENT_SECURITY_CONTEXT
+0x39c IrpList : _LIST_ENTRY
..........
ntdll!_KTHREAD
+0x000 Header : _DISPATCHER_HEADER
+0x010 SListFaultAddress : Ptr32 Void
+0x018 QuantumTarget : Uint8B
+0x020 InitialStack : Ptr32 Void
+0x024 StackLimit : Ptr32 Void
+0x028 StackBase : Ptr32 Void
+0x02c ThreadLock : Uint4B
............
+0x060 Tag : UChar
+0x061 SystemHeteroCpuPolicy : UChar
+0x062 UserHeteroCpuPolicy : Pos 0, 7 Bits
+0x062 ExplicitSystemHeteroCpuPolicy : Pos 7, 1 Bit
+0x063 Spare0 : UChar
+0x064 SystemCallNumber : Uint4B
+0x068 FirstArgument : Ptr32 Void
+0x06c TrapFrame : Ptr32 _KTRAP_FRAME
+0x070 ApcState : _KAPC_STATE
+0x070 ApcStateFill : [23] UChar
+0x087 Priority : Char
+0x088 UserIdealProcessor : Uint4B
+0x08c ContextSwitches : Uint4B
在KTHREAD结构中包含KAPC_STATE数据结构
KAPC_STATE数据结构的定义如下:
ntdll!_KAPC_STATE
+0x000 ApcListHead : [2] _LIST_ENTRY
+0x010 Process : Ptr32 _KPROCESS
+0x014 InProgressFlags : UChar
+0x014 KernelApcInProgress : Pos 0, 1 Bit
+0x014 SpecialApcInProgress : Pos 1, 1 Bit
+0x015 KernelApcPending : UChar
+0x016 UserApcPendingAll : UChar
+0x016 SpecialUserApcPending : Pos 0, 1 Bit
+0x016 UserApcPending : Pos 1, 1 Bit
APCListHead就是对应的APC队列的头部,而且ApcListHead是一个拥有两个成员的数组,这是因为APC分为用户态的APC队列和内核态的APC队列,两个状态各有各的APC队列,方便分别管理。所谓用户态APC以及内核态APC的意思是,用户态的APC函数在用户态执行,内核态的APC函数在内核中执行。
值得关注的是在分析rootkit的过程中经常会遇到,某一个线程Attach(挂靠)到另一个进程上的操作。我们知道在Windows操作系统中五哦为的用户空间是低2G的空间,而系统空间则是高2G的内奸,但是内核空间是公用的。当进程B的线程A运行于内核空间的时候,如果这个时候该线程的活动和用户空间进行交互,那么当前的用户空间就是进程B的用户空间,但是如果执行上述的挂靠操作的话,就相当于把当前线程A管靠到另一个新进程C中,这个时候为了让防止上下文遭到破坏就需要将APC队列存储起来,方便回到原始进程空间的时候恢复现场。那么存储在哪?存储过去之后,用什么来表示当前状态是原始环境状态还是其他进程环境(挂靠状态)?这里就要使用到前边同样是KTHREAD结构中的其他两个字段:
ntdll!KTHREAD
+0x134 ApcStateIndex : UChar
+0x135 BasePriority : Char
+0x136 PriorityDecrement : Char
+0x136 ForegroundBoost : Pos 0, 4 Bits
+0x136 UnusualBoost : Pos 4, 4 Bits
+0x137 Preempted : UChar
+0x138 AdjustReason : UChar
+0x139 AdjustIncrement : Char
+0x13a PreviousMode : Char
+0x13b Saturation : Char
+0x13c SystemCallNumber : Uint4B
+0x140 FreezeCount : Uint4B
+0x144 UserAffinity : _GROUP_AFFINITY
+0x150 Process : Ptr32 _KPROCESS
+0x154 Affinity : _GROUP_AFFINITY
+0x160 IdealProcessor : Uint4B
+0x164 UserIdealProcessor : Uint4B
+0x168 ApcStatePointer : [2] Ptr32 _KAPC_STATE
+0x170 SavedApcState : _KAPC_STATE
+0x170 SavedApcStateFill : [23] UChar
字段:SavedApcState用来存储原始环境的APC队列
字段:ApcStateIndex用来表示当前状态是管靠状态还是原始环境状态ApcStateIndex存在几个枚举变量,如下:
typedef enmu _KAPC_ENVIRONMENT
{
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
}
但是实际上,用于ApcStateIndex的值只有OriginalApcEnvironment和AttachedApcEnvironmentAttachedApcEnvironment即0和1。还有一点需要注意,当线程挂靠在其他进程之后,APC队列转移到SavedApcState终止后,如果这个时候产生对挂靠进程的APC请求,怎么去执行APC请求?这时,就要靠KTHREAD 中的另一个成员ApcStatePointer去调控,ApcStatePointer是一个指针数组,存储着两个APC_STATE指针,这个数组把ApcStateIndex的值当作标志位进行对应寻址。具体方式如下:
正常情况下的“寻址”方式:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState
APcStatePointer和ApcStateIndex进行联合寻址的方式:
正常情况下:
ApcStatePointer[0] 指向 ApcState,此时 ApcStateIndex 的值为 0
ApcStatePointer[ApcStateIndex] 指向ApcState
(因为正常情况下ApcStateIndex的值为0)
挂靠情况下:
ApcStatePointer[1] 指向 ApcState,此时 ApcStateIndex 的值为 1
ApcStatePointer[ApcStateIndex] 指向 ApcState
(因为挂靠情况下ApcStateIndex的值为1)
通过上述对寻址方式的了解不难总结出,不论什么情况下,ApcStatePointer[ApcStateIndex]都是指向当前的ApcState,因为ApcState表示线程当前所使用的APC状态。
先研究到这里,消化消化
参考:
毛德操教授的《漫谈兼容内核之十二:Windows的APC机制》