首页
社区
课程
招聘
[原创]Windows内存放血篇,突破物理内存的CopyOnWrite
发表于: 2017-11-25 15:10 19935

[原创]Windows内存放血篇,突破物理内存的CopyOnWrite

2017-11-25 15:10
19935
本篇以x86(开启PAE) 以及x64 Win7系统 不借助微软API突破内存的写拷贝机制进行讲述

0x01 Before Starting

1. PAE: 
       Physical Address Extension,Inter为了支持更大的物理内存寻址而设计的x86寻址方式,虚拟地址没有变化都是32位,只是描述物理内存的位数由原先的32为增加到36位,能够最多寻址 2^4 * 4GB = 64GB内存,也就意味着你机器上如果存在超过4GB的内存条,那么一般都可以被充分利用到,这只是体现在多进程多任务的性能上,并没有增加一个进程的寻址空间,仍然为4GB。微软喜欢把页面表基地址放在0xC0000000上,当发生进程切换操作时这块页表内容会随CR3引导的页面表的内容而发生改变(一般内核的高2GB不会变化太大,主要体现在低2GB内存),那么这就有规律可言,在内核情景分析中可能大家都已经见过未开启PAE的几个公式:

 1) 未开启PAE状态下 (10/10/12)
          PTE = (VA >> 12) << 2 + PTE_BASE
          PDE = (VA >> 22) << 2 + PTE_BASE
          因为 PDE_BASE 是描述PTE_BASE的PTE
          显然  PDE_BASE = (PTE_BASE >> 12) << 2 + PTE_BASE = (0xC0000000 >> 12) << 2 +  0xC0000000 = 0xC0300000

那么自己推导下PAE下的计算方式
2) 开启PAE状态下 (2/9/9/12)

          PTE = (VA >> 12) << 3 + PTE_BASE

          PDE = (VA >> 21) << 3 + PTE_BASE

          PDPE = (VA >> 30) << 3 + PDE_BASE

          因为 PDE_BASE 是描述PTE_BASE的PTE

          显然 PDE_BASE = (PTE_BASE >> 12) << 3 + PTE_BASE = (0xC0000000 >> 12) << 3 + 0xC0000000 = 0xC0600000


2. x64 公式推导

WRK或者WDK开发包头文件中定义了64位下 PTE_BASE 的内容

#define PTE_BASE  0xFFFFF68000000000UI64
#define PPE_BASE  0xFFFFF6FB7DA00000UI64
#define PDE_BASE  0xFFFFF6FB40000000UI64
#define PXE_BASE  0xFFFFF6FB7DBED000UI64



自然,这几个值看起来都是固定了,其实是因为PTE_BASE固定的,才有个下面这几个固定的值,计算方式如下:

PDE_BASE = ((PTE_BASE & 0x0000FFFFFFFFF000) >> 12) * 8 + PTE_BASE

                    = 0xF68000000 * 8 + PTE_BASE 

                    = 0x7B40000000 + PTE_BASE = 0xFFFFF6FB40000000
PPE_BASE = ((PDE_BASE & 0x0000FFFFFFFFF000) >> 12) * 8 + PTE_BASE 

                    = 0xF6FB40000 * 8 + PTE_BASE = 0x7B7DA00000 + PTE_BASE

                    = 0xFFFFF6FB7DA00000
PXE_BASE = ((PPE_BASE & 0x0000FFFFFFFFF000) >> 12) * 8 + PTE_BASE 

                    = 0xF6FB7DA00 * 8 + PTE_BASE 

                    = 0x7B7DBED000 + PTE_BASE = 0xFFFFF6FB7DBED000


在PAE开启状态下 (下文默认) 或者x64系统下,描述PTE结构的定义为:

typedef struct _MMPTE_HARDWARE {
    ULONGLONG Valid : 1;
    ULONGLONG Write : 1;        // UP version
    ULONGLONG Owner : 1;
    ULONGLONG WriteThrough : 1;
    ULONGLONG CacheDisable : 1;
    ULONGLONG Accessed : 1;
    ULONGLONG Dirty : 1;
    ULONGLONG LargePage : 1;
    ULONGLONG Global : 1;
    ULONGLONG CopyOnWrite : 1; // software field
    ULONGLONG Prototype : 1;   // software field
    ULONGLONG reserved0 : 1;  // software field
    ULONGLONG PageFrameNumber : 28;
    ULONG64 reserved1 : 24 - (_HARDWARE_PTE_WORKING_SET_BITS+1);
    ULONGLONG SoftwareWsIndex : _HARDWARE_PTE_WORKING_SET_BITS;
    ULONG64 NoExecute : 1;
} MMPTE_HARDWARE, *PMMPTE_HARDWARE;

