windows進程中的記憶體結構

來源:互聯網
上載者:User
在閱讀本文之前,如果你連堆棧是什麼多不知道的話,請先閱讀文章後面的基礎知識。

接觸過編程的人都知道,進階語言都能通過變數名來訪問記憶體中的資料。那麼這些變數在記憶體中是如何存放的呢?程式又是如何使用這些變數的呢?下面就會對此進行深入的討論。下文中的C語言代碼如沒有特別聲明,預設都使用VC編譯的release版。

首先,來瞭解一下 C 語言的變數是如何在記憶體分部的。C 語言有全域變數(Global)、本地變數(Local),靜態變數(Static)、寄存器變數(Regeister)。每種變數都有不同的分配方式。先來看下面這段代碼:

#include <stdio.h>

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

int main()
{
static int s1=0, s2=0, s3=0;
int v1=0, v2=0, v3=0;

//列印出各個變數的記憶體位址

printf("0x%08x\n",&v1); //列印各本地變數的記憶體位址
printf("0x%08x\n",&v2);
printf("0x%08x\n\n",&v3);
printf("0x%08x\n",&g1); //列印各全域變數的記憶體位址
printf("0x%08x\n",&g2);
printf("0x%08x\n\n",&g3);
printf("0x%08x\n",&s1); //列印各靜態變數的記憶體位址
printf("0x%08x\n",&s2);
printf("0x%08x\n\n",&s3);
return 0;
}

編譯後的執行結果是:

0x0012ff78
0x0012ff7c
0x0012ff80

0x004068d0
0x004068d4
0x004068d8

0x004068dc
0x004068e0
0x004068e4

輸出的結果就是變數的記憶體位址。其中v1,v2,v3是本地變數,g1,g2,g3是全域變數,s1,s2,s3是靜態變數。你可以看到這些變數在記憶體是連續分布的,但是本地變數和全域變數分配的記憶體位址差了十萬八千裡,而全域變數和靜態變數分配的記憶體是連續的。這是因為本地變數和全域/靜態變數是分配在不同類型的記憶體地區中的結果。對於一個進程的記憶體空間而言,可以在邏輯上分成3個部份:代碼區,待用資料區和動態資料區。動態資料區一般就是“堆棧”。“棧(stack)”和“堆(heap)”是兩種不同的動態資料區,棧是一種線性結構,堆是一種鏈式結構。進程的每個線程都有私人的“棧”,所以每個線程雖然代碼一樣,但本地變數的資料都是互不干擾。一個堆棧可以通過“基地址”和“棧頂”地址來描述。全域變數和靜態變數分配在待用資料區,本地變數分配在動態資料區,即堆棧中。程式通過堆棧的基地址和位移量來訪問本地變數。

├———————┤低端記憶體地區
│ …… │
├———————┤
│ 動態資料區 │
├———————┤
│ …… │
├———————┤
│ 代碼區 │
├———————┤
│ 待用資料區 │
├———————┤
│ …… │
├———————┤高端記憶體地區

堆棧是一個先進後出的資料結構,棧頂地址總是小於等於棧的基地址。我們可以先瞭解一下函數調用的過程,以便對堆棧在程式中的作用有更深入的瞭解。不同的語言有不同的函數調用規定,這些因素有參數的壓入規則和堆棧的平衡。windows API的調用規則和ANSI C的函數調用規則是不一樣的,前者由被調函數調整堆棧,後者由調用者調整堆棧。兩者通過“__stdcall”和“__cdecl”首碼區分。先看下面這段代碼:

#include <stdio.h>

void __stdcall func(int param1,int param2,int param3)
{
int var1=param1;
int var2=param2;
int var3=param3;
printf("0x%08x\n",¶m1); //列印出各個變數的記憶體位址
printf("0x%08x\n",¶m2);
printf("0x%08x\n\n",¶m3);
printf("0x%08x\n",&var1);
printf("0x%08x\n",&var2);
printf("0x%08x\n\n",&var3);
return;
}

int main()
{
func(1,2,3);
return 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)、高端記憶體地區

