第一次在此版發帖,更多的是涉及內存加載執行。

該方案比較簡單,只適用于測試和簡單情景下的應用,測試中能夠運行大多數體積和功能比較簡單的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", 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", 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", 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.exe
    PeShell -bd sogou.exe sbiedll.dll -c params.dat -o out.dll
    PeShell -e sogou.exe -c params.dat -o out.exe
    PeShell -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 _MYDEBUG
    wsprintfA(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 _MYDEBUG
        wsprintfA(szout, "VirtualAlloc address:%x error", imagebase);
        MessageBoxA(0, szout, szout, MB_OK);
#endif
        chBaseAddress = (char*)lpVirtualAlloc(0, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
        if (NULL == chBaseAddress)
        {
#ifdef _MYDEBUG
            wsprintfA(szout, "VirtualAlloc address:%x error", imagebase);
            MessageBoxA(0, szout, szout, MB_OK);
#endif
            return 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 _MYDEBUG
        wsprintfA(szout, "VirtualProtect address:%x error", imagebase);
        MessageBoxA(0, szout, szout, MB_OK);
#endif
        return NULL;
    }
    ret = SetImageBase(chBaseAddress);
    PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)(chBaseAddress + dos->e_lfanew);
#ifdef _MYDEBUG
    wsprintfA(szout, "pe type:%x", nt->FileHeader.Characteristics);
    MessageBoxA(0, szout, szout, MB_OK);
#endif
    if (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位程序都可以加載執行,但是稍微大一些的程序無法正確運行。原因百思不得其解,望各位賜教。