typedef struct _MMPTE {
    union  {
        //ULONG_PTR Long;
        MMPTE_HARDWARE Hard;
        //MMPTE_HARDWARE_LARGEPAGE HardLarge;
        //HARDWARE_PTE Flush;
        //MMPTE_PROTOTYPE Proto;
        //MMPTE_SOFTWARE Soft;
        //MMPTE_TRANSITION Trans;
        //MMPTE_SUBSECTION Subsect;
        //MMPTE_LIST List;
        } u;
} MMPTE;

typedef MMPTE *PMMPTE;


0x02 Physical Memory Patch

实际上这个ULONGLONG CopyOnWrite : 1; // software field我并没有看出什么玄机,重点是这个ULONGLONG Write : 1;        // UP version

找到虚拟地址对应的PTE项,将Write位置为1,自然这块内存就不再为写拷贝了,看Inter手册上对这个字段的描述也不是特别的清楚,下图为2MB的大页面对应的结构,跟4KB的小页面也差不了多少,对R/W字段的描述也不是很明显,只是WRK/Win2000上的这个software field的3个字段全部为Ignored... 


这个位起着的作用看上去不是只有一个可写属性,当我写一个Dll让一个目标进程去Load然后用这种方式把他的PE头给Patch了之后,达到了与MDL修改物理内存一样的效果(MDL其实也是一个突破CopyOnWrite的一个方法),以后这个进程再也加载不起来这个Dll了,因为原始的物理页已经被修改了。
typedef struct tag_CTRLV2
{
	PVOID lpAddress;
	PVOID lpPatchContext;
	ULONG ulSize;

} CtrlV2, *PCtrlV2;

BOOLEAN ModifyPhysicalAddressX86(PCtrlV2 pV2)
{
	if (g_bPAEON)
	{
		PMMPTE_PAE ProtectPTE = MiGetPteAddressForPAE(pV2->lpAddress);
		__try
		{
			if (ProtectPTE->Valid)
			{
				// Disable CopyOnWrite
				ProtectPTE->Write = 1;
				// Now Patch Physical Memory
				memcpy(pV2->lpAddress, pV2->lpPatchContext, pV2->ulSize);

				DbgPrint("[Wxoit] ModifyPhysicalAddressX86 pV2->lpAddress:%x, Context:%x\r\n", 
					pV2->lpAddress, *(ULONG*)pV2->lpAddress);
			}
		}
		__except(EXCEPTION_EXECUTE_HANDLER)
		{
			DbgPrint("[Wxoit] ModifyPhysicalAddressX86 Raise Exception %x", GetExceptionCode());
		}
	}

	return TRUE;
}

第一次加载NopDll.dll 并Patch NopDll.dll 的PE DOS_SIGNATURE。


第二次加载NopDll.dll时,发现这个Dll已经是一个bad exe format


当然这个方法,我也给大家支持了64位,但是警告大家不要去随意搞系统的内存,出问题本人概不负责... 
代码写的比较急,没有支持跨进程操作物理内存,大家如果想做只要KeStackAttachProcess下就OK了,
代码在最后的附件中

0x02 Things of MDL

最后就当作福利吧,前段时间在看MDL的一些API,把我所学分享给大家。

IoAllocateMdl
MmProbeAndLockPages/MmBuildMdlForNonPagedPool
MmMapLockedPagesSpecifyCache

MDL不止只有下面描述的结构,在这个结构的后面还存在着这个MDL描述的所有的物理页的页面帧号
typedef struct _MDL {
    struct _MDL *Next;
    CSHORT Size;
    CSHORT MdlFlags;
    struct _EPROCESS *Process;
    PVOID MappedSystemVa;
    PVOID StartVa;
    ULONG ByteCount;
    ULONG ByteOffset;
} MDL, *PMDL;

1. IoAllocateMdl
PMDL
IoAllocateMdl(
    IN PVOID VirtualAddress,
    IN ULONG Length,
    IN BOOLEAN SecondaryBuffer,
    IN BOOLEAN ChargeQuota,
    IN OUT PIRP Irp OPTIONAL
    )
       这个API没啥好说的,就是小心点大小检测,当传入的Length越过了0x17个页面时,对MDL的大小有要求(不能超过0xFFFF),第三参数只有在第五参数存在时才有意义:标志这个是不是一个链式内存(一般只有在IRP结构中需要处理),第四参数没看到在哪用。一般地,三四五参数都传NULL。

2.  MmProbeAndLockPages
         VOID
MmProbeAndLockPages (
     IN OUT PMDL MemoryDescriptorList,
     IN KPROCESSOR_MODE AccessMode,
     IN LOCK_OPERATION Operation
     )

好了,这个API开始就要注意了,这块特别容易抛异常
1. 进入这个函数之前,不要随便给MDL置标记(不管是你手动的还是API帮你置的位),特别是
MDL_PAGES_LOCKED 
MDL_MAPPED_TO_SYSTEM_VA
MDL_SOURCE_IS_NONPAGED_POOL
MDL_PARTIAL
MDL_IO_SPACE
2. 存在当前模式,如果传入UserMode,那么在第一步初始化MDL如果描述的虚拟地址是一个内核地址,那么这直接抛0xC0000005异常
3. 这个API紧接这会去锁住MDL描述的物理内存页面,当你传入MDL的虚拟地址是一个Ring3地址, 也会校验你传入的Operation, 其中
    一个页面不具有写属性你却传入了 IoWriteAccess/IoModifyAccess 那么不好意思,同样RaiseException
4. 检查当前进程(对是当前进程!,调用这个函数如果你要修改别人家的物理内存那么请先KeStackAttachProcess ) 的虚拟内存对应的物理
    页面映射关系,如果你尝试传入一个缺页的内存,这个函数会尝试处理这个缺页情况,再做类似第三步的动作
5. 即使找到了虚拟页面映射的物理页面,如果传入 IoWriteAccess/IoModifyAccess  也会校验对应的VAD是否具有MM_READWRITE属性

使用这个函数时,如果你要修改内存那么不必急着传入 IoWriteAccess/IoModifyAccess 这样会造成这个函数代码内部的检测逻辑,因为最
后在调用MmMapLockedPagesSpecifyCache 函数时,不管是Ring3还是Ring0应该都是具有读写属性的。在我的理解上来看.......

3.  MmBuildMdlForNonPagedPool
VOID
MmBuildMdlForNonPagedPool (
    IN OUT PMDL MemoryDescriptorList
    )

这个函数很简单,就负责置MDL的标志位以及填充页面帧号,当然也要求当前进程的页面表能够访问到的内存
MemoryDescriptorList->MdlFlags |= MDL_SOURCE_IS_NONPAGED_POOL;

4.  MmMapLockedPagesSpecifyCache
PVOID
MmMapLockedPagesSpecifyCache (
     IN PMDL MemoryDescriptorList,
     IN KPROCESSOR_MODE AccessMode,
     IN MEMORY_CACHING_TYPE CacheType,
     IN PVOID RequestedAddress,
     IN ULONG BugCheckOnFailure,
     IN MM_PAGE_PRIORITY Priority
     )

当MDL的页面帧号都填充完毕时,通过 MmMapLockedPagesSpecifyCache最后一步映射物理内存到当前进程页面表中,
不知道微软是怎么想到设计这个接口的,这个函数实在过于强大。强大不光体现在他能越过内存的CopyOnWrite机制,
而且通过 MmMapLockedPagesSpecifyCache得到的虚拟内存地址具有读写属性......

1. KernelMode 内核模式下会得到一个内核地址,我们都知道内核中申请或者Map的内存都是可读可写可执行的
2. UserMode 用户模式下Map的地址同样具有读写属性,具体实现见MiMapLockedPagesInUserSpace,在LoadImage回调下
    这个函数有进程的AddressCreationLock限制,所以在模块回调时不要用UserMode!

至少到目前为止的Windows版本都是可读写的。

说到这里,我想到某厂的驱动开发人员写了这样一段代码,看的我哭笑不得


这个人即想把MDL映射到内核地址( MDL_MAPPED_TO_SYSTEM_VA ),又使用UserMode的映射....... 局外人啊。不过
这段代码不会出什么问题,因为 MmMapLockedPagesSpecifyCache 还是先校验 AccessMode的,如果是UserMode就不会
看 MDL_MAPPED_TO_SYSTEM_VA标记了,而且这个厂商用这个方法 Patch 动态库让动态库无法加载,实在让人深恶痛
绝,因为改了物理内存,所有进程都加载不了这个动态库了。

而且从时间上的观察来看,这个厂商甚至不知道这些函数干了些啥,只知道这样可以获取内存的写权限......






[课程]FART 脱壳王!加量不加价!FART作者讲授!

