內存加載DLL

一、前言

目前不少敏感和重要的DLL(Dynamic-link library) 都沒有提供靜態版本供編譯器進行靜態鏈接(.lib文件),即便提供了靜態版本也由於兼容性問題致使沒法使用,而只提供DLL版本,而且不少專業軟件的受權部分的API,都是單獨提供一個DLL來完成,而主模塊經過調用DLL中的接口來完成受權功能。雖然這些軟件通常都採用了加殼和反調試等保護,可是一旦這些功能失去做用,好比脫殼,反反調試,HOOK API或者乾脆寫一個仿真的受權DLL(模擬受權DLL的全部導出函數接口),而後仿真的DLL再調用受權DLL,這樣全部的輸入首先被仿真DLL截獲再傳遞給受權DLL,而受權DLL的輸出也首先傳遞給仿真DLL再傳遞給主程序,這樣就能夠輕易的監視兩者之間的輸入輸出之間的關係,從而輕易的截獲DLL中的受權信息進行修改再返回給主程序。算法

二、目前隱式調用敏感DLL中可能存在的安全隱患

如下經過兩個軟件的受權DLL來講明這種問題的嚴重性。以下是兩個軟件中受權DLL的部分信息,以下圖所示:數組

 

(圖1)安全

經過工具OllyICE能夠輕易的看出IoMonitor.exe調用受權DLL(XKeyAPI.DLL),這樣就很容易在調用這些API的地方設置斷點,而後判斷輸入輸出的關係,從而達到破解的目的。ide

 

(圖2)函數

經過工具OllyICE能夠輕易的看出sfeng.DLL中導出了不少函數,其中含義也很明顯。GetHDID獲取硬盤的ID,GetCpuId獲取cpu的ID,WinAntiDebug反調試接口。而這些都是主程序須要調用的,好比:主程序經過GetHDID來獲取硬盤編碼,以這個硬盤ID的僞碼來生成受權碼,破解者很容易修改這些接口的輸出值或者乾脆寫一個sfeng.DLL來導出跟目標sfeng.DLL如出一轍的導出函數,而主程序卻徹底不知曉。只要用戶有一套受權碼就可讓GetHDID無論什麼機器都返回同樣的值,從而達到任何機器均可以使用同一套受權碼。工具

 

(圖3)開發工具

如上圖所示,直接修改DLL中函數GetHDID(RVA地址:0093FF3C開始)的實現,讓它直接返回固定的硬盤ID就能夠達到一個受權處處使用的目的。其中:」WD-Z=AM9N086529ksaiy」爲須要返回的已經受權的硬盤ID,咱們直接返回這個值便可。把原來0093FF3C 部分的代碼用nop替換掉,添加Call 008FFF60,後面添加字符串」WD-Z=AM9N086529ksaiy」,Call 008FFF60以後,ESP=Call後的返回地址(Call指令的下一行),也就是字符串」WD-Z=AM9N086529ksaiy」的首地址,而後pop EAX 後,返回值就是字符串的首地址,經過這種簡單的修改就能夠達到破解的目的,說明這種隱式的調用是很是危險的。測試

三、模擬Windows PE加載器,從資源中加載DLL

本文主要介紹將DLL文件進行加密壓縮後存放在程序的資源段,而後在程序中讀取資源段數據進行解壓和解密工做後,從內存中加載這個DLL,而後模擬PE加載器完成DLL的加載過程。本文主要以Visual C++ 6.0爲工具進行介紹,其它開發工具實現過程與此相似。優化

這樣做的好處也很明顯,DLL文件存放在主程序的資源段,並且通過了加密壓縮處理,破解者很難找到下斷點的地方,也不能輕易修改資源DLL,由於只有主程序完成解壓和解密工做,完成PE加載工做後此DLL纔開始工做。編碼

咱們知道,要顯式加載一個DLL,並取得其中導出的函數地址通常是經過以下步驟:
(1) 用LoadLibrary加載DLL文件,得到該DLL的模塊句柄;
(2) 定義一個函數指針類型,並聲明一個變量;
(3) 用GetProcAddress取得該DLL中目標函數的地址,賦值給函數指針變量;
(4) 調用函數指針變量。
這個方法要求DLL文件位於硬盤上面,而咱們的DLL如今在內存中。如今假設咱們的DLL已經位於內存中,好比經過脫殼、解密或者解壓縮獲得,能不能不把它寫入硬盤文件,而直接從內存加載呢?答案是確定的,方法就是完成跟Windows PE加載器一樣的工做便可。

加載過程大體包括如下幾個部分:

一、調用API讀取DLL資源數據拷貝到內存中

二、調用解壓和解密函數對內存中的DLL進行處理

三、檢查DOS頭和PE頭判斷是否爲合法的PE格式

四、計算加載該DLL所需的虛擬地址空間大小

五、向操做系統申請指定大小的虛擬地址空間並提交

六、將DLL數據複製到所分配的虛擬內存塊中,注意文件段對齊方式和內存段對齊方式

七、對每一個 DLL文件來講都存在一個重定位節(.reloc),用於記錄DLL文件的重定位信息,須要處理重定位信息

八、讀取DLL的引入表部分,加載引入表部分須要的DLL,並填充須要的函數入口的真實地址

九、根據DLL每一個節的屬性設置其對應內存頁的讀寫屬性

十、調用入口函數DLLMain,完成初始化工做

十一、保存DLL的基地址(即分配的內存塊起始地址),用於查找DLL的導出函數

十二、不須要DLL的時候,釋放所分配的虛擬內存,釋放全部動態申請的內存

如下部分分別介紹這幾個步驟,以改造過的網上下載的CMemLoadDLL類爲例程(原類存在幾個錯誤的地方)

 

A. 調用API讀取DLL資源數據拷貝到內存中

//加載資源DLL

#define strKey (char)0x15

char DLLtype[4]={'D' ^ strKey ,'l'^ strKey,'l'^ strKey,0x00};

HINSTANCE hinst=AfxGetInstanceHandle();

HRSRC hr=NULL;

HGLOBAL hg=NULL;

//對資源名稱字符串進行簡單的異或操做,達到不能經過外部字符串參考下斷點

for(int i=0;i<sizeof(DLLtype)-1;i++)

{

DLLtype[i]^=strKey;

}

hr=FindResource(hinst,MAKEINTRESOURCE(IDR_DLL),TEXT(DLLtype));

if (NULL == hr) return FALSE;

//獲取資源的大小

DWORD dwSize = SizeofResource(hinst, hr);

if (0 == dwSize) return FALSE;

hg=LoadResource(hinst,hr);

if (NULL == hg) return FALSE;

//鎖定資源

LPVOID pBuffer =(LPSTR)LockResource(hg);

if (NULL == pBuffer) return FALSE;

FreeResource(hg); //在資源使用完畢後咱們不須要使用UnlockResource和FreeResource來手動地釋放資源,由於它們都是16位Windows遺留下來的,在Win32中,在使用完畢後系統會自動回收

 

 

B. 調用解壓和解密函數對內存總的DLL進行處理

對於上面獲取的pBuffer能夠進行解壓和解密操做,算法應該跟你加入的資源採起的算法進行逆變換便可,具體算法能夠本身選擇,此處省略。

 

C. 檢查DOS頭和PE頭判斷是否爲合法的PE格式

 

//CheckDataValide函數用於檢查緩衝區中的數據是否有效的DLL文件

//返回值:是一個可執行的DLL則返回TRUE,不然返回FALSE。

//lpFileData: 存放DLL數據的內存緩衝區

//DataLength: DLL文件的長度

BOOL CMemLoadDLL::CheckDataValide(void* lpFileData, int DataLength)

{

//檢查長度

if(DataLength < sizeof(IMAGE_DOS_HEADER)) return FALSE;

pDosHeader = (PIMAGE_DOS_HEADER)lpFileData; // DOS頭

//檢查dos頭的標記

if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) return FALSE; //0*5A4D : MZ

//檢查長度

if((DWORD)DataLength < (pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS)) ) return FALSE;

//取得pe頭

pNTHeader = (PIMAGE_NT_HEADERS)( (unsigned long)lpFileData + pDosHeader->e_lfanew); // PE頭

//檢查pe頭的合法性

if(pNTHeader->Signature != IMAGE_NT_SIGNATURE) return FALSE; //0*00004550 : PE00

if((pNTHeader->FileHeader.Characteristics & IMAGE_FILE_DLL) == 0) //0*2000 : File is a DLL

return FALSE;

if((pNTHeader->FileHeader.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) == 0) //0*0002 : 指出文件能夠運行

return FALSE;

if(pNTHeader->FileHeader.SizeOfOptionalHeader != sizeof(IMAGE_OPTIONAL_HEADER)) return FALSE;

//取得節表(段表)

pSectionHeader = (PIMAGE_SECTION_HEADER)((int)pNTHeader + sizeof(IMAGE_NT_HEADERS));

//驗證每一個節表的空間

for(int i=0; i< pNTHeader->FileHeader.NumberOfSections; i++)

{

if((pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData) > (DWORD)DataLength)return FALSE;

}

return TRUE;

}

D. 計算加載該DLL所需的虛擬地址空間大小

計算整個DLL映像文件的尺寸,最大映像尺寸應該爲VOffset最大的一個段的VOffset+VSize,而後補齊段對齊便可。以下圖中,最大映像尺寸應該爲0x0000D000+0x00000DA6,而後按段對齊(如爲:0x1000對齊)則結果爲0x0000E000。其中DOS Header和PE Header就佔用0x1000字節,代碼段.text從0x1000開始佔用了0x7000字節。

段名稱   虛擬地址  虛擬大小  物理地址 物理大小  標誌

 

int CMemLoadDLL::CalcTotalImageSize()

{

int Size;

if(pNTHeader == NULL)return 0;

int nAlign = pNTHeader->OptionalHeader.SectionAlignment; //段對齊字節數

// 計算全部頭的尺寸。包括dos, coff, pe頭和段表的大小

Size = GetAlignedSize(pNTHeader->OptionalHeader.SizeOfHeaders, nAlign);

// 計算全部節的大小

for(int i=0; i < pNTHeader->FileHeader.NumberOfSections; ++i)

{

//獲得該節的大小

int CodeSize = pSectionHeader[i].Misc.VirtualSize ;

int LoadSize = pSectionHeader[i].SizeOfRawData;

int MaxSize = (LoadSize > CodeSize)?(LoadSize):(CodeSize);

int SectionSize = GetAlignedSize(pSectionHeader[i].VirtualAddress + MaxSize, nAlign);

if(Size < SectionSize)

Size = SectionSize; //Use the Max;

}

return Size;

}

 

//計算對齊邊界

int CMemLoadDLL::GetAlignedSize(int Origin, int Alignment)

{

return (Origin + Alignment - 1) / Alignment * Alignment;

}

E. 向操做系統申請指定大小的虛擬地址空間並提交

調用操做系統API VirtualAlloc保留指定大小的虛擬內存並提交內存,VirtualAlloc的第一個參數不能指定地址,若是指定地址已經被佔用或者指定地址後面沒有足夠的連續的地址空間來知足提交的大小則會調用失敗,而咱們也沒有必要獲取指定地址空間,這樣第一個參數必須保留爲NULL(0)。

void *pMemoryAddress=VirtualAlloc((LPVOID)NULL, ImageSize,MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);

if(pMemoryAddress == NULL)

{

return FALSE;

}

 

F. 將DLL數據複製到所分配的虛擬內存塊中,注意文件段對齊方式和內存段對齊方式

 

拷貝內存DLL到提交的虛擬地址空間,拷貝的部分包括PE文件的全部部分,DOS Header、 PE Header 、Section Table、Section 1~Section N,以下圖所示:

DOS MZ header

DOS stub

PE header

Section table

Section 1

Section 2

Section ...

Section n

 

 

//CopyDLLDatas函數將DLL數據複製到指定內存區域,並對齊全部節

//pSrc: 存放DLL數據的原始緩衝區

//pDest:目標內存地址

void CMemLoadDLL::CopyDLLDatas(void* pDest, void* pSrc)

{

// 計算須要複製的PE頭+段表字節數

int HeaderSize = pNTHeader->OptionalHeader.SizeOfHeaders;

int SectionSize = pNTHeader->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);

 

int MoveSize = HeaderSize + SectionSize;

 

//複製頭和段信息

memmove(pDest, pSrc, MoveSize);

 

//複製每一個節

for(int i=0; i < pNTHeader->FileHeader.NumberOfSections; ++i)

{

if(pSectionHeader[i].VirtualAddress == 0 || pSectionHeader[i].SizeOfRawData == 0) continue;

// 定位該節在內存中的位置

void *pSectionAddress = (void *)((unsigned long)pDest + pSectionHeader[i].VirtualAddress);

// 複製段數據到虛擬內存

memmove((void *)pSectionAddress,

(void *)((DWORD)pSrc + pSectionHeader[i].PointerToRawData),

pSectionHeader[i].SizeOfRawData);

}

//修正指針,指向新分配的內存

//新的dos頭

pDosHeader = (PIMAGE_DOS_HEADER)pDest;

//新的pe頭地址

pNTHeader = (PIMAGE_NT_HEADERS)((int)pDest + (pDosHeader->e_lfanew));

//新的節表地址

pSectionHeader = (PIMAGE_SECTION_HEADER)((int)pNTHeader + sizeof(IMAGE_NT_HEADERS));

return ;

}

G. 每一個 DLL文件來講都存在一個重定位節(.reloc),用於記錄DLL文件的重定位信息,須要處理重定位信息

 

Windows加載DLL時就能夠按照該節的信息對須要重定位的地址進行修正,在32位代碼中,凡涉及到直接尋址的指令都是須要重定位的,而PE文件的的(.reloc)段則是可選的,由於PE文件通常均可以加載到默認地址(如:0x00400000)。固然系統的DLL其默認加載地址都能知足要求,由於這些DLL都在系統加載其它程序前首先被加載(如:Kernel32.DLL,User32.DLL)等。

對於操做系統來講,其任務就是在對可執行程序透明的狀況下完成重定位操做,在現實中,重定位信息是在編譯的時候由編譯器生成並被保留在可執行文件中的,在程序被執行前由操做系統根據重定位信息修正代碼,這樣在開發程序的時候就不用考慮重定位問題了。

重定位信息在DLL文件中被存放在重定位表中,重定位的算法能夠描述爲:將直接尋址指令中的雙字地址加上模塊實際裝入地址與模塊建議裝入地址之差。爲了進行這個運算,須要有3個數據,首先是須要修正的機器碼地址;其次是模塊的建議裝入地址;最後是模塊的實際裝入地址。

在這3個數據中,模塊的建議裝入地址已經在PE文件頭中定義了(編譯後就已經肯定),而模塊的實際裝入地址是Windows裝載器肯定的,到裝載文件的時候天然會知道,因此被保存在重定位表中的僅僅是須要修正的代碼的地址。

事實上正是如此,DLL文件的重定位表中保存的就是一大堆須要修正的代碼的地址。

重定位表通常會被單獨存放在一個可丟棄的以「.reloc」命名的節中,可是這並非必然的,由於重定位表放在其餘節中也是合法的,唯一能夠確定的是,假如重定位表存在的話,它的地址確定能夠在DLL文件頭中的數據目錄中找到。重定位表的位置和大小能夠從數據目錄中的第6個 IMAGE_DATA_DIRECTORY結構中獲取,雖然重定位表中的有用數據是那些須要重定位機器碼的地址指針,但爲了節省空間,DLL文件對存放的方式作了一些優化。

在正常的狀況下,每一個32位的指針佔用4個字節,假若有n個重定位項,那麼重定位表的總大小是4×n字節大小。 直接尋址指令在程序中仍是比較多的,在比較靠近的重定位表項中,32位指針的高位地址老是相同的,假如把這些相近表項的高位地址統一表示,那麼就能夠省略一部分的空間,當按照一個內存頁來分割時,在一個頁面中尋址須要的指針位數是12位(一頁等於4096字節,等於2的12次方),假如將這12位湊齊16 位放入一個字類型的數據中,並用一個附加的雙字來表示頁的起始指針,另外一個雙字來表示本頁中重定位項數的話,那麼佔用的總空間會是4+4+2×n字節大 小,計算一下就能夠發現,當某個內存頁中的重定位項多於4項的時候,後一種方法的佔用空間就會比前面的方法要小。

// 重定向PE用到的地址

void CMemLoadDLL::DoRelocation( void *NewBase)

{

/* 重定位表的結構:

// DWORD sectionAddress, DWORD size (包括本節須要重定位的數據)

// 例如 1000節須要修正5個重定位數據的話,重定位表的數據是

// 00 10 00 00 14 00 00 00 xxxx xxxx xxxx xxxx xxxx 0000

// ———– ———– —-

// 給出節的偏移總尺寸=8+6*2 須要修正的地址用於對齊4字節

// 重定位表是若干個相連,若是address 和 size都是0 表示結束

// 須要修正的地址是12位的,高4位是形態字,intel cpu下是3

*/

//假設NewBase是0×600000,而文件中設置的缺省ImageBase是0×400000,則修正偏移量就是0×200000

DWORD Delta = (DWORD)NewBase - pNTHeader->OptionalHeader.ImageBase;

//注意重定位表的位置可能和硬盤文件中的偏移地址不一樣,應該使用加載後的地址

PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((unsigned long)NewBase

+ pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

while((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //開始掃描重定位表

{

WORD *pLocData = (WORD *)((int)pLoc + sizeof(IMAGE_BASE_RELOCATION));

//計算本節須要修正的重定位項(地址)的數目

int NumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))/sizeof(WORD);

 

for( int i=0 ; i < NumberOfReloc; i++)

{

if( (DWORD)(pLocData[i] & 0xF000) == 0x00003000) //這是一個須要修正的地址

{

// 舉例:

// pLoc->VirtualAddress = 0×1000;

// pLocData[i] = 0×313E; 表示本節偏移地址0×13E處須要修正

// 所以 pAddress = 基地址 + 0×113E

// 裏面的內容是 A1 ( 0c d4 02 10) 彙編代碼是: mov eax , [1002d40c]

// 須要修正1002d40c這個地址

DWORD * pAddress = (DWORD *)((unsigned long)NewBase + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));

 

*pAddress += Delta;

 

}

}

 

//轉移到下一個節進行處理

pLoc = (PIMAGE_BASE_RELOCATION)((DWORD)pLoc + pLoc->SizeOfBlock);

 

}

 

}

H. 讀取DLL的引入表部分,加載引入表部分須要的DLL,並填充須要的函數入口的真實地址

對引入表中的DLL,經過GetModuleHandle得到其加載基地址,若是這些DLL在加載本DLL以前尚未加載,那麼先調用LoadLibrary進行加載,若是加載失敗則不能繼續處理直接報錯,說明找不到依賴的DLL。

//填充引入地址表

BOOL CMemLoadDLL::FillRavAddress(void *pImageBase)

{

// 引入表其實是一個 IMAGE_IMPORT_DESCRIPTOR 結構數組,所有是0表示結束

// 數組定義以下:

//

// DWORD OriginalFirstThunk; // 0表示結束,不然指向未綁定的IAT結構數組

// DWORD TimeDateStamp;

// DWORD ForwarderChain; // -1 if no forwarders

// DWORD Name; // 給出DLL的名字

// DWORD FirstThunk; // 指向IAT結構數組的地址(綁定後,這些IAT裏面就是實際的函數地址)

unsigned long Offset = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress ;

if(Offset == 0) return TRUE; //No Import Table

PIMAGE_IMPORT_DESCRIPTOR pID = (PIMAGE_IMPORT_DESCRIPTOR)((unsigned long) pImageBase + Offset);

while(pID->Characteristics != 0 )

{

PIMAGE_THUNK_DATA pRealIAT = (PIMAGE_THUNK_DATA)((unsigned long)pImageBase + pID->FirstThunk);

 

PIMAGE_THUNK_DATA pOriginalIAT = (PIMAGE_THUNK_DATA)((unsigned long)pImageBase + pID->OriginalFirstThunk);

//獲取DLL的名字

char buf[256]; //DLL name;

//修改,須要將buf清零,不然DLL名稱不對

memset(buf,0,sizeof(buf));

BYTE* pName = (BYTE*)((unsigned long)pImageBase + pID->Name);

for(int i=0;i<256;i++)

{

if(pName[i] == 0)break;

buf[i] = pName[i];

}

HMODULE hDLL = GetModuleHandle(buf);

if(hDLL == NULL)

{

hDLL = LoadLibrary (buf); //有可能依賴的DLL尚未加載,若是沒有加載加載後再判斷是否加載成功

if (hDLL == NULL)

return FALSE; //NOT FOUND DLL

} //獲取DLL中每一個導出函數的地址,填入IAT

//每一個IAT結構是:

// union { PBYTE ForwarderString;

// PDWORD Function;

// DWORD Ordinal;

// PIMAGE_IMPORT_BY_NAME AddressOfData;

// } u1;

// 長度是一個DWORD ,正好容納一個地址。

for(i=0; ;i++)

{

if(pOriginalIAT[i].u1.Function == 0) break;

FARPROC lpFunction = NULL;

if(pOriginalIAT[i].u1.Ordinal & IMAGE_ORDINAL_FLAG) //這裏的值給出的是導出序號

{

lpFunction = GetProcAddress(hDLL, (LPCSTR)(pOriginalIAT[i].u1.Ordinal & 0x0000FFFF));

}

else //按照名字導入

{

//獲取此IAT項所描述的函數名稱

PIMAGE_IMPORT_BY_NAME pByName = (PIMAGE_IMPORT_BY_NAME)

((DWORD)pImageBase + (DWORD)(pOriginalIAT[i].u1.AddressOfData));

// if(pByName->Hint !=0)

// lpFunction = GetProcAddress(hDLL, (LPCSTR)pByName->Hint);

// else

lpFunction = GetProcAddress(hDLL, (char *)pByName->Name);

}

if(lpFunction != NULL) //找到了!

{

pRealIAT[i].u1.Function = (PDWORD) lpFunction;

}

else return FALSE;

}

//move to next

pID = (PIMAGE_IMPORT_DESCRIPTOR)( (DWORD)pID + sizeof(IMAGE_IMPORT_DESCRIPTOR));

}

 

return TRUE;

}

I. 根據DLL每一個節的屬性設置其對應內存頁的讀寫屬性

 

修改段屬性。應該根據每一個段的屬性單獨設置其對應內存頁的屬性。這裏簡化一下。

統一設置成一個屬性PAGE_EXECUTE_READWRITE,若是代碼段沒有執行屬性,調用的時候會產生異常,頁屬性的設置單位至少爲一個頁。

unsigned long old;

VirtualProtect(pMemoryAddress, ImageSize, PAGE_EXECUTE_READWRITE,&old);

 

J. 調用入口函數DLLMain,完成初始化工做

接下來要調用一下DLL的入口函數,作初始化工做,每一個PE文件都有一個OEP, 它就是AddressOfEntryPoint,一切代碼都是從這裏開始,OEP+DLL基地址就是其真實入口地址,固然這個入口地址通常都不是你所寫的main或者DLLMain,而是運行庫提供的一段代碼,先完成全局變量的一些初始化和庫函數相關的初始化等,而這段代碼最後會調用真正的main或者DLLMain。

