驱动病毒那些事(三)----APC注入

  • 驱动病毒那些事(三)----APC注入已关闭评论
  • 27 views
  • A+

APC注入是DLL注入的一种,恶意软件可以利用异步过程调用(APC)控制另一个线程通过将其附加到目标线程的APC队列来执行其自定义代码。APC分为两种类型,内核模式和用户模式,本文讨论的APC注入为内核层级的APC注入。

介绍APC注入之前,先了解一下APC中几个常见的结构体。

APC

_KAPC_STATE

typedef struct _KAPC_STATE {
LIST_ENTRY ApcListHead[MaximumMode]; //线程的APC链表
struct _KPROCESS *Process; //当前线程的进程体
BOOLEAN KernelApcInProgress; //是否有内核APC正在执行
BOOLEAN KernelApcPending; //是否有正在等待执行的内核APC
BOOLEAN UserApcPending; //是否有正在等待执行的用户APC
} KAPC_STATE, *PKAPC_STATE, *PRKAPC_STATE;

APC是针对具体线程,由具体线程加以执行的,所以每个线程都有自己的APC队列。内核中代表着线程的数据结构是ETHREAD,而ETHREAD中的第一个成分Tcb 是KTHREAD数据结构,线程的APC队列就在KTHREAD里面。

在KTHREAD结构体中我们主要需要关注ApcStateSavedApcState字段。两个字段都为_KAPC_STATE数据结构(这不是巧合)。

1.png

:::

ApcListHead字段:APC队列头,是两个APC队列。分别代表内核APC和用户APC。

2.png

:::

Process:线程所属进程或者所挂靠的进程
KernelApcInProgress:内核APC是否正在执行
KernelApcPending:是否有正在等待执行的内核APC
UserApcPending:是否有正在等待执行的用户APC

下面来说一下ApcStateSavedApcState关系:当A线程运行在B进程中时,A线程中所有的APC函数要访问的地址都属于B进程的。但Windows内核是允许跨进程的操作的,一个线程C运行暂时挂靠在另一进程D,挂靠C线程执行完毕后,需返回原始进程。此时就需要一个数据结构用来保存原始环境,这就是SavedApcState的作用。

在挂靠环境下,A进程的T线程挂靠B进程,A是T的所属进程,B是T的挂靠进程。
ApcState:B进程相关的APC函数
SavedApcState:A进程相关的APC函数
在正常情况下,当前进程就是所属进程A,如果是挂靠情况下,当前进程就是挂靠进程B

为了方便寻址,系统还提供了ApcStatePointerApcStateIndex两个字段。

ApcStateIndex

typedef enum _KAPC_ENVIRONMENT{
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
}KAPC_ENVIRONMENT;

实际可用于ApcStateIndex的只是OriginalApcEnvironmentAttachedApcEnvironment,即0和1。

系统以ApcStateIndex的数值作为下标,从指针数组ApcStatePointer[2]中就可以得到指向ApcStateSavedApcState的指针。当ApcStateIndex为0时指向的是ApcState;当处于挂靠环境,ApcStateIndex数值为1,指向的是SavedApcState

下面用实例进行验证,当ApcStateIndex为0时,表示正常环境状态,则ApcStatePointer[0]指向的是ApcState

3.png

4.png

:::

ApcStateIndex为1时,即在挂靠环境下,ApcStatePointer[1]指向的是ApcState。即无论什么环境下,ApcStatePointer[ApcStateIndex]指向的都是ApcState,ApcState则总是表示线程当前使用的APC状态。

5.png

6.png

7.png

:::

Alertable

Alertable位于KTHREAD结构体中,是一个标志位。为True时,代表线程处于可唤醒状态。当从内核模式返回时,DISPATCH_USER_APC在交付用户模式APC前会判断这个标志,如果为FALSE,则不会交付User APC。只有当线程处于Alertable状态时,Usermode APC注入才能成功执行。
所以当我们在ring 3层APC注入dll时,可以调用KeTestAlertThread(UserMode)使进程处于可唤醒状态(方法不唯一)。

KeInitializeApc

这个函数用来初始化APC对象。

VOID KeInitializeApc
(
IN PKAPC Apc,//KAPC指针
IN PKTHREAD Thread,//目标线程
IN KAPC_ENVIRONMENT TargetEnvironment,//0 1 2 3四种状态
IN PKKERNEL_ROUTINE KernelRoutine,//销毁KAPC的函数地址
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine,//用户APC总入口或者内核apc函数
IN KPROCESSOR_MODE Mode,//要插入用户apc队列还是内核apc队列
IN PVOID Context//内核APC:NULL 用户APC:真正的APC函数
)

KeInsertQueueApc

APC对象完成初始化后,设备驱动调用KeInsertQueueApc来将APC对象存放到目标线程的相应的APC队列中。

BOOLEAN KeInsertQueueApc {
IN PRKAPC Apc, //APC 结构
IN PVOID SystemArgument1, //传给 APC 函数的参数
IN PVOID SystemArgument2,
IN KPRIORITY Increment //线程优先级增量
}

tip:当要插入的线程为当前线程时,APC会立刻执行。

Apc执行时机

