关于APC机制

在病毒、逆向中,关于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机制》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值