pDLLMain = (ProcDLLMain)(pNTHeader->OptionalHeader.AddressOfEntryPoint +(DWORD) pMemoryAddress);

BOOL InitResult = pDLLMain((HINSTANCE)pMemoryAddress,DLL_PROCESS_ATTACH,0);

if(!InitResult) //初始化失敗

{

pDLLMain((HINSTANCE)pMemoryAddress,DLL_PROCESS_DETACH,0);

VirtualFree(pMemoryAddress,0,MEM_RELEASE);

pDLLMain = NULL;

 

return FALSE;

}

 

K.  保存DLL的基地址(即分配的內存塊起始地址),用於查找DLL的導出函數

 

//修正基地址

pNTHeader->OptionalHeader.ImageBase = (DWORD)pMemoryAddress;

//MemGetProcAddress函數從dll中獲取指定函數的地址

//返回值:成功返回函數地址 , 失敗返回NULL

//lpProcName: 要查找函數的名字或者序號

FARPROC CMemLoadDll::MemGetProcAddress(LPCSTR lpProcName)

{

if(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0 ||

pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size == 0)

return NULL;

if(!isLoadOk) return NULL;

DWORD OffsetStart = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

DWORD Size = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pImageBase + pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

int iBase = pExport->Base;

int iNumberOfFunctions = pExport->NumberOfFunctions;

int iNumberOfNames = pExport->NumberOfNames; //<= iNumberOfFunctions

LPDWORD pAddressOfFunctions = (LPDWORD)(pExport->AddressOfFunctions + pImageBase);

LPWORD pAddressOfOrdinals = (LPWORD)(pExport->AddressOfNameOrdinals + pImageBase);

LPDWORD pAddressOfNames = (LPDWORD)(pExport->AddressOfNames + pImageBase);

int iOrdinal = -1;

if(((DWORD)lpProcName & 0xFFFF0000) == 0) //IT IS A ORDINAL!

{

iOrdinal = (DWORD)lpProcName & 0x0000FFFF - iBase;

}

else //use name

{

int iFound = -1;

for(int i=0;i<iNumberOfNames;i++)

{

char* pName= (char* )(pAddressOfNames[i] + pImageBase);

if(strcmp(pName, lpProcName) == 0)

{

iFound = i; break;

}

}

if(iFound >= 0)

{

iOrdinal = (int)(pAddressOfOrdinals[iFound]);

}

}

if(iOrdinal < 0 || iOrdinal >= iNumberOfFunctions ) return NULL;

else

{

DWORD pFunctionOffset = pAddressOfFunctions[iOrdinal];

if(pFunctionOffset > OffsetStart && pFunctionOffset < (OffsetStart+Size))//maybe Export Forwarding

return NULL;

else return (FARPROC)(pFunctionOffset + pImageBase);

}

}

 

L. 不須要DLL的時候,釋放所分配的虛擬內存,釋放全部動態申請的內存

 

CMemLoadDll::~CMemLoadDll()

{

if(isLoadOk)

{

ASSERT(pImageBase != NULL);

ASSERT(pDllMain != NULL);

//脫鉤,準備卸載dll

pDllMain((HINSTANCE)pImageBase,DLL_PROCESS_DETACH,0);

VirtualFree((LPVOID)pImageBase, 0, MEM_RELEASE);

}

}

四、所有詳細代碼

 

//如下代碼通過Win2k Sp4/WinXp Sp2下測試經過

 

// MemLoadDll.h: interface for the CMemLoadDll class.

//

//////////////////////////////////////////////////////////////////////

 

#if !defined(AFX_MEMLOADDLL_H__E1F5150A_B534_4940_9FBF_1E6CA0E50576__INCLUDED_)

#define AFX_MEMLOADDLL_H__E1F5150A_B534_4940_9FBF_1E6CA0E50576__INCLUDED_

 

#if _MSC_VER > 1000

#pragma once

#endif // _MSC_VER > 1000

 

 

typedef BOOL (__stdcall *ProcDllMain)(HINSTANCE, DWORD, LPVOID );

 

class CMemLoadDll

{

public:

CMemLoadDll();

virtual ~CMemLoadDll();

 

BOOL MemLoadLibrary( void* lpFileData , int DataLength); // Dll file data buffer

FARPROC MemGetProcAddress(LPCSTR lpProcName);

private:

BOOL isLoadOk;

BOOL CheckDataValide(void* lpFileData, int DataLength);

int CalcTotalImageSize();

void CopyDllDatas(void* pDest, void* pSrc);

BOOL FillRavAddress(void* pBase);

void DoRelocation(void* pNewBase);

int GetAlignedSize(int Origin, int Alignment);

 

private:

 

ProcDllMain pDllMain;

 

 

private:

 

DWORD pImageBase;

PIMAGE_DOS_HEADER pDosHeader;

PIMAGE_NT_HEADERS pNTHeader;

PIMAGE_SECTION_HEADER pSectionHeader;

 

 

};

 

#endif // !defined(AFX_MEMLOADDLL_H__E1F5150A_B534_4940_9FBF_1E6CA0E50576__INCLUDED_)

 

// MemLoadDll.cpp: implementation of the CMemLoadDll class.

//

//////////////////////////////////////////////////////////////////////

 

#include "stdafx.h"

#include "MemLoadDll.h"

 

#ifdef _DEBUG

#undef THIS_FILE

static char THIS_FILE[]=__FILE__;

#define new DEBUG_NEW

#endif

 

//////////////////////////////////////////////////////////////////////

// Construction/Destruction

//////////////////////////////////////////////////////////////////////

CMemLoadDll::CMemLoadDll()

{

isLoadOk = FALSE;

pImageBase = NULL;

pDllMain = NULL;

}

CMemLoadDll::~CMemLoadDll()

