APC初始化

1.APC初始化 R3APC插入

简述

线程出现等待的情况下sleep WaitForSingleObject线程的资源再利用

apc--异步过程调用本质上就是一个异步call,线程本身是一直走,直到出现等待。但是有的时候需要当线程的某一个函数执行完,产生一个通知,但是如果是采用等待的方式的话,那么对于一些UI相关的不够友好,因为卡在那里了。异步call就是为了解决这个需求,当执行了需要通知的函数,不会一直卡在那里而是会一直向下走且不创建新的线程。线程会在某个时刻触发这个异步call,不过因为触发的地方很多,所以基本上是插入就执行。

apc都是插入到线程的,在线程结构中有两个链表,一个是内核链表(在上面),一个是用户链表(在下面),apc就是插在这里面,很多线程在R3->R0或者R0中的时候都会检查自身有没有apc,有的话会优先执行。

apc是插入进去后,线程中会有某些函数会进行检查是否有apc,有的话调用,所以称为异步

同步是等待的,异步是不等待。同步就是等事情做完后再执行(一直等待直到触发那个事件),是顺序的。异步是不等待,一直执行,同时注意有没有触发指定事件,如果有的话,就先停下之前的事情,执行触发后需要执行的动作

与apc相关的字段

线程中

详细信息看4.3的线程结构

Windbg执行dt _kthread查看线程结构

其中与apc有关的如下

+0x03a Alerted          : [2] UChar //警惕,0内核1,三环
+0x03c Alertable        : Pos 5, 1 Bit//可警惕,是否可以唤醒,只有填是,警惕才有意义
+0x040 ApcState         : _KAPC_STATE//r0下和警惕性无关,r3下必须是可警惕
+0x0b8 ApcQueueable     : Pos 5, 1 Bit//是否允许apc插入队列,为0的话代表调用api无法插入,默认1
+0x168 ApcStatePointer  : [2] Ptr32 _KAPC_STATE
+0x170 SavedApcState    : _KAPC_STATE
+0x134 ApcStateIndex    : UChar//索引值

ApcStatePointer

本质上是存了两个指针,代表+0x040 ApcState+0x170 SavedApcState

+0x040 ApcState当前上下文环境

+0x170 SavedApcState备用的上下文环境

+0x134 ApcStateIndexapc索引

我们的线程在内核环境下是可以脱离进程的(切换进程上下文),保留原始环境表,为还原使用

没有挂靠的时候,原始apc链表是有值的,备用是空的,ApcStatePointer[0] = ApcStateApcStatePointer[1] = SavedApcState同时+0x134 ApcStateIndex索引值是0,这个索引代表ApcStatePointer[index]是这个的索引号

已挂靠的时候,将ApcState中的值复制到SavedApcState中,之后清空ApcState的值,将ApcStateIndex的改成1

KAPC_STATE

kd> dt _kapc_state
ntdll!_KAPC_STATE
   +0x000 ApcListHead      : [2] _LIST_ENTRY//1->R3,0->R0
   +0x010 Process          : Ptr32 _KPROCESS//当前环境的进程,挂靠后就是挂靠的进程
   +0x014 KernelApcInProgress : UChar//内核apc是否正在执行
   +0x015 KernelApcPending : UChar//内核apc是否可以执行,1的时候`ApcListHead`有值,反之没有
   +0x016 UserApcPending   : UChar//用户apc是否可以执行,1的时候`ApcListHead`有值,反之没有

kapc

可以理解为KAPC_STATEapc管理器,kapc才是真正的具体的apc结构

_kapc_state+0x000 ApcListHead 指向的是_kapc

kd> dt _kapc
ntdll!_KAPC
   +0x000 Type             : UChar//apc类型
   +0x001 SpareByte0       : UChar//保留
   +0x002 Size             : UChar//apc结构大小
   +0x003 SpareByte1       : UChar//保留
   +0x004 SpareLong0       : Uint4B//保留
   +0x008 Thread           : Ptr32 _KTHREAD//当前apc的结构是挂在那个线程上的
   +0x00c ApcListEntry     : _LIST_ENTRY//和KAPC_STATE串联,减去0x0c才可以得到kapc
   +0x014 KernelRoutine    : Ptr32     void //内核函数
   +0x018 RundownRoutine   : Ptr32     void //特殊的函数
   +0x01c NormalRoutine    : Ptr32     void //正常的apc的函数表,如果是用户态apc必须写,内核apc可写可不写
   +0x020 NormalContext    : Ptr32 Void//正常的apc的函数表对应的参数1
   +0x024 SystemArgument1  : Ptr32 Void//正常的apc的函数表对应的参数2
   +0x028 SystemArgument2  : Ptr32 Void//正常的apc的函数表对应的参数3
   +0x02c ApcStateIndex    : Char//和线程中的ApcStateIndex没有关系
   +0x02d ApcMode          : Char//模式,用户apc节点还是用户apc节点,0代表内核,1代表用户
   +0x02e Inserted         : UChar//插入完是1,记录自己是否被插入过

