一、回顾
上次课我们学习了 _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 specified
process 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 process
to be read.
Buffer - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from
the specified process.
NumberOfBytesRead - Receives the actual number of bytes
transferred 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_VIOLATION
if (((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.
// 获取目标进程EPROCESS
Status = 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 != 0
if (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 space
and 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
// 否则就正常 attach
if (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 is
called.
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 receives
the 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 队列保存起来,保存到 SavedApcState
KiMoveApcState(&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 state
Thread->ApcStatePointer[1] = &Thread->ApcState; // attach 进程的 APC state
Thread->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);
#endif
Thread->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 specified
process 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 process
to be read.
ToProcess - Supplies an open handle to a process object.
ToAddress - Supplies the address of a buffer which receives the
contents from the specified process address space.
BufferSize - Supplies the requested number of bytes to read from
the specified process.
PreviousMode - Supplies the previous processor mode.
NumberOfBytesRead - Receives the actual number of bytes
transferred 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();
#endif
while (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;
}