这篇文章的内容有的读者可能有了解过,但对我来说这是一个全新的发现。而且我感觉它有些让人匪夷所思,所以这里来分享给大家,它可能对大家有用。我是在编写一个POC的时候发现的这种情况。
这篇文章的核心思想简单来说是,Win32 API函数:WriteProcessMemory可以写入可执行PAGE_EXECUTE
和可执行可读PAGE_EXECUTE_READ
属性的页面,当然前提是你有相应的权限(译注:按照常识来说,可执行的内存必定不可写,可写的内存必定不可执行)。我想在开头强调,这不用绕过任何内置安全功能,也不需要利用任何东西,这只是一个方便的技巧。
首先我会介绍它是如何工作的,最后来解释为什么这样。
第一部分-How
这是WriteProcessMemory函数在最新版本的Windows1803上的实现。
如图所示,在函数内部首先会调用NtQueryVirtualMemory获得region的属性。
然后检查该内存页是否具有以下某个属性
PAGE_NOACCESS(0x1) | PAGE_READONLY(0x2) | PAGE_EXECUTE (0x10) | PAGE_EXECUTE_READ (0x20)
这是按位进行检查的,可以得出0xcc就是对以上标志的检查
0xcc = 1100 1100
0x1 = 0000 0001
0x2 = 0000 0010
0x10 = 0001 0000
0x20 = 0010 0000
在执行test指令之后,如果其中存在任一flag,它将设置ZF标志。 否则会直接进入NtWriteVirtualMemory调用,也就是说内存页具有WRITE位。
如果前面设置了某一个flag值,则会进行接下来的检查
如果设置了PAGE_NOACCESS或PAGE_READONLY则会跳转,并且我们会按预期被拒绝访问:
否则,如果不具有这两个flag会进行另外两个检查
如果页面是MEM_IMAGE(0x1000000)和MEM_PRIVATE(0x20000)属性 它将设置一个EAX值。如果内存页这两个属性都不具有,那就会走到ACCESS_DENIED处理例程中了。
这个EAX值最终在RSI中传递给NtProtectVirtualMemory:
现在内存具有以下这些标志:
0x40 - PAGE_EXECUTE_READWRITE
0x80 - PAGE_EXECUTE_WRITECOPY
0x20000000 - MEM_LARGE_PAGES(大页面支持)
这意味着操作系统会将页面保护更改为可写,而不是直接拒绝访问。 如果它是一个图像,它会将其设置为写入复制标志,这意味着它将创建进程图像的私有副本,因此它不会覆盖共享内存。
在此之后,会同样调用NtWriteVirtualMemory,如上所示。 最后,页面保护将恢复为原始值。 因此我们通过win api获得了对EXECUTABLE页面的写访问权限, 但是只有当我们的进程有相应的权限时才会这样,所以它没有去绕过任何保护。
在旧版本的Windows 10上,该功能略有不同,但逻辑完全相同:
在Windows 7和8上,该行为同样存在,但功能逻辑不同。 它会尝试将内存设置为PAGE_EXECUTE_READWRITE,如果失败就尝试设置到PAGE_READWRITE:
然后它检查旧保护是否为PAGE_EXECUTE_READWRITE、PAGE_READWRITE或PAGE_WRITECOPY。
如果是,会继续并恢复原始保护(因为原属性就是可写)并写入它。
如果不是,会检查它是否是PAGE_NOACCESS |PAGE_READONLY。 具有这些标志的话,会返回ACCESS_DENIED。否则当页面保护设置为PAGE_EXECUTE_READWRITE / PAGE_READWRITE时,它将调用NtWriteVirtualMemory。又是一个可以对EXECUTABLE页面进行写访问的机会。
在ReactOS中也存在类似的实现:
https://github.com/mirror/reactos/blob/master/reactos/dll/win32/kernel32/client/proc.c
当然,我们也可以自己来设置页面的属性,但是操作系统的这种行为让我们开发EXP更方便了。但是根据MSDN的描述,这些操作并不合法,MSDN是这么说的:
PAGE_EXECUTE - 0x10 - 允许对COMMIT的内存页进行execute访问。 尝试write会导致访问冲突。
PAGE_EXECUTE_READ - 0x20 - 允许对COMMIT的内存页进行read和execute访问。尝试写入已提交的区域会导致访问冲突。
如果我们直接调用NtWriteVirtualMemory它会返回失败,因为页面保护没有被修改:
0x8000000D - STATUS_PARTIAL_COPY - 由于保护冲突,并非所有请求的字节都可以复制。
第二部分-Why
我联系了微软为什么会存在这种特性,微软的解释是这是为调试器提供的功能,如果调试器需要写入内存,他们可以简单地调用这个API,而不必每次都关心页面保护的情况。以下是详细信息:
https://blogs.msdn.microsoft.com/oldnewthing/20120808-00/?p=6913
简而言之就是:
有许多函数允许操作其他进程的地址空间,比如WriteProcessMemory和VirtualAllocEx。他们可能的合法使用途径是什么?为什么一个进程需要操作另一个进程的地址空间呢,这有什么好处?
这些函数是为调试器而设计的。例如,当你需要用调试器查看被调试的进程的内存时,它就会使用ReadProcessMemory来执行此操作。同样,当你需要用调试器更新进程中变量的值时,它使用WriteProcessMemory来执行此操作。当你需要用调试器设置断点时,它使用VirtualProtectEx函数将你的代码页从read-execute更改为read-write-execute,以便它可以将int 3加入到程序中。
如果需要调试器进入进程,可以使用CreateRemoteThread函数将一个线程注入到立即调用DebugBreak的进程中。 (随后添加了DebugBreakProcess以使其更简单。)
但对于通用编程,这些功能实际上没有多少有效用途。
因此它们往往被用于恶意目的,如DLL注入和游戏外挂作弊之中。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-12-29 14:21
被Ox9A82编辑
,原因: