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,第二是因为系统也会挂靠

posted @ 2024-05-18 17:27  MuRKuo  阅读(11)  评论(0编辑  收藏  举报