来了解一下Windows的APC机制。
APC
APC介绍
APC
即 Asyncroneus Procedure Call
,异步过程调用。学过之前的知识我们知道,线程是不能被杀掉、挂起和恢复的,线程在执行的时候自己占据着CPU
,其他线程如何控制它呢?改变一个线程的行为,这就需要APC
了。
APC结构体
APC的结构体如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| kd> dt _KAPC ntdll!_KAPC +0x000 Type : UChar +0x001 SpareByte0 : UChar +0x002 Size : UChar +0x003 SpareByte1 : UChar +0x004 SpareLong0 : 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 相关的结构体。
1 2 3 4 5 6 7 8 9 10 11 12
| kd> dt _KTHREAD ntdll!_KTHREAD .... +0x040 ApcState : _KAPC_STATE .... kd> dt _KAPC_STATE ntdll!_KAPC_STATE +0x000 ApcListHead : [2] _LIST_ENTRY +0x010 Process : Ptr32 _KPROCESS +0x014 KernelApcInProgress : UChar +0x015 KernelApcPending : UChar +0x016 UserApcPending : UChar
|
下面来介绍一下相关成员:
ApcListHead
:是个双向链表的数组,一共有两个成员,所谓的APC
就是插入到里面的,一个是用户APC队列,一个是内核APC队列。
Process
:线程线程所属或者所挂靠的进程,这个在逆向线程切换的时候我们就用过。具体细节都在进程线程篇的总结与提升讲过,就不再赘述了。
KernelApcInProgress
:指示内核APC
是否正在执行。
KernelApcPending
:指示是否有正在等待执行的内核APC
。
UserApcPending
:指示是否有正在等待执行的用户APC
。
备用APC队列
看到线程跟 APC 相关的成员:
1 2 3 4 5 6 7 8 9 10
| kd> dt _KTHREAD ntdll!_KTHREAD +0x040 ApcState : _KAPC_STATE +0x134 ApcStateIndex : UChar +0x168 ApcStatePointer : [2] Ptr32 _KAPC_STATE +0x170 SavedApcState : _KAPC_STATE
|
可以看到除了一个 APC 队列的成员 ApcState
以外 ,还有一个备用 APC 队列 SavedApcState
,为什么要有一个这个且听笔者娓娓道来。
考虑一种情况:P1 进程产生了线程 T1,此时它要去挂靠 P2 进程,挂靠之后 T1 线程的页目录基址被切换到了进程 P2。如果此时往该线程插入 APC 让它指定读取某个地址的内存,本意可能想读取 P1 进程的内存,可是却读到了 P2 进程的内存。为了避免这种情况的发生,在线程处于挂靠状态的时候,ApcState
中的值就会存到 SavedApcState
中。而 ApcState
则会存储 P2 进程相关的 APC 队列。
总结一句话就是:ApcState
总会存储当前线程所挂靠的进程的 APC
队列。
可是,如何判断当前进程是自己的“生父”进程而不是“养父”进程呢?
ApcStateIndex
用来标识当前线程处于什么状态。如果值为0则为正常状态;如果值为1则为挂靠状态,这也就回答了刚刚那个问题。
ApcStatePointer
这里存储了两个 APC 队列,为了操作方便,ApcStatePointer[0]
总能取得自己的“生父”进程相关的 APC 队列。
情况 |
ApcStatePointer[0] |
ApcStatePointer[1] |
正常情况 |
ApcState |
SavedApcState |
挂靠情况 |
SavedApcState |
ApcState |
而 ApcStatePointer[ApcStateIndex]
总能取得 ApcState
的值。
挂起,杀死线程分析
R3 代码就不分析了,直接看到 nt!NtTerminateThread
函数,最终调用了 nt!PspTerminateThreadByPointer
函数。
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
| int __stdcall PspTerminateThreadByPointer(_KTHREAD *Thread, NTSTATUS a2, char a3) { volatile signed __int32 *p_WaitTime; int v4; PVOID addr; void *v7;
p_WaitTime = (volatile signed __int32 *)&Thread[1].WaitTime; if ( (Thread[1].WaitTime & 0x40) != 0 && ((int)Thread->Process[4].ProfileListHead.Flink & 0x40000008) == 0 ) PspCatchCriticalBreak("Terminating critical thread 0x%p (in %s)\n", &Thread->Process[2].Affinity.Reserved); v4 = 0; if ( a3 && Thread == KeGetCurrentThread() ) { _InterlockedOr(p_WaitTime, 1u); PspExitThread((_KSTACK_COUNT)a2); __debugbreak(); } if ( (Thread->MiscFlags & 0x2000) != 0 ) return -1073741790; while ( 1 ) { if ( (*p_WaitTime & 1) != 0 ) return 0; addr = ExAllocatePoolWithTag(NonPagedPool, 0x30u, 0x78457350u); v7 = addr; if ( addr ) break; KeDelayExecutionThread(0, 0, (PLARGE_INTEGER)&PspShortTime); } if ( _interlockedbittestandset(p_WaitTime, 0) ) { ExFreePoolWithTag(addr, 0); } else { KeInitializeApc(addr, Thread, 0, PsExitSpecialApc, PspExitApcRundown, PspExitNormalApc, 0, a2); if ( (unsigned __int8)KeInsertQueueApc(v7, v7, 0, 2) ) { KeAlertThread(Thread, 0); KeForceResumeThread(); } else { ExFreePoolWithTag(v7, 0); return -1073741823; } } return v4; }
|
可以看出,如果要杀死的线程为当前线程,那么直接自己执行 PspExitThread
退出。否则往指定的线程插入 APC,使得线程自杀,挂起线程同理。
附加进程分析
也就是传说中的 KeStackAttachProcess
函数。
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
| void __stdcall KeStackAttachProcess(PRKPROCESS PROCESS, PRKAPC_STATE ApcState) { _KTHREAD *CurrentThread; volatile __int32 *p_ApcQueueLock; int v4; KIRQL NewIrql;
CurrentThread = KeGetCurrentThread(); if ( (KeGetPcr()->PrcbData.DpcRequestSummary & 0x10001) != 0 ) KeBugCheckEx( 5u, (ULONG_PTR)PROCESS, (ULONG_PTR)CurrentThread->ApcState.Process, CurrentThread->ApcStateIndex, KeGetPcr()->PrcbData.DpcRequestSummary & 0x10001); if ( CurrentThread->ApcState.Process == PROCESS ) { ApcState->Process = (_KPROCESS *)1; } else { NewIrql = KeRaiseIrqlToDpcLevel(); p_ApcQueueLock = (volatile __int32 *)&CurrentThread->ApcQueueLock; v4 = 0; while ( _InterlockedExchange(p_ApcQueueLock, 1) ) { do { if ( (++v4 & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 ) _mm_pause(); else HvlNotifyLongSpinWait(v4); } while ( *p_ApcQueueLock ); } if ( CurrentThread->ApcStateIndex ) { KiAttachProcess(CurrentThread, PROCESS, NewIrql, ApcState); } else { KiAttachProcess(CurrentThread, PROCESS, NewIrql, &CurrentThread->SavedApcState); ApcState->Process = 0; } } }
|
如果要附加的进程和当前线程的 ApcState
指向的进程相同,那么直接把传入的 APCState->Process
置为 1,用过这个 API 的应该知道,这个 APCState
是要在取消附加进程的时候用到的,如果进程相同根本没必要附加,因此就置为了 1,应该会做一些特殊判断。
果然,稍微一翻取消附加进程的函数 KeUnstackDetachProcess
,就可以看到这样的一句话:
if ( ApcState->Process != (_KPROCESS *)1 )
继续分析,如果要尝试挂靠某个进程,那么先获取当前线程的 APC
队列的锁。然后判断当前是否为挂靠状态,如果是则使用传入的 ApcState
指针,如果不是则使用备用 APC
队列传入,那么用户传入的 APC
中的 Process
被设为 0。
然后继续分析 KiAttachProcess
函数。
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
| void __userpurge KiAttachProcess(_KTHREAD *thread@<eax>, _KPROCESS *process, KIRQL NewIrql, KAPC_STATE *a4) { KAPC_STATE *p_ApcState; volatile __int32 *p_ApcQueueLock; int processa;
p_ApcState = &thread->ApcState; KiMoveApcState(a4, thread->ApcState.ApcListHead); thread->ApcState.ApcListHead[0].Blink = thread->ApcState.ApcListHead; p_ApcState->ApcListHead[0].Flink = (_LIST_ENTRY *)p_ApcState; InitializeListHead(&thread->ApcState.ApcListHead[1]); thread->ApcState.KernelApcInProgress = 0; thread->ApcState.KernelApcPending = 0; thread->ApcState.UserApcPending = 0; if ( a4 == &thread->SavedApcState ) { thread->ApcStatePointer[0] = &thread->SavedApcState; thread->ApcStatePointer[1] = p_ApcState; thread->ApcStateIndex = 1; } if ( (_InterlockedExchangeAdd(&process->StackCount.Value, 8u) & 7) != 0 ) { p_ApcQueueLock = (volatile __int32 *)&thread->ApcQueueLock; _InterlockedAnd((volatile signed __int32 *)&thread->ApcQueueLock, 0); KiInSwapSingleProcess(); processa = 0; while ( _InterlockedExchange(p_ApcQueueLock, 1) ) { do { if ( (++processa & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 ) _mm_pause(); else HvlNotifyLongSpinWait(processa); } while ( *p_ApcQueueLock ); } thread->ApcState.Process = process; _InterlockedAnd(p_ApcQueueLock, 0); KiSwapProcess(process, a4->Process); } else { thread->ApcState.Process = process; _InterlockedAnd((volatile signed __int32 *)&thread->ApcQueueLock, 0); KiSwapProcess(process, a4->Process); } KfLowerIrql(NewIrql); }
|
其中 KiMoveApcState(a1,a2)
是将 a2
的 APC
队列移动到 a1
中,笔者当时可能想,移动 apc
队列难道不是直接一个指针赋值就完事了嘛,深入分析该函数之后才意识到,里面主要做的操作还有修改双向链表,使得链表第一个成员的 bk
和最后一个成员的 fd
指针指向了新的正确的位置。
紧接着初始化当前线程的新的 ApcState
,当附加之前线程不处于挂靠态,则设置 ApcStateIndex
和 ApcStatePointer
,与我们之前分析对应成员的结果一致。
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
| int __stdcall KiSwapProcess(_KPROCESS *a1, _KPROCESS *a2) { _KPROCESS *v2; unsigned int SetMember; unsigned int DirectoryTableBase; int result; _KGDTENTRY *GDT;
v2 = a1; SetMember = KeGetPcr()->SetMember; _InterlockedXor((volatile signed __int32 *)a1->ActiveProcessors.Bitmap, SetMember); _InterlockedXor((volatile signed __int32 *)a2->ActiveProcessors.Bitmap, SetMember); if ( *(_DWORD *)&a2->LdtDescriptor.LimitLow | *(_DWORD *)&a1->LdtDescriptor.LimitLow ) { _EAX = *(_DWORD *)&a1->LdtDescriptor.LimitLow; if ( _EAX ) { GDT = KeGetPcr()->GDT; *(_DWORD *)&GDT[9].LimitLow = _EAX; GDT[9].HighWord.Bits = a1->LdtDescriptor.HighWord.Bits; KeGetPcr()->IDT[33] = a1->Int21Descriptor; LOWORD(_EAX) = 72; } __asm { lldt ax } } DirectoryTableBase = a1->DirectoryTableBase; if ( (HvlEnlightenments & 1) != 0 ) { HvlSwitchVirtualAddressSpace(DirectoryTableBase); v2 = a1; } else { __writecr3(DirectoryTableBase); } result = v2->IopmOffset; *((_WORD *)KeGetPcr()->NtTib.SubSystemTib + 51) = result; return result; }
|
最后当然就是将当前 APCState.Process
设置为附加的目标进程,且切换页目录表到目标进程,那么此时的线程就可以读取目标进程相关的虚拟内存了。
脱离进程分析
有始有终,也来分析一下脱离进程的函数。
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 78
| void __stdcall KeUnstackDetachProcess(PRKAPC_STATE ApcState) { _KTHREAD *CurrentThread; volatile __int32 *p_ApcQueueLock; KAPC_STATE *p_ApcState; int v4; _KPROCESS *Process; int v6; int v7; KIRQL NewIrql;
if ( ApcState->Process != (_KPROCESS *)1 ) { CurrentThread = KeGetCurrentThread(); Process = CurrentThread->ApcState.Process; v6 = 0; NewIrql = KeRaiseIrqlToDpcLevel(); p_ApcQueueLock = (volatile __int32 *)&CurrentThread->ApcQueueLock; while ( _InterlockedExchange(p_ApcQueueLock, 1) ) { do { if ( (++v6 & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 ) _mm_pause(); else HvlNotifyLongSpinWait(v6); } while ( *p_ApcQueueLock ); } while ( CurrentThread->ApcState.KernelApcPending && !CurrentThread->SpecialApcDisable && !NewIrql ) { _InterlockedAnd(p_ApcQueueLock, 0); KfLowerIrql(0); v7 = 0; NewIrql = KeRaiseIrqlToDpcLevel(); while ( _InterlockedExchange(p_ApcQueueLock, 1) ) { do { if ( (++v7 & HvlLongSpinCountMask) != 0 || (HvlEnlightenments & 0x40) == 0 ) _mm_pause(); else HvlNotifyLongSpinWait(v7); } while ( *p_ApcQueueLock ); } } if ( !CurrentThread->ApcStateIndex || CurrentThread->ApcState.KernelApcInProgress || (p_ApcState = &CurrentThread->ApcState, (KAPC_STATE *)p_ApcState->ApcListHead[0].Flink != p_ApcState) || CurrentThread->ApcState.ApcListHead[1].Flink != &CurrentThread->ApcState.ApcListHead[1] ) { KeBugCheck(6u); } if ( ApcState->Process ) { KiMoveApcState(&CurrentThread->ApcState, ApcState->ApcListHead); } else { KiMoveApcState(&CurrentThread->ApcState, CurrentThread->SavedApcState.ApcListHead); CurrentThread->SavedApcState.Process = 0; CurrentThread->ApcStatePointer[0] = p_ApcState; CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState; CurrentThread->ApcStateIndex = 0; } _InterlockedAnd(p_ApcQueueLock, 0); KiSwapProcess(CurrentThread->ApcState.Process, Process); KfLowerIrql(NewIrql); KiDecrementProcessStackCount(); if ( (KAPC_STATE *)p_ApcState->ApcListHead[0].Flink != p_ApcState ) { LOBYTE(v4) = 1; CurrentThread->ApcState.KernelApcPending = 1; HalRequestSoftwareInterrupt(v4); } } }
|
之前分析的时候已经解释过了相关的子函数,配合注释应该能看明白,总之就是三种情况的判断:
ApcState.Process==1
,未进行任何附加,因此也不需要脱离
ApcState.Process==0
,附加之前线程为非挂靠态,因此脱离时需要回到非挂靠态。
- 其余情况,附加之前线程就已经是挂靠态,因此脱离不需要做额外的操作。
参考文献