Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html
APC
1. APC的本质
一个线程,其一直占用着CPU,对CPU拥有所有权,不可能从外部改变行为。
APC的本质就是:通过一个函数,让线程执行,从而可以从外部改变该线程的行为。
1)APC的挂入位置 _KAPC_STATE
_ETHREAD+0x034 ApcState,存在一个_KAPC_STATE结构体:
其存在两个双向链表,称为APC队列,一个挂内核APC队列,一个挂用户APC队列。
//0x18 bytes (sizeof) struct _KAPC_STATE {
struct _LIST_ENTRY ApcListHead[2]; //0x0 用户/内核 APC队列
struct _KPROCESS* Process; //0x10 所挂靠的进程
UCHAR
KernelApcInProgress; //0x14 当前内核APC函数是否执行 0/1
UCHAR KernelApcPending; //0x15 是否存在内核APC 0/1
UCHAR UserApcPending; //0x16 是否存在用户APC 0/1
};
2)APC存储的单元 _KAPC
前面我们介绍过 _KAPC_STATE,其中一个Entry_List,里面一个个结构就是 _KAPC结构,如下:
//0x30 bytes (sizeof)
struct _KAPC {
SHORT Type; //0x0 类型
SHORT Size; //0x2 大小
ULONG Spare0; //0x4 未发现使用
struct _KTHREAD* Thread; //0x8 目标线程
struct _LIST_ENTRY ApcListEntry; //0xc APC队列挂的位置(双向链表)
VOID (*KernelRoutine)(struct _KAPC* arg1, VOID (**arg2)(VOID* arg1, VOID* arg2, VOID* arg3), VOID** arg3, VOID** arg4, VOID** arg5); //0x14 APC完成释放内存
VOID (*RundownRoutine)(struct _KAPC* arg1); //0x18
VOID (*NormalRoutine)(VOID* arg1, VOID* arg2, VOID* arg3); //0x1c APC函数所在的位置:
如果是内核APC,其是函数地址;如果是用户APC,则是三环总入口
VOID* NormalContext; //0x20 VOID* SystemArgument1; //0x24 内核APC:略;用户APC:当前函数的总入口。
VOID* SystemArgument1; //0x24 APC函数的参数
VOID* SystemArgument2; //0x28 APC函数的参数
CHAR ApcStateIndex; //0x2c 挂哪个队列,有四个值 0,1,2,3
CHAR ApcMode; //0x2d UCHAR 用户APC 内核APC
Inserted; //0x2e 表示当前APC是否已经挂入
};
3)APC函数何时执行
关注一下KiServiceExit,从零环返回三环就通过这个函数,该函数是系统调用、异常和中断的必经之路。
APC处理函数通过 _KiDeliverApc 函数来执行,而 KiServiceExit 上来就先检查是否存在用户APC,如果有就调用该函数来执行。
该函数先判断是否存在内核APC,如果内核APC存在就先执行内核APC,然后再执行用户APC。
2. 备用APC队列
_Kthread+0x14c SavedApcState存在一个备用APC队列,其与 +0x034 ApcState位置结构体完全一样。
其和进程挂靠相关,如果不了解,可以去看《进程与线程》一节,该节后面介绍了进程挂靠相关细节。
1)线程APC中的函数都是与进程相关联的
线程APC中的函数要执行,执行的是当前CR3的内存地址,但是线程可以挂靠,当线程A挂靠到其他进程的CR3时,
如果此时线程A的APC函数要进行内存读写,其就会读写挂靠进程的内存地址,显然会发生错误。
2)SavedApc作用:
SavedApc函数就是为了避免当出现线程挂靠时内存读取错误,当线程挂靠时,其将该线程的APC存储到SavedApc中。
等到解除挂靠,再还原回来,这样就避免了内存执行错误。
3)SavedApc真实运行策略:
在挂靠环境下,也是可以向当前线程插入APC的,比如X进程中A线程挂靠T进程,此时也可以插入APC函数,只不过针对B进程的。
ApcState:B进程相关的APC函数。
SavedApcState:A进程相关的APC函数。
4)_KTHREAD+0x138 ApcStatePointer[2]:
Windows为了方便操作这两个_APC_STATE,设置了一组指针,在_KTHREAD+0x138处 ApcStatePointer[2],其操作情况如下。
因此,如果找原线程的APC,直接ApcStatePointer[0]就好,找Saved就找ApcStatePointer[1],很好理解。
5)_KTHREAD+0x165 ApcStateIndex 实现组合寻址
0 正常状态 / 1 挂靠状态
其经常会结合ApcStatePointer来进行寻址。
A进程的线程挂靠B进程,如果在非挂靠的情况下,此时插入的是A进程的APC,因此为ApcStatePointer[ApcState];
如果此时在挂靠情况下,插入的进程就是关于B进程的APC,此时A进程的APC被备份到SavedApcState,B进程的也为ApcStatePointer[ApcState]。
6)_KTHREAD+0x166 ApcQueueAble
表示当前线程是否可以插入APC,比如线程退出时,不允许插入APC。
此时会将ApcQueueAble置为0,则进制APC挂入。
3. APC的插入
1)KeInitalizeApc函数分析
该函数声明如下,简单来说就是对应KAPC中的各个成员(可在文章开头查看)
2)KAPC.ApcStateIndex 作用
注意,其与KTHREAD.ApcStateIndex同名,但其值只有0/1,我们在之前的进程挂靠讲过,配合ApcStatePointer来指向有关地址。
0 原始环境 ;1 挂靠环境 ;2 当前环境 ;3 插入APC时的当前环境。
结合挂靠那一节,我们来分析下面的各种情况,以A进程的线程挂靠B进程为例(可能有点乱,一定结合上面挂靠来看)
0 原始环境:ApcStatePointer[0] 正常:ApcState;挂靠:SavedApcState,其都是写入A进程的ApcState。
1 挂靠环境:ApcStatePointer[1] 正常:SavedApcState;挂靠:ApcState,都是写入B进程的ApcState。
2 当前环境:其在初始化时修改为当前线程的Kthread.ApcStateIndex,Pointer[ApcStateIndex],挂靠哪个插入哪个。
3 插入Apc时当前环境: 真正指向插入时(KiInserQueueApc),再做判断,插入当前进程的Apc中。(初始化到插入时,可能APC又被修改)
3)KiInsertQueueApc函数分析
该函数虽然长,但结构体比较单一,很好分析其对应的操作步骤。
4)Kthread+0x164 Alterable属性
Kthread+0x164 Alterable,其表示是否可以被用户APC唤醒。
我们在挂起线程调用SleepEx或WaitForSingleObjectEx,其最后一个参数就是修改这个值(注意,必须是Ex结尾的函数)。
当在KiInsertQueueApc插入用户KAPC之后,其会判断是否需要唤醒当前线程,如果此时值为1,则唤醒线程执行用户APC。
4. 内核APC执行过程
1)APC函数的执行与插入不是一个线程
A线程向B线程插入一个APC,插入的动作在A线程中完成的,但什么时候执行则由B线程决定!所以叫“异步过程调用”。
内核APC函数与用户APC函数的执行时间和执行方式也有区别。
2)内核APC的时机
①SwapContext
我们在线程切换时,会判断是否要有用户APC执行,注意,此时作为SwapContext的返回值返回,其一直返回到KiSwapThread中。
此时如果返回值为1,其会调用KiDeliverApc函数来处理当前线程的Apc。
②KiServiceExit
KiServiceExit中也会判断是否存在用户APC,调用KiDeliverApc函数来执行。
3)KiDeliverApc函数分析
内核APC如下(注意,其_LIST_ENTRY偏移在中间,故看起来很不美观),其直接从_KAPC中取出kernel
5. 用户APC执行过程
1) 用户APC函数的执行时机
当程序在零环执行完成返回三环时,其调用_KiServiceExit,此时其调用_KiDeliverApc来检查是否有派发的APC函数,然后执行。
2) 用户APC执行流程
当发现有用户APC要执行时,其处于零环,要执行必须返回三环。
执行流程为:零环->三环(执行用户APC)->零环->三环(正常退出)。
之前我们在系统调用这中提到过如何从三环进到零环,其三环现场保存在_KTRAP_FRAME(_ETHREAD+0x124)这个结构体中。
此时回去肯定不能从_KTRPA_FRAME中返回三环。
3)构建_CONTEXT结构体返回三环
返回三环时根据_KTRAP_FRAME.Eip来返回三环,因此我们想要处理用户APC,其必须修改KTrapFrame.EIP。
1> KiDiverApc函数中调用KiInitalUserApc来初始化用户APC环境
2> KiInitalUserApc函数中调用KiContextFromKframes将TrapFrame转换为CONTEXT结构体
这一步的目的是为了备份原来的TrapFrame,因为返回三环必然修改TrapFrame,因此将旧的转换为CONTEXT预先放到三环的堆栈。
3> KiInitalUserApc函数中将_APC_RECORD和_CONTEXT保存到三环的堆栈中
虽然此时处于零环,但是可以从TrapFrame.esp来获取三环的堆栈地址,然后将两者保存进去。
因为APC执行完之后还必须从三环进入零环,此时直接在堆栈进行操作进行复原即可。
1* 获取esp并计算提升堆栈大小
2* 将 _Context 写入三环地址
3* 将ApcRecord写入三环地址
4* 保存之后三环的堆栈空间
4> KiInitalUserApc函数中修改TrapFrame为返回三环做准备
其修改很多TrapFrame的值,但对于我们最重要的就是回到三环后的落脚点。
其回到KeUserApcDispatch来执行用户的APC函数,至于函数地址,ApcRecord.NormalContext存放的是真正的APC函数。
4)总结
理解上面过程,此时,我们就可以通过KiServiceExit函数利用KTrapFrame来返回用户层,其返回的就是KiInitalizeUserApc函数,然后执行用户APC。
当用户APC执行完成之后,返回零环,此时就是Context。我们直接在三环把Context再转换为KtrapFrame,这之后就很好理解了。