最后于 2018-3-16 11:53 被FaEry编辑 ,原因:
上传的附件:
收藏
免费 3
支持
分享
最新回复 (31)
雪    币: 1421
活跃值: (2428)
能力值: ( LV9,RANK:190 )
在线值:
发帖
回帖
粉丝
2
感谢分享
2017-11-25 17:25
0
雪    币: 615
活跃值: (530)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
内存加载DLL
2017-11-25 20:47
0
雪    币: 3700
活跃值: (3817)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
4
感谢分享!
2017-11-25 21:08
0
雪    币: 12848
活跃值: (9108)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
5
你软唯一指定修改用户空间内存的方法NtProtectVirtualMemory其他皆是玩蛇
2017-11-25 22:02
0
雪    币: 5039
活跃值: (2591)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
6
hzqst 你软唯一指定修改用户空间内存的方法NtProtectVirtualMemory其他皆是玩蛇
说的好
2017-11-25 22:40
0
雪    币: 23
活跃值: (13)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
hzqst 你软唯一指定修改用户空间内存的方法NtProtectVirtualMemory其他皆是玩蛇
你倒是说出个所以然啊。都是r0级,操作系统也可以打个洞从另外的通道走,不要认为微软规定的,就是唯一,这不束缚了自己和别人的手脚吗?
2017-11-26 08:00
0
雪    币: 155
活跃值: (2181)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
感谢分享
2017-11-26 11:36
0
雪    币: 8865
活跃值: (2379)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
9
其实取PFN,映射PFN,访问,撤销映射就行了。
既然是突破COW,不是带着COW。
取pfn手工
映射pfn,可以自己build pagetable
访问简单RtlCopyMemory什么的
撤销,清理build的pagetable

PS:
x64 公式推导给出的基址在Win10 新版上是不行的哦.

2017-11-26 11:50
0
雪    币: 5039
活跃值: (2591)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
10
cvcvxk 其实取PFN,映射PFN,访问,撤销映射就行了。既然是突破COW,不是带着COW。取pfn手工映射pfn,可以自己build pagetable访问简单RtlCopyMemory什么的撤销,清理bui ...
嗯,也想过自己重建PFN映射,但是怕这么做不稳定~~,Win10新版我再看看
2017-11-26 21:12
0
雪    币: 1264
活跃值: (1850)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
11
PFN  映射  貌似还得刷一次cach
2017-11-30 15:36
0
雪    币: 310
活跃值: (2227)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
mark
2017-11-30 17:24
0
雪    币: 5039
活跃值: (2591)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
13
库尔 PFN 映射 貌似还得刷一次cach
这是一方面,另一方面  虚拟地址你用哪块,是不是还得自己找,然后你如何知道当前哪块PTE是没有用过的
2017-11-30 19:01
0
雪    币: 60
活跃值: (1010)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
cvcvxk 其实取PFN,映射PFN,访问,撤销映射就行了。既然是突破COW,不是带着COW。取pfn手工映射pfn,可以自己build pagetable访问简单RtlCopyMemory什么的撤销,清理bui ...
哪个版本开始不行?
2017-11-30 22:03
0
雪    币: 719
活跃值: (777)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
15
10240以上。需要特征定位各级表
2017-12-5 03:13
0
雪    币: 965
活跃值: (89)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
mark
2017-12-9 11:38
0
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
mark
2018-1-3 23:26
0
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
18
MmGetPhysicalAddress  +  MmMapIoSpace  直接改不行?
2018-1-4 01:34
0
雪    币: 5039
活跃值: (2591)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
19



malokch

MmGetPhysicalAddress + MmMapIoSpace 直接改不行?
你既然都能MmGetPhysicalAddress了,那还有什么必要MmMapIoSpace呢,不过确实这种方法可以不用找各级表,233333,注: 开头说是不借助API
2018-1-4 11:55
0
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
20
FaEry malokch MmGetPhysicalAddress + MmMapIoSpace 直接改不行? 你既然都能MmGetPhysicalAddress ...
是我被别的资料误导了
2018-1-4 13:39
0
雪    币: 207
活跃值: (101)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
21
看学长的帖子学到了好多
2018-1-25 22:02
0
雪    币: 36
活跃值: (1021)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
22
mark
2018-1-25 22:45
0
雪    币: 9
活跃值: (165)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
23
谢谢分享
2018-1-29 12:25
0
雪    币: 204
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
你可以重新启动一下系统再加载试试看
2018-3-16 11:42
0
雪    币: 919
活跃值: (1340)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
25
MDL中的PFN才是精髓呀,感觉MDL就是简单的描述一下....最近也大概看了一下。其实有个问题,win 1024以上,都是根据特征码定位表,那么操作系统它是怎么找到的呢
2020-10-26 13:23
0
游客
登录 | 注册 方可回帖
返回
//