属于是没想到,我会因为一个API写一篇博客。没办法,它的坑实在是太大了,让我惊叹于微软文档工程师的“牛逼”。
起因是,我想跨进程的对一个程序的代码段做修改。代码段通常是可读可执行不可写的,三环程序WriteProcessMemory可以直接修改成功,但是我Ring0调用MmCopyVirtualMemory却失败返回STATUS_PARTIAL_COPY。因为之前知道WriteProcessMemory底层调用的就是MmCopyVirtualMemory,所以认为有上层操作对它改了权限。然后逆向一波ntoskrnl
WriteProcessMemory进入零环后,调用NtWriteVirtualMemory,而NtWriteVirtualMemory仅调用了MiReadWriteVirtualMemory。而在MiReadWriteVirtualMemory中,可以看到先获取了上下文和Mode,然后对地址范围做了检查
然后可以看到通过目标进程句柄获取到对象后,就调用MmCopyVirtualMemory去写内存了。
也就是说,从 NtWriteVirtualMemory -> MiReadWriteVirtualMemory -> MmCopyVirtualMemory ,并没有看到有修改内存权限的操作。那么,为什么对同一段不具有写权限的代码段,WriteProcessMemory可以修改而MmCopyVirtualMemory却不行。
我把这个问题抛到了群里想问问WriteProcessMemory是在哪里把内存属性改掉的,但是师傅们的答案却让我很震惊。大家都是在WriteProcessMemory之前调用VirtualProtect去修改内存属性,他们甚至非常惊讶为什么我可以直接用WriteProcessMemory去修改一个不具有可写权限的内存页。 然后,有意思的来了,我们去翻了微软官方文档
可以看到,官方文档说,使用WriteProcessMemory,需要句柄拥有 PROCESS_VM_WRITE 和 PROCESS_VM_OPERATION 权限,并且目标进程地址空间是具有可写权限的。 可是我们确实使用了这个API对读可执行不可写的代码段做了修改。
然后我们又做了一些测试,发现WriteProcessMemory并不是可以对所有存在的内存页都修改成功的,比如我们测试修改一个模块的PE头(尝试把内存中ntdll的PE MagicNumber从0x4D5A改成0x0000)就失败了,非常的迷惑。
后面和存哥在三环WriteProcessMemory上下断点,虚拟机+Windbg从三环向零环跟,发现在KernelBase.dll里,有神奇操作。
惊了,KenelBase.dll的WriteProcessMemory里,竟然调了这么多API,其中有NtQueryVirtualMemory查询内存属性、有NtProtectVirtualMemory修改内存属性!以Windows10为例,说一下里面的操作流程:
- 首先调用 NtQueryVirtualMemory 查询内存信息
- 如果原内存属性可写,直接调用 NtWriteVirtualMemory 写入内存
- 如果原内存属性不可执行,则还原内存属性,然后返回 STATUS_ACCESS_VIOLATION
- 如果内存类型为 MEM_IMAGE,则修改内存属性为 OldAccessProtection | PAGE_ENCLAVE_UNVALIDATED | PAGE_EXECUTE_WRITECOPY
- 如果内存类型为 MEM_PRIVATE,则修改内存属性为 OldAccessProtection | OldAccessProtection | PAGE_EXECUTE_READWRITE
- 调用NtWriteVirtualMemory写入内存
- 还原内存属性
最后吐槽一下,这种三环的究极封装,严重影响效率就算了,文档也不改就离谱(甚至好像Windows7下就可以用WriteProcessMemory改可读可执行的内存了)
参考资料: