技术 内核 APC机制 ZEROKO14 2023-11-24 2023-11-24 APC机制 APC机制使耗时操作可以异步回调处理
线程是不能被“杀死”,“挂起”,“恢复”的,线程在执行的时候自己占据着CPU,别人怎么可能控制它呢?
如果说线程如果想“死”,一定是自己执行代码把自己杀死 ,不存在”他杀”这种情况!
如果要改变一个线程的行为该怎么办呢?
可以给他提供一个函数,让它自己去调用,这个函数就是APC(Asyncroneus Procedure Call) ,即异步过程调用
_KAPC_STATE 1 2 3 4 5 6 7 8 9 10 11 12 kd> dt _KTHREAD nt!_KTHREAD ... +0x034 ApcState : _KAPC_STATE ... kd> dt _KAPC_STATE nt!_KAPC_STATE +0x000 ApcListHead : [2 ] _LIST_ENTRY +0x010 Process : Ptr32 _KPROCESS +0x014 KernelApcInProgress : UChar +0x015 KernelApcPending : UChar +0x016 UserApcPending : UChar
用户APC:APC函数地址位于用户空间,在用户空间执行.
内核APC:APC函数地址位于内核空间,在内核空间执行.
_KAPC (详细解读在后面)
ApcListHead中存的就是_KAPC结构的地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 kd> dt _kapc nt!_KAPC +0x000 Type : Int2B +0x002 Size : Int2B +0x004 Spare0 : Uint4B +0x008 Thread : Ptr32 _KTHREAD +0x00c ApcListEntry : _LIST_ENTRY +0x014 KernelRoutine : Ptr32 void +0x018 RundownRoutine : Ptr32 void +0x01c NormalRoutine : Ptr32 void +0x020 NormalContext : Ptr32 Void +0x024 SystemArgument1 : Ptr32 Void +0x028 SystemArgument2 : Ptr32 Void +0x02c ApcStateIndex : Char +0x02d ApcMode : Char +0x02e Inserted : UChar
如果想让线程做什么事情,就给它的APC队列里面挂一个APC。
什么时候执行APC中的函数?
总结:当系统调用,异常,中断的时候,都有机会调用KiDeliverApc函数执行APC函数。
备用APC队列 _KTHREAD中APC相关成员 1 2 3 4 5 6 7 8 9 10 11 12 13 14 kd> dt _KTHREAD nt!_KTHREAD ... +0x02e Alerted : [2 ] UChar +0x034 ApcState : _KAPC_STATE ... +0x138 ApcStatePointer : [2 ] Ptr32 _KAPC_STATE ... +0x14c SavedApcState : _KAPC_STATE ... +0x164 Alertable : UChar +0x165 ApcStateIndex : UChar +0x166 ApcQueueable : UChar ...
ApcStatePointer 为了操作方便,_KTHREAD结构体中定义了一个指针数组ApcStatePointer,长度为2
正常情况下:
ApcStatePointer[0] 指向ApcState (里面存的是所属进程 的APC队列)
ApcStatePointer[1] 指向SavedApcState (里面存的是挂靠进程 的APC队列)
挂靠情况下:
ApcStatePointer[0] 指向SavedApcState (里面存的是所属进程 的APC队列)
ApcStatePointer[1] 指向ApcState (里面存的是挂靠进程 的APC队列)
SavedApcState的意义 线程APC队列中的APC函数都是与进程相关联的,具体点说:A进程的T线程中的所有APC函数,要访问的内存地址都是A进程的。
但线程是可以挂靠到其他的进程,比如A进程的线程T,通过修改Cr3(改为B进程的页目录基址),就可以访问B进程地址空间,即所谓“进程挂靠”
当T线程挂靠B进程后,APC队列中存储的却仍然是原来的APC。具体点说,比如某个APC函数要读取一个地址为0x12345678的数据,如果此时进行读取,读到的将是B进程的地址空间,这样逻辑就错误了。
为了避免混乱,在T线程挂靠B进程时,会将Apcstate中的值暂时存储到SavedApcState中,等回到原进程A时,再将APC队列恢复。
所以,SavedApcState又称为备用APC队列
挂靠环境下ApcState的意义 在挂靠的环境下,也是可以在线程APC队列插入APC的 ,那这种情况下,使用的是哪个APC队列呢?
A进程的T线程挂靠B进程,A是T的所属进程,B是T的挂靠进程
ApcState—B进程(挂靠进程)相关的APC函数
SavedApcState—A(所属进程)进程相关的APC函数
KTHREAD.ApcStatePointer与KTHREAD.ApcStateIndex组合寻址
正常情况下,向ApcState队列中插入APC时:
ApcStatePointer[0] 指向 ApcState此时ApcStateIndex的值为0
ApcStatePointer[KTHREAD.ApcStateIndex] 指向 ApcState
挂靠情况下,向ApcState队列中插入APC时:
ApcStatePointer[1] 指向 ApcState此时ApcStateIndex的值为1
ApcStatePointer[KTHREAD.ApcStateIndex] 指向 ApcState
【总结】无论什么环境下,ApcStatePointer[ApcStateIndex] 指向的都是 ApcState 。
ApcState 则总是标识线程当前使用的APC状态 。
分析NtReadVirtualMemory 在挂靠时是如何备份和恢复APC队列的
NtReadVirtualMemory 内有挂靠进程函数_KeStackAttachProcess以及恢复不挂靠状态的函数_KeUnstackDetachProcess
1 2 3 4 _KeStackAttachProcess --->_KeStackAttachProcess --->_KiAttachProcess --->
_KiMoveApcState
APC挂入过程(内含KAPC详细结构) 无论是正常状态还是挂靠状态,都有两个APC队列,一个内核队列,一个用户队列。
每当要挂入一个APC函数时,不管是内核APC还是用户APC,内核都要准备一个KAPC的数据结构,并且将这个KAPC结构挂到相应的APC队列中。
_KAPC 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 kd> dt _kapc nt!_KAPC +0x000 Type : Int2B +0x002 Size : Int2B +0x004 Spare0 : Uint4B +0x008 Thread : Ptr32 _KTHREAD +0x00c ApcListEntry : _LIST_ENTRY +0x014 KernelRoutine : Ptr32 void +0x018 RundownRoutine : Ptr32 void +0x01c NormalRoutine : Ptr32 void +0x020 NormalContext : Ptr32 Void +0x024 SystemArgument1 : Ptr32 Void +0x028 SystemArgument2 : Ptr32 Void +0x02c ApcStateIndex : Char +0x02d ApcMode : Char +0x02e Inserted : UChar
ApcStateIndex 与KTHREAD(+0x165)的属性同名但含义不一样:
ApcStateIndex有四个值
0 —原始环境,不管是否挂靠都插入到当前线程所属进程(亲生父母)相关的APC队列(未挂靠就插入ApcState,挂靠了就插入SavedApcState)
1 —挂靠环境,插入到当前线程挂靠进程相关的APC队列(未挂靠就插入SavedApcState,挂靠了就插入ApcState)
2 —当前环境,使用_KTHREAD.ApcStateIndex,ApcStatePointer[_KTHREAD.ApcStateIndex], 即都是写入ApcState 。初始化时如果没有挂靠,插入到所属进程相关的APC队列;初始化时如果挂靠了,插入到挂靠进程相关的APC队列
3 —插入APC时的当前环境,在插入APC之前会更新当前线程是否处于挂靠状态(将_ETHREAD.Tcb.ApcStateIndex覆盖到_KAPC.ApcStateIndex中) 再进行APC插入
NtQueueApcThread到KiInsertQueueApc才真正插入完,插入的过程中有可能线程的状态改变,因此3的意思指的是在KiInsertQueueApc这一步才更新线程状态
TargetEnvironment(ApcStateIndex)里存的四种情况的枚举类型,出自WRK
用户APC挂入流程
QueueUserAPC函数 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 .text:7C82C07A ; DWORD __stdcall QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData) .text:7C82C07A public _QueueUserAPC@12 .text:7C82C07A _QueueUserAPC@12 proc near ; DATA XREF: .text:off_7C802654↑o .text:7C82C07A .text:7C82C07A pvBuffer = dword ptr -8 .text:7C82C07A var_4 = byte ptr -4 .text:7C82C07A pfnAPC = dword ptr 8 .text:7C82C07A hThread = dword ptr 0Ch .text:7C82C07A dwData = dword ptr 10h .text:7C82C07A .text:7C82C07A ; FUNCTION CHUNK AT .text:7C844C8E SIZE 00000022 BYTES .text:7C82C07A .text:7C82C07A mov edi, edi .text:7C82C07C push ebp .text:7C82C07D mov ebp, esp .text:7C82C07F push ecx .text:7C82C080 push ecx .text:7C82C081 and [ebp+pvBuffer], 0 .text:7C82C085 push edi .text:7C82C086 xor eax, eax .text:7C82C088 push eax ; pcbWrittenOrRequired .text:7C82C089 push 8 ; cbBuffer .text:7C82C08B lea edi, [ebp+var_4] .text:7C82C08E stosd .text:7C82C08F lea eax, [ebp+pvBuffer] .text:7C82C092 push eax ; pvBuffer .text:7C82C093 push 1 ; ulInfoClass .text:7C82C095 push 0 ; pvSubInstance .text:7C82C097 push 0 ; Context .text:7C82C099 push 1 ; dwFlags .text:7C82C09B call ds:__imp__RtlQueryInformationActivationContext@28 ; RtlQueryInformationActivationContext(x,x,x,x,x,x,x) .text:7C82C0A1 test eax, eax .text:7C82C0A3 pop edi .text:7C82C0A4 jl loc_7C844C8E .text:7C82C0AA test [ebp+var_4], 1 .text:7C82C0AE mov eax, [ebp+pvBuffer] .text:7C82C0B1 jnz loc_7C844CA8 .text:7C82C0B7 .text:7C82C0B7 loc_7C82C0B7: ; CODE XREF: QueueUserAPC(x,x,x)+18C31↓j .text:7C82C0B7 push eax ; SystemArgument2,来源于__imp__RtlQueryInformationActivationContext函数的返回值 .text:7C82C0B8 push [ebp+dwData] ; SystemArgument1 .text:7C82C0BB push [ebp+pfnAPC] ; NormalContext .text:7C82C0BE push offset _BaseDispatchAPC@12 ; ApcRoutine,此处填入的是NormalRoutine,即_BaseDispatchAPC,分发函数的函数地址 .text:7C82C0C3 push [ebp+hThread] ; ThreadHandle .text:7C82C0C6 call ds:__imp__NtQueueApcThread@20 ; NtQueueApcThread(x,x,x,x,x) .text:7C82C0CC xor ecx, ecx .text:7C82C0CE test eax, eax .text:7C82C0D0 setnl cl .text:7C82C0D3 mov eax, ecx .text:7C82C0D5 .text:7C82C0D5 locret_7C82C0D5: ; CODE XREF: QueueUserAPC(x,x,x)+18C29↓j .text:7C82C0D5 leave .text:7C82C0D6 retn 0Ch .text:7C82C0D6 _QueueUserAPC@12 endp
上面可见,三环插入用户apc往NormalRoutine填入的是_BaseDispatchAPC@12函数
NtQueueApcThread函数 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 PAGE:004BA08B ; NTSTATUS __stdcall NtQueueApcThread(HANDLE ThreadHandle, PKNORMAL_ROUTINE ApcRoutine, PVOID NormalContext, PVOID SystemArgument1, PVOID SystemArgument2) PAGE:004BA08B _NtQueueApcThread@20 proc near ; DATA XREF: .text:0040B978↑o PAGE:004BA08B PAGE:004BA08B AccessMode = byte ptr -4 PAGE:004BA08B ThreadHandle = dword ptr 8 PAGE:004BA08B ApcRoutine = dword ptr 0Ch PAGE:004BA08B NormalContext = dword ptr 10h PAGE:004BA08B SystemArgument1 = dword ptr 14h PAGE:004BA08B SystemArgument2 = dword ptr 18h PAGE:004BA08B PAGE:004BA08B ; FUNCTION CHUNK AT PAGE:00523983 SIZE 00000025 BYTES PAGE:004BA08B PAGE:004BA08B mov edi, edi PAGE:004BA08D push ebp PAGE:004BA08E mov ebp, esp PAGE:004BA090 push ecx PAGE:004BA091 push ebx PAGE:004BA092 push esi PAGE:004BA093 mov eax, large fs:124h PAGE:004BA099 mov al, [eax+140h] PAGE:004BA09F mov [ebp+AccessMode], al PAGE:004BA0A2 xor esi, esi PAGE:004BA0A4 push esi ; HandleInformation PAGE:004BA0A5 lea eax, [ebp+ThreadHandle] PAGE:004BA0A8 push eax ; Object PAGE:004BA0A9 push dword ptr [ebp+AccessMode] ; AccessMode PAGE:004BA0AC push _PsThreadType ; ObjectType PAGE:004BA0B2 push 10h ; DesiredAccess PAGE:004BA0B4 push [ebp+ThreadHandle] ; Handle PAGE:004BA0B7 call _ObReferenceObjectByHandle@24 ; ObReferenceObjectByHandle(x,x,x,x,x,x) PAGE:004BA0BC mov ebx, eax PAGE:004BA0BE cmp ebx, esi PAGE:004BA0C0 jl short loc_4BA123 PAGE:004BA0C2 mov eax, [ebp+ThreadHandle] PAGE:004BA0C5 xor ebx, ebx PAGE:004BA0C7 test byte ptr [eax+_ETHREAD.___u24.CrossThreadFlags], 10h ; 判断当前线程是否是系统线程 PAGE:004BA0CE jnz loc_523983 PAGE:004BA0D4 push edi PAGE:004BA0D5 push 70617350h ; Tag PAGE:004BA0DA push 30h ; NumberOfBytes PAGE:004BA0DC push 8 ; PoolType PAGE:004BA0DE call _ExAllocatePoolWithQuotaTag@12 ; ExAllocatePoolWithQuotaTag(x,x,x)创建0x30字节的空间,给KAPC使用 PAGE:004BA0E3 mov edi, eax ; 返回的内存地址放入edi中 PAGE:004BA0E5 cmp edi, esi ; 判断是否返回NULL PAGE:004BA0E7 jz loc_52398D PAGE:004BA0ED push [ebp+NormalContext] PAGE:004BA0F0 push 1 PAGE:004BA0F2 push [ebp+ApcRoutine];NormalRoutine,上层函数传入的参数,统一的分发函数 PAGE:004BA0F5 push esi ; 0 PAGE:004BA0F6 push offset _IopDeallocateApc@20 ; _IopDeallocateApc函数地址,其内部执行了_ExFreePoolWithTag@8 PAGE:004BA0FB push esi ; 0 PAGE:004BA0FC push [ebp+ThreadHandle] PAGE:004BA0FF push edi ; KAPC PAGE:004BA100 call _KeInitializeApc@32 ; KeInitializeApc(x,x,x,x,x,x,x,x),初始化APC函数 PAGE:004BA105 push esi ; 0 PAGE:004BA106 push [ebp+SystemArgument2] PAGE:004BA109 push [ebp+SystemArgument1] PAGE:004BA10C push edi ; KPAC PAGE:004BA10D call _KeInsertQueueApc@16 ; KeInsertQueueApc(x,x,x,x) PAGE:004BA112 test al, al PAGE:004BA114 jz loc_523997 PAGE:004BA11A PAGE:004BA11A loc_4BA11A: ; CODE XREF: NtQueueApcThread(x,x,x,x,x)+69907↓j PAGE:004BA11A ; NtQueueApcThread(x,x,x,x,x)+69918↓j PAGE:004BA11A pop edi PAGE:004BA11B PAGE:004BA11B loc_4BA11B: ; CODE XREF: NtQueueApcThread(x,x,x,x,x)+698FD↓j PAGE:004BA11B mov ecx, [ebp+ThreadHandle] ; Object PAGE:004BA11E call @ObfDereferenceObject@4 ; ObfDereferenceObject(x) PAGE:004BA123 PAGE:004BA123 loc_4BA123: ; CODE XREF: NtQueueApcThread(x,x,x,x,x)+35↑j PAGE:004BA123 pop esi PAGE:004BA124 mov eax, ebx PAGE:004BA126 pop ebx PAGE:004BA127 leave PAGE:004BA128 retn 14h PAGE:004BA128 _NtQueueApcThread@20 endp
KeInitializeApc函数 该函数并未公布,但导出了
1 2 3 4 5 6 7 8 9 10 11 12 VOID KeInitializeApc ( IN PKAPC Apc, IN PKTHREAD Thread, IN KAPC_ENVIRONMENT TargetEnvironment, IN PKKERNEL_ROUTINE KernelRoutine, IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL, IN PKNORMAL_ROUTINE NormalRoutine, IN KPROCESSOR_MODE Mode, IN PVOID Context )
KeInsertQueueApc函数
KiInsertQueueApc函数
根据KAPC.ApcStateIndex找到对应的APC队列
再根据KAPC.ApcMode确定是用户队列还是内核队列
将KAPC挂到对应的队列中(挂到KAPC的ApcListEntry处)
再根据KAPC结构中的Inserted置1,标识当前的KAPC为已插入状态
KAPC_STATE结构中的KernelApcPending/UserApcPending置1(UserApcPending未必置1,只有当前线程处于等待状态,并且是用户自己导致的等待,并且是可以吵醒(Alertable)的线程才置1 )
该函数返回值为1表示APC已插入,但如果UserApcPending未置1,则该用户APC未必有机会执行
插入的上半部分
插入的下半部分
反汇编:
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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 VOID FASTCALL KiInsertQueueApc (IN PKAPC Apc,IN KPRIORITY PriorityBoost) { if (Apc->ApcStateIndex == InsertApcEnvironment) { Apc->ApcStateIndex = Thread->ApcStateIndex; } ApcState = Thread->ApcStatePointer[(UCHAR)Apc->ApcStateIndex]; ApcMode = Apc->ApcMode; ASSERT(Apc->Inserted == TRUE); if (Thread->ApcStateIndex == Apc->ApcStateIndex) { if (当前线程 ) { if (KernelMode) { Thread->ApcState.KernelApcPending = TRUE; if (!Thread->SpecialApcDisable) { HalRequestSoftwareInterrupt(APC_LEVEL); } } } else { if (KernelMode) { Thread->ApcState.KernelApcPending = TRUE; if (Thread->State == Running) HalRequestSoftwareInterrupt(APC_LEVEL); else if (一堆条件){ KiUnwaitThread(Thread, Status, PriorityBoost); } } else { if ((Thread->State == Waiting) && (Thread->WaitMode == UserMode) && ((Thread->Alertable) || (Thread->ApcState.UserApcPending))) { Thread->ApcState.UserApcPending = TRUE; Status = STATUS_USER_APC; KiUnwaitThread(Thread, Status, PriorityBoost); } } } } }
KiInsertQueueApc函数插入的三种方式,上面只节选了如下第2种方式
Alertable属性 意思为:是否允许被APC吵醒
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 kd> dt _KTHREAD ntdll!_KTHREAD ... +0x164 Alertable : UChar ... DWORD SleepEx ( DWORD dwMilliseconds, BOOL bAlertable ) ;DWORD WaitForSingleObjectEx ( HANDLE hHandle, DWORD dwMilliseconds, BOOL bAlertable ) ;
三环APC API挂入实验 QueueUserAPC 1 2 3 4 5 DWORD QueueUserAPC ( [in] PAPCFUNC pfnAPC, [in] HANDLE hThread, [in] ULONG_PTR dwData ) ;
三环APC API挂入实验 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 VOID NTAPI myAPC (_In_ ULONG_PTR Parameter) { printf ("APC执行,参数为%d\r\n" , Parameter); } void threadCall () { printf ("线程开始执行\r\n" ); while (1 ) { printf ("线程执行中...\r\n" ); SleepEx(1000 ,TRUE); } } void main () { HANDLE hThread = CreateThread(NULL , 0 , (LPTHREAD_START_ROUTINE)threadCall, NULL , 0 , NULL ); if (hThread==0 ) { cout << "CreateThread wrong! wrong num:" << GetLastError() << endl ; } ULONG a = 30 ; getchar(); QueueUserAPC(myAPC, hThread, a); getchar(); }
内核APC执行流程 APC函数的执行与插入并不是同一个线程,具体点讲:
在线程A中向B线程插入一个APC,插入的动作是在A线程中完成的,但什么时候执行则由B线程决定。所以叫”异步过程调用”。
内核APC函数与用户APC函数的执行时间和执行方式也有区别。
执行点:线程切换 该执行点只执行内核APC
1 2 3 4 5 6 7 SwapContext | 执行完返回到 KiSwapContext | 执行完返回到 KiSwapThread | 执行完返回到 KiDeliverApc
SwapContext函数截取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ;末端截取 .text:004049F6 cmp [esi+_ETHREAD.Tcb.ApcState.KernelApcPending], 0 ; 判断要切换的线程是否有要执行的内核APC .text:004049FA jnz short loc_404A00 .text:004049FC popf .text:004049FD xor eax, eax .text:004049FF retn .text:00404A00 ; --------------------------------------------------------------------------- .text:00404A00 .text:00404A00 loc_404A00: ; CODE XREF: SwapContext+D6↑j .text:00404A00 popf .text:00404A01 jnz short loc_404A06 .text:00404A03 mov al, 1 ; 有要执行的内核apc返回1 .text:00404A05 retn .text:00404A06 ; --------------------------------------------------------------------------- .text:00404A06 .text:00404A06 loc_404A06: ; CODE XREF: SwapContext+DD↑j .text:00404A06 mov cl, 1 .text:00404A08 call ds:__imp_@HalRequestSoftwareInterrupt@4 ; HalRequestSoftwareInterrupt(x) .text:00404A0E xor eax, eax ; 没有要执行的内核apc返回0 .text:00404A10 retn .text:00404A11 ; ---------------------------------------------------------------------------
KiSwapThread函数截取
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ;截取KiSwapThread .text:004050F0 loc_4050F0: ; CODE XREF: KiSwapThread()+B8F4↓j .text:004050F0 mov ecx, eax ; 取要切换的目标线程 .text:004050F2 call @KiSwapContext@4 ; KiSwapContext(x) .text:004050F7 test al, al .text:004050F9 mov cl, [edi+_KTHREAD.WaitIrql] ; NewIrql .text:004050FC mov edi, [edi+_KTHREAD.WaitStatus] .text:004050FF mov esi, ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x) .text:00405105 jnz loc_415ADB ; 这个跳转处理APC .text:00415ADB ; --------------------------------------------------------------------------- .text:00415ADB .text:00415ADB loc_415ADB: ; CODE XREF: KiSwapThread()+46↑j .text:00415ADB mov cl, 1 ; NewIrql .text:00415ADD call esi ; KfLowerIrql(x) ; KfLowerIrql(x) .text:00415ADF xor eax, eax .text:00415AE1 push eax .text:00415AE2 push eax .text:00415AE3 push eax ; 0处理内核APC 1处理内核APC和用户APC .text:00415AE4 call _KiDeliverApc@12 ; KiDeliverApc(x,x,x) 专门用来处理APC的函数 .text:00415AE9 xor cl, cl .text:00415AEB jmp loc_40510B .text:00415AF0 ; ---------------------------------------------------------------------------
执行点:系统调用,中断或异常(_KiServiceExit) 即从内核返回用户空间的途中
_KiServiceExit 是[[系统调用]],中断或异常回三环的时候必须要走的函数 。
该执行点先执行内核APC,再执行用户APC
_KiServiceExit截取
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 37 38 39 40 41 42 43 44 45 46 47 48 49 .text:004077FD _KiServiceExit proc near ; CODE XREF: _KiSetLowWaitHighThread+7C↓j .text:004077FD ; NtContinue(x,x)+42↓j ... .text:004077FD .text:004077FD arg_C = dword ptr 10h .text:004077FD arg_10 = dword ptr 14h .text:004077FD arg_40 = dword ptr 44h .text:004077FD arg_44 = dword ptr 48h .text:004077FD arg_48 = dword ptr 4Ch .text:004077FD arg_60 = dword ptr 64h .text:004077FD arg_64 = dword ptr 68h .text:004077FD arg_68 = dword ptr 6Ch .text:004077FD arg_6C = dword ptr 70h .text:004077FD .text:004077FD ; FUNCTION CHUNK AT .text:00407908 SIZE 00000088 BYTES .text:004077FD .text:004077FD cli .text:004077FE test dword ptr [ebp+70h], 20000h ; 判断CPU模式 [ebp+70]是3环的Eflags .text:00407805 jnz short loc_40780D ; 获取_KTHREAD .text:00407807 test byte ptr [ebp+6Ch], 1 ; 三环的SegCs .text:0040780B jz short loc_407864 .text:0040780D .text:0040780D loc_40780D: ; CODE XREF: _KiServiceExit+8↑j .text:0040780D ; _KiServiceExit+63↓j .text:0040780D mov ebx, ds:0FFDFF124h ; 获取_KTHREAD .text:00407813 mov [ebx+_KTHREAD.Alerted], 0 .text:00407817 cmp [ebx+_KTHREAD.ApcState.UserApcPending], 0 ; 判断是否存在用户APC等待执行 .text:0040781B jz short loc_407864 ; 没有用户APC等待执行就跳转 .text:0040781D mov ebx, ebp .text:0040781F mov [ebx+44h], eax .text:00407822 mov dword ptr [ebx+50h], 3Bh .text:00407829 mov dword ptr [ebx+38h], 23h .text:00407830 mov dword ptr [ebx+34h], 23h .text:00407837 mov dword ptr [ebx+30h], 0 .text:0040783E mov ecx, 1 ; NewIrql .text:00407843 call ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x) .text:00407849 push eax ; KfRaiseIrql返回老的运行级别,入栈 .text:0040784A sti .text:0040784B push ebx .text:0040784C push 0 .text:0040784E push 1 ; 0处理内核APC 1处理内核APC和用户APC .text:00407850 call _KiDeliverApc@12 ; 执行内核APC函数,并为用户空间的APC的执行进行准备 .text:00407850 ; 下面是将老的运行级别出栈,然后执行KfLowerIrql函数恢复原来的运行级别 .text:00407855 pop ecx ; NewIrql .text:00407856 call ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x) .text:0040785C mov eax, [ebx+44h] .text:0040785F cli .text:00407860 jmp short loc_40780D ; 获取_KTHREAD .text:00407860 ; --------------------------------------------------------------------------- .text:00407862 align 4
【注意】这个执行点必须是有用户APC存在的情况下,才可以触发该执行点 (先处理内核APC再处理用户APC),不然就直接返回了
KiDeliverApc函数内核APC执行流程
判断第一个链表是否为空
判断KTHREAD.ApcState.KernelApcInProgress是否为1
判断是否禁用内核APC(KTHREAD.KernelApcDisable是否为1)
将当前KAPC结构体从链表中摘除
执行KAPC.KernelRoutine执行的函数,释放KAPC结构体占用的空间
将KTHREAD.ApcState.KernelApcInProgress设置为1,标识正在执行内核APC
执行真正的内核APC函数(KAPC.NormalRoutine)
执行完毕,将KernelApcInProgress改为0
循环
汇编流程如下:
内核APC总结
内核APC在线程切换的时候就会执行,这也就意味着,只要插入内核APC很快就会执行。
在执行用户APC之前就会先执行内核APC。
内核APC在内核空间执行,不需要换栈,一个循环全部执行完毕
用户APC执行流程 当产生[[系统调用]],中断或者异常,线程在返回用户空间前都会调用_KiServiceExit函数,在_KiServiceExit会判断是否有要执行的用户APC,如果有则调用KiDeliverApc函数(第一个参数为1)进行处理
上图是从用户模式调用Native API才走KiServiceExit2
总览流程图
执行用户APC时的堆栈操作 处理用户APC要比内核APC复杂的多,因为,用户APC函数要在用户空间执行的,这里涉及到大量换栈的操作 :
当线程从用户层进入内核层时,要保留原来的运行环境,比如各种寄存器,栈的位置等等(_Trap_Frame),然后切换成内核的堆栈,如果正常返回,恢复堆栈环境即可。
但如果有用户APC要执行的话,就意味着线程要提前返回到用户空间去执行 ,而且返回的位置不是线程进入内核时的位置,而是返回到其他的位置 ,每处理一个用户APC都会涉及到: $$ 内核 —> 用户空间 —> 再回到内核空间 $$ 堆栈的操作比较复杂,如果不了解堆栈的操作细节不可能理解用户APC是如何执行的!
堆栈变化总结:
KiInitializeUserApc函数分析 线程进入0环时,原来的运行环境(寄存器栈顶等)保存到_Trap_Frame结构体中,如果要提前返回到3环取处理用户APC,就必须要修改_Trap_Frame结构体 :
比如:进入0环时的位置存储在EIP中,现在要提前返回,而且返回的并不是原来的位置,那就意味着必须要修改EIP为新的返回位置。还有堆栈ESP,也要修改为处理APC需要的堆栈。那原来的值怎么办呢?处理完APC后该如何返回原来的位置呢?
KiInitializeUserApc要做的第一件事就是备份 :
将原来的_Trap_Frame结构体的值备份到一个新的结构体中(CONTEXT ),这个功能由其子函数KeContextFromKframes 来完成。
KiInitializeUserApc函数分析:准备回用户层的执行环境
段寄存器:SS DS FS GS
修改EFLAGS寄存器
修改ESP
修改EIP(此EIP是固定修改为全局变量_KeUserApcDispatcher存的值,其值指向ntdll.KiUserApcDispatcher函数)
CONTEXT结构体 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 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 typedef struct _CONTEXT { DWORD ContextFlags DWORD Dr0 DWORD Dr1 DWORD Dr2 DWORD Dr3 DWORD Dr6 DWORD Dr7 FLOATING_SAVE_AREA FloatSave; DWORD SegGs DWORD SegFs DWORD SegEs DWORD SegDs DWORD Edi DWORD Esi DWORD Ebx DWORD Edx DWORD Ecx DWORD Eax DWORD Ebp DWORD Eip DWORD SegCs DWORD EFlag DWORD Esp DWORD SegSs BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT; nt!_CONTEXT +0x000 ContextFlags : Uint4B +0x004 Dr0 : Uint4B +0x008 Dr1 : Uint4B +0x00c Dr2 : Uint4B +0x010 Dr3 : Uint4B +0x014 Dr6 : Uint4B +0x018 Dr7 : Uint4B +0x01c FloatSave : _FLOATING_SAVE_AREA +0x08c SegGs : Uint4B +0x090 SegFs : Uint4B +0x094 SegEs : Uint4B +0x098 SegDs : Uint4B +0x09c Edi : Uint4B +0x0a0 Esi : Uint4B +0x0a4 Ebx : Uint4B +0x0a8 Edx : Uint4B +0x0ac Ecx : Uint4B +0x0b0 Eax : Uint4B +0x0b4 Ebp : Uint4B +0x0b8 Eip : Uint4B +0x0bc SegCs : Uint4B +0x0c0 EFlags : Uint4B +0x0c4 Esp : Uint4B +0x0c8 SegSs : Uint4B +0x0cc ExtendedRegisters : [512 ] UChar
ntdll.KiUserApcDispatcher分析 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 .text:7C92E430 ; __stdcall KiUserApcDispatcher(x, x, x, x, x) .text:7C92E430 public _KiUserApcDispatcher@20 .text:7C92E430 _KiUserApcDispatcher@20 proc near ; DATA XREF: .text:off_7C923428↑o .text:7C92E430 .text:7C92E430 arg_C = byte ptr 10h .text:7C92E430 .text:7C92E430 lea edi, [esp+arg_C]; CONTEXT指针 .text:7C92E434 pop eax .text:7C92E435 call eax ; 处理用户APC的总入口:NormalRoutine .text:7C92E437 push 1 .text:7C92E439 push edi .text:7C92E43A call _ZwContinue@8 ; ZwContinue(x,x) 返回内核 .text:7C92E43F nop .text:7C92E43F _KiUserApcDispatcher@20 endp ; sp-analysis failed .text:7C92E43F .text:7C92E440 ; Exported entry 46. KiUserCallbackDispatcher
当用户在3环调用QueueUserAPC函数来插入APC时,不需要提供NormalRoutine,这个参数是在QueueUserAPC内部指定的:
Kernel32.BaseDispatchAPC函数 :调用用户提供的真正的用户APC函数
ZwContinue函数的意义:
返回内核,如果还有用户APC,重复上面的执行过程
如果没有需要执行的用户APC,会将CONTEXT赋值给Trap_Frame结构体。就像从来没有修改过一样。ZwContinue后面的代码不会执行,线程从哪里进0环仍然会从哪里回去。
总结
内核APC在线程切换时执行,不需要换栈,比较简单,一个循环执行完毕。
用户APC在[[系统调用]],中断或异常返回3环前会进行判断,如果有要执行的用户APC,再执行。
用户APC执行前会先执行内核APC