内存管理
使用virtualkd+windbg+winxp sp3 进行双机调试,
kd> ! process 0 0
查看所有进程
查看notepad.exe进程EPROCESS结构体
kd> dt _EPROCESS 81c502a0
0x11c VADRoot :是一个搜索二叉树的入口点 ,树的每一个节点记录了被占用的虚拟内存地址空间,这个搜索二叉树就是VAD树。
VAD的属性以及遍历
VAD是管理虚拟内存的,每一个进程有自己单独的一个VAD树
使用VirtualAllocate函数申请一个内存,则会在VAD树上增加一个结点,其是_MMVAD结构体
- StringVpn 起始页 / EndingVpn结束页
1)两者算法是不同的。起始页:startingVpn*0x1000/结束页:EndVpn*0x1000+0xfff
- Parent、LeftChild、RightChild - 其父节点、左子树、右子树。
1)我们遍历这些树时用到的就是这些结构体成员。
- u - 其是_MMVAN_FLAGS属性,非常重要的。
1)CommitCharge 实际提交的页数。
其19Bits,我们内存低字节7ffffff,正好十九位。
比如我们以MEN_RESERVED保留形式提交了4页大小的内存,此时这里为2,将一页改为EXECUTE属性,这时这里就会变成2。
2)PyhsicalMapping:内核物理页映射。
3)UserPhysicalPages:内核物理页映射。
4)PrivateMemory:如果私有设置为1。
5)ImageMap:对dll/exe等文件进行保护,防止其被修改(使用映射写拷贝之类的原理)
如果ImageMap为1,PrivateMemory为0,说明其为DLL。
6)NoChange:关于锁页技术。当置为1,像VirtualProtect等函数不会改变其页的属性。
7)LargePage:标志是否为大页。
8)MemCommit:提交状态,只要提交就会置为1(CommitCharge存储提交了多少页)
9)Protection:3bit,关于保护(比如页的读写、可执行等)。
4. ContraArea 控制结构:
其指向一个_CONTROL_AREA的数据结构。
1)_CONTROL_AREA+ 0x24 FilePointer,文件指针,指向一个_FILE_OBJECT结构体。
2)_FILE_OBJECT结构体中,保存着文件对象很多关键的信息。
a> +0x30 FileName 文件名
若想知道该页属于哪个文件,可以查看这里。
将.sys文件伪装成.dll文件,则必须修改这里。
比如一个文件被独占无法删除,在内核中你可以将DeleteAccess位置1,之后强制删除。
显示该进程的VAD树,遍历搜索
kd> !vad 0x81effea0
根结点,没有父结点
StartingVpn和这个EndingVpn是以页为单位
FilePointer
如果想知道这块线性地址到底是被谁占用的呢?可以通过ControlArea
如果FilePointer的值是NULL,那就说明这块线性地址对应的是真正的物理页(这个内存是使用VirtualAlloc 来分配的)
其他情况:
它是一个Map内存,通过文件映射的内存(这块内存通过文件映射得到的)
查看属性:
_MMVAD_FLAGS=MMVAD+0X14
+0x000 CommitCharge
+0x000 PhysicalMapping
+0x000 ImageMap //1.镜像文件 0其他
+0x000 UserPhysicalPages
+0x000 Nochange
+0x000 WriteWatch
+0x000 Protection
//1.READONLY 2.EXECUTE 3.EXECUTE_READ 4.READWITER
//5.WRITECOPY 6.EXECUTE_READWITER 7.EXECUTE_WRITECOPY
+0x000 LargePages
+0x000 MemCommit
+0x000 PrivateMemory
//1.PrivateMemory 2.Map
虚拟内存分为两类:
(1)通过VirtualAlloc/VirtualAllocEx 申请的:Private Memory ,独享物理页
(2)通过CreateFlieMapping映射的:Mapped Memory,多个进程共享物理页。
Commit:
PVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的内存区域的地址
DWORD dwSize, // 分配的大小,这个参数以字节为单位,而不是页,系统会根据这个大小一直分配到下页的边界DWORD
DWORD flAllocationType, // 分配的类型,MEM_COMMIT,MEM_RESERVE和MEM_TOP_DOWN
DWORD flProtect // 该内存的初始保护属性,可读/可写/可执行。。。。。
};
MEM_COMMIT 0x1000 | **为指定地址空间提交物理内存。**这个函数初始化内在为零试图提交已提交的内存页不会导致函数失败。这意味着您可以在不确定当前页的当前提交状态的情况下提交一系列页面。如果尚未保留内存页,则设置此值会导致函数同时保留并提交内存页。 |
---|---|
MEM_RESERVE 0x2000 | 保留指定地址空间,不分配物理内存。这样可以阻止其他内存分配函数malloc和LocalAlloc等再使用已保留的内存范围,直到它被被释放。当使用上面的VirtualAlloc函数保留了一段地址空间后,接下还你还可以继续多次调用同样的函数提交这段地址空间中的不同页面。 |
其他可能数值参考MSDN、百度百科
privateMemory
1,申请内存,测试代码
#include <windows.h>
#include <stdio.h>
LPVOID lpAddress;
int main()
{
printf("程序运行了,内存还没有申请\n");
getchar();
//要申请的内存,申请内存的线性地址;大小,以页为单位;保留还是提交,MEM_RESERVER|MEM_COMMIT;访问权限
lpAddress = VirtualAlloc(NULL, 0x1000 * 2, MEM_COMMIT, PAGE_READWRITE);
printf("申请的线性地址为:%x\n", lpAddress);
getchar();
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y5pAlK8q-1632918671221)(https://i.imgur.com/qXeREKw.png)]
2.堆栈
malloc():C 库函数 void *malloc(size_t size) 分配所需的内存空间,并返回一个指向它的指针。
new():分配内存
malloc/new最终都是调用HeapAlloc()
堆是程序启动的时候操作系统调用VirtualAlloc分配了很大一块内存,当我们使用malloc/new(调用HeapAlloc),HeapAlloc分配内存是从操作系统已分好的内存中拿来用(细分),HeapAlloc,本质没有分配内存,只是从操作系统已经分配的好的内存中取来一部分。
栈也是操作系统已经分配好的内存
测试代码:
#include <stdio.h>
#include <stdlib.h>
int x = 0x1111;
int main()
{
printf("申请内存前\n");
getchar();
int y = 0x2222;
int *z = (int*)malloc(sizeof(int) * 128);
printf("全局变量:%x\n", &x);
printf("栈:%x\n", &y);
printf("堆:%x\n", z);
getchar();
return 0;
}
申请内存后:
Mapped内存 Private内存 都是提前分配好的内存
Mapped Memory
分为两类。一类共享文件,一类共享物理页
1.共享物理页
#include <windows.h>
#include <stdio.h>
#define MapFileName "共享内存"
int main()
{
//内核对象:1、物理页 2、文件
HANDLE g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, BUFSIZ, MapFileName);
getchar();
//将物理页与线性地址进行映射
LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
*(PDWORD)g_lpBuff = 0x12345678;
printf("A进程写入地址,内容%p - %x", g_lpBuff, *(PDWORD)g_lpBuff);
getchar();
return 0;
}
所以CreateFileMapping函数并没有影响进程的线性地址空间,MapViewOfFile才将线性地址空间与物理内存映射上
#include <windows.h>
#include <stdio.h>
#define MapFileName "共享内存"
int main()
{
//内核对象:1、物理页 2、文件
HANDLE g_hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, MapFileName);
//将物理页与线性地址进行映射
LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
printf("B进程读取%x", *(PDWORD)g_lpBuff);
getchar();
return 0;
}
2.共享文件
#include <windows.h>
#include <stdio.h>
#define MapFileName "共享内存"
int main()
{
HANDLE g_hFile = CreateFile("C:\\NOTEPAD.EXE", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
//内核对象:1、物理页 2、文件
HANDLE g_hMapFile = CreateFileMapping(g_hFile, NULL, PAGE_READWRITE, 0, BUFSIZ, MapFileName);
//将物理页与线性地址进行映射
LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);
getchar();
return 0;
}
物理内存的管理
1.物理内存
(1)最大物理内存
10-10-12分页,最多识别物理内存为4GB(32位操作系统)
2-9-9-12分页,最多识别物理内存为64GB(32位操作系统)
(2)操作系统限制
为什么xp系统中,2-9-9-12分页,任然无法超越4GB是受操作系统
本身的限制
(3)实际物理内存
MmNumberOfPhysicalPages * 4 = 物理内存(K)
dd MmNumberOfPhysicalPages查看物理内存,以页为单位,4kb/页
1ff6c*4/1024=511mb
2.管理物理内存
1.全局变量(数组):
数组指针:_MMPFN * MmPfnDatabase
MmPfnDatabase
描述:
全局结构体数组,称为“页帧数据库”,包含了当前操作系统中所有的物理页
每一个物理页都有一个对应的MMPFN结构体
通过全局变量MmPfnDatabase可以找到这个结构体的起始位置(在WinDbg中使用命令dd MmPfnDatabase进行查看)
每一个MMPFN结构体之间在内存中是紧挨着的
定义:_MMPFN *MmPfnDatabase
长度:MmNumberOfPhysicalPages
大小:0x1c
kd> dt _MMPFN
nt!_MMPFN
+0x000 u1 : __unnamed
+0x004 PteAddress : Ptr32 _MMPTE
+0x008 u2 : __unnamed
+0x00c u3 : __unnamed
+0x010 OriginalPte : _MMPTE
+0x018 u4 : __unnamed
//FROM WRK v1.2
typedef struct _MMPFN {
union {
PFN_NUMBER Flink;
WSLE_NUMBER WsIndex;//该页面在进程工作集链表中的索引
PKEVENT Event;
NTSTATUS ReadStatus;
//
// Note: NextStackPfn is actually used as SLIST_ENTRY, however
// because of its alignment characteristics, using that type would
// unnecessarily add padding to this structure.
//
SINGLE_LIST_ENTRY NextStackPfn;
} u1;
PMMPTE PteAddress;//执行此页面的PTE的虚拟地址
union {
PFN_NUMBER Blink;
//
// ShareCount transitions are protected by the PFN lock.
//
ULONG_PTR ShareCount;//指向该页面的PTE数量
} u2;
union {
//
// ReferenceCount transitions are generally done with InterlockedXxxPfn
// sequences, and only the 0->1 and 1->0 transitions are protected
// by the PFN lock. Note that a *VERY* intricate synchronization
// scheme is being used to maximize scalability.
//
struct {
USHORT ReferenceCount;//代表这个页面必须要保留在内存中的引用计数
MMPFNENTRY e1;// 物理页状态
};
struct {
USHORT ReferenceCount;
USHORT ShortFlags;
} e2;
} u3;
#if defined (_WIN64)
ULONG UsedPageTableEntries;
#endif
union {
MMPTE OriginalPte;//包含了指向此页面的PTE的原始内容
LONG AweReferenceCount;
};
union {
ULONG_PTR EntireFrame;
struct {
#if defined (_WIN64)
ULONG_PTR PteFrame: 57;
#else
ULONG_PTR PteFrame: 25;
#endif
ULONG_PTR InPageError : 1;
ULONG_PTR VerifierAllocation : 1;
ULONG_PTR AweAllocation : 1;
ULONG_PTR Priority : MI_PFN_PRIORITY_BITS;
ULONG_PTR MustBeCached : 1;
};
} u4;//指向该页面的PTE所在的页表页面的物理页帧编号,以及一些标志位
} MMPFN, *PMMPFN;
!这个结构体里面并没有成员存放该结构体所对应的物理页地址
如何寻找相应的物理页所在的物理地址:物理页地址 = 当前_MMPFN索引值*0x1000
物理页状态
u3.e1.PageLocation 这个成员标识了当前物理页空闲状态(状态:空闲,使用中)
MMPFN->u3.e1
//FROM WRK v1.2
typedef struct _MMPFNENTRY {
USHORT Modified : 1;
USHORT ReadInProgress : 1;
USHORT WriteInProgress : 1;
USHORT PrototypePte: 1;
USHORT PageColor : 4;
USHORT PageLocation : 3; //决定了当前页的状态,空闲情况下分为六种状态
//0:MmZeroedPageListHead,零化
//1:MmFreePageListHead,空闲
//2:MmStandbyPageListHead,备用
//3:MmModifiedPageListHead,修改 OriginalPte.u.Soft.Prototype=1 或者 外存都会在这
//4:MmModifiedNoWritePageListHead,已修改但不写出
//5:MmBadPageListHead,坏页
USHORT RemovalRequested : 1;
USHORT CacheAttribute : 2;
USHORT Rom : 1;
USHORT ParityError : 1;
} MMPFNENTRY;
3.操作系统的六个链表
物理页处于空闲状态
<1>MmBadPageListHead 坏链
<2>MmZeroedPageListHead 零化链表:(是系统空闲时候进行零化的,不是程序自己清0的那种)
<3>MmFreePageListHead 空闲链表:(物理页是周转使用的,刚被释放的物理页没有被清0,系统空闲的时候有专门的进程从这个链表摘物理页,加以清零后挂入MmZeroedPageListHead)
<4>MmStandByPageListHead备用链表:(当系统内存不够的时候,操作系统会把物理内存中的数据交换到硬盘上,此时页面不是挂到空闲链表上,而是挂到备用链表上,虽然我释放了,但里面的内容还是有意义的)不清0
<5>MmModifiedPageListHead
以修改状态。类似于备用链表,已经从原来的工作集中移除,但是,页面包含的内容已经被修改过,原来工作集的PTE,仍然指向物理页面,但已被标记成正在转移无效的PTE。
如果系统要把这个页面回收给别人用,必须将其中的内容写到磁盘上
<6>MmModifiedNoWritePageListHead
已修改但不写出。类似于以修改状态,但区别在于,内存管理器不会将它的内容写到磁盘上
遍历零化链表
4.物理页处于被进程使用中的状态
EPROCESS+0x1f8=Vm
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mEyoHLF-1632918671240)(https://i.imgur.com/fUJV0hc.png)]
+0x020 VmWorkingSetList
这个成员可以找到这个进程使用的所有物理页
+0x018 MinimumWorkingSetSize
最小的物理页大小
+0x01c MaximumWorkingSetSize
最大的物理页大小
0x014 Wsle 物理页起始地址
0x018 LastInitializedWsle 物理页个数
缺页异常
P: 当前页面是否有效(p位是1:地址有效,p位是0:地址无效)
当CPU访问一个地址,其PTE的P位为0,此时会产生缺页异常,
Windows是只有正在被使用的线性地址才会给你挂上物理页,如果你的线性地址隔了一段时间没使用或者当前的物理页快被占用完了,这时它会将你这些物理页的数据拿出来存到硬盘上
设置虚拟内存后它会在你的c盘下生成一个pagefile.sys,它的大小刚好是你设置的虚拟内存大小
如果我们在某个程序中使用了0x12345678这个线性地址,它的物理页被存放的pagefile.sys了,如果我门在次使用这个地址就出问题了,这时就需要用到缺页异常
了
1,无效PTE的四种情形
(a)位于页面文件
如图a,当该进程再次读取该物理页对应的线性地址的时候,p位为0,触发缺页异常处理,在windows xp系统中 缺页异常处理位于IDT表的E号中断。进入缺页异常处理程序,查看PTE,1-9,12-31位有值,说明线性地址有效,但是数据放在页面文件中了,缺页异常处理程序根据PTE中得到的值去查询pagefile.sys
,将原来的物理页的内容挂入新的物理页,然后将P位置1,再将新的物理页的物理地址挂入PTE的第12-31位。
(b)要求一个零页面
页面尚未分配,下次访问时请求一个0页面
(c):页面正在转移
页面在物理内存中,但已被转移到某个物理页面链表中,可以通过查询_MMPFN数据库获取实际情况
(d):未知原因,需检查VAD
缺页异常处理发现你PTE全部为0就会去查VAD树,发现这个线性地址已经分配了它会帮你把物理页挂上,如果这个线性地址没有分配就会报0xC0000005。
缺页异常处理使windows物理页面使用更加高效
2.保留和提交的误区
commit 有权利拥有的物理页有多少。标红行的9指的是最多可以为这块线性地址分配9个物理页(0-9),但不是每块线性地址都已被挂上有效的物理页,只有当对应线性地址块被使用时,才会真正被挂上物理页
LPVOID VirtualAlloc(
LPVOID lpAddress, // 要分配的内存区域的地址
DWORD dwSize, // 分配的大小
DWORD flAllocationType, // 类型:MEM_RESERVE、 MEM_COMMIT
DWORD flProtect // 该内存的初始保护属性
);
MEM_RESERVE:不拥有物理页,只保留线性地址,commit为0
MEM_COMMIT:提交线性地址 (可以有物理页,但不是立即有 或一直有)
实验代码:
#include <stdio.h>
#include <windows.h>
int main()
{
LPVOID pAddr = VirtualAlloc(NULL, 0x1000*8, MEM_COMMIT, PAGE_READWRITE);
printf("%p\n", pAddr);
getchar();
*(PDWORD)pAddr = 0x12345678;
getchar();
return 0;
}
PTE为空,没有挂上物理页
继续运行
线性地址被挂上物理页。
3.EXECUTE_WRITECOPY
也利用缺页异常
先将PTE的R/W设置为0(只读),_MMVAD_FLAGS设为EXECUTE_WRITECOPY(7)
当你HOOK一些属性为EXECUTE_WRITECOPY的模块时:
1.修改它的物理页,这时它PTE.R/W为0就会触发缺页异常
2.进入异常处理程序发现PTE.R/W=0,查这个页对应的VAD发现Protection为写拷贝
3.创建一份新的物理页,修改的数据放到新的物理页,将你的线性地址会指向这个新的物理页。
4.别的进程查看,并未受到影响。
如果写驱动程序将它的PTE.R/W强行修改为1,这样它就不会触发缺页异常,可实现A进程中hook了这个模块,所有使用这个模块的进程也会被HOOK。
模块隐藏
1.断链-PEB
PEB的三个链表内容相同,顺序不同
取一个链表进行遍历,根据DllBase找到自己的DLL之后,从三个链中RemoveEntryList就可以了,这样所有使用PEB->Ldr结构来枚举DLL链表的就无法找到了
2.VAD树
把ControlArea->FilePointer->FileName.Buffer填0就可以实现该 DLL的隐藏,ZwQueryVirtualMemory将返回0xC0000039错 误,即“指定的路径无效
3.抹掉pe特征
4.自行加载
不用LoadLibrary,自已实现Loader的功能.
实现Loader功能之后,不管你是Load别的DLL,或者DLL自已Load自己,在Load完 成后,不会出现在PEB->Ldr链中,它的VAD也不会与FILE_OBJECT发生任何关系,
DLL也可以完全不要,注入具有相同功能的shellcode然后开线程执行就可以了
反射型dll注入,dll模块并未被注册