3.5 Windows驱动开发:应用层与内核层内存映射

简介: 在上一篇博文`《内核通过PEB得到进程参数》`中我们通过使用`KeStackAttachProcess`附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。

在上一篇博文《内核通过PEB得到进程参数》中我们通过使用KeStackAttachProcess附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。

3.5.1 应用层映射到内核层

先来实现将R3内存数据拷贝到R0中,功能实现所调用的API如下:

  • 调用IoAllocateMdl创建一个MDL结构体。这个结构体描述了一个要锁定的内存页的位置和大小。
  • 调用MmProbeAndLockPages用于锁定创建的地址其中UserMode代表用户层,IoReadAccess以读取的方式锁定
  • 调用MmGetSystemAddressForMdlSafe用于从MDL中得到映射内存地址
  • 调用RtlCopyMemory用于内存拷贝,将DstAddr应用层中的数据拷贝到pMappedSrc
  • 调用MmUnlockPages拷贝结束后解锁pSrcMdl
  • 调用IoFreeMdl释放之前创建的MDL结构体。

如上则是应用层数据映射到内核中的流程,我们将该流程封装成SafeCopyMemory_R3_to_R0方便后期的使用,函数对数据的复制进行了分块操作,因此可以处理更大的内存块。

下面是对该函数的分析:

  • 1.首先进行一些参数的检查,如果有任何一个参数为0,那么函数就会返回 STATUS_UNSUCCESSFUL

  • 2.使用一个 while 循环来分块复制数据,每个块的大小为 PAGE_SIZE (通常是4KB)。这个循环在整个内存范围内循环,每次复制一个内存页的大小,直到复制完整个内存范围。

  • 3.在循环内部,首先根据起始地址和当前要复制的大小来确定本次要复制的大小。如果剩余的内存不足一页大小,则只复制剩余的内存。

  • 4.调用 IoAllocateMdl 创建一个 MDL,表示要锁定和复制的内存页。这里使用了 (PVOID)(SrcAddr & 0xFFFFFFFFFFFFF000) 来确定页的起始地址。因为页的大小为 0x1000,因此在计算页的起始地址时,将 SrcAddr 向下舍入到最接近的 0x1000 的倍数。

  • 5.如果 IoAllocateMdl 成功,则调用 MmProbeAndLockPages 来锁定页面。这个函数将页面锁定到物理内存中,并返回一个虚拟地址,该虚拟地址指向已锁定页面的内核地址。

  • 6.使用 MmGetSystemAddressForMdlSafe 函数获取一个映射到内核空间的地址,该地址可以直接访问锁定的内存页。

  • 6.如果获取到了映射地址,则使用 RtlCopyMemory 函数将要复制的数据从应用层内存拷贝到映射到内核空间的地址。在复制结束后,使用 MmUnlockPages 函数解锁内存页,释放对页面的访问权限。

最后,释放 MDL 并更新 SrcAddrDstAddr 以复制下一个内存块。如果复制过程中发生任何异常,函数将返回 STATUS_UNSUCCESSFUL

总的来说,这个函数是一个很好的实现,它遵循了内核驱动程序中的最佳实践,包括对内存的安全处理、分块复制、错误处理等。

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

// 分配内存
void* RtlAllocateMemory(BOOLEAN InZeroMemory, SIZE_T InSize)
{
   
   
    void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, 'lysh');
    if (InZeroMemory && (Result != NULL))
        RtlZeroMemory(Result, InSize);
    return Result;
}

// 释放内存
void RtlFreeMemory(void* InPointer)
{
   
   
    ExFreePool(InPointer);
}

