微软技术分享
+ -

使用CR3切换实现读取指定进程内存数据

2023-11-16 274 1
csdn《微软技术分享》原文转载分享:https://lyshark.blog.csdn.net/

首先CR3是什么,CR3是一个寄存器,该寄存器内保存有页目录表物理地址(PDBR地址),其实CR3内部存放的就是页目录表的内存基地址,运用CR3切换可实现对特定进程内存地址的强制读写操作,此类读写属于有痕读写,多数驱动保护都会将这个地址改为无效,此时CR3读写就失效了,当然如果能找到CR3的正确地址,此方式也是靠谱的一种读写机制。

在x86体系结构中,页表用于将虚拟内存地址映射到物理内存地址。CR3寄存器中存储的页目录表基地址是操作系统在进行虚拟内存管理时使用的关键信息。通过修改CR3寄存器的值,可以切换页目录表,从而改变虚拟地址到物理地址的映射关系。

当发生页表切换时,操作系统会更新CR3寄存器的值,然后通过刷新TLB(Translation Lookaside Buffer)来使新的页表生效。TLB是一个高速缓存,用于加速虚拟地址到物理地址的转换过程。刷新TLB是确保新的页表生效的关键步骤。

操作系统可以使用CR3寄存器来实现进程间的内存隔离和地址空间切换。每个进程都有自己的页目录表,操作系统通过修改CR3寄存器的值来切换不同进程的页目录表,从而实现不同进程之间的内存隔离。

总结起来,CR3寄存器存储了当前正在使用的页目录表的物理地址,通过修改CR3寄存器的值,操作系统可以实现页表切换和进程间的内存隔离。CR3寄存器在虚拟内存管理中起到重要作用,影响着地址映射和内存访问的正确性和性能。

关键知识点

要修改CR3,首先得知道某个进程的CR3位置。
在EPROCESS结构体中,DirectoryTableBase字段存储的是该进程的CR3.这里首先通过Windbg确定进程EPROCESS的CR3地址为:

1: kd> dt _KPROCESS
nt!_KPROCESS
   +0x000 Header           : _DISPATCHER_HEADER
   +0x018 ProfileListHead  : _LIST_ENTRY
   +0x028 DirectoryTableBase : Uint8B

Windows10 x64操作系统地址为:0x28

打开关闭写保护

Write Protect(写保护)是CR0寄存器的第16位(bit 16)。

  • 当设置了WP标志时,阻止特权级程序对只读页面进行写操作;
  • 当清除WP标志时,允许特权级程序对只读页面进行写操作(不考虑U/S位的设置)。

简单来说,CR0.WP用于控制特权级程序对只读页面的写操作。当WP标志被设置时,特权级程序将被禁止写入只读页面,从而保护只读页面的内容不被修改。当WP标志被清除时,特权级程序可以对只读页面进行写操作,即使该页面被标记为只读。这个特性对于实现写时复制(copy-on-write)方法非常有用,该方法用于创建新进程时,将进程的内存映射到相同的物理页面,直到需要修改页面内容时才进行实际的拷贝。

// 关闭写保护
KIRQL Open()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

// 开启写保护
void Close(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

MmIsAddressValid

MmIsAddressValid routine checks whether a page fault will occur for a read or write operation at a given virtual address.

MmIsAddressValid用于地址检查是否可以读写,该读写是不会引起换页的,即该地址是一个non paged pool。

// 检查内存
ULONG64 CheckAddressVal(PVOID p)
{
    if (MmIsAddressValid(p) == FALSE)
        return 0;
    return *(PULONG64)p;
}

CR3寄存器的读写

CR3专门的寄存器读写函数,详见:
https://learn.microsoft.com/zh-cn/cpp/intrinsics/readcr3?view=msvc-170
https://learn.microsoft.com/zh-cn/cpp/intrinsics/writecr3?view=msvc-170

void writecr3(
   unsigned __int64 Data
);
unsigned __int64 __readcr3(void);

禁用启用中断

禁用启用中断:https://learn.microsoft.com/zh-cn/cpp/intrinsics/disable?view=msvc-170

使用时声明:

#pragma  intrinsic(_disable)
#pragma  intrinsic(_enable)

Intrinsic函数是编译器内建的函数,由编译器提供,类似于内联函数。

函数原型:

void _enable(void);
void _disable(void);

读内存数据

可通过两种方式来获取进程EPROCESS地址:
第一种枚举比较进程名称:

#include <ntifs.h>
#include <windef.h>
#include <intrin.h>

NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process);
NTKERNELAPI CHAR* PsGetProcessImageFileName(PEPROCESS Process);

