本文基于Windows平臺(tái)對(duì)PE文件加殼的項(xiàng)目,經(jīng)過(guò)一個(gè)月的緩沖,決定復(fù)習(xí)總結(jié)及分享下的我的心得。
主要工具: 010Editor、VS2017、x64dbg、LordPE、OD
實(shí)驗(yàn)平臺(tái):win10 64位
實(shí)現(xiàn)功能:加殼,壓縮,對(duì)代碼段加密。
一、加殼原理
要想弄明白怎么對(duì)PE文件加殼,首先需要對(duì)PE文件比較熟悉,而最快的熟悉PE文件的方法就是自己寫(xiě)一個(gè)PE解析工具和寫(xiě)殼了。
先只用工具010Editor完成一個(gè)手工加殼,那么就明白加殼的原理了。
首先進(jìn)行手工加殼
先用VS隨便生成一個(gè)exe文件,我們使用它進(jìn)行實(shí)驗(yàn)。
可以先使用010Editor、LordPE、OD等工具查看節(jié)區(qū)個(gè)數(shù),我實(shí)驗(yàn)程序的原始區(qū)段(節(jié)區(qū))個(gè)數(shù)是8個(gè)。
1. 給PE文件添加一個(gè)新區(qū)段
修改文件頭的NumberOfSection
使用010Editor打開(kāi)測(cè)試程序,按alt+4出現(xiàn)一個(gè)模板菜單找到NumberOfSection把該數(shù)字加1,這里改為了9。
2. 設(shè)置新的區(qū)段頭
添加保存之后, 重新運(yùn)行010Editor的模板(或者重啟010),區(qū)段就增加了一個(gè)。
設(shè)置整個(gè)新增加的區(qū)段的數(shù)據(jù),主要需要設(shè)置的字段如下:
① 區(qū)段名(可選)
② 區(qū)段數(shù)據(jù)的實(shí)際字節(jié)數(shù)Misc.VirtualSize
③ 區(qū)段的VirtualAddress(區(qū)段數(shù)據(jù)在內(nèi)存中的RVA),此值必須是: 上一個(gè)區(qū)段的VirtualAddress + 上一個(gè)區(qū)段經(jīng)內(nèi)存對(duì)齊粒度對(duì)齊后的大?。▋?nèi)存對(duì)齊大小是0x1000的整數(shù)倍)
④ 區(qū)段以文件對(duì)齊粒度對(duì)齊后的大小SizeOfRawData(文件對(duì)齊大小是0x200的整數(shù)倍)
⑤ 區(qū)段的PointerToRawData(區(qū)段數(shù)據(jù)在文件中的偏移),此值必須是:上一個(gè)區(qū)段的PointerToRawData + 上一個(gè)區(qū)段的SizeOfRawData
⑥ 區(qū)段屬性主要設(shè)置區(qū)段為可讀可寫(xiě)可執(zhí)行如下圖
對(duì)比上一個(gè)區(qū)段修改新添加的區(qū)段里的字段。
3. 添加區(qū)段數(shù)據(jù)
區(qū)段頭內(nèi)容雖然設(shè)置好了,但真正重要的區(qū)段里的數(shù)據(jù)還需要插入到文件中,以擴(kuò)充文件的大小,因?yàn)閰^(qū)段頭只是一個(gè)相當(dāng)于目錄的存在,如果只有目錄而沒(méi)有內(nèi)容,就會(huì)造成這個(gè)文件成為一個(gè)無(wú)效的PE文件。
把010Editor里的數(shù)據(jù)頁(yè)滾動(dòng)到最下面按Ctrl+shift+i添加200h個(gè)(16進(jìn)制)字節(jié)
4. 修改PE文件的擴(kuò)展頭的SizeofImage
現(xiàn)在PE文件已經(jīng)被擴(kuò)充了大小,擴(kuò)展頭中的映像大小必須更新,否則當(dāng)PE文件加載到內(nèi)存后,新區(qū)段的數(shù)據(jù)將無(wú)法得到正常加載。
這個(gè)字段的值記錄的是一個(gè)PE文件在內(nèi)存中的大小,可以將之設(shè)置為: 最后一個(gè)區(qū)段在內(nèi)存中的位置 + 最后一個(gè)區(qū)段在內(nèi)存中的大小,即:
OptionalHeader.SizeofImage = 最后一個(gè)區(qū)段.VirtualAddress + 最后一個(gè)區(qū)段.SizeOfRawData按內(nèi)存對(duì)齊粒度對(duì)齊的大小
保存之后,運(yùn)行該程序,就能正常運(yùn)行(中間某些環(huán)節(jié)操作錯(cuò)了就會(huì)導(dǎo)致該文件無(wú)法正常運(yùn)行)到此添加區(qū)段成功了。那么加殼也就成功了90%,這個(gè)新區(qū)段之后稱為殼代碼段。
5. 添加殼代碼
先找到擴(kuò)展頭的DLL屬性字段,去掉隨機(jī)基址,把40 81改為 00 81后保存。
在這里為了方便,就使用LordPE來(lái)操作剩下的步驟了,先記錄原始的OEP入口點(diǎn)為11055,把他改為新區(qū)段的RVA 1F000然后點(diǎn)擊保存。
然后再使用OD打開(kāi),進(jìn)入到入口點(diǎn)就是41F000,因?yàn)槟J(rèn)加載基址是0x400000, 發(fā)現(xiàn)全是00 00 00的字節(jié),沒(méi)用內(nèi)容。把第一行代碼改為跳轉(zhuǎn)到原來(lái)的入口點(diǎn)jmp 0x411055,然后打一個(gè)補(bǔ)丁,程序就能正常運(yùn)行了。
這就是一個(gè)完整的殼流程了,雖然這個(gè)殼的內(nèi)容只有一條跳轉(zhuǎn)到原入口點(diǎn)的代碼,但萬(wàn)丈高樓平地起?;A(chǔ)的東西弄懂了后面才能少遇見(jiàn)一些坑!
二、為什么用C++寫(xiě)殼?
我的答案是簡(jiǎn)單、便捷、方便新手入門(mén)。
很多常見(jiàn)的殼都用匯編寫(xiě)的,確實(shí),匯編確實(shí)可以寫(xiě)出很多短小精悍、騷操作的代碼,這是C++所沒(méi)有的,但是C++支持內(nèi)聯(lián)匯編,在一定程度上彌補(bǔ)了它的不足。
使用DLL動(dòng)態(tài)庫(kù)文件保存殼代碼,我們稱它為存根部分(stub),直接把這個(gè)文件里的內(nèi)容移植到我們新添加的區(qū)段里面,因?yàn)镻E文件涉及到重定位,而DLL也是一個(gè)PE文件,移植后里面的數(shù)據(jù)就變得很容易修復(fù)了。
三、C++加殼流程
1. 處理加殼程序
在加殼過(guò)程中,有一個(gè)加殼器程序和stub.dll兩個(gè)文件,加殼器程序會(huì)把原文件(要加殼的文件)以文件方式讀取到堆內(nèi)存,它還是以文件對(duì)齊粒度(200h)對(duì)齊的,而stub.dll是以不處理的方式讀取到了內(nèi)存中,它是以內(nèi)存粒度(1000h)對(duì)齊的。
使用LoadLibraryExA加載DLL并且第三個(gè)參數(shù)使用DONT_RESOLVE_DLL_REFERENCES的時(shí)候,他就不會(huì)對(duì)這個(gè)文件進(jìn)行重定位等操作,是以原始形態(tài)加載到內(nèi)存。
//將DLL以不會(huì)執(zhí)行代碼的標(biāo)志加載到進(jìn)程中.
HMODULE hStubDll = LoadLibraryExA("Stub.dll", 0,
DONT_RESOLVE_DLL_REFERENCES);
再自定義一個(gè)共享頭文件share.h,這個(gè)文件保存一些加殼程序和stub.dll中都會(huì)用到的一些數(shù)據(jù),封裝的函數(shù),及共用的結(jié)構(gòu)體!
流程如下:
① 使用加殼器給被加殼程序添加新區(qū)段。
② 加密/壓縮被加殼程序。
③ 將stub的代碼段移植到新區(qū)段。
④ 將被加殼程序的OEP記錄到share.h中。
⑤ 將被加殼程序的EP設(shè)置到新區(qū)段。
⑥ 去掉隨機(jī)基址。
⑦ 保存為新文件。
移植數(shù)據(jù)到新區(qū)段,把整個(gè)stub.dll的代碼段.text移植到目標(biāo)文件新添加的區(qū)段中,這樣就完成了最簡(jiǎn)單加殼操作。
當(dāng)然事實(shí)上并沒(méi)有那么簡(jiǎn)單,stub.dll里的.text段里面的數(shù)據(jù)需要先進(jìn)行重定位修復(fù),修復(fù)完成后再移植過(guò)去,這樣殼區(qū)段才能正常運(yùn)行起來(lái)。
首先根據(jù)stub.dll的重定位表獲取出stub.dll中.text段需要重定位的數(shù)據(jù),然后把該數(shù)據(jù)
① 減去原始基址
② 減去原始代碼段Rva
③ 加上新基址(exe目標(biāo)文件)
④ 加上新Rva (exe中新添加的區(qū)段RVA)
用C++寫(xiě)代碼,首先封裝了很多常用的函數(shù),如獲取DOS頭和NT頭,區(qū)段頭等。這樣會(huì)節(jié)省后面大量敲代碼的時(shí)間。
//獲取DOS頭
PIMAGE_DOS_HEADER GetDosHeader(char* pBase)
{
return (PIMAGE_DOS_HEADER)pBase;
}
//獲取NT頭
PIMAGE_NT_HEADERS GetNtHeader(char* pBase)
{
return (PIMAGE_NT_HEADERS)
(GetDosHeader(pBase)->e_lfanew + (DWORD)pBase);
}
例如獲取NT頭:
auto pNt = (PIMAGE_NT_HEADERS)GetNtHeader(pBase);
C++里auto的功能是自動(dòng)獲取后面數(shù)據(jù)類型,這也體現(xiàn)了C++的強(qiáng)大之處。
完整重定位代碼:
//修復(fù)stub的重定位
void FixStubReloc(char* pTarBuff, char*& hModule,DWORD dwNewBase,DWORD dwNewSecRva)
{
//獲取sutb.dll重定位va
auto pReloc = (PIMAGE_BASE_RELOCATION)
(GetOptHeader(hModule)->DataDirectory[5].VirtualAddress
+ hModule);
//獲取stub.dll的.text區(qū)段的Rva
DWORD dwTextRva = (DWORD)GetSecHeader(hModule, ".text")->VirtualAddress;
//修復(fù)重定位
while (pReloc->SizeOfBlock)
{
struct TypeOffset
{
WORD offset : 12;
WORD type : 4;
};
TypeOffset* pTyOf = (TypeOffset*)(pReloc + 1);
DWORD dwCount = (pReloc->SizeOfBlock - 8) / 2;
for (size_t i = 0; i < dwCount; i++)
{
if(pTyOf[i].type != 3)
continue;
//要修復(fù)的Rva
DWORD dwFixRva = pTyOf[i].offset + pReloc->VirtualAddress;
//要修復(fù)的地址
DWORD* pFixAddr = (DWORD*)(dwFixRva + (DWORD)hModule);
DWORD dwOldProc;
VirtualProtect(pFixAddr, 4, PAGE_READWRITE, &dwOldProc);
*pFixAddr -= (DWORD)hModule; //減去原始基址
*pFixAddr -= dwTextRva; //減去原始代碼段Rva
*pFixAddr += dwNewBase; //加上新基址
*pFixAddr += dwNewSecRva; //加上新Rva
VirtualProtect(pFixAddr, 4, dwOldProc, &dwOldProc);
}
//指向下一個(gè)重定位塊
pReloc = (PIMAGE_BASE_RELOCATION)
((DWORD)pReloc + pReloc->SizeOfBlock);
}
}
現(xiàn)在只是暫時(shí)搭建一個(gè)殼框架所以先不處理隨機(jī)基址的問(wèn)題,所以要去掉隨機(jī)基址,后期再來(lái)解決隨機(jī)基址的問(wèn)題。
-
WINDOWS
+關(guān)注
關(guān)注
4文章
3551瀏覽量
88864 -
C++
+關(guān)注
關(guān)注
22文章
2111瀏覽量
73704 -
PE文件
+關(guān)注
關(guān)注
0文章
4瀏覽量
5457
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論