/*
将应用层中的内存复制到内核变量中

SrcAddr  r3地址要复制
DstAddr  R0申请的地址
Size     拷贝长度
*/
NTSTATUS SafeCopyMemory_R3_to_R0(ULONG_PTR SrcAddr, ULONG_PTR DstAddr, ULONG Size)
{
   
   
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    ULONG nRemainSize = PAGE_SIZE - (SrcAddr & 0xFFF);
    ULONG nCopyedSize = 0;

    if (!SrcAddr || !DstAddr || !Size)
    {
   
   
        return status;
    }

    while (nCopyedSize < Size)
    {
   
   
        PMDL pSrcMdl = NULL;
        PVOID pMappedSrc = NULL;

        if (Size - nCopyedSize < nRemainSize)
        {
   
   
            nRemainSize = Size - nCopyedSize;
        }

        // 创建MDL
        pSrcMdl = IoAllocateMdl((PVOID)(SrcAddr & 0xFFFFFFFFFFFFF000), PAGE_SIZE, FALSE, FALSE, NULL);
        if (pSrcMdl)
        {
   
   
            __try
            {
   
   
                // 锁定内存页面(UserMode代表应用层)
                MmProbeAndLockPages(pSrcMdl, UserMode, IoReadAccess);

                // 从MDL中得到映射内存地址
                pMappedSrc = MmGetSystemAddressForMdlSafe(pSrcMdl, NormalPagePriority);
            }
            __except (EXCEPTION_EXECUTE_HANDLER)
            {
   
   
            }
        }

        if (pMappedSrc)
        {
   
   
            __try
            {
   
   
                // 将MDL中的映射拷贝到pMappedSrc内存中
                RtlCopyMemory((PVOID)DstAddr, (PVOID)((ULONG_PTR)pMappedSrc + (SrcAddr & 0xFFF)), nRemainSize);
            }
            __except (1)
            {
   
   
                // 拷贝内存异常
            }

            // 释放锁
            MmUnlockPages(pSrcMdl);
        }

        if (pSrcMdl)
        {
   
   
            // 释放MDL
            IoFreeMdl(pSrcMdl);
        }

        if (nCopyedSize)
        {
   
   
            nRemainSize = PAGE_SIZE;
        }

        nCopyedSize += nRemainSize;
        SrcAddr += nRemainSize;
        DstAddr += nRemainSize;
    }

    status = STATUS_SUCCESS;
    return status;
}

有了封装好的SafeCopyMemory_R3_to_R0函数,那么接下来就是使用该函数实现应用层到内核层中的拷贝,为了能实现拷贝我们需要做以下几个准备工作;

  • 1.使用PsLookupProcessByProcessId函数通过进程ID查找到对应的EProcess结构体,以获取该进程在内核中的信息。
  • 2.使用KeStackAttachProcess函数将当前进程的执行上下文切换到指定进程的上下文,以便能够访问该进程的内存。
  • 3.使用RtlAllocateMemory函数在当前进程的内存空间中分配一块缓冲区,用于存储从指定进程中读取的数据。
  • 4.调用SafeCopyMemory_R3_to_R0函数将指定进程的内存数据拷贝到分配的缓冲区中。
  • 5.将缓冲区中的数据转换为BYTE类型的指针,并将其输出。

PsLookupProcessByProcessId函数用于通过进程ID查找到对应的EProcess结构体,这个结构体是Windows操作系统内核中用于表示一个进程的数据结构。

NTSTATUS status = PsLookupProcessByProcessId(ProcessId, &ProcessObject);
if (!NT_SUCCESS(status)) {
   
   
    return status;
}

KeStackAttachProcess函数将当前进程的执行上下文切换到指定进程的上下文,以便能够访问该进程的内存。这个函数也只能在内核态中调用。

KeStackAttachProcess(ProcessObject, &ApcState);

RtlAllocateMemory函数在当前进程的内存空间中分配一块缓冲区,用于存储从指定进程中读取的数据。这个函数是Windows操作系统内核中用于动态分配内存的函数,其中第一个参数TRUE表示允许操作系统在分配内存时进行页面合并,以减少内存碎片的产生。第二个参数nSize表示需要分配的内存空间的大小。如果分配失败,就需要将之前的操作撤销并返回错误状态。

PVOID pTempBuffer = RtlAllocateMemory(TRUE, nSize);
if (pTempBuffer == NULL) {
   
   
    KeUnstackDetachProcess(&ApcState);
    ObDereferenceObject(ProcessObject);
    return STATUS_NO_MEMORY;
}

