APC注入原理流程

APC注入解析综述

线程是不能被“杀掉”、“挂起”、“恢复”的,线程在执行的时候自己占据着CPU,别人怎么可能控制它呢?

举个极端的例子:如果不调用API,屏蔽中断,并保证代码不出现异常,线程将永久占用CPU,何谈控制呢?所以说线程如果想“死”,一定是自己执行代码把自己杀死,不存在“他杀”这种情况!

那如果想改变一个线程的行为该怎么办呢?

可以给他提供一个函数,让它自己去调用,这个函数就是APC(Asyncroneus Procedure Call),即异步过程调用。

apc的最大特点就是在本函数返回后才执行,而且是在本线程中。

而内核提供的原生的定时器,执行的环境可能就不是原始的线程了。

windows天生就是个异步框架,里面大量的设计都是为异步而设计,比如IRP,就是贯穿整个windows的异步框架

apc它的执行时机有多,比如在线程wait、线程切换到应用层、线程被挂起等等等等,而且apc也分几个层次的优先级.就是说apc一般是不太需要立马执行的低优先级的函数。所以一旦线程有空隙了,windows就会执行一下windows在执行完线程的主要任务何后,顺便把apc队列执行一遍

APC注入原理

在一个进程中,当一个执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断,当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数,利用QueueUserAPC()这个API,并以此去执行我们的DLL加载代码,进而完成DLL注入的目的.
1.当对面程序执行到某一个上面的等待函数的时候,系统会产生一个中断

2.当线程唤醒的时候,这个线程会优先去Apc队列中调用回调函数

3.我们利用QueueUserApc,往这个队列中插入一个回调

4.插入回调的时候,把插入的回调地址改为LoadLibrary,插入的参数我们使用VirtualAllocEx申请内存,并且写入进去

APC注入细节:

  (1) 当线程调用SleepEx、SignalObjectAndWait、MsgWaitForMultipleObjectsEx,WaitForMultipleObjectsEx或者WaitForSingleObjectEx函数时,会切换到警戒状态(alertable state)

  注:

  警戒状态可参考:

  https://msdn.microsoft.com/en-us/library/windows/desktop/aa363772(v=vs.85).aspx

  (2) 当线程进入警戒状态时,会循环检查线程中的APC队列,如果APC队列中存在函数指针,那么就会调用该函数

  (3) 使用QueueUserAPC函数向APC队列插入函数指针Loadlibrary(),实现加载DLL

  (4) 注入成功后,警戒状态结束,程序继续运行,有可能造成程序不稳定,导致程序崩溃

  (5) 如果没有删除APC队列,不能反复注入同一函数

  (6) 使用APC注入,需要目标进程中至少有一个线程处于警戒状态或者能够进入警戒状态,否则无法实现APC注入

APC注入流程

1.根据进程名称得进程ID
2.枚举该进程中的线程,利用快照枚举所有的线程
3.写入远程内存,写入的是Dll的路径,将自己的函数DLL插入到每个线程的APC队列中
    3)当EXE里某个线程执行到SleepEx()或者WaitForSingleObjectEx()时,系统就会产生一个软中断(或者是Messagebox弹窗的时候不点OK的时候也能注入)。
    4)当线程再次被唤醒时,此线程会首先执行APC队列中的被注册的函数。
    5)利用QueueUserAPC()这个API可以在软中断时向线程的APC队列插入一个函数指针,如果我们插入的是Loadlibrary()执行函数的话,就能达到注入DLL的目的。

APC对象分类

在NT中,有两种类型的APC:用户模式和内核模式。

用户APC运行在用户模式下目标线程当前上下文中,并且需要从目标线程得到许可来运行。特别是,用户模式的APC需要目标线程处在alertable等待状态才能被成功的调度执行。通过调用下面任意一个函数,都可以让线程进入这种状态。这些函数是:KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject, KeDelayExecutionThread。
对于用户模式下,可以调用函数SleepEx, SignalObjectAndWait, WaitForSingleObjectEx, WaitForMultipleObjectsEx,MsgWaitForMultipleObjectsEx 都可以使目标线程处于alertable等待状态,从而让用户模式APCs执行,原因是这些函数最终都是调用了内核中的KeWaitForSingleObject, KeWaitForMultipleObjects, KeWaitForMutexObject, KeDelayExecutionThread等函数。另外通过调用一个未公开的alert-test服务KeTestAlertThread,用户线程可以使用户模式APC执行。

当一个用户模式APC被投递到一个线程,调用上面的等待函数,如果返回等待状态STATUS_USER_APC,在返回用户模式时,内核转去控制APC例程,当APC例程完成后,再继续线程的执行.