下面就来看执行APC的时机:1.在(系统调用、中断、或异常处理之后)从内核返回用户空间的途中。2.线程切换,KiSwapThread返回以前调用一次KiDeliverApc
首先进行判断:1.即将返回用户空间 2.APC队列是否为空

KiDeliverApc()

满足条件后,调用KiDeliverApc(),对APC函数进行投递,可以把它理解为执行APC的函数。先看一下WRK源码中Routine Description。当从内核空间返回用户空间时,如果Pending标志位(KernelApcPending、UserApcPending)被设置,并且IRQL为0(PASSIVE_LEVEL)时,会调用此函数进行APC投递。投递顺序为优先投递特殊的内核APC,然后是普通的内核APC,最后投递用户APC。投递用户APC之前会进行判断,用户APC队列是否为空,UserApcPending被置位,模式是否为usermode。进入该例程后,IRQL设置为APC_LEVEL。下面通过源代码了解内部调用机制。

Kernelmode Apc

这里主要关注两个变量NormalRoutineKernelRoutine。当NormalRoutine参数为空时,系统执行KernelRoutine。当NormalRoutine不为空时,系统仍然会先执行KernelRoutine,并将NormalRoutine作为参数。待KernelRoutine执行完毕后,再次判断NormalRoutine是否为空,不为空,则执行NormalRoutine

8.png

9.png

10.png

:::

Usermode APC

执行前首先进行判断,用户APC队列是否为空,UserApcPending被置位,模式是否为usermode。用户APC执行逻辑与内核APC逻辑略有不同。第一点,用户APC不同于内核APC通过一个while循环,一口气执行完APC队列,而是只处理用户队列中的第一个请求。但是KiDeliverApc( )会重复调用(执行完KiDeliverApc函数后,系统会跳转至KiServiceExit函数,如果此时APC队列不为空,继续调用KiDeliverApc)。
tips: KiServiceExit函数是系统调用、异常或中断返回用户空间的必经之路。

第二点,如果KernelRoutine执行完毕后,NormalRoutine为空,则执行KeTestAlertThread()检测是否还有用户APC请求,该函数会改变 _ETHREAD结构体中Alertable和UserApcPending,使线程处于唤醒状态。如果NormalRoutine不为空,调用KilnitializeUserApc()。因为NormalRoutine所指的空间为用户空间,需要CPU返回到用户空间才能执行,此处只是做好准备。

11.png

12.png

不管是内核模式还是用户模式,APC请求中一定有KernelRoutine,而NormalRoutine则可能有也可能没有。

测试

我们可以通过APC注入,向指定线程内插入APC,调用PspExitThread(STATUS_SUCCESS),结束线程,进而达到杀指定进程的作用。
插入APC的难点在于定位到指定线程,有两种思路。
1.通过传入要杀的进程名如(calc.exe),调用ZwQuerySystemInformation函数获取_SYSTEM_PROCESSES结构体,通过偏移获取进程pid,调用PsLookupProcessByProcessId获取进程的EPROCESS结构体,调用PsGetProcessImageFileName得到进程名A,通过循环对比,进程名A与要杀的进程名进行匹配,即可得到进程pid。再次调用ZwQuerySystemInformation,循环匹配进程pid,进而获取threadid。通过PsLookupThreadByThreadId函数即可获取指定进程的ETHREAD结构。调用KeInitializeApc函数初始化APC,将PspExitThread(STATUS_SUCCESS)调用放在 KernelRoutine,将APC插入kernelmode即可(usermode也行,但是需要唤醒逻辑)。

2.获取pid过程略,假设已知进程pid,通过PsLookupProcessByProcessId获取进程的EProcessA结构体。然后进行暴力枚举对比,调用PsLookupThreadByThreadIdIoThreadToProcess获取进程的EProcessB结构体,通过EProcessAEProcessB进行对比,即可获取特定进程的ETHREAD结构。(还有一种方法,调试是可行的。通过pid获取进程EProcess结构体,进而获取_KPROCESS结构体,在_KPROCESS结构体中硬编码偏移获取ThreadListHead)

思路是通过ring 3接收要杀掉的进程名,同时实现装载驱动的功能,ring 0层接收pid执行上述步骤即可。但是当装载驱动写注册表时被卫士拦截,关掉即可正常运行。

运行效果:

13.png

14.png

参考链接:

1.https://bbs.pediy.com/thread-215368.htm
2.https://wenku.baidu.com/view/b555e6360b4c2e3f5727634f.html
3.https://www.cnblogs.com/xuanyuan/p/5384472.html
4.https://blog.csdn.net/qq_38474570/article/details/104326170?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_title-3&spm=1001.2101.3001.4242
5.https://blog.csdn.net/weixin_30906185/article/details/95745603?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.control

相关推荐: VulnHub-[DC-8-9]-系列通关手册

DC8-通关手册 DC-8是另一个专门构建的易受攻击的实验室,目的是在渗透测试领域积累经验。 这个挑战有点复杂,既是实际挑战,又是关于在Linux上安装和配置的两因素身份验证是否可以阻止Linux服务器被利用的“概念证明”。 由于在Twitter上询问了有关双…