windows内存结构:windows进程中的内存结构

接触过编程人都知道高级语言都能通过变量名来访问内存中数据那么这些变量在内存中是如何存放呢?又是如何使用这些变量呢?下面就会对此进行深入讨论下文中C语言代码如没有特别声明默认都使用VC编译release版

首先来了解下 C 语言变量是如何在内存分部C 语言有全局变量(Global)、本地变量(Local)静态变量(Static)、寄存器变量(Regeister)每种变量都有区别分配方式先来看下面这段代码:

# <stdio.h>

g1=0, g2=0, g3=0;


{
s1=0, s2=0, s3=0;
v1=0, v2=0, v3=0;

//打印出各个变量内存地址

prf("0x%08x\n",&v1); //打印各本地变量内存地址
prf("0x%08x\n",&v2);
prf("0x%08x\n\n",&v3);
prf("0x%08x\n",&g1); //打印各全局变量内存地址
prf("0x%08x\n",&g2);
prf("0x%08x\n\n",&g3);
prf("0x%08x\n",&s1); //打印各静态变量内存地址
prf("0x%08x\n",&s2);
prf("0x%08x\n\n",&s3);
0;
}

编译后执行结果是:

0x0012ff78
0x0012ff7c
0x0012ff80

0x004068d0
0x004068d4
0x004068d8

0x004068dc
0x004068e0
0x004068e4

输出结果就是变量内存地址其中v1,v2,v3是本地变量g1,g2,g3是全局变量s1,s2,s3是静态变量你可以看到这些变量在内存是连续分布但是本地变量和全局变量分配内存地址差了十万 8千里而全局变量和静态变量分配内存是连续这是本地变量和全局/静态变量是分配在区别类型内存区域中结果对于个进程内存空间而言可以在逻辑上分成3个部份:代码区静态数据区和动态数据区动态数据区般就是“堆栈”“栈(stack)”和“堆(heap)”是两种区别动态数据区栈是种线性结构堆是种链式结构进程每个线程都有私有“栈”所以每个线程虽然代码但本地变量数据都是互不干扰个堆栈可以通过“基地址”和“栈顶”地址来描述全局变量和静态变量分配在静态数据区本地变量分配在动态数据区即堆栈中通过堆栈基地址和偏移量来访问本地变量


├———————┤低端内存区域
│ …… │
├———————┤
│ 动态数据区 │
├———————┤
│ …… │
├———————┤
│ 代码区 │
├———————┤
│ 静态数据区 │
├———————┤
│ …… │
├———————┤高端内存区域


堆栈是个先进后出数据结构栈顶地址总是小于等于栈基地址我们可以先了解过程以便对堆栈在作用有更深入了解区别语言有区别规定这些原因有参数压入规则和堆栈平衡windows API规则和ANSI C规则是不前者由被调调整堆栈后者由者调整堆栈两者通过“__stdcall”和“__cdecl”前缀区分先看下面这段代码:

# <stdio.h>

void __stdcall func( param1, param2, param3)
{
var1=param1;
var2=param2;
var3=param3;
prf("0x%08x\n",&para;m1); //打印出各个变量内存地址
prf("0x%08x\n",&para;m2);
prf("0x%08x\n\n",&para;m3);
prf("0x%08x\n",&var1);
prf("0x%08x\n",&var2);
prf("0x%08x\n\n",&var3);
;
}


{
func(1,2,3);
0;
}

编译后执行结果是:

0x0012ff78
0x0012ff7c
0x0012ff80

0x0012ff68
0x0012ff6c
0x0012ff70



├———————┤<—执行时栈顶(ESP)、低端内存区域
│ …… │
├———————┤
│ var 1 │
├———————┤
│ var 2 │
├———————┤
│ var 3 │
├———————┤
│ RET │
├———————┤<—“__cdecl”返回后栈顶(ESP)
│ parameter 1 │
├———————┤
│ parameter 2 │
├———————┤
│ parameter 3 │
├———————┤<—“__stdcall”返回后栈顶(ESP)
│ …… │
├———————┤<—栈底(基地址 EBP)、高端内存区域


上图就是过程中堆栈样子了首先 3个参数以从又到左次序压入堆栈先压“param3”再压“param2”最后压入“param1”;然后压入返回地址(RET)接着跳转到地址接着执行(这里要补充介绍UNIX下缓冲溢出原理文章中都提到在压入RET后继续压入当前EBP然后用当前ESP代替EBP然而篇介绍windows下文章中说在windows下也有这步骤但根据我实际调试并未发现这这还可以从param3和var1的间只有4字节间隙这点看出来);第 3步将栈顶(ESP)减去个数为本地变量分配内存空间上例中是减去12字节(ESP=ESP-3*4每个变量占用4个字节);接着就化本地变量内存空间由于“__stdcall”由被调调整堆栈所以在返回前要恢复堆栈先回收本地变量占用内存(ESP=ESP+3*4)然后取出返回地址填入EIP寄存器回收先前压入参数占用内存(ESP=ESP+3*4)继续执行代码参见下列汇编代码:

