本篇原文為 Depths of Windows APC ,如果有良好的英文基礎,可以點選該連結進行閱讀。本文為我個人:寂靜的羽夏(wingsummer) 中文翻譯,非機翻,著作權歸原作者 Rbmm 和 Dennis A. Babkin 所有。
由於原文十分冗長,也十分乾貨,採用機翻輔助,人工閱讀比對修改的方式進行,如有翻譯不得當的地方,歡迎批評指正。翻譯不易,如有閒錢,歡迎支援。注意在轉載文章時注意保留原文的作者連結,我(譯者)的相關資訊。
本篇文章包含一些沒有被原廠家(指微軟)進行文件化的功能和特色。在閱讀本篇文章的相關內容和建議之後,你應該為自己的所作所為負責。展示在本文的方法依賴於內部實現並且可能以後不再有效。
介紹
在我們的第一篇關於錯綜複雜的使用者APC
的博文釋出之後,我們決定從細節的深度擴充該主題,將會介紹有關非同步過程呼叫(APC
)在Windows
作業系統的內部實現。
那我們就開始吧,這裡介紹沒有什麼特別的先後順序。
目錄
以下所有主題之間的聯絡並不是特別緊密,所以你或許想要一個可以更容易查閱的目錄表:
- 介紹
- 目錄
- APC 內部實現概要
- 來自核心的使用者 APC
- Windows XP 中漏洞百出的使用者模式 APC 實現
- 錯綜複雜的使用者模式 APC 實現 DLL 注入
- ZwQueueApcThread 與 QueueUserAPC 孰優孰劣
- 使用者 APC 演示程式碼
- 32 位程式中的 64 位使用者 APC
- 後記
APC 內部實現概要
為了在後面的文章——《伸入 NT 核心的非同步過程呼叫內幕》,能夠更好讓大家更深入的理解核心 APC 的內部實現,我們不會重複原來已經講過的了,而是多多陳述一些其他且鮮為人知的 APC 相關的細節。
為了更簡潔的描述,在技術上說 APC 就是一堆在記憶體裡儲存的二進位制位元組,也就是所謂的 KAPC 結構體:
typedef struct _KAPC {
UCHAR Type;
UCHAR SpareByte0;
UCHAR Size;
UCHAR SpareByte1;
ULONG SpareLong0;
_KTHREAD * Thread;
_LIST_ENTRY ApcListEntry;
void (* KernelRoutine)( _KAPC * , void (* * )( void * , void * , void * ), void * * , void * * , void * * );
void (* RundownRoutine)( _KAPC * );
void (* NormalRoutine)( void * , void * , void * );
void * Reserved[0x3];
void * NormalContext;
void * SystemArgument1;
void * SystemArgument2;
CHAR ApcStateIndex;
CHAR ApcMode;
UCHAR Inserted;
}KAPC, *PKAPC;
上述結構體是在KAPC_STATE
結構體裡面的雙向連結串列一部分:
typedef struct _KAPC_STATE {
_LIST_ENTRY ApcListHead[0x2];
_KPROCESS * Process;
UCHAR InProgressFlags;
UCHAR KernelApcInProgress : 01; // 0x01;
UCHAR SpecialApcInProgress : 01; // 0x02;
UCHAR KernelApcPending;
UCHAR UserApcPendingAll;
UCHAR SpecialUserApcPending : 01; // 0x01;
UCHAR UserApcPending : 01; // 0x02;
}KAPC_STATE, *PKAPC_STATE;
並且KAPC_STATE
自身也是執行緒物件的一部分,儲存在核心裡的KTHREAD
結構體中:
? 點選檢視 KTHREAD ?
typedef struct _KTHREAD {
_DISPATCHER_HEADER Header;
void * SListFaultAddress;
ULONGLONG QuantumTarget;
void * InitialStack;
void * volatile StackLimit;
void * StackBase;
ULONGLONG ThreadLock;
ULONGLONG volatile CycleTime;
ULONG CurrentRunTime;
ULONG ExpectedRunTime;
void * KernelStack;
_XSAVE_FORMAT * StateSaveArea;
_KSCHEDULING_GROUP * volatile SchedulingGroup;
_KWAIT_STATUS_REGISTER WaitRegister;
UCHAR volatile Running;
UCHAR Alerted[0x2];
ULONG AutoBoostActive : 01; // 0x00000001;
ULONG ReadyTransition : 01; // 0x00000002;
ULONG WaitNext : 01; // 0x00000004;
ULONG SystemAffinityActive : 01; // 0x00000008;
ULONG Alertable : 01; // 0x00000010;
ULONG UserStackWalkActive : 01; // 0x00000020;
ULONG ApcInterruptRequest : 01; // 0x00000040;
ULONG QuantumEndMigrate : 01; // 0x00000080;
ULONG UmsDirectedSwitchEnable : 01; // 0x00000100;
ULONG TimerActive : 01; // 0x00000200;
ULONG SystemThread : 01; // 0x00000400;
ULONG ProcessDetachActive : 01; // 0x00000800;
ULONG CalloutActive : 01; // 0x00001000;
ULONG ScbReadyQueue : 01; // 0x00002000;
ULONG ApcQueueable : 01; // 0x00004000;
ULONG ReservedStackInUse : 01; // 0x00008000;
ULONG UmsPerformingSyscall : 01; // 0x00010000;
ULONG TimerSuspended : 01; // 0x00020000;
ULONG SuspendedWaitMode : 01; // 0x00040000;
ULONG SuspendSchedulerApcWait : 01; // 0x00080000;
ULONG CetUserShadowStack : 01; // 0x00100000;
ULONG BypassProcessFreeze : 01; // 0x00200000;
ULONG Reserved : 10; // 0xffc00000;
LONG MiscFlags;
ULONG BamQosLevel : 02; // 0x00000003;
ULONG AutoAlignment : 01; // 0x00000004;
ULONG DisableBoost : 01; // 0x00000008;
ULONG AlertedByThreadId : 01; // 0x00000010;
ULONG QuantumDonation : 01; // 0x00000020;
ULONG EnableStackSwap : 01; // 0x00000040;
ULONG GuiThread : 01; // 0x00000080;
ULONG DisableQuantum : 01; // 0x00000100;
ULONG ChargeOnlySchedulingGroup : 01; // 0x00000200;
ULONG DeferPreemption : 01; // 0x00000400;
ULONG QueueDeferPreemption : 01; // 0x00000800;
ULONG ForceDeferSchedule : 01; // 0x00001000;
ULONG SharedReadyQueueAffinity : 01; // 0x00002000;
ULONG FreezeCount : 01; // 0x00004000;
ULONG TerminationApcRequest : 01; // 0x00008000;
ULONG AutoBoostEntriesExhausted : 01; // 0x00010000;
ULONG KernelStackResident : 01; // 0x00020000;
ULONG TerminateRequestReason : 02; // 0x000c0000;
ULONG ProcessStackCountDecremented : 01; // 0x00100000;
ULONG RestrictedGuiThread : 01; // 0x00200000;
ULONG VpBackingThread : 01; // 0x00400000;
ULONG ThreadFlagsSpare : 01; // 0x00800000;
ULONG EtwStackTraceApcInserted : 08; // 0xff000000;
LONG volatile ThreadFlags;
UCHAR volatile Tag;
UCHAR SystemHeteroCpuPolicy;
UCHAR UserHeteroCpuPolicy : 07; // 0x7f;
UCHAR ExplicitSystemHeteroCpuPolicy : 01; // 0x80;
UCHAR RunningNonRetpolineCode : 01; // 0x01;
UCHAR SpecCtrlSpare : 07; // 0xfe;
UCHAR SpecCtrl;
ULONG SystemCallNumber;
ULONG ReadyTime;
void * FirstArgument;
_KTRAP_FRAME * TrapFrame;
_KAPC_STATE ApcState;
UCHAR ApcStateFill[0x2b];
CHAR Priority;
ULONG UserIdealProcessor;
LONGLONG volatile WaitStatus;
_KWAIT_BLOCK * WaitBlockList;
_LIST_ENTRY WaitListEntry;
_SINGLE_LIST_ENTRY SwapListEntry;
_DISPATCHER_HEADER * volatile Queue;
void * Teb;
ULONGLONG RelativeTimerBias;
_KTIMER Timer;
_KWAIT_BLOCK WaitBlock[0x4];
UCHAR WaitBlockFill4[0x14];
ULONG ContextSwitches;
UCHAR WaitBlockFill5[0x44];
UCHAR volatile State;
CHAR Spare13;
UCHAR WaitIrql;
CHAR WaitMode;
UCHAR WaitBlockFill6[0x74];
ULONG WaitTime;
UCHAR WaitBlockFill7[0xa4];
SHORT KernelApcDisable;
SHORT SpecialApcDisable;
ULONG CombinedApcDisable;
UCHAR WaitBlockFill8[0x28];
_KTHREAD_COUNTERS * ThreadCounters;
UCHAR WaitBlockFill9[0x58];
_XSTATE_SAVE * XStateSave;
UCHAR WaitBlockFill10[0x88];
void * volatile Win32Thread;
UCHAR WaitBlockFill11[0xb0];
_UMS_CONTROL_BLOCK * Ucb;
_KUMS_CONTEXT_HEADER * volatile Uch;
void * Spare21;
_LIST_ENTRY QueueListEntry;
ULONG volatile NextProcessor;
ULONG NextProcessorNumber : 31; // 0x7fffffff;
ULONG SharedReadyQueue : 01; // 0x80000000;
LONG QueuePriority;
_KPROCESS * Process;
_GROUP_AFFINITY UserAffinity;
UCHAR UserAffinityFill[0xa];
CHAR PreviousMode;
CHAR BasePriority;
CHAR PriorityDecrement;
UCHAR ForegroundBoost : 04; // 0x0f;
UCHAR UnusualBoost : 04; // 0xf0;
UCHAR Preempted;
UCHAR AdjustReason;
CHAR AdjustIncrement;
ULONGLONG AffinityVersion;
_GROUP_AFFINITY Affinity;
UCHAR AffinityFill[0xa];
UCHAR ApcStateIndex;
UCHAR WaitBlockCount;
ULONG IdealProcessor;
ULONGLONG NpxState;
_KAPC_STATE SavedApcState;
UCHAR SavedApcStateFill[0x2b];
UCHAR WaitReason;
CHAR SuspendCount;
CHAR Saturation;
USHORT SListFaultCount;
_KAPC SchedulerApc;
UCHAR SchedulerApcFill0[0x1];
UCHAR ResourceIndex;
UCHAR SchedulerApcFill1[0x3];
UCHAR QuantumReset;
UCHAR SchedulerApcFill2[0x4];
ULONG KernelTime;
UCHAR SchedulerApcFill3[0x40];
_KPRCB * volatile WaitPrcb;
UCHAR SchedulerApcFill4[0x48];
void * LegoData;
UCHAR SchedulerApcFill5[0x53];
UCHAR CallbackNestingLevel;
ULONG UserTime;
_KEVENT SuspendEvent;
_LIST_ENTRY ThreadListEntry;
_LIST_ENTRY MutantListHead;
UCHAR AbEntrySummary;
UCHAR AbWaitEntryCount;
UCHAR AbAllocationRegionCount;
CHAR SystemPriority;
ULONG SecureThreadCookie;
_KLOCK_ENTRY LockEntries[0x6];
_SINGLE_LIST_ENTRY PropagateBoostsEntry;
_SINGLE_LIST_ENTRY IoSelfBoostsEntry;
UCHAR PriorityFloorCounts[0x10];
ULONG PriorityFloorSummary;
LONG volatile AbCompletedIoBoostCount;
LONG volatile AbCompletedIoQoSBoostCount;
SHORT volatile KeReferenceCount;
UCHAR AbOrphanedEntrySummary;
UCHAR AbOwnedEntryCount;
ULONG ForegroundLossTime;
_LIST_ENTRY GlobalForegroundListEntry;
_SINGLE_LIST_ENTRY ForegroundDpcStackListEntry;
ULONGLONG InGlobalForegroundList;
LONGLONG ReadOperationCount;
LONGLONG WriteOperationCount;
LONGLONG OtherOperationCount;
LONGLONG ReadTransferCount;
LONGLONG WriteTransferCount;
LONGLONG OtherTransferCount;
_KSCB * QueuedScb;
ULONG volatile ThreadTimerDelay;
LONG volatile ThreadFlags2;
ULONG PpmPolicy : 02; // 0x00000003;
ULONG ThreadFlags2Reserved : 30; // 0xfffffffc;
ULONGLONG TracingPrivate[0x1];
void * SchedulerAssist;
void * volatile AbWaitObject;
}KTHREAD, *PKTHREAD;
將執行緒掛靠到另一個程式
值得注意的一點就是任何一個執行緒都可以通過呼叫KeStackAttachProcess
(該函式會接收KAPC_STATE
物件,並檢視它的ApcState
引數)臨時地掛靠到另一個程式上,也可以通過呼叫KeUnstackDetachProcess
函式脫離程式。但是這會有會導致問題一點點的可能性,所以開發者需要把注意力放到上面。
因此,有一個十分重要的事情去理解,我們需要通過使用一個未被文件化但是匯出的KeInitializeApc
呼叫初始化一個APC
物件:
VOID KeInitializeApc(
IN PRKAPC Apc, //pointer to KAPC
IN PKTHREAD Thread,
IN KAPC_ENVIRONMENT Environment,
IN PKKERNEL_ROUTINE KernelRoutine,
IN PKRUNDOWN_ROUTINE RundownRoutine OPTIONAL,
IN PKNORMAL_ROUTINE NormalRoutine OPTIONAL,
IN KPROCESSOR_MODE ApcMode,
IN PVOID NormalContext
);
我們使用該函式需要提供KAPC_ENVIRONMENT
型別的引數,它的列舉如下:
typedef enum _KAPC_ENVIRONMENT {
OriginalApcEnvironment,
AttachedApcEnvironment,
CurrentApcEnvironment
} KAPC_ENVIRONMENT;
這個引數指定了APC
環境,換句話說,當我們插入一個APC
,我們會告訴系統是應該為當前執行緒啟用它,還是應該線上程掛靠到另一個程式之前為儲存的狀態(KTHREAD::SavedApcState
)啟用它。該引數將會在後面儲存到KAPC::ApcStateIndex
成員當中。
為了更好的說明這個概念,讓我們回顧如下的KiInsertQueueApc
程式碼:
// KiInsertQueueApc() excerpt:
Thread = Apc->Thread;
PKAPC_STATE ApcState;
if (Apc->ApcStateIndex == 0 && Thread->ApcStateIndex != 0)
{
ApcState = &Thread->SavedApcState;
}
else
{
Apc->ApcStateIndex = Thread->ApcStateIndex;
ApcState = &Thread->ApcState;
}
所以本質上KAPC::ApcStateIndex
是一個布林值:
- 非0:指示
APC
插入到當前執行緒中,話句話說,APC
應該執行在當前程式的上下文環境中,也就是執行緒當前執行的環境。 - 0:指示當前
APC
應該僅僅在源程式的環境中執行,或者線上程在程式掛靠之前的程式環境中。
在KeStackAttachProcess
函式中,有如下邏輯:
// KeStackAttachProcess() excerpt:
if (Thread->ApcStateIndex != 0)
{
KiAttachProcess(Thread, Process, &LockHandle, ApcState);
}
else
{
KiAttachProcess(Thread, Process, &LockHandle, &Thread->SavedApcState);
ApcState->Process = NULL;
}
也就是意味著,當我們第一次掛靠到另一個程式,打個比方:如果它的KAPC::ApcStateIndex
值為0,當前的KTHREAD::ApcState
儲存在KTHREAD::SavedApcState
當中,並且以前的ApcState
不會被使用,除非設定KAPC_STATE::Process
為0表示這個狀態儲存在KTHREAD::SavedApcState
。
但是如果我們遞迴式掛靠,或當一個執行緒已經掛靠到另一個程式時我們已經呼叫了KeStackAttachProcess
,在那種情況下APC
的狀態被儲存在ApcState
物件中,被作為引數傳遞到函式當中。
這種邏輯處理是為了讓系統始終可以訪問執行緒的原始APC
狀態。這可以用於將APC
插入原始執行緒,或通過呼叫KeUnstackDetachProcess
將執行緒脫離原程式。
APC 的型別
APC
有兩個基礎型別:核心APC
和使用者APC
。核心APC
給予了開發者更多便利來處理APC
排列和處理(我們在本篇博文已討論過使用者APC
)。核心APC
不向使用者層開發者們開放能夠直接訪問的許可權。
KAPC_STATE::ApcListHead
裡面包含了兩個連結串列用來存放核心APC
和使用者APC
。這兩個連結串列分別有APC
排隊等待執行緒處理:
typedef enum _MODE {
KernelMode = 0x0,
UserMode = 0x1,
MaximumMode = 0x2
}MODE;
核心使用這些列表來維護每種型別的APC
的狀態。當APC
排隊或呼叫KeInsertQueueApc
處理時,KAPC::ApcMode
用作KAPC_STATE::ApcListHead
的索引:
NTSTATUS NtQueueApcThread(
IN HANDLE Thread,
IN PKNORMAL_ROUTINE NormalRoutine,
IN PVOID NormalContext,
IN PVOID SystemArgument1,
IN PVOID SystemArgument2
);
核心 APC 的使用記憶體的易錯點
許多核心開發新手犯了一個錯誤:為核心模式APC
指定了錯誤的記憶體型別。認識到這一點很重要,以防止各種意外的藍屏當機(BSOD
)。
這是一定要記住經驗,KAPC
結構體只能使用從非分頁記憶體分配的記憶體(或者從類似NonPagedPool*
型別分配)。即使在PASSIVE_LEVEL
的IRQL
初始化並插入APC
,這也是沒問題的。
為什麼要有這樣的記憶體型別限制呢?其他一些APC
也可以插入到執行在更高排程級別IRQL
的同一執行緒中。在插入雙連結APC
列表期間,系統將嘗試訪問列表中已經存在的其他KAPC
結構。因此,如果其中任何一個使用的是從分頁記憶體分配的記憶體,你將會從DISPATCH_LEVEL
間接訪問分頁記憶體,這也是一種會導致藍屏保護原因。
比較棘手的是,我在描述如上的情況非常少見,在開發和測試階段可能不會出現。這將很難在生產程式碼中進行診斷,正如我在上面解釋的,可能過一段時間之後,會在你無法控制的環境中發生藍屏。
中斷和阻塞核心 APC
關於核心模式APC
,需要記住的重要一點是,它通過中斷實現,這意味著它可以發生在程式碼中的任意兩個CPU
指令之間。
核心開發允許我們阻止APC
的執行。只有在程式碼的某些特殊部分起作用:將IRQL
提升到APC_LEVEL
或更高階別或將寫的程式碼放在KeEnterCriticalRegion
和KeLeaveCriticalRegion
的呼叫之間。(請注意,這些函式不會阻止所謂的特殊核心APC
的執行,只有提高IRQL
級別才能阻止這些APC
的執行)。
關於我在上面展示的IRQL
條件限制,一個有趣的事實是,如果APC
到達關鍵區域,它不會丟失,稍後將在以下任一函式中處理:KeLeaveGuardedRegion
、 KeLeaveCriticalRegion
、KeLowerIrql
或者在臨界區的結尾。
RundownRoutine 細節
如果我再次引用這篇博文:
簡單點說,任何一種APC都可以定義一個有效的 RundownRoutine 。此例程必須駐留在核心記憶體中,並且僅在系統需要丟棄 APC 佇列的內容時(例如執行緒退出時)呼叫。在這種情況下,既不執行 KernelRoutine ,也不執行 NormalRoutine ,只執行 RundownRoutine。沒有此類例程的 APC 將被釋放。
——《伸入 NT 核心的非同步過程呼叫內幕》
還有幾點可以補充:
- RundownRoutine 回撥僅線上程退出且掛起的 APC 仍在排隊時呼叫(對於使用者 APC 這種情況很可能會發生),但它不會以其他方式被呼叫。
- 如果 RundownRoutine 的值為 NULL ,核心只呼叫ExFreeProol(Apc),這是在該博文的“沒有此類例程的 APC 將被釋放”中假設的。當然,如果程式設計師通過呼叫
ExAllocatePool(NonPagedPool,sizeof(KAPC))
來分配記憶體,並且之後不涉及額外的分配,那麼我們可以依靠系統來為我們釋放分配的記憶體。但是,如果KAPC
的分配方式不同,或KAPC
的地址與所分配記憶體的起始地址不匹配,或者由於其他原因,則必須在RunDownRoute
回撥過載中釋放所有分配的記憶體。
APC 和驅動解除安裝的細微差別
在呼叫核心APC
回撥例程時,有一個微妙的時刻。例如,必須始終提供核心例程(KernelRoutine
)回撥。因此當驅動程式的APC
回撥仍在執行時,自己可能無法從記憶體中解除安裝,這將一定會導致藍屏。
有一種方式可以很容易地復現因正在解除安裝的驅動程式繫結到掛起的 APC 而導致導致的藍屏。在某個執行緒上設定一個斷點,並將一個 APC 排入佇列。強制驅動程式解除安裝,然後恢復執行緒,並通過呼叫 NtTestAlert 執行 APC ,保證一定會藍屏。
理想情況下,APC
的系統實現應如下所示:
- 必須在
KAPC
中有對DriverObject
的引用,在插入APC
之前,KeInsertQueueApc
函式應該已經完成了ObfReferenceObject(Apc->DriverObject)
(此外,如果KeInsertQueueApc
失敗,也可以在內部呼叫ObfDereferenceObject(Apc->DriverObject)
),通過這些步驟,當有正在排隊的APC
時,驅動程式將不會被解除安裝。 - 那麼,在最後呼叫
KernelRoutine
/NormalRoutine
/RundownRoutine
之前,系統應該已經將DriverObject = Apc->DriverObject
讀入本地堆疊,呼叫適當的Apc
回撥,然後呼叫ObfDereferenceObject(DriverObject)
,因為回撥返回後Apc本身將無效。 - 此外,如果
RundownRoutine
是無條件呼叫的,而不是現在的呼叫方式,也會非常有用。
有了我上面提出的建議,核心模式APC
回撥例程的編碼將簡單得多。但不幸的是,這些回撥的呼叫沒有正確編碼。?
順便說一句,WorkItem 物件已經實現了這種功能,請參閱 IoInitializeWorkItem 函式說明。我們向它傳遞一個指向 DriverObject 或裝置物件的指標,它將把我們的驅動程式儲存在記憶體中,並且在 WorkItem 仍處於活動狀態時不會讓它解除安裝。換句話說,當我們新增一個 WorkItem 物件,系統會為我們呼叫 ObfReferenceObject ,然後當呼叫我們的最終回撥時,系統會呼叫 ObfDereferenceObject ,這是實現它的正確方法。
那麼,正確設定核心APC
回撥的解決方法是什麼呢?
顯然,我們可以在初始化過程中從驅動程式本身呼叫ObfReferenceObject
。但是,在物件的生命週期結束時,我們如何從物件內部呼叫ObfDereferenceObject
呢?如果我們這樣做,並且執行從ObfDereferenceObject
函式返回,我們將遇到下面的情況:我們正在執行的驅動程式程式碼已經被解除安裝,這又會導致藍屏。
我對這個問題的解決方案是使用匯編語言,並使用JMP
指令呼叫ObfDereferenceObject
函式,而不是像大多數編譯器那樣使用常規的呼叫指令。通過使用JMP
指令,我們可以保證執行不會返回到正在解除安裝的程式碼。不幸的是,這種解決方案目前不能通過C
或C++
語言來實現。
檢視此 [彙編程式碼] 以獲取實現示例,或者檢視我的 GitHub 以獲取完整示例。
案例研究 - 早期注入 Kernel32.dll 的陷阱
這是我在為一家防病毒公司做自由職業時幫助解決的實際案例(應該保持匿名)。
假設一家防病毒公司想要將他們自己的DLL
注入所有正在執行的程式中。此外,他們很早就想在他們的DLL
中執行程式碼,甚至在其他載入的DLL
有機會收到DLL_PROCESS_ATTACH
通知之前。
這對他們來說效果很好,除非系統上還安裝了一個競爭產品,如果這樣的話一切都崩了。
他們後來發現另一個反病毒軟體在載入kernel32.dll
時插入了一個APC
,這使得他們注入的DLL
更早地載入,他們無法弄清楚導致崩潰的原因。
這個難題的答案是瞭解我在這裡描述的早期 DLL 載入過程。當我們的反病毒公司的自定義DLL
在kernel32.dll
之前被注入和載入時,該DLL
不應該對除本機ntdll.dll
之外的任何其他DLL
有任何依賴(直接或間接通過其他模組中的依賴)。情況並非如此,這就是導致崩潰的原因。
如果一個驅動程式,就像我在這裡展示的那樣,呼叫一個使用者模式的APC
回撥,這反過來又在一些自定義DLL
上呼叫LoadLibrary
,並且如果在kernel32.dll
有機會載入自身之前呼叫了這樣的回撥,那麼呼叫LoadLibrary
將嘗試匯入 ntdll.dll
,而匯入尚未設定。因此,從kernel32.dll
中對ntdll.dll
中任何函式的第一次匯入呼叫將使程式崩潰。
作為反病毒公司的一種解決方法,他們需要以不同的方式編寫注入器。APC
不是最好的解決方案,因為我上面描述的限制,並且因為他們的DLL
應該被載入到系統中的每個模組中。
如果我們使用 APC 回撥,我們必須準備好我們的回撥可以在我們排隊之後隨時被呼叫。 但是,如果我們從回撥中呼叫 LoadLibrary[Ex] 型別的函式,該函式本身是從 kernel32.dll 匯入的,我們就違反了該規則,因為該庫可能尚未在我們的程式中初始化。
在這種情況下,特製的shellcode
可能是更好的方法,它將使用本機函式載入DLL
,例如ntdll!LdrLoadDll
:
NTSTATUS LdrLoadDll(
IN PCWSTR SearchPaths,
IN PULONG pFlags,
IN PCUNICODE_STRING DllName,
OUT HMODULE* pDllBase
);
此外,此類自定義DLL
本身必須僅具有來自ntdll.dll
的靜態匯入,或者使用來自kernel32.dll
的延遲載入匯入。 此類DLL
不能使用任何C執行時庫 (CRT) 和許多C++
構造器,因為它們(即使是靜態連結)會給kernel32.dll
和其他庫帶來隱式匯入。
來自核心的使用者 APC
對於使用者模式APC
,情況在以下方面有所不同:
- 它不能在任何兩條
CPU
指令之間執行,或者換句話說,它不是通過CPU
中斷傳遞的。 - 它必須在3環程式碼或使用者模式上下文中執行。
- 它僅線上程處於警報狀態時執行特定的可等待
Windows
函式後執行。
為了實現這一點,核心和本機子系統的編碼方式是在CPU
離開系統呼叫時執行使用者模式APC
。許多Windows
函式(或WinAPI
)需要呼叫核心,這是通過sysenter
這個CPU
指令傳遞的。在執行時,CPU
首先進入負責路由系統呼叫的Windows
核心部分,稱為系統服務排程程式。然後根據EAX
暫存器中提供的系統函式索引處理系統呼叫本身。只有在那之後,但在離開核心空間之前,系統服務排程程式檢查使用者模式APC
的存在並調整核心堆疊上的KTRAP_FRAME
以稍後處理使用者模式APC
。
檢查是否存在使用者模式APC
在核心中的nt!KiDeliverApc
函式中完成。簡而言之,在處理執行緒的核心模式APC
之後,它檢查KTHREAD::PreviousMode == UserMode
和KTHREAD.SpecialApcDisable
是否未設定。如果是,則檢查KTHREAD.ApcState.UserApcPending
是否不為零,表示使用者模式APC
的存在。然後它呼叫nt!KiInitializeUserApc
修改使用者模式上下文從系統呼叫返回以處理使用者模式APC
。
為此,在調整KTRAP_FRAME
以執行返回到本機子系統中的特殊ntdll!KiUserApcDispatcher
函式之前,nt!KiInitializeUserApc
會儲存系統呼叫應該返回的原始3環上下文,之後再由nt!KiInitializeUserApc
返回。
只是稍後由它返回,在執行sysexit
的CPU
指令時,由於修改了KTRAP_FRAME
上下文,CPU
返回到3環中的ntdll!KiUserApcDispatcher
函式。該函式依次處理單個使用者模式APC
,然後呼叫ntdll!NtContinue(context, TRUE)
將執行返回給核心。我上面描述的迴圈一直持續到執行緒佇列中沒有更多的使用者模式APC
。
使用者模式 APC 的實現
我需要指出使用者模式APC
的一些特殊點:
- 儘管
CPU
可以在中斷後的任意兩條指令之間的任何時刻進入核心模式,但此時不會呼叫使用者模式APC
回撥。使用者模式APC
只能在執行特殊的Windows API
呼叫後才能呼叫,正如我在此處所描述的。 - 假設任何需要
sysenter
的Windows API
都可用於在返回時處理使用者模式APC
,前提是某些核心程式碼為執行緒設定了KTHREAD.ApcState.UserApcPending
,並且使用者模式APC
在呼叫之前排隊。 - 設定
KTHREAD.ApcState.UserApcPending
是MSDN
稱為執行緒的警報狀態。這是一個有點令人困惑的術語。 - 哪些
API
可以設定KTHREAD.ApcState.UserApcPending
標誌?顯然,以下記錄的函式可以做到這一點:SleepEx
、SignalObjectAndWait
、MsgWaitForMultipleObjectsEx
、WaitForMultipleObjectsEx
或WaitForSingleObjectEx
。但也有這些未記錄的函式也可以做到這一點:-
ntdll!NtTestAlert
:沒有輸入引數。似乎它的唯一功能是準備所有排隊的使用者模式APC
。它在內部呼叫nt!KiInitializeUserApc
本身,我在這裡描述:NTSTATUS NtTestAlert();
-
ntdll!NtContinue
:它將執行返回給核心以繼續處理(就像我在此處描述的那樣),然後將執行傳遞給提供的使用者模式ThreadContext
,同時如果設定了RaiseAlert
,則可以選擇設定KTHREAD.ApcState.UserApcPending
:NTSTATUS NtContinue( IN PCONTEXT ThreadContext, IN BOOLEAN RaiseAlert );
-
“特殊”的使用者 APC
KAPC_STATE
結構中還有一個新成員,稱為SpecialUserApcPending
。除了真正的Windows 內部探索者
中的一些點點滴滴之外,對此知之甚少:
自從 APC 被弄亂以來已經有一段時間了。 RS5 現在新增了“特殊使用者 APC”(KTHREAD->SpecialUserApcPending),可以使用 NtQueueApcThreadEx 作為保留控制程式碼傳入 1 來排隊。 這些與 Mode == KernelMode 一起用一個強制執行緒訊號進行傳遞。這是一個巨大的變化。
—— Alex Ionescu
Windows XP 中漏洞百出的使用者模式 APC 實現
此資訊僅適用於 Windows XP 和更早系統上的舊版。
如果我們查閱QueueUserAPC
函式的文件,我們可以看到以下關於APC
的部分:
如果應用程式線上程開始執行之前對 APC 進行排隊,則執行緒通過呼叫 APC 函式開始......
—— MSDN
在Windows Vista
之前,當一個執行緒開始執行時(從核心這發生在呼叫KiStartUserThread
和PspUserThreadStartup
之後),核心會將一個使用者模式APC
排隊,並將回撥設定為ntdll!LdrInitializeThunk
。但這意味著在使用者模式下,執行緒將從特殊的後System-Service-Dispatcher
函式ntdll!KiUserApcDispatcher
開始執行(正如我在此處描述的),而不是從預期的ntdll!LdrInitializeThunk
開始執行。
在這種情況下的問題是,如果我們自己將APC
新增到該執行緒中,它可能在ntdll!LdrInitializeThunk
之前開始執行,因此我們將收到尚未初始化的執行緒上下文。這可能會導致一些間歇性崩潰和令人討厭的計時錯誤。
當時的解決方案是呼叫GetThreadContext
來保證執行緒上下文在返回之前被初始化。只有在那之後,才可以安全地將APC
排隊:
//WARNING: Deprecated code - do not use!
HANDLE hThread = CreateThread(NULL, 0, ThreadProc, 0, CREATE_SUSPENDED, NULL);
if (hThread)
{
CONTEXT ctx;
GetThreadContext(hThread, &ctx); //XP bug workaround
//Now it's safe to queue APC
QueueUserAPC(Papcfunc, hThread, 0);
//Because thread is originally suspended, this will ensure that our APC callback
//in 'Papcfunc' is executed before 'ThreadProc'
ResumeThread(hThread);
CloseHandle(hThread);
}
GetThreadContext 能夠解決該計時錯誤的原因是檢索執行緒上下文的方式。 它是通過將一個特殊的核心模式 APC 排隊到目標執行緒中來完成的,回撥函式收集其上下文,然後設定一個由被呼叫執行緒等待的事件,稱為 GetThreadContext,當內部事件發生時讀取上下文時進行設定。
錯綜複雜的使用者模式 APC 實現 DLL 注入
有一種技術可以將DLL
注入到我們自己啟動的程式中。它是這樣工作的:
- 建立一個最初掛起的程式(帶有
CREATE_SUSPENDED
標誌的CreateProcess
)我們只需要它的初始執行緒。 - 將
APC
新增到該執行緒(QueueUserAPC
)中,並將回撥設定為LoadLibrary
函式並恢復它(ResumeThread
)。 - 我們的
APC
回撥或對LoadLibrary
的呼叫保證在目標程式中在其入口點程式碼之前被呼叫。
但是我們的APC
回撥什麼時候會被呼叫呢?從技術上講,這應該發生在程式中的入口點程式碼有機會執行之前,在ntdll!LdrInitializeThunk
函式呼叫的出口處(當其中的程式碼呼叫NtTestAlert
時)。所以我們可以保證APC
回撥不會比那晚。但是有什麼辦法可以提前呼叫嗎?
如果在建立過程中載入到程式中的DLL
有一個呼叫其DLL_PROCESS_ATTACH
處理程式中的一個可警報等待函式(alertable wait functions
),那該怎麼辦呢?這對於Windows
系統DLL
來說是極不可能的,但對於也載入到程式中的自定義DLL
仍然這就是可能的。最起碼這種情況會導致我們的APC
回撥被提前呼叫。
但實際上,誰在乎我們是否更早地呼叫LoadLibrary
並注入我們的DLL
?在大多數情況下,這無關緊要。
PsSetLoadImageNotifyRoutine 陷阱
載入DLL
時,有一種複雜的情況可能非常關鍵。比如說,驅動程式可能會使用PsSetLoadImageNotifyRoutine
函式來攔截某些DLL
的載入。為此,它會在DLL
載入過程的早期將自己的APC
排隊。然後,驅動程式通常會通過呼叫KeDelayExecutionThread
或使用未文件化的函式KeTestAlertThread
(隱式呼叫)設定KAPC_STATE::UserApcPending
標誌,從而強制使用者模式程式碼(在APC
回撥中)在正在載入的DLL
中的程式碼有機會執行之前執行。
這可以用下面的虛擬碼來說明:
下面程式碼的完整版本可以在我的 GitHub 上找到。
? 點選檢視程式碼 ?
#ifndef _WIN64
#error Showing this for 64-bit builds only!
#endif
LONG gFlags;
PDRIVER_OBJECT g_DriverObject;
enum{
flImageNotifySet,
};
extern "C" NTSTATUS NTAPI DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
g_DriverObject = DriverObject;
DriverObject->DriverUnload = DriverUnload;
NTSTATUS status = PsSetLoadImageNotifyRoutine(OnLoadImage);
if (0 <= status)
{
_bittestandset(&gFlags, flImageNotifySet);
}
return status;
}
void NTAPI DriverUnload(PDRIVER_OBJECT DriverObject)
{
FreeLoadImageData();
}
void FreeLoadImageData()
{
if (_bittestandreset(&gFlags, flImageNotifySet)) PsRemoveLoadImageNotifyRoutine(OnLoadImage);
}
VOID CALLBACK OnLoadImage(
IN PUNICODE_STRING FullImageName,
IN HANDLE ProcessId, // Process where image is mapped
IN PIMAGE_INFO ImageInfo
)
{
STATIC_UNICODE_STRING(kernel32, "\\kernel32.dll");
if (
!ImageInfo->SystemModeImage &&
ProcessId == PsGetCurrentProcessId() && // section can be "remotely" mapped from another process
SuffixUnicodeString(FullImageName, &kernel32) &&
IsByLdrLoadDll(&kernel32)
)
{
BeginInject(&NATIVE_DLL::di);
}
}
VOID CALLBACK RundownRoutine(PKAPC );
VOID CALLBACK KernelRoutine(PKAPC , PKNORMAL_ROUTINE *, PVOID * , PVOID * ,PVOID * );
VOID CALLBACK NormalRoutine(PVOID , PVOID ,PVOID );
void BeginInject(DLL_INFORMATION* pdi)
{
PVOID Section;
if (0 <= pdi->GetSection(&Section))
{
if (PKAPC Apc = ExAllocatePool(NonPagedPool, sizeof(KAPC)))
{
KeInitializeApc(Apc, KeGetCurrentThread(), OriginalApcEnvironment,
KernelRoutine, RundownRoutine, NormalRoutine, KernelMode, Apc);
ObfReferenceObject(g_DriverObject);
ObfReferenceObject(Section);
if (!KeInsertQueueApc(Apc, Section, pdi, IO_NO_INCREMENT))
{
ObfDereferenceObject(Section);
RundownRoutine(Apc);
}
}
}
}
extern "C" NTSYSAPI BOOLEAN NTAPI KeTestAlertThread(IN KPROCESSOR_MODE AlertMode);
VOID CALLBACK _NormalRoutine (
PKAPC Apc,
PVOID Section,
DLL_INFORMATION* pdi
)
{
PVOID BaseAddress;
NTSTATUS status = pdi->MapSection(BaseAddress);
ObfDereferenceObject(Section);
if (0 <= status)
{
union {
PVOID pvNormalRoutine;
PKNORMAL_ROUTINE NormalRoutine;
};
PVOID NormalContext = BaseAddress;
pvNormalRoutine = (PBYTE)BaseAddress + pdi->rva_1;
if (pdi == &WOW_DLL::di) PsWrapApcWow64Thread(&NormalContext, &pvNormalRoutine);
KeInitializeApc(Apc, KeGetCurrentThread(), OriginalApcEnvironment,
KernelRoutine, RundownRoutine, NormalRoutine, UserMode, NormalContext);
ObfReferenceObject(g_DriverObject);
if (KeInsertQueueApc(Apc, NtCurrentProcess(), BaseAddress, IO_NO_INCREMENT))
{
//Force user-mode APC callback
KeTestAlertThread(UserMode);
return;
}
ObfDereferenceObject(g_DriverObject);
MmUnmapViewOfSection(IoGetCurrentProcess(), BaseAddress);
}
_RundownRoutine(Apc);
}
VOID CALLBACK _KernelRoutine(
PKAPC Apc,
PKNORMAL_ROUTINE * /*NormalRoutine*/,
PVOID * /*NormalContext*/,
PVOID * /*SystemArgument1*/,
PVOID * /*SystemArgument2*/
)
{
if (Apc->ApcMode == KernelMode)
{
//Kernel-mode APC
ObfReferenceObject(g_DriverObject); //NormalRoutine will be called
return;
}
//User-mode APC -> free Apc object
_RundownRoutine(Apc);
}
VOID CALLBACK _RundownRoutine(PKAPC Apc)
{
ExFreePool(Apc);
}
使用特殊的提供組合語言程式碼實現:
請注意,我在彙編中編寫這些函式是為了能夠使用 JMP 指令安全地取消引用 KAPC 物件。在 此處 閱讀更多詳細資訊。
EXTERN g_DriverObject:QWORD
EXTERN __imp_ObfDereferenceObject:QWORD
EXTERN ?_RundownRoutine@NT@@YAXPEAU_KAPC@1@@Z : PROC
EXTERN ?_NormalRoutine@NT@@YAXPEAU_KAPC@1@PEAXPEAUDLL_INFORMATION@1@@Z : PROC
EXTERN ?_KernelRoutine@NT@@YAXPEAU_KAPC@1@PEAP6AXPEAX11@ZPEAPEAX33@Z : PROC
_TEXT SEGMENT
; VOID CALLBACK RundownRoutine(PKAPC );
?RundownRoutine@NT@@YAXPEAU_KAPC@1@@Z PROC
sub rsp,40
; void __cdecl NT::_RundownRoutine(struct NT::_KAPC *)
call ?_RundownRoutine@NT@@YAXPEAU_KAPC@1@@Z
add rsp,40
mov rcx,g_DriverObject
jmp __imp_ObfDereferenceObject
?RundownRoutine@NT@@YAXPEAU_KAPC@1@@Z ENDP
; VOID CALLBACK KernelRoutine(PKAPC , PKNORMAL_ROUTINE *, PVOID * , PVOID * ,PVOID * );
?KernelRoutine@NT@@YAXPEAU_KAPC@1@PEAP6AXPEAX11@ZPEAPEAX33@Z PROC
mov rax,[rsp + 40]
mov [rsp + 24],rax
mov rax,[rsp]
mov [rsp + 32],rax
push rax
; void __cdecl NT::_KernelRoutine(struct NT::_KAPC *,void (__cdecl **)(void *,void *,void *),void **,void **,void **)
call ?_KernelRoutine@NT@@YAXPEAU_KAPC@1@PEAP6AXPEAX11@ZPEAPEAX33@Z
pop rax
mov rax,[rsp + 32]
mov [rsp],rax
mov rcx,g_DriverObject
jmp __imp_ObfDereferenceObject
?KernelRoutine@NT@@YAXPEAU_KAPC@1@PEAP6AXPEAX11@ZPEAPEAX33@Z ENDP
; VOID CALLBACK NormalRoutine(PVOID , PVOID ,PVOID );
?NormalRoutine@NT@@YAXPEAX00@Z PROC
sub rsp,40
; void __cdecl NT::_NormalRoutine(struct NT::_KAPC *,void *,struct NT::DLL_INFORMATION *)
call ?_NormalRoutine@NT@@YAXPEAU_KAPC@1@PEAXPEAUDLL_INFORMATION@1@@Z
add rsp,40
mov rcx,g_DriverObject
jmp __imp_ObfDereferenceObject
?NormalRoutine@NT@@YAXPEAX00@Z ENDP
_TEXT ENDS
END
你在上面看到的讓人抓狂的外部變數是經過編譯器處理的
C++
函式名稱。您可以在編譯原始碼期間使用__FUNCDNAME__
前處理器命令獲取它們,方法是這樣放置:int SomeFunction(WCHAR* pstr, int value) { __pragma(message("extern " __FUNCDNAME__ " : PROC ; " __FUNCSIG__)) }
當該程式碼編譯時,Visual Studio 中的輸出視窗將包含所需的 C++ 被處理破壞的函式名稱:
extern ?SomeFunction@@YAHPEA_WH@Z : PROC ; int __cdecl SomeFunction(wchar_t *,int)
瞭解PsSetLoadImageNotifyRoutine
回撥是在呼叫將DLL
對映到記憶體的ZwMapViewOfSection
函式中執行的,這是十分重要的。此回撥發生在該函式完成設定DLL
之前,這意味著DLL
已對映但尚未初始化。例如,它的匯入函式尚未處理。所以換句話說,那個DLL
還不能用!
作為上述陳述的結果,如果您決定使用 PsSetLoadImageNotifyRoutine 函式將您自己的模組載入到所有其他模組中,則必須遵循一條經驗法則:您不能將任何其他 DLL 匯入到您的模組中,除了 ntdll.dll。該 DLL 和其他任何 DLL 都保證被對映到任何使用者模式程式。
ZwQueueApcThread 與 QueueUserAPC 孰優孰劣
讓我問一下,您會使用哪個函式?
QueueUserAPC
顯然已或多或少被文件化了,因此使用起來應該更安全,而ZwQueueApcThread
或NtQueueApcThread
則沒有。
對於使用者模式程式碼,ZwQueueApcThread 和 NtQueueApcThread 函式之間沒有區別。這只是你喜歡什麼字首的問題。
在繼續之前,讓我們檢查一下原生ZwQueueApcThread
函式是如何宣告的:
NTSTATUS ZwQueueApcThread(
HANDLE hThread,
PKNORMAL_ROUTINE ApcRoutine,
PVOID ApcContext,
PVOID Argument1,
PVOID Argument2
);
如你所見,我們有機會使用本機函式傳遞3
個自定義引數,而不是單個自定義引數或QueueUserAPC
中的dwData
。確實,對於原生函式來說,這稍微簡化了一些事情,但只要我們可以傳遞一個指標,我們就可以傳遞任意數量的引數。所以QueueUserAPC
沒什麼大不了的,對吧?
好吧,正如我們將在下面看到的,區別實際上在於QueueUserAPC
使用的啟用上下文。這不僅僅是差異,而且實際上是一個錯誤。
啟用上下文控制程式碼錯誤
QueueUserAPC
函式的文件中根本沒有提到使用者模式APC
處理啟用上下文的方式。相反,這裡只是簡單地涉及了一下:
非同步過程呼叫、完成埠回撥和其他執行緒上的任何其他回撥會自動獲取源的啟用上下文。
—— MSDN
您可以從QueueUserAPC
的實現中瞭解這意味著什麼。在我的Windows 10
上大致如此:
? 點選檢視程式碼 ?
typedef struct _ACTIVATION_CONTEXT_BASIC_INFORMATION {
HANDLE hActCtx;
DWORD dwFlags;
} ACTIVATION_CONTEXT_BASIC_INFORMATION, *PACTIVATION_CONTEXT_BASIC_INFORMATION;
DWORD QueueUserAPC(PAPCFUNC pfnAPC, HANDLE hThread, ULONG_PTR dwData)
{
ACTIVATION_CONTEXT_BASIC_INFORMATION ContextInfo = {};
NTSTATUS status = RtlQueryInformationActivationContext(
1, //RTL_QUERY_ACTIVATION_CONTEXT_FLAG_USE_ACTIVE_ACTIVATION_CONTEXT,
NULL,
NULL,
1, //ActivationContextBasicInformation,
&ContextInfo,
sizeof(ContextInfo),
NULL);
if(FAILED(status))
{
BaseSetLastNTError(status);
return FALSE;
}
status = ZwQueueApcThread(hThread, RtlDispatchAPC, pfnAPC, dwData,
!(ContextInfo.dwFlags & 1) ? ContextInfo.hActCtx : INVALID_HANDLE_VALUE);
if(FAILED(status))
{
BaseSetLastNTError(status);
return FALSE;
}
return TRUE;
}
typedef struct _RTL_ACTIVATION_CONTEXT_STACK_FRAME
{
PRTL_ACTIVATION_CONTEXT_STACK_FRAME Previous;
_ACTIVATION_CONTEXT * ActivationContext;
ULONG Flags;
} RTL_ACTIVATION_CONTEXT_STACK_FRAME, *PRTL_ACTIVATION_CONTEXT_STACK_FRAME;
typedef struct _RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED
{
SIZE_T Size;
ULONG Format;
RTL_ACTIVATION_CONTEXT_STACK_FRAME Frame;
PVOID Extra1;
PVOID Extra2;
PVOID Extra3;
PVOID Extra4;
} RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED,
*PRTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED;
void RtlDispatchAPC(PAPCFUNC pfnAPC, ULONG_PTR dwData, HANDLE hActCtx)
{
RTL_CALLER_ALLOCATED_ACTIVATION_CONTEXT_STACK_FRAME_EXTENDED ActEx = {};
ActEx.Size = sizeof(ActEx);
ActEx.Format = 1;
if(hActCtx != INVALID_HANDLE_VALUE)
{
RtlActivateActivationContextUnsafeFast(&ActEx, hActCtx);
pfnAPC(dwData);
RtlDeactivateActivationContextUnsafeFast(&ActEx);
RtlReleaseActivationContext(hActCtx);
}
else
pfnAPC(dwData);
}
如你所見,它們獲取當前啟用上下文(新增了對其的引用),然後呼叫ZwQueueApcThread
以使用指向ntdll!RtlDispatchAPC
的回撥函式對APC
進行排隊。在其中,它們傳遞由使用者指定的原始回撥函式,以及用於呼叫QueueUserAPC
的使用者提供的引數,最後是啟用上下文的控制程式碼。
順便說一下,這是 QueueUserAPC 中所有 3 個引數都用完的地方。 所以使用者在可用的 3 個引數中只剩下 1 個引數。
在APC
回撥中,ntdll!RtlDispatchAPC
實現啟用上下文,使用引數呼叫使用者提供的回撥,然後停用並釋放它。
需要注意的重要一點以及錯誤所在的地方是啟用上下文“控制程式碼”並不是真正的控制程式碼。它只是指向某些內部資料結構的指標。如果我們對RtlReleaseActivationContext
函式中的程式碼進行逆向工程,就更容易理解了:
; RtlReleaseActivationContext function
; rcx = activation context handle
test rcx, rcx
jnz @@1
retn
@@1:
mov [rsp+0x8], rbx
push rdi
sub rsp, 20h
lea rax, [rcx-1]
mov rbx, rcx
or rax, 7
cmp rax, 0FFFFFFFFFFFFFFFFh
jz @@exit
mov eax, [rcx] ; potential crash
mov ecx, 1
sub eax, ecx
cmp eax, 7FFFFFFDh
ja @@exit
mov eax, [rbx]
lea edi, [rax-1]
lock cmpxchg [rbx], edi ; potential overwrite of memory
; ....
如你所見,RtlReleaseActivationContext
只需要一個輸入引數,即啟用上下文控制程式碼,它在rcx
暫存器中傳遞。但是稍後在彙編程式碼中使用了它。此函式快速檢查它是否為0,如果是則退出。然後除了低3位,它對控制程式碼位是否全為1進行再一次基本檢查。如果是,它也退出。
但這留下了絕大多數非零啟用上下文“控制程式碼”值被允許傳遞給mov eax, [rcx]
指令,該指令僅將其視為記憶體中的地址。此外,lock cmpxchg [rbx], edi
指令可以稍後開始寫入該地址。
真正的控制程式碼是對字典的索引或核心記憶體中控制程式碼表中物件的對映。它不應該僅用作指標,特別是如果這樣的控制程式碼可以在程式之間傳遞!
當在同一程式中使用時,啟用上下文“控制程式碼”的這種處理不會造成問題。但是,如果我們使用QueueUserAPC
在另一個程式中對APC
進行排隊呢?那麼他們對“控制程式碼”/指標的使用僅意味著:
但這樣的崩潰不會是最糟糕的事情。考慮啟用上下文“控制程式碼”是否指向目標程式中的有效記憶體。那時會發生什麼?例如,RtlReleaseActivationContext
將覆蓋該程式中的一些可寫記憶體,這不僅會導致未定義的行為(UB,undefined behavior
),而且之後也很難診斷和除錯。
那麼為什麼這個錯誤沒有引起很多騷動呢?啟用上下文畢竟不是一個新概念。
原因是通常不存在程式的啟用上下文。因此,使用ActivationContextBasicInformation
呼叫RtlQueryInformationActivationContext
或其記錄的等效GetCurrentActCtx
將返回NULL
作為啟用上下文“控制程式碼”。Microsoft
的回撥函式可以優雅地處理NULL
。
當模組具有啟用上下文時,問題就會發生。例如,在DllMain
中,如果模組本身具有帶有ISOLATIONAWARE_MANIFEST_RESOURCE_ID
識別符號的清單。但這非常罕見,因此,我猜,這個問題無人關注。
嚴謹的 APC 文件
讓我們檢視有關我在此處解釋的啟用上下文“控制程式碼”錯誤的MSDN
文件:
注意:出於多種原因,不建議將 APC 排隊到呼叫者程式之外的執行緒。 ...
—— QueueUserAPC 函式
?真的嗎?那是因為你有一個實現錯誤。那麼為什麼不直接寫,啟用上下文“控制程式碼”不能在另一個程式中使用呢?或者更好的是,它可能會導致崩潰、未定義的行為和損壞的記憶體。
但理想情況下,QueueUserAPC
函式應該有一個單獨的引數,或者可能是一個新函式QueueUserAPCEx
,它應該告訴它是否完全使用啟用上下文。而且,他們還應該在技術上修改QueueUserAPC
的當前實現,如果hThread
輸入控制程式碼指向不同程式中的執行緒,則在內部將啟用上下文的NULL
傳遞給APC
回撥函式。
然後是這個:
...類似地,如果 64 位程式將 APC 排隊到 32 位程式,反之亦然,地址將不正確,應用程式將崩潰。
—— QueueUserAPC 函式
同樣,他們並沒有說出全部真相。
你不能將32
位APC
回撥排隊到64
位程式中。但是您可以將64
位APC
回撥排隊到32
位程式中。為此,需要使用另一個鮮為人知且未記錄的本機函式RtlQueueApcWow64Thread
而不是ZwQueueApcThread
,它在32
位WOW64
程式中將64
位APC
回撥排隊:
NTSTATUS RtlQueueApcWow64Thread (
HANDLE hThread,
PKNORMAL_ROUTINE ApcRoutine,
PVOID ApcContext,
PVOID Argument1,
PVOID Argument2
);
或者,從核心模式而不是呼叫需要呼叫PsWrapApcWow64Thread
函式的KeInsertQueueApc
:
NTSTATUS PsWrapApcWow64Thread (
_Inout_ PVOID *ApcContext,
_Inout_ PVOID *ApcRoutine
);
但是為什麼有人需要將64
位APC
排隊到32
位程式中呢?我們稍後會回來再研究它。
使用者 APC 演示程式碼
為了說明我上面解釋的使用者模式APC
的概念和缺陷,我們編寫了一個小示例程式碼:
? 點選檢視程式碼 ?
{
//Check that we don't have an activation context yet
QueryCtx();
//Set our activation context for this process
ULONG_PTR dwCookie;
if (ActivateActCtx(hActCtx, &dwCookie))
{
//Check that we have an activation context now
QueryCtx();
//Queue APC in this process on this thread
QueueUserAPC(OnApc, GetCurrentThread(), 0);
//Make APC callback execute now
ZwTestAlert(); //same as: SleepEx(0, TRUE);
//Queue APC in a remote process (using native API)
//It will succeed
TestAPC_InRemoteProcess(true);
//Queue APC in a remote process (using Win32 API)
//It will crash the remote process!
TestAPC_InRemoteProcess(false);
DeactivateActCtx(0, dwCookie);
}
ReleaseActCtx(hActCtx);
}
return 0;
}
void TestAPC_InRemoteProcess(bool bUseNativeApi)
{
//Invoke a user-mode APC callback in a remote process
//Get path to cmd.exe
WCHAR appname[MAX_PATH];
if (GetEnvironmentVariableW(L"comspec", appname, _countof(appname)))
{
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(si) };
//Run cmd.exe suspended
if (CreateProcessW(appname, 0, 0, 0, 0, CREATE_SUSPENDED, 0, 0, &si, &pi))
{
//Invoke APC in cmd.exe, using either a native or documented Win32 function
//We don't care about the callback function itself, for as long as it can
//handle our input parameters. Thus I will use LPVOID TlsGetValue(DWORD)
bUseNativeApi
? ZwQueueApcThread(pi.hThread, (PKNORMAL_ROUTINE)TlsGetValue, 0, 0, 0)
: QueueUserAPC((PAPCFUNC)TlsGetValue, pi.hThread, 0);
//Resume thread to let APC execute
ResumeThread(pi.hThread);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
}
}
void QueryCtx()
{
//Query activation context in this process and output it into (debugger) console
SIZE_T cb = 0;
ACTIVATION_CONTEXT_RUN_LEVEL_INFORMATION acrli;
union {
PVOID buf;
PACTIVATION_CONTEXT_ASSEMBLY_DETAILED_INFORMATION pacadi;
};
buf = 0;
ACTIVATION_CONTEXT_QUERY_INDEX QueryIndex = { 1, 0 };
__again:
switch (QueryActCtxW(QUERY_ACTCTX_FLAG_USE_ACTIVE_ACTCTX, 0, &QueryIndex,
AssemblyDetailedInformationInActivationContext, buf, cb, &cb) ? NOERROR : GetLastError())
{
case ERROR_INSUFFICIENT_BUFFER:
buf = alloca(cb);
goto __again;
break;
case NOERROR:
if (buf)
{
DbgPrint("==========\nPID=%u: %S\n%S\n",
GetCurrentProcessId(),
pacadi->lpAssemblyManifestPath,
pacadi->lpAssemblyEncodedAssemblyIdentity);
}
break;
}
if (QueryActCtxW(QUERY_ACTCTX_FLAG_USE_ACTIVE_ACTCTX, 0, 0,
RunlevelInformationInActivationContext, &acrli, sizeof(acrli), &cb))
{
DbgPrint("PID=%u: RunLevel = %x\n", GetCurrentProcessId(), acrli.RunLevel);
}
}
VOID NTAPI OnApc(
_In_ ULONG_PTR /*Parameter*/
)
{
//User-mode APC callback
QueryCtx();
}
要在沒有WDK
的情況下在Visual Studio
中編譯此程式碼示例,您將需要以下宣告:
#pragma comment(lib, "ntdll.lib") //For native function calls
typedef
VOID
KNORMAL_ROUTINE(
__in_opt PVOID NormalContext,
__in_opt PVOID SystemArgument1,
__in_opt PVOID SystemArgument2
);
typedef KNORMAL_ROUTINE* PKNORMAL_ROUTINE;
extern "C" {
__declspec(dllimport) NTSTATUS CALLBACK ZwQueueApcThread(HANDLE hThread,
PKNORMAL_ROUTINE ApcRoutine,
PVOID ApcContext,
PVOID Argument1,
PVOID Argument2);
__declspec(dllimport) NTSTATUS CALLBACK ZwTestAlert();
__declspec(dllimport) ULONG CALLBACK
DbgPrint(
_In_z_ _Printf_format_string_ PCSTR Format,
...
);
}
32 位程式中的 64 位使用者 APC
將64
位使用者模式APC
排隊到32
位程式中的一個原因是將DLL
注入其中。但這不是唯一的用途。
比如說,如果您需要知道載入到程式中的模組列表怎麼辦?
為您自己的流程執行此操作的一種方法是呼叫未記錄的LdrQueryProcessModuleInformation
函式。它將在提供的記憶體緩衝區中寫入完整列表:
NTSTATUS LdrQueryProcessModuleInformation
(
PRTL_PROCESS_MODULES psmi,
ULONG BufferSize,
PULONG RealSize
);
但是,對於遠端程式中的模組,您如何呼叫它?這也可能具有不同的位數。
讓我給你一些步驟:
-
我們需要建立一個部分(
NtCreateSection
),我們將使用它來收集和傳遞有關目標程式中的模組的資訊(在Win32
用語中,它稱為檔案對映物件。) -
將該部分對映到目標程式(
ZwMapViewOfSection
)中進行寫入。 -
在目標程式中建立掛起狀態的執行緒,並將其入口點的地址設定為
RtlExitUserThread
。我們並不真正需要執行緒函式本身,因此我們將分流它以儘快退出。在這種情況下,重要的是使用本機函式 RtlCreateUserThread 來啟動執行緒,而不是文件中的 CreateRemoteThread。這需要確保我們可以控制執行緒入口點的位數。這是在 CreateRemoteThread 不允許的,因為它使用的實際入口點是 kernel32!BaseThreadInitThunk 而不是我們在其 lpStartAddress 引數中提供給它的函式。
要定義執行緒將在哪個上下文中啟動:
64
位或32
位,系統將使用執行緒入口點所在的模組的位數。(或者如果沒有模組,就像在純shellcode
中一樣,預設情況下執行緒將接收32
位上下文。)請注意,可以在 64 位作業系統中的 32 位(所謂的 WOW64)程式中執行 64 位執行緒。同時也有一個 64 位版本的 ntdll.dll 模組對映到每個 32 位 WOW64 程式。
-
在我們掛起的執行緒中插入一個使用者模式
APC
。回撥的位數將取決於目標程式的位數:
64
位程式:我們只需要ZwQueueApcThread
函式就可以對64
位APC
回撥進行原生排隊。這裡很簡單。
32
位程式:首先使用ZwQueueApcThread
對64
位回撥進行排隊,以檢索所有對映的64
位模組。(正如我上面所說,任何32
位WOW64
程式都將至少載入一個64
位模組。)然後使用RtlQueueApcWow64Thread
將32
位APC
回撥排隊。
我們將使用LdrQueryProcessModuleInformation
函式作為適當位數的APC
的回撥。對我們來說非常方便,它有3
個輸入引數,與ZwQueueApcThread
和RtlQueueApcWow64Thread
函式的自定義引數相匹配。這也是我們選擇那些原生函式而不是已經被文件化的QueueUserAPC
的另一個原因。 -
恢復執行緒,它將在目標程式中執行我們排隊的
APC
。由於我們將其回撥設定為LdrQueryProcessModuleInformation
,因此該函式將使用有關目標程式中模組的所需資訊填充對映部分的記憶體。 -
執行緒本身將執行將終止它的
RtlExitUserThread
函式。(與Create[Remote]Thread
不同,它將線上程返回時將控制權傳遞給內部包裝函式) -
在我們自己的程式中,我們只是等待遠端執行緒完成執行。
-
然後我們可以從目標程式中取消對映該部分,並將其對映到我們自己的程式中並讀取我們收集的模組資訊。
-
銷燬該部分並進行其他清理。
在較舊的(32 位)Microsoft Word
程式上執行上述演算法後,我們可以獲得其載入模組的列表:
獲取程式模組的程式碼示例
為了更好地說明此處概述的概念,讓我給您以下程式碼示例,它將檢索對映到任意程式的模組:
注意:下面是一個未優化的程式碼,旨在提高讀者的可讀性。 我們使用 goto 語句對其進行格式化只是為了防止需要水平滾動。 請參閱評論以獲取更多詳細資訊。
NTSTATUS ListModulesForProc(DWORD dwPID)
{
//'dwPID' = process ID of the process to retrieve modules for
NTSTATUS status = S_FALSE;
HANDLE hProcess = NULL;
LARGE_INTEGER liSectionSize = {};
SIZE_T ViewSize = 0;
NTDLL_FN_PTRS nfp = {};
ULONG_PTR wow = 0;
#ifndef _WIN64
#error Must be compiled as x64 only!
#endif
hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION, FALSE, dwPID);
if (!hProcess)
{
status = GetLastError();
goto cleanup;
}
//Collect 64-bit modules
nfp.pRtlExitUserThread.pstrName = "RtlExitUserThread";
nfp.pRtlExitUserThread.pfn = (FARPROC)RtlExitUserThread;
nfp.pLdrQueryProcessModuleInformation.pstrName = "LdrQueryProcessModuleInformation";
nfp.pLdrQueryProcessModuleInformation.pfn = (FARPROC)LdrQueryProcessModuleInformation;
status = CollectModules(hProcess, TRUE, &nfp);
if (FAILED(status))
goto cleanup;
//Get process bitness
status = NtQueryInformationProcess(hProcess, ProcessWow64Information, &wow, sizeof(wow), NULL);
if (FAILED(status))
goto cleanup;
if (wow)
{
//Collect 32-bit modules
status = ResolveNtDllFuncs32bit(&nfp);
if (FAILED(status))
goto cleanup;
status = CollectModules(hProcess, FALSE, &nfp);
if (FAILED(status))
goto cleanup;
}
else
status = STATUS_SUCCESS;
cleanup:
//Clean-up process
if(hProcess)
CloseHandle(hProcess);
assert(SUCCEEDED(status));
return status;
}
將APC
注入目標程式的實際功能在以下函式中實現:
? 點選檢視程式碼 ?
NTSTATUS CollectModules(HANDLE hProcess, BOOL b64bit, NTDLL_FN_PTRS* pfnPtrs)
{
//INFO: It is not the most efficient way of calling this function twice with
// repeated creation of the section and then mapping it into a process.
// Ideally, you'd create it once and then close and re-create it ONLY if its
// original size is too small to fit all the modules.
//
// But, I will leave this code as-is for brevity, as such optimization
// has nothing to do with the APC concepts that we discuss in this blog post.
NTSTATUS status;
HANDLE hThread = NULL;
BYTE* pThisBaseAddr = NULL;
SIZE_T ViewSize = 0;
ULONG uiRealSize = 0;
PRTL_PROCESS_MODULES pRPMs = NULL;
PRTL_PROCESS_MODULES32 pRPMs32 = NULL;
HANDLE hSection = NULL;
LARGE_INTEGER liSectionSize = {};
PVOID pBaseAddr = NULL;
ULONG szBufferSz = 0;
bool bExportSuppression = false;
bool bDone = false;
typedef NTSTATUS(CALLBACK PFN_PTR)(HANDLE hThread,
PKNORMAL_ROUTINE ApcRoutine,
PVOID ApcContext,
PVOID Argument1,
PVOID Argument2);
PFN_PTR* pQueueAPC;
assert(pfnPtrs);
assert(pfnPtrs->pLdrQueryProcessModuleInformation.pfn);
assert(pfnPtrs->pRtlExitUserThread.pfn);
//Assume 8 memory pages as the original section size
SYSTEM_INFO si = {};
GetSystemInfo(&si);
szBufferSz = si.dwPageSize * 8;
assert(szBufferSz);
//See if export suppression is enabled in Control Flow Guard (CFG) for the target process
//INFO: If so, we need to enable our thread's EP function and APC callback for CFG,
// since calling them otherwise will crash the target process as a security measure!
status = IsExportSuppressionEnabled(hProcess, &bExportSuppression);
if (FAILED(status))
goto cleanup;
if (bExportSuppression)
{
//Enable our function pointers for CFG in the process
status = SetValidExport(hProcess, pfnPtrs->pRtlExitUserThread.pfn);
if (FAILED(status))
goto cleanup;
status = SetValidExport(hProcess, pfnPtrs->pLdrQueryProcessModuleInformation.pfn);
if (FAILED(status))
goto cleanup;
}
while (!bDone)
{
bDone = true;
liSectionSize.QuadPart = szBufferSz;
//Create section
assert(!hSection);
status = NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &liSectionSize, PAGE_READWRITE, SEC_COMMIT, 0);
if (FAILED(status))
goto cleanup;
assert(!pBaseAddr);
pBaseAddr = NULL;
ViewSize = 0;
//Map section into target process for writing
status = ZwMapViewOfSection(hSection, hProcess, &pBaseAddr, 0, 0, NULL, &ViewSize, ViewShare, 0, PAGE_READWRITE);
if (FAILED(status))
goto cleanup;
//Create remote thread in the target process (and shunt it to RtlExitUserThread)
//Ensure that the thread is created suspended!
assert(!hThread);
status = RtlCreateUserThread(hProcess, NULL, TRUE, 0, 0, 0, pfnPtrs->pRtlExitUserThread.pfn, NULL, &hThread, NULL);
if (FAILED(status))
goto cleanup;
//(Optional call)
//INFO: Notifications about creation and termination of this thread will not be passed to an attached debugger.
// And, exceptions in such thread will not be passed to a debugger either.
NtSetInformationThread(hThread, ThreadHideFromDebugger, 0, 0);
//Pick which APC function to use (depending on the bitness)
pQueueAPC = b64bit ? ZwQueueApcThread : RtlQueueApcWow64Thread;
//We'll reserve last ULONG in our buffer for LdrQueryProcessModuleInformation to return its RequiredSize
status = pQueueAPC(hThread,
(PKNORMAL_ROUTINE)pfnPtrs->pLdrQueryProcessModuleInformation.pfn,
pBaseAddr,
(PVOID)(szBufferSz - sizeof(ULONG)),
(BYTE*)pBaseAddr + szBufferSz - sizeof(ULONG));
if (FAILED(status))
goto cleanup;
//Let our APC callback and the thread itself run
if (ResumeThread(hThread) != 1)
{
status = GetLastError();
goto cleanup;
}
//Wait for the thread to finish
if (WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0)
{
status = GetLastError();
goto cleanup;
}
//Unmap the section from the target process
status = ZwUnmapViewOfSection(hProcess, pBaseAddr);
if (FAILED(status))
goto cleanup;
pBaseAddr = NULL;
assert(!pThisBaseAddr);
pThisBaseAddr = NULL;
ViewSize = 0;
//Map the same section into our own process so that we can read it
status = ZwMapViewOfSection(hSection, GetCurrentProcess(),
(PVOID*)&pThisBaseAddr, 0, 0, NULL, &ViewSize, ViewShare, 0, PAGE_READONLY);
if (FAILED(status))
goto cleanup;
assert(ViewSize <= szBufferSz);
//Check if the size of the section that we assumed earlier was enough to fill in all modules
uiRealSize = *(ULONG*)(pThisBaseAddr + szBufferSz - sizeof(ULONG));
if (uiRealSize <= szBufferSz)
{
//Unfortunately we cannot check the return value from the LdrQueryProcessModuleInformation() call. Here's why:
//The LdrQueryProcessModuleInformation() function is called from an APC callback, and by the time
//our remote thread gets to calling RtlExitUserThread() its context will be restored by a call to ntdll!NtContinue()
if (b64bit)
{
//64-bit modules
pRPMs = (PRTL_PROCESS_MODULES)pThisBaseAddr;
ULONG nNumberOfModules = pRPMs->NumberOfModules;
//Check that we have at least one module loaded, otherwise it's an error
if (!nNumberOfModules)
{
status = STATUS_PROCEDURE_NOT_FOUND;
goto cleanup;
}
//Output results to the console
wprintf(L"64-bit Modules (%u):\n", nNumberOfModules);
RTL_PROCESS_MODULE_INFORMATION* pPMI = pRPMs->Modules;
do
{
printf("%p sz=%08X flg=%08X Ord=%02X %s\n"
,
pPMI->ImageBase,
pPMI->ImageSize,
pPMI->Flags,
pPMI->InitOrderIndex,
pPMI->FullPathName
);
}
while (pPMI++, --nNumberOfModules);
}
else
{
//32-bit modules
pRPMs32 = (PRTL_PROCESS_MODULES32)pThisBaseAddr;
ULONG nNumberOfModules = pRPMs32->NumberOfModules;
//Check that we have at least one module loaded, otherwise it's an error
if (!nNumberOfModules)
{
status = STATUS_PROCEDURE_NOT_FOUND;
goto cleanup;
}
//Output results to the console
wprintf(L"32-bit Modules (%u):\n", nNumberOfModules);
RTL_PROCESS_MODULE_INFORMATION* pPMI32 = pRPMs32->Modules;
do
{
printf("%08X sz=%08X flg=%08X Ord=%02X %s\n"
,
pPMI32->ImageBase,
pPMI32->ImageSize,
pPMI32->Flags,
pPMI32->InitOrderIndex,
pPMI32->FullPathName
);
}
while (pPMI32++, --nNumberOfModules);
}
status = STATUS_SUCCESS;
}
else
{
//Need more memory - allocate it on a page boundary
if (uiRealSize % si.dwPageSize)
{
szBufferSz = uiRealSize / si.dwPageSize;
szBufferSz++;
szBufferSz *= si.dwPageSize;
}
else
szBufferSz = uiRealSize;
//Retry
bDone = false;
}
cleanup:
//Clean-up
if (pBaseAddr)
{
ZwUnmapViewOfSection(GetCurrentProcess(), pBaseAddr);
pBaseAddr = NULL;
}
if (pThisBaseAddr)
{
ZwUnmapViewOfSection(GetCurrentProcess(), pThisBaseAddr);
pThisBaseAddr = NULL;
}
if (hSection)
{
ZwClose(hSection);
hSection = NULL;
}
if (hThread)
{
ZwClose(hThread);
hThread = NULL;
}
}
return status;
}
您可能已經注意到上面的函式呼叫帶有 ThreadHideFromDebugger 標誌的 NtSetInformationThread。這是一個可選呼叫,偵錯程式程式可以使用它來確保注入到目標程式中的自己的執行緒不會引起通知,例如執行緒建立、終止等。通常這些通知被傳遞給偵錯程式,即附加到被除錯程式。通過使用 ThreadHideFromDebugger 偵錯程式可以防止這種情況。
此外,通過為執行緒指定 ThreadHideFromDebugger,其中的所有異常也不會傳遞給附加的偵錯程式。
其他重要函式解析對映的ntdll!LdrQueryProcessModuleInformation
和ntdll!RtlExitUserThread
本機函式的32
位匯出指標,我們需要將APC
回撥注入32
位WOW64
程式:
? 點選檢視程式碼 ?
NTSTATUS ResolveNtDllFuncs32bit(NTDLL_FN_PTRS* pfnPtrs)
{
NTSTATUS status;
HANDLE hSection;
SECTION_IMAGE_INFORMATION sii;
PVOID pBaseAddr = NULL;
SIZE_T ViewSize = 0;
//We'll need the special 32-bit image section for ntdll.dll
static const WCHAR oa_ntdll_str[] = L"\\KnownDlls32\\ntdll.dll";
static const UNICODE_STRING oa_ntdll_ustr = { sizeof(oa_ntdll_str) - sizeof((oa_ntdll_str)[0]), sizeof(oa_ntdll_str), const_cast<PWSTR>(oa_ntdll_str) };
static OBJECT_ATTRIBUTES oa_ntdll = { sizeof(oa_ntdll), 0, const_cast<PUNICODE_STRING>(&oa_ntdll_ustr), OBJ_CASE_INSENSITIVE };
pfnPtrs->pLdrQueryProcessModuleInformation.pfn = NULL;
pfnPtrs->pRtlExitUserThread.pfn = NULL;
status = ZwOpenSection(&hSection, SECTION_QUERY | SECTION_MAP_READ, &oa_ntdll);
if (FAILED(status))
goto cleanup;
status = ZwQuerySection(hSection, SectionImageInformation, &sii, sizeof(sii), 0);
if (FAILED(status))
goto cleanup;
status = ZwMapViewOfSection(hSection, GetCurrentProcess(), &pBaseAddr, 0, 0, 0, &ViewSize, ViewUnmap, 0, PAGE_READONLY);
if (FAILED(status))
goto cleanup;
__try
{
//We will have to parse PE structure manually
//(Remember, the image section here is of a different bitness than our own process!)
if (PIMAGE_NT_HEADERS32 pinth = (PIMAGE_NT_HEADERS32)RtlImageNtHeader(pBaseAddr))
{
//We'll do a really quick-and-dirty parsing here ...
status = ResolveModuleExports((PBYTE)sii.TransferAddress - pinth->OptionalHeader.AddressOfEntryPoint,
pBaseAddr, (EXPORT_ENTRY *)pfnPtrs, 2);
}
else
status = STATUS_BAD_FILE_TYPE;
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
//Catch exceptions in case the section is not a valid PE file
status = STATUS_BAD_DATA;
}
cleanup:
//Clean-up
if (pBaseAddr)
ZwUnmapViewOfSection(GetCurrentProcess(), pBaseAddr);
if(hSection)
ZwClose(hSection);
return status;
}
NTSTATUS ResolveModuleExports(PVOID ImageBase, PVOID pBaseAddr, EXPORT_ENTRY* pfnExports, int nCntExports)
{
//Resolve exported functions by their names provided in 'pfnExports', using the image section mapped in memory
NTSTATUS status;
ULONG exportSize, exportRVA;
ULONG NumberOfFunctions;
ULONG NumberOfNames;
ULONG OrdinalBase;
PULONG AddressOfFunctions;
PULONG AddressOfNames;
PWORD AddressOfNameOrdinals;
PIMAGE_EXPORT_DIRECTORY pied = (PIMAGE_EXPORT_DIRECTORY)
RtlImageDirectoryEntryToData(pBaseAddr, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &exportSize);
if (!pied)
{
status = STATUS_INVALID_IMAGE_FORMAT;
goto cleanup;
}
exportRVA = RtlPointerToOffset(pBaseAddr, pied);
NumberOfFunctions = pied->NumberOfFunctions;
if (!NumberOfFunctions)
{
status = STATUS_SOURCE_ELEMENT_EMPTY;
goto cleanup;
}
NumberOfNames = pied->NumberOfNames;
OrdinalBase = pied->Base;
AddressOfFunctions = (PULONG)RtlOffsetToPointer(pBaseAddr, pied->AddressOfFunctions);
AddressOfNames = (PULONG)RtlOffsetToPointer(pBaseAddr, pied->AddressOfNames);
AddressOfNameOrdinals = (PWORD)RtlOffsetToPointer(pBaseAddr, pied->AddressOfNameOrdinals);
status = STATUS_SUCCESS;
for (EXPORT_ENTRY* pEnd = pfnExports + nCntExports; pfnExports < pEnd; pfnExports++)
{
ULONG i;
PCSTR Name = pfnExports->pstrName;
assert(*Name != '#'); //Can't process ordinals
//Match each export by name
i = GetNameOrdinal(pBaseAddr, AddressOfNames, NumberOfNames, Name);
if (i == UINT_MAX)
{
status = STATUS_OBJECT_NAME_NOT_FOUND;
break;
}
if (i < NumberOfNames)
i = AddressOfNameOrdinals[i];
if (i >= NumberOfFunctions)
{
status = STATUS_FOUND_OUT_OF_SCOPE;
break;
}
DWORD Rva = AddressOfFunctions[i];
if ((ULONG_PTR)Rva - (ULONG_PTR)exportRVA >= exportSize)
{
(FARPROC&)pfnExports->pfn = (FARPROC)RtlOffsetToPointer(ImageBase, Rva);
}
else
{
//For brevity, we won't handle forwarded function exports ...
//(This has nothing to do with the subject of this blog post.)
status = STATUS_ILLEGAL_FUNCTION;
break;
}
}
cleanup:
//Clean-up process
return status;
}
ULONG GetNameOrdinal(PVOID pBaseAddr, PDWORD AddressOfNames, DWORD NumberOfNames, PCSTR Name)
{
//Resolve ordinal index by a function name
//RETURN:
// Such index, or
// UINT_MAX if error
if (NumberOfNames)
{
DWORD a = 0;
do
{
int u = (a + NumberOfNames) >> 1;
PCSTR pNm = RtlOffsetToPointer(pBaseAddr, AddressOfNames[u]);
int i = strcmp(pNm, Name);
if (!i)
{
return u;
}
0 > i ? a = u + 1 : NumberOfNames = u;
} while (a < NumberOfNames);
}
//Name was not found
return UINT_MAX;
}
我們還需要考慮其他可能干擾我們上述方法的因素。 這在技術上與 APC 的主題無關,因此我將非常簡要地討論它。
我說的是控制流保護(CFG,Control Flow Guard)。如果它為目標程式啟用,並且它具有匯出抑制的功能之一,這將阻止我們的 APC 程式碼注入通過。也就是說,如果我們的 APC 回撥和遠端執行緒入口點不在 CFG 點陣圖中,則目標程式將被 CFG 強制崩潰。這是一個很好的安全措施,但對我們的目的不是很好。
不過,對於我們的用例,我們需要繞過 CFG。對我們來說幸運的是,這很容易做到。我們只需要在需要的匯出函式上呼叫 SetProcessValidCallTargets 函式來禁用它。這就是下面的程式碼為我們完成的。
下面的第一個函式(IsExportSuppressionEnabled
) 確定是否啟用了帶有匯出抑制的CFG
。第二個函式(SetValidExport
)在目標程式中為我們的匯出禁用匯出抑制:
為了完整性,當我們的主函式退出時啟用這些匯出也是謹慎的。這是微不足道的,因此我們不會在這裡詳述。
請注意,以下函式在某種意義上構成了競爭條件,即在我們禁用它們之後,某些其他執行緒甚至程式可能會在我們的匯出上啟用 CFG。
? 點選檢視程式碼 ?
NTSTATUS IsExportSuppressionEnabled(HANDLE hProcess, bool* enabled)
{
//Checks if CFG with export suppression is enabled for 'hProcess' and returns it in 'enabled'
//The 'hProcess' handle must be opened with the PROCESS_QUERY_INFORMATION permission flag
struct PROCESS_MITIGATION {
PROCESS_MITIGATION_POLICY Policy;
ULONG Flags;
};
bool bEnabled = false;
PROCESS_MITIGATION m = { ProcessControlFlowGuardPolicy };
NTSTATUS status = NtQueryInformationProcess(hProcess, ProcessMitigationPolicy, &m, sizeof(m), 0);
if (SUCCEEDED(status))
{
PROCESS_MITIGATION_CONTROL_FLOW_GUARD_POLICY* pCFG = (PROCESS_MITIGATION_CONTROL_FLOW_GUARD_POLICY*)&m.Flags;
bEnabled = pCFG->EnableControlFlowGuard &&
pCFG->EnableExportSuppression;
}
if(enabled)
*enabled = bEnabled;
return status;
}
#pragma comment(lib, "mincore.lib")
NTSTATUS SetValidExport(HANDLE hProcess, LPCVOID pv)
{
//Disables CFG export-suppression on 'pv' function in 'hProcess'
MEMORY_BASIC_INFORMATION mbi;
NTSTATUS status = NtQueryVirtualMemory(hProcess, (void*)pv, MemoryBasicInformation, &mbi, sizeof(mbi), 0);
if (SUCCEEDED(status))
{
if (mbi.State != MEM_COMMIT || mbi.Type != MEM_IMAGE)
{
return STATUS_INVALID_ADDRESS;
}
CFG_CALL_TARGET_INFO OffsetInformation = {
(ULONG_PTR)pv - (ULONG_PTR)mbi.BaseAddress,
CFG_CALL_TARGET_CONVERT_EXPORT_SUPPRESSED_TO_VALID | CFG_CALL_TARGET_VALID
};
return SetProcessValidCallTargets(hProcess, mbi.BaseAddress, mbi.RegionSize, 1, &OffsetInformation) &&
(OffsetInformation.Flags & CFG_CALL_TARGET_PROCESSED) ? STATUS_SUCCESS : STATUS_STRICT_CFG_VIOLATION;
}
return status;
}
最後,要在VisualStudio
中編譯上述程式碼,理想情況下需要安裝WDK
。或者,您可以使用以下宣告在沒有WDK
的情況下對其進行編譯:
? 點選檢視程式碼 ?
#include <iostream>
#include Windows.h>
#include <assert.h>
#pragma comment(lib, "ntdll.lib") //For native API calls
struct EXPORT_ENTRY {
FARPROC pfn;
PCSTR pstrName;
};
struct NTDLL_FN_PTRS {
EXPORT_ENTRY pLdrQueryProcessModuleInformation;
EXPORT_ENTRY pRtlExitUserThread;
};
typedef
VOID
KNORMAL_ROUTINE(
__in_opt PVOID NormalContext,
__in_opt PVOID SystemArgument1,
__in_opt PVOID SystemArgument2
);
typedef KNORMAL_ROUTINE* PKNORMAL_ROUTINE;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer;
} UNICODE_STRING;
typedef UNICODE_STRING* PUNICODE_STRING;
typedef const UNICODE_STRING* PCUNICODE_STRING;
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor; // Points to type SECURITY_DESCRIPTOR
PVOID SecurityQualityOfService; // Points to type SECURITY_QUALITY_OF_SERVICE
} OBJECT_ATTRIBUTES;
typedef OBJECT_ATTRIBUTES* POBJECT_ATTRIBUTES;
typedef CONST OBJECT_ATTRIBUTES* PCOBJECT_ATTRIBUTES;
typedef enum _SECTION_INHERIT {
ViewShare = 1,
ViewUnmap = 2
} SECTION_INHERIT;
typedef struct _CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID;
typedef CLIENT_ID* PCLIENT_ID;
typedef struct RTL_PROCESS_MODULE_INFORMATION {
HANDLE Section; // Not filled in
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
CHAR FullPathName[256];
} *PRTL_PROCESS_MODULE_INFORMATION;
typedef struct RTL_PROCESS_MODULES {
ULONG NumberOfModules;
RTL_PROCESS_MODULE_INFORMATION Modules[1];
} *PRTL_PROCESS_MODULES;
typedef int HANDLE32;
typedef int PVOID32;
#pragma pack(push)
#pragma pack(4)
typedef struct RTL_PROCESS_MODULE_INFORMATION32 {
HANDLE32 Section; // Not filled in
PVOID32 MappedBase;
PVOID32 ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
CHAR FullPathName[256];
} *PRTL_PROCESS_MODULE_INFORMATION32;
typedef struct RTL_PROCESS_MODULES32 {
ULONG NumberOfModules;
RTL_PROCESS_MODULE_INFORMATION32 Modules[1];
} *PRTL_PROCESS_MODULES32;
#pragma pack(pop)
typedef enum _PROCESSINFOCLASS {
ProcessBasicInformation = 0,
ProcessQuotaLimits = 1,
ProcessIoCounters = 2,
ProcessVmCounters = 3,
ProcessTimes = 4,
ProcessBasePriority = 5,
ProcessRaisePriority = 6,
ProcessDebugPort = 7,
ProcessExceptionPort = 8,
ProcessAccessToken = 9,
ProcessLdtInformation = 10,
ProcessLdtSize = 11,
ProcessDefaultHardErrorMode = 12,
ProcessIoPortHandlers = 13, // Note: this is kernel mode only
ProcessPooledUsageAndLimits = 14,
ProcessWorkingSetWatch = 15,
ProcessUserModeIOPL = 16,
ProcessEnableAlignmentFaultFixup = 17,
ProcessPriorityClass = 18,
ProcessWx86Information = 19,
ProcessHandleCount = 20,
ProcessAffinityMask = 21,
ProcessPriorityBoost = 22,
ProcessDeviceMap = 23,
ProcessSessionInformation = 24,
ProcessForegroundInformation = 25,
ProcessWow64Information = 26,
ProcessImageFileName = 27,
ProcessLUIDDeviceMapsEnabled = 28,
ProcessBreakOnTermination = 29,
ProcessDebugObjectHandle = 30,
ProcessDebugFlags = 31,
ProcessHandleTracing = 32,
ProcessIoPriority = 33,
ProcessExecuteFlags = 34,
ProcessTlsInformation = 35,
ProcessCookie = 36,
ProcessImageInformation = 37,
ProcessCycleTime = 38,
ProcessPagePriority = 39,
ProcessInstrumentationCallback = 40,
ProcessThreadStackAllocation = 41,
ProcessWorkingSetWatchEx = 42,
ProcessImageFileNameWin32 = 43,
ProcessImageFileMapping = 44,
ProcessAffinityUpdateMode = 45,
ProcessMemoryAllocationMode = 46,
ProcessGroupInformation = 47,
ProcessTokenVirtualizationEnabled = 48,
ProcessOwnerInformation = 49,
ProcessWindowInformation = 50,
ProcessHandleInformation = 51,
ProcessMitigationPolicy = 52,
ProcessDynamicFunctionTableInformation = 53,
ProcessHandleCheckingMode = 54,
ProcessKeepAliveCount = 55,
ProcessRevokeFileHandles = 56,
ProcessWorkingSetControl = 57,
ProcessHandleTable = 58,
ProcessCheckStackExtentsMode = 59,
ProcessCommandLineInformation = 60,
ProcessProtectionInformation = 61,
ProcessMemoryExhaustion = 62,
ProcessFaultInformation = 63,
ProcessTelemetryIdInformation = 64,
ProcessCommitReleaseInformation = 65,
ProcessReserved1Information = 66,
ProcessReserved2Information = 67,
ProcessSubsystemProcess = 68,
ProcessInPrivate = 70,
ProcessRaiseUMExceptionOnInvalidHandleClose = 71,
ProcessSubsystemInformation = 75,
ProcessWin32kSyscallFilterInformation = 79,
ProcessEnergyTrackingState = 82,
MaxProcessInfoClass // MaxProcessInfoClass should always be the last enum
} PROCESSINFOCLASS;
#define OBJ_CASE_INSENSITIVE 0x00000040L
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
#define STATUS_BAD_DATA ((NTSTATUS)0xC000090BL)
#define STATUS_BAD_FILE_TYPE ((NTSTATUS)0xC0000903L)
#define STATUS_INVALID_IMAGE_FORMAT ((NTSTATUS)0xC000007BL)
#define STATUS_SOURCE_ELEMENT_EMPTY ((NTSTATUS)0xC0000283L)
#define STATUS_FOUND_OUT_OF_SCOPE ((NTSTATUS)0xC000022EL)
#define STATUS_ILLEGAL_FUNCTION ((NTSTATUS)0xC00000AFL)
#define STATUS_OBJECT_NAME_NOT_FOUND ((NTSTATUS)0xC0000034L)
#define STATUS_PROCEDURE_NOT_FOUND ((NTSTATUS)0xC000007AL)
#define STATUS_INVALID_ADDRESS ((NTSTATUS)0xC0000141L)
#define STATUS_STRICT_CFG_VIOLATION ((NTSTATUS)0xC0000606L)
#define RtlPointerToOffset(B,P) ((ULONG)( ((PCHAR)(P)) - ((PCHAR)(B)) ))
#define RtlOffsetToPointer(B,O) ((PCHAR)( ((PCHAR)(B)) + ((ULONG_PTR)(O)) ))
struct SECTION_IMAGE_INFORMATION
{
PVOID TransferAddress;
ULONG ZeroBits;
SIZE_T MaximumStackSize;
SIZE_T CommittedStackSize;
ULONG SubSystemType;
union
{
struct
{
USHORT SubSystemMinorVersion;
USHORT SubSystemMajorVersion;
};
ULONG SubSystemVersion;
};
ULONG GpValue;
USHORT ImageCharacteristics;
USHORT DllCharacteristics;
USHORT Machine;
BOOLEAN ImageContainsCode;
union
{
UCHAR ImageFlags;
struct
{
UCHAR ComPlusNativeReady : 1;
UCHAR ComPlusILOnly : 1;
UCHAR ImageDynamicallyRelocated : 1;
UCHAR ImageMappedFlat : 1;
UCHAR BaseBelow4gb : 1;
UCHAR Reserved : 3;
};
};
ULONG LoaderFlags;
ULONG ImageFileSize;
ULONG CheckSum;
};
enum SECTION_INFORMATION_CLASS
{
SectionBasicInformation,
SectionImageInformation
};
typedef enum _THREADINFOCLASS {
ThreadBasicInformation = 0,
ThreadTimes = 1,
ThreadPriority = 2,
ThreadBasePriority = 3,
ThreadAffinityMask = 4,
ThreadImpersonationToken = 5,
ThreadDescriptorTableEntry = 6,
ThreadEnableAlignmentFaultFixup = 7,
ThreadEventPair_Reusable = 8,
ThreadQuerySetWin32StartAddress = 9,
ThreadZeroTlsCell = 10,
ThreadPerformanceCount = 11,
ThreadAmILastThread = 12,
ThreadIdealProcessor = 13,
ThreadPriorityBoost = 14,
ThreadSetTlsArrayAddress = 15, // Obsolete
ThreadIsIoPending = 16,
ThreadHideFromDebugger = 17,
ThreadBreakOnTermination = 18,
ThreadSwitchLegacyState = 19,
ThreadIsTerminated = 20,
ThreadLastSystemCall = 21,
ThreadIoPriority = 22,
ThreadCycleTime = 23,
ThreadPagePriority = 24,
ThreadActualBasePriority = 25,
ThreadTebInformation = 26,
ThreadCSwitchMon = 27, // Obsolete
ThreadCSwitchPmu = 28,
ThreadWow64Context = 29,
ThreadGroupInformation = 30,
ThreadUmsInformation = 31, // UMS
ThreadCounterProfiling = 32,
ThreadIdealProcessorEx = 33,
ThreadCpuAccountingInformation = 34,
ThreadSuspendCount = 35,
ThreadActualGroupAffinity = 41,
ThreadDynamicCodePolicyInfo = 42,
ThreadSubsystemInformation = 45,
MaxThreadInfoClass = 51,
} THREADINFOCLASS;
typedef enum _MEMORY_INFORMATION_CLASS {
MemoryBasicInformation
} MEMORY_INFORMATION_CLASS;
//Imported native functions from ntdll
extern "C" {
__declspec(dllimport) NTSTATUS CALLBACK ZwQueueApcThread
(
HANDLE hThread,
PKNORMAL_ROUTINE ApcRoutine,
PVOID ApcContext,
PVOID Argument1,
PVOID Argument2
);
__declspec(dllimport) NTSTATUS CALLBACK NtCreateSection
(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_opt_ PLARGE_INTEGER MaximumSize,
_In_ ULONG SectionPageProtection,
_In_ ULONG AllocationAttributes,
_In_opt_ HANDLE FileHandle
);
__declspec(dllimport) NTSTATUS CALLBACK ZwClose
(
_In_ HANDLE Handle
);
__declspec(dllimport) NTSTATUS CALLBACK ZwMapViewOfSection
(
_In_ HANDLE SectionHandle,
_In_ HANDLE ProcessHandle,
_Outptr_result_bytebuffer_(*ViewSize) PVOID* BaseAddress,
_In_ ULONG_PTR ZeroBits,
_In_ SIZE_T CommitSize,
_Inout_opt_ PLARGE_INTEGER SectionOffset,
_Inout_ PSIZE_T ViewSize,
_In_ SECTION_INHERIT InheritDisposition,
_In_ ULONG AllocationType,
_In_ ULONG Win32Protect
);
__declspec(dllimport) NTSTATUS CALLBACK ZwUnmapViewOfSection
(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress
);
__declspec(dllimport) NTSTATUS CALLBACK RtlCreateUserThread
(
IN HANDLE hProcess,
PVOID SecurityDescriptor,
BOOLEAN CreateSuspended,
ULONG ZeroBits,
SIZE_T StackReserve,
SIZE_T StackCommit,
PVOID EntryPoint,
const void* Argument,
PHANDLE phThread,
PCLIENT_ID pCid
);
__declspec(dllimport) NTSTATUS CALLBACK RtlExitUserThread
(
DWORD dwExitCode
);
__declspec(dllimport) NTSTATUS CALLBACK RtlQueueApcWow64Thread
(
HANDLE hThread,
PKNORMAL_ROUTINE ApcRoutine,
PVOID ApcContext,
PVOID Argument1,
PVOID Argument2
);
__declspec(dllimport) NTSTATUS CALLBACK LdrQueryProcessModuleInformation
(
PRTL_PROCESS_MODULES psmi,
ULONG BufferSize,
PULONG RealSize
);
__declspec(dllimport) NTSTATUS CALLBACK NtQueryInformationProcess
(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
__declspec(dllimport) NTSTATUS CALLBACK ZwOpenSection
(
_Out_ PHANDLE SectionHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ POBJECT_ATTRIBUTES ObjectAttributes
);
__declspec(dllimport) NTSTATUS CALLBACK ZwQuerySection
(
IN HANDLE SectionHandle,
IN ULONG SectionInformationClass,
OUT PVOID SectionInformation,
IN ULONG SectionInformationLength,
OUT PSIZE_T ResultLength OPTIONAL
);
__declspec(dllimport) PIMAGE_NT_HEADERS CALLBACK RtlImageNtHeader
(
PVOID Base
);
__declspec(dllimport) PVOID CALLBACK RtlImageDirectoryEntryToData
(
PVOID Base,
BOOLEAN MappedAsImage,
USHORT DirectoryEntry,
PULONG Size
);
__declspec(dllimport) NTSTATUS CALLBACK NtSetInformationThread(
_In_ HANDLE ThreadHandle,
_In_ THREADINFOCLASS ThreadInformationClass,
_When_((ThreadInformationClass != ThreadManageWritesToExecutableMemory),
_In_reads_bytes_(ThreadInformationLength))
_When_((ThreadInformationClass == ThreadManageWritesToExecutableMemory),
_Inout_updates_(ThreadInformationLength))
PVOID ThreadInformation,
_In_ ULONG ThreadInformationLength
);
__declspec(dllimport) NTSTATUS CALLBACK NtQueryVirtualMemory(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_ MEMORY_INFORMATION_CLASS MemoryInformationClass,
_Out_writes_bytes_(MemoryInformationLength) PVOID MemoryInformation,
_In_ SIZE_T MemoryInformationLength,
_Out_opt_ PSIZE_T ReturnLength
);
}
後記
從這篇博文的篇幅可以看出,非同步過程呼叫在Windows
中是一個棘手的主題。理解它的最好方法是自己編寫程式碼並在實踐中進行測試。如果您自己遇到了與APC
打交道的有趣情況,請隨時在下方發表評論。
或者,如果您想直接聯絡我(Rbmm
)或Dennis A. Babkin
,請隨時聯絡。