和用户模式APC比较,内核模式APC执行在内核模式下。可以被划分为常规的和特殊的两类。当APCs被投递到一个特殊的线程,特殊的内核模式APC不需要从线程得到许可来运行。然而,常规的内核模式APC在他们成功执行前,需要有特定的环境。此外,特殊的内核APC被尽可能快地执行,既只要APC_LEVEL级上有可调度的活动。在很多情况下,特殊的内核APC甚至能唤醒阻塞的线程。普通的内核APC仅在所有特殊APC都被执行完,并且目标线程仍在运行,同时该线程中也没有其它内核模式APC正执行时才执行。用户模式APC在所有内核模式APC执行完后才执行,并且仅在目标线程有alertable属性时才执行。

每一个等待执行的APC都存在于一个线程执行体,由内核管理的队列中。系统中的每一个线程都包含两个APC队列,一个是为用户模式APC,另一个是为内核模式APCs的。

NT通过一个成为KAPC的内核控制对象来描述一个APC.尽管DDK中没有明确的文档化APCs,但是在NTDDK.H中却非常清楚的定义了APC对象。从下面的KAPC对象的定义看,有些是不需要说明的。像Type和Size。Type表示了这是一个APC内核对象。在nt中,每一个内核对象或者执行体对象都有Type和Size这两个域。由此处理函数可以确定当前处理的对象。Size表示一个字对齐的结构体的大小。也就是指明了对象占的内存空间大小。Spare0看起来有些晦涩难懂,但是它是没用什么任何深远的意义,仅仅是为了内存补齐。

APC环境

一个线程在它执行的任意时刻,假设当前的IRQL是在Passive级,它可能需要临时在其他的进程上下文中执行代码,为了完成这个操作,线程调用系统功能函数KeAttachProcess,在从这个调用返回时,线程执行在另一个进程的地址空间。先前所有在线程自己的进程上下文中等待执行的APCs,由于这时其所属进程的地址空间不是当前可用的,因此他们不能被投递执行。然而,新的插入到这个线程的APCs可以执行在这个新的进程空间。甚至当线程最后从新的进程中分离时,新的插入到这个线程的APC还可以在这个线程所属的进程上下文中执行。
为了达到控制APC传送的这个程度,NT中每个线程维护了两个APC环境或者说是状态。每一个APC环境包含了用户模式的APC队列和内核模式的APC队列,一个指向当前进程对象的指针和三个控制变量,用于指出:是否有未决的内核模式APCs(KernelApcPending),是否有常规内核模式APC在进行中(KernelApcInProgress),是否有未决的用户模式的APC(UserApcPending). 这些APC的环境保存在线程对象的ApcStatePointer域中。

APC对象结构体

APC对象结构定义如下:
 
typedef struct _KAPC {  UCHAR Type; //类型ApcObject
  UCHAR SpareByte0;  UCHAR Size; //APC结构体大小
  UCHAR SpareByte1;  ULONG SpareLong0;
  struct _KTHREAD *Thread; //当前线程的KTHREAD
  LIST_ENTRY ApcListEntry; //当前线程的APC链表
  PKKERNEL_ROUTINE KernelRoutine; //
  PKRUNDOWN_ROUTINE RundownRoutine; //
  PKNORMAL_ROUTINE NormalRoutine; //
  PVOID NormalContext; //用户定义的Apc函数
  PVOID SystemArgument1; //用户Apc函数的参数
  PVOID SystemArgument2;//
  CCHAR ApcStateIndex; //Apc状态
  KPROCESSOR_MODE ApcMode; //Apc所处的Mode,UserMode/KernelMode
  BOOLEAN Inserted;     //是否已经被插入队列} KAPC, *PKAPC, *RESTRICTED_POINTER PRKAPC;

APC注入注意问题

1、如果OpenProce或者OpenThread失败,可以尝试提权,或者给UAC,以管理员身份启动。
2、大部分系统进程都满足条件,支持APC注入

  可供参考的APC注入代码:

  https://github.com/3gstudent/Inject-dll-by-APC
3、这种注入方式局限性很明显,一是必须要等待时机,而是当注入成功后,SleepEx或者其他等待函数直接就会跳过当前等待继续往下走,这样可能造成被注入程序的不稳定行,经常导致被注入程序崩溃。

4、APC注入因为受目标进程使用API的条件而受限,并且处于等待的线程被注入后会立即返回,也有可能造成线程的运行错误,所以应用起来不是很通用。

5、win7 测试成功,Win10不是每次成功。另外没有提供删除APC队列中函数的方法,所以不能反复注入。

6、必须枚举目标进程所有线程,(由于并不是每个线程都有机会进入可变等待状态,为了增加APC的机会,向目标进程的每个线程都添加APC是个比较保险的做法)


发布日期:

所属分类: 编程 标签:   


没有相关文章!