用C++实现的壳(基础版)
生活随笔
收集整理的這篇文章主要介紹了
用C++实现的壳(基础版)
小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
原文鏈接:https://bbs.pediy.com/thread-206804.htm
最近一直在15PB學習,現(xiàn)階段學的主要是關于殼的知識,正好現(xiàn)在也在做殼這個階段項目,用了2天的時間實現(xiàn)了一個基礎版的C++寫的殼,拿出來跟大家分享一下,代碼量不多,但知識點不少,適合新手學習提高~
????????殼的流程看上去并不復雜,但需要的是你對PE文件有一定的了解,在了解了一些關于導入表、導出表、重定位表、IAT等基礎知識以后方可寫出一個具有基本功能的殼。但如果想要寫一個加密、壓縮或者兼容性很強的殼的話,絕對不是一件容易的事,此貼只是簡單實現(xiàn)了殼的基本加載流程,展現(xiàn)的殼都做了哪些基礎操作,關于更高技術的加密、反調(diào)試、壓縮等技術暫不討論,可能過后我會發(fā)一個講述如何在此版本上進行一些拓展,實現(xiàn)如IAT加密、IAT-Hook、花指令、反調(diào)試等功能的殼。
????????此項目的參考書目:《黑客免殺攻防》(任曉琿)、《加密與解密?第三版》(段鋼)
????????
????????基礎版殼所實現(xiàn)的功能:
????????1.在原程序中添加一塊區(qū)段,將殼部分的代碼移植進去。
????????2.在程序啟動前優(yōu)先獲得控制權,執(zhí)行完自己的代碼以后再將控制權交還給原程序。
????????3.對代碼段進行簡單的亦或加密。
????????4.對原程序的導入表(IAT)進行修復。
????????5.如果原程序開啟了隨機基址,則對源程序進行重定位修復。
????????項目分為兩部分,第一部分為加殼程序(Pack),第二部分為外殼程序(Shell.dll)。
????????其中涉及到的重點是修復導入表和重定位表,這是加殼后的程序能夠正常運行的基礎,下面我就針對每個部分單獨展開來說。
????????為縮減篇幅,詳細的代碼就不在此貼出,只貼一些比較重要的代碼,具體每個功能所用到的代碼我會標注出在源碼的哪個文件。
第一部分:加殼部分的編寫
????????先說一下文件加殼前后的變化:
???
????????上圖簡單的示意了加殼前后的PE文件變化,首先是多出了一個區(qū)段,用于存放Shell部分的代碼,再就是入口點變?yōu)榱薙hell部分的入口點,這樣保證能夠先運行我們殼部分的代碼,執(zhí)行我們想要的操作,最后就是跳回到原始程序的OEP,開始執(zhí)行原始程序。
????????流程看著簡單,但如果能夠讓一般的PE文件正常運行,需要注意的細節(jié)還是很多的,下面我就來詳細道來。
????????框架:在一個普通的MFC新建工程基礎上,自己添加了兩個類,一個為Pack類,另一個為PE類。?MFC自帶工程僅僅負責界面,加殼的主要流程是在Pack類里,在加殼過程中需要對PE文件操作是,就調(diào)用PE類中的函數(shù)來實現(xiàn)。
????????加殼部分的流程(此流程在Pack類中的Pack()函數(shù)中實現(xiàn)):
????????1.讀取文件PE文件信息并保存?
????????2.加密代碼段操作
????????3.將必要的信息保存到Shell?(Pack部分和Shell部分的數(shù)據(jù)交換)
????????4.將Shell部分附加到PE文件
????????5.保存文件,完成加殼
????????6.釋放資源
?具體實現(xiàn):
?1.讀取文件PE文件信息并保存?
????????要為一個PE文件加殼,首先就是要了解這個PE文件。那么就需要把這個文件讀到內(nèi)存中,加載到內(nèi)存中的方式有兩種,一種是以文件對齊的方式讀到內(nèi)存,也就是直接讀取文件的二進制數(shù)據(jù),另一種方式是以內(nèi)存對齊的方式讀到內(nèi)存,或者其實就在模擬程序運行時的內(nèi)存分部,我選擇的是第二種,以內(nèi)存對齊的方式讀到內(nèi)存,這樣的好處就是在對PE文件進行操作的時候,不需要將相對虛擬地址(RVA)轉換為文件偏移(ROffset),操作起來也更直觀,直接就是內(nèi)存中的偏移地址。讀取文件的代碼詳見源碼PE類中的InitPE(CString?strFilePath)函數(shù),或者參考《加密與解密?第三版》第443頁內(nèi)容。
????????將文件讀取到內(nèi)存以后,下一步就是獲取我們關注的信息并保存了,信息保存在PE類中的成員變量中,這樣在Pack類中只需要定義一個PE類的對象,即可調(diào)用這些信息。
????????我保存的關鍵信息有:保存PE文件的緩沖區(qū)的指針,PE文件的NT頭指針、鏡像大小、鏡像基址、OEP地址、區(qū)段數(shù)量以及重定位表、導入表指針信息。這里保存的信息其實越詳細越好,方便以后拓展功能的時候能夠用到。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | //PE.h public: ??HANDLE??????????m_hFile;??????//PE文件句柄 ??LPBYTE??????????m_pFileBuf;??????//PE文件緩沖區(qū) ??DWORD??????????m_dwFileSize;????//文件大小 ??DWORD??????????m_dwImageSize;????//鏡像大小 ??PIMAGE_DOS_HEADER????m_pDosHeader;????//Dos頭 ??PIMAGE_NT_HEADERS????m_pNtHeader;????//NT頭 ??PIMAGE_SECTION_HEADER??m_pSecHeader;????//第一個SECTION結構體指針 ??DWORD??????????m_dwImageBase;????//鏡像基址 ??DWORD??????????m_dwCodeBase;????//代碼基址 ??DWORD??????????m_dwCodeSize;????//代碼大小 ??DWORD??????????m_dwPEOEP;??????//OEP地址 ??DWORD??????????m_dwShellOEP;????//新OEP地址 ??DWORD??????????m_dwSizeOfHeader;??//文件頭大小 ??DWORD??????????m_dwSectionNum;????//區(qū)段數(shù)量 ??DWORD??????????m_dwFileAlign;????//文件對齊 ??DWORD??????????m_dwMemAlign;????//內(nèi)存對齊 ??DWORD??????????m_IATSectionBase;??//IAT所在段基址 ??DWORD??????????m_IATSectionSize;??//IAT所在段大小 ??IMAGE_DATA_DIRECTORY??m_PERelocDir;????//重定位表信息 ??IMAGE_DATA_DIRECTORY??m_PEImportDir;????//導入表信息 |
????????獲取PE文件信息的函數(shù)?void?GetPEInfo():
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | void?CPE::GetPEInfo() { ??m_pDosHeader??=?(PIMAGE_DOS_HEADER)m_pFileBuf; ??m_pNtHeader????=?(PIMAGE_NT_HEADERS)(m_pFileBuf?+?m_pDosHeader->e_lfanew); ??m_dwFileAlign??=?m_pNtHeader->OptionalHeader.FileAlignment; ??m_dwMemAlign??=?m_pNtHeader->OptionalHeader.SectionAlignment; ??m_dwImageBase??=?m_pNtHeader->OptionalHeader.ImageBase; ??m_dwPEOEP????=?m_pNtHeader->OptionalHeader.AddressOfEntryPoint; ??m_dwCodeBase??=?m_pNtHeader->OptionalHeader.BaseOfCode; ??m_dwCodeSize??=?m_pNtHeader->OptionalHeader.SizeOfCode; ??m_dwSizeOfHeader=?m_pNtHeader->OptionalHeader.SizeOfHeaders; ??m_dwSectionNum??=?m_pNtHeader->FileHeader.NumberOfSections; ??m_pSecHeader??=?IMAGE_FIRST_SECTION(m_pNtHeader); ??m_pNtHeader->OptionalHeader.SizeOfImage?=?m_dwImageSize; ??//保存重定位目錄信息 ??m_PERelocDir?=? ????IMAGE_DATA_DIRECTORY(m_pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]); ??//保存IAT信息目錄信息 ??m_PEImportDir?= ????IMAGE_DATA_DIRECTORY(m_pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]); } |
2.加密代碼段操作
????????說道亦或加密可能大家會笑話我了哈哈,由于加密操作并不是此基礎版殼的主要功能,所以我就用亦或操作象征性的意思意思,能防止靜態(tài)分析就可以啦~
????????加密代碼段是需要用到的PE信息是:PE文件的緩沖區(qū)指針,代碼段基址,代碼段大小。這三個信息在上一步操作中已經(jīng)獲取:
| 1 2 3 4 5 6 7 8 9 | DWORD?CPE::XorCode(BYTE?byXOR) { ??PBYTE?pCodeBase?=?(PBYTE)((DWORD)m_pFileBuf?+?m_dwCodeBase); ??for?(DWORD?i?=?0;?i?<?m_dwCodeSize;?i++) ??{ ????pCodeBase[i]?^=?byXOR; ??} ??return?m_dwCodeSize; } |
????????該函數(shù)返回的是加密的長度,這個變量需要保存在Shell部分,以供Shell部分解密的時候用。
3.將必要的信息保存到Shell?(Pack部分和Shell部分的數(shù)據(jù)交換)
????????對PE文件進行操作是在兩個時候,一個是加殼前的操作,由Pack部分實現(xiàn);另一個是加完殼、程序運行的時候,由Shell部分實現(xiàn),這兩個部分的操作或多或少的都需要一些PE信息,Pack部分可以在主程序中調(diào)用函數(shù)獲取PE信息,而Shell部分如果再去獲取PE信息的話,就顯得繁瑣了許多,所以還不如直接讓Pack部分把Shell所需要的信息告訴它,這時就涉及到兩個部分之間的數(shù)據(jù)交換。
????????我采用的是讓Shell部分導出一個結構體供Pack使用,Pack將Shell加載到內(nèi)存之后,獲取這個結構體的地址,然后將要傳遞的信息保存進這個結構體,保存完畢、在生成文件的時候,Pack所傳遞的結構體數(shù)據(jù)也會一并保存在被加殼后的PE文件中,這樣在殼運行的時候,Shell部分就可以像調(diào)用自己的內(nèi)部變量一樣調(diào)用這些數(shù)據(jù)了,方便了Shell部分的操作。
????????Shell部分所導出的結構體如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //導出ShellData結構體 extern"C"??typedef?struct?_SHELL_DATA { ??DWORD?dwStartFun;??????????????//啟動函數(shù) ??DWORD?dwPEOEP;????????????????//程序入口點 ??DWORD?dwXorKey;????????????????//解密KEY ??DWORD?dwCodeBase;??????????????//代碼段起始地址 ??DWORD?dwXorSize;??????????????//代碼段加密大小 ??DWORD?dwPEImageBase;????????????//PE文件映像基址 ??IMAGE_DATA_DIRECTORY??stcPERelocDir;????//重定位表信息 ??IMAGE_DATA_DIRECTORY??stcPEImportDir;????//導入表信息 ??DWORD??????????dwIATSectionBase;??//IAT所在段基址 ??DWORD??????????dwIATSectionSize;??//IAT所在段大小 ??BOOL??????????bIsShowMesBox;????//是否顯示MessageBox }SHELL_DATA,?*PSHELL_DATA; |
????????Pack部分要做的就是載入Shell.dll這個文件,然后獲取這個結構體信息,往里面保存數(shù)據(jù):
| 1 2 3 4 5 6 7 8 9 10 11 12 | ??HMODULE?hShell?=?LoadLibrary(L"Shell.dll"); ??PSHELL_DATA?pstcShellData?=?(PSHELL_DATA)GetProcAddress(hShell,?"g_stcShellData"); ??pstcShellData->dwXorKey?=?0x15; ??pstcShellData->dwCodeBase?=?objPE.m_dwCodeBase; ??pstcShellData->dwXorSize?=?dwXorSize; ??pstcShellData->dwPEOEP?=?objPE.m_dwPEOEP; ??pstcShellData->dwPEImageBase?=?objPE.m_dwImageBase; ??pstcShellData->stcPERelocDir?=?objPE.m_PERelocDir; ??pstcShellData->stcPEImportDir?=?objPE.m_PEImportDir; ??pstcShellData->dwIATSectionBase?=?objPE.m_IATSectionBase; ??pstcShellData->dwIATSectionSize?=?objPE.m_IATSectionSize; ??pstcShellData->bIsShowMesBox?=?bIsShowMesBox; |
4.將Shell部分附加到PE文件?(此部分代碼不一一貼出,詳見Pack類中的Pack()函數(shù))
????????這一步操作就有點繁瑣了,因為需要注意到很多細節(jié),要不然很容易使得加殼后的程序變成一個“不是有效的Win32程序”,這樣的話你練調(diào)試的機會都沒有,最大的悲劇莫過于此,造成這個的原因大概就是你在添加區(qū)段的時候,沒有適當修改PE文件目錄信息表中的數(shù)據(jù)。
4.1.讀取Shell代碼
????????之前已經(jīng)通過LoadLibrary(L"Shell.dll")的方式加載了Shell模塊,但為了操作方便我還是申請了一塊空間專門存放Shell部分,同時獲取一下Shell的鏡像大小,為增加區(qū)段做準備。
4.2.設置Shell重定位信息
????????由于我們的Shell部分是DLL,默認加載基址是0x10000000,而我們要將DLL文件移植到EXE文件,EXE文件大多默認加載基址是0x00400000,再加上有些程序會有隨機基址,這時候基址就更加不定了,所以要想順利執(zhí)行Shell部分的代碼,進行重定位是必須的!
????????重定位的實現(xiàn)有兩種,一種是系統(tǒng)的PE加載器通過重定位表的信息,在加載程序之前給你重定位好;另一種則是用代碼進行手動重定位,模擬PE加載器所進行的重定位操作。由于我們的Shell部分是最先執(zhí)行的,所以不妨讓系統(tǒng)直接給我們的殼部分的代碼進行重定位,而原程序部分的代碼重定位,則只能通過我們在Shell部分用代碼手動實現(xiàn)。
????????簡言之,讓系統(tǒng)重定位Shell部分的代碼,保證Shell部分的函數(shù)可以正常執(zhí)行。然后我們在Shell部分手動重定位原程序代碼,讓原程序能夠執(zhí)行。
????????那么問題來了,我們該如何重定位Shell部分的代碼呢?下面可是重點哦!
????????由于Shell部分的代碼我是通過LoadLibrary(L"Shell.dll")的方式加載的,這說明加載到內(nèi)存中之前,PE加載器以及幫我們修復過重定位了。而我們現(xiàn)在再去訪問重定位表的信息的時候,是已經(jīng)修復過的正確的重定位信息,而我們想要的是原始的重定位信息,把原始的重定位信息寫入加殼后的文件,當PE加載器運行被加殼程序的時候,才能通過原始的重定位信息給我們的Shell部分進行正確的重定位,所以首先就是恢復重定位之前的原始信息。
????????拿一條重定位數(shù)據(jù)舉例來說:
????????①重定位原始地址=重定位后的地址-加載時的鏡像基址
????????重定位原始地址是一個內(nèi)存相對偏移(RVA),需要把這個RVA加上PE文件默認的加載基址,然后再寫回重定位表中的數(shù)據(jù),才能讓系統(tǒng)正確的進行重定位。
????????②新的重定位地址=重定位原始地址+新的鏡像基址
????????由①②可得:③新的重定位地址=重定位后的地址-加載時的鏡像基址+新的鏡像基址
????????但由于我們的Shell部分是加載到PE文件的末尾,所以RVA地址還需要加上那個PE文件的鏡像大小
????????最終得出④新的重定位地址=重定位后的地址-加載時的鏡像基址+新的鏡像基址+代碼基址(PE文件鏡像大小)
????????只需要把思所得出的地址信息,寫到加殼后的的PE文件中,系統(tǒng)就可以幫你正確的進行重定位了,當然,由于要重定位的信息我們自己添加的,需要通過修改PE文件目錄表中的重定位信息來告訴系統(tǒng)應該去哪找重定位表。
????????這些操作必須對PE文件的重定位信息有一定的了解,如果不了解的話,建議大家還是先惡補一下這方面的知識,推薦參考資料《黑客免殺攻防》第七章?PE文件格式詳解。當你對這些基本信息有一點了解之后,再閱讀這段代碼應該會很容易:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | BOOL?CPE::SetShellReloc(LPBYTE?pShellBuf,?DWORD?hShell) { ??typedef?struct?_TYPEOFFSET ??{ ????WORD?offset?:?12;??????//偏移值 ????WORD?Type??:?4;??????//重定位屬性(方式) ??}TYPEOFFSET,?*PTYPEOFFSET; ??//1.獲取被加殼PE文件的重定位目錄表指針信息 ??PIMAGE_DATA_DIRECTORY?pPERelocDir?= ????&(m_pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]); ??? ??//2.獲取Shell的重定位表指針信息 ??PIMAGE_DOS_HEADER????pShellDosHeader?=?(PIMAGE_DOS_HEADER)pShellBuf; ??PIMAGE_NT_HEADERS????pShellNtHeader?=?(PIMAGE_NT_HEADERS)(pShellBuf?+?pShellDosHeader->e_lfanew); ??PIMAGE_DATA_DIRECTORY??pShellRelocDir?= ????&(pShellNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]); ??PIMAGE_BASE_RELOCATION??pShellReloc?=? ????(PIMAGE_BASE_RELOCATION)((DWORD)pShellBuf?+?pShellRelocDir->VirtualAddress); ??? ??//3.還原修復重定位信息 ??//由于Shell.dll是通過LoadLibrary加載的,所以系統(tǒng)會對其進行一次重定位 ??//我們需要把Shell.dll的重定位信息恢復到系統(tǒng)沒加載前的樣子,然后在寫入被加殼文件的末尾 ??PTYPEOFFSET?pTypeOffset?=?(PTYPEOFFSET)(pShellReloc?+?1); ??DWORD?dwNumber?=?(pShellReloc->SizeOfBlock?-?8)?/?2; ??for?(DWORD?i?=?0;?i?<?dwNumber;?i++) ??{ ????if?(*(PWORD)(&pTypeOffset[i])?==?NULL) ??????break; ????//RVA ????DWORD?dwRVA?=pTypeOffset[i].offset?+?pShellReloc->VirtualAddress; ????//FAR地址(LordPE中這樣標注) ????//***新的重定位地址=重定位后的地址-加載時的鏡像基址+新的鏡像基址+代碼基址(PE文件鏡像大小) ????DWORD?AddrOfNeedReloc?=??*(PDWORD)((DWORD)pShellBuf?+?dwRVA); ????*(PDWORD)((DWORD)pShellBuf?+?dwRVA)? ??????=?AddrOfNeedReloc?-?pShellNtHeader->OptionalHeader.ImageBase?+?m_dwImageBase?+?m_dwImageSize; ??} ??//3.1修改Shell重定位表中.text的RVA ??pShellReloc->VirtualAddress?+=?m_dwImageSize; ??//4.修改PE重定位目錄指針,指向Shell的重定位表信息 ??pPERelocDir->Size?=?pShellRelocDir->Size; ??pPERelocDir->VirtualAddress?=?pShellRelocDir->VirtualAddress?+?m_dwImageSize; ??return?TRUE; } |
4.3.修改被加殼程序的OEP,指向Shell
????????這樣就可以讓程序運行時從Shell部分開始執(zhí)行,那么這個地址改如何確定呢?還記得之前我們的Shell部分所導出的那個結構體么?對,其第一個變量就是Shell部分啟動函數(shù)的地址,讓OEP執(zhí)行這個地址即可。
4.4.合并PE文件和Shell的代碼到新的緩沖區(qū)
????????目前我們內(nèi)存中有兩個緩沖區(qū),一個是原PE程序的緩沖區(qū),另一個是Shell部分的緩沖區(qū),我們要做的就是重新申請一塊連續(xù)的空間,大小為這兩個緩沖區(qū)的大小,然后那他們拷貝進去。內(nèi)存中這樣處理是沒問題的,這時如果保存這個緩沖區(qū)的話就是兩個文件的結合體,但邏輯上并沒有如此簡單,因為你從物理上多出來一個Shell區(qū)段,但邏輯上并沒有變,也就是系統(tǒng)并不認識這個新加的Shell區(qū)段,這時候你就又需要去修改PE文件信息了,這次需要修改的是區(qū)段表中的信息,涉及到新增一個區(qū)段目錄表信息,并正確設置該區(qū)段的起始地址、大小等信息,而且在設置這些信息的時候還要考慮到文件對齊的問題,源代碼中則是MergeBuf(?)函數(shù)實現(xiàn)了這兩個緩沖區(qū)的合并,并添加區(qū)段信息:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | void?CPE::MergeBuf(LPBYTE?pFileBuf,?DWORD?pFileBufSize, ??LPBYTE?pShellBuf,?DWORD?pShellBufSize,? ??LPBYTE&?pFinalBuf,?DWORD&?pFinalBufSize) { ??//獲取最后一個區(qū)段的信息 ??PIMAGE_DOS_HEADER?pDosHeader?=?(PIMAGE_DOS_HEADER)pFileBuf; ??PIMAGE_NT_HEADERS?pNtHeader?=?(PIMAGE_NT_HEADERS)(pFileBuf?+?pDosHeader->e_lfanew); ??PIMAGE_SECTION_HEADER?pSectionHeader?=?IMAGE_FIRST_SECTION(pNtHeader); ??PIMAGE_SECTION_HEADER?pLastSection?= ????&pSectionHeader[pNtHeader->FileHeader.NumberOfSections?-?1]; ??//1.修改區(qū)段數(shù)量 ??pNtHeader->FileHeader.NumberOfSections?+=?1; ??//2.編輯區(qū)段表頭結構體信息 ??PIMAGE_SECTION_HEADER?AddSectionHeader?= ????&pSectionHeader[pNtHeader->FileHeader.NumberOfSections?-?1]; ??memcpy_s(AddSectionHeader->Name,?8,?".cyxvc",?7); ??//VOffset(1000對齊) ??DWORD?dwTemp?=?0; ??dwTemp?=?(pLastSection->Misc.VirtualSize?/?m_dwMemAlign)?*?m_dwMemAlign; ??if?(pLastSection->Misc.VirtualSize?%?m_dwMemAlign) ??{ ????dwTemp?+=?0x1000; ??} ??AddSectionHeader->VirtualAddress?=?pLastSection->VirtualAddress?+?dwTemp; ??//Vsize(實際添加的大小) ??AddSectionHeader->Misc.VirtualSize?=?pShellBufSize; ??//ROffset(舊文件的末尾) ??AddSectionHeader->PointerToRawData?=?pFileBufSize; ??//RSize(200對齊) ??dwTemp?=?(pShellBufSize?/?m_dwFileAlign)?*?m_dwFileAlign; ??if?(pShellBufSize?%?m_dwFileAlign) ??{ ????dwTemp?+=?m_dwFileAlign; ??} ??AddSectionHeader->SizeOfRawData?=?dwTemp; ??//區(qū)段屬性標志(可讀可寫可執(zhí)行) ??AddSectionHeader->Characteristics?=?0XE0000040; ??//3.修改PE頭文件大小屬性,增加文件大小 ??dwTemp?=?(pShellBufSize?/?m_dwMemAlign)?*?m_dwMemAlign; ??if?(pShellBufSize?%?m_dwMemAlign) ??{ ????dwTemp?+=?m_dwMemAlign; ??} ??pNtHeader->OptionalHeader.SizeOfImage?+=?dwTemp; ??//4.申請合并所需要的空間 ??pFinalBuf?=?new?BYTE[pFileBufSize?+?dwTemp]; ??pFinalBufSize?=?pFileBufSize?+?dwTemp; ??memset(pFinalBuf,?0,?pFileBufSize?+?dwTemp); ??memcpy_s(pFinalBuf,?pFileBufSize,?pFileBuf,?pFileBufSize); ??memcpy_s(pFinalBuf?+?pFileBufSize,?dwTemp,?pShellBuf,?dwTemp); } |
5.保存文件
????????保存文件就是保存上個函數(shù)中所合并的緩沖區(qū),由于我是直接從從內(nèi)存中dump出來的,其分布也是以內(nèi)存對齊大小所對齊的,所以保存成文件的時候我順便修改了文件對齊大小,同內(nèi)存對齊大小相同;同時將數(shù)據(jù)目錄表中不必要的信息摸掉:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | BOOL?CPACK::SaveFinalFile(LPBYTE?pFinalBuf,?DWORD?pFinalBufSize,?CString?strFilePath) { ??//修正區(qū)段信息中?文件對齊大小(文件對齊大小同內(nèi)存對齊大小) ??PIMAGE_DOS_HEADER?pDosHeader?=?(PIMAGE_DOS_HEADER)pFinalBuf; ??PIMAGE_NT_HEADERS?pNtHeader?=?(PIMAGE_NT_HEADERS)(pFinalBuf?+?pDosHeader->e_lfanew); ??PIMAGE_SECTION_HEADER?pSectionHeader?=?IMAGE_FIRST_SECTION(pNtHeader); ??for?(DWORD?i?=?0;?i?<?pNtHeader->FileHeader.NumberOfSections;?i++,?pSectionHeader++) ??{ ????pSectionHeader->PointerToRawData?=?pSectionHeader->VirtualAddress; ??} ??//清除不需要的目錄表信息 ??//只留輸出表,重定位表,資源表 ??DWORD?dwCount?=?15; ??for?(DWORD?i?=?0;?i?<?dwCount;?i++) ??{ ????if?(i?!=?IMAGE_DIRECTORY_ENTRY_EXPORT?&&? ??????i?!=?IMAGE_DIRECTORY_ENTRY_RESOURCE?&& ??????i?!=?IMAGE_DIRECTORY_ENTRY_BASERELOC?) ????{ ??????pNtHeader->OptionalHeader.DataDirectory[i].VirtualAddress?=?0; ??????pNtHeader->OptionalHeader.DataDirectory[i].Size?=?0; ????} ??} ??//獲取保存路徑 ??TCHAR?strOutputPath[MAX_PATH]?=?{?0?}; ??LPWSTR?strSuffix?=?PathFindExtension(strFilePath); ??wcsncpy_s(strOutputPath,?MAX_PATH,?strFilePath,?wcslen(strFilePath)); ??PathRemoveExtension(strOutputPath); ??wcscat_s(strOutputPath,?MAX_PATH,?L"_cyxvc"); ??wcscat_s(strOutputPath,?MAX_PATH,?strSuffix); ??//保存文件 ??HANDLE?hNewFile?=?CreateFile( ????strOutputPath, ????GENERIC_READ?|?GENERIC_WRITE, ????0, ????NULL, ????CREATE_ALWAYS,?FILE_ATTRIBUTE_NORMAL,?NULL); ??if?(hNewFile?==?INVALID_HANDLE_VALUE) ??{ ????MessageBox(NULL,?_T("保存文件失敗!"),?_T("提示"),?MB_OK); ????return?FALSE; ??} ??DWORD?WriteSize?=?0; ??BOOL?bRes?=?WriteFile(hNewFile,?pFinalBuf,?pFinalBufSize,?&WriteSize,?NULL); ??if?(bRes) ??{ ????CloseHandle(hNewFile); ????return?TRUE; ??} ??else ??{ ????CloseHandle(hNewFile); ????MessageBox(NULL,?_T("保存文件失敗!"),?_T("提示"),?MB_OK); ????return?FALSE; ??} } |
6.釋放資源
????????萬事大吉以后不要忘記擦屁股,養(yǎng)成良好的習慣,釋放掉資源,避免資源泄露(雖然這個小程序也用不了多少內(nèi)存吧哈哈!)
| 1 2 3 4 | ??delete[]?objPE.m_pFileBuf; ??delete[]?pShellBuf; ??delete[]?pFinalBuf; ??objPE.InitValue(); |
至此,第一部分(Pack)的功能已經(jīng)完成,剩下的就是Shell部分的編寫了!
第二部分:外殼程序(Shell.dll)的編寫
????????殼的Shell部分,網(wǎng)上很多人在用匯編編寫,匯編的優(yōu)勢是不需要進行重定位,移植性好,用C++的話需要重定位,而這個問題在第一部分已經(jīng)解決了,再配合上兩部分的數(shù)據(jù)交換,Shell部分用C++寫起來也變得如魚得水,代碼的可讀性有了很大的提高(好吧,我承認,其實是我不會用匯編寫...)。
????????外殼程序部分的流程(此流程在Shell工程中的Start()函數(shù)中實現(xiàn)):
????????1.獲取Shell部分所用到的函數(shù)
????????2.解密代碼段
????????3.修復原程序的重定位信息
????????4.修復原程序的導入表(IAT)信息
????????5.跳到程序入口點,將控制權交還給程序
具體實現(xiàn):
1.獲取Shell部分所用到的函數(shù)
????????有很多殼為了隱藏自己的行為,不讓別人看出它用到了哪些函數(shù),直接沒有導入表,不讓PE加載器為其導入函數(shù)。而是直接自己獲取所用到的函數(shù),此項目也是用的這種方法,通過代碼實現(xiàn)導入Shell部分所需要的函數(shù)。
????????無論一個PE文件是否有導入表,系統(tǒng)都會為其加載兩個模塊,ntdll.dll和Kernel32.dll,那么我們就從這兩個模塊入手,目標是獲取GetProcAddress()這個函數(shù),而這個函數(shù)位于Kernel32.dll中,那么我們首先要做的就是獲取Kernel32.dll的加載基址,常用的方法有三種:①通過特征匹配的暴力搜索。②利用系統(tǒng)的SEH機制找到Kernel32.dll的加載基址。③通過線程環(huán)境塊TEB的信息逐步找到Kernel32.dll的加載基址。我在這里用的是第三種,詳細的代碼請見Shell部分的MyGetProcAddress()函數(shù),第三種方法代碼簡介,但存在兼容性問題,因為XP和Win7下獲取基址時的代碼略有出入,所以如果你為了兼容性考慮,請改用其他兩種方法。
????????在獲取了Kernel32.dll的加載基址以后,就可以通過遍歷Kernel32.dll的導出表來搜索GetProcAddress(),從而獲取GetProcAddress()函數(shù)的函數(shù)地址:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | DWORD?MyGetProcAddress() { ??HMODULE?hModule?=?GetKernel32Addr(); ??//1.獲取DOS頭 ??PIMAGE_DOS_HEADER?pDosHeader?=?(PIMAGE_DOS_HEADER)(PBYTE)hModule; ??//2.獲取NT頭 ??PIMAGE_NT_HEADERS?pNtHeader?=?(PIMAGE_NT_HEADERS)((PBYTE)hModule?+?pDosHeader->e_lfanew); ??//3.獲取導出表的結構體指針 ??PIMAGE_DATA_DIRECTORY?pExportDir?= ????&(pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]); ??PIMAGE_EXPORT_DIRECTORY?pExport?=? ????(PIMAGE_EXPORT_DIRECTORY)((PBYTE)hModule?+?pExportDir->VirtualAddress); ??//EAT ??PDWORD?pEAT?=?(PDWORD)((DWORD)hModule?+?pExport->AddressOfFunctions); ??//ENT ??PDWORD?pENT?=?(PDWORD)((DWORD)hModule?+?pExport->AddressOfNames); ??//EIT ??PWORD?pEIT?=?(PWORD)((DWORD)hModule?+?pExport->AddressOfNameOrdinals); ??//4.遍歷導出表,獲取GetProcAddress()函數(shù)地址 ??DWORD?dwNumofFun?=?pExport->NumberOfFunctions; ??DWORD?dwNumofName?=?pExport->NumberOfNames; ??for?(DWORD?i?=?0;?i?<?dwNumofFun;?i++) ??{ ????//如果為無效函數(shù),跳過 ????if?(pEAT[i]?==?NULL) ??????continue; ????//判斷是以函數(shù)名導出還是以序號導出 ????DWORD?j?=?0; ????for?(;?j?<?dwNumofName;?j++) ????{ ??????if?(i?==?pEIT[j]) ??????{ ????????break; ??????} ????} ????if?(j?!=?dwNumofName) ????{ ??????//如果是函數(shù)名方式導出的 ??????//函數(shù)名 ??????char*?ExpFunName?=?(CHAR*)((PBYTE)hModule?+?pENT[j]); ??????//進行對比,如果正確返回地址 ??????if?(!strcmp(ExpFunName,?"GetProcAddress")) ??????{ ????????return?pEAT[i]?+?pNtHeader->OptionalHeader.ImageBase; ??????} ????} ????else ????{ ??????//序號 ????} ??} ??return?0; } |
????????以上這段代碼就是遍歷Kernel32.dll的導出表并返回GetProcAddress()的地址,有了該函數(shù)的地址,我們就可以自己定義一個和GetProcAddress()函數(shù)原型一模一樣的函數(shù)指針來調(diào)用GetProcAddress()函數(shù)了,有了這個函數(shù)指針,你想用什么函數(shù)自己獲取就行了,然后在順便獲取一下LoadLibraryA()的函數(shù)地址,這樣就可以任意得加載模塊了。
2.解密代碼段
????????解密代碼段的操作其實就是在做與Pack部分加密的逆向操作,由于亦或操作是可逆的(兩次亦或相同的值還是等于原來的值),所以Pack部分亦或什么,這里解密也就亦或什么就可以了。代碼過于簡單就不貼了...
3.修復原程序的重定位信息
????????由于加殼程序的重定位指針指向了Shell部分的重定位,PE加載器在加載PE文件的時候對Shell部分的代碼進行了重定位,所以本應給原程序進行的重定位就需要我們在Shell部分實現(xiàn)了,此項目原程序的重定位表并沒有遭到破壞(有些殼會對重定位表進行破壞或加密),所以我們只要在Pack部分加殼的時候保存一下原程序的重定位表指針,然后在Shell部分對這個指針所指向的重定位表進行重定位就可以了,其實就是在模擬PE加載器的重定位操作。
????????原程序的重定位表指針在Pack的時候有保存過,這里直接拿來用就可以了。
????????重定位表最終指向的是一個需要重定位的地址,這個地址是基于原PE文件默認基址(一般為0x00400000)的地址,原PE文件的默認基址我們也有保存過,所以修復起來還是比較方便的,只需要遍歷原PE文件的重定位表,然后通過一個公式計算出重定位后的地址再填充回去就可以了。
????????計算公式:重定位后的地址=需要重定位的地址-默認加載基址+當前真實的加載基址。
????????還有一點需要注意的是,在修復的時候你所修復的地址的內(nèi)存屬性不一定是可寫的,所以最好在修復之前用VirtualProtect()修改內(nèi)存屬性為可寫,修復完以后再將原來的屬性設置回去。
????????遍歷重定位表并修復:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | void?RecReloc() { ??typedef?struct?_TYPEOFFSET ??{ ????WORD?offset?:?12;????//偏移值 ????WORD?Type?:?4;??????//重定位屬性(方式) ??}TYPEOFFSET,?*PTYPEOFFSET; ??//1.獲取重定位表結構體指針 ??PIMAGE_BASE_RELOCATION??pPEReloc= ????(PIMAGE_BASE_RELOCATION)(dwImageBase?+?g_stcShellData.stcPERelocDir.VirtualAddress); ??? ??//2.開始修復重定位 ??while?(pPEReloc->VirtualAddress) ??{ ????//2.1修改內(nèi)存屬性為可寫 ????DWORD?dwOldProtect?=?0; ????g_pfnVirtualProtect((PBYTE)dwImageBase?+?pPEReloc->VirtualAddress, ??????0x1000,?PAGE_EXECUTE_READWRITE,?&dwOldProtect); ????//2.2修復重定位 ????PTYPEOFFSET?pTypeOffset?=?(PTYPEOFFSET)(pPEReloc?+?1); ????DWORD?dwNumber?=?(pPEReloc->SizeOfBlock?-?8)?/?2; ????for?(DWORD?i?=?0;?i?<?dwNumber;?i++) ????{ ??????if?(*(PWORD)(&pTypeOffset[i])?==?NULL) ????????break; ??????//RVA ??????DWORD?dwRVA?=?pTypeOffset[i].offset?+?pPEReloc->VirtualAddress; ??????//FAR地址 ??????DWORD?AddrOfNeedReloc?=?*(PDWORD)((DWORD)dwImageBase?+?dwRVA); ??????*(PDWORD)((DWORD)dwImageBase?+?dwRVA)?=? ????????AddrOfNeedReloc?-?g_stcShellData.dwPEImageBase?+?dwImageBase; ????} ????//2.3恢復內(nèi)存屬性 ????g_pfnVirtualProtect((PBYTE)dwImageBase?+?pPEReloc->VirtualAddress, ??????0x1000,?dwOldProtect,?&dwOldProtect); ????//2.4修復下一個區(qū)段 ????pPEReloc?=?(PIMAGE_BASE_RELOCATION)((DWORD)pPEReloc?+?pPEReloc->SizeOfBlock); ??} } |
4.修復原程序的導入表(IAT)信息
????????這又是一個很重要的知識點!導入表的用途是保存該PE文件用到的API函數(shù)的信息,由于每次啟動程序時這些API函數(shù)的加載地址可能會不一樣,所以PE文件中無法直接保存一個函數(shù)地址,而是保存這些函數(shù)的信息,當啟動程序時,PE加載器會通過導入表信息為其加載所用到的模塊,并獲取需要調(diào)用的API函數(shù)的地址,再將該地址填充到IAT,這樣程序才能正常的調(diào)用API。由于修復導入表最終是為了填充IAT(只要IAT中的函數(shù)地址正確,沒有導入表信息也可以),所以又叫做修復IAT,這項技術不僅用再殼來修復原程序的IAT,在脫殼的時候也會用到IAT修復,只有正確修復了IAT才能讓程序正常運行,所以很多殼會在IAT加密上做文章,來防止脫殼成功。
????????此項目中沒有破換原有PE文件的導入表信息,所以在Pack的時候保存導入表指針就可以找到導入表信息。有人會問,那為什么不直接讓PE加載器來修復呢,其實這樣也是可以的,只要你填寫正確的導入表指針信息,系統(tǒng)PE加載器就可以幫你修復,但殼很少會這么做,我也是為了給IAT加密做基礎,所以也沒讓PE加載器來替我修復原PE文件的導入表。
????????那我就來簡單說一下修復IAT的過程,那就是通過導入表指針遍歷導入表信息,里面保存著需要導入的函數(shù)的名稱和所在模塊,我們所要做的就是加載這些模塊,并從中獲取函數(shù)地址,然后舔到正確的IAT位置即可。
????????加載模塊和獲取函數(shù)地址這兩個函數(shù)之前我們通過自定義函數(shù)指針的方式已經(jīng)獲取到了,那么剩下的就是在遍歷導入表了,具體代碼實現(xiàn)如下:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | void?RecIAT() { ??//1.獲取導入表結構體指針 ??PIMAGE_IMPORT_DESCRIPTOR?pPEImport?=? ????(PIMAGE_IMPORT_DESCRIPTOR)(dwImageBase?+?g_stcShellData.stcPEImportDir.VirtualAddress); ??? ??//2.修改內(nèi)存屬性為可寫 ??DWORD?dwOldProtect?=?0; ??g_pfnVirtualProtect( ????(LPBYTE)(dwImageBase?+?g_stcShellData.dwIATSectionBase),?g_stcShellData.dwIATSectionSize, ????PAGE_EXECUTE_READWRITE,?&dwOldProtect); ??//3.開始修復IAT ??while?(pPEImport->Name) ??{ ????//獲取模塊名 ????DWORD?dwModNameRVA?=?pPEImport->Name; ????char*?pModName?=?(char*)(dwImageBase?+?dwModNameRVA); ????HMODULE?hMod?=?g_pfnLoadLibraryA(pModName); ????//獲取IAT信息(有些PE文件INT是空的,最好用IAT解析,也可兩個都解析作對比) ????PIMAGE_THUNK_DATA?pIAT?=?(PIMAGE_THUNK_DATA)(dwImageBase?+?pPEImport->FirstThunk); ????? ????//獲取INT信息(同IAT一樣,可將INT看作是IAT的一個備份) ????//PIMAGE_THUNK_DATA?pINT?=?(PIMAGE_THUNK_DATA)(dwImageBase?+?pPEImport->OriginalFirstThunk); ????//通過IAT循環(huán)獲取該模塊下的所有函數(shù)信息(這里之獲取了函數(shù)名) ????while?(pIAT->u1.AddressOfData) ????{ ??????//判斷是輸出函數(shù)名還是序號 ??????if?(IMAGE_SNAP_BY_ORDINAL(pIAT->u1.Ordinal)) ??????{ ????????//輸出序號 ????????DWORD?dwFunOrdinal?=?(pIAT->u1.Ordinal)?&?0x7FFFFFFF; ????????DWORD?dwFunAddr?=?g_pfnGetProcAddress(hMod,?(char*)dwFunOrdinal); ????????*(DWORD*)pIAT?=?(DWORD)dwFunAddr; ??????} ??????else ??????{ ????????//輸出函數(shù)名 ????????DWORD?dwFunNameRVA?=?pIAT->u1.AddressOfData; ????????PIMAGE_IMPORT_BY_NAME?pstcFunName?=?(PIMAGE_IMPORT_BY_NAME)(dwImageBase?+?dwFunNameRVA); ????????DWORD?dwFunAddr?=?g_pfnGetProcAddress(hMod,?pstcFunName->Name); ????????*(DWORD*)pIAT?=?(DWORD)dwFunAddr; ??????} ??????pIAT++; ????} ????//遍歷下一個模塊 ????pPEImport++; ??} ??//4.恢復內(nèi)存屬性 ??g_pfnVirtualProtect( ????(LPBYTE)(dwImageBase?+?g_stcShellData.dwIATSectionBase),?g_stcShellData.dwIATSectionSize, ????dwOldProtect,?&dwOldProtect); } |
5.跳到程序入口點,將控制權交還給程序
????????經(jīng)過了修復重定位和修復IAT的操作以后,原PE文件就已經(jīng)可以正常執(zhí)行了,在跳回到原程序入口點之前把你想做的事做完吧!
第三部分:總結
????????至此,這個基礎版的殼就完成了,雖然并沒有什么加密、壓縮、反調(diào)試的功能,但在這個框架的基礎上進行拓展也會容易很多,畢竟都是用C++寫的嘛。
????????為了演示,我在殼的Shell部分增加了一個彈出MessageBox的操作,表示成功執(zhí)行了Shell部分的代碼。
????????界面壓根就沒做,看著很挫,這個基礎版就沒有在意這些,為了演示大家將就看:
???
????????加殼以后,運行程序彈出MessageBox:
???
????????加殼測試在?Win7?x64?系統(tǒng)下,對大多數(shù)程序加殼后都可以正常運行,不支持dll加殼。
????????有興趣的朋友可以下載源碼看一下,由于本人才疏學淺、能力有限,有錯的第地方還請大牛指出!
????????注:此殼不以加密為目的,被脫秒秒種的事...??
???源碼下載:?CyxvcProtect@15PB.rar
總結
以上是生活随笔為你收集整理的用C++实现的壳(基础版)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一个感染型的病毒逆向分析
- 下一篇: C++ 操作64位系统,默认读取Wow6