寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。
看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。
? 華麗的分割線 ?
NtReadVirtualMemory 分析
由於是僅僅分析掛靠時該函式是如何備份和恢復APC
佇列的,為了縮短篇幅增加可讀性,我會盡可能使用IDA
翻譯的虛擬碼,你的虛擬碼結果應該和我的不一樣,因為我進行了一些重新命名操作。我們先定位到NtReadVirtualMemory
這個虛擬碼:
NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, SIZE_T NumberOfBytesToRead, PSIZE_T NumberOfBytesRead)
{
_KTHREAD *v5; // edi
PSIZE_T v6; // ebx
int v8; // [esp+10h] [ebp-28h] BYREF
PVOID Object; // [esp+14h] [ebp-24h] BYREF
KPROCESSOR_MODE AccessMode[4]; // [esp+18h] [ebp-20h]
NTSTATUS v11; // [esp+1Ch] [ebp-1Ch]
CPPEH_RECORD ms_exc; // [esp+20h] [ebp-18h]
v5 = KeGetCurrentThread();
AccessMode[0] = v5->PreviousMode;
if ( AccessMode[0] )
{
if ( BaseAddress + NumberOfBytesToRead < BaseAddress
|| Buffer + NumberOfBytesToRead < Buffer
|| BaseAddress + NumberOfBytesToRead > MmHighestUserAddress
|| Buffer + NumberOfBytesToRead > MmHighestUserAddress )
{
return 0xC0000005;
}
v6 = NumberOfBytesRead;
if ( NumberOfBytesRead )
{
ms_exc.registration.TryLevel = 0;
if ( NumberOfBytesRead >= MmUserProbeAddress )
*MmUserProbeAddress = 0;
*NumberOfBytesRead = *NumberOfBytesRead;
ms_exc.registration.TryLevel = -1;
}
}
else
{
v6 = NumberOfBytesRead;
}
v8 = 0;
v11 = 0;
if ( NumberOfBytesToRead )
{
v11 = ObReferenceObjectByHandle(ProcessHandle, 0x10u, PsProcessType, AccessMode[0], &Object, 0);
if ( !v11 )
{
v11 = MmCopyVirtualMemory(
Object,
BaseAddress,
v5->ApcState.Process,
Buffer,
NumberOfBytesToRead,
AccessMode[0],
&v8);
ObfDereferenceObject(Object);
}
}
if ( v6 )
{
*v6 = v8;
ms_exc.registration.TryLevel = -1;
}
return v11;
}
我們可以看到,該函式實現記憶體拷貝是通過MmCopyVirtualMemory
這個函式實現的,我們點選去看看:
NTSTATUS __stdcall MmCopyVirtualMemory(PEX_RUNDOWN_REF RunRef, int a2, PRKPROCESS KPROCESS, volatile void *Address, SIZE_T Length, KPROCESSOR_MODE AccessMode, int a7)
{
struct _KPROCESS *v8; // ebx
PRKPROCESS kprocess; // ecx
NTSTATUS res; // esi
struct _EX_RUNDOWN_REF *RunRefa; // [esp+8h] [ebp+8h]
if ( !Length )
return 0;
v8 = RunRef;
kprocess = RunRef;
if ( RunRef == KeGetCurrentThread()->ApcState.Process )
kprocess = KPROCESS;
RunRefa = &kprocess[1].ProfileListHead.Blink;
if ( !ExAcquireRundownProtection(&kprocess[1].ProfileListHead.Blink) )
return STATUS_PROCESS_IS_TERMINATING;
if ( Length <= 0x1FF )
goto LABEL_10;
res = MiDoMappedCopy(v8, a2, KPROCESS, Address, Length, AccessMode, a7);
if ( res == STATUS_WORKING_SET_QUOTA )
{
*a7 = 0;
LABEL_10:
res = MiDoPoolCopy(v8, a2, KPROCESS, Address, Length, AccessMode, a7);
}
ExReleaseRundownProtection(RunRefa);
return res;
}
你可能看到一個新奇的函式ExAcquireRundownProtection
,這個函式是申請一個鎖,從網上查閱翻譯過來是停運保護(RundownProtection
)鎖,名字怪怪的聽起來怪怪的。
這個不涉及我們的核心,我們繼續分析,發現它內部又是通過MiDoMappedCopy
實現程式記憶體讀取的:
NTSTATUS __stdcall MiDoMappedCopy(PRKPROCESS PROCESS, int a2, PRKPROCESS a3, volatile void *Address, SIZE_T Length, KPROCESSOR_MODE AccessMode, int a7)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
v13 = 0;
v22 = a2;
v17 = Address;
v7 = 0xE000;
if ( Length <= 0xE000 )
v7 = Length;
v16 = &MemoryDescriptorList;
Length_1 = Length;
v19 = v7;
v20 = 0;
v14 = 0;
v15 = 0;
while ( Length_1 )
{
if ( Length_1 < v19 )
v19 = Length_1;
KeStackAttachProcess(PROCESS, &ApcState);
BaseAddress = 0;
v12 = 0;
v11 = 0;
ms_exc.registration.TryLevel = 0;
if ( v22 == a2 && AccessMode )
{
v20 = 1;
if ( Length && (a2 + Length < a2 || a2 + Length > MmUserProbeAddress) )
ExRaiseAccessViolation();
v20 = 0;
}
MemoryDescriptorList.Next = 0;
MemoryDescriptorList.Size = 4 * (((v22 & 0xFFF) + v19 + 0xFFF) >> 12) + 28;
MemoryDescriptorList.MdlFlags = 0;
MemoryDescriptorList.StartVa = (v22 & 0xFFFFF000);
MemoryDescriptorList.ByteOffset = v22 & 0xFFF;
MemoryDescriptorList.ByteCount = v19;
MmProbeAndLockPages(&MemoryDescriptorList, AccessMode, IoReadAccess);
v12 = 1;
BaseAddress = MmMapLockedPagesSpecifyCache(&MemoryDescriptorList, 0, MmCached, 0, 0, 0x20u);
if ( !BaseAddress )
{
v13 = 1;
ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);
}
KeUnstackDetachProcess(&ApcState);
KeStackAttachProcess(a3, &ApcState);
if ( v22 == a2 )
{
if ( AccessMode )
{
v20 = 1;
ProbeForWrite(Address, Length, 1u);
v20 = 0;
}
}
v11 = 1;
qmemcpy(v17, BaseAddress, v19);
ms_exc.registration.TryLevel = -1;
KeUnstackDetachProcess(&ApcState);
MmUnmapLockedPages(BaseAddress, &MemoryDescriptorList);
MmUnlockPages(&MemoryDescriptorList);
Length_1 -= v19;
v22 += v19;
v17 += v19;
}
*a7 = Length;
return STATUS_SUCCESS;
}
經過分析,發現與APC
備份恢復的都是在程式掛靠相關函式上:KeStackAttachProcess
和KeUnstackDetachProcess
。我們先看看KeStackAttachProcess
:
void __stdcall KeStackAttachProcess(PRKPROCESS PROCESS, PRKAPC_STATE ApcState)
{
_KTHREAD *CurrentThread; // esi
char PROCESSa; // [esp+10h] [ebp+8h]
CurrentThread = KeGetCurrentThread();
if ( KeGetPcr()->PrcbData.DpcRoutineActive )
KeBugCheckEx(
5u,
PROCESS,
CurrentThread->ApcState.Process,
CurrentThread->ApcStateIndex,
KeGetPcr()->PrcbData.DpcRoutineActive);
if ( CurrentThread->ApcState.Process == PROCESS )
{
ApcState->Process = 1;
}
else
{
PROCESSa = KeRaiseIrqlToDpcLevel();
if ( CurrentThread->ApcStateIndex )
{
KiAttachProcess(CurrentThread, PROCESS, PROCESSa, ApcState);
}
else
{
KiAttachProcess(CurrentThread, PROCESS, PROCESSa, &CurrentThread->SavedApcState);
ApcState->Process = 0;
}
}
}
重點我們來看看ApcStateIndex
,上一篇我們講過,當正常狀態為0,掛靠狀態為1.也就是說,他將會走如下程式碼:
KiAttachProcess(CurrentThread, PROCESS, PROCESSa, ApcState);
點選去看看裡面有啥程式碼:
void __stdcall KiAttachProcess(_KTHREAD *thread, PRKPROCESS Process, KIRQL irql, PRKAPC_STATE ApcState)
{
// [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
++Process->StackCount;
KiMoveApcState(&thread->ApcState, ApcState);
InitializeListHead(thread->ApcState.ApcListHead);
InitializeListHead(&thread->ApcState.ApcListHead[1]);
thread->ApcState.Process = Process;
thread->ApcState.KernelApcInProgress = 0;
thread->ApcState.KernelApcPending = 0;
thread->ApcState.UserApcPending = 0;
if ( ApcState == &thread->SavedApcState )
{
thread->ApcStatePointer[0] = &thread->SavedApcState;
thread->ApcStatePointer[1] = &thread->ApcState;
thread->ApcStateIndex = 1;
}
if ( Process->State )
{
thread->State = 1;
thread->ProcessReadyQueue = 1;
v9 = Process->ReadyListHead.Blink;
thread->WaitListEntry.Flink = &Process->ReadyListHead;
thread->WaitListEntry.Blink = v9;
v9->Flink = &thread->WaitListEntry;
Process->ReadyListHead.Blink = &thread->WaitListEntry;
if ( Process->State == 1 )
{
Process->State = 2;
v10 = KiProcessInSwapListHead;
v11 = &Process->SwapListEntry;
Processa = &Process->SwapListEntry;
ApcStatea = KiProcessInSwapListHead;
do
{
v11->Next = v10;
v12 = v10;
v10 = ApcStatea;
_ECX = &KiProcessInSwapListHead;
_EDX = Processa;
__asm { cmpxchg [ecx], edx }
}
while ( ApcStatea != v12 );
KiSetSwapEvent();
}
thread->WaitIrql = irql;
KiSwapThread();
}
else
{
v4 = &Process->ReadyListHead;
while ( 1 )
{
v8 = v4->Flink;
if ( v4->Flink == v4 )
break;
v5 = v8->Flink;
v6 = v8 - 12;
v7 = v8->Blink;
v7->Flink = v5;
v5->Blink = v7;
BYTE1(v6[37].Flink) = 0;
KiReadyThread(v6);
}
KiSwapProcess(Process, ApcState->Process);
KiUnlockDispatcherDatabase(irql);
}
}
我們就可以看到裡面與APC
備份相關操作了:
if ( ApcState == &thread->SavedApcState )
{
thread->ApcStatePointer[0] = &thread->SavedApcState;
thread->ApcStatePointer[1] = &thread->ApcState;
thread->ApcStateIndex = 1;
}
我們再來看看KeUnstackDetachProcess
這個函式:
void __stdcall KeUnstackDetachProcess(PRKAPC_STATE ApcState)
{
PRKAPC_STATE v1; // ebx
_KTHREAD *CurrentThread; // esi
_KPROCESS *CurrentProcess; // edi
int v4; // eax
int v7; // ecx
_KAPC_STATE *v8; // [esp-Ch] [ebp-20h]
int v9; // [esp+4h] [ebp-10h]
int v10; // [esp+Ch] [ebp-8h]
signed __int8 v11; // [esp+13h] [ebp-1h]
v1 = ApcState;
if ( ApcState->Process != 1 )
{
CurrentThread = KeGetCurrentThread();
v11 = KeRaiseIrqlToDpcLevel();
if ( !CurrentThread->ApcStateIndex
|| CurrentThread->ApcState.KernelApcInProgress
|| CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->ApcState
|| CurrentThread->ApcState.ApcListHead[1].Flink != &CurrentThread->ApcState.ApcListHead[1] )
{
KeBugCheck(6u);
}
CurrentProcess = CurrentThread->ApcState.Process;
if ( !--CurrentProcess->StackCount && CurrentProcess->ThreadListHead.Flink != &CurrentProcess->ThreadListHead )
{
CurrentProcess->State = 3;
v4 = KiProcessOutSwapListHead;
v10 = KiProcessOutSwapListHead;
do
{
CurrentProcess->SwapListEntry.Next = v4;
v9 = v4;
v4 = v10;
_ECX = &KiProcessOutSwapListHead;
_EDX = &CurrentProcess->SwapListEntry;
__asm { cmpxchg [ecx], edx }
}
while ( v10 != v9 );
KiSetSwapEvent();
v1 = ApcState;
}
v8 = &CurrentThread->ApcState;
if ( v1->Process )
{
KiMoveApcState(v1, v8);
}
else
{
KiMoveApcState(&CurrentThread->SavedApcState, v8);
CurrentThread->SavedApcState.Process = 0;
CurrentThread->ApcStatePointer[0] = &CurrentThread->ApcState;
CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState;
CurrentThread->ApcStateIndex = 0;
}
if ( CurrentThread->ApcState.ApcListHead[0].Flink != &CurrentThread->ApcState )
{
LOBYTE(v7) = 1;
CurrentThread->ApcState.KernelApcPending = 1;
HalRequestSoftwareInterrupt(v7);
}
KiSwapProcess(CurrentThread->ApcState.Process, CurrentProcess);
KiUnlockDispatcherDatabase(v11);
}
}
我們很快找到了與APC
恢復相關的程式碼:
if ( v1->Process )
{
KiMoveApcState(v1, v8);
}
else
{
KiMoveApcState(&CurrentThread->SavedApcState, v8);
CurrentThread->SavedApcState.Process = 0;
CurrentThread->ApcStatePointer[0] = &CurrentThread->ApcState;
CurrentThread->ApcStatePointer[1] = &CurrentThread->SavedApcState;
CurrentThread->ApcStateIndex = 0;
}
分析至此,本題就結束了。
QueueUserAPC 引發的血案
還記著 APC 篇——備用 APC 佇列 提供的第一題的參考程式碼中的一行註釋了嗎?
DWORD WINAPI ThreadProc(VOID* Param)
{
for (int i =0 ;i<100;i++)
{
SleepEx(1000,TRUE); //思考為什麼?
//Sleep(1000);
printf("Running\n");
}
return 0;
}
為什麼我用SleepEx
函式而不是用Sleep
嗎?你思考這個問題了嗎?我們來看看下面幾個圖:
我們將SleepEx
函式用Sleep
替換,並註釋掉主函式的Sleep
看看效果:
APC
正常被執行,接下來我們去掉註釋掉主函式的Sleep
,繼續執行看看:
這次竟然發現APC
沒有執行,到底是為什麼呢?我們改回原答案,就可以正常執行APC
了,也就是我在參考中給的效果圖:
原因將會在本篇後部分進行揭曉。
KAPC
無論是正常狀態還是掛靠狀態,都有兩個APC
佇列,一個核心佇列,一個使用者佇列。每當要掛入一個APC函式時,不管是核心APC
還是使用者APC
,核心都要準備一個KAPC
的資料結構,並且將這個KAPC
結構掛到相應的APC
佇列中。現在我們看看KAPC
的結構:
kd> dt _KAPC
ntdll!_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
Type
指明結構體的型別,APC
型別為0x12
。
Size
該結構體的大小,值為0x30
。
Thread
指向目標執行緒的執行緒結構體的指標,因為任何一個APC
都是讓目標執行緒進行完成。
ApcListEntry
APC
佇列掛的位置。
KernelRoutine
指向一個函式,呼叫ExFreePoolWithTag
釋放APC
。
NormalRoutine
儲存著使用者APC
總入口或真正的核心APC
函式地址,裡面具體的細節將會在後面的文章進行介紹。
NormalContext
當為核心APC
,該成員儲存著NULL
;如果為使用者APC
,則為真正的APC
函式。
SystemArgument1
APC
函式的引數。
SystemArgument2
APC
函式的引數。
ApcStateIndex
掛哪個佇列,有四個值:0、1、2、3,裡面的細節將在後面進行介紹。
ApcMode
指示該APC
是核心APC
還是使用者APC
。
Inserted
表示本APC
是否已掛入佇列。掛入前值為0,掛入後值為1。
掛入流程
為了方便理解,我們先擼一下函式大體呼叫流程:
其中QueueUserAPC
這個函式位於kernel32.dll
,它會呼叫核心模組的NtQueueApcThread
進行實現,經歷過重重呼叫,使用KeInitializeApc
為APC
結構體分配記憶體並進行初始化,呼叫KeInsertQueueApc
進行插入到指定佇列,而插入最終由KiInsertQueueApc
實現。
KeInitializeApc 函式說明
為了做好本篇練習,我們先過一下KeInitializeApc
的相關說明:
VOID KeInitializeApc
(
IN PKAPC Apc, //KAPC 指標
IN PKTHREAD Thread, //目標執行緒
IN KAPC_ENVIRONMENT TargetEnvironment, //四種狀態
IN PKKERNEL_ROUTINE KernelRoutine, //銷燬 KAPC 的函式地址
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine, //使用者 APC 總入口或者核心 APC 函式
IN KPROCESSOR_MODE Mode,//要插入使用者 APC 佇列還是核心 APC 佇列
IN PVOID Context//核心APC:NULL,使用者APC:真正的APC函式
)
ApcStateIndex 詳解
該成員與KTHREAD + 0x165
偏移處的屬性同名,但含義不一樣。該ApcStateIndex
有四個值,如下面表格所示:
值 | 含義 |
---|---|
0 | 原始環境 |
1 | 掛靠環境 |
2 | 當前環境 |
3 | 插入APC時的當前環境 |
前兩個值挺好理解,當值為0時,就是指執行緒的“親生父母”;如果值為1時,就是指自己的“養父母”。後面的兩個值比較繞,下面將會詳細解釋一下:
上一篇我們說過,執行緒在正常情況下ApcStatePointer[0]
指向ApcState
,ApcStatePointer[1]
指向SavedApcState
;而在掛靠情況下ApcStatePointer[0]
指向SavedApcState
,ApcStatePointer[1]
指向ApcState
。當值為2的時候,插入的是當前程式的佇列。什麼是當前佇列,是我不管你環境是掛靠還是不掛靠,我就插入當前程式的APC
佇列裡面,以初始化APC
的時候為基準。還剩下最玄學的一個值,當值為3時,插入的是當前程式的APC
佇列,此時有修復ApcStateIndex
的操作,以插入APC
的時候為基準。
KiInsertQueueApc 呼叫流程
為了降低本篇思考題難度,我把該函式的呼叫流程說一下:
- 根據
KAPC
結構中的ApcStateIndex
找到對應的APC
佇列 - 再根據
KAPC
結構中的ApcMode
確定是使用者佇列還是核心佇列 - 將
KAPC
掛到對應的佇列中(掛到KAPC
的ApcListEntry
處) - 再根據
KAPC
結構中的Inserted
置1,標識當前的KAPC
為已插入狀態 - 修改
KAPC_STATE
結構中的KernelApcPending
/UserApcPending
Alertable 詳解
Alertable
屬性位於KTHREAD
當中,如下所示:
kd> dt _KTHREAD
ntdll!_KTHREAD
...
+0x164 Alertable : UChar
...
我們可以發現很多與執行緒相關的結尾帶Ex
的函式的引數都會有一個bAlertable
,舉例如下:
DWORD SleepEx(
DWORD dwMilliseconds, // time-out interval
BOOL bAlertable // early completion option
);
DWORD WaitForSingleObjectEx(
HANDLE hHandle, // handle to object
DWORD dwMilliseconds, // time-out interval
BOOL bAlertable // alertable option
);
該值指示執行緒是否執行被APC
吵醒,我們開頭說QueueUserAPC 引發的血案
解決辦法就是由該屬性搗的鬼。當該屬性為0時,當前插入的使用者APC
函式未必有機會執當UserApcPending = 0
時就會無法執行插入的APC
,如果Alertable = 1
,就會使UserApcPending = 1
,從而將目標執行緒喚醒,從等待連結串列中被摘出來,並掛到排程連結串列當中執行。
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成,本篇參考將會在正文給出。
1️⃣ 逆向分析QueueUserAPC
完整的呼叫流程。
2️⃣ 如果在一個無法被喚醒的執行緒插入一個APC
,然後緊接又插入一個,如果設定執行緒可被喚醒,那麼它會執行幾個APC
呢?請用程式碼論證。
下一篇
APC 篇—— APC 執行