目前很多敏感和重要的DLL(Dynamic-link library) 都沒有提供靜態版本供編譯器進行靜態連接(.lib文件),即使提供了靜態版本也因為兼容性問題導致無法使用,而只提供DLL版本,并且很多專業軟件的授權部分的API,都是單獨提供一個DLL來完成,而主模塊通過調用DLL中的接口來完成授權功能。雖然這些軟件一般都采用了加殼和反調試等保護,但是一旦這些功能失去作用,比如脫殼,反反調試,HOOK API或者干脆寫一個仿真的授權DLL(模擬授權DLL的所有導出函數接口),然后仿真的DLL再調用授權DLL,這樣所有的輸入首先被仿真DLL截獲再傳遞給授權DLL,而授權DLL的輸出也首先傳遞給仿真DLL再傳遞給主程序,這樣就可以輕易的監視二者之間的輸入輸出之間的關系,從而輕易的截獲DLL中的授權信息進行修改再返回給主程序。
2、目前隱式調用敏感DLL中可能存在的安全隱患
以下通過兩個軟件的授權DLL來說明這種問題的嚴重性。如下是兩個軟件中授權DLL的部分信息,如下圖所示:
?
(圖1)
通過工具OllyICE可以輕易的看出IoMonitor.exe調用授權DLL(XKeyAPI.DLL),這樣就很容易在調用這些API的地方設置斷點,然后判斷輸入輸出的關系,從而達到破解的目的。
?
(圖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 后,返回值就是字符串的首地址,通過這種簡單的修改就可以達到破解的目的,說明這種隱式的調用是非常危險的。
3、模擬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加載器同樣的工作即可。
加載過程大致包括以下幾個部分:
1
、調用
API
讀取
DLL
資源數據拷貝到內存中
2
、調用解壓和解密函數對內存中的DLL
進行處理
3
、檢查DOS
頭和PE
頭判斷是否為合法的PE
格式
4
、計算加載該DLL
所需的虛擬地址空間大小
5
、向操作系統申請指定大小的虛擬地址空間并提交
6
、將DLL
數據復制到所分配的虛擬內存塊中,注意文件段對齊方式和內存段對齊方式
7
、對每個 DLL
文件來說都存在一個重定位節(.reloc)
,用于記錄DLL
文件的重定位信息,需要處理重定位信息
8
、讀取DLL
的引入表部分,加載引入表部分需要的DLL
,并填充需要的函數入口的真實地址
9
、根據DLL
每個節的屬性設置其對應內存頁的讀寫屬性
10
、調用入口函數DLLMain
,完成初始化工作
11
、保存DLL
的基地址(即分配的內存塊起始地址),
用于查找DLL
的導出函數
12
、不需要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);
}
}
4、全部詳細代碼
//
以下代碼經過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
中,在使用完畢后系統會自動回收。