windows内核研究(内存管理-线性地址的管理)
内存管理
线性地址的管理
进程空间的地址划分
分区 | x86 32位Windows |
---|---|
空指针赋值区 | 0x00000000 - 0x0000FFFF |
用户模式区 | 0x00010000 - 0x7FFEFFFF |
64KB禁入区 | 0x7FFF0000 - 0x7FFFFFFF |
内核 | 0x80000000 - 0xFFFFFFFF |
线性地址有4GB,但是并不是所有的地方都能访问(这里的不能访问只是默认情况下,一但给这些区域挂上物理页还是可以访问的),所以需要记录哪些地方分配了
在内核空间是通过一个链表把所有未分配的空间链在一起
但是在用户空间,这样管理的效率太低,而是通过收索二叉树来管理
在_EPROCESS结构体当中有一个成员VadRoot,这个成员就是这个二叉树的入口点
由于我是用64位windbg分析32位的系统,版本等原因导致VadRoot的地址未被正常解析出来,所以我们直接加上偏移来解析这个地址
dt _RTL_AVL_TREE (ac110040 + 310) // 进程地址ac110040 + 偏移310 VadRoot处
得到地址:0xbb925678
VadRoot通常每一个节点都是_MMVAD结构,但是现在的windows对VadRoot进行了优化,并不直接指向_MMVAD,而是通过AVL 树/红黑树的结构来进行优化访问和存储可以使用以下命令直接遍历VadRoot
!vad 地址 // 遍历vad
字段 | 示例值 | 含义 |
---|---|---|
VAD 节点地址 | bb923260 | 该 VAD 节点在内核中的内存地址(_MMVAD 结构地址) |
Level | 8 | 该节点在 VAD 树中的深度(层级) |
Start | 580 | 内存区域的起始页号(需转换为虚拟地址:Start << PAGE_SHIFT ,32位系统 PAGE_SHIFT=12 ,即 0x580000 ) |
End | 5a7 | 内存区域的结束页号(0x5A7000 ) |
Commit | 5 | 已提交的物理页数量(单位:页,每页通常 4KB) |
Type | Mapped | 内存类型: • Private (私有内存,如堆/栈)• Mapped (映射文件或共享内存) |
Subtype | Exe | 子类型(仅适用于 Mapped 类型):• Exe (可执行文件映射)• Image (镜像文件)• 其他(如 Pagefile ) |
Protection | EXECUTE_WRITECOPY | 内存保护标志: • READONLY /READWRITE • EXECUTE /EXECUTE_WRITECOPY • PAGE_GUARD (保护页) |
File/Desc | \Users\...\x32dbg.exe | 如果是文件映射,显示文件路径;如果是共享内存,显示描述信息(如 Pagefile section ) |
Private Memory
申请内存的两种方式:
- 通过VirtualAlloc/VirtualAllocEx申请的:Private Memory(当前的进程独享内存)
- 通过CreateFileMapping映射的:Mapped Memory
我们来通过代码来看一下VirtualAlloc在没有分配和分配后的线性地址
#include<iostream>
#include<windows.h>LPVOID lpAddr;int main() {printf("当前内存还未申请!");getchar();lpAddr = VirtualAlloc(NULL, 0x1000 * 2, MEM_COMMIT, PAGE_READWRITE);printf("申请的内存地址:0x%x", lpAddr);system("pasue");return 0;
}
此时内存还未申请,我们用windbg查看一下当前进程的线程地址
回到程序让程序申请内存后我们再来看下
可以看到在我们没有分配内存时,
0xbc0
位置是没有分配的,可以看上面对应的属性和我们申请时填写的一致
堆与栈
那这个VirtualAlloc和我们在写c/c++程序时,用到的molloc/new关键字有什么区别呢,c/c++使用的申请是在堆
当中申请的它们的低层实现是HeapAlloc,它是由操作系统提前通过VirtualAlloc申请好的一块内存空间,当使用molloc/new时,就会把申请好的地址给挂过去
代码测试
#include<iostream>
#include<Windows.h>int num = 0x789;int main() {printf("申请内存之前!");getchar();// 在栈上分配内存int stack = 0x123;// 在堆上分配内存int* heap = new int(0x456);printf("栈空间的地址:0x%x\n",&stack);printf("堆空间的地址:0x%x\n",heap);printf("全局变量的地址:0x%x\n", &num);system("pause");return 0;
}
可以发现在我们程序中无论是全局变量,还是堆空间,栈空间中的内存在程序运行时就已经存在了
可以发现全局变量是在我们的程序中的一个位置写死的
Mapped Memory
上面讲到Private Memory是推私有的,而Mapped Memory是共享的,可以是文件共享或者是物理页共享
在上图中,Mapped后面有对应文件路径的就是文件共享,反之就是物理页共享
代码测试
#include<iostream>
#include<windows.h>int main(){// 第一个参数如果提供一个文件的句柄,那么创建出来的就是文件映射,否则就是内存映射。HANDLE g_hMapFile = CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,BUFSIZ,L"共享内存");// 将物理页与线性地址进行关联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);system("pause");return 0;
}
我们再到windbg中遍历一下
可以看到
B30
的位置已经分配好了物理页,然后我们就可以在其他进程获取到这个创建好的内存空间
代码测试
#include <iostream>
#include <windows.h>int main() {HANDLE g_hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"共享内存");// 将物理页与线性地址进行映射LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);printf("B进程读取%x", *(PDWORD)g_lpBuff);system("pause");return 0;
}
可以看到我们成功的读取到了内容
共享文件
#include<iostream>
#include<windows.h>int main(){HANDLE g_hFile = CreateFile(L"newMemory.exe",GENERIC_READ|GENERIC_WRITE,FILE_SHARE_READ,NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_READONLY, NULL);HANDLE g_hMapFile = CreateFileMapping(g_hFile,NULL,PAGE_READWRITE,0,BUFSIZ,NULL);LPTSTR g_lpBuff = (LPTSTR)MapViewOfFile(g_hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, BUFSIZ);printf("地址:0x%x",g_lpBuff);system("pause");return 0;
}
windbg中查看
可以看到已经成功的映射到了我们的文件上
写拷贝
可以看到这里的有一部分它的类型是EXECUTE_WRITECOPY,Mapped的后面还有一个Exe,这又是什么呢?
代码测试
#include<iostream>
#include<windows.h>int main(){LoadLibrary(L"C:\\Users\\win10x32\\Desktop\\gxnc.exe");system("pause");return 0;
}
可以看到当我们以LoadLibrary载入一个PE文件时,它的属性会被设置为EXECUTE_WRITECOPOY,所以我们看到的kernel32.dll,KernelBase.dll,其实都是操作系统用LoadLibrary一个个加载的,本质上没有任何区别,设置为EXECUTE_WRITECOPOY是因为当前系统环境有很多进程都在使用,也都可以对该文件进行修改,那这样以来,一但某一个进程修改了系统dll,那其他使用这个dll的进程就会出问题