;--------------func 汇编代码-------------------

:00401000 83EC0C sub esp, 0000000C //创建本地变量内存空间
:00401003 8B442410 mov eax, dword ptr [esp+10]
:00401007 8B4C2414 mov ecx, dword ptr [esp+14]
:0040100B 8B542418 mov edx, dword ptr [esp+18]
:0040100F 89442400 mov dword ptr [esp], eax
:00401013 8D442410 lea eax, dword ptr [esp+10]
:00401017 894C2404 mov dword ptr [esp+04], ecx

……………………(省略若干代码)

:00401075 83C43C add esp, 0000003C ;恢复堆栈回收本地变量内存空间
:00401078 C3 ret 000C ;返回恢复参数占用内存空间
;如果是“__cdecl”这里是“ret”堆栈将由者恢复

;-------------------结束-------------------------


;--------------主func代码--------------

:00401080 6A03 push 00000003 //压入参数param3
:00401082 6A02 push 00000002 //压入参数param2
:00401084 6A01 push 00000001 //压入参数param1
:00401086 E875FFFFFF call 00401000 //func
;如果是“__cdecl”将在这里恢复堆栈“add esp, 0000000C”

聪明读者看到这里差不多就明白缓冲溢出原理了先来看下面代码:

# <stdio.h>
# <.h>

void __stdcall func
{
char lpBuff[8]="\0";
strcat(lpBuff,"AAAAAAAAAAA");
;
}


{
func;
0;
}

编译后执行下回如何样?哈“"0x00414141"指令引用"0x00000000"内存该内存不能为"read"“非法操作”喽!"41"就是"A"16进制ASCII码了那明显就是strcat这句出问题了"lpBuff"大小只有8字节算进结尾'\0'那strcat最多只能写入7个"A"实际写入了11个"A"外加1个'\0'再来看看上面那幅图多出来4个字节正好覆盖了RET所在内存空间导致返回到内存地址执行了指令如果能精心构造这个使它分成 3部分部份仅仅是填充无意义数据以达到溢出接着是个覆盖RET数据紧接着是段shellcode那只要着个RET地址能指向这段shellcode个指令返回时就能执行shellcode了但是软件Software区别版本和区别运行环境都可能影响这段shellcode在内存中位置那么要构造这个RET是十分困难般都在RET和shellcode的间填充大量NOP指令使得exploit有更强通用性


├———————┤<—低端内存区域
│ …… │
├———————┤<—由exploit填入数据开始
│ │
│ buffer │<—填入无用数据
│ │
├———————┤
│ RET │<—指向shellcode或NOP指令范围
├———————┤
│ NOP │
│ …… │<—填入NOP指令是RET可指向范围
│ NOP │
├———————┤
│ │
│ shellcode │
│ │
├———————┤<—由exploit填入数据结束
│ …… │
├———————┤<—高端内存区域


windows下动态数据除了可存放在栈中还可以存放在堆中了解C朋友都知道C可以使用关键字来动态分配内存来看下面C代码:

# <stdio.h>
# <iostream.h>
# <windows.h>

void func
{
char *buffer= char[128];
char bufflocal[128];
char buff[128];
prf("0x%08x\n",buffer); //打印堆中变量内存地址
prf("0x%08x\n",bufflocal); //打印本地变量内存地址
prf("0x%08x\n",buff); //打印静态变量内存地址
}

void
{
func;
;
}

执行结果为:

0x004107d0
0x0012ff04
0x004068c0

可以发现用关键字分配内存即不在栈中也不在静态数据区VC编译器是通过windows下“堆(heap)”来实现关键字内存动态分配在讲“堆”的前先来了解下和“堆”有关几个API:

HeapAlloc 在堆中申请内存空间
HeapCreate 创建个新堆对象
HeapDestroy 销毁个堆对象
HeapFree 释放申请内存
HeapWalk 枚举堆对象所有内存块
GetProcessHeap 取得进程默认堆对象
GetProcessHeaps 取得进程所有堆对象
LocalAlloc
GlobalAlloc