{

if(isLoadOk)

{

ASSERT(pImageBase != NULL);

ASSERT(pDllMain != NULL);

//脫鉤,準備卸載dll

pDllMain((HINSTANCE)pImageBase,DLL_PROCESS_DETACH,0);

 

VirtualFree((LPVOID)pImageBase, 0, MEM_RELEASE);

}

}

 

//MemLoadLibrary函數從內存緩衝區數據中加載一個dll到當前進程的地址空間,缺省位置0×10000000

//返回值: 成功返回TRUE , 失敗返回FALSE

//lpFileData: 存放dll文件數據的緩衝區

//DataLength: 緩衝區中數據的總長度

BOOL CMemLoadDll::MemLoadLibrary(void* lpFileData, int DataLength)

{

if(pImageBase != NULL)

{

return FALSE; //已經加載一個dll,尚未釋放,不能加載新的dll

}

 

//檢查數據有效性,並初始化

if(!CheckDataValide(lpFileData, DataLength))return FALSE;

 

//計算所需的加載空間

int ImageSize = CalcTotalImageSize();

 

if(ImageSize == 0) return FALSE;

 

// 分配虛擬內存

//void *pMemoryAddress = VirtualAlloc((LPVOID)0x10000000, ImageSize,MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);

 

//修改,不指定dll基址申請內存

 

void *pMemoryAddress = VirtualAlloc((LPVOID)NULL, ImageSize,MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);

 

if(pMemoryAddress == NULL)

{

return FALSE;

}

else

{

CopyDllDatas(pMemoryAddress, lpFileData); //複製dll數據,並對齊每一個段

//重定位信息

if(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress >0

&& pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size>0)

{

DoRelocation(pMemoryAddress);

}

//填充引入地址表

if(!FillRavAddress(pMemoryAddress)) //修正引入地址表失敗

{

VirtualFree(pMemoryAddress,0,MEM_RELEASE);

 

return FALSE;

}

//修改頁屬性。應該根據每一個頁的屬性單獨設置其對應內存頁的屬性。這裏簡化一下。

//統一設置成一個屬性PAGE_EXECUTE_READWRITE

 

unsigned long old;

 

VirtualProtect(pMemoryAddress, ImageSize, PAGE_EXECUTE_READWRITE,&old);

 

}

 

//修正基地址

pNTHeader->OptionalHeader.ImageBase = (DWORD)pMemoryAddress;

 

//接下來要調用一下dll的入口函數,作初始化工做。

 

pDllMain = (ProcDllMain)(pNTHeader->OptionalHeader.AddressOfEntryPoint +(DWORD) pMemoryAddress);

 

BOOL InitResult = pDllMain((HINSTANCE)pMemoryAddress,DLL_PROCESS_ATTACH,0);

 

if(!InitResult) //初始化失敗

{

pDllMain((HINSTANCE)pMemoryAddress,DLL_PROCESS_DETACH,0);

 

VirtualFree(pMemoryAddress,0,MEM_RELEASE);

 

pDllMain = NULL;

 

return FALSE;

}

 

isLoadOk = TRUE;

 

pImageBase = (DWORD)pMemoryAddress;

 

return TRUE;

}

 

//MemGetProcAddress函數從dll中獲取指定函數的地址

//返回值: 成功返回函數地址 , 失敗返回NULL

//lpProcName: 要查找函數的名字或者序號

FARPROC CMemLoadDll::MemGetProcAddress(LPCSTR lpProcName)

{

if(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress == 0 ||

pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size == 0)

return NULL;

 

if(!isLoadOk) return NULL;

 

DWORD OffsetStart = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

 

DWORD Size = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;

 

PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pImageBase + pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

 

int iBase = pExport->Base;

 

int iNumberOfFunctions = pExport->NumberOfFunctions;

 

int iNumberOfNames = pExport->NumberOfNames; //<= iNumberOfFunctions

 

LPDWORD pAddressOfFunctions = (LPDWORD)(pExport->AddressOfFunctions + pImageBase);

 

LPWORD pAddressOfOrdinals = (LPWORD)(pExport->AddressOfNameOrdinals + pImageBase);

 

LPDWORD pAddressOfNames = (LPDWORD)(pExport->AddressOfNames + pImageBase);

 

int iOrdinal = -1;

 

if(((DWORD)lpProcName & 0xFFFF0000) == 0) //IT IS A ORDINAL!

{

iOrdinal = (DWORD)lpProcName & 0x0000FFFF - iBase;

}

else //use name

{

int iFound = -1;

 

for(int i=0;i<iNumberOfNames;i++)

{

char* pName= (char* )(pAddressOfNames[i] + pImageBase);

if(strcmp(pName, lpProcName) == 0)

{

iFound = i; break;

}

}

 

if(iFound >= 0)

{

iOrdinal = (int)(pAddressOfOrdinals[iFound]);

}

}

 

if(iOrdinal < 0 || iOrdinal >= iNumberOfFunctions ) return NULL;

else

{

DWORD pFunctionOffset = pAddressOfFunctions[iOrdinal];

 

if(pFunctionOffset > OffsetStart && pFunctionOffset < (OffsetStart+Size))//maybe Export Forwarding

return NULL;

else return (FARPROC)(pFunctionOffset + pImageBase);

}

 

}

 

// 重定向PE用到的地址

void CMemLoadDll::DoRelocation( void *NewBase)

{

/* 重定位表的結構:

// DWORD sectionAddress, DWORD size (包括本節須要重定位的數據)

// 例如 1000節須要修正5個重定位數據的話,重定位表的數據是

// 00 10 00 00 14 00 00 00 xxxx xxxx xxxx xxxx xxxx 0000

// ———– ———– —-

// 給出節的偏移 總尺寸=8+6*2 須要修正的地址 用於對齊4字節

// 重定位表是若干個相連,若是address 和 size都是0 表示結束

// 須要修正的地址是12位的,高4位是形態字,intel cpu下是3

*/

//假設NewBase是0×600000,而文件中設置的缺省ImageBase是0×400000,則修正偏移量就是0×200000

 

DWORD Delta = (DWORD)NewBase - pNTHeader->OptionalHeader.ImageBase;

 

//注意重定位表的位置可能和硬盤

文件中的偏移地址不一樣,應該使用加載後的地址

PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((unsigned long)NewBase

+ pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

 

while((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //開始掃描重定位表

{

WORD *pLocData = (WORD *)((int)pLoc + sizeof(IMAGE_BASE_RELOCATION));

 

//計算本節須要修正的重定位項(地址)的數目

int NumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION))/sizeof(WORD);

 

for( int i=0 ; i < NumberOfReloc; i++)

{

if( (DWORD)(pLocData[i] & 0xF000) == 0x00003000) //這是一個須要修正的地址

{

// 舉例:

// pLoc->VirtualAddress = 0×1000;

// pLocData[i] = 0×313E; 表示本節偏移地址0×13E處須要修正

// 所以 pAddress = 基地址 + 0×113E

// 裏面的內容是 A1 ( 0c d4 02 10) 彙編代碼是: mov eax , [1002d40c]

// 須要修正1002d40c這個地址

DWORD * pAddress = (DWORD *)((unsigned long)NewBase + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));

 

*pAddress += Delta;

 

}

}

 

//轉移到下一個節進行處理

pLoc = (PIMAGE_BASE_RELOCATION)((DWORD)pLoc + pLoc->SizeOfBlock);

 

}

 

}

 

//填充引入地址表

BOOL CMemLoadDll::FillRavAddress(void *pImageBase)

{

// 引入表其實是一個 IMAGE_IMPORT_DESCRIPTOR 結構數組,所有是0表示結束

// 數組定義以下:

//

// DWORD OriginalFirstThunk; // 0表示結束,不然指向未綁定的IAT結構數組

// DWORD TimeDateStamp;

// DWORD ForwarderChain; // -1 if no forwarders

// DWORD Name; // 給出dll的名字

// DWORD FirstThunk; // 指向IAT結構數組的地址(綁定後,這些IAT裏面就是實際的函數地址)

unsigned long Offset = pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress ;

 

if(Offset == 0) return TRUE; //No Import Table

 

PIMAGE_IMPORT_DESCRIPTOR pID = (PIMAGE_IMPORT_DESCRIPTOR)((unsigned long) pImageBase + Offset);

 

while(pID->Characteristics != 0 )

{

PIMAGE_THUNK_DATA pRealIAT = (PIMAGE_THUNK_DATA)((unsigned long)pImageBase + pID->FirstThunk);

 

PIMAGE_THUNK_DATA pOriginalIAT = (PIMAGE_THUNK_DATA)((unsigned long)pImageBase + pID->OriginalFirstThunk);

 

//獲取dll的名字

char buf[256]; //dll name;

 

//修改,須要buf清零,不然dll名稱不對

memset(buf,0,sizeof(buf));

 

BYTE* pName = (BYTE*)((unsigned long)pImageBase + pID->Name);

 

for(int i=0;i<256;i++)

{

if(pName[i] == 0)break;

 

buf[i] = pName[i];

}

 

HMODULE hDll = GetModuleHandle(buf);

 

if(hDll == NULL)

{

hDll = LoadLibrary (buf); //有可能依賴的dll尚未加載,若是沒有加載加載後再判斷是否加載成功

 

if (hDll == NULL)

 

return FALSE; //NOT FOUND DLL

 

} //獲取DLL中每一個導出函數的地址,填入IAT

 

//每一個IAT結構是 :

// union { PBYTE ForwarderString;

// PDWORD Function;

// DWORD Ordinal;

// PIMAGE_IMPORT_BY_NAME AddressOfData;

// } u1;

// 長度是一個DWORD ,正好容納一個地址。

for(i=0; ;i++)

{

if(pOriginalIAT[i].u1.Function == 0) break;

 

FARPROC lpFunction = NULL;

 

if(pOriginalIAT[i].u1.Ordinal & IMAGE_ORDINAL_FLAG) //這裏的值給出的是導出序號

{

lpFunction = GetProcAddress(hDll, (LPCSTR)(pOriginalIAT[i].u1.Ordinal & 0x0000FFFF));

}

else //按照名字導入

{

//獲取此IAT項所描述的函數名稱

PIMAGE_IMPORT_BY_NAME pByName = (PIMAGE_IMPORT_BY_NAME)

((DWORD)pImageBase + (DWORD)(pOriginalIAT[i].u1.AddressOfData));

// if(pByName->Hint !=0)

// lpFunction = GetProcAddress(hDll, (LPCSTR)pByName->Hint);

// else

lpFunction = GetProcAddress(hDll, (char *)pByName->Name);

}

 

if(lpFunction != NULL) //找到了!

{

pRealIAT[i].u1.Function = (PDWORD) lpFunction;

}

else return FALSE;

}

 

//move to next

pID = (PIMAGE_IMPORT_DESCRIPTOR)( (DWORD)pID + sizeof(IMAGE_IMPORT_DESCRIPTOR));

 

}

 

return TRUE;

}

 

//CheckDataValide函數用於檢查緩衝區中的數據是否有效的dll文件

//返回值: 是一個可執行的dll則返回TRUE,不然返回FALSE。

//lpFileData: 存放dll數據的內存緩衝區

//DataLength: dll文件的長度

BOOL CMemLoadDll::CheckDataValide(void* lpFileData, int DataLength)

{

//檢查長度

if(DataLength < sizeof(IMAGE_DOS_HEADER)) return FALSE;

 

pDosHeader = (PIMAGE_DOS_HEADER)lpFileData; // DOS頭

 

//檢查dos頭的標記

if(pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) return FALSE; //0*5A4D : MZ

 

//檢查長度

if((DWORD)DataLength < (pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS)) ) return FALSE;

 

//取得pe頭

pNTHeader = (PIMAGE_NT_HEADERS)( (unsigned long)lpFileData + pDosHeader->e_lfanew); // PE頭

//檢查pe頭的合法性

if(pNTHeader->Signature != IMAGE_NT_SIGNATURE) return FALSE; //0*00004550 : PE00

 

if((pNTHeader->FileHeader.Characteristics & IMAGE_FILE_DLL) == 0) //0*2000 : File is a DLL

return FALSE;

 

if((pNTHeader->FileHeader.Characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) == 0) //0*0002 : 指出文件能夠運行

return FALSE;

 

if(pNTHeader->FileHeader.SizeOfOptionalHeader != sizeof(IMAGE_OPTIONAL_HEADER)) return FALSE;

 

 

//取得節表(段表)

pSectionHeader = (PIMAGE_SECTION_HEADER)((int)pNTHeader + sizeof(IMAGE_NT_HEADERS));

 

//驗證每一個節表的空間

for(int i=0; i< pNTHeader->FileHeader.NumberOfSections; i++)

{

if((pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData) > (DWORD)DataLength)return FALSE;

}

 

 

return TRUE;

}

 

//計算對齊邊界

int CMemLoadDll::GetAlignedSize(int Origin, int Alignment)

{

return (Origin + Alignment - 1) / Alignment * Alignment;

}

//計算整個dll映像文件的尺寸

int CMemLoadDll::CalcTotalImageSize()

{

int Size;

 

if(pNTHeader == NULL)return 0;

 

int nAlign = pNTHeader->OptionalHeader.SectionAlignment; //段對齊字節數

 

// 計算全部頭的尺寸。包括dos, coff, pe頭 和 段表的大小

Size = GetAlignedSize(pNTHeader->OptionalHeader.SizeOfHeaders, nAlign);

 

// 計算全部節的大小

for(int i=0; i < pNTHeader->FileHeader.NumberOfSections; ++i)

{

//獲得該節的大小

int CodeSize = pSectionHeader[i].Misc.VirtualSize ;

int LoadSize = pSectionHeader[i].SizeOfRawData;

int MaxSize = (LoadSize > CodeSize)?(LoadSize):(CodeSize);

 

int SectionSize = GetAlignedSize(pSectionHeader[i].VirtualAddress + MaxSize, nAlign);

if(Size < SectionSize)

Size = SectionSize; //Use the Max;

}

 

return Size;

 

}

//CopyDllDatas函數將dll數據複製到指定內存區域,並對齊全部節

//pSrc: 存放dll數據的原始緩衝區

//pDest:目標內存地址

void CMemLoadDll::CopyDllDatas(void* pDest, void* pSrc)

{

// 計算須要複製的PE頭+段表字節數

 

int HeaderSize = pNTHeader->OptionalHeader.SizeOfHeaders;

 

int SectionSize = pNTHeader->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);

 

int MoveSize = HeaderSize + SectionSize;

 

//複製頭和段信息

memmove(pDest, pSrc, MoveSize);

 

 

//複製每一個節

for(int i=0; i < pNTHeader->FileHeader.NumberOfSections; ++i)

{

if(pSectionHeader[i].VirtualAddress == 0 || pSectionHeader[i].SizeOfRawData == 0) continue;

 

// 定位該節在內存中的位置

void *pSectionAddress = (void *)((unsigned long)pDest + pSectionHeader[i].VirtualAddress);

 

// 複製段數據到虛擬內存

memmove((void *)pSectionAddress,

(void *)((DWORD)pSrc + pSectionHeader[i].PointerToRawData),

pSectionHeader[i].SizeOfRawData);

 

}

 

//修正指針,指向新分配的內存

//新的dos頭

pDosHeader = (PIMAGE_DOS_HEADER)pDest;

//新的pe頭地址

pNTHeader = (PIMAGE_NT_HEADERS)((int)pDest + (pDosHeader->e_lfanew));

//新的節表地址

pSectionHeader = (PIMAGE_SECTION_HEADER)((int)pNTHeader + sizeof(IMAGE_NT_HEADERS));

 

return ;

}

 

 

 

//加載資源DLL

#define strKey (char)0x15

char DLLtype[4]={'D' ^ strKey ,'l'^ strKey,'l'^ strKey,0x00};

HINSTANCE hinst=AfxGetInstanceHandle();

HRSRC hr=NULL;

HGLOBAL hg=NULL;

//對資源名稱字符串進行簡單的異或操做,達到不能經過外部字符串參考下斷點

for(int i=0;i<sizeof(DLLtype)-1;i++)

{

DLLtype[i]^=strKey;

}

hr=FindResource(hinst,MAKEINTRESOURCE(IDR_DLL),TEXT(DLLtype));

if (NULL == hr) return FALSE;

//獲取資源的大小

DWORD dwSize = SizeofResource(hinst, hr);

if (0 == dwSize) return FALSE;

hg=LoadResource(hinst,hr);

if (NULL == hg) return FALSE;

//鎖定資源

LPVOID pBuffer =(LPSTR)LockResource(hg);

if (NULL == pBuffer) return FALSE;

 

//對pBuffer進行處理

pMemLoadDll=new CMemLoadDll();

if(pMemLoadDll->MemLoadLibrary(pBuffer, dwSize)) //加載dll到當前進程的地址空間

{

for(int i=0;i<sizeof(dllname)-1;i++)

{

dllname[i]^=strKey;

}

 

SENSE3 = (DllSENSE3)pMemLoadDll->MemGetProcAddress(dllname);

if(SENSE3 == NULL)

{

return TRUE;

}

}

 

FreeResource(hg); //在資源使用完畢後咱們不須要使用UnlockResource和FreeResource來手動地釋放資源,由於它們都是16位Windows遺留下來的,在Win32中,在使用完畢後系統會自動回收。

相關文章
相關標籤/搜索