2021-08-16

一、回顾
上次课我们学习了 _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;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值