欢迎关注技术公众号:汉客儿
翻开
翻开小Win的菜单,APC赫然在目…
做工讲究,味道不错,是小Win的热门菜,我们点一来尝尝!
吃了可以做很多事情…
- APC注入
- APC注入
- APC注入
- …
细节来自于
ReactOS
源码分析。
如果对这个发神经的文风有任何不适,请谅解,因为我确实神经了
来一份APC
ring3这么做的
点APC的正确姿势是使用QueueUserApc
,不走寻常路的也可以使用NtQueueApcThread
1 | DWORD WINAPI QueueUserApc(PARCFUNC pfnApc, HANDLE hThread, ULONG_PTR dwData); |
也就是QueueUserApc内部是NtQueueApcThread做的,两者区别不大,当然,使用后者可以字节加点调料(不使用IntCallUserApc、换成自己的函数,函数参数也可以有三个了,而PARCFUNC只有一个参数)。
小Win默认是通过统一的接口IntCallUserApc来调用的顾客指定的Apc函数。
1 | static void CALLBACK |
ring0这么做的
NtQueueApcThread经过系统调用进入到ring0,一般人是看不到了…,我也是一般人来着,下面努力变成二班的…。
1. 创建APC对象
进了NtQueueApcThread,先通过KeInitializeApc初始化一个Apc对象1
2
3
4
5
6
7
8
9
10
/* Initialize the APC */
KeInitializeApc(Apc,
&Thread->Tcb, //KTHREAD
OriginalApcEnvironment,
PspQueueApcSpecialApc,
NULL,
ApcRoutine,
UserMode,
NormalContext);
APC对象结构定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18typedef struct _KAPC {
UCHAR Type; //类型ApcObject
UCHAR SpareByte0;
UCHAR Size; //APC结构体大小
UCHAR SpareByte1;
ULONG SpareLong0;
struct _KTHREAD *Thread; //当前线程的KTHREAD
LIST_ENTRY ApcListEntry; //当前线程的APC链表
PKKERNEL_ROUTINE KernelRoutine; //
PKRUNDOWN_ROUTINE RundownRoutine; //
PKNORMAL_ROUTINE NormalRoutine; //
PVOID NormalContext; //用户定义的Apc函数
PVOID SystemArgument1; //用户Apc函数的参数
PVOID SystemArgument2;//
CCHAR ApcStateIndex; //Apc状态
KPROCESSOR_MODE ApcMode; //Apc所处的Mode,UserMode/KernelMode
BOOLEAN Inserted; //是否已经被插入队列
} KAPC, *PKAPC, *RESTRICTED_POINTER PRKAPC;
根据KeInitializeApc传入参数,Apc被赋值如下:
1 | Apc->KernelRoutine = PspQueueApcSpecialApc; |
其中关于ApcStateIndex有4中值,如下:1
2
3
4
5
6
7
8
9// APC Environment Types
//
typedef enum _KAPC_ENVIRONMENT
{
OriginalApcEnvironment,//0
AttachedApcEnvironment,//1
CurrentApcEnvironment,//2
InsertApcEnvironment
} KAPC_ENVIRONMENT;
Apc->KernelRoutine总是有值的,被赋值为PspQueueApcSpecialApc,用于Apc结束时候释放Apc对象内存1
2
3
4
5
6
7
8
9
10
11VOID
NTAPI
PspQueueApcSpecialApc(IN PKAPC Apc,
IN OUT PKNORMAL_ROUTINE* NormalRoutine,
IN OUT PVOID* NormalContext,
IN OUT PVOID* SystemArgument1,
IN OUT PVOID* SystemArgument2)
{
/* Free the APC and do nothing else */
ExFreePool(Apc);
}
2. 插入APC队列
通过KeInsertQueueApc
插入队列,在队列中等待被上菜…
1 | KeInsertQueueApc(Apc, |
- 确认Apc未被插入,Thread->ApcQueueable为真
- Apc->Inserted = True
- 然后通过
KiInsertQueueApc
插入队列,可能通过软中断或者唤醒线程得到执行Apc的机会
1 | VOID |
先不管Apc是怎么得到执行的,来看看KAPC_STATE
1 | typedef struct _KAPC_STATE |
其中ApcListHead保存了线程的两个Apc链表,分别对应UserMode和KernelMode。
Thread->ApcState表示当前需要执行的ApcState,可能是挂靠进程的
Thread->SavedApcState表示挂靠后保存的当前线程的ApcState,
KTHREAD的ApcStatePointer[2]字段保存了两个ApcState的指针
具体看下面的代码
1 | KeAttachProcess-> |
来一个结构图
上菜吃饭
Apc已经点了,什么时候才能端上来呢?我们接着看…
Apc投递
线程wait、线程切换到应用层、线程被挂起等,一旦线程有空隙了,windows就会把apc队列顺便执行一遍
搜索NormalRoutine
和KernelRoutine
字段,找到KiDeliverApc
,这个函数是具体分发Apc的函数
1 | VOID |
那在哪里调用的KiDeliverApc的呢,找到多处
1 | //hal\halx86\generic\irq.S |
根据《windows内核情景分析》介绍, 执行用户APC的时机在从内核返回用户空间的途中(可能是系统调用、中断、异常处理之后需要返回用户空间)
也就是肯定会经过_KiServiceExit
,那就跟着来看看吧。
CHECK_FOR_APC_DELIVER
宏 检查是不是需要投递Apc,具体检查trapframe是不是指向返回用户模式的,是则继续检查用户模式Apc是否需要投递。
参数:ebp = PKTRAP_FRAME
,PreserveEax
- trap_frame.Eflags == EFLAGS_V86_MASK,运行在V86模式,不检查是否是用户模式的trap_frame
- trap_frame.Segcs != 1(KernelMode),表示是用户模式
- kthread = PCR[KPCR_CURRENT_THREAD],kthread.alerted = 0,置为不可唤醒
- kthread->ApcState.UserApcPending 是FALSE,啥也不做,TRUE才进行投递
- 如果PreserveEax=1,保存eax,保存一些IRQL提升会清除的信息到trap_frame,fs,ds,es,gs
- 提示irql到APC_LEVEL
- 调用KiDeliverApc(UserMode, 0, trap_frame);
- 恢复irql
- 如果PreserveEax=1,恢复eax
TRAP_EPILOG
是自陷处理,参数:ebp = PKTRAP_FRAME
// This macro creates an epilogue for leaving any system trap.
// It is used for exiting system calls, exceptions, interrupts and generic
// traps.
- 通过TrapFrame恢复一堆寄存器、堆栈信息,然后sysexit回到用户态空间
继续看一下调用KiDeliverApc
内部究竟是怎么处理的
1 | KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode, |
根据注释应该很清楚deliver的逻辑了,还是在看张图
CHECK_FOR_APC_DELIVER
用户态Apc的delvier有个重点,Thread->ApcState.UserApcPending必须是TRUE,那什么时候才会是TRUE,我蛮来看看
在KiInsertQueueApc,如果线程等待,且Alertable是TRUE
1
2
3
4
5
6
7
8
9
10
11
12else if ((Thread->State == Waiting) &&
(Thread->WaitMode == UserMode) &&
((Thread->Alertable) || //
(Thread->ApcState.UserApcPending)))
{
/* Set user-mode APC pending */
Thread->ApcState.UserApcPending = TRUE;
Status = STATUS_USER_APC;
goto Unwait;
}
```
2. KiCheckAlertability中(wrk中是TestForAlertPending)FORCEINLINE
NTSTATUS
KiCheckAlertability(IN PKTHREAD Thread,IN BOOLEAN Alertable, IN KPROCESSOR_MODE WaitMode)
{
/ Check if the wait is alertable /
if (Alertable)
{/* It is, first check if the thread is alerted in this mode */ if (Thread->Alerted[WaitMode]) { /* It is, so bail out of the wait */ Thread->Alerted[WaitMode] = FALSE; return STATUS_ALERTED; } else if ((WaitMode != KernelMode) && (!IsListEmpty(&Thread->ApcState.ApcListHead[UserMode]))) { /* It's isn't, but this is a user wait with queued user APCs */ Thread->ApcState.UserApcPending = TRUE; return STATUS_USER_APC;
1
2
3
4
5
6
7
两种情况都需要Alertable = TRUE,这个字段表示线程是唤醒的,也就是说只有可唤醒的线程,才能拿投递他的用态APC,否则不会
> SleepEx, WaitForSingleObject,WaitForMultipleObjects都可以设置线程为Alertable
接着继续看看`KiInitializeUserApc`是怎么切换到用户空间执行的用户态函数VOID
NTAPI
KiInitializeUserApc(IN PKEXCEPTION_FRAME ExceptionFrame,IN PKTRAP_FRAME TrapFrame, IN PKNORMAL_ROUTINE NormalRoutine, IN PVOID NormalContext, IN PVOID SystemArgument1, IN PVOID SystemArgument2)
{
//V86模式下,不投递
/ Save the full context /
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
KeTrapFrameToContext(TrapFrame, ExceptionFrame, &Context);//检查不是KernleMode
ASSERT((TrapFrame->SegCs & MODE_MASK) != KernelMode);…
/ Get the aligned size /
AlignedEsp = Context.Esp & ~3;//来自于TrapFrame.HardwareEsp或TempEsp
//Context和4个参数的长度
ContextLength = CONTEXT_ALIGNED_SIZE + (4 * sizeof(ULONG_PTR));
//将原始堆栈扩展ContextLength,用来保存Context和参数
Stack = ((AlignedEsp - 8) & ~3) - ContextLength;/ Probe the stack /
ProbeForWrite((PVOID)Stack, AlignedEsp - Stack, 1);
ASSERT(!(Stack & 3));/ Copy data into it /
//(4 sizeof(ULONG_PTR)))是后面4个参数的位置,然后接着拷贝Context,将老的TrapFrame内容拷贝到用户太堆栈中
RtlCopyMemory((PVOID)(Stack + (4 sizeof(ULONG_PTR))),&Context, sizeof(CONTEXT));
/ Run at APC dispatcher /
TrapFrame->Eip = (ULONG)KeUserApcDispatcher; //KeUserApcDispatcher保存的其实就是KiUserApcDispatcher,是用户空间函数
TrapFrame->HardwareEsp = Stack;//栈顶/ Setup Ring 3 state /
TrapFrame->SegCs = Ke386SanitizeSeg(KGDT_R3_CODE, UserMode);
TrapFrame->HardwareSegSs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegDs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegEs = Ke386SanitizeSeg(KGDT_R3_DATA, UserMode);
TrapFrame->SegFs = Ke386SanitizeSeg(KGDT_R3_TEB, UserMode);
TrapFrame->SegGs = 0;
TrapFrame->ErrCode = 0;/ Sanitize EFLAGS /
TrapFrame->EFlags = Ke386SanitizeFlags(Context.EFlags, UserMode);/ Check if thread has IOPL and force it enabled if so /
if (KeGetCurrentThread()->Iopl) TrapFrame->EFlags |= 0x3000;/ Setup the stack /
(PULONG_PTR)(Stack + 0 sizeof(ULONG_PTR)) = (ULONG_PTR)NormalRoutine;
(PULONG_PTR)(Stack + 1 sizeof(ULONG_PTR)) = (ULONG_PTR)NormalContext;
(PULONG_PTR)(Stack + 2 sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument1;
(PULONG_PTR)(Stack + 3 sizeof(ULONG_PTR)) = (ULONG_PTR)SystemArgument2;
…
}1
2
3
4
5
6
7
执行流程根据注释应该很清楚了,这里要解释一下TrapFrame。
> CPU进入啮合之后,内核堆栈就会有个TrapFrame,保存的是用户空间的线程(因进入内核原因不同,可能是自陷、中断、异常框架,都是一样的结构)。CPU返回用户空间时会使用这个TrapFrame,才能正确返回原理啊的断点,并回复寄存器的状态
> 这里为了让Apc返回到用户空间执行,就会修改这个TrapFrame,原来的TrapFrame就需要保存,这里保存在了用户空间堆栈中(CONTEXT)
> 执行完Apc函数之后,执行一个NtContinue,将这个CONTEXT作为参数,这样保存的TrapFrame就会还原到原来的状态,然后CPU又能正常回之前的用户空间了。
KiDeliverApc完了之后,回到_KiServiceExit,会使用被修改过的TrapFrame回到用户空间,执行指定的`KiUserApcDispatcher`(ntdll提供)
//更具这个执行KiUserApcDispatcher
TrapFrame->Eip = (ULONG)KeUserApcDispatcher; //其实就是KiUserApcDispatcher,是用户空间函数
TrapFrame->HardwareEsp = Stack;//栈顶
.func KiUserApcDispatcher@16
.globl _KiUserApcDispatcher@16
_KiUserApcDispatcher@16:
/* Setup SEH stack */
lea eax, [esp+CONTEXT_ALIGNED_SIZE+16];原始堆栈的位置,SEH
mov ecx, fs:[TEB_EXCEPTION_LIST]
mov edx, offset _KiUserApcExceptionHandler
mov [eax], ecx
mov [eax+4], edx
/* Enable SEH */
mov fs:[TEB_EXCEPTION_LIST], eax
/* Put the Context in EDI */
pop eax;弹出第一个参数
lea edi, [esp+12];context的位置
/* Call the APC Routine */
call eax //调用IntCallUserApc
/* Restore exception list */
mov ecx, [edi+CONTEXT_ALIGNED_SIZE]
mov fs:[TEB_EXCEPTION_LIST], ecx
/* Switch back to the context */
push 1
push edi;Context
call _ZwContinue@8 //正常是不会返回的
/* Save callback return value */
mov esi, eax
/* Raise status */
StatusRaiseApc:
push esi
call _RtlRaiseStatus@4 //如果ZwContinue失败了,这里处理
jmp StatusRaiseApc
ret 16
.endfunc1
2
3
4
5
6
7
8
`KiUserApcDispatcher`其实挺简单的,通过esp弹出APc函数,然后调用,就进入了IntCallUserApc,
## 恢复TrapFrame
执行完成后,调用_ZwContinue(Context, 1),回到内核回复之前修改TrapFrame,也会重新检查是否有Apc需要投递,有则继续投递,
重复上面的步骤,直到没有了则可以回到之前被中断的用户态的断点处。
.func NtContinue@8
_NtContinue@8:
/* NOTE: We -must- be called by Zw* to have the right frame! */
/* Push the stack frame */
push ebp ; 指向本次调用的自陷框架,记为T1
/* Get the current thread and restore its trap frame */
mov ebx, PCR[KPCR_CURRENT_THREAD]
mov edx, [ebp+KTRAP_FRAME_EDX]
mov [ebx+KTHREAD_TRAP_FRAME], edx;thread->TrapFrame = edx
/* Set up stack frame */
mov ebp, esp ; ESP指向新的框架(函数调用框架)
/* Save the parameters */
mov eax, [ebp+0] ; 原来的EBP,就是自陷框架指针,就是T1
mov ecx, [ebp+8] ; Context
/* Call KiContinue */
push eax ;TrapFrame
push 0 ;ExceptionFrame
push ecx ;Context
call _KiContinue@12 ; 将Context恢复到T1中
/* Check if we failed (bad context record) */
or eax, eax
jnz Error
/* Check if test alert was requested */
cmp dword ptr [ebp+12], 0
je DontTest
/* Test alert for the thread */
mov al, [ebx+KTHREAD_PREVIOUS_MODE]
push eax
call _KeTestAlertThread@4 ; 检查用户模式APC队列是否为空,不空将UserApcPending置为TRUE
DontTest:
/ Return to previous context /
pop ebp
mov esp, ebp
jmp _KiServiceExit2 ; 本质和_KiServiceExit相同,如果还有用户APC,会继续投递,直到投递完,才会回到用户被中断的点
Error:
pop ebp
mov esp, ebp
jmp _KiServiceExit
.endfunc
`
下面将_KiServiceExit到IntCallUserApc的流程总结一下:
到这里,终于执行到了用户的Apc函数。
结账走人
到这,APC流程基本弄清楚了。
下一篇将结合APC机制分析一下最近比较新的AtomBombing注入技术的详细实现和各个细节。
参考
转载请注明出处,博客原文:http://anhkgg.github.io/win-apc-analyze1/