就是函數調用過程中堆棧的樣子了。首先,三個參數以從又到左的次序壓入堆棧,先壓“param3”,再壓“param2”,最後壓入“param1”;然後壓入函數的返回地址(RET),接著跳轉到函數地址接著執行(這裡要補充一點,介紹UNIX下的緩衝溢出原理的文章中都提到在壓入RET後,繼續壓入當前EBP,然後用當前ESP代替EBP。然而,有一篇介紹windows下函數調用的文章中說,在windows下的函數調用也有這一步驟,但根據我的實際調試,並未發現這一步,這還可以從param3和var1之間只有4位元組的間隙這點看出來);第三步,將棧頂(ESP)減去一個數,為本地變數分配記憶體空間,上例中是減去12位元組(ESP=ESP-3*4,每個int變數佔用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”

聰明的讀者看到這裡,差不多就明白緩衝溢出的原理了。先來看下面的代碼:

#include <stdio.h>
#include <string.h>

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

int main()
{
func();
return 0;
}

編譯後執行一下回怎麼樣?哈,“"0x00414141"指令引用的"0x00000000"記憶體。該記憶體不能為"read"。”,“非法操作”嘍!"41"就是"A"的16進位的ASCII碼了,那明顯就是strcat這句出的問題了。"lpBuff"的大小隻有8位元組,算進結尾的‘\0‘,那strcat最多隻能寫入7個"A",但程式實際寫入了11個"A"外加1個‘\0‘。再來看看上面那幅圖,多出來的4個位元組正好覆蓋了RET的所在的記憶體空間,導致函數返回到一個錯誤的記憶體位址,執行了錯誤的指令。如果能精心構造這個字串,使它分成三部分,前一部份僅僅是填充的無意義資料以達到溢出的目的,接著是一個覆蓋RET的資料,緊接著是一段shellcode,那隻要著個RET地址能指向這段shellcode的第一個指令,那函數返回時就能執行shellcode了。但是軟體的不同版本和不同的運行環境都可能影響這段shellcode在記憶體中的位置,那麼要構造這個RET是十分困難的。一般都在RET和shellcode之間填充大量的NOP指令,使得exploit有更強的通用性。

├———————┤<—低端記憶體地區
│ …… │
├———————┤<—由exploit填入資料的開始
│ │
│ buffer │<—填入無用的資料
│ │
├———————┤
│ RET │<—指向shellcode,或NOP指令的範圍
├———————┤
│ NOP │
│ …… │<—填入的NOP指令,是RET可指向的範圍
│ NOP │
├———————┤
│ │
│ shellcode │
│ │
├———————┤<—由exploit填入資料的結束
│ …… │
├———————┤<—高端記憶體地區

windows下的動態資料除了可存放在棧中,還可以存放在堆中。瞭解C++的朋友都知道,C++可以使用new關鍵字來動態分配記憶體。來看下面的C++代碼:

#include <stdio.h>
#include
#include <windows.h>

void func()
{
char *buffer=new char[128];
char bufflocal[128];
static char buffstatic[128];
printf("0x%08x\n",buffer); //列印堆中變數的記憶體位址
printf("0x%08x\n",bufflocal); //列印本地變數的記憶體位址
printf("0x%08x\n",buffstatic); //列印靜態變數的記憶體位址
}

void main()
{
func();
return;
}

程式執行結果為:

0x004107d0
0x0012ff04
0x004068c0

可以發現用new關鍵字分配的記憶體即不在棧中,也不在待用資料區。VC編譯器是通過windows下的“堆(heap)”來實現new關鍵字的記憶體動態分配。在講“堆”之前,先來瞭解一下和“堆”有關的幾個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:main") //定義程式的入口
#include <windows.h>

_CRTIMP int (__cdecl *printf)(const char *, ...); //定義STL函數printf
/*---------------------------------------------------------------------------
寫到這裡,我們順便來複習一下前面所講的知識:
(*注)printf函數是C語言的標準函數庫中函數,VC的標準函數庫由msvcrt.dll模組實現。
由函數定義可見,printf的參數個數是可變的,函數內部無法預Crowdsourced Security Testing道調用者壓入的參數個數,函數只能通過分析第一個參數字串的格式來獲得壓入參數的資訊,由於這裡參數的個數是動態,所以必須由調用者來平衡堆棧,這裡便使用了__cdecl調用規則。BTW,Windows系統的API函數基本上是__stdcall調用形式,只有一個API例外,那就是wsprintf,它使用__cdecl調用規則,同printf函數一樣,這是由於它的參數個數是可變的緣故。
---------------------------------------------------------------------------*/
void main()
{
HANDLE hHeap=GetProcessHeap();
char *buff=HeapAlloc(hHeap,0,0x10);
char *buff2=HeapAlloc(hHeap,0,0x10);
HMODULE hMsvcrt=LoadLibrary("msvcrt.dll");
printf=(void *)GetProcAddress(hMsvcrt,"printf");
printf("0x%08x\n",hHeap);
printf("0x%08x\n",buff);
printf("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三個不同編譯器編譯出來的程式的執行結果:

#include <stdio.h>

int main()
{
int a;
char b;
int c;
printf("0x%08x\n",&a);
printf("0x%08x\n",&b);
printf("0x%08x\n",&c);
return 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
變數在記憶體中的順序:同上。

三個編譯器都做到了資料對齊,但是後兩個編譯器顯然沒VC“聰明”,讓一個char佔了4位元組,浪費記憶體哦。

基礎知識:
堆棧是一種簡單的資料結構,是一種只允許在其一端進行插入或刪除的線性表。允許插入或刪除操作的一端稱為棧頂,另一端稱為棧底,對堆棧的插入和刪除操作被稱為入棧和出棧。有一組CPU指令可以實現對進程的記憶體實現堆棧訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。CPU的ESP寄存器存放當前線程的棧頂指標,EBP寄存器中儲存當前線程的棧底指標。CPU的EIP寄存器存放下一個CPU指令存放的記憶體位址,當CPU執行完當前的指令後,從EIP寄存器中讀取下一條指令的記憶體位址,然後繼續執行。

參考:《Windows下的HEAP溢出及其利用》by: isno
《windows核心編程》by: Jeffrey Richter

-------------------------------------------------------------------
http://blog.chinaunix.net/u1/46016/index.php
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.