当进程化时系统会自动为进程创建个默认堆这个堆默认所占内存大小为1M堆对象由系统进行管理它在内存中以链式结构存在通过下面代码可以通过堆动态申请内存空间:

HANDLE hHeap=GetProcessHeap;
char *buff=HeapAlloc(hHeap,0,8);

其中hHeap是堆对象句柄buff是指向申请内存空间地址那这个hHeap究竟是什么呢?它值有什么意义吗?看看下面这段代码吧:

#pragma comment(linker,"/entry:") //定义入口
# <windows.h>

_CRTIMP (__cdecl *prf)(const char *, ...); //定义STLprf
/*---------------------------------------------------------------------------
写到这里我们顺便来复习下前面所讲知识:
(*注)prf是C语言标准库中VC标准库由msvcrt.dll模块实现
定义可见prf参数个数是可变内部无法预先知道者压入参数个数只能通过分析第个参数格式来获得压入参数信息由于这里参数个数是动态所以必须由者来平衡堆栈这里便使用了__cdecl规则BTWWindows系统API基本上是__stdcall形式只有个API例外那就是wsprf它使用__cdecl规则同prf这是由于它参数个数是可变缘故
---------------------------------------------------------------------------*/
void
{
HANDLE hHeap=GetProcessHeap;
char *buff=HeapAlloc(hHeap,0,0x10);
char *buff2=HeapAlloc(hHeap,0,0x10);
HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");
prf=(void *)GetProcAddress(hMsvcrt,"prf");
prf("0x%08x\n",hHeap);
prf("0x%08x\n",buff);
prf("0x%08x\n\n",buff2);
}

执行结果为:

0x00130000
0x00133100
0x00133118

hHeap值如何和那个buff值那么接近呢?其实hHeap这个句柄就是指向HEAP首部地址在进程用户区存着个叫PEB(进程环境块)结构这个结构中存放着些有关进程重要信息其中在PEB首地址偏移0x18处存放ProcessHeap就是进程默认堆地址而偏移0x90处存放了指向进程所有堆地址列表指针windows有很多API都使用进程默认堆来存放动态数据如windows 2000下所有ANSI版本都是在默认堆中申请内存来转换ANSI串到Unicode个堆访问是顺序进行时刻只能有个线程访问堆中数据当多个线程同时有访问要求时只能排队等待这样便造成执行效率下降

最后来说说内存中数据对齐所位数据对齐是指数据所在内存地址必须是该数据长度整数倍DWORD数据内存起始地址能被4除尽WORD数据内存起始地址能被2除尽x86 CPU能直接访问对齐数据当他试图访问个未对齐数据时会在内部进行系列调整这些调整对于来说是透明但是会降低运行速度所以编译器在编译时会尽量保证数据对齐同样段代码我们来看看用VC、Dev-C和lcc 3个区别编译器编译出来执行结果:

# <stdio.h>


{
a;
char b;
c;
prf("0x%08x\n",&a);
prf("0x%08x\n",&b);
prf("0x%08x\n",&c);
0;
}

这是用VC编译后执行结果:
0x0012ff7c
0x0012ff7b
0x0012ff80
变量在内存中顺序:b(1字节)-a(4字节)-c(4字节)

这是用Dev-C编译后执行结果:
0x0022ff7c
0x0022ff7b
0x0022ff74
变量在内存中顺序:c(4字节)-中间相隔3字节-b(占1字节)-a(4字节)

这是用lcc编译后执行结果:
0x0012ff6c
0x0012ff6b
0x0012ff64
变量在内存中顺序:同上

3个编译器都做到了数据对齐但是后两个编译器显然没VC“聪明”个char占了4字节浪费内存哦


基础知识:
堆栈是种简单数据结构种只允许在其端进行插入或删除线性表允许插入或删除操作端称为栈顶端称为栈底对堆栈插入和删除操作被称为入栈和出栈组CPU指令可以实现对进程内存实现堆栈访问其中POP指令实现出栈操作PUSH指令实现入栈操作CPUESP寄存器存放当前线程栈顶指针EBP寄存器中保存当前线程栈底指针CPUEIP寄存器存放下个CPU指令存放内存地址当CPU执行完当前指令后从EIP寄存器中读取下条指令内存地址然后继续执行


参考:Windows下HEAP溢出及其利用by: isno
            windows核心编程by: Jeffrey Richter


  • 篇文章: Foxmail5远程缓冲区溢出漏洞分析

  • 篇文章: 整理总结windows下堆溢出 3种利用方式
  • Tags:  windows74g内存 windows7内存 windows进程管理器 windows内存结构

    延伸阅读

    最新评论

    发表评论