来了解一下Windows的APC机制。

APC

APC介绍

APCAsyncroneus 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; // edi
int v4; // ebx
PVOID addr; // eax
void *v7; // esi

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; // esi
volatile __int32 *p_ApcQueueLock; // edi
int v4; // ebx
KIRQL NewIrql; // [esp+Ch] [ebp-4h]

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; // edi
volatile __int32 *p_ApcQueueLock; // edi
int processa; // [esp+14h] [ebp+8h]

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) 是将 a2APC 队列移动到 a1 中,笔者当时可能想,移动 apc 队列难道不是直接一个指针赋值就完事了嘛,深入分析该函数之后才意识到,里面主要做的操作还有修改双向链表,使得链表第一个成员的 bk 和最后一个成员的 fd 指针指向了新的正确的位置。

紧接着初始化当前线程的新的 ApcState,当附加之前线程不处于挂靠态,则设置 ApcStateIndexApcStatePointer,与我们之前分析对应成员的结果一致。

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; // edx
unsigned int SetMember; // ecx
unsigned int DirectoryTableBase; // eax
int result; // eax
_KGDTENTRY *GDT; // ecx

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; // esi
volatile __int32 *p_ApcQueueLock; // ebx
KAPC_STATE *p_ApcState; // edi
int v4; // ecx
_KPROCESS *Process; // [esp+10h] [ebp-Ch]
int v6; // [esp+14h] [ebp-8h]
int v7; // [esp+14h] [ebp-8h]
KIRQL NewIrql; // [esp+1Bh] [ebp-1h]

if ( ApcState->Process != (_KPROCESS *)1 ) // 如果当时附加进程的时候和当前线程所属进程一致,则 ApcState->Process==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 ) // 如果附加进程时,线程不为挂靠状态,则 ApcState->Process==0
{
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,附加之前线程为非挂靠态,因此脱离时需要回到非挂靠态。
  • 其余情况,附加之前线程就已经是挂靠态,因此脱离不需要做额外的操作。

参考文献