// 定义全局EProcess结构
PEPROCESS Global_Peprocess = NULL;

// 根据进程名获得EPROCESS结构
NTSTATUS GetProcessObjectByName(char *name)
{
    NTSTATUS Status = STATUS_UNSUCCESSFUL;
    SIZE_T i;

    __try
    {
        for (i = 100; i<20000; i += 4)
        {
            NTSTATUS st;
            PEPROCESS ep;
            st = PsLookupProcessByProcessId((HANDLE)i, &ep);
            if (NT_SUCCESS(st))
            {
                char *pn = PsGetProcessImageFileName(ep);
                if (_stricmp(pn, name) == 0)
                {
                    Global_Peprocess = ep;
                }
            }
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        return Status;
    }
    return Status;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint(("Uninstall Driver Is OK \n"));
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    NTSTATUS nt = GetProcessObjectByName("Tutorial-i386.exe");

    if (NT_SUCCESS(nt))
    {
        DbgPrint("[+] eprocess = %x \n", Global_Peprocess);
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

以打开Tutorial-i386.exe为例,打开后即可返回他的Proces,当然也可以直接传入进程PID同样可以得到进程Process结构地址。

// 根据PID打开进程
PEPROCESS Peprocess = NULL;
DWORD PID = 6672;
NTSTATUS nt = PsLookupProcessByProcessId((HANDLE)PID, &Peprocess);

通过CR3读取内存实现代码如下,我们读取Tutorial-i386.exe里面的0x0009EDC8这段内存,读出长度是4字节,代码如下。

#include <ntifs.h>
#include <windef.h>
#include <intrin.h>

#define DIRECTORY_TABLE_BASE 0x028

#pragma  intrinsic(_disable)
#pragma  intrinsic(_enable)

NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process);
NTKERNELAPI CHAR* PsGetProcessImageFileName(PEPROCESS Process);

// 关闭写保护
KIRQL Open()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

// 开启写保护
void Close(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

// 检查内存
ULONG64 CheckAddressVal(PVOID p)
{
    if (MmIsAddressValid(p) == FALSE)
        return 0;
    return *(PULONG64)p;
}

// CR3 寄存器读内存
BOOLEAN CR3_ReadProcessMemory(IN PEPROCESS Process, IN PVOID Address, IN UINT32 Length, OUT PVOID Buffer)
{
    ULONG64 pDTB = 0, OldCr3 = 0, vAddr = 0;
    pDTB = CheckAddressVal((UCHAR*)Process + DIRECTORY_TABLE_BASE);
    if (pDTB == 0)
    {
        return FALSE;
    }

    _disable();
    OldCr3 = __readcr3();
    __writecr3(pDTB);
    _enable();

    if (MmIsAddressValid(Address))
    {
        RtlCopyMemory(Buffer, Address, Length);
        DbgPrint("读入数据: %ld", *(PDWORD)Buffer);
        return TRUE;
    }

    _disable();
    __writecr3(OldCr3);
    _enable();
    return FALSE;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint(("Uninstall Driver Is OK \n"));
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    // 根据PID打开进程
    PEPROCESS Peprocess = NULL;
    DWORD PID = 6672;
    NTSTATUS nt = PsLookupProcessByProcessId((HANDLE)PID, &Peprocess);

    DWORD buffer = 0;

    BOOLEAN bl = CR3_ReadProcessMemory(Peprocess, (PVOID)0x0009EDC8, 4, &buffer);

    DbgPrint("readbuf = %x \n", buffer);
    DbgPrint("readbuf = %d \n", buffer);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

读出后输出效果如下:
150608783613

写内存数据:

#include <ntifs.h>
#include <windef.h>
#include <intrin.h>

#define DIRECTORY_TABLE_BASE 0x028

#pragma  intrinsic(_disable)
#pragma  intrinsic(_enable)

NTKERNELAPI NTSTATUS PsLookupProcessByProcessId(HANDLE ProcessId, PEPROCESS *Process);
NTKERNELAPI CHAR* PsGetProcessImageFileName(PEPROCESS Process);

// 关闭写保护
KIRQL Open()
{
    KIRQL irql = KeRaiseIrqlToDpcLevel();
    UINT64 cr0 = __readcr0();
    cr0 &= 0xfffffffffffeffff;
    __writecr0(cr0);
    _disable();
    return irql;
}

// 开启写保护
void Close(KIRQL irql)
{
    UINT64 cr0 = __readcr0();
    cr0 |= 0x10000;
    _enable();
    __writecr0(cr0);
    KeLowerIrql(irql);
}

// 检查内存
ULONG64 CheckAddressVal(PVOID p)
{
    if (MmIsAddressValid(p) == FALSE)
        return 0;
    return *(PULONG64)p;
}

// CR3 寄存器写内存
BOOLEAN CR3_WriteProcessMemory(IN PEPROCESS Process, IN PVOID Address, IN UINT32 Length, IN PVOID Buffer)
{
    ULONG64 pDTB = 0, OldCr3 = 0, vAddr = 0;

    // 检查内存
    pDTB = CheckAddressVal((UCHAR*)Process + DIRECTORY_TABLE_BASE);
    if (pDTB == 0)
    {
        return FALSE;
    }

    _disable();

    // 读取CR3
    OldCr3 = __readcr3();

    // 写CR3
    __writecr3(pDTB);
    _enable();

    // 验证并拷贝内存
    if (MmIsAddressValid(Address))
    {
        RtlCopyMemory(Address, Buffer, Length);
        return TRUE;
    }
    _disable();

    // 恢复CR3
    __writecr3(OldCr3);
    _enable();
    return FALSE;
}

VOID UnDriver(PDRIVER_OBJECT driver)
{
    DbgPrint(("Uninstall Driver Is OK \n"));
}

NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
    DbgPrint("hello lyshark \n");

    // 根据PID打开进程
    PEPROCESS Peprocess = NULL;
    DWORD PID = 6672;
    NTSTATUS nt = PsLookupProcessByProcessId((HANDLE)PID, &Peprocess);

    DWORD buffer = 999;

    BOOLEAN bl = CR3_WriteProcessMemory(Peprocess, (PVOID)0x0009EDC8, 4, &buffer);
    DbgPrint("写出状态: %d \n", bl);

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

写出后效果如下:
150647537313

CR3被改掉的情况

没看懂。。。

至于进程将CR3改掉了读取不到该寄存器该如何处理,这里我找到了一段参考代码,可以实现寻找CR3地址这个功能。

#include <ntddk.h>
#include <ntstrsafe.h>
#include <windef.h>
#include <intrin.h>

#pragma pack(push, 1)

typedef struct _IDTR // IDT基址
{
    USHORT limit;    // 范围 占8位
    ULONG64 base;    // 基地址 占32位 _IDT_ENTRY类型指针
}IDTR, *PIDTR;

typedef union _IDT_ENTRY
{
    struct kidt
    {
        USHORT OffsetLow;
        USHORT Selector;
        USHORT IstIndex : 3;
        USHORT Reserved0 : 5;
        USHORT Type : 5;
        USHORT Dpl : 2;
        USHORT Present : 1;
        USHORT OffsetMiddle;
        ULONG OffsetHigh;
        ULONG Reserved1;
    }idt;
    UINT64 Alignment;
} IDT_ENTRY, *PIDT_ENTRY;

#pragma pack(pop)

// 输出调试内容
void DebugPrint(const char* fmt, ...)
{
    UNREFERENCED_PARAMETER(fmt);
    va_list ap;
    va_start(ap, fmt);
    vDbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, fmt, ap);
    va_end(ap);
    return;
}

// 获取IDT表地址
ULONG64 GetIdtAddr(ULONG64 pIdtBaseAddr, UCHAR pIndex)
{
    PIDT_ENTRY Pidt_info = (PIDT_ENTRY)(pIdtBaseAddr);
    Pidt_info += pIndex;
    ULONG64 vCurrentAddr = 0;
    ULONG64 vCurrentHighAddr = 0;
    vCurrentAddr = Pidt_info->idt.OffsetMiddle;
    vCurrentAddr = vCurrentAddr << 16;
    vCurrentAddr += Pidt_info->idt.OffsetLow;

    vCurrentHighAddr = Pidt_info->idt.OffsetHigh;
    vCurrentHighAddr = vCurrentHighAddr << 32;
    vCurrentAddr += vCurrentHighAddr;
    return vCurrentAddr;
}

VOID UnLoadDriver()
{

}

NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT pPDriverObj, _In_ PUNICODE_STRING pRegistryPath)
{
    UNREFERENCED_PARAMETER(pRegistryPath);
    pPDriverObj->DriverUnload = (PDRIVER_UNLOAD)UnLoadDriver;

    /**
    TP版KiPageFault
    fffff880`09f54000 50              push    rax
    // 这里实际上是真实处理函数的地址 需要 & 0xFFFFFFFFFFF00000
    fffff880`09f54001 48b87830ce0980f8ffff mov rax,0FFFFF88009CE3078h
    fffff880`09f5400b 4883ec08        sub     rsp,8
    fffff880`09f5400f 48890424        mov     qword ptr [rsp],rax
    fffff880`09f54013 48311424        xor     qword ptr [rsp],rdx
    fffff880`09f54017 e810000000      call    fffff880`09f5402c
    fffff880`09f5401c 896eff          mov     dword ptr [rsi-1],ebp
    fffff880`09f5401f 230500000089    and     eax,dword ptr [fffff87f`92f54025]
    **/
    //得到TP KiPageFault地址
    // _IDTR vContent;
    // __sidt(&vContent);
    ULONG64 vTpKiPageFault = GetIdtAddr(vContent.base, 0xE);

    //得到TP 动态内存起始值
    ULONG64 vTpMemory = *(PULONG64)(vTpKiPageFault + 0x3) & 0xFFFFFFFFFFF00000;

    //得到TP KiPageFault真实处理函数
    ULONG64 vTpKiPageFaultFuncAddr = vTpMemory + 0x4CE7C;

    if (MmIsAddressValid((PVOID)vTpKiPageFaultFuncAddr))
    {//真实处理函数有效

        //得到TP数据对象基地址
        ULONG64 vTpDataObjectBase = *(PULONG)(vTpMemory + 0x1738B) + vTpMemory + 0x1738F;

        if (MmIsAddressValid((PVOID)vTpDataObjectBase))
        {//基地址有效

            //得到TP 用来保存真实CR3 保存当前所属进程ID 的对象
            ULONG64 vTpDataObject = *(PULONG64)vTpDataObjectBase;

            DebugPrint("数据对象:0x%016llx, 真实CR3:0x%016llx, 所属进程ID:%d\n", vTpDataObject, *(PULONG64)(vTpDataObject + 0x70), *(PULONG)(vTpDataObject + 0x18));
        }
        else
            DebugPrint("vTpDataObjectBase无法读取:0x%016llx\n", vTpDataObjectBase);
    }
    else
        DebugPrint("vTpKiPageFaultFuncAddr无法读取:0x%016llx\n", vTpKiPageFaultFuncAddr);

    return STATUS_SUCCESS;
}

0 篇笔记 写笔记

使用CR3切换实现读取指定进程内存数据
首先CR3是什么,CR3是一个寄存器,该寄存器内保存有页目录表物理地址(PDBR地址),其实CR3内部存放的就是页目录表的内存基地址,运用CR3切换可实现对特定进程内存地址的强制读写操作,此类读写属于有痕读写,多数驱动保护都会将这个地址改为无效,此时CR3读写就失效了,当然如果能找到CR3的正确地址......
作者信息
我爱内核
Windows驱动开发,网站开发
好好学习,天天向上。
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!