APC

Asyncroneus Procedure Call

什么是APC

Asyncroneus Procedure Call也就是异步过程调用

image-20200805112723064

windbg查看一波,位于kthread+0x34处

image-20200805112751269

上图是apc状态结构体成员

第一个是个双向链表,分为俩个队列,用户态和内核态,所以APC注入也分三环零环

process是线程所属或挂靠的进程

下面三都是标志位,第一个表示是否有内核apc函数在运行

然后是表示是否有内核apc和是否有用户apc正在等待中

image-20200805114122583

再来看一下备用APC队列

image-20200810201936038这个主要是在进程挂靠时起作用,保存原始进程的apc队列

当进程恢复时再根据备用apc队列恢复原始apc队列

image-20200810201717250

这边是ApcStatePointer

为了操作方便,_KTHREAD结构体中定义了一个指针数组ApcStatePointer ,长度为2。

正常情况下:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState

挂靠情况下:
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState

image-20200810202058017

+0x165 ApcStateIndex用来表示apc状态

0 正常状态 1 挂靠状态

结合一下ApcStatePointer可以发现ApcStatePointer[ApcStateIndex]永远表示当前apc队列

image-20200810202316162

紧接着他的是ApcQueueable,这用来表示是否能向当前线程插入apc,当线程正在执行退出的代码时,会将这个值设置为0 ,如果此时执行
插入APC的代码(KeInsertQueueApc),在插入函数中会判断这个值的状态,如果为0,则插入失败。

之所以叫apc呢主要是因为当apc执行,一般都是俩个线程A,B

B线程向A线程中插入APC然后等A线程执行时再执行APC,所以叫异步过程调用

具体见下

image-20200812164609526

APC挂入

image-20200810203214212

这边来介绍下APC的挂入过程

ring3

1
2
3
4
5
DWORD WINAPI QueueUserAPC(
_In_ PAPCFUNC pfnAPC,
_In_ HANDLE hThread,
_In_ ULONG_PTR dwData
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdint.h>
#include <pthread.h>
#include <time.h>
#include <windows.h>
#include <string.h>
#include <tlhelp32.h>
#pragma comment(lib, "pthreadVC2.lib")
DWORD WINAPI threadproc();
void WINAPI apcfunc(LPVOID dwParam);
int main()
{
HANDLE hthread = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadproc, NULL, 0, NULL);
QueueUserAPC((PAPCFUNC)apcfunc, hthread, (DWORD)(PVOID)"This is APC\n");
getchar();
return 0;
}
void WINAPI apcfunc(LPVOID dwParam)
{
printf("%s", (PVOID)dwParam);
}
DWORD WINAPI threadproc()
{
while (true)
{
SleepEx(1000, 1);
printf("hello\n");
}
return 0;
}
/*同理转换下可以注其他程序

注意下这边的SleepEx(1000, 1);

用户模式的APC需要目标线程处在alertable等待状态才能被成功的调度执行*/

一般情况下都选择直接使用QueueUserApc来挂入APC,当然也能用_NtQueueApcThread@20,后者可以不使用IntCallUserApc、换成自己的函数,函数参数也可以有三个了,而PARCFUNC只有一个参数,整体区别不大

image-20200812160006391

ring0

见流程图,首先从KeInitializeApc开始,该函数主要是填充KAPC结构体

KiInsertQueueApc根据KAPC结构中的ApcStateIndex找到对应的APC队列,再根据KAPC结构中的ApcMode确定是用户队列还是内核队列,将KAPC挂到对应的队列中(挂到KAPC的ApcListEntry处),再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态,修改KAPC_STATE结构中的KernelApcPending/UserApcPending

内核APC执行

既然已经知道APC挂入流程了,现在来康康APC执行

image-20200812165125835

系统调用、中断、异常处理时系统都会查看APC队列是否有需要执行的APC,因为每当发生时都会执行KiServiceExit

下面来逆一波KiServiceExit

image-20200812174017589

检查是否有用户apc,有则处理

image-20200812183050966

再看看KiDeliverApc

image-20200812183623033

这边有个判断内核APC是否为空,不空则跳转

image-20200812191529622

然后判断函数地址是否为空,并往栈中填充参数

image-20200812192455067

下面就是负责执行内核apc并删除队列恢复状态然后再次循环

image-20200812183108021

流程如上,这边注意下ApcState.UserApcPending必须为真,这边就是说明只有可唤醒得线程才会投递APC

用户APC执行

用户APC相对内核来说就复杂多了,因为涉及到许多堆栈切换操作

首先得从内核态到用户态,再到内核态

而且到用户态不能回到当初进入内核态处,得回到对应apc函数入口

image-20200812202353445

具体用户态APC如上,首先判断条件,第一个参数是否为1,是否有用户态apc再执行,用户态apc队列是否为空

满足条件后讲ApcState.UserApcPending置0然后开始处理,删除队列,释放KAPC结构体内存

然后开始KiInitializeUserApc

image-20200812203145476

image-20200812203213871

用户空间堆栈完事后开始各种初始化

image-20200812203303206

填充寄存器

image-20200812203326893

最后让eip指向KeUserApcDispatcher

image-20200812203452432

apc处理完后执行_ZwContinue@8返回内核恢复trapframe继续上面得循环判断处理,如没有则恢复trapframe然后回三环原来进0环的地方

image-20200812203825208

以后会写APC注入,用户态和内核态的

用户态的很简单其实,用上面发的代码改一改,注自己线程换成注别的,拿进程pid然后拿线程句柄后写内存然后照搬套路。

内核态有空写下。

文章目录
  1. 1. 什么是APC
  2. 2. APC挂入
    1. 2.1. ring3
    2. 2.2. ring0
  3. 3. 内核APC执行
  4. 4. 用户APC执行
|