您好,登錄后才能下訂單哦!
本篇內容介紹了“如何從零開始寫一個加殼器”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
即我們向PE文件添加一個區段并將其設置為入口點,這樣PE文件最開始執行的命令就是我們添加的區段也就是殼的指令,殼對加密區進行解密,對壓縮區進行解壓,將原本的EXE文件還原出來,然后跳轉至原程序入口,程序照常運行。
首先生成一個打印hello的exe文件。
#include <stdio.h> int main() { printf("hello"); }
我們目前要干的事情是:以手動的形式向PE文件添加一個殼部分并設為程序入口,并使其能跳轉回原入口。 那就來吧
用010editor打開我們的exe文件,啟用exe模板分析。 我們首先修改其文件頭numverofsection屬性,這個屬性用來定義當前PE文件存在多少個區段,因為我們要添加一個殼區段,所以我們將其加1變成6
在我們重載模板后我們就會在區段表發現多出來一個空的區段表
從上到下各個比較重要字段的意思是 \1. Name 表示該區段的名字 2.VirtualSize 表示在內存中的大小(一般內存對齊為0x1000) 3.virtualaddress 虛擬地址 即上一個區段的VirtualAddress + 上一個區段經內存對齊粒度對齊后的大小 4.sizeofdata 表示在文件中的大小(一般文件對齊為0x200) 5.pointertorawdata 文件的偏移 即 上一個區段的PointerToRawData + 上一個區段的SizeOfRawData
然后我們通過修改以上各值來定義一個新區段(殼區段)的屬性
這里的virtualsize看著填一個就行了。 此時我們只是定義了區段表,但文件中并沒有該區段存在,所以我們得創建該區段。 然后還要讓區段可編輯,把下列值改為1即可
ctrl+shift+i 向目標文件偏移處插入0x200大小的空間。 這樣一來,殼區段就創建好了。 然后我們還要修改 擴展頭的SizeofImage 。將他改為最后一個區段的內存地址+內存大小
然后去掉隨機基址選項。
找到擴展頭的DLL屬性字段,去掉隨機基址,把40 81改為 00 81
接下來我們把程序入口點設置給殼區段。 使用LORDPE把入口點設為殼區塊的虛擬地址
然后我們用OD打開這個文件
剛剛提到的手工加殼,不過是最最基礎的加殼原型而已,真正的加殼還涉及了代碼加解密等操作.
真正寫殼時一般寫兩個東西,加殼器和stub 所謂加殼器,就是給被加殼文件創造出一個新的區段, 在此同時將程序以某種方式加密,然后把stub放入新區段,并將程序入口點設為新區段的地址,然后在新區段結束后跳轉回原程序入口。這個新區段我們叫做殼區段. 那么這個stub就是加殼后程序最先執行的命令了,它執行解密算法,將原程序釋放出來。
https://github.com/ConsT27/PackingEXE/tree/master項目地址
stub是被植入到PE文件中的代碼,它一般會干下面這些事情。
流程如下
0.合并data,rdata到text 1.PEB動態尋址,遍歷導出表找到GetProcAddress函數 2.解密 3.修改入口點到原入口點
同時stub一般以dll的形式存在。原因是DLL通常自帶重定位表,這在我們的移植過程中的重定位操作中提供了巨大的便利。
我們要移植stub過去,肯定需要移植代碼段,也需要移植數據段。不如我們干脆把數據段合并到代碼段,一塊移植過去。
為什么會用到這個技術編寫stub? 因為我們的stub.dll植入到宿主程序時,只有.text植入過去,沒有對應的導入表,所以我們的stub無法直接調用一些API。所以我們需要動態獲取各種API。 其中我采用的是PEB動態查詢得到GetProcAddress函數,然后用GetProcAddress函數去獲取各個API。
那么,什么是PEB? PEB是一個微軟還未完全公開作用的一個結構,它叫做 進程環境信息塊 ,包含了進程的信息。其結構如下
typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; //被調試狀態 BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; BYTE Reserved4[104]; PVOID Reserved5[52]; PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine; BYTE Reserved6[128]; PVOID Reserved7[1]; ULONG SessionId; } PEB, *PPEB; 復制代碼
我們關心的是PEB偏移0c得到的 PPEB_LDR_DATA Ldr; 它是一個指針,指向一個 PPEB_LDR_DATA 結構, 存放著已經被進程裝在的動態鏈接庫的信息
typedef struct _PEB_LDR_DATA { ULONG Length; // +0x00 BOOLEAN Initialized; // +0x04 PVOID SsHandle; // +0x08 LIST_ENTRY InLoadOrderModuleList; // +0x0c LIST_ENTRY InMemoryOrderModuleList; // +0x14 LIST_ENTRY InInitializationOrderModuleList;// +0x1c } PEB_LDR_DATA,*PPEB_LDR_DATA; // +0x24
PPEB_LDR_DATA 偏移1c是一個指向LIST_ENTRY InInitializationOrderModuleList結構的指針,這個結構 存放著指向模塊初始化鏈表的頭 , 按順序存放著PE裝入運行時初始化模塊信息,一般來說第一個鏈表結點是ntdll.dll,第二個鏈表結點就是kernel32.dll 。我們就在其中找到kernel32.dll的信息,獲取其PE信息,得到導出表,循環遍歷得到GetProcAddress函數。 另外,PEB地址再TEB偏移0x30處。用匯編語言表示就是 fs:[0x30]。
以上是PEB尋址的大致流程,另外還有一個比較關鍵的點是遍歷kernel32.dll導出表獲得GetProcAddress函數信息。 關于導出表可以看看這個文章https://blog.csdn.net/evileagle/article/details/12176797
首先一個導出表結構體如下
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; //一般為0,沒啥用 DWORD TimeDateStamp; //導出表生成的時間 WORD MajorVersion; //版本,也是0沒啥用 WORD MinorVersion; //也是沒啥用的版本信息一般為0 DWORD Name; //當前導出表的模塊名字 DWORD Base; //序號表中序號的基數 DWORD NumberOfFunctions; //導出函數數量 DWORD NumberOfNames; //按名字導出函數的數量 DWORD AddressOfFunctions; // 序號表 DWORD AddressOfNames; // 名稱表 DWORD AddressOfNameOrdinals; // 地址表 } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
其中序號表的起始序號是Base屬性定義的值。以下是導出表的序號名稱地址表的關系
我們的遍歷流程是,先遍歷名稱表找到GetProcAddress在名稱數組中的下標,然后根據這個下標去序號數組中找相同下標的序號值,然后以這個序號值為下標去找地址數組中的對應值。我們找到的地址表中的值就是函數入口
下面我把這段程序的匯編代碼放出來。我是用內聯匯編把這段代碼塞進C++的
void GetApis() { HMODULE hKernel32; _asm { pushad; ; //獲取kernel32.dll的加載基址; mov eax, fs: [0x30] ; //得到PEB地址 mov eax, [eax + 0ch]; //獲得LDR_PEB_DATA地址 mov eax, [eax + 0ch]; //獲得LIST_ENTRY InLoadOrderModuleList;地址 mov eax, [eax]; //獲得LIST_ENTRY InLoadOrderModuleList下一項的地址 mov eax, [eax]; /獲得LIST_ENTRY InLoadOrderModuleList下下項即我們需要的LIST_ENTRY InInitializationOrderModuleList的地址 mov eax, [eax + 018h]; //獲得kernel32.dll地址 mov hKernel32, eax; mov ebx, [eax + 03ch];//獲得kernel32.dll NT頭RVA add ebx, eax; //NT頭的VA add ebx, 078h; //獲得區段表 mov ebx, [ebx]; //獲得導出表RVA add ebx, eax; //導出表VA lea ecx, [ebx + 020h]; mov ecx, [ecx]; // ecx => 名稱表的首地址(rva); add ecx, eax; // ecx => 名稱表的首地址(va); xor edx, edx; // 作為索引(index)來使用. _WHILE:; mov esi, [ecx + edx * 4];//名稱數組入口點rva,名稱數組單位大小4字節 lea esi, [esi + eax]; //入口點VA cmp dword ptr[esi], 050746547h; //進行名稱匹配,050746547h即小端存儲的GetP jne _LOOP;//不相等就跳入_LOOP段 cmp dword ptr[esi + 4], 041636f72h; //名陳匹配,rocA,以下依次為ddre,ss jne _LOOP; cmp dword ptr[esi + 8], 065726464h; jne _LOOP; cmp word ptr[esi + 0ch], 07373h; jne _LOOP; mov edi, [ebx + 024h]; add edi, eax; //獲得序號表VA mov di, [edi + edx * 2]; //獲得序號數組中對應下標的地址,序號數組單位大小2字節 and edi, 0FFFFh; //給di提位到32位,即給予edi 序號表中對應下標的地址 mov edx, [ebx + 01ch]; add edx, eax; //獲得地址表 mov edi, [edx + edi * 4]; //獲得地址數組中,序號對應的值,地址數組單位大小4字節 add edi, eax; //獲得GetProcAddress的入口地址 mov MyGetProcAddress, edi; //賦值 jmp _ENDWHILE; //END _LOOP:; inc edx; // ++index; jmp _WHILE; _ENDWHILE:; popad; }
解密代碼段。這段好寫。
void Decrypt() { unsigned char* pText = (unsigned char*)g_conf.textScnRVA + 0x400000;//鎖定到PE文件的text段(因為加殼時去掉了基址隨機化,所以自信的把基址填成0x400000 DWORD old = 0; MyVirtualProtect(pText, g_conf.textScnSize, PAGE_READWRITE, &old);//修改代碼段的屬性,注意我們這里使用了動態獲得的 //解密代碼段 for (DWORD i = 0; i < g_conf.textScnSize; i++) { pText[i] ^= g_conf.key; } //把屬性修改回去 MyVirtualProtect(pText, g_conf.textScnSize, old, &old); }
_asm { mov eax, g_conf.srcOep; //入口點是g_conf.srcOep add eax, 0x400000 jmp eax }
加殼器流程如下
1.打開需要被加殼的PE文件 2.加載stub 3.加密代碼段 4.添加新區段 5.stub重定位修復 6.stub移植 7.PE文件入口點修改 8.去隨機基址 9.保存文件
以下的各個流程描述中會用到諸多自定義函數,我先貼上來吧。
//**************** //對齊處理 //time:2020/11/5 //**************** int AlignMent(_In_ int size, _In_ int alignment) { return (size) % (alignment)==0 ? (size) : ((size) / alignment+1) * (alignment); } //*********************** //PE信息獲取函數簇 //time:2020/11/2 //*********************** PIMAGE_DOS_HEADER GetDosHeader(_In_ char* pBase) { return PIMAGE_DOS_HEADER(pBase); } PIMAGE_NT_HEADERS GetNtHeader(_In_ char* pBase) { return PIMAGE_NT_HEADERS(GetDosHeader(pBase)->e_lfanew+(SIZE_T)pBase); } PIMAGE_FILE_HEADER GetFileHeader(_In_ char* pBase) { return &(GetNtHeader(pBase)->FileHeader); } PIMAGE_OPTIONAL_HEADER32 GetOptHeader(_In_ char* pBase) { return &(GetNtHeader(pBase)->OptionalHeader); } PIMAGE_SECTION_HEADER GetLastSec(_In_ char* pBase) { DWORD SecNum = GetFileHeader(pBase)->NumberOfSections; PIMAGE_SECTION_HEADER FirstSec = IMAGE_FIRST_SECTION(GetNtHeader(pBase)); PIMAGE_SECTION_HEADER LastSec = FirstSec + SecNum - 1; return LastSec; } PIMAGE_SECTION_HEADER GetSecByName(_In_ char* pBase,_In_ const char* name) { DWORD Secnum = GetFileHeader(pBase)->NumberOfSections; PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(GetNtHeader(pBase)); char buf[10] = { 0 }; for (DWORD i = 0; i < Secnum; i++) { memcpy_s(buf, 8, (char*)Section[i].Name, 8); if (!strcmp(buf, name)) { return Section + i; } } return nullptr; } typedef struct _StubConf { DWORD srcOep; //入口點 DWORD textScnRVA; //代碼段RVA DWORD textScnSize; //代碼段的大小 DWORD key; //解密密鑰 }StubConf; struct StubInfo { char* dllbase; //stub.dll的加載基址 DWORD pfnStart; //stub.dll(start)導出函數的地址 StubConf* pStubConf; //stub.dll(g_conf)導出全局變量的地址 };
打開PE文件
這里采用的方法是利用CreateFileA函數。同時這個函數還拋出了一個指向PE文件大小的指針
char* GetFileHmoudle(_In_ const char* path,_Out_opt_ DWORD* nFileSize) { //打開一個文件并獲得文件句柄 HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); //獲得文件大小 DWORD FileSize = GetFileSize(hFile, NULL); //返回文件大小到變量nFileSize if(nFileSize) *nFileSize = FileSize; //申請一片大小為FileSize的內存并將指針置于首位 char* pFileBuf = new CHAR[FileSize]{ 0 }; //給剛剛申請的內存讀入數據 DWORD dwRead; ReadFile(hFile, pFileBuf, FileSize, &dwRead, NULL); CloseHandle(hFile); return pFileBuf; }
加載STUB
void LoadStub(_In_ StubInfo* pstub) { pstub->dllbase = (char*)LoadLibraryEx(L"F:\\stubdll.dll", NULL, DONT_RESOLVE_DLL_REFERENCES); pstub->pfnStart = (DWORD)GetProcAddress((HMODULE)pstub->dllbase, "Start"); //獲得stub的入口函數Start(自己定義在stub中的一個函數 pstub->pStubConf = (StubConf*)GetProcAddress((HMODULE)pstub->dllbase, "g_conf"); } //不僅加載了stub,還獲得了stub拋出的用于收集信息的全局結構體(g_conf,是一個stub拋出的結構體,用于獲取信息,結構如下) typedef struct _StubConf { DWORD srcOep; //入口點 DWORD textScnRVA; //代碼段RVA DWORD textScnSize; //代碼段的大小 DWORD key; //解密密鑰 }StubConf;
加密代碼段
DWORD textRVA = GetSecByName(PeHmoudle, ".text")->VirtualAddress; DWORD textSize = GetSecByName(PeHmoudle, ".text")->Misc.VirtualSize; Encry(PeHmoudle,pstub); void Encry(_In_ char* hpe,_In_ StubInfo pstub) { //獲取代碼段首地址 BYTE* TargetText = GetSecByName(hpe, ".text")->PointerToRawData + (BYTE*)hpe; //獲取代碼段大小 DWORD TargetTextSize = GetSecByName(hpe, ".text")->Misc.VirtualSize; //加密代碼段 for (int i = 0; i < TargetTextSize; i++) { TargetText[i] ^= 0x15; } pstub.pStubConf->textScnRVA = GetSecByName(hpe, ".text")->VirtualAddress; pstub.pStubConf->textScnSize = TargetTextSize; pstub.pStubConf->key = 0x15; } //加密代碼段,并給予了stub一些信息
char* AddSec(_In_ char*& hpe, _In_ DWORD& filesize, _In_ const char* secname, _In_ const int secsize) { GetFileHeader(hpe)->NumberOfSections++; PIMAGE_SECTION_HEADER pesec = GetLastSec(hpe); //設置區段表屬性 memcpy(pesec->Name, secname, 8); pesec->Misc.VirtualSize = secsize; pesec->VirtualAddress = (pesec - 1)->VirtualAddress + AlignMent((pesec - 1)->SizeOfRawData,GetOptHeader(hpe)->SectionAlignment); pesec->SizeOfRawData = AlignMent(secsize, GetOptHeader(hpe)->FileAlignment); pesec->PointerToRawData = AlignMent(filesize,GetOptHeader(hpe)->FileAlignment); pesec->Characteristics = 0xE00000E0; //設置OPT頭映像大小 GetOptHeader(hpe)->SizeOfImage = pesec->VirtualAddress + pesec->SizeOfRawData; //擴充文件數據 int newSize = pesec->PointerToRawData + pesec->SizeOfRawData; char* nhpe = new char [newSize] {0}; //向新緩沖區錄入數據 memcpy(nhpe, hpe, filesize); //緩存區更替 delete hpe; filesize = newSize; return nhpe; }
stub重定位
好家伙,這個東西稍有不慎就會讓整個程序拉跨掉(過來人的忠告 為什么需要stub重定位呢?因為我們的stub最開始是加載在內存中的,它的許多指令如跳轉到的地址是按內存為基準確定的,但是我們需要把他移植進文件,所以它的代碼里許多地址就是錯誤的,我們需要對這些地址進行處理,即重定位,使其以宿主程序為標準進行地址修復。 可能我表述的不是很清楚
舉個例子吧,比如stub在加載進內存時,有一條跳轉指令時jmp 12345678, 如果我們不處理就把這條指令移植進PE文件,那么PE文件執行到此處時就會跳轉到12345678,此時的12345678地址可能就已經不是PE文件加載的內存區間了,從而程序會崩潰。所以要修復。根據stub的重定位表進行修復。 重定位表就是記錄哪些地址的數據需要被修復的,我們遍歷這些地址進行修復即可。 如果以下代碼看起來吃力,可以先去了解一下重定位表
void FixStub(DWORD targetDllbase, DWORD stubDllbase,DWORD targetNewScnRva,DWORD stubTextRva ) { //找到stub.dll的重定位表 DWORD dwRelRva = GetOptHeader((char*)stubDllbase)->DataDirectory[5].VirtualAddress; IMAGE_BASE_RELOCATION* pRel = (IMAGE_BASE_RELOCATION*)(dwRelRva + stubDllbase); //遍歷重定位表 while (pRel->SizeOfBlock) { struct TypeOffset { WORD offset : 12; WORD type : 4; }; TypeOffset* pTypeOffset = (TypeOffset*)(pRel + 1); DWORD dwCount = (pRel->SizeOfBlock - 8) / 2; //需要重定位的數量 for (int i = 0; i < dwCount; i++) { if (pTypeOffset[i].type != 3) { continue; } //需要重定位的地址 DWORD* pFixAddr = (DWORD*)(pRel->VirtualAddress + pTypeOffset[i].offset + stubDllbase); DWORD dwOld; //修改屬性為可寫 VirtualProtect(pFixAddr, 4, PAGE_READWRITE, &dwOld); //去掉dll當前加載基址 *pFixAddr -= stubDllbase; //去掉默認的段首RVA *pFixAddr -= stubTextRva; //換上目標文件的加載基址 *pFixAddr += targetDllbase; //加上新區段的段首RVA *pFixAddr += targetNewScnRva; //把屬性修改回去 VirtualProtect(pFixAddr, 4, dwOld, &dwOld); } //切換到下一個重定位塊 pRel = (IMAGE_BASE_RELOCATION*)((DWORD)pRel + pRel->SizeOfBlock); }
stub移植
這個簡單,沒啥說的
memcpy(GetLastSec(PeNewHmoudle)->PointerToRawData+ PeNewHmoudle, GetSecByName(pstub.dllbase, ".text")->VirtualAddress+pstub.dllbase, GetSecByName(pstub.dllbase,".text")->Misc.VirtualSize);
GetOptHeader(PeNewHmoudle)->AddressOfEntryPoint = pstub.pfnStart-(DWORD)pstub.dllbase-GetSecByName(pstub.dllbase,".text")->VirtualAddress+GetLastSec(PeNewHmoudle)->VirtualAddress;
去隨機基址
不去掉隨機基址,加載基址就是不固定的,不方便操作
GetOptHeader(PeNewHmoudle)->DllCharacteristics &= (~0x40);
void SaveFile(_In_ const char* path, _In_ const char* data, _In_ int FileSize) { HANDLE hFile = CreateFileA( path, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL ); DWORD Buf = 0; WriteFile(hFile, data, FileSize, &Buf,NULL); CloseHandle(hFile); }
“如何從零開始寫一個加殼器”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。