APC挂靠
5.APC挂靠
用户态apc
和上一课的内核apc几乎一致,唯一的变动就是这个
//插入当前线程
KeInitializeApc(pKapc, eThread, OriginalApcEnvironment, KernelAPCRoutineFunc, NULL, 0x4011d0, UserMode, NULL);
改成了UserMode
函数地址改成了进程的地址0x4011d0
完整代码
Driver-main.c
#include <ntifs.h>
#include "struct.h"
//特殊apc
VOID KernelAPCRoutineFunc(
IN struct _KAPC* Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2
)
{
DbgPrint("----target1:%d---\r\n", PsGetCurrentProcessId());
//打印一句话然后释放内存
DbgPrint("KernelAPCRoutineFunc insert\r\n");
ExFreePool(Apc);
}
VOID Unload(PDRIVER_OBJECT pDriver)
{
DbgPrint("unload\r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//定义一个apc申请内存,因为要进到dpc等级上,所以不能分页
//不同版本的vs的`KAPC`结构体会有变化,所以需要增加一些大小
PKAPC pKapc = ExAllocatePool(NonPagedPool, sizeof(KAPC) + 0x100);
//清空内存
memset(pKapc, 0, sizeof(KAPC) + 0x100);
//插入外部线程apc
PETHREAD eThread = NULL;
PsLookupThreadByThreadId(3000, &eThread);
if (!eThread)
{
DbgPrint("获取线程失败\r\n");
ExFreePool(pKapc);
return STATUS_UNSUCCESSFUL;
}
DbgPrint("----main:%d---\r\n", PsGetCurrentProcessId());
//初始化apc
//插入当前线程
KeInitializeApc(pKapc, eThread, OriginalApcEnvironment, KernelAPCRoutineFunc, NULL, 0x4011d0, UserMode, NULL);
//插入apc
BOOLEAN isRet = KeInsertQueueApc(pKapc, NULL, NULL, 0);
//如果插入失败,释放内存
if (!isRet)
{
ExFreePool(pKapc);
}
pDriver->DriverUnload = Unload;
//DbgBreakPoint();
DbgPrint("TEST_Entry\r\n");
return STATUS_SUCCESS;
}
Driver-struct.h
#pragma once
#pragma once
#include <ntifs.h>
//定义和原型
//内核中最常用第一个,其他几个基本用不到,因为无论怎么样,线程最终都会回归原始的环境
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment,
InsertApcEnvironment
} KAPC_ENVIRONMENT;
typedef VOID(*PKNORMAL_ROUTINE) (
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
typedef VOID(*PKKERNEL_ROUTINE) (
IN struct _KAPC* Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2
);
typedef VOID(*PKRUNDOWN_ROUTINE) (
IN struct _KAPC* Apc
);
//初始化apc函数
VOID KeInitializeApc(
__out PRKAPC Apc,
__in PRKTHREAD Thread,
__in KAPC_ENVIRONMENT Environment,
__in PKKERNEL_ROUTINE KernelRoutine,
__in_opt PKRUNDOWN_ROUTINE RundownRoutine,
__in_opt PKNORMAL_ROUTINE NormalRoutine,
__in_opt KPROCESSOR_MODE ApcMode,
__in_opt PVOID NormalContext
);
//插入apc函数
BOOLEAN KeInsertQueueApc(
__inout PRKAPC Apc,
__in_opt PVOID SystemArgument1,
__in_opt PVOID SystemArgument2,
__in KPRIORITY Increment
);
测试程序
main.c
#include <windows.h>
#include <stdio.h>
//三个参数
VOID test(PVOID parm1, PVOID parm2, PVOID parm3)
{
printf("apc被执行了\r\n");
}
int main()
{
//打印函数地址
printf("Test Func = %x\r\n", test);
//打印线程地址
printf("Local Thread = %d\r\n", GetCurrentThreadId());
system("pause");
while (1)
{
printf("----1min----\r\n");
//可以唤醒的等待
SleepEx(1000, TRUE);
}
return 0;
}
一定要在程序程序执行等待后再apc插入(执行到while循环里面),否则会蓝屏!
实验
测试程序代码改成如下
#include <windows.h>
#include <stdio.h>
//三个参数
VOID test(PVOID parm1, PVOID parm2, PVOID parm3)
{
printf("apc被执行了\r\n");
}
int main()
{
//打印函数地址
printf("Test Func = %x\r\n", test);
//打印线程地址
printf("Local Thread = %d\r\n", GetCurrentThreadId());
system("pause");
while (1)
{
printf("----1min----\r\n");
//不可以唤醒的等待(死等)
Sleep(1000);
}
return 0;
}
不使用可以唤醒的SleepEx
而是使用Sleep
可以看到,此时再用上面的apc插入代码会失败
先windbg找到对应的进程!process 0 0
dt _kthread 872c2d48
查看一下ethread结构体中的警惕标志
+0x03c Alertable : 0y0
发现可警惕标志为0,代表不可以被唤醒
接下来我们改一下标志位
+0x03c KernelStackResident : 0y1 1
+0x03c ReadyTransition : 0y0 2
+0x03c ProcessReadyQueue : 0y0 3
+0x03c WaitNext : 0y0 4
+0x03c SystemAffinityActive : 0y0 1
+0x03c Alertable : 0y0 2
+0x03c GdiFlushActive : 0y0 3
+0x03c UserStackWalkActive : 0y0 4
+0x03c ApcInterruptRequest : 0y0
+0x03c ForceDeferSchedule : 0y0
因为Alertable
是第二个0的第二个,所以给那一位改成了2
想不明白就将每一位16进制转换成2进制对照一下
然后继续运行尝试插入apc
发现还是没有反应,失败
此时重新dt _kthread 872c2d48
查看一下结构
发现竟然被改回去了,原因是sleep是循环执行的,所以每次都会被重新执行的时候标志位都会被改回去
此时我们改一下代码,把sleep
的时间改长一些
Sleep(100000);
重新运行程序改一下标志位
可以看到可警惕已经被改成了1
再次加载驱动可以发现已经被唤醒了
这证明了我们插入apc的时候需要把这个标志改成1
用处:可以执行程序内部的代码(远程call)
代码实现思路
可以看一下WRK的KeAlertThread
具体代码
BOOLEAN
KeAlertThread (
__inout PKTHREAD Thread,
__in KPROCESSOR_MODE AlertMode
)
/*++
Routine Description:
This function attempts to alert a thread and cause its execution to
be continued if it is currently in an alertable Wait state. Otherwise
it just sets the alerted variable for the specified processor mode.
Arguments:
Thread - Supplies a pointer to a dispatcher object of type thread.
AlertMode - Supplies the processor mode for which the thread is
to be alerted.
Return Value:
The previous state of the alerted variable for the specified processor
mode.
--*/
{
BOOLEAN Alerted;
KLOCK_QUEUE_HANDLE LockHandle;
ASSERT_THREAD(Thread);
ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
//
// Raise IRQL to SYNCH_LEVEL, acquire the thread APC queue lock, and lock
// the dispatcher database.
//
KeAcquireInStackQueuedSpinLockRaiseToSynch(&Thread->ApcQueueLock, &LockHandle);
KiLockDispatcherDatabaseAtSynchLevel();
//
// Capture the current state of the alerted variable for the specified
// processor mode.
//
Alerted = Thread->Alerted[AlertMode];
//
// If the alerted state for the specified processor mode is Not-Alerted,
// then attempt to alert the thread.
//
if (Alerted == FALSE) {
//如果可警惕位是0的话
// If the thread is currently in a Wait state, the Wait is alertable,
// and the specified processor mode is less than or equal to the Wait
// mode, then the thread is unwaited with a status of "alerted".
//
if ((Thread->State == Waiting) && (Thread->Alertable == TRUE) &&
(AlertMode <= Thread->WaitMode)) {
KiUnwaitThread(Thread, STATUS_ALERTED, ALERT_INCREMENT);
} else {
Thread->Alerted[AlertMode] = TRUE;//将可警惕位置为1
}
}
//
// Unlock the dispatcher database from SYNCH_LEVEL, release the thread
// APC queue lock, exit the scheduler, and return the previous alerted
// state for the specified mode.
//
//接下来是切换线程,必然会触发apc
KiUnlockDispatcherDatabaseFromSynchLevel();
KeReleaseInStackQueuedSpinLockFromDpcLevel(&LockHandle);
KiExitDispatcher(LockHandle.OldIrql);
return Alerted;
}
由于这个是未文档化的函数,所以需要提前声明
完整代码 x86
struct.h
#pragma once
#pragma once
#include <ntifs.h>
//定义和原型
//内核中最常用第一个,其他几个基本用不到,因为无论怎么样,线程最终都会回归原始的环境
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment,
InsertApcEnvironment
} KAPC_ENVIRONMENT;
typedef VOID(*PKNORMAL_ROUTINE) (
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
typedef VOID(*PKKERNEL_ROUTINE) (
IN struct _KAPC* Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2
);
typedef VOID(*PKRUNDOWN_ROUTINE) (
IN struct _KAPC* Apc
);
//初始化apc函数
VOID KeInitializeApc(
__out PRKAPC Apc,
__in PRKTHREAD Thread,
__in KAPC_ENVIRONMENT Environment,
__in PKKERNEL_ROUTINE KernelRoutine,
__in_opt PKRUNDOWN_ROUTINE RundownRoutine,
__in_opt PKNORMAL_ROUTINE NormalRoutine,
__in_opt KPROCESSOR_MODE ApcMode,
__in_opt PVOID NormalContext
);
//插入apc函数
BOOLEAN KeInsertQueueApc(
__inout PRKAPC Apc,
__in_opt PVOID SystemArgument1,
__in_opt PVOID SystemArgument2,
__in KPRIORITY Increment
);
//更改线程的可警惕
BOOLEAN
KeAlertThread(
__inout PKTHREAD Thread,
__in KPROCESSOR_MODE AlertMode
);
main.c
#include <ntifs.h>
#include "struct.h"
//特殊apc
VOID KernelAPCRoutineFunc(
IN struct _KAPC* Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2
)
{
DbgPrint("----target1:%d---\r\n", PsGetCurrentProcessId());
//打印一句话然后释放内存
DbgPrint("KernelAPCRoutineFunc insert\r\n");
ExFreePool(Apc);
}
VOID Unload(PDRIVER_OBJECT pDriver)
{
DbgPrint("unload\r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//定义一个apc申请内存,因为要进到dpc等级上,所以不能分页
//不同版本的vs的`KAPC`结构体会有变化,所以需要增加一些大小
PKAPC pKapc = ExAllocatePool(NonPagedPool, sizeof(KAPC) + 0x100);
//清空内存
memset(pKapc, 0, sizeof(KAPC) + 0x100);
//插入外部线程apc
PETHREAD eThread = NULL;
PsLookupThreadByThreadId(2732, &eThread);
if (!eThread)
{
DbgPrint("获取线程失败\r\n");
ExFreePool(pKapc);
return STATUS_UNSUCCESSFUL;
}
DbgPrint("----main:%d---\r\n", PsGetCurrentProcessId());
//初始化apc
//插入当前线程
KeInitializeApc(pKapc, eThread, OriginalApcEnvironment, KernelAPCRoutineFunc, NULL, 0x4011c0, UserMode, NULL);
/* 将Alertable置为1,让他可警惕,才可以apc插入*/
*((PUCHAR)eThread + 0x3c) |= 0x20;
//插入apc
BOOLEAN isRet = KeInsertQueueApc(pKapc, NULL, NULL, 0);
//更改线程的apc并且通过切换线程唤醒apc
KeAlertThread(eThread, UserMode);
//如果插入失败,释放内存
if (!isRet)
{
ExFreePool(pKapc);
}
pDriver->DriverUnload = Unload;
//DbgBreakPoint();
DbgPrint("TEST_Entry\r\n");
return STATUS_SUCCESS;
}
x64
因为x64下线程是加密了的,所以我们需要使用一个为文档化函数进行解密
不过这里我这个函数可以直接使用了。。。
这个我感觉有点怪,驱动要编译成x64的,但是测试程序需要用x86的,也就是wow64
这个要注意几个点
第一个就是KeInitializeApc
初始化的时候需要填上一个解密前的函数地址
第二个就是需要在线程内部才可以使用PsWrapApcWow64Thread
否则没法使用,获取不到当前线程,没法解密
完整代码
main.c
#include <ntifs.h>
#include "struct.h"
//特殊apc
VOID KernelAPCRoutineFunc(
IN struct _KAPC* Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2
)
{
/*在指定线程里面后才可以取到当前线程的id,才可以进行地址转化*/
ULONG64 addr = 0x4011c0;
PsWrapApcWow64Thread(NULL, &addr);
/*跳转到我们需要执行的代码位置*/
*NormalRoutine = addr;
DbgPrint("----target1:%d---\r\n", PsGetCurrentProcessId());
//打印一句话然后释放内存
DbgPrint("KernelAPCRoutineFunc insert\r\n");
ExFreePool(Apc);
}
VOID Unload(PDRIVER_OBJECT pDriver)
{
DbgPrint("unload\r\n");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING pReg)
{
//定义一个apc申请内存,因为要进到dpc等级上,所以不能分页
//不同版本的vs的`KAPC`结构体会有变化,所以需要增加一些大小
PKAPC pKapc = ExAllocatePool(NonPagedPool, sizeof(KAPC) + 0x100);
//清空内存
memset(pKapc, 0, sizeof(KAPC) + 0x100);
//插入外部线程apc
PETHREAD eThread = NULL;
PsLookupThreadByThreadId(2496, &eThread);
if (!eThread)
{
DbgPrint("获取线程失败\r\n");
ExFreePool(pKapc);
return STATUS_UNSUCCESSFUL;
}
DbgPrint("----main:%d---\r\n", PsGetCurrentProcessId());
#ifdef _WIN64
/* 将Alertable置为1,让他可警惕,才可以apc插入*/
* ((PUCHAR)eThread + 0x4c) |= 0x20;
#else
/* 将Alertable置为1,让他可警惕,才可以apc插入*/
* ((PUCHAR)eThread + 0x3c) |= 0x20;
ULONG addr = 0x10000;
#endif
//初始化apc
//插入当前线程
/* x64下那个NormalRoutine要填解密前的地址 */
KeInitializeApc(pKapc, eThread, OriginalApcEnvironment, KernelAPCRoutineFunc, NULL, 0x4011c0, UserMode, NULL);
//插入apc
BOOLEAN isRet = KeInsertQueueApc(pKapc, NULL, NULL, 0);
//更改线程的apc并且通过切换线程唤醒apc
KeAlertThread(eThread, UserMode);
//如果插入失败,释放内存
if (!isRet)
{
ExFreePool(pKapc);
}
pDriver->DriverUnload = Unload;
//DbgBreakPoint();
DbgPrint("TEST_Entry\r\n");
return STATUS_SUCCESS;
}
struct.h
#pragma once
#pragma once
#include <ntifs.h>
//定义和原型
//内核中最常用第一个,其他几个基本用不到,因为无论怎么样,线程最终都会回归原始的环境
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment,
InsertApcEnvironment
} KAPC_ENVIRONMENT;
typedef VOID(*PKNORMAL_ROUTINE) (
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
typedef VOID(*PKKERNEL_ROUTINE) (
IN struct _KAPC* Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2
);
typedef VOID(*PKRUNDOWN_ROUTINE) (
IN struct _KAPC* Apc
);
//初始化apc函数
VOID KeInitializeApc(
__out PRKAPC Apc,
__in PRKTHREAD Thread,
__in KAPC_ENVIRONMENT Environment,
__in PKKERNEL_ROUTINE KernelRoutine,
__in_opt PKRUNDOWN_ROUTINE RundownRoutine,
__in_opt PKNORMAL_ROUTINE NormalRoutine,
__in_opt KPROCESSOR_MODE ApcMode,
__in_opt PVOID NormalContext
);
//插入apc函数
BOOLEAN KeInsertQueueApc(
__inout PRKAPC Apc,
__in_opt PVOID SystemArgument1,
__in_opt PVOID SystemArgument2,
__in KPRIORITY Increment
);
//更改线程的可警惕
BOOLEAN
KeAlertThread(
__inout PKTHREAD Thread,
__in KPROCESSOR_MODE AlertMode
);
//x64下解密用的函数
EXTERN_C NTSTATUS PsWrapApcWow64Thread(PVOID* ApcContext, PVOID* ApcRoutine);
挂靠
理论
原本的apc里面是有值的,如果是第一次挂靠对方线程,那么会将原本有值的APC_STATE
里面的值复制到SAVE_APC_STATE
链表,然后将APC_STATE
清空
如果是第二次挂靠,那么会将原本有值的APC_STATE
里面的值复制到PARAM_APC_STATE
链表,然后将APC_STATE
清空
本质上就是切换cr3,但是因为对方的进程可能被放到了磁盘上,所以需要经过一系列的判断
挂靠检测
使用这个函数进行检测,对当前线程的ApcStateIndex
进行检测,如果是1那么就是被挂靠了
但是这个检测不准,第一是因为你挂靠的速度很快,这个值可能一会是1一会是2,第二是因为系统也会挂靠