程序隐藏、加壳、内存加载执行的一种实验方案
源码地址点击此处
该方案比较简单,只适用于测试和简单情景下的应用,测试中能够运行大多数体积和功能比较简单的32位程序,兼容性和稳定性有待测试和提高。
(一)原理
源码包含两个工程,加壳程序PeShell和脱壳程序PeUnshell。
加壳程序:PeShell工程。该工程根据命令行参数,将需要加壳的程序和文件先用zip压缩,后用xor加密,生成一段新的数据,接着在PEUnshell.exe程序中创建一个新的段区,名称默认为"ldata",最后将这段数据写入在PEUnshell.exe文件的新的"ldata"段中。这块加密数据的内存影像(从低地址到高地址)为:
文件个数(int型),密钥(char型,16字节,用的当前时间的md5值),文件大小1(int),文件名1(char型,64字节),文件大小2(int),文件名2(char型,64字节),。。。,,文件大小n(int),文件名n(char型,64字节)。
代码实现:
unsigned char* Crypto::makeDataBlock(int flag, const char filename[MAX_FILE_COUNT][256], int cnt, int& dstdatasize) {int ret = 0;int filesize = 0;for (int i = 0; i < cnt; i++){int fz = FileHelper::getfilesize(filename[i]);filesize += fz;printf("file name:%s size:%d\\r\\n", filename[i], fz);}int dstbufsize = filesize + 0x1000;unsigned char* dstblock = new unsigned char[dstbufsize];*(int*)dstblock = flag;// if (cnt == 1 && strstr((char*)filename[0],".exe") )// {// *(int*)dstblock = ONLY_ONE_EXE;// }// else if (cnt == 1 && strstr((char*)filename[0], ".dll"))// {// *(int*)dstblock = ONLY_ONE_DLL;// }// else if (cnt > 1 )// {// int flagexe = 0;// int flagdll = 0;// for (int i = 0;i < cnt; i ++)// {// if (strstr((char*)filename[i], ".dll")) {// flagdll = 1;// }else if (strstr(filename[i],".exe"))// {// flagexe = 1;// }// }// // if (flagexe && flagdll)// {// *(int*)dstblock = ONE_EXE_AND_ONE_DLL;// }// else {// *(int*)dstblock = SOME_OTHER_FILES;// }// }// else {// return 0;// }unsigned char* key = dstblock + 4;getkey(key);*(int*)(dstblock + 4 + CRYPT_KEY_SIZE) = cnt;unsigned char* dstbuf = dstblock + 4 + CRYPT_KEY_SIZE + 4;int dstbuflimit = dstbufsize - 4 - CRYPT_KEY_SIZE - 4;for (int i = 0; i < cnt; i++){lstrcpyA((char*)dstbuf, filename[i]);PathStripPathA((char*)dstbuf);dstbuf += FILENAME_LEN;dstbuflimit -= FILENAME_LEN;char* lpdata = 0;ret = FileHelper::fileReader(filename[i], &lpdata, &filesize);if (ret > 0){unsigned long cmpresssize = dstbuflimit - 4;ret = Compress::compressdata((unsigned char*)lpdata, filesize, dstbuf + 4, &cmpresssize);delete[] lpdata;if (ret != 0){delete dstblock;printf("compress file:%s error:%u\\r\\n", filename[i], GetLastError());return 0;}*(int*)(dstbuf) = cmpresssize;dstbuf += 4;dstbuf += cmpresssize;dstbuflimit -= 4;dstbuflimit -= cmpresssize;}else {delete dstblock;printf("read file:%s error\\r\\n", filename[i]);return 0;}}dstdatasize = dstbuf - dstblock;CryptData(dstblock + 4 + CRYPT_KEY_SIZE, dstdatasize - 4 - CRYPT_KEY_SIZE, key, CRYPT_KEY_SIZE);//revertkey(key);return dstblock;
}
加壳命令行的写法:
PeShell -be sogou.exe sbiedll.dll -c params.dat -o out.exePeShell -bd sogou.exe sbiedll.dll -c params.dat -o out.dllPeShell -e sogou.exe -c params.dat -o out.exePeShell -d sogou.exe -c params.dat -o out.dll
参数的解释:
-be:b代表bind,绑定的意思;e代表executable,可执行的。
-bd:b代表bind,绑定的意思;d代表dll,即动态重定位文件。
-o: 输出文件,即加壳后的程序。
-c: 参数。
加壳程序比较复杂的代码是添加区段。pe文件的区段结构如下:
typedef struct _IMAGE_SECTION_HEADER {BYTE Name[IMAGE_SIZEOF_SHORT_NAME];union {DWORD PhysicalAddress;DWORD VirtualSize;} Misc;DWORD VirtualAddress;DWORD SizeOfRawData;DWORD PointerToRawData;DWORD PointerToRelocations;DWORD PointerToLinenumbers;WORD NumberOfRelocations;WORD NumberOfLinenumbers;DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
其中,VirtualSize是区段大小,VirtualAddress是对齐后的虚拟地址,SizeOfRawData是文件对齐后的大小,PointerToRawData是区段在文件中相对于文件头的偏移地址。这里采用的思路是,一般情况下,段表结构PIMAGE_SECTION_HEADER中有空余的项,找到后空项后,计算和重新修改该段表项即可。其中,
VirtualAddress字段=上一个段表的值+上一个段表内存页面对齐后的大小,
PointerToRawData = 上一个段表的值+上一个段表扇区对齐后的大小,
SizeOfRawData是段文件对齐后大小,
VirtualSize是段实际大小,
Characteristics必须是代码或者数据段,否则不会被装入内存,
IMAGE_FILE_HEADER中NumberOfSections字段加1,
IMAGE_OPTIONAL_HEADER32中SizeOfImage字段要加上该段对齐后的大小。
上述功能中使用的结构体如下:
typedef struct _IMAGE_FILE_HEADER {WORD Machine;WORD NumberOfSections;DWORD TimeDateStamp;DWORD PointerToSymbolTable;DWORD NumberOfSymbols;WORD SizeOfOptionalHeader;WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;typedef struct _IMAGE_OPTIONAL_HEADER {WORD Magic;BYTE MajorLinkerVersion;BYTE MinorLinkerVersion;DWORD SizeOfCode;DWORD SizeOfInitializedData;DWORD SizeOfUninitializedData;DWORD AddressOfEntryPoint;DWORD BaseOfCode;DWORD BaseOfData;DWORD ImageBase;DWORD SectionAlignment;DWORD FileAlignment;WORD MajorOperatingSystemVersion;WORD MinorOperatingSystemVersion;WORD MajorImageVersion;WORD MinorImageVersion;WORD MajorSubsystemVersion;WORD MinorSubsystemVersion;DWORD Win32VersionValue;DWORD SizeOfImage;DWORD SizeOfHeaders;DWORD CheckSum;WORD Subsystem;WORD DllCharacteristics;DWORD SizeOfStackReserve;DWORD SizeOfStackCommit;DWORD SizeOfHeapReserve;DWORD SizeOfHeapCommit;DWORD LoaderFlags;DWORD NumberOfRvaAndSizes;IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
至此,加壳程序工作基本完成。
解壳和内存加载执行:即PEUnshell工程。该工程支持编译为PEUnshell.exe和PEUnshell.dll,可以根据命令行参数,将程序隐藏在exe或者dll中,并且都可以正确的释放和加载运行。在程序运行时(PEUnshell.exe是在WinMain函数入口处,PEUnshell.dll是在DllEntry入口处)在自己的内存加载映像中查找"ldata"区段,然后按照加壳中定义的数据结构,将这段压缩加密内存数据解密解压后,写入当前目录,并将其中后缀名为exe的程序启动起来。启动方法是内存加载执行。内存加载执行主要分为三个部分,段加载,导入表填充,重定位。
其中,程序释放程序跟加壳程序压缩加密和内存布局是相反的。
内存加载执行代码如下:
int LoadPE::RunPE(char* pFileBuff, DWORD dwSize)
{int ret = 0;char szout[1024];DWORD dwSizeOfImage = GetSizeOfImage(pFileBuff);DWORD imagebase = GetImageBase(pFileBuff);if (imagebase <= 0){imagebase = DEFAULT_PE_BASE_ADDRESS;}#ifdef _MYDEBUGwsprintfA(szout, "image base:%x,size:%x", imagebase, dwSizeOfImage);MessageBoxA(0, szout, szout, MB_OK);
#endif//使用MEM_RESERVE分配类型参数 Windows会以64 KB为边界计算该区域的起始地址 跟PE文件加载边界一致//使用MEM_COMMIT分配类型参数 区域的起始和结束地址都被计算到4KB边界//VirtualAlloc 当程序访问这部分内存时RAM内存才会被真正分配char* chBaseAddress = (char*)lpVirtualAlloc(imagebase, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);if (NULL == chBaseAddress){
#ifdef _MYDEBUGwsprintfA(szout, "VirtualAlloc address:%x error", imagebase);MessageBoxA(0, szout, szout, MB_OK);
#endifchBaseAddress = (char*)lpVirtualAlloc(0, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);if (NULL == chBaseAddress){
#ifdef _MYDEBUGwsprintfA(szout, "VirtualAlloc address:%x error", imagebase);MessageBoxA(0, szout, szout, MB_OK);
#endifreturn NULL;}}RtlZeroMemory(chBaseAddress, dwSizeOfImage);ret = MapFile(pFileBuff, chBaseAddress);//Reloc::recovery((DWORD)chBaseAddress);ret = RelocationTable(chBaseAddress);//ImportFunTable::recover((DWORD)chBaseAddress);ret = ImportTable(chBaseAddress);DWORD dwOldProtect = 0;if (FALSE == lpVirtualProtect(chBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)){lpVirtualFree(chBaseAddress, dwSizeOfImage, MEM_DECOMMIT);lpVirtualFree(chBaseAddress, 0, MEM_RELEASE);
#ifdef _MYDEBUGwsprintfA(szout, "VirtualProtect address:%x error", imagebase);MessageBoxA(0, szout, szout, MB_OK);
#endifreturn NULL;}ret = SetImageBase(chBaseAddress);PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)chBaseAddress;PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(chBaseAddress + dos->e_lfanew);#ifdef _MYDEBUGwsprintfA(szout, "pe type:%x", nt->FileHeader.Characteristics);MessageBoxA(0, szout, szout, MB_OK);
#endifif (nt->OptionalHeader.Subsystem == IMAGE_SUBSYSTEM_WINDOWS_GUI){}else if (nt->OptionalHeader.Subsystem == IMAGE_SUBSYSTEM_WINDOWS_CUI){gType = 3;ghPEModule = (HMODULE)chBaseAddress;gPEImageSize = dwSizeOfImage;ret = CallConsoleEntry(chBaseAddress);lpVirtualFree(chBaseAddress, dwSizeOfImage, MEM_DECOMMIT);lpVirtualFree(chBaseAddress, 0, MEM_RELEASE);return ret;}if (nt->FileHeader.Characteristics & 0x2000){gType = 2;gPEImageSize = dwSizeOfImage;ghPEModule = (HMODULE)chBaseAddress;ret = recoverEAT(chBaseAddress);ret = CallDllEntry(chBaseAddress);return ret;}else {gType = 1;ghPEModule = (HMODULE)chBaseAddress;gPEImageSize = dwSizeOfImage;ret = CallExeEntry(chBaseAddress);lpVirtualFree(chBaseAddress, dwSizeOfImage, MEM_DECOMMIT);lpVirtualFree(chBaseAddress, 0, MEM_RELEASE);return ret;}return TRUE;
}
pe文件中的OptionalHeader.ImageBase字段可以修改,故程序编译时指定的加载地址并不是必须的。
通过内存分配,MapFile函数映射区段,RelocationTable重定位,导入表ImportTable,VirtualProtect修改程序内存属性可读写可执行,SetImageBase设置OptionalHeader.ImageBase字段后,程序内存映射基本完毕。接下来,如果是exe程序,那么直接跳转地址:加载地址 + OptionalHeader.AddressOfEntryPoint,如果是dll程序,直接带参数DLL_PROCESS_ATTACH执行DllMainEntry函数,如果是console程序,则带参数执行main函数。
(二)测试
测试时,保证peshell.exe,peunshell.exe或者peunshell.dll,加壳运行的exe或者dll在同一个目录,通过命令运行peshell.exe即可。测试中,大部分体积比较小的32位程序都可以加载执行,但是稍微大一些的程序无法正确运行。原因百思不得其解,望各位赐教。