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结构体中我们主要需要关注ApcState
和SavedApcState
字段。两个字段都为_KAPC_STATE数据结构(这不是巧合)。
:::
ApcListHead
字段:APC队列头,是两个APC队列。分别代表内核APC和用户APC。
:::
Process
:线程所属进程或者所挂靠的进程
KernelApcInProgress
:内核APC是否正在执行
KernelApcPending
:是否有正在等待执行的内核APC
UserApcPending
:是否有正在等待执行的用户APC
下面来说一下ApcState
和SavedApcState
关系:当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
为了方便寻址,系统还提供了ApcStatePointer
与ApcStateIndex
两个字段。
ApcStateIndex
typedef enum _KAPC_ENVIRONMENT{
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
}KAPC_ENVIRONMENT;
实际可用于ApcStateIndex
的只是OriginalApcEnvironment
和AttachedApcEnvironment
,即0和1。
系统以ApcStateIndex
的数值作为下标,从指针数组ApcStatePointer[2]
中就可以得到指向ApcState
或SavedApcState
的指针。当ApcStateIndex
为0时指向的是ApcState
;当处于挂靠环境,ApcStateIndex
数值为1,指向的是SavedApcState
。
下面用实例进行验证,当ApcStateIndex
为0时,表示正常环境状态,则ApcStatePointer[0]
指向的是ApcState
。
:::
当ApcStateIndex
为1时,即在挂靠环境下,ApcStatePointer[1]
指向的是ApcState
。即无论什么环境下,ApcStatePointer[ApcStateIndex]
指向的都是ApcState,ApcState则总是表示线程当前使用的APC状态。
:::
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
这里主要关注两个变量NormalRoutine
和KernelRoutine
。当NormalRoutine
参数为空时,系统执行KernelRoutine
。当NormalRoutine
不为空时,系统仍然会先执行KernelRoutine
,并将NormalRoutine
作为参数。待KernelRoutine
执行完毕后,再次判断NormalRoutine
是否为空,不为空,则执行NormalRoutine
。
:::
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返回到用户空间才能执行,此处只是做好准备。
不管是内核模式还是用户模式,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结构体。然后进行暴力枚举对比,调用PsLookupThreadByThreadId
和IoThreadToProcess
获取进程的EProcessB结构体,通过EProcessA
和EProcessB
进行对比,即可获取特定进程的ETHREAD结构。(还有一种方法,调试是可行的。通过pid获取进程EProcess
结构体,进而获取_KPROCESS
结构体,在_KPROCESS结构体中硬编码偏移获取ThreadListHead)
思路是通过ring 3接收要杀掉的进程名,同时实现装载驱动的功能,ring 0层接收pid执行上述步骤即可。但是当装载驱动写注册表时被卫士拦截,关掉即可正常运行。
运行效果:
参考链接:
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
DC8-通关手册 DC-8是另一个专门构建的易受攻击的实验室,目的是在渗透测试领域积累经验。 这个挑战有点复杂,既是实际挑战,又是关于在Linux上安装和配置的两因素身份验证是否可以阻止Linux服务器被利用的“概念证明”。 由于在Twitter上询问了有关双…
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论