RundownRoutine

特殊的情况下执行这里面的函数,例如线程挂起/线程结束

普通的派发情况下不执行

NormalRoutine

用户态必须写这个函数

内核态没写这个函数的话叫做特殊内核apc,写叫做普通内核apc

特殊内核apc是插入在ApcListHead这个链表的头部

普通内核apc是插入在ApcListHead这个链表的尾部

也就是特殊的优先级比较高

ApcStateIndex

和线程中的ApcStateIndex没有关系

挂靠apc的四种环境

  1. 无论挂不挂靠,apc都插入到原始环境(插入到创建者的上下文中),本质上就是插入到ApcState表中
  2. 无论挂不挂靠,apc都插入到挂靠环境(插入到创建者的上下文中),本质上就是插入到SavedApcState表中
  3. 选择插入,在初始化apc函数中,看线程中的ApcStateIndex,如果等于0,就插入到原始环境,否则插入到挂靠环境
  4. 选择插入,初始化函数不插入,在插入apc函数的时候选择插入,判断原理同上一个

这四种环境对应了ApcStateIndex0,1,2,3

函数声明

WRK中的KeInitializeApc初始化apc函数声明

VOID
KeInitializeApc (
    __out PRKAPC Apc,//APC结构
    __in PRKTHREAD Thread,//apc要插入哪个线程
    __in KAPC_ENVIRONMENT Environment,//插入的环境
    __in PKKERNEL_ROUTINE KernelRoutine,//内核回调,必须写
    __in_opt PKRUNDOWN_ROUTINE RundownRoutine,//填NULL就行
    __in_opt PKNORMAL_ROUTINE NormalRoutine,//挂在`ApcState`链表中还是`SavedApcState`链表中
    __in_opt KPROCESSOR_MODE ApcMode,
    __in_opt PVOID NormalContext
    )

KAPC_ENVIRONMENT环境和上面的ApcStateIndex中所说的4种环境一致

typedef enum _KAPC_ENVIRONMENT {
    OriginalApcEnvironment,//原始环境
    AttachedApcEnvironment,//挂靠环境
    CurrentApcEnvironment,//当前环境
    InsertApcEnvironment//插入后选择环境
} KAPC_ENVIRONMENT;

R3下APC注入代码

注入器main.c

#include <windows.h>
#include <stdio.h>
int main()
{
	//打开进程
	HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 1044);
	if (!hProcess)
	{
		printf("打开进程失败\r\n");
		return -1;
	}
	//打开线程
	HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, 1896);
	if (!hThread)
	{
		printf("打开线程失败\r\n");
		return -1;
	}
	//获取loadlibraryA
	HMODULE hModule = GetModuleHandleA("kernel32.dll");
	PVOID func = (PVOID)GetProcAddress(hModule, "LoadLibraryA");
	printf("%x\r\n", func);
	//system("pause");
	//给目标进程申请内存,存dll路径
	PUCHAR targetMemory = (PUCHAR)VirtualAllocEx(hProcess, NULL, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
	if (!targetMemory)
	{
		printf("申请内存失败\r\n");
		return -1;
	}
	printf("targetMemony :%x\r\n", targetMemory);
	//system("pause");
	//返回大小
	SIZE_T proc = NULL;
	//dll路径
	char* dllpath = "C:\\Users\\Administrator\\Desktop\\testDll.dll";
	//给目标进程写内容 
	//写+1的原因,因为`WriteProcessMemory`函数到`\0`就结束了,所以这个`dllpath`没有写`\0`
	//又因为`VirtualAllocEx`申请的内存没有初始化,所以里面可能有很多的0xcc所以我们需要将dllpath和`\0`一起写进去,所以要+1
	if (!WriteProcessMemory(hProcess, targetMemory, dllpath, strlen(dllpath) + 1, NULL))
	{
		printf("写入失败\r\n");
		return -1;
	}
	/* 接下来是apc注入*/

	//插入apc
	//3个参数,第一个是要执行的函数,第二个是要插入的线程,第三个是带进去的参数
	QueueUserAPC((PAPCFUNC)func, hThread, (ULONG_PTR)targetMemory);
	system("pause\r\n");
	return 0;
}

用来测试的dll

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		MessageBox(NULL, L"1", L"TEST", MB_OK);
		break;
	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
	return TRUE;
}

被注入的进程

#include <windows.h>
#include <stdio.h>
int main()
{
	while (1)
	{
		//TRUE代表可以唤醒,在插入apc的时候可以运行起来
		SleepEx(1000, TRUE);
		printf("-----test-----\r\n");
	}
	return 0;
}

效果

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