(72)进程挂靠(attach)使用备用APC队列 SavedApcState 保存父进程 APC 队列,分析 NtReadVirtualMemory
一、回顧
上次課我們學習了 _KAPC_STATE , _KAPC 結構,分析了 TerminateThread 函數最終如何通過插入 APC 的方式來通知目標線程終止。
這次課我們來學習備用APC隊列 SavedApcState ,這個結構在進程掛靠attach時會使用到。
課后作業是分析 NtReadVirtualMemory 函數,通過分析,我們可以了解什么是 attach ,以及 attach 時如何使用 SavedApcState 。
二、SavedApcState
進程 attach
kd> dt _KTHREAD nt!_KTHREAD...+0x034 ApcState : _KAPC_STATE...+0x138 ApcStatePointer : [2] Ptr32 _KAPC_STATE...+0x14c SavedApcState : _KAPC_STATE...+0x165 ApcStateIndex : UChar+0x166 ApcQueueable : UChar...假設A進程創建T線程。
當不發生 attach 時,A的APC隊列存到 ApcState,SavedApcState 不使用;
當T線程 attach 到B進程時,把A的APC隊列暫時保存到 SavedApcState ,B的存到 ApcState。
ApcStatePointer 和 ApcStateIndex
再介紹一下 ApcStatePointer 和 ApcStateIndex 。
為了操作方便,_KTHREAD結構體中定義了一個指針數組ApcStatePointer ,長度為2。
正常情況下:
ApcStatePointer[0] 指向 ApcState
ApcStatePointer[1] 指向 SavedApcState
掛靠情況下:
ApcStatePointer[0] 指向 SavedApcState
ApcStatePointer[1] 指向 ApcState
ApcStateIndex用來標識當前線程處于什么狀態:0 正常狀態 1 掛靠狀態。
所以不論是正常狀態還是attach狀態, ApcStatePointer[ApcStateIndex] 都指向線程當前所使用的CR3的進程的 ApcState 。
另外,KAPC結構里也有一個同名成員 ApcStateIndex ,這里介紹的是 KTHREAD 里的,二者含義不同,注意區分。
ApcQueueable
這個值表示線程當前是否可以插入 APC ,如果線程正在退出,那么是不能插入的。
三、分析 NtReadVirtualMemory
這部分是課后練習。
分析 NtReadVirtualMemory 在attach時如何備份和恢復APC隊列。
這部分函數層層調用,全部貼出來會導致博客的篇幅過長,但是不貼又不方便以后復習,所以我會在不太重要的函數后標注,大家可以有選擇性地跳過外層的函數,關注底層函數。
NtReadVirtualMemory(底層調用了 MmCopyVirtualMemory)
NTSTATUS NtReadVirtualMemory (IN HANDLE ProcessHandle,IN PVOID BaseAddress,OUT PVOID Buffer,IN SIZE_T BufferSize,OUT PSIZE_T NumberOfBytesRead OPTIONAL)/*++Routine Description:This function copies the specified address range from the specifiedprocess into the specified address range of the current process.Arguments:ProcessHandle - Supplies an open handle to a process object.BaseAddress - Supplies the base address in the specified processto be read.Buffer - Supplies the address of a buffer which receives thecontents from the specified process address space.BufferSize - Supplies the requested number of bytes to read fromthe specified process.NumberOfBytesRead - Receives the actual number of bytestransferred into the specified buffer.Return Value:NTSTATUS.--*/{SIZE_T BytesCopied;KPROCESSOR_MODE PreviousMode;PEPROCESS Process;NTSTATUS Status;PETHREAD CurrentThread;PAGED_CODE();//// Get the previous mode and probe output argument if necessary.//CurrentThread = PsGetCurrentThread ();PreviousMode = KeGetPreviousModeByThread(&CurrentThread->Tcb);// 如果是3環調用這個函數if (PreviousMode != KernelMode) {// 非法訪問內存,返回 STATUS_ACCESS_VIOLATIONif (((PCHAR)BaseAddress + BufferSize < (PCHAR)BaseAddress) ||((PCHAR)Buffer + BufferSize < (PCHAR)Buffer) ||((PVOID)((PCHAR)BaseAddress + BufferSize) > MM_HIGHEST_USER_ADDRESS) ||((PVOID)((PCHAR)Buffer + BufferSize) > MM_HIGHEST_USER_ADDRESS)) {return STATUS_ACCESS_VIOLATION;}// 如果 NumberOfBytesRead 不是空指針,確保其可寫if (ARGUMENT_PRESENT(NumberOfBytesRead)) {try {// ProbeForWriteUlong_ptr 在 ex.h 定義,作用是確保地址可寫ProbeForWriteUlong_ptr (NumberOfBytesRead);} except(EXCEPTION_EXECUTE_HANDLER) {return GetExceptionCode();}}}//// If the buffer size is not zero, then attempt to read data from the// specified process address space into the current process address// space.// 如果緩沖區大小不為0,從目標進程復制數據到當前進程BytesCopied = 0;Status = STATUS_SUCCESS;if (BufferSize != 0) {//// Reference the target process.// 獲取目標進程EPROCESSStatus = ObReferenceObjectByHandle(ProcessHandle,PROCESS_VM_READ,PsProcessType,PreviousMode,(PVOID *)&Process,NULL);//// If the process was successfully referenced, then attempt to// read the specified memory either by direct mapping or copying// through nonpaged pool.// 如果獲取目標進程成功,嘗試讀取數據,通過映射或復制的方式if (Status == STATUS_SUCCESS) {Status = MmCopyVirtualMemory (Process,BaseAddress,PsGetCurrentProcessByThread(CurrentThread),Buffer,BufferSize,PreviousMode,&BytesCopied);//// Dereference the target process.//ObDereferenceObject(Process); // 引用計數-1}}//// If requested, return the number of bytes read.// 如果 NumberOfBytesRead 非空,返回讀取到的字節數if (ARGUMENT_PRESENT(NumberOfBytesRead)) {try {*NumberOfBytesRead = BytesCopied;} except(EXCEPTION_EXECUTE_HANDLER) {NOTHING;}}return Status; }發現干活的是 MmCopyVirtualMemory 函數,所以接下來分析 MmCopyVirtualMemory。
MmCopyVirtualMemory (底層調用 MiDoMappedCopy 或 MiDoPoolCopy )
通過分析,發現這個函數判斷要讀取的內存大小,決定用 MiDoMappedCopy 還是 MiDoPoolCopy,如果大于 511 字節,使用 MiDoMappedCopy ,否則使用 MiDoPoolCopy .
NTSTATUS MmCopyVirtualMemory(IN PEPROCESS FromProcess,IN CONST VOID *FromAddress,IN PEPROCESS ToProcess,OUT PVOID ToAddress,IN SIZE_T BufferSize,IN KPROCESSOR_MODE PreviousMode,OUT PSIZE_T NumberOfBytesCopied) {NTSTATUS Status;PEPROCESS ProcessToLock;// 斷言 BufferSize != 0if (BufferSize == 0) {ASSERT (FALSE); // No one should call with a zero size.return STATUS_SUCCESS;}// 鎖定要讀取數據的進程ProcessToLock = FromProcess;if (FromProcess == PsGetCurrentProcess()) {ProcessToLock = ToProcess;}//// Make sure the process still has an address space.// 確保進程還活著if (ExAcquireRundownProtection (&ProcessToLock->RundownProtect) == FALSE) {return STATUS_PROCESS_IS_TERMINATING;}//// If the buffer size is greater than the pool move threshold,// then attempt to write the memory via direct mapping.//// #define POOL_MOVE_THRESHOLD 511// 如果要復制的字節數大于511,采用內存映射方式if (BufferSize > POOL_MOVE_THRESHOLD) {Status = MiDoMappedCopy(FromProcess,FromAddress,ToProcess,ToAddress,BufferSize,PreviousMode,NumberOfBytesCopied);//// If the completion status is not a working quota problem,// then finish the service. Otherwise, attempt to write the// memory through nonpaged pool.//if (Status != STATUS_WORKING_SET_QUOTA) {goto CompleteService;}*NumberOfBytesCopied = 0;}//// There was not enough working set quota to write the memory via// direct mapping or the size of the write was below the pool move// threshold. Attempt to write the specified memory through nonpaged// pool.//Status = MiDoPoolCopy(FromProcess,FromAddress,ToProcess,ToAddress,BufferSize,PreviousMode,NumberOfBytesCopied);//// Dereference the target process.//CompleteService://// Indicate that the vm operation is complete.//ExReleaseRundownProtection (&ProcessToLock->RundownProtect);return Status; }真正干活的是 MiDoMappedCopy 和 MiDoPoolCopy。
為了給后面的分析作鋪墊,我這里先給出 KeStackAttachProcess 函數的源碼,先弄明白 attach 做了哪些工作。
KeStackAttachProcess (底層調用 KiAttachProcess )
VOID KeStackAttachProcess (IN PRKPROCESS Process,OUT PRKAPC_STATE ApcState)/*++Routine Description:This function attaches a thread to a target process' address spaceand returns information about a previous attached process.Arguments:Process - Supplies a pointer to a dispatcher object of type process.Return Value:None.--*/{KIRQL OldIrql;PRKTHREAD Thread;ASSERT_PROCESS(Process);ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);//// If the current thread is executing a DPC, then bug check.// 如果當前線程正在執行 DPC,則藍屏。Thread = KeGetCurrentThread();if (KeIsExecutingDpc() != FALSE) {KeBugCheckEx(INVALID_PROCESS_ATTACH_ATTEMPT,(ULONG_PTR)Process,(ULONG_PTR)Thread->ApcState.Process,(ULONG)Thread->ApcStateIndex,(ULONG)KeIsExecutingDpc());}//// If the target process is not the current process, then attach the// target process. Otherwise, return a distinguished process value to// indicate that an attach was not performed.// 如果嘗試 attach 自己,那么設置ApcState->Process 等于1// 否則就正常 attachif (Thread->ApcState.Process == Process) {ApcState->Process = (PRKPROCESS)1;} else {//// Raise IRQL to dispatcher level and lock dispatcher database.// 提升 IRQL 等級到 dispatcher level 并鎖定 dispatcher database。//// If the current thread is attached to a process, then save the// current APC state in the callers APC state structure. Otherwise,// save the current APC state in the saved APC state structure, and// return a NULL process pointer.// 如果當前線程已經 attach 了別的進程,那么保存當前 APC state 到調用者的 APC state 結構// 否則,保存當前 APC state 到 SavedApcState 結構,并設置 ApcState->Process = NULL//// N.B. The dispatcher lock is released ay the attach routine.// dispatcher lock 在 attach 函數中釋放KiLockDispatcherDatabase(&OldIrql);if (Thread->ApcStateIndex != 0) {// 當前線程已經 attach 了一個進程KiAttachProcess(Thread, Process, OldIrql, ApcState);} else {// 當前線程的所屬進程就是創建線程的進程KiAttachProcess(Thread, Process, OldIrql, &Thread->SavedApcState);ApcState->Process = NULL;}}return; }KiAttachProcess (真正的進程 attach 函數)
attach 進程最底層干活的函數,功能是把當前線程 attach 到目標進程中,并返回 SavedApcState 。
VOID KiAttachProcess (IN PRKTHREAD Thread,IN PKPROCESS Process,IN KIRQL OldIrql,OUT PRKAPC_STATE SavedApcState)/*++Routine Description:This function attaches a thread to a target process' address space.N.B. The dispatcher database lock must be held when this routine iscalled.Arguments:Thread - Supplies a pointer to a dispatcher object of type thread.Process - Supplies a pointer to a dispatcher object of type process.OldIrql - Supplies the previous IRQL.SavedApcState - Supplies a pointer to the APC state structure that receivesthe saved APC state.Return Value:None.--*/{PRKTHREAD OutThread;KAFFINITY Processor;PLIST_ENTRY NextEntry;KIRQL HighIrql;ASSERT(Process != Thread->ApcState.Process);//// Bias the stack count of the target process to signify that a// thread exists in that process with a stack that is resident.//Process->StackCount += 1;//// Save current APC state and initialize a new APC state.//// KiMoveApcState 的功能是將參數1的APC隊列復制一份到參數2// 這里這么做的原因是 attach 前要把父進程的 APC 隊列保存起來,保存到 SavedApcStateKiMoveApcState(&Thread->ApcState, SavedApcState);// InitializeListHead 的功能是讓鏈表頭指向自己InitializeListHead(&Thread->ApcState.ApcListHead[KernelMode]); // ApcListHead[0]InitializeListHead(&Thread->ApcState.ApcListHead[UserMode]); // ApcListHead[1]Thread->ApcState.Process = Process;Thread->ApcState.KernelApcInProgress = FALSE;Thread->ApcState.KernelApcPending = FALSE;Thread->ApcState.UserApcPending = FALSE;if (SavedApcState == &Thread->SavedApcState) {Thread->ApcStatePointer[0] = &Thread->SavedApcState; // 原進程的 APC stateThread->ApcStatePointer[1] = &Thread->ApcState; // attach 進程的 APC stateThread->ApcStateIndex = 1; // 表示現在已經 attach}//// If the target process is in memory, then immediately enter the// new address space by loading a new Directory Table Base. Otherwise,// insert the current thread in the target process ready list, inswap// the target process if necessary, select a new thread to run on the// the current processor and context switch to the new thread.//if (Process->State == ProcessInMemory) {//// It is possible that the process is in memory, but there exist// threads in the process ready list. This can happen when memory// management forces a process attach.//NextEntry = Process->ReadyListHead.Flink;while (NextEntry != &Process->ReadyListHead) {OutThread = CONTAINING_RECORD(NextEntry, KTHREAD, WaitListEntry);RemoveEntryList(NextEntry);OutThread->ProcessReadyQueue = FALSE;KiReadyThread(OutThread);NextEntry = Process->ReadyListHead.Flink;}KiSwapProcess(Process, SavedApcState->Process);KiUnlockDispatcherDatabase(OldIrql);} else {Thread->State = Ready;Thread->ProcessReadyQueue = TRUE;InsertTailList(&Process->ReadyListHead, &Thread->WaitListEntry);if (Process->State == ProcessOutOfMemory) {Process->State = ProcessInTransition;InterlockedPushEntrySingleList(&KiProcessInSwapListHead,&Process->SwapListEntry);KiSetSwapEvent();}//// Clear the active processor bit in the previous process and// set active processor bit in the process being attached to.//#if !defined(NT_UP)KiLockContextSwap(&HighIrql);Processor = KeGetCurrentPrcb()->SetMember;SavedApcState->Process->ActiveProcessors &= ~Processor;Process->ActiveProcessors |= Processor;KiUnlockContextSwap(HighIrql);#endifThread->WaitIrql = OldIrql;KiSwapThread();}return; }MiDoMappedCopy
調用了 KeStackAttachProcess 函數,附加到目標進程,讀取數據后 detach。
NTSTATUS MiDoMappedCopy (IN PEPROCESS FromProcess,IN CONST VOID *FromAddress,IN PEPROCESS ToProcess,OUT PVOID ToAddress,IN SIZE_T BufferSize,IN KPROCESSOR_MODE PreviousMode,OUT PSIZE_T NumberOfBytesRead)/*++Routine Description:This function copies the specified address range from the specifiedprocess into the specified address range of the current process.Arguments:FromProcess - Supplies an open handle to a process object.FromAddress - Supplies the base address in the specified processto be read.ToProcess - Supplies an open handle to a process object.ToAddress - Supplies the address of a buffer which receives thecontents from the specified process address space.BufferSize - Supplies the requested number of bytes to read fromthe specified process.PreviousMode - Supplies the previous processor mode.NumberOfBytesRead - Receives the actual number of bytestransferred into the specified buffer.Return Value:NTSTATUS.--*/{KAPC_STATE ApcState;SIZE_T AmountToMove;ULONG_PTR BadVa;LOGICAL Moving;LOGICAL Probing;LOGICAL LockedMdlPages;CONST VOID *InVa;SIZE_T LeftToMove;PSIZE_T MappedAddress;SIZE_T MaximumMoved;PMDL Mdl;PFN_NUMBER MdlHack[(sizeof(MDL)/sizeof(PFN_NUMBER)) + (MAX_LOCK_SIZE >> PAGE_SHIFT) + 1];PVOID OutVa;LOGICAL MappingFailed;LOGICAL ExceptionAddressConfirmed;PAGED_CODE();MappingFailed = FALSE;InVa = FromAddress;OutVa = ToAddress;//#define MAX_LOCK_SIZE ((ULONG)(14 * PAGE_SIZE))MaximumMoved = MAX_LOCK_SIZE;if (BufferSize <= MAX_LOCK_SIZE) {MaximumMoved = BufferSize;}Mdl = (PMDL)&MdlHack[0];//// Map the data into the system part of the address space, then copy it.//LeftToMove = BufferSize;AmountToMove = MaximumMoved;Probing = FALSE;//// Initializing BadVa & ExceptionAddressConfirmed is not needed for// correctness but without it the compiler cannot compile this code// W4 to check for use of uninitialized variables.//BadVa = 0;ExceptionAddressConfirmed = FALSE;#if 0//// It is unfortunate that Windows 2000 and all the releases of NT always// inadvertently returned from this routine detached, as we must maintain// this behavior even now.//KeDetachProcess();#endifwhile (LeftToMove > 0) {if (LeftToMove < AmountToMove) {//// Set to move the remaining bytes.//AmountToMove = LeftToMove;}// attach 到目標進程,返回 attach 前的進程的 APC 隊列,即 SavedApcState KeStackAttachProcess (&FromProcess->Pcb, &ApcState);MappedAddress = NULL;LockedMdlPages = FALSE;Moving = FALSE;ASSERT (Probing == FALSE);//// We may be touching a user's memory which could be invalid,// declare an exception handler.// 讀3環內存,確保可讀,設置異常處理try {//// Probe to make sure that the specified buffer is accessible in// the target process.//if ((InVa == FromAddress) && (PreviousMode != KernelMode)){Probing = TRUE;ProbeForRead (FromAddress, BufferSize, sizeof(CHAR));Probing = FALSE;}//// Initialize MDL for request.//MmInitializeMdl (Mdl, (PVOID)InVa, AmountToMove);MmProbeAndLockPages (Mdl, PreviousMode, IoReadAccess);LockedMdlPages = TRUE;MappedAddress = MmMapLockedPagesSpecifyCache (Mdl,KernelMode,MmCached,NULL,FALSE,HighPagePriority);if (MappedAddress == NULL) {MappingFailed = TRUE;ExRaiseStatus(STATUS_INSUFFICIENT_RESOURCES);}//// Deattach from the FromProcess and attach to the ToProcess.// deattach 即分離,已經讀好了KeUnstackDetachProcess (&ApcState);KeStackAttachProcess (&ToProcess->Pcb, &ApcState);//// Now operating in the context of the ToProcess.//if ((InVa == FromAddress) && (PreviousMode != KernelMode)){Probing = TRUE;ProbeForWrite (ToAddress, BufferSize, sizeof(CHAR));Probing = FALSE;}Moving = TRUE;RtlCopyMemory (OutVa, MappedAddress, AmountToMove);} except (MiGetExceptionInfo (GetExceptionInformation(),&ExceptionAddressConfirmed,&BadVa)) {//// If an exception occurs during the move operation or probe,// return the exception code as the status value.//KeUnstackDetachProcess (&ApcState);if (MappedAddress != NULL) {MmUnmapLockedPages (MappedAddress, Mdl);}if (LockedMdlPages == TRUE) {MmUnlockPages (Mdl);}if (GetExceptionCode() == STATUS_WORKING_SET_QUOTA) {return STATUS_WORKING_SET_QUOTA;}if ((Probing == TRUE) || (MappingFailed == TRUE)) {return GetExceptionCode();}//// If the failure occurred during the move operation, determine// which move failed, and calculate the number of bytes// actually moved.//*NumberOfBytesRead = BufferSize - LeftToMove;if (Moving == TRUE) {if (ExceptionAddressConfirmed == TRUE) {*NumberOfBytesRead = (SIZE_T)((ULONG_PTR)BadVa - (ULONG_PTR)FromAddress);}}return STATUS_PARTIAL_COPY;}KeUnstackDetachProcess (&ApcState);MmUnmapLockedPages (MappedAddress, Mdl);MmUnlockPages (Mdl);LeftToMove -= AmountToMove;InVa = (PVOID)((ULONG_PTR)InVa + AmountToMove);OutVa = (PVOID)((ULONG_PTR)OutVa + AmountToMove);}//// Set number of bytes moved.//*NumberOfBytesRead = BufferSize;return STATUS_SUCCESS; }MiDoPoolCopy
略。
總結
以上是生活随笔為你收集整理的(72)进程挂靠(attach)使用备用APC队列 SavedApcState 保存父进程 APC 队列,分析 NtReadVirtualMemory的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: (71)APC队列,KAPC结构,分析
- 下一篇: (73)分析 KeInitializeA