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 ApcStateIndex
apc索引
我们的线程在内核环境下是可以脱离进程的(切换进程上下文),保留原始环境表,为还原使用
没有挂靠的时候,原始apc链表是有值的,备用是空的,ApcStatePointer[0] = ApcState
且ApcStatePointer[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_STATE
是apc
管理器,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的四种环境
- 无论挂不挂靠,apc都插入到原始环境(插入到创建者的上下文中),本质上就是插入到
ApcState
表中 - 无论挂不挂靠,apc都插入到挂靠环境(插入到创建者的上下文中),本质上就是插入到
SavedApcState
表中 - 选择插入,在初始化apc函数中,看线程中的
ApcStateIndex
,如果等于0,就插入到原始环境,否则插入到挂靠环境 - 选择插入,初始化函数不插入,在插入apc函数的时候选择插入,判断原理同上一个
这四种环境对应了ApcStateIndex
的0,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;
}