SafeCopyMemory_R3_to_R0函数将指定进程的内存数据拷贝到分配的缓冲区中。

if (!SafeCopyMemory_R3_to_R0(ModuleBase, pTempBuffer, nSize)) {
   
   
    RtlFreeMemory(pTempBuffer);
    KeUnstackDetachProcess(&ApcState);
    ObDereferenceObject(ProcessObject);
    return STATUS_UNSUCCESSFUL;
}

最后,将缓冲区中的数据转换为BYTE类型的指针,并将其输出。需要注意的是,在返回之前需要先将当前进程的执行上下文切换回原先的上下文。

BYTE* data = (BYTE*)pTempBuffer;
KeUnstackDetachProcess(&ApcState);
ObDereferenceObject(ProcessObject);
return data;

如上实现细节用一段话总结,首先PsLookupProcessByProcessId得到进程EProcess结构,并KeStackAttachProcess附加进程,声明pTempBuffer指针用于存储RtlAllocateMemory开辟的内存空间,nSize则代表读取应用层进程数据长度,ModuleBase则是读入进程基址,调用SafeCopyMemory_R3_to_R0即可将应用层数据拷贝到内核空间,并最终BYTE* data转为BYTE字节的方式输出。这样就完成了将指定进程的内存数据拷贝到当前进程中的操作。

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

    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PEPROCESS eproc = NULL;
    KAPC_STATE kpc = {
   
    0 };

    __try
    {
   
   
        // HANDLE 进程PID
        status = PsLookupProcessByProcessId((HANDLE)4556, &eproc);

        if (NT_SUCCESS(status))
        {
   
   
            // 附加进程
            KeStackAttachProcess(eproc, &kpc);

            // -------------------------------------------------------------------
            // 开始映射
            // -------------------------------------------------------------------

            // 将用户空间内存映射到内核空间
            PVOID pTempBuffer = NULL;
            ULONG nSize = 0x1024;
            ULONG_PTR ModuleBase = 0x0000000140001000;

            // 分配内存
            pTempBuffer = RtlAllocateMemory(TRUE, nSize);
            if (pTempBuffer)
            {
   
   
                // 拷贝数据到R0
                status = SafeCopyMemory_R3_to_R0(ModuleBase, (ULONG_PTR)pTempBuffer, nSize);
                if (NT_SUCCESS(status))
                {
   
   
                    DbgPrint("[*] 拷贝应用层数据到内核里 \n");
                }

                // 转成BYTE方便读取
                BYTE* data = pTempBuffer;

                for (size_t i = 0; i < 10; i++)
                {
   
   
                    DbgPrint("%02X \n", data[i]);
                }
            }

            // 释放空间
            RtlFreeMemory(pTempBuffer);

            // 脱离进程
            KeUnstackDetachProcess(&kpc);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
   
   
        Driver->DriverUnload = UnDriver;
        return STATUS_SUCCESS;
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

代码运行后即可将进程中0x0000000140001000处的数据读入内核空间并输出:

3.5.2 内核层映射到应用层

与上方功能实现相反SafeCopyMemory_R0_to_R3函数则用于将一个内核层中的缓冲区写出到应用层中,SafeCopyMemory_R0_to_R3函数接收源地址SrcAddr、要复制的数据长度Length以及目标地址DstAddr作为参数,其写出流程可总结为如下步骤:

  • 1.使用IoAllocateMdl函数分别为源地址SrcAddr和目标地址DstAddr创建两个内存描述列表(MDL)。

  • 2.使用MmBuildMdlForNonPagedPool函数,将源地址的MDL更新为描述非分页池的虚拟内存缓冲区,并更新MDL以描述底层物理页。

  • 3.通过两次调用MmGetSystemAddressForMdlSafe函数,分别获取源地址和目标地址的指针,即pSrcMdlpDstMdl

  • 4.使用MmProbeAndLockPages函数以写入方式锁定用户空间中pDstMdl指向的地址,并将它的虚拟地址映射到物理内存页,从而确保该内存页在复制期间不会被交换出去或被释放掉。

  • 5.然后使用MmMapLockedPagesSpecifyCache函数将锁定的用户空间内存页映射到内核空间,并返回内核空间中的虚拟地址。

  • 6.最后使用RtlCopyMemory函数将源地址的数据复制到目标地址。

  • 7.使用MmUnlockPages函数解除用户空间内存页的锁定,并使用MmUnmapLockedPages函数取消内核空间与用户空间之间的内存映射。

  • 8.最后释放源地址和目标地址的MDL,使用IoFreeMdl函数进行释放。

内存拷贝SafeCopyMemory_R0_to_R3函数,函数首先分配源地址和目标地址的MDL结构,然后获取它们的虚拟地址,并以写入方式锁定目标地址的MDL,最后使用RtlCopyMemory函数将源地址的内存数据拷贝到目标地址。拷贝完成后,函数解锁目标地址的MDL,并返回操作状态。

封装代码SafeCopyMemory_R0_to_R3()功能如下:

// 分配内存
void* RtlAllocateMemory(BOOLEAN InZeroMemory, SIZE_T InSize)
{
   
   
    void* Result = ExAllocatePoolWithTag(NonPagedPool, InSize, 'lysh');
    if (InZeroMemory && (Result != NULL))
        RtlZeroMemory(Result, InSize);
    return Result;
}

// 释放内存
void RtlFreeMemory(void* InPointer)
{
   
   
    ExFreePool(InPointer);
}

/*
将内存中的数据复制到R3中

SrcAddr  R0要复制的地址
DstAddr  返回R3的地址
Size     拷贝长度
*/
NTSTATUS SafeCopyMemory_R0_to_R3(PVOID SrcAddr, PVOID DstAddr, ULONG Size)
{
   
   
    PMDL  pSrcMdl = NULL, pDstMdl = NULL;
    PUCHAR pSrcAddress = NULL, pDstAddress = NULL;
    NTSTATUS st = STATUS_UNSUCCESSFUL;

    // 分配MDL 源地址
    pSrcMdl = IoAllocateMdl(SrcAddr, Size, FALSE, FALSE, NULL);
    if (!pSrcMdl)
    {
   
   
        return st;
    }

    // 该 MDL 指定非分页虚拟内存缓冲区,并对其进行更新以描述基础物理页。
    MmBuildMdlForNonPagedPool(pSrcMdl);

    // 获取源地址MDL地址
    pSrcAddress = MmGetSystemAddressForMdlSafe(pSrcMdl, NormalPagePriority);


    if (!pSrcAddress)
    {
   
   
        IoFreeMdl(pSrcMdl);
        return st;
    }

    // 分配MDL 目标地址
    pDstMdl = IoAllocateMdl(DstAddr, Size, FALSE, FALSE, NULL);
    if (!pDstMdl)
    {
   
   
        IoFreeMdl(pSrcMdl);
        return st;
    }

    __try
    {
   
   
        // 以写入的方式锁定目标MDL
        MmProbeAndLockPages(pDstMdl, UserMode, IoWriteAccess);

        // 获取目标地址MDL地址
        pDstAddress = MmGetSystemAddressForMdlSafe(pDstMdl, NormalPagePriority);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
   
   
    }

    if (pDstAddress)
    {
   
   
        __try
        {
   
   
            // 将源地址拷贝到目标地址
            RtlCopyMemory(pDstAddress, pSrcAddress, Size);
        }
        __except (1)
        {
   
   
            // 拷贝内存异常
        }
        MmUnlockPages(pDstMdl);
        st = STATUS_SUCCESS;
    }

    IoFreeMdl(pDstMdl);
    IoFreeMdl(pSrcMdl);

    return st;
}

调用该函数实现拷贝,此处除去附加进程以外,在拷贝之前调用了ZwAllocateVirtualMemory将内存属性设置为PAGE_EXECUTE_READWRITE可读可写可执行状态,然后在向该内存中写出pTempBuffer变量中的内容,此变量中的数据是0x90填充的区域。

此处的ZwAllocateVirtualMemory函数,用于在进程的虚拟地址空间中分配一块连续的内存区域,以供进程使用。它属于Windows内核API的一种,与用户态的VirtualAlloc函数相似,但是它运行于内核态,可以分配不受用户空间地址限制的虚拟内存,并且可以用于在驱动程序中为自己或其他进程分配内存。

函数的原型为:

NTSYSAPI NTSTATUS NTAPI ZwAllocateVirtualMemory(
  _In_    HANDLE    ProcessHandle,
  _Inout_ PVOID     *BaseAddress,
  _In_    ULONG_PTR ZeroBits,
  _Inout_ PSIZE_T   RegionSize,
  _In_    ULONG     AllocationType,
  _In_    ULONG     Protect
);

其中ProcessHandle参数是进程句柄,BaseAddress是分配到的内存区域的起始地址,ZeroBits指定保留的高位,RegionSize是分配内存大小,AllocationTypeProtect分别表示内存分配类型和内存保护属性。

ZwAllocateVirtualMemory函数成功返回NT_SUCCESS,返回值为0,否则返回相应的错误代码。如果函数成功调用,会将BaseAddress参数指向分配到的内存区域的起始地址,同时将RegionSize指向的值修改为实际分配到的内存大小。

当内存属性被设置为PAGE_EXECUTE_READWRITE之后,则下一步直接调用SafeCopyMemory_R0_to_R3进行映射即可,其调用完整案例如下所示;

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

    NTSTATUS status = STATUS_UNSUCCESSFUL;
    PEPROCESS eproc = NULL;
    KAPC_STATE kpc = {
   
    0 };

    __try
    {
   
   
        // HANDLE 进程PID
        status = PsLookupProcessByProcessId((HANDLE)4556, &eproc);

        if (NT_SUCCESS(status))
        {
   
   
            // 附加进程
            KeStackAttachProcess(eproc, &kpc);

            // -------------------------------------------------------------------
            // 开始映射
            // -------------------------------------------------------------------

            // 将用户空间内存映射到内核空间
            PVOID pTempBuffer = NULL;
            ULONG nSize = 0x1024;
            PVOID ModuleBase = 0x0000000140001000;

            // 分配内存
            pTempBuffer = RtlAllocateMemory(TRUE, nSize);
            if (pTempBuffer)
            {
   
   
                memset(pTempBuffer, 0x90, nSize);

                // 设置内存属性 PAGE_EXECUTE_READWRITE
                ZwAllocateVirtualMemory(NtCurrentProcess(), &ModuleBase, 0, &nSize, MEM_RESERVE, PAGE_EXECUTE_READWRITE);
                ZwAllocateVirtualMemory(NtCurrentProcess(), &ModuleBase, 0, &nSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

                // 将数据拷贝到R3中
                status = SafeCopyMemory_R0_to_R3(pTempBuffer, &ModuleBase, nSize);
                if (NT_SUCCESS(status))
                {
   
   
                    DbgPrint("[*] 拷贝内核数据到应用层 \n");
                }
            }

            // 释放空间
            RtlFreeMemory(pTempBuffer);

            // 脱离进程
            KeUnstackDetachProcess(&kpc);
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
   
   
        Driver->DriverUnload = UnDriver;
        return STATUS_SUCCESS;
    }

    Driver->DriverUnload = UnDriver;
    return STATUS_SUCCESS;
}

拷贝成功后,应用层进程内将会被填充为Nop指令。

目录
相关文章
|
1月前
|
存储 安全 数据安全/隐私保护
Windows部署WebDAV服务并映射到本地盘符实现公网访问本地存储文件
Windows部署WebDAV服务并映射到本地盘符实现公网访问本地存储文件
251 0
|
3月前
|
存储 缓存 Java
释放C盘空间:释放Windows休眠文件和关闭虚拟内存
在 Windows 11 专业版中,可以通过以下步骤来释放休眠文件(Hibernate File),以释放磁盘空间。休眠文件是系统休眠(Hibernate)功能所需要的文件,它保存了系统的当前状态,以便在休眠状态下恢复。如果你不使用休眠功能,如果因为C盘空间不足,可以考虑释放这个文件来腾出磁盘空间。
3305 0
|
3月前
|
C++ Windows
windows下内存检测工具
windows下内存检测工具
|
29天前
|
监控 算法 Android开发
安卓应用开发中的内存优化策略
【2月更文挑战第30天】随着移动设备性能的不断提升,用户对应用程序的体验要求越来越高。在安卓应用开发中,内存管理是影响应用性能和用户体验的关键因素之一。本文将探讨针对安卓平台的内存优化技巧,包括避免内存泄漏、合理使用数据结构和算法、优化图片资源处理等策略,旨在帮助开发者提升应用性能和稳定性。
19 1
|
1月前
|
数据可视化 数据库 C++
Qt 5.14.2揭秘高效开发:如何用VS2022快速部署Qt 5.14.2,打造无与伦比的Windows应用
Qt 5.14.2揭秘高效开发:如何用VS2022快速部署Qt 5.14.2,打造无与伦比的Windows应用
|
4月前
|
监控 安全 API
5.9 Windows驱动开发:内核InlineHook挂钩技术
在上一章`《内核LDE64引擎计算汇编长度》`中,`LyShark`教大家如何通过`LDE64`引擎实现计算反汇编指令长度,本章将在此基础之上实现内联函数挂钩,内核中的`InlineHook`函数挂钩其实与应用层一致,都是使用`劫持执行流`并跳转到我们自己的函数上来做处理,唯一的不同的是内核`Hook`只针对`内核API`函数,但由于其身处在`最底层`所以一旦被挂钩其整个应用层都将会受到影响,这就直接决定了在内核层挂钩的效果是应用层无法比拟的,对于安全从业者来说学会使用内核挂钩也是很重要。
39 1
5.9 Windows驱动开发:内核InlineHook挂钩技术
|
4月前
|
缓存 C# Windows
一款.NET开源的小巧、智能、免费的Windows内存清理工具 - WinMemoryCleaner
一款.NET开源的小巧、智能、免费的Windows内存清理工具 - WinMemoryCleaner
|
4月前
|
监控 API C++
8.4 Windows驱动开发:文件微过滤驱动入门
MiniFilter 微过滤驱动是相对于`SFilter`传统过滤驱动而言的,传统文件过滤驱动相对来说较为复杂,且接口不清晰并不符合快速开发的需求,为了解决复杂的开发问题,微过滤驱动就此诞生,微过滤驱动在编写时更简单,多数`IRP`操作都由过滤管理器`(FilterManager或Fltmgr)`所接管,因为有了兼容层,所以在开发中不需要考虑底层`IRP`如何派发,更无需要考虑兼容性问题,用户只需要编写对应的回调函数处理请求即可,这极大的提高了文件过滤驱动的开发效率。
40 0
|
4月前
|
监控 Windows
7.4 Windows驱动开发:内核运用LoadImage屏蔽驱动
在笔者上一篇文章`《内核监视LoadImage映像回调》`中`LyShark`简单介绍了如何通过`PsSetLoadImageNotifyRoutine`函数注册回调来`监视驱动`模块的加载,注意我这里用的是`监视`而不是`监控`之所以是监视而不是监控那是因为`PsSetLoadImageNotifyRoutine`无法实现参数控制,而如果我们想要控制特定驱动的加载则需要自己做一些事情来实现,如下`LyShark`将解密如何实现屏蔽特定驱动的加载。
32 0
7.4 Windows驱动开发:内核运用LoadImage屏蔽驱动
|
存储 Windows
《windows核心编程》–Windows内存体结构(二)
13.6页面保护属性     内存页面保护属性有 PAGE_NOACCESS、PAGE_READONLY、PAGE_READWRITE、PAGE_EXECUTE、PAGE_EXECUTE_READ、PAGE_EXECUTE_READWRITE、PAGE_WRITECOPY、PAGE_EXECUTE_WRITECOPY。
911 0

热门文章

最新文章