【技術分享】結合實例淺析殼編寫的流程與難點
前言
- 前段時間自己學習了一下殼的編寫,發現網上相關資料不是非常全,而且講解順序不太對胃口。因此參考了《黑客免殺攻防》中的代碼對DLL型殼編寫的結構進行了一次歸納整理,并附上相應代碼解析。
一、殼的簡介
1.1 殼的簡介
- 在日常生活中我們所接觸到的大量瓜果蔬菜存在殼,例如花生殼,瓜子殼等,這些殼是為了保護自身而存在。同理,對于我們在逆向工程中接觸到的殼同樣是出于保護自身減少被破解的可能性而研發的。
- 一般大部分的殼可以分為兩種,加密殼與壓縮殼。壓縮殼以通過壓縮算法減少程序體積為目的,而加密殼則以干擾逆向分析為主要目的。但是兩者都會對靜態分析產生較大的影響
- 例如惡意代碼分析中常見的特征匹配的辦法,以其中最廣為使用的yara為例,其工作的原理就是匹配一串特殊的數據,也就是特征。這段數據是一個惡意軟件的核心功能代碼,而且正常代碼,甚至包括其他惡意代碼幾乎沒有這段代碼。而哪怕是一個簡單的壓縮殼都有可能導致這段特征產生嚴重的變形進而干擾分析。
1.2 殼的原理
- 一般原始程序結構如下。在操作系統加載該程序后,程序從PE頭指定的OEP(入口點)開始執行

而加殼之后,程序的.text段就被加密了(有些殼還會加密IAT表,資源段等等),程序的入口點指向殼添加的代碼的入口(Stub的入口),Stub將程序的更改恢復(例如解密.text,IAT等等),最后執行完后跳轉到原始的OEP。

- 在執行到原先的OEP后,實際上程序就與加殼前沒有差別了(除了添加的Stub的區塊),而Stub的區塊也并非必須。此處先介紹一個概念,區塊對齊。在內存中與在硬盤中,程序都存在一個對齊值,例如內存中是SectionAlignment,文件中是FileAlignment。當一個區塊的大小不是對齊值的整數倍時,會被填充成對齊值的整數部分。也就是說在這種情況下區塊可能存在一部分的空余空間。我們可以將Stub填寫到該空余空間中,這樣文件大小就不會改變,這也是不少病毒的感染做法。
1.3 殼的編寫流程以及其中的問題
- 殼的編寫流程非常簡單,流程如下:
- 編寫Stub代碼將原程序加載到內存,加密.text段(或更多,例如資源,IAT等等)在原程序中添加一個Section,將恢復原程序的代碼(Stub)添加到原程序中。在Stub的Section結尾添加一個跳轉,跳轉到原始的OEP。將程序的OEP修改到Stub的起始地址。這個過程看似沒有什么難度,實際上存在一些比較難以實現的點。DLL編譯過程中生成的引導代碼會導致代碼不可控,大概率出現不
- 涉及到的地址會出現很大問題(jmp短跳轉用的是相對地址,但是長跳轉和大部分call用的是絕對地址,移植到原程序后會出現對應不上的問題)
- 由于無法將DLL的導入表移植過去,DLL中的API調用將全部無效。
- 加密text段時我們無法獲得目標程序的信息,也就無法指定起始與結束加密位置
- 編寫的Stub格式代碼如果純粹用匯編寫,之后可移植性不強,因此建議編譯為dll格式。
- 如果將stub的dll拷貝到原程序中
- 由于程序在內存中和在硬盤中的狀態不同(主要體現在FileAlignment與SectionAlignment的不同上,SectionAlignment一般大于FileAlignment),進行FileOffset與VirtualAddress轉換時容易引起混淆。實際上讀入內存時無論是使用硬盤中的布局或者內存中的布局都是可以的,但需要注意兩者的不同
- 那么如何解決?
- 引導代碼的問題,我們可以加上naked的關鍵字不生成引導代碼,因為我們的DLLMain不需要執行,所以實際上不會影響。
- PE文件PE頭存在一個重定位的Table,我們可以根據這張表修復重定位,具體修復位置根據我們的程序基址與DLL的基址來決定。
- 對于導入表不存在的問題,我們使用fs的特性定位GetProcAddress,之后通過GetProcAddress獲取API地址,動態加載API。
- 對于無法獲得信息的問題,我們導出一個全局變量來獲取信息,使得用于加殼的程序(不是被加殼程序)能夠從外部讀取并寫入這個全局變量。
- 對于移植性,我們的Stub編譯為DLL格式
- 對于DLL拷貝的問題
- 對于程序內存與硬盤中不同的布局,我們直接指定加載原程序的方式
二、殼代碼的編寫
該部分主要包括:
- 初始化,將所有區塊合并
- 獲取.text段必要信息,導出變量
- 添加API調用
2.1 初始化操作
- 之前我們已經提過使用編寫DLL的方式來完成Stub的實現。在此為了方便起見,我們只使用異或來加密.text段。
- 由于我們移植stub時候只需要一個代碼段,所以dll編譯的時候我們指定多個段合并
#pragma comment(linker, "/merge:.data=.text") // 將.data合并到.text#pragma comment(linker, "/merge:.rdata=.text") // 將.rdata合并到.text#pragma comment(linker, "/section:.text,RWE") // 將.text段的屬性設置為可讀、可寫、可執行
2.2 獲取加密.text時的必要信息
之后我們導出一個全局變量用于獲取信息
typedef struct _GLOBAL_PARAM{ BOOL bShowMessage; // 是否顯示MessageBox DWORD dwOEP; // 程序入口點 PBYTE lpStartVA; // 起始.text段地址 PBYTE lpEndVA; // 結束.text段地址}GLOBAL_PARAM, *PGLOBAL_PARAM;extern "C"__declspec(dllexport) GLOBAL_PARAM g_stcParam;
之后我們手動定義一個入口函數,如我們前面所提到的原因所說,我們需要將dll的加載代碼去掉,所以我們使用naked關鍵字修飾
#pragma comment(linker, "/entry:"StubEntryPoint"") // 指定程序入口函數為StubEntryPoint()void start(){//這里存放對被加殼文件的操作}void __declspec(naked) StubEntryPoint(){ __asm sub esp, 0x50; // 分配0x50的棧空間 start(); // 執行Stub代碼 __asm add esp, 0x50; // 平衡堆棧 __asm retn;}
顯然,我們的Stub的核心之一就是解密,我們需要編寫一個解密函數,從導入變量中獲取起始地址與結束地址,之后解密。
void Decrypt(){// 在導出的全局變量中讀取需解密區域的起始與結束VA PBYTE lpStart = g_stcParam.lpStartVA; PBYTE lpEnd = g_stcParam.lpEndVA;while (lpStart < lpEnd) { *lpStart ^= 0x15; lpStart++; }}
這樣我們的原始的Stub就完成了。
2.3 添加API調用功能
- 但是這樣還不夠,為了方便擴展,我們需要添加對API調用的支持。例如,我們的程序可能會出現一些兼容性問題,那么讓它觸發異常時ExitProcess(0)就能提高兼容性。
- 這一部分主要包括
- 獲取kernel32.dll基地址
- 根據kernel32.dll基地址獲取GetProcAddress基地址
- 根據GetProcAddress獲得API地址并定義API指針
2.3.1 獲取kernel32.dlll的基地址
- 我們在第一部分的最后一段已經提到過,由于沒有導入表,我們需要通過GetProcAddress動態獲得API地址后調用。而GetProcAddress位于kernel32.dll中,所以我們的問題就從調用API轉換為了獲取kernel32的地址。
- 查詢資料可得,fs寄存器指向TEB,而TEB結構我們可以在Windbg中查看。
kd> dt _tebnt!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : Ptr32 Void +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : Ptr32 Void +0x02c ThreadLocalStoragePointer : Ptr32 Void +0x030 ProcessEnvironmentBlock : Ptr32 _PEB
PEB在TEB偏移0x30的位置上,而查看PEB結構如下
kd> dt _pebnt!_PEB +0x000 InheritedAddressSpace : UChar +0x001 ReadImageFileExecOptions : UChar +0x002 BeingDebugged : UChar +0x003 SpareBool : UChar +0x004 Mutant : Ptr32 Void +0x008 ImageBaseAddress : Ptr32 Void +0x00c Ldr : Ptr32 _PEB_LDR_DATA
從PEB中獲得_PEB_LDR_DATA,windbg查看LDR_DATA定義
0:000> dt 0x77847880 _PEB_LDR_DATA ntdll!_PEB_LDR_DATA +0x000 Length : 0x30 +0x004 Initialized : 0x1 '' +0x008 SsHandle : (null) +0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x3126f8 - 0x36bd60 ] +0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x312700 - 0x36bd68 ] +0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x312798 - 0x36bd70 ] +0x024 EntryInProgress : (null) +0x028 ShutdownInProgress : 0 '' +0x02c ShutdownThreadId : (null)
從MSDN上獲得對InInitializationOrderModuleList的描述(MSDN上對_PEB_LDR_DATA定義與windbg上有所不同,此處以windbg為準)
The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure.
查看MSDN的LDR_DATA_TABLE_ENTRY的定義
typedef struct _LDR_DATA_TABLE_ENTRY {PVOID Reserved1[2];LIST_ENTRY InMemoryOrderLinks;PVOID Reserved2[2];PVOID DllBase;PVOID EntryPoint;PVOID Reserved3;UNICODE_STRING FullDllName;BYTE Reserved4[8];PVOID Reserved5[3];union {ULONG CheckSum;PVOID Reserved6;};ULONG TimeDateStamp;} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
此時我們就看到了我們希望得到的DllBase,我們可以遍歷搜索,當名字與”kernel32.dll”重合時獲取DllBase,而由于每個操作系統中的Dll加載順序恒定,kernel32.dll一般都是第二個加載的。因此我們可以通過如下方式獲取
extern DWORD GetKernel32Base(); // 獲取Kernel32.dll的模塊基址DWORD GetKernel32Base(){DWORD dwKernel32Addr = 0;__asm{push eaxmov eax,dword ptr fs:[0x30] // eax = PEB的地址mov eax,[eax+0x0C] // eax = 指向PEB_LDR_DATA結構的指針mov eax,[eax+0x1C] // eax = 模塊初始化鏈表的頭指針InInitializationOrderModuleListmov eax,[eax] // eax = 列表中的第二個條目mov eax,[eax+0x08] // eax = 獲取到的Kernel32.dll基址mov dwKernel32Addr,eaxpop eax}return dwKernel32Addr;}
- 2.3.2 獲取GetProcAddress的地址
- 獲得基址之后,我們根據PE結構從kernel32.dll中的導出表中獲得”GetProcAddress”
- 我們可以通過查詢資料得到,IAT表所屬的IMAGE_DIRECTORY_ENTRY_EXPORT位于IMAGE_NT_HEADER->IMAGE_OPTIONAL_HEADER->DataDirectory,那我們先獲取導出表
DWORD dwAddrBase = GetKernel32Base(); // 1. 獲取DOS頭、NT頭PIMAGE_DOS_HEADER pDos_Header;PIMAGE_NT_HEADERS pNt_Header;pDos_Header = (PIMAGE_DOS_HEADER)dwAddrBase;pNt_Header = (PIMAGE_NT_HEADERS)(dwAddrBase + pDos_Header->e_lfanew); // 2. 獲取導出表項PIMAGE_DATA_DIRECTORY pDataDir;PIMAGE_EXPORT_DIRECTORY pExport;pDataDir = pNt_Header->OptionalHeader.DataDirectory; pDataDir = &pDataDir[IMAGE_DIRECTORY_ENTRY_EXPORT];pExport = (PIMAGE_EXPORT_DIRECTORY)(dwAddrBase + pDataDir->VirtualAddress);...
然后我們查看IMAGE_EXPORT_DIRECTORY的結構,如下所示。此處需要注意的是AddressOf…三個都是指針
typedef struct _IMAGE_EXPORT_DIRECTORY {DWORD Characteristics;DWORD TimeDateStamp;WORD MajorVersion;WORD MinorVersion;DWORD Name;DWORD Base;DWORD NumberOfFunctions;DWORD NumberOfNames;DWORD AddressOfFunctions; // RVA from base of imageDWORD AddressOfNames; // RVA from base of imageDWORD AddressOfNameOrdinals; // RVA from base of image} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
三個指針結構如下所示

根據上述結構,我們可以根據函數名字遍歷鏈表獲得GetProcAddress地址
// 3、獲取導出表的必要信息 DWORD dwModOffset = pExport->Name; // 模塊的名稱 DWORD dwFunCount = pExport->NumberOfFunctions; // 導出函數的數量 DWORD dwNameCount = pExport->NumberOfNames; // 導出名稱的數量 PDWORD pEAT = (PDWORD)(dwAddrBase + pExport->AddressOfFunctions); // 獲取地址表的RVA PDWORD pENT = (PDWORD)(dwAddrBase + pExport->AddressOfNames); // 獲取名稱表的RVA PWORD pEIT = (PWORD)(dwAddrBase + pExport->AddressOfNameOrdinals); //獲取索引表的RVA// 4、獲取GetProAddress函數的地址for (DWORD i = 0; i < dwFunCount; i++) {if (!pEAT[i]) {continue; }// 4.1 獲取序號 DWORD dwID = pExport->Base + i;// 4.2 變量EIT 從中獲取到 GetProcAddress的地址for (DWORD dwIdx = 0; dwIdx < dwNameCount; dwIdx++) {// 序號表中的元素的值 對應著函數地址表的位置if (pEIT[dwIdx] == i) {//根據序號獲取到名稱表中的名字 DWORD dwNameOffset = pENT[dwIdx]; char * pFunName = (char*)(dwAddrBase + dwNameOffset);//判斷是否是GetProcAddress函數if (!strcmp(pFunName, "GetProcAddress")) {// 獲取EAT的地址 并將GetProcAddress地址返回 DWORD dwFunAddrOffset = pEAT[i];return dwAddrBase + dwFunAddrOffset; } } } }
2.3.3獲取API
- 那么之后我們使用typedef定義需要的API指針,然后用GetProcAddress獲得的API地址給指針賦值,之后我們就可以使用這些API了,另:自己定義的指針名不能與API名相同
LPGETPROCADDRESS g_funGetProcAddress = nullptr;
LPLOADLIBRARYEX g_funLoadLibraryEx = nullptr;
LPEXITPROCESS g_funExitProcess = nullptr;
LPMESSAGEBOX g_funMessageBox = nullptr;
LPGETMODULEHANDLE g_funGetModuleHandle = nullptr;
LPVIRTUALPROTECT g_funVirtualProtect = nullptr;
bool InitializationAPI(){ HMODULE hModule;
// 1. 初始化基礎API 這里使用的是LoadLibraryExW
g_funGetProcAddress = (LPGETPROCADDRESS)GetGPAFunAddr();
g_funLoadLibraryEx = (LPLOADLIBRARYEX)g_funGetProcAddress((HMODULE)GetKernel32Base(), "LoadLibraryExW");
// 2. 初始化其他API
hModule = NULL;
if (!(hModule = g_funLoadLibraryEx(L"kernel32.dll", NULL, NULL)))
return false;
g_funExitProcess = (LPEXITPROCESS)g_funGetProcAddress(hModule, "ExitProcess");
hModule = NULL;
if (!(hModule = g_funLoadLibraryEx(L"user32.dll", NULL, NULL)))
return false;
g_funMessageBox = (LPMESSAGEBOX)g_funGetProcAddress(hModule, "MessageBoxW");
hModule = NULL;
if (!(hModule = g_funLoadLibraryEx(L"kernel32.dll", NULL, NULL)))
return false;
g_funGetModuleHandle = (LPGETMODULEHANDLE)g_funGetProcAddress(hModule, "GetModuleHandleW");
hModule = NULL;
if (!(hModule = g_funLoadLibraryEx(L"kernel32.dll", NULL, NULL)))
return false;
g_funVirtualProtect = (LPVIRTUALPROTECT)g_funGetProcAddress(hModule, "VirtualProtect");
return true;
- 之后我們完善一下我們的start函數
extern __declspec(dllexport) GLOBAL_PARAM g_stcParam={0};void start(){// 1. 初始化所有APIif (!InitializationAPI()) return;
// 2. 解密宿主程序 Decrypt();
// 3. 跳轉到OEP __asm jmp g_stcParam.dwOEP;
}
這樣一來我們就完成了DLL的Stub部分
三、Stub的植入
之后我們就需要將Stub植入了。
但在此之前我們需要先將帶加殼文件加載入內存并預處理文件
- 這一部分主要包括:
- 加載目標文件
- 目標文件預處理
- 植入Stub
- 保存文件
3.1 加載目標文件
- 由于我們只是普通的加載目標文件而不是按照內存的格式展開,所以加載過程非常簡單,但之后操作要注意將所有VirtualAddress的地址轉換為FileOffset。
// 2. 獲取文件信息,并映射進內存中if (INVALID_HANDLE_VALUE == (hFile_In = CreateFile(strPath, GENERIC_READ, FILE_SHARE_READ,NULL, OPEN_EXISTING, 0, NULL))) {return false; }if (INVALID_FILE_SIZE == (dwFileSize = GetFileSize(hFile_In, NULL))) { CloseHandle(hFile_In);return false; }if (!(lpFileImage = VirtualAlloc(NULL, dwFileSize * 2, MEM_COMMIT, PAGE_READWRITE))) { CloseHandle(hFile_In);return false; } DWORD dwRet;if (!ReadFile(hFile_In, lpFileImage, dwFileSize, &dwRet, NULL)) { CloseHandle(hFile_In); VirtualFree(lpFileImage, 0, MEM_RELEASE);return false; }
3.2目標文件預處理
- 所謂目標文件預處理就是將.text段加密,之后運行時由植入的Stub部分解密后才會恢復正常。
- 那么我們只需要將獲取的目標文件信息傳入即可執行,代碼如下:
// 3. 獲取PE文件信息
objProcPE.GetPeInfo(lpFileImage, dwFileSize, &stcPeInfo);
// 4. 獲取目標文件代碼段的起始結束信息
// 讀取第一個區段的相關信息,并將其加密(默認第一個區段為代碼段)
PBYTE lpStart = (PBYTE)(stcPeInfo.pSectionHeader->PointerToRawData + (DWORD)lpFileImage);
PBYTE lpEnd = (PBYTE)((DWORD)lpStart + stcPeInfo.pSectionHeader->SizeOfRawData);
PBYTE lpStartVA = (PBYTE)(stcPeInfo.pSectionHeader->VirtualAddress + stcPeInfo.dwImageBase);
PBYTE lpEndVA = (PBYTE)((DWORD)lpStartVA + stcPeInfo.pSectionHeader->SizeOfRawData);
// 5. 對文件進行預處理
Pretreatment(lpStart, lpEnd, stcPeInfo);
void Pretreatment(PBYTE lpCodeStart, PBYTE lpCodeEnd, PE_INFO stcPeInfo)
{
// 1. 加密指定區域while (lpCodeStart < lpCodeEnd) { *lpCodeStart ^= 0x15; lpCodeStart++; }
// 2. 給第一個區段附加上可寫屬性 PDWORD pChara = &(stcPeInfo.pSectionHeader->Characteristics); *pChara = *pChara | IMAGE_SCN_MEM_WRITE;
}
- 那么之后我們只需將Stub植入就可以保存文件了(上述處理PE結構使用了一個CProcessingPE類,定義之后詳細給出)
3.3植入Stub
- 植入Stub可以說是DLL型殼中的最大難點。其主要包括以下部分
- 將Stub讀入緩沖區
- 在原文件中增加區塊
- 修復Stub的重定位
- 將Stub復制到原文件中
- 計算并設置新的OEP
- 其中由于篇幅所限,本人僅僅介紹增加區塊以及Stub重定位修復部分。
3.3.1區塊增加
- PE文件中的區塊與PE結構中的SectionHeader有關,所以為了增加一個Section,我們首先應該定位到SectionTable的起始位置。
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header);// 1. 獲取基本信息DWORD dwDosSize = m_pDos_Header->e_lfanew;DWORD dwPeSize = sizeof(IMAGE_NT_HEADERS32);DWORD dwStnSize = m_pNt_Header->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER);DWORD dwHeadSize = dwDosSize+dwPeSize+dwStnSize;
我們查詢一下關于PE文件的SectionTable的結構,單個SectionHeader的結構如下
#define IMAGE_SIZEOF_SHORT_NAME 8typedef struct _IMAGE_SECTION_HEADER{0X00 BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //節(段)的名字.text/.data/.rdata/等//由于長度固定8字節,所以可以沒有結束符0X08 union{ DWORD PhysicalAddress; //物理地址 DWORD VirtualSize; //虛擬大小 }Misc;0X0C DWORD VirtualAddress; //塊的RVA,相對虛擬地址0X10 DWORD SizeOfRawData; //該節在文件對齊后的尺寸大小(FileAlignment的整數倍)0X14 DWORD PointerToRawData; //節區在文件中的偏移量//0X18 DWORD PointerToRelocations; //重定位偏移(obj中使用)//0X1C DWORD PointerToLinenumbers; //行號表偏移(調試用)//0X20 WORD NumberOfRelocations; //重定位項目數(obj中使用)//0X22 WORD NumberOfLinenumbers; //行號表中行號的數目0X24 DWORD Characteristics; //節屬性(按bit位設置屬性)} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;#define IMAGE_SIZEOF_SECTION_HEADER 40
- 那么我們應該相應的組裝一個類似的結構,然后增加區塊數目并將組裝的SectionHeader賦值到目標文件中。
// 2.4 組裝一個新的區段頭 IMAGE_SECTION_HEADER stcNewSect = {0}; CopyMemory(stcNewSect.Name, szVarName, 7); // 區段名稱 stcNewSect.Misc.VirtualSize = dwVirtualSize; // 虛擬大小 stcNewSect.VirtualAddress = dwVStart; // 虛擬地址 stcNewSect.SizeOfRawData = dwSizeOfRawData; // 文件大小 stcNewSect.PointerToRawData = dwFStart; // 文件地址 stcNewSect.Characteristics = dwChara; // 區段屬性
// 2.5 寫入指定位置 CopyMemory( (PVOID)((DWORD)m_dwFileDataAddr+dwHeadSize), &stcNewSect, sizeof(IMAGE_SECTION_HEADER) );
// 3. 修改區段數目字段NumberOfSections m_pNt_Header->FileHeader.NumberOfSections++;
- 之后我們需要將SizeOfImage大小增加我們Stub的大小(要求按照SectionAlignment對齊)并將我們的Stub復制進新的區塊。
// 4. 修改PE文件的景象尺寸字段SizeOfImage m_pNt_Header->OptionalHeader.SizeOfImage += dwVirtualSize;// 5. 返回新區段的詳細信息、大小,以及可直接訪問的地址 CopyMemory(pNewSection, &stcNewSect, sizeof(IMAGE_SECTION_HEADER)); *lpSize = dwSizeOfRawData;return (PVOID)(m_dwFileDataAddr+dwFStart); }
3.3.2修復重定位
- 修復重定位決定了我們的Stub能否在目標程序中正常運行,同樣,我們首先需要先定位RelocTable。
// 1. 獲取映像基址與代碼段指針DWORD dwImageBase;PVOID lpCode;dwImageBase = m_pNt_Header->OptionalHeader.ImageBase;lpCode = (PVOID)( (DWORD)m_dwFileDataAddr + RVAToOffset(m_pNt_Header->OptionalHeader.BaseOfCode) );
- 之后我們查看一下RelocTable的結構

- 每個IMAGE_BASE_RELOCATION元素包含了VirtualAddress、SizeOfBlock,后邊跟著數目不定的重定位項,所以重定位項的數量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)÷2。
- 那么我們只需要遍歷重定位表,逐個進行修復。注意,此處重定位表的地址與實際內存地址不同,應該先減去基址計算為偏移,之后加上當前基址計算。
// 2. 獲取重定位表在內存中的地址 PIMAGE_DATA_DIRECTORY pDataDir; PIMAGE_BASE_RELOCATION pReloc; pDataDir = m_pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_BASERELOC; pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)m_dwFileDataAddr + RVAToOffset(pDataDir->VirtualAddress));
// 3. 遍歷重定位表,并對目標代碼進行重定位while ( pReloc->SizeOfBlock && pReloc->SizeOfBlock < 0x100000 ) {// 3.1 取得重定位項TypeOffset與其數量 PWORD pTypeOffset = (PWORD)((DWORD)pReloc+sizeof(IMAGE_BASE_RELOCATION)); DWORD dwCount = (pReloc->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
// 3.2 循環檢查重定位項for ( DWORD i=0; i {if ( !*pTypeOffset ) continue;
// 3.2.1 獲取此重定位項指向的指針 DWORD dwPointToRVA = (*pTypeOffset&0x0FFF)+pReloc->VirtualAddress; PDWORD pPtr = (PDWORD)(RVAToOffset(dwPointToRVA)+(DWORD)m_dwFileDataAddr);// 3.2.2 計算重定位增量值 DWORD dwIncrement = dwLoadImageAddr - dwImageBase;// 3.2.3 修復需重定位的地址數據 *((PDWORD)pPtr) += dwIncrement; pTypeOffset++; }
// 3.3 指向下一個重定位塊,開始另一次循環 pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock); }
由此我們成功修復了重定位表
3.4 CProcessingPE
- 在此前植入Stub的過程中我們為了方便手動定義了一個CProcessingPE類,下面附上這個類的成員以及方法供參考。由于較為重要的方法此前都已經有介紹,此處就不在贅述。
CProcessingPE::CProcessingPE(void) { ZeroMemory(&m_stcPeInfo, sizeof(PE_INFO)); } CProcessingPE::~CProcessingPE(void) { }/********************************************************************** 相對虛擬地址(RVA)轉文件偏移(Offset)* 此函數負責將傳入的RVA轉換為Offset。** 注意:此轉換函數并未考慮到所有細節,但是在絕大多數情況可以正常運轉。** 參數:* ULONG uRvaAddr:RVA地址值* * 返回值:* DWORD:成功返回Offset,失敗則返回0*********************************************************************/ DWORD CProcessingPE::RVAToOffset(ULONG uRvaAddr) { PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); for (DWORD i=0; iFileHeader.NumberOfSections; i++) { if((pSectionHeader[i].VirtualAddress <= uRvaAddr) && (pSectionHeader[i].VirtualAddress + pSectionHeader[i].SizeOfRawData > uRvaAddr)) { return (pSectionHeader[i].PointerToRawData + (uRvaAddr - pSectionHeader[i].VirtualAddress)); } } return 0; }/********************************************************************** 文件偏移(Offset)轉相對虛擬地址(RVA)* 此函數負責將傳入的Offset轉換為RVA。** 注意:此轉換函數并未考慮到所有細節,但是在絕大多數情況可以正常運轉。** 參數:* ULONG uOffsetAddr:Offset地址值* * 返回值:* DWORD:成功返回RVA地址,失敗則返回0*********************************************************************/ DWORD CProcessingPE::OffsetToRVA(ULONG uOffsetAddr) { PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); for (DWORD i=0; iFileHeader.NumberOfSections; i++) { if((pSectionHeader[i].PointerToRawData <= uOffsetAddr) && (pSectionHeader[i].PointerToRawData + pSectionHeader[i].SizeOfRawData > uOffsetAddr)) { return (pSectionHeader[i].VirtualAddress + (uOffsetAddr - pSectionHeader[i].PointerToRawData)); } } return 0; }/********************************************************************** 獲取PE文件信息* 此函數負責獲取目標文件的關鍵PE信息。** 參數:* LPVOID lpImageData:目標文件所在緩存區的指針* DWORD dwImageSize:目標文件的大小* PPE_INFO pPeInfo :[OUT]用于傳出目標文件的關鍵PE信息* * 返回值:* BOOL:成功返回true,失敗則返回false*********************************************************************/ BOOL CProcessingPE::GetPeInfo(LPVOID lpImageData, DWORD dwImageSize, PPE_INFO pPeInfo) { // 1、判斷映像指針是否有效 if ( m_stcPeInfo.dwOEP ) { CopyMemory(pPeInfo, &m_stcPeInfo, sizeof(PE_INFO)); return true; } else { if ( !lpImageData ) return false; m_dwFileDataAddr = (DWORD)lpImageData; m_dwFileDataSize = dwImageSize; } // 2. 獲取基本信息 // 2.1 獲取DOS頭、NT頭 m_pDos_Header = (PIMAGE_DOS_HEADER)lpImageData; m_pNt_Header = (PIMAGE_NT_HEADERS)((DWORD)lpImageData+m_pDos_Header->e_lfanew); // 2.2 獲取OEP m_stcPeInfo.dwOEP = m_pNt_Header->OptionalHeader.AddressOfEntryPoint; // 2.3 獲取映像基址 m_stcPeInfo.dwImageBase = m_pNt_Header->OptionalHeader.ImageBase; // 2.4 獲取關鍵數據目錄表的內容 PIMAGE_DATA_DIRECTORY lpDataDir = m_pNt_Header->OptionalHeader.DataDirectory; m_stcPeInfo.pDataDir = lpDataDir; CopyMemory(&m_stcPeInfo.stcExport, lpDataDir+IMAGE_DIRECTORY_ENTRY_EXPORT, sizeof(IMAGE_DATA_DIRECTORY)); // 2.5 獲取區段表與其他詳細信息 m_stcPeInfo.pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); // 3. 檢查PE文件是否有效 if ( (m_pDos_Header->e_magic!=IMAGE_DOS_SIGNATURE) || (m_pNt_Header->Signature!=IMAGE_NT_SIGNATURE) ) { // 這不是一個有效的PE文件 return false; } // 4. 傳出處理結果 CopyMemory(pPeInfo, &m_stcPeInfo, sizeof(PE_INFO)); return true; }/********************************************************************** 修復重定位項* 此函數負責修復映像的重定位項,此函數依賴于RVAToOffset函數。* * 注意:* 1. dwLoadImageAddr指的并非是其本身ImageBase的值,而是其被加載后的預* 計模塊基址。* 2. 此重定位函數并未考慮到修復類型問題,如果要提高兼容性,應該分別對* 三種重定位類型進行區別對待。** 參數:* DWORD dwLoadImageAddr:此映像被加載后的預計模塊基址* * 返回值:無*********************************************************************/ void CProcessingPE::FixReloc(DWORD dwLoadImageAddr) { // 1. 獲取映像基址與代碼段指針 DWORD dwImageBase; PVOID lpCode; dwImageBase = m_pNt_Header->OptionalHeader.ImageBase; lpCode = (PVOID)( (DWORD)m_dwFileDataAddr + RVAToOffset(m_pNt_Header->OptionalHeader.BaseOfCode) ); // 2. 獲取重定位表在內存中的地址 PIMAGE_DATA_DIRECTORY pDataDir; PIMAGE_BASE_RELOCATION pReloc; pDataDir = m_pNt_Header->OptionalHeader.DataDirectory+IMAGE_DIRECTORY_ENTRY_BASERELOC; pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)m_dwFileDataAddr + RVAToOffset(pDataDir->VirtualAddress)); // 3. 遍歷重定位表,并對目標代碼進行重定位 while ( pReloc->SizeOfBlock && pReloc->SizeOfBlock < 0x100000 ) { // 3.1 取得重定位項TypeOffset與其數量 PWORD pTypeOffset = (PWORD)((DWORD)pReloc+sizeof(IMAGE_BASE_RELOCATION)); DWORD dwCount = (pReloc->SizeOfBlock-sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); // 3.2 循環檢查重定位項 for ( DWORD i=0; iVirtualAddress; PDWORD pPtr = (PDWORD)(RVAToOffset(dwPointToRVA)+(DWORD)m_dwFileDataAddr); // 3.2.2 計算重定位增量值 DWORD dwIncrement = dwLoadImageAddr - dwImageBase; // 3.2.3 修復需重定位的地址數據 *((PDWORD)pPtr) += dwIncrement; pTypeOffset++; } // 3.3 指向下一個重定位塊,開始另一次循環 pReloc = (PIMAGE_BASE_RELOCATION)((DWORD)pReloc + pReloc->SizeOfBlock); } }/********************************************************************** 獲取PE文件信息* 此函數負責獲取目標文件的關鍵PE信息。** 參數:* LPVOID lpImageData:目標文件所在緩存區的指針* DWORD dwImageSize:目標文件的大小* PPE_INFO pPeInfo :[OUT]用于傳出目標文件的關鍵PE信息* * 返回值:* BOOL:成功返回true,失敗則返回false*********************************************************************/ PVOID CProcessingPE::GetExpVarAddr(LPCTSTR strVarName) { // 1、獲取導出表地址,并將參數strVarName轉為ASCII形式,方便對比查找 CHAR szVarName[MAX_PATH] = {0}; PIMAGE_EXPORT_DIRECTORY lpExport = (PIMAGE_EXPORT_DIRECTORY)(m_dwFileDataAddr + RVAToOffset(m_stcPeInfo.stcExport.VirtualAddress)); WideCharToMultiByte(CP_ACP, NULL, strVarName, -1, szVarName, _countof(szVarName), NULL, FALSE); // 2、循環讀取導出表輸出項的輸出函數,并依次與szVarName做比對,如果相同,則取出相對應的函數地址 for (DWORD i=0; iNumberOfNames; i++) { PDWORD pNameAddr = (PDWORD)(m_dwFileDataAddr+RVAToOffset(lpExport->AddressOfNames+i)); PCHAR strTempName = (PCHAR)(m_dwFileDataAddr + RVAToOffset(*pNameAddr)); if ( !strcmp(szVarName, strTempName) ) { PDWORD pFunAddr = (PDWORD)(m_dwFileDataAddr+RVAToOffset(lpExport->AddressOfFunctions+i)); return (PVOID)(m_dwFileDataAddr + RVAToOffset(*pFunAddr)); } } return 0; }/********************************************************************** 添加區段函數* 此函數負責在目標文件中添加一個自定義的區段。** 注:* 此函數并未考慮到目標函數存在附加數據等細節問題。** 參數:* LPCTSTR strName :新區段的名稱* DWORD dwSize :新區段的最小體積* DWORD dwChara :新區段的屬性* PIMAGE_SECTION_HEADER pNewSection:[OUT]新區段的段結構指針* PDWORD lpSize :[OUT]新區段的最終大小* * 返回值:* PVOID:成功返回指向新區段現在所在內存的指針*********************************************************************/ PVOID CProcessingPE::AddSection(LPCTSTR strName, DWORD dwSize, DWORD dwChara, PIMAGE_SECTION_HEADER pNewSection, PDWORD lpSize) { PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(m_pNt_Header); // 1. 獲取基本信息 DWORD dwDosSize = m_pDos_Header->e_lfanew; DWORD dwPeSize = sizeof(IMAGE_NT_HEADERS32); DWORD dwStnSize = m_pNt_Header->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER); DWORD dwHeadSize = dwDosSize+dwPeSize+dwStnSize; // 2. 在區段表中加入新區段的信息 // 2.1 獲取基本信息 CHAR szVarName[7] = {0}; DWORD dwFileAlign = m_pNt_Header->OptionalHeader.FileAlignment; // 文件粒度 DWORD dwSectAlign = m_pNt_Header->OptionalHeader.SectionAlignment; // 區段粒度 WORD dwNumOfsect = m_pNt_Header->FileHeader.NumberOfSections; // 區段數目 // 2.2 獲取最后一個區段的信息 IMAGE_SECTION_HEADER stcLastSect = {0}; CopyMemory(&stcLastSect, &pSectionHeader[dwNumOfsect-1], sizeof(IMAGE_SECTION_HEADER)); // 2.3 根據區段粒度計算相應地址信息 DWORD dwVStart = 0; // 虛擬地址起始位置 DWORD dwFStart = stcLastSect.SizeOfRawData + stcLastSect.PointerToRawData; // 文件地址起始位置 if ( stcLastSect.Misc.VirtualSize%dwSectAlign ) dwVStart = (stcLastSect.Misc.VirtualSize / dwSectAlign+1) * dwSectAlign + stcLastSect.VirtualAddress; else dwVStart = (stcLastSect.Misc.VirtualSize / dwSectAlign ) * dwSectAlign + stcLastSect.VirtualAddress; DWORD dwVirtualSize = 0; // 區段虛擬大小 DWORD dwSizeOfRawData = 0; // 區段文件大小 if ( dwSize%dwSectAlign) dwVirtualSize = (dwSize / dwSectAlign+1) * dwSectAlign; else dwVirtualSize = (dwSize / dwSectAlign ) * dwSectAlign; if ( dwSize%dwFileAlign ) dwSizeOfRawData = (dwSize / dwFileAlign+1) * dwFileAlign; else dwSizeOfRawData = (dwSize / dwFileAlign ) * dwFileAlign; WideCharToMultiByte(CP_ACP, NULL, strName, -1, szVarName, _countof(szVarName), NULL, FALSE); // 2.4 組裝一個新的區段頭 IMAGE_SECTION_HEADER stcNewSect = {0}; CopyMemory(stcNewSect.Name, szVarName, 7); // 區段名稱 stcNewSect.Misc.VirtualSize = dwVirtualSize; // 虛擬大小 stcNewSect.VirtualAddress = dwVStart; // 虛擬地址 stcNewSect.SizeOfRawData = dwSizeOfRawData; // 文件大小 stcNewSect.PointerToRawData = dwFStart; // 文件地址 stcNewSect.Characteristics = dwChara; // 區段屬性 // 2.5 寫入指定位置 CopyMemory( (PVOID)((DWORD)m_dwFileDataAddr+dwHeadSize), &stcNewSect, sizeof(IMAGE_SECTION_HEADER) ); // 3. 修改區段數目字段NumberOfSections m_pNt_Header->FileHeader.NumberOfSections++; // 4. 修改PE文件的景象尺寸字段SizeOfImage m_pNt_Header->OptionalHeader.SizeOfImage += dwVirtualSize; // 5. 返回新區段的詳細信息、大小,以及可直接訪問的地址 CopyMemory(pNewSection, &stcNewSect, sizeof(IMAGE_SECTION_HEADER)); *lpSize = dwSizeOfRawData; return (PVOID)(m_dwFileDataAddr+dwFStart); }/********************************************************************** 修改目標文件OEP* 此函數負責修改目標文件OEP。** 參數:* DWORD dwOEP:新OEP* * 返回值:無*********************************************************************/ void CProcessingPE::SetOEP(DWORD dwOEP) { m_pNt_Header->OptionalHeader.AddressOfEntryPoint = dwOEP; }
總結
殼的編寫看似簡單,實際有不少難點。本文中沒有涉及IAT表的加密以及一系列反調試,混淆手段,只是作為一個殼的原型方便讀者了解殼編寫的流程以及其中的難點,為下一步深入學習打下基礎。
本人純屬萌新,本文如有錯漏,希望各位大佬不吝指正。
本文參考的代碼來自于《黑客免殺攻防》,如有需要源碼可以自行前往華章圖 書的官網下載。