首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
编程技术
发新帖
5
31
[原创]实现简易ARK工具(2) 切CR3进程读写
发表于: 2025-7-13 22:39
4208
[原创]实现简易ARK工具(2) 切CR3进程读写
X66iaM
2025-7-13 22:39
4208
[TOC] 本文主要讲32位分页的一些格式和内核附加读写的方法 ## 前置知识 ### 过渡:从分段到分页 分页"废除了"分段 来看windbg拿到的GDT表,没学过的看下前文 [实现简易ARK工具(1) GDT表](https://bbs.kanxue.com/thread-287572.htm)  ```plain Sel Base Limit Type P Si Gr Pr Lo Flags ---- -------- -------- ---------- - -- -- -- -- -------- //关注前面四个"平坦化的段" 0008 00000000 ffffffff Code RE Ac 0 Bg Pg P Nl 00000c9b ; 内核代码段 0010 00000000 ffffffff Data RW Ac 0 Bg Pg P Nl 00000c93 ; 内核数据段 0018 00000000 ffffffff Code RE Ac 3 Bg Pg P Nl 00000cfb ; 用户代码段 0020 00000000 ffffffff Data RW Ac 3 Bg Pg P Nl 00000cf3 ; 用户数据段 //后面还有一些特殊功能段 我也不懂~ 无视吧 0028 80b93c00 000020ab TSS32 Busy 0 Nb By P Nl 0000008b ; 当前TSS 0050 80b95d20 00000067 TSS32 Avl 0 Nb By P Nl 00000089 ; 备用TSS 0058 80b95cb0 00000067 TSS32 Avl 0 Nb By P Nl 00000089 ; 备用TSS 0030 84d26000 00004fff Data RW Ac 0 Bg By P Nl 00000493 ; 处理器控制区域段 (PCR) 0038 00000000 0000ffff Data RW Ac 3 Bg By P Nl 000004f3 ; 16位兼容段 0040 00000400 0000ffff Data RW 3 Nb By P Nl 000000f2 ; DOS兼容段 0070 80b93800 0000003f Data RW 0 Nb By P Nl 00000092 ; 内核数据结构 ``` 前面四个段实现了windows的平坦内存模型,他们的共性: 1. 所有段基址为0:`Base` = 00000000 2. 所有段界限为4GB:`Limit` = ffffffff 3. 粒度为页:`Gr` = Pg (4KB粒度) 4. 32位段:`Si` = Bg (Big, 32位) ```plain 开启分页后: 1. 程序使用32位虚拟地址 (如: 0x00401000) 2. CPU自动加上段基址: 0x00000000 + 0x00401000 = 0x00401000 3. 得到线性地址: 0x00401000 4. 通过分页机制转换为物理地址 ``` 此时看到的虚拟地址就是线性地址,选择子CS已经是0 ,分段变得"透明",所以我们平时写软件只需要关心虚拟内存就可以了。 ### 分页是什么 Windows有多种分页机制: **1. 32位分页** - 虚拟地址:32位(4GB虚拟地址空间) - 物理地址:32位(4GB物理内存) - 页大小:4KB或4MB - 页表结构:二级页表(PDE + PTE) - 适用:早期32位Windows系统 **2. PAE分页(物理地址扩展)** - 虚拟地址:32位(4GB虚拟地址空间) - 物理地址:36位(64GB物理内存) - 页大小:4KB或2MB - 页表结构:三级页表(PDPTE + PDE + PTE) - 适用:32位Windows支持大内存 **3. IA-32e分页(64位)** - 虚拟地址:48位(256TB虚拟地址空间) - 物理地址:52位(4PB物理内存) - 页大小:4KB、2MB或1GB - 页表结构:四级页表(PML4E + PDPTE + PDE + PTE) - 适用:64位Windows系统 **本文涉及前两种**,也就是XP,win7 相关,欸其他的等我学了再说(狗头 分页由寄存器中的三个标志位控制: + `PG`位于`CR0[31]`,是分页开关 - PG = 0: 禁用分页,使用物理地址 - PG = 1: 启用分页,虚拟地址需要转换 + `PSE`位于`CR4[4]`,控制大小页 - PSE = 0: 只支持4KB页面 - PSE = 1: 支持4KB和4MB页面 + `PAE`位于`CR4[5]`,置1可以扩展物理地址空间从32位到36位。windows32位系统开了也没用,得给微软交钱买特殊的内存条,或者改内核文件,搞得好像是他的功能似的。(PS:用windbg尝试在进入系统后把虚拟机的PG位清除,CPU直接断电关机了0.0)<a href="elink@aceK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6D9k6h3q4J5L8W2)9J5k6h3#2A6j5%4u0G2M7$3!0X3N6q4)9J5k6h3y4G2L8g2)9J5c8Y4A6Z5i4K6u0V1j5$3&6Q4x3V1k6%4K9h3&6V1L8%4N6K6i4K6u0r3N6$3W2F1x3K6u0Q4x3V1k6E0k6h3#2G2M7Y4W2Q4x3V1k6H3K9s2W2K6K9h3y4S2L8q4)9J5k6r3q4V1k6s2u0W2M7%4y4Q4x3X3c8W2P5s2c8W2L8Y4y4A6L8$3^5`.">物理地址扩展 - Win32 apps</a> + `CR3`<u>寄存器在32位标准分页模式负责存储页目录表的首地址(物理地址),会在切换进程的时候定位到对应的页目录表;在PAE模式负责存储页目录指针表的首地址(物理地址)</u> + `PS`位于页目录项的第7位,控制当前页目录项是否使用大页 - PS = 0: 使用4KB小页,需要继续查找页表 - PS = 1: 使用4MB大页,直接在页目录项中包含物理地址 - 注意:PSE是全局开关,PS是单个页目录项的控制位 由此排列组合有六种情况  **数据结构** **CR3寄存器:** ```c++ // 32位标准分页模式下的CR3 typedef union _CR3_32 { struct { ULONG Reserved1 : 3; // 0-2: 保留位 ULONG PWT : 1; // 3: 页级写直达 ULONG PCD : 1; // 4: 页级缓存禁止 ULONG Reserved2 : 7; // 5-11: 保留位 ULONG PageDirectoryBase : 20; // 12-31: 页目录基址 }; ULONG AsULong; } CR3_32, *PCR3_32; // PAE模式下的CR3 typedef union _CR3_PAE { struct { ULONG PWT : 1; // 3: 页级写直达 ULONG PCD : 1; // 4: 页级缓存禁止 ULONG Reserved : 3; // 5-7: 保留位 ULONG Reserved2 : 1; // 8: 必须为0 ULONG PDPT_Base : 23; // 9-31: PDPT基址(物理地址高23位,低9位为0) }; ULONG AsULong; } CR3_PAE, *PCR3_PAE; ``` **PDPTE** ```c++ // PAE模式下的PDPTE结构 typedef union _PDPTE_PAE { struct { ULONGLONG P : 1; // 0: 存在位 ULONGLONG Reserved1 : 2; // 1-2: 保留位(必须为0) ULONGLONG PWT : 1; // 3: 页级写直达 ULONGLONG PCD : 1; // 4: 页级缓存禁止 ULONGLONG Reserved2 : 4; // 5-8: 保留位(必须为0) ULONGLONG AVL : 3; // 9-11: 软件可用位 ULONGLONG PageDirBase : 24; // 12-35: 页目录基址(物理地址) ULONGLONG Reserved3 : 28; // 36-63: 保留位(必须为0) }; ULONGLONG AsULongLong; } PDPTE_PAE, *PPDPTE_PAE; ``` **PDE:**  ```cpp //常规模式下 struct PageDirectoryEntry { unsigned P : 1; // 0: 存在位 (Present) unsigned RW : 1; // 1: 读写位 (0=只读, 1=读写) unsigned US : 1; // 2: 用户/内核位 (0=内核, 1=用户) unsigned PWT : 1; // 3: 写缓存 (Page Write Through) 指TLB 偷懒不写了 unsigned PCD : 1; // 4: 页级缓存禁止 (Page Cache Disable) unsigned A : 1; // 5: 访问位 (Accessed) unsigned D : 1; // 6: 脏位 (Dirty) - 仅对页表项有效 unsigned PS : 1; // 7: 页大小 (Page Size) - 仅对页目录项有效 unsigned G : 1; // 8: 全局位 (Global) unsigned AVL : 3; // 9-11: 软件可用位 (Available) unsigned BASE : 20; // 12-31: 物理页首地址 }; // PAE模式下的PDE结构(64位) typedef union _PDE_PAE { struct { ULONGLONG P : 1; // 0: 存在位 ULONGLONG RW : 1; // 1: 读写位 ULONGLONG US : 1; // 2: 用户/内核位 ULONGLONG PWT : 1; // 3: 页级写直达 ULONGLONG PCD : 1; // 4: 页级缓存禁止 ULONGLONG A : 1; // 5: 访问位 ULONGLONG D : 1; // 6: 脏位 ULONGLONG PS : 1; // 7: 页大小(0=4KB, 1=2MB) ULONGLONG G : 1; // 8: 全局位 ULONGLONG AVL : 3; // 9-11: 软件可用位 ULONGLONG PageTableBase : 24; // 12-35: 页表基址或物理页基址 ULONGLONG Reserved : 28; // 36-63: 保留位(必须为0) }; ULONGLONG AsULongLong; } PDE_PAE, *PPDE_PAE; ``` **PTE:**  ```cpp // 页表项结构相同,但第7位含义不同 struct PageTableEntry { unsigned P : 1; // 0: 存在位 unsigned RW : 1; // 1: 读写位 unsigned US : 1; // 2: 用户/内核位 unsigned PWT : 1; // 3: 写缓存 unsigned PCD : 1; // 4: 页级缓存禁止 unsigned A : 1; // 5: 访问位 unsigned D : 1; // 6: 脏位 (数据已修改) unsigned PAT : 1; // 7: 页属性表索引 (Page Attribute Table) unsigned G : 1; // 8: 全局位 unsigned AVL : 3; // 9-11: 软件可用位 unsigned BASE : 20; // 12-31: 物理页首地址 }; ``` + 第2项是内存权限:没有表达对执行(e)的控制 这是设计的缺陷 早期windows上不存在不可执行的内存,给了栈溢出,代码注入等操作的空间。只能通过额外的机制和硬件方法限制执行。 + 另外,xp的gdt表地址是固定的。 ### 虚拟地址到物理地址的转换 死去的408在攻击我(呕呕呕 ) **分页情况太多,直接看Intel手册卷3吧,我这块基本都删了,写的没啥营养**,这是网上的中文版卷3链接<a href="elink@aebK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2C8k6h3y4Z5N6h3q4F1k6#2)9J5k6h3!0J5k6#2)9J5c8Y4u0Q4x3V1j5J5y4K6M7^5x3K6p5`.">IA-32架构软件开发人员指南:卷3</a> 还有前辈文章: https://bbs.kanxue.com/thread-270490.htm #### 未开启PAE,页大小为4KB时 转换需要用到页目录表 (Page Directory Table)和页表 (Page Table)。  **为什么要设计两张表?** ```plain 普通情况举例 虚拟地址: |高20位:页号| 低12位:页内偏移 | 找到物理页 在页内的具体位置 1)看图先确认 低12bits是映射到某个物理页后在页内的偏移,虚拟地址和物理地址低12位是一致的 ,那么重点在于如何通过查表找到物理页。 2)页表需要描述:地址+属性 已知物理地址32位,而一个页表项描述的是页首地址,后12bits一定为0,所以地址只需要存储高20位。 举例: 物理页首地址: 0x12345000 (后12位必为0) 页表项只需存储: 0x12345 (高20位) 节省空间: 32位 → 20位地址 + 12位属性 3)假设用1张表存储页表项 段大小: 4GB = 2^32 字节 页大小: 4KB = 2^12 字节 页数量: 4GB/4KB = 2^32/2^12 = 2^20 = 1M 页 页表项: 1M个 × 4字节 = 4MB 问题: 每个进程都需要4MB的页表,这样的开销在win32的512MB内存年代极不合理 所以设计了二级页表 4)二级页表 PDE (Page Directory Entry): 页目录索引,2^10 = 1024项 PTE (Page Table Entry): 页表索引,2^10 = 1024项 通过2级寻址 也可以描述2^20数量的页表项 大大节省空间 ``` **转换为物理地址后,如何读写物理内存** 与设备通讯或者使用内核API。 早期可以通过这个设备,和他通讯就可以操作物理内存,在XP中R3也可以打开,win10修复了。 **XP内核四个版本** XP有四种内核模式,可以将这些文件拖入ida分析内核API + ntoskrnl ;普通 + ntkrnlpa ;pa 开了物理地址拓展 + ntkrnlimp ;mp 多核cpu + ntkrpamp ;拓展且多核 ## 附加进程读写数据 附加读进程的本质是切换CR3到目标进程的页目录基址然后查它的页表 **相关结构体与API:** + `PsLookupProcessByProcessId(ProcessId, &Process)` 通过PID获取进程的EPROCESS结构指针,可以拿到进程的页目录表基址。 **EPROCESS结构体层次关系:** ```cpp // 简化的结构体关系 EPROCESS { KPROCESS Pcb; // 内核进程控制块(偏移0x0) // ... }; KPROCESS { ULONG DirectoryTableBase[2]; // 页目录表基址(CR3值) // DirectoryTableBase[0]: 用户态页目录基址 // DirectoryTableBase[1]: 内核态页目录基址(PAE模式) // ... }; // 获取页目录基址: CR3_VALUE = EPROCESS->Pcb.DirectoryTableBase[0]; ``` <u>注意这个函数会增加引用计数。</u>  + `KeStackAttachProcess(Process, &ApcState)` 附加进程,给一个EPROCESS它会切换到目标进程CR3,还会保存切换前的寄存器环境。 + `KeUnstackDetachProcess(&ApcState)` 切回自己的CR3 + `KeRaiseIrql(DISPATCH_LEVEL, &OldIrql)` 提升IRQL到DISPATCH_LEVEL + `PhysicalAdress = MmGetPhysicalAddress(BaseAddress)` 虚拟地址转物理地址 + `PVOID lpMapBase = MmMapIoSpace(PhysicalAdress, Size, MmNonCached/MmCached)` 将物理地址映射到当前内核虚拟地址空间 **思路** ```c++ 用户态应用 ↓ DeviceIoControl(CTL_ATTACH_MEM_READ/WRITE) ↓ ┌─────────────────────────────────────────────────────────────┐ │ 内核驱动处理流程 │ │ │ │ 1. 获取目标进程对象 │ │ ├─ PsLookupProcessByProcessId(ProcessId) │ │ └─ 返回 EPROCESS 结构指针 用它拿进程页目录基址 │ │ │ │ 2. 准备进程切换 │ │ ├─ KeRaiseIrql(DISPATCH_LEVEL) 提升中断级别 避免数据拷贝的时候被切CR3│ │ └─ 准备 KAPC_STATE 结构 │ │ │ │ 3. 切换到目标进程地址空间 │ │ ├─ KeStackAttachProcess(Process, &ApcState) │ │ └─ 切换 CR3 寄存器到目标进程页目录 │ │ │ │ 4. 虚拟地址转物理地址 │ │ ├─ MmGetPhysicalAddress(VirtualAddress) │ │ └─ 使用目标进程的页表进行地址转换 │ │ │ │ 5. 物理内存映射 │ │ ├─ MmMapIoSpace(PhysicalAddress, Size, CacheType) │ │ └─ 将物理地址映射到当前内核虚拟地址空间 可以避免内存权限问题 │ │ │ │ 6. 执行读写操作 │ │ ├─ 读取: RtlCopyMemory(Buffer, MappedAddr, Size) │ │ └─ 写入: RtlCopyMemory(MappedAddr, Buffer, Size) │ │ │ │ 7. 清理和恢复 │ │ ├─ MmUnmapIoSpace(MappedAddr, Size) │ │ ├─ KeUnstackDetachProcess(&ApcState) │ │ ├─ KeLowerIrql(OldIrql) │ │ └─ ObDereferenceObject(Process) │ │ │ └─────────────────────────────────────────────────────────────┘ ``` ## 为什么要使用这种方式 可以过早期的保护?(在今天看是不是也不管用了) 常规的R3读写目标进程常常使用: `OpenProcess`、 `VirtualProtectEx`、`ReadProcessMemory` `CreateRemoteThread``SetWindowsHookEx`等API。实际这些api最后会进`ntdll`调内核函数。 ```cpp // 1. 标准API方式 HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); ReadProcessMemory(hProcess, addr, buffer, size, NULL); WriteProcessMemory(hProcess, addr, buffer, size, NULL); // 2. 修改内存保护 VirtualProtectEx(hProcess, addr, size, PAGE_EXECUTE_READWRITE, &old); // 3. 远程线程注入 CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)addr, param, 0, NULL); // 4. DLL注入 SetWindowsHookEx(WH_GETMESSAGE, HookProc, hMod, threadId); ``` R0常常使用`ZwReadVirtualMemory` `ZwWriteVirtualMemory` `MmCopyVirtualMemory` `KeStackAttachProcess` `RtlCopyMemory` `KeUnstackDetachProcess`等API。 ```cpp // 1. 内核标准API ZwReadVirtualMemory(hProcess, addr, buffer, size, NULL); ZwWriteVirtualMemory(hProcess, addr, buffer, size, NULL); // 2. 内核内存复制 MmCopyVirtualMemory(srcProcess, srcAddr, dstProcess, dstAddr, size, KernelMode, &bytesRead); // 3. 进程attach方式 KeStackAttachProcess(targetProcess, &apcState); // 直接内存访问 RtlCopyMemory(buffer, targetAddr, size); KeUnstackDetachProcess(&apcState); ``` 安全软件会把大部分内核函数全部hook一遍,上述方法都会被监控。 附加读写+物理内存映射的优势:首先可以不管读写权限(自己映射可读可写),其次能绕过一些被监控的API。 `MmMapIoSpace`<font style="color:rgb(51, 51, 51);">由于系统大量使用,hook它会极大影响性能,可能会让系统卡顿,相对不容易被监控。</font> `NyMmGetPhysicalAddress`<font style="color:rgb(51, 51, 51);">可以自己实现。(偷懒</font> ```cpp PHYSICAL_ADDRESS MyMmGetPhysicalAddress(PVOID VirtualAddress) { // 直接读取页表项 ULONG_PTR cr3 = __readcr3(); // 手动解析页表...记得要对分页情况分类讨论 // 返回物理地址 } ``` **一些问题** 问1:修改CR3把页目录表切为目标进程的,那怎么跑后续自己的程序代码? 答:我们的代码在内核,不分进程,我们切换的是低2G的内存。所以读写的时候缓冲区地址一定要给内核地址(放心用systembuffer)。 问2:本章的思路是附加进程拿到CR3,为对方的物理内存映射虚拟内存,对虚拟内存进行内存拷贝,然后切回自己的CR3,如果KeStackAttachProcess之后的操作中被切了线程,会不会寄? 答:需要提升中断级别 或 屏蔽中断:CLi SLi指令来阻止线程切换,<u>注意Cli只能防止当前核心切换。</u> 问3:假设我们现在想要读0x00401000,但是目标进程没有这个虚拟内存,映射到的物理地址是0x00000000,那么会将内核地址0地址映射为一块虚拟地址。此时UI上显示的还是我们传入的00401000,这会误导使用者。 答:可以在驱动验证一下映射的物理地址是否为0:`if (PhysicalAddress.QuadPart == 0)` 在32位Windows中,内核地址空间通常在 0x80000000 以上, 内核虚拟地址会被正确映射到物理地址。`MmGetPhysicalAddress`<u>结果为0表示虚拟地址没有被映射,可能是无效虚拟地址或页面被换出(缺页)</u>。 ## 效果图和代码 Imgui有开源的内存编辑器控件,直接拿来用了。遍历进程的文章之后再写。 读个EditPlus,写内存把dos头抹了打不开了,说明读写成功。(为什么会改到文件,是不是用了映射)  **代码** ```plain //R3的 //读写目标进程内存 typedef struct PROCESS_MEM_REQ { HANDLE ProcessId; // 目标进程ID PVOID VirtualAddress; // 虚拟地址 unsigned Size; // 数据大小 //写到systembuffer 不需要在结构体定义缓冲区 }*PPROCESS_MEM_REQ; PVOID memBuffer_; DWORD memBufferSize_; DWORD memDataSize_; BOOL EnsureBufferSize(DWORD requiredSize);//可以不调 BOOL AttachReadMem(DWORD ProcessId, ULONG VirtualAddress, DWORD Size); //附加读 BOOL AttachWriteMem(DWORD ProcessId, ULONG VirtualAddress, DWORD Size);//附加写 void ClearBuffer(); //清空内存读写的缓冲区 void ArkR3::ClearBuffer() { if (memBuffer_ && memBufferSize_ > 0) { memset(memBuffer_, 0, memBufferSize_); } memDataSize_ = 0; } BOOL ArkR3::EnsureBufferSize(DWORD requiredSize) //AI写的 { if (requiredSize > 0x100000) { // 限制最大1MB Log("EnsureBufferSize: Size too large (%d bytes)", requiredSize); return FALSE; } if (memBufferSize_ >= requiredSize) { return TRUE; // 当前缓冲区已足够 } // 计算新的缓冲区大小(向上取整到4KB边界) DWORD newSize = ((requiredSize + 4095) / 4096) * 4096; PVOID newBuffer = realloc(memBuffer_, newSize); if (!newBuffer) { Log("EnsureBufferSize: Failed to allocate %d bytes", newSize); return FALSE; } memBuffer_ = newBuffer; memBufferSize_ = newSize; Log("EnsureBufferSize: Buffer resized to %d bytes", newSize); return TRUE; } //读取 //R3 : [PROCESS_MEM_REQ] → R0 → R0 : [读取数据] → R3 //发送12字节请求 接收Size字节 BOOL ArkR3::AttachReadMem(DWORD ProcessId, ULONG VirtualAddress, DWORD Size) { // 参数验证 if (ProcessId == 0 || Size == 0) { LogErr("AttachReadMem: Invalid args"); return FALSE; } if (!EnsureBufferSize(Size)) { LogErr("AttachReadMem: Failed to ensure buffer size"); return FALSE; } PROCESS_MEM_REQ req; req.ProcessId = (HANDLE)ProcessId; req.VirtualAddress = (PVOID)VirtualAddress; req.Size = Size; DWORD dwRetBytes = 0; BOOL bResult = DeviceIoControl(m_hDriver, CTL_ATTACH_MEM_READ, &req, sizeof(PROCESS_MEM_REQ), // 输入:请求结构体 memBuffer_, Size, // 输出:直接写入内部缓冲区 &dwRetBytes, NULL); if (bResult) { memDataSize_ = Size; Log("AttachReadMem: PID=%d, Addr=0x%08X, Size=%d - Success", ProcessId, VirtualAddress, Size); return TRUE; } else { Log("AttachReadMem: DeviceIoControl failed, PID=%d, Addr=0x%08X, Size=%d", ProcessId, VirtualAddress, Size); return FALSE; } } //写入: //R3 : [PROCESS_MEM_REQ] [写入数据] → R0 → 处理完成 //发送12 + Size字节 不需要返回数据 BOOL ArkR3::AttachWriteMem(DWORD ProcessId, ULONG VirtualAddress, DWORD Size) { if (ProcessId == 0 || VirtualAddress == 0 || Size == 0) { Log("AttachWriteMem: Invalid parameters"); return FALSE; } if (memDataSize_ < Size) { Log("AttachWriteMem: Not enough data in buffer, available: %d, required: %d", memDataSize_, Size); return FALSE; } DWORD totalSize = sizeof(PROCESS_MEM_REQ) + Size; PVOID pBuffer = malloc(totalSize); if (!pBuffer) { Log("AttachWriteMem: Failed to allocate buffer, size: %d", totalSize); return FALSE; } PPROCESS_MEM_REQ req = (PPROCESS_MEM_REQ)pBuffer; req->ProcessId = (HANDLE)ProcessId; req->VirtualAddress = (PVOID)VirtualAddress; req->Size = Size; // 从内部缓冲区复制数据到请求缓冲区 memcpy((PUCHAR)pBuffer + sizeof(PROCESS_MEM_REQ), memBuffer_, Size); DWORD dwRetBytes = 0; BOOL bResult = DeviceIoControl(m_hDriver, CTL_ATTACH_MEM_WRITE, pBuffer, totalSize, NULL, 0, &dwRetBytes, NULL); if (bResult) { Log("AttachWriteMem: PID=%d, Addr=0x%08X, Size=%d - Success", ProcessId, VirtualAddress, Size); } else { Log("AttachWriteMem: PID=%d, Addr=0x%08X, Size=%d (dwRetBytes=%d)", ProcessId, VirtualAddress, Size, dwRetBytes); } free(pBuffer); return bResult; } ``` ```plain //R0的 NTSTATUS AttachReadVirtualMem(HANDLE ProcessId, PVOID BaseAddress, PVOID Buffer, unsigned ReadBytes); NTSTATUS AttachWriteVirtualMem(HANDLE ProcessId, PVOID BaseAddress, PVOID Buffer, unsigned WriteBytes); NTSTATUS AttachReadVirtualMem(HANDLE ProcessId, PVOID BaseAddress, PVOID Buffer, unsigned ReadBytes) { PEPROCESS Process = NULL; NTSTATUS Status = STATUS_UNSUCCESSFUL; KAPC_STATE ApcState = { 0 }; PHYSICAL_ADDRESS PhysicalAddress = { 0 }; KIRQL OldIrql = 0; Status = PsLookupProcessByProcessId(ProcessId, &Process); if (!NT_SUCCESS(Status)) { KdPrint(("[test] AttachReadVirtualMem PsLookupProcessByProcessId Status:%08X\n", Status)); return Status; } KdPrint(("[test] AttachReadVirtualMem PEPROCESS:%p\n", Process)); KeRaiseIrql(DISPATCH_LEVEL, &OldIrql); KeStackAttachProcess(Process, &ApcState);//切换CR3 PhysicalAddress = MmGetPhysicalAddress(BaseAddress); KdPrint(("[test] AttachReadVirtualMem PhysicalAddress: 0x%08X\n", PhysicalAddress.LowPart)); PVOID lpMapBase = MmMapIoSpace(PhysicalAddress, ReadBytes, MmNonCached); if (lpMapBase != NULL) { KdPrint(("[test] AttachReadVirtualMem MmMapIoSpace lpMapBase: %p\n", lpMapBase)); RtlCopyMemory(Buffer, lpMapBase, ReadBytes); MmUnmapIoSpace(lpMapBase, ReadBytes); Status = STATUS_SUCCESS; } else { KdPrint(("[test] AttachReadVirtualMem MmMapIoSpace Failed\n")); MmUnmapIoSpace(lpMapBase, ReadBytes); return Status; } KeUnstackDetachProcess(&ApcState); KeLowerIrql(OldIrql); if (Process) { ObDereferenceObject(Process); } return Status; } NTSTATUS AttachWriteVirtualMem(HANDLE ProcessId, PVOID BaseAddress, PVOID Buffer, unsigned WriteBytes) { PEPROCESS Process = NULL; NTSTATUS Status = STATUS_UNSUCCESSFUL; KAPC_STATE ApcState = { 0 }; PHYSICAL_ADDRESS PhysicalAddress = { 0 }; KIRQL OldIrql = 0; Status = PsLookupProcessByProcessId(ProcessId, &Process); if (!NT_SUCCESS(Status)) { KdPrint(("[test] AttachWriteVirtualMem PsLookupProcessByProcessId Status:%08X\n", Status)); return Status; } KdPrint(("[test] AttachWriteVirtualMem Process:%p\n", Process)); KeRaiseIrql(DISPATCH_LEVEL, &OldIrql); KeStackAttachProcess(Process, &ApcState);//切换CR3 PhysicalAddress = MmGetPhysicalAddress(BaseAddress); //虚拟地址没有被映射为物理地址 不读了 if (PhysicalAddress.QuadPart == 0) { KdPrint(("[test] AttachReadVirtualMem VA: %p (maps to physical 0x0)\n", BaseAddress)); KeUnstackDetachProcess(&ApcState); KeLowerIrql(OldIrql); ObDereferenceObject(Process); return STATUS_INVALID_ADDRESS; } KdPrint(("[test] AttachWriteVirtualMem PhysicalAddress: 0x%08X\n", PhysicalAddress.LowPart)); PVOID lpMapBase = MmMapIoSpace(PhysicalAddress, WriteBytes, MmCached); if (lpMapBase != NULL) { RtlCopyMemory(lpMapBase, Buffer, WriteBytes); MmUnmapIoSpace(lpMapBase, WriteBytes); Status = STATUS_SUCCESS; KdPrint(("[test] AttachWriteVirtualMem MmMapIoSpace Success: %p\n", lpMapBase)); } else { KdPrint(("[test] AttachWriteVirtualMem MmMapIoSpace Failed\n")); MmUnmapIoSpace(lpMapBase, WriteBytes); return Status; } KeUnstackDetachProcess(&ApcState); KeLowerIrql(OldIrql); if (Process) { ObDereferenceObject(Process); } return Status; } NTSTATUS DispatchDeviceControl( _In_ struct _DEVICE_OBJECT* DeviceObject, _Inout_ struct _IRP* Irp) { UNREFERENCED_PARAMETER(DeviceObject); KdPrint(("[test] %s\n", __FUNCTION__)); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST; ULONG_PTR info = 0; switch (code) { case CTL_ATTACH_MEM_READ: { PPROCESS_MEM_REQ memReq = (PPROCESS_MEM_REQ)Irp->AssociatedIrp.SystemBuffer; __try { KdPrint(("[test] CTL_ATTACH_MEM_READ ProcessId:%p Address:%p Size:%d\n", memReq->ProcessId, memReq->VirtualAddress, memReq->Size)); HANDLE processId = memReq->ProcessId; PVOID VirtualAddress = memReq->VirtualAddress; unsigned Size = memReq->Size; status = AttachReadVirtualMem(processId, VirtualAddress, Irp->AssociatedIrp.SystemBuffer, Size); if (NT_SUCCESS(status)) { info = Size; KdPrint(("[test] CTL_ATTACH_MEM_READ Success\n")); } } __except (EXCEPTION_EXECUTE_HANDLER) { status = STATUS_UNSUCCESSFUL; KdPrint(("[test] CTL_ATTACH_MEM_READ exception\n")); } } break; case CTL_ATTACH_MEM_WRITE: { PPROCESS_MEM_REQ memReq = (PPROCESS_MEM_REQ)Irp->AssociatedIrp.SystemBuffer; __try { KdPrint(("[test] CTL_ATTACH_MEM_WRITE ProcessId:%p Address:%p Size:%d\n", memReq->ProcessId, memReq->VirtualAddress, memReq->Size)); //要写的数据在请求头后 PVOID writeData = (PUCHAR)Irp->AssociatedIrp.SystemBuffer + sizeof(PROCESS_MEM_REQ); status = AttachWriteVirtualMem(memReq->ProcessId, memReq->VirtualAddress, writeData, memReq->Size); if (NT_SUCCESS(status)) { info = sizeof(PROCESS_MEM_REQ); KdPrint(("[test] CTL_ATTACH_MEM_WRITE Success\n")); } } __except (EXCEPTION_EXECUTE_HANDLER) { status = STATUS_UNSUCCESSFUL; KdPrint(("[test] CTL_ATTACH_MEM_WRITE exception\n")); } } break; } Irp->IoStatus.Status = status; Irp->IoStatus.Information = info; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } ``` 待续:如何在内核中遍历进程...
回复或点赞可查看完整内容
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于
2025-7-31 22:47 被X66iaM编辑 ,原因:
#基础知识
#系统内核
#驱动开发
收藏
・
5
点赞
・
31
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
git_51951meggadf3df
非常支持你的观点!
4天前
mb_lthgjpwj
为你点赞!
2026-8-21 08:54
耶巴蒂
非常支持你的观点!
2026-8-4 10:28
wx_晨梦
为你点赞!
2026-7-16 10:40
马来
非常支持你的观点!
2026-5-7 20:11
npc0vo
非常支持你的观点!
2026-4-26 01:46
lihuacl
你的帖子非常有用,感谢分享!
2026-2-8 17:07
司空绝伦
非常支持你的观点!
2025-12-20 10:12
黑手鱼
+2
感谢你的贡献,论坛因你而更加精彩!
2025-11-25 16:10
小柯南
为你点赞!
2025-11-20 23:14
qq喷火龙
这个讨论对我很有帮助,谢谢!
2025-9-16 13:26
CreateFile
你的分享对大家帮助很大,非常感谢!
2025-9-7 14:36
水朝夕
期待更多优质内容的分享,论坛有你更精彩!
2025-8-30 23:37
MarsW
感谢你的积极参与,期待更多精彩内容!
2025-8-27 00:10
我不是深渊
感谢你的积极参与,期待更多精彩内容!
2025-8-24 09:11
岁月。
感谢你分享这么好的资源!
2025-7-23 14:34
咖啡_741298
你的帖子非常有用,感谢分享!
2025-7-21 22:31
appview
你的分享对大家帮助很大,非常感谢!
2025-7-18 14:19
Elice
谢谢你的细致分析,受益匪浅!
2025-7-18 09:58
ldljlzw
感谢你分享这么好的资源!
2025-7-18 09:40
outed
谢谢你的细致分析,受益匪浅!
2025-7-16 22:49
0x指纹
你的帖子非常有用,感谢分享!
2025-7-16 09:20
git_46183EBalloon
这个讨论对我很有帮助,谢谢!
2025-7-16 05:29
kuang110
你的帖子非常有用,感谢分享!
2025-7-15 14:22
DIO-HOHO
为你点赞!
2025-7-15 09:23
ONewTach
感谢你的积极参与,期待更多精彩内容!
2025-7-15 09:18
边缘
谢谢你的细致分析,受益匪浅!
2025-7-14 20:38
道友请留步
期待更多优质内容的分享,论坛有你更精彩!
2025-7-14 15:08
TkBinary
感谢你的积极参与,期待更多精彩内容!
2025-7-14 09:55
huangyalei
谢谢你的细致分析,受益匪浅!
2025-7-14 00:07
蒙奇·D
你的帖子非常有用,感谢分享!
2025-7-13 23:30
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
14
)
X66iaM
雪 币:
1049
活跃值:
(3441)
能力值:
( LV3,RANK:30 )
在线值:
发帖
8
回帖
33
粉丝
19
关注
私信
X66iaM
2
楼
分页体系好庞大,讲的废话有点多了,对照intel卷三辩证的看
最后于
2025-7-14 04:34 被X66iaM编辑 ,原因:
上传的附件:
INTEL开发手册卷3(中文版).pdf
(1.99MB,18次下载)
2025-7-13 23:00
0
李翔_702165
雪 币:
0
活跃值:
(903)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
14
粉丝
0
关注
私信
李翔_702165
3
楼
感谢分享
2025-7-14 21:41
0
mb_llakvlta
雪 币:
25
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
45
粉丝
0
关注
私信
mb_llakvlta
4
楼
感谢分享
2025-7-15 17:20
0
wx_五角星
雪 币:
10
活跃值:
(1694)
能力值:
( LV2,RANK:10 )
在线值:
发帖
2
回帖
40
粉丝
0
关注
私信
wx_五角星
5
楼
感谢分享
2025-7-16 16:49
0
£千寻不懂爱
雪 币:
414
活跃值:
(257)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
17
粉丝
0
关注
私信
£千寻不懂爱
6
楼
谢谢分享
2025-7-23 13:42
0
刻骨柔情
雪 币:
8
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
54
粉丝
0
关注
私信
刻骨柔情
7
楼
学习学习 正好要用的
2025-8-22 11:59
0
mb_vfttriqb
雪 币:
201
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
8
粉丝
0
关注
私信
mb_vfttriqb
8
楼
66666666
2025-9-4 21:07
0
byhk
雪 币:
2646
活跃值:
(8785)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
186
粉丝
1
关注
私信
byhk
9
楼
学习 谢谢分享
2025-9-19 07:49
0
魔童
雪 币:
937
活跃值:
(2377)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
113
粉丝
1
关注
私信
魔童
10
楼
666666666
2025-10-26 11:15
0
mb_dmpagumk
雪 币:
205
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
6
粉丝
0
关注
私信
mb_dmpagumk
11
楼
谢谢分享
2025-12-24 18:47
0
pipidou-J
雪 币:
211
活跃值:
(1080)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
5
粉丝
0
关注
私信
pipidou-J
12
楼
6666
2026-5-4 19:02
0
pipidou-J
雪 币:
211
活跃值:
(1080)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
5
粉丝
0
关注
私信
pipidou-J
13
楼
6666
2026-5-4 19:03
0
mb_oysbcaej
雪 币:
0
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
25
粉丝
0
关注
私信
mb_oysbcaej
14
楼
666
2026-8-14 17:08
0
Fzzz
雪 币:
3584
活跃值:
(265)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
62
粉丝
0
关注
私信
Fzzz
15
楼
分页讲得很清楚,CR3切换思路很有收获
2026-8-25 01:45
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
X66iaM
8
发帖
33
回帖
30
RANK
关注
私信
他的文章
[原创]实现简易ARK工具(5) x64信息快查
4588
[原创]实现简易ARK工具(4) SSDT Hook
4057
[原创]实现简易ARK工具(3) 遍历进程和内核模块
9470
[原创]实现简易ARK工具(2) 切CR3进程读写
4208
[原创]实现简易ARK工具(1) GDT表
7100
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部