SimpleDpack_PE结构与壳的原理讲解


  • SimpleDpack_C++编写32位与64位shellcode压缩壳_PE结构与壳的原理讲解

    by devseed,此篇教程同时发在论坛和我的博客上,完整源码见我的github

    0. 前言

    5年前,初入逆向,看着PE结构尤其是IAT一头雾水,对于脱壳原理理解不深刻,于是就用c++自己写了个简单的加壳工具(SimpleDpack)。最近回顾发现以前代码写的挺乱的,于是重构了一下代码,规范了命名和拆分了几个函数,使得结构清晰,稍微拓展一下支持64位。虽然这个toy example程序本身意义不大,但是通过这个程序可以来熟悉PE结构和加壳原理,深刻理解各种指针和内存分布等操作,对于初学者非常有帮助。于是我打算以此例来讲解Windows PE结构,谈谈加壳原理、编写shellcode等方法,解决方案和一些技巧等。

    1. 分析PE64结构

    来讲述PE结构的教程虽然已经有很多了,但好多都是偏向于理论,很多东西不去文件中自己看看很不容易理解。这里将结合PE实例来分析其结构与作用。由于32位程序PE结构分析很多了,此处以64位程序为例分析 。其实pe64也就ImageBaseVAIATOFT等、堆栈大小等是ULONGLONG,其他和pe32基本保持一致。

    (1) PE文件头总览

    Windows PE的数据结构定义在winnt.h头文件里,大体可以归纳下列几点:

    • NT header包括file headeroptional header
    • optional header,末尾含有16个元素的data directory数组;
    • IMAGE_OPTIONAL_HEADER64,里面ImageBase、还有堆栈尺寸类型是ULONGLONG
    • 紧随着NT header的是各section的headers,数量为fi le header里面的NumberOfSections
    |DOS header // e_lfanew
    |NT header
      |file header // NumberOfSections, SizeOfOptionalHeader(x86=0xe0, x64=0xf0)
      |optional header
        |... //AddressOfEntryPoint(oep), ImageBase, SizeOfImage, SizeOfHeaders
        |data directory[16] //IMAGE_DIRECTORY_ENTRY_EXPORT, ..._IMPORT, ..._IAT
    |section headers[n]
    

    具体细节可以看此图(来源于网络)

    pe_structure

    (2) DataDirectory

    OptionalHeader的最后,有DataDirectory[16],定义了PE文件各 Directory的RVAsize,如下:

    typedef struct _IMAGE_OPTIONAL_HEADER { 
        ...
    	IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; 
    } IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
    
    typedef struct _IMAGE_DATA_DIRECTORY {  
        DWORD   VirtualAddress;  
        DWORD   Size;  
    } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
    
    #define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory, .edata
    #define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory, .idata
    #define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory , .rsrc
    #define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory , .pdata
    #define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory 
    #define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table, .reloc 
    #define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory 
    //      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage) 
    #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data , 0
    #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of Global Ptr
    #define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory , 线程局部存储
    #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory 
    #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11  // Bound Import Directory in headers 
    #define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table (.data)
    #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors 
    #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor
    

    .1 IMAGE_DIRECTORY_ENTRY_EXPORT

    DLL导出表头,一般在.rdata.edata

    里面有三个表的指针(RVA),都是数组形式存储(每个表里的项地址上是连续的),

    • AddressOfFunctions指向函数RVA表
    • AddressOfNames指向函数名RVA表(存储字符串指针)、
    • AddressOfNameOrdinals指向序号表
    typedef struct _IMAGE_EXPORT_DIRECTORY { //Export Directory Table
        DWORD   Characteristics;
        DWORD   TimeDateStamp;
        WORD    MajorVersion;
        WORD    MinorVersion;
        DWORD   Name; //  the name of the DLL, RVA
        DWORD   Base; // The starting ordinal number for exports in this image, usually 1
        DWORD   NumberOfFunctions;
        DWORD   NumberOfNames;
        DWORD   AddressOfFunctions;     // Export Address Table, RVA from base of image
        DWORD   AddressOfNames;         // Export Name Pointer Table, RVA
        DWORD   AddressOfNameOrdinals;  // Export Ordinal Table, RVA from base of image
    } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
    

    上面这个看着有点抽象,来用user32.dll举个例子,可以手动的计算段内偏移。

    user32.dll (.rdata RVA=91000h) RVA section offset
    Name (dll name) A4812h 13812h
    AddressOfFunctions A1D88h 10D88h
    AddressOfNames A3084h 12084h
    (first name addr) A4839h 13839h
    AddressOfNameOrdinals A4038h 13038h

    IMAGE_EXPORT_DIRECTORY结构如下图,

    user32_export

    之后可以根据上表段内偏移来看查看函数RVA表函数名称RVA表,如下:

    user32_export_ables

    .2 IMAGE_DIRECTORY_ENTRY_IMPORT

    DLL导入表,一般在.rdata.idata

    描述了若干个导入的DLL(IMAGE_IMPORT_DESCRIPTOR),每个DLL导入若干个函数(IMAGE_THUNK_DATA)

    • 若干个IMAGE_IMPORT_DESCRIPTOR项组成数组,描述导入的若干个DLL,以全0项结尾

    • IMAGE_IMPORT_DESCRIPTOR结构中有IMAGE_THUNK_DAT数组指针(RVA),同样以全0项结尾。结构内含有其导入DLL中的函数信息指针(RVA)。两个数组指针如下:

      OriginalFirstThunk(OFT)表:导入函数的函数序数、名称表,AddressOfData指针(RVA)指向IMAGE_IMPORT_BY_NAME结构

      FirstThunk(FT)表:运行前的内容和OriginalFirstThunk一样,运行时加载为各函数的VA,即IAT

    typedef struct _IMAGE_IMPORT_DESCRIPTOR { 
        union {
            DWORD   Characteristics;    // 0 for terminating null import descriptor
            DWORD   OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
        } DUMMYUNIONNAME;
        DWORD   TimeDateStamp;           
        DWORD   ForwarderChain; //index of the first forwarder reference, -1 if no
        DWORD   Name;             // RVA to the name of dll
        DWORD   FirstThunk;      // RVA to IAT (if bound this IAT has actual addresses)
    } IMAGE_IMPORT_DESCRIPTOR;
    typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
    
    typedef struct _IMAGE_THUNK_DATA32 {
        union {
            DWORD ForwarderString;      // PBYTE 
            DWORD Function;             // PDWORD, va of the function, in ft, oat
            DWORD Ordinal;
            DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME, in oft
        } u1;
    } IMAGE_THUNK_DATA32;
    
    typedef struct _IMAGE_THUNK_DATA64 {
        union {
            ULONGLONG ForwarderString;  // PBYTE 
            ULONGLONG Function;         // PDWORD, va of the function, in ft, oat
            ULONGLONG Ordinal;
            ULONGLONG AddressOfData;    // PIMAGE_IMPORT_BY_NAME, in oft
        } u1;
    } IMAGE_THUNK_DATA64;
    
    typedef struct _IMAGE_IMPORT_BY_NAME { //in oft
        WORD    Hint;
        CHAR   Name[1]; // char *Name
    } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
    

    IMAGE_IMPORT_DESCRIPTOR中的一项,上面用section offset表示的,这里就用file offset表示了, 如下图所示:

    GDI32.dll(RVA 91000h, File Offset 8fe00h) RVA File Offset
    &OriginalFirstThunk[0] a9490h a8290h
    &FirstThunk[0] 91c58h 90a58h

    user32_importdescripter

    加载前OFT表和FT(IAT)表内容相同,都是指向IMAGE_IMPORT_BY_NAME结构,里面有序号和函数名。b1148h的file offset为b1148h-91000h + 8fe00h = aff48h,如下图所示。

    user32_OFT_FT

    .3 IMAGE_DIRECTORY_ENTRY_IAT

    即为我们所说的IAT表,多在.rdata

    IAT表存储了各DLL函数的运行时地址VA(IAT在data directory中的声明并不是必要的,主要在运行时调用)。

    • 各个IMAGE_IMPORT_DESCRIPTORFTFirstThunk数组指针(RVA)始终指向IAT表内的元素,因此FT表就是IAT

    • 程序运行前,IAT表的值与OFT表的值一样(即上一节说的运行前FT表与OFT表内的值一样)

    • 编译器会把动态库函数用call [imagebase + iat + offset]这种内存间接寻址,即

      call -> IAT(FT) -> func_addr

    这个IMAGE_DIRECTORY_ENTRY_IATIMAGE_DIRECTORY_ENTRY_IMPORT的概念还挺绕的,为了形象说明,下面再以user32.dll为例分析,这次来分析x64的IATIAT的首项(64位每项占8字节)RVA91c58h,正好是FT指向的RVA,其值(b1148h)在程序加载前和OFT一样 。

    user32_IAT_rva

    user32_import_directory

    user32_IAT_addr

    我们在IDA中找到一处调用IAT第一项的call,即下图call cs:PatBlt。由于是64位汇编,call和jmp只能是对于于此RIP的+-2g地址空间跳转。即此处call (44 FF 15)后四字节(8e bb 06 00)为下一条指令地址和间接寻址内存的相对地址,即6bb8eh+260cah = 91c58h,正好是IAT的第一项地址。

    user32_IAT_call

    user32_IAT_addrida

    .4 IMAGE_DIRECTORY_ENTRY_BASERELOC

    重定向表,多在.reloc

    记载了需要重定向的地址,在DLL中或是开启ASLR后,基址改变,通过此表来修改地址以匹配新的基址。

    • reloc内包含多个BASE_RELOCATION
    • 每个BASE_RELOCATION块内头描述了此块relocVirtualAddress(RVA)和SizeOfBlock
    • 之后块内若干个两字节的TypeOffset,低12位为offset,高4位是type,64位也是两字节。
    • RVA+offset即为需要重定向基地址的位置
    typedef struct _IMAGE_BASE_RELOCATION { // it has multi base relocation block,
        DWORD   VirtualAddress; // rva of this base relocation area
        DWORD   SizeOfBlock; //The total number of bytes in the base relocation block, including the Page RVA and Block Size fields and the Type/Offset fields that follow.
    //  WORD    TypeOffset[1]; 
    } IMAGE_BASE_RELOCATION;  //Each base relocation block starts with this struct
    
    typedef struct TypeOffset // after one base_relation, it has multi typeoffset
    {
    	WORD offset : 12;			//偏移值
    	WORD type	: 4;			//重定位属性(方式), 高4位
    	// IMAGE_REL_BASED_ABSOLUTE 0  The base relocation is skipped,used to pad a block.
    	// IMAGE_REL_BASED_HIGHLOW 3 The base relocation applies all 32 bits of the difference to the 32-bit field at offset. va = offset + base_rva + imagebase
        // IMAGE_REL_BASED_DIR64 10 for 64bit
    }TypeOffset,*PTypeOffset
    

    如下图,RVA=b20a0h中存储的地址需要重定向,因为F0 CE 02 80 01 00 00 00 00是以18000000000h为基址的VA,程序运行前需要重定向到对应基址。

    user32_reloc

    (3) Section header

    section header为PE头的最后一部分,里面储存的各个区段的File OffsetRVASizeCharacteristics等。RVAFile Offset地址转换要来查此表。关于区段头注意:

    • 这里SizeOfRawData(文件中的大小)可以为零(比如说动态生成的数据,区段之留个头声明,文件里不需要对应的数据),
    • SizeOfRawData必须是FileAlignment的整数倍,VirtualSize为实际内存空间(不包括MemoryAlign后的)
    • 各区段之间在内存上不能有空隙(比如说我中间删除一个区段,修改了文件指针与内存指针,但是内存上两个区段地址没有接上,就没法运行了)。

    数据结构如下:

    typedef struct _IMAGE_SECTION_HEADER { //0x28 bytes, the last is all zero
      BYTE  Name[IMAGE_SIZEOF_SHORT_NAME]; // 8  bytes, null end
      union {
        DWORD PhysicalAddress;
        DWORD VirtualSize; 
      } Misc;
      DWORD VirtualAddress; //rva (, relative to the image base)
      DWORD SizeOfRawData; // The size of the initialized data on disk, in bytes
      DWORD PointerToRawData; // fileoffset of the section data
      DWORD PointerToRelocations;
      DWORD PointerToLinenumbers; // for debug line number
      WORD  NumberOfRelocations;
      WORD  NumberOfLinenumbers;
      DWORD Characteristics;//IMAGE_SCN_MEM_EXECUTE 0x20000000,IMAGE_SCN_MEM_READ 0x40000000, IMAGE_SCN_MEM_WRITE 0x80000000
    } IMAGE_SECTION_HEADER, *PIMAGE_SWECTION_HEADER;
    

    (4) 编程实现解析PE文件头

    这部分主要就是根据结构,和偏移,来用指针指向对应的数据,详见CPEinfo类。

    PIMAGE_NT_HEADERS CPEinfo::getNtHeader(LPBYTE pPeBuf)
    {
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pPeBuf;
    	return (PIMAGE_NT_HEADERS)(pPeBuf + pDosHeader->e_lfanew);
    }
    
    PIMAGE_FILE_HEADER CPEinfo::getFileHeader(LPBYTE pPeBuf)
    {
    	return &getNtHeader(pPeBuf)->FileHeader;
    }
    
    PIMAGE_OPTIONAL_HEADER CPEinfo::getOptionalHeader(LPBYTE pPeBuf)
    {
    	return &getNtHeader(pPeBuf)->OptionalHeader;
    }
    
    PIMAGE_DATA_DIRECTORY CPEinfo::getImageDataDirectory(LPBYTE pPeBuf) 
    {
    	PIMAGE_OPTIONAL_HEADER pOptionalHeader = getOptionalHeader(pPeBuf);
    	return pOptionalHeader->DataDirectory;
    }
    
    PIMAGE_SECTION_HEADER CPEinfo::getSectionHeader(LPBYTE pPeBuf)
    {
    	PIMAGE_NT_HEADERS pNtHeader = getNtHeader(pPeBuf);
    	return (PIMAGE_SECTION_HEADER)((LPBYTE)pNtHeader + sizeof(IMAGE_NT_HEADERS));
    }
    
    PIMAGE_IMPORT_DESCRIPTOR CPEinfo::getImportDescriptor(LPBYTE pPeBuf, bool bMemAlign = true)
    {
    	PIMAGE_DATA_DIRECTORY pImageDataDirectory = getImageDataDirectory(pPeBuf);
    	DWORD rva =  pImageDataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
    	DWORD offset = bMemAlign ? rva: rva2faddr(pPeBuf, rva);
    	return (PIMAGE_IMPORT_DESCRIPTOR)(pPeBuf + offset);
    }
    
    PIMAGE_EXPORT_DIRECTORY CPEinfo::getExportDirectory(LPBYTE pPeBuf, bool bMemAlign = true)
    {
    	PIMAGE_DATA_DIRECTORY pImageDataDirectory = getImageDataDirectory(pPeBuf);
    	DWORD rva = pImageDataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
    	DWORD offset = bMemAlign ? rva : rva2faddr(pPeBuf, rva);
    	return (PIMAGE_EXPORT_DIRECTORY)(pPeBuf + offset);
    }
    
    DWORD CPEinfo::getOepRva(LPBYTE pPeBuf)
    {
    	if (pPeBuf == NULL) return 0;
    	if (isPe(pPeBuf) <= 0) return 0;
    	return getOptionalHeader(pPeBuf)->AddressOfEntryPoint;
    }
    
    WORD CPEinfo::getSectionNum(LPBYTE pPeBuf)
    {
    	return getFileHeader(pPeBuf)->NumberOfSections;
    }
    

    2. 壳的数据结构设计

    上一节说了好多,其实并不难,就是PE结构有一些地方比较绕,因此来分析了实际PE文件的几个部分。熟悉了PE结构,接下来开始谈谈加壳相关的了。

    加壳主要有两部分:负责压缩修改等写入exe的加壳程序、嵌入exe的负责解压还原等操作的壳程序本身。

    • 加壳程序作用:将区段压缩等原来的数据结构写入exe,重建PE结构等;把原程序OEP等参数重定向在壳内,重定向壳shellcode的地址等。

    • 壳的作用:大体上来讲就是还原源程序各区段代码,同时模拟windows对程序的初始化,比如IAT表的载入等。

    对于压缩壳,我们壳内的索引需要有

    • 原来区段位置大小
    • 压缩的缓存区位置大小、压缩类型
    • 源程序的OEPIAT

    落实到代码上,在dPackType.h

    #include <Windows.h>
    #ifndef _DPACKPROC_H
    #define _DPACKPROC_H
    #define MAX_DPACKSECTNUM 16 // 最多可pack区段数量
    #include "lzma\lzmalib.h"
    
    typedef struct _DLZMA_HEADER
    {
    	size_t RawDataSize;//原始数据尺寸(不含此头)
    	size_t DataSize;//压缩后的数据大小
    	char LzmaProps[LZMA_PROPS_SIZE];//原始lzma的文件头
    }DLZMA_HEADER, *PDLZMA_HEADER;//此处外围添加适用于dpack的lzma头
    
    typedef struct _DPACK_ORGPE_INDEX   //源程序被隐去的信息,此结构为明文表示,地址全是rva
    {
    #ifdef _WIN64
    	ULONGLONG ImageBase;			//源程序基址
    #else
    	DWORD ImageBase;			//源程序基址
    #endif
    	DWORD OepRva;				//原程序rva入口
    	DWORD ImportRva;			//导入表信息
    	DWORD ImportSize;
    }DPACK_ORGPE_INDEX, * PDPACK_ORGPE_INDEX;
    
    #define DPACK_SECTION_RAW 0
    #define DPACK_SECTION_DLZMA 1
    
    typedef struct _DPACK_SECTION_ENTRY //源信息与压缩变换后信息索引表是
    {
    	//假设不超过4g
    	DWORD OrgRva; // OrgRva为0时则是不解压到原来区段
    	DWORD OrgSize; 
    	DWORD DpackRva;
    	DWORD DpackSize; 
    	DWORD Characteristics;
    	DWORD DpackSectionType; // dpack区段类型
    }DPACK_SECTION_ENTRY, * PDPACK_SECTION_ENTRY;
    
    typedef struct _DPACK_SHELL_INDEX//DPACK变换头
    {
    	union 
    	{
    		PVOID DpackOepFunc;  // 初始化壳的入口函数(放第一个元素方便初始化)
    		DWORD DpackOepRva;  // 加载shellcode后也许改成入口RVA
    	};
    	DPACK_ORGPE_INDEX OrgIndex;
    	WORD SectionNum;									//变换的区段数,最多MAX_DPACKSECTNUM区段
    	DPACK_SECTION_ENTRY SectionIndex[MAX_DPACKSECTNUM];		//变换区段索引, 以全0结尾
    	PVOID Extra;									//其他信息,方便之后拓展
    }DPACK_SHELL_INDEX, * PDPACK_SHELL_INDEX;
    
    size_t dlzmaPack(LPBYTE pDstBuf, LPBYTE pSrcBuf, size_t srcSize);
    size_t dlzmaUnpack(LPBYTE pDstBuf, LPBYTE pSrcBuf, size_t srcSize);
    #endif
    

    压缩我们采取开源算法LZMA,简单wrapper一下,将LZMA的参数与解压大小等放到压缩数据头即可。其他方面如加密、反调试、花指令什么的暂不考虑,不过在这个我定义的框架下也很好添加。

    #include <Windows.h>
    #include "dpackType.h"
    size_t dlzmaPack(LPBYTE pDstBuf,LPBYTE pSrcBuf,size_t srcSize)
    {
    	size_t dstSize = -1; //最大的buffersize, 为0会出错
    	size_t propSize = sizeof(DLZMA_HEADER);
    	PDLZMA_HEADER pDlzmah=(PDLZMA_HEADER)pDstBuf;
    	
    	LzmaCompress(pDstBuf+sizeof(DLZMA_HEADER), &dstSize,
    				 pSrcBuf, srcSize,
    				 pDlzmah->LzmaProps, (size_t *)&propSize,
    				 -1 ,0, -1, -1, -1, -1, -1);
    	
    	pDlzmah->RawDataSize = srcSize;
    	pDlzmah->DataSize = dstSize;
    	return dstSize;
    }
    
    size_t dlzmaUnpack(LPBYTE pDstBuf, LPBYTE pSrcBuf, size_t srcSize)
    {
    	PDLZMA_HEADER pdlzmah = (PDLZMA_HEADER)pSrcBuf;
    	size_t dstSize = pdlzmah->RawDataSize;//release版不赋初值会出错,由于debug将其赋值为cccccccc很大的数
    	LzmaUncompress(pDstBuf, &dstSize,//此处必须赋最大值
    		          pSrcBuf + sizeof(DLZMA_HEADER), &srcSize,
    		          pdlzmah->LzmaProps, LZMA_PROPS_SIZE);
    	return dstSize;
    }
    

    3. 壳的shellcode编写

    shellcode一般都是用汇编去编写,但是我们要同时去做32位和64位程序,就要写两份汇编了。因此我们采取用c来编写shellcode,必要的地方加入汇编即可。同时,为了方便将shellcode附加到源程序上,我们采取将shellcode编译为DLL,这样就可以通过reloc方便的调整基址了。

    在我们这个简单的压缩壳中,主要的是四部分:

    • 分配解压后的内存(如果把区段头信息也删除了,需要自己分配)
    • 解压缩各区段数据(暂不考虑TLSrsrc的压缩)
    • 初始化原始的IAT
    • 跳转到原OEP

    为了方便扩展,比如说加密,添加stolen oep等,前后分别加上BeforeUnpack()AfterUnpack()空函数。此部分的完整代码在simpledpackshell.cppshellcode64.asm

    #ifdef _WIN64 
    void dpackStart()
    #else
    __declspec(naked) void dpackStart()//此函数中不要有局部变量
    #endif
    {
    	BeforeUnpack();
    	MallocAll(NULL);
    	UnpackAll(NULL);
    	g_orgOep = g_dpackShellIndex.OrgIndex.ImageBase + g_dpackShellIndex.OrgIndex.OepRva;
    	LoadOrigionIat(NULL);
    	AfterUnpack();
    	JmpOrgOep();
    }
    

    (1) 分配解压内存

    直接用VirtualQueryExVirtualAllocEx即可

    void MallocAll(PVOID arg)
    {
    	MEMORY_BASIC_INFORMATION mi = { 0 };
    	HANDLE hProcess = GetCurrentProcess();
    	HMODULE imagebase = GetModuleHandle(NULL);
    	for (int i = 0; i < g_dpackShellIndex.SectionNum; i++)
    	{
    		if (g_dpackShellIndex.SectionIndex[i].OrgSize == 0) continue;
    		LPBYTE tVa = (LPBYTE)imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva;
    		DWORD tSize = g_dpackShellIndex.SectionIndex[i].OrgSize;
    		VirtualQueryEx(hProcess, tVa, &mi, tSize);
    		if(mi.State == MEM_FREE)
    		{
    			DWORD flProtect = PAGE_EXECUTE_READWRITE;
    			switch (g_dpackShellIndex.SectionIndex[i].Characteristics)
    			{
    			    case IMAGE_SCN_MEM_EXECUTE:
    					flProtect = PAGE_EXECUTE;
    					break;
    				case IMAGE_SCN_MEM_READ:
    					flProtect = PAGE_READONLY;
    					break;
    				case IMAGE_SCN_MEM_WRITE:
    					flProtect = PAGE_READWRITE;
    					break;
    			}
    			if(!VirtualAllocEx(hProcess, tVa, tSize, MEM_COMMIT, flProtect))
    			{
    				MessageBox(NULL,"Alloc memory failed", "error", NULL);
    				ExitProcess(1);
    			}
    		}
    	}   
    }
    

    (2) 解压区段

    VirtualProtect申请写权限,解压代码到缓冲区再memcpy到制定位置即可,之后再恢复原来的保护权限。注意这里new的缓冲区一定要够,否则运行的时候会出现heap损坏等exception。同时,我们引入DPACK_SECTION_RAWDPACK_SECTION_DLZMA宏来作为压缩标志。

    void UnpackAll(PVOID arg)
    {
    	DWORD oldProtect;
    #ifdef _WIN64
    	ULONGLONG imagebase = g_dpackShellIndex.OrgIndex.ImageBase;
    #else
    	DWORD imagebase = g_dpackShellIndex.OrgIndex.ImageBase;
    #endif
    	for(int i=0; i<g_dpackShellIndex.SectionNum; i++)
    	{
    		switch(g_dpackShellIndex.SectionIndex[i].DpackSectionType)
    		{
    		case  DPACK_SECTION_RAW:
    		{
    			if (g_dpackShellIndex.SectionIndex[i].OrgSize == 0) continue;
    			VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva),
    				g_dpackShellIndex.SectionIndex[i].OrgSize,
    				PAGE_EXECUTE_READWRITE, &oldProtect);
    			memcpy((void*)(imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva),
    				(void*)(imagebase + g_dpackShellIndex.SectionIndex[i].DpackRva),
    				g_dpackShellIndex.SectionIndex[i].OrgSize);
    			VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva),
    				g_dpackShellIndex.SectionIndex[i].OrgSize,
    				oldProtect, &oldProtect);
    			break;
    		}
    		case DPACK_SECTION_DLZMA:
    		{
    			LPBYTE buf = new BYTE[g_dpackShellIndex.SectionIndex[i].OrgSize];
    			if (!dlzmaUnpack(buf, 
    				(LPBYTE)(g_dpackShellIndex.SectionIndex[i].DpackRva + imagebase),
    				g_dpackShellIndex.SectionIndex[i].DpackSize))
    			{
    				MessageBox(0, "unpack failed", "error", 0);
    				ExitProcess(1);
    			}
    			VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva),
    				g_dpackShellIndex.SectionIndex[i].OrgSize,
    				PAGE_EXECUTE_READWRITE, &oldProtect);
    			memcpy((void*)(imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva),
    				buf, g_dpackShellIndex.SectionIndex[i].OrgSize);
    			VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex[i].OrgRva),
    				g_dpackShellIndex.SectionIndex[i].OrgSize,
    				oldProtect, &oldProtect);
    			delete[] buf;
    			break;
    		}
    		default:
    			break;
    		}
    	}
    }
    

    (3) 初始化源程序的IAT

    DPACK_SHELL_INDEX这个结构记载了原程序IAT,我们需要LoadLibraryGetProcAddress手动得到函数的地址,再写入源IAT中。

    void LoadOrigionIat(PVOID arg)  // 因为将iat改为了壳的,所以要还原原来的iat
    {
    	DWORD i,j;
    	DWORD dll_num = g_dpackShellIndex.OrgIndex.ImportSize
    		/sizeof(IMAGE_IMPORT_DESCRIPTOR);//导入dll的个数,含最后全为空的一项
    	DWORD item_num=0;//一个dll中导入函数的个数,不包括全0的项
    	DWORD oldProtect;
    	HMODULE tHomule;//临时加载dll的句柄
    	LPBYTE tName;//临时存放名字
    #ifdef _WIN64
    	ULONGLONG tVa;//临时存放虚拟地址
    	ULONGLONG imagebase = g_dpackShellIndex.OrgIndex.ImageBase;
    #else
    	DWORD tVa;//临时存放虚拟地址
    	DWORD imagebase = g_dpackShellIndex.OrgIndex.ImageBase;
    #endif
    	PIMAGE_IMPORT_DESCRIPTOR pImport=(PIMAGE_IMPORT_DESCRIPTOR)(imagebase+
    		g_dpackShellIndex.OrgIndex.ImportRva);//指向第一个dll
    	PIMAGE_THUNK_DATA pfThunk;//ft
    	PIMAGE_THUNK_DATA poThunk;//oft
    	PIMAGE_IMPORT_BY_NAME pFuncName;
    	for(i=0;i<dll_num;i++)
    	{
    		if(pImport[i].OriginalFirstThunk==0) continue;
    		tName=(LPBYTE)(imagebase+pImport[i].Name);
    		tHomule=LoadLibrary((LPCSTR)tName);
    		pfThunk=(PIMAGE_THUNK_DATA)(imagebase+pImport[i].FirstThunk);
    		poThunk=(PIMAGE_THUNK_DATA)(imagebase+pImport[i].OriginalFirstThunk);
    		for(j=0;poThunk[j].u1.AddressOfData!=0;j++){}//注意个数。。。
    		item_num=j;
    
    		VirtualProtect((LPVOID)(pfThunk),item_num * sizeof(IMAGE_THUNK_DATA),
    						PAGE_EXECUTE_READWRITE,&oldProtect);//注意指针位置
    		for(j=0;j<item_num;j++)
    		{
    			if((poThunk[j].u1.Ordinal >>31) != 0x1) //不是用序号
    			{
    				pFuncName=(PIMAGE_IMPORT_BY_NAME)(imagebase+poThunk[j].u1.AddressOfData);
    				tName=(LPBYTE)pFuncName->Name;
    #ifdef _WIN64
    				tVa = (ULONGLONG)GetProcAddress(tHomule, (LPCSTR)tName);
    #else
    				tVa = (DWORD)GetProcAddress(tHomule, (LPCSTR)tName);
    #endif
    			}
    			else
    			{
    				//如果此参数是一个序数值,它必须在一个字的低字节,高字节必须为0。
    #ifdef _WIN64			
    				tVa = (ULONGLONG)GetProcAddress(tHomule,(LPCSTR)(poThunk[j].u1.Ordinal & 0x0000ffff));
    #else
    				tVa = (DWORD)GetProcAddress(tHomule, (LPCSTR)(poThunk[j].u1.Ordinal & 0x0000ffff));
    #endif
    			}
    			if (tVa == NULL)
    			{
    				MessageBox(NULL, "IAT load error!", "error", NULL);
    				ExitProcess(1);
    			}
    			pfThunk[j].u1.Function = tVa;//注意间接寻址
    		}
    		VirtualProtect((LPVOID)(pfThunk),item_num * sizeof(IMAGE_THUNK_DATA),
    				oldProtect,&oldProtect);
    	}
    }
    

    (4) 跳转到源OEP

    这个最简单的方法就是用push和ret实现了,我们用g_orgOep来表示源OEP的地址。

    #ifndef _WIN64
    __declspec(naked) void JmpOrgOep()
    {
    	__asm
    	{
    		push g_orgOep;
    		ret;
    	}
    }
    #endif
    

    4. 加壳程序的编写

    加壳程序主要进行下面方面的处理:

    • 加载要加壳的exe文件,获取PE文件头的相关信息,对区段进行压缩,放入临时缓冲区
    • 加载shellcode的DLL,将索引信息写入DPACK_SHELL_INDEX,对shellcode的地址重定向(exe的imagebase + shellcode附加在exe后面的偏移)
    • IAT的位置加上shellcode附加在exe后面的偏移
    • 将shellcode代码附加到exe代码后面,修改OEPIAT等索引信息
    • 修正exe的pe头,将压缩区段的RawSize改为0,并保存。
    DWORD CSimpleDpack::packPe(const char* dllpath, int dpackSectionType)//加壳,失败返回0,成功返回pack数据大小
    {
    	if (m_packpe.getPeBuf() == NULL) return 0;
    	initDpackTmpbuf(); // 初始化pack buf
    	DWORD packsize = packSection(dpackSectionType); // pack各区段
    	DWORD shellsize = loadShellDll(dllpath); // 载入dll shellcode
    	
    	DWORD packpeImgSize = m_packpe.getOptionalHeader()->SizeOfImage;
    	DWORD shellStartRva = m_shellpe.getSectionHeader()[0].VirtualAddress;
    	DWORD shellEndtRva = m_shellpe.getSectionHeader()[3].VirtualAddress; // rsrc
    	
    	adjustShellReloc(packpeImgSize); // reloc调整后全局变量g_dpackShellIndex的oep也变成之后
    	adjustShellIat(packpeImgSize);
    	initShellIndex(shellEndtRva); // 初始化dpack shell index,一定要在reloc之后, 因为reloc后这里的地址也变了
    	makeAppendBuf(shellStartRva, shellEndtRva, packpeImgSize);
    	adjustPackpeHeaders(0);   // 调整要pack的pe头
    	return packsize + shellEndtRva - shellStartRva;
    }
    

    下面挑重点说一些操作,加壳程序完整代码在CSimpleDpack,对PE进行修改的代码见CPEedit

    (1) shellcode的处理

    由于我们的shellcode在DLL中,因此可以直接LoadLibrary载入,GetProcAddress可以获取g_dpackShellIndex这个我们导出的壳的索引结构。对shellcode进行重定向和IAT的处理如下:

    DWORD CPEedit::shiftReloc(LPBYTE pPeBuf, size_t oldImageBase, size_t newImageBase, DWORD offset, bool bMemAlign)
    {
    	//修复重定位,其实此处pShellBuf为hShell副本
    	DWORD all_num = 0;
    	DWORD sumsize = 0;
    	auto pRelocEntry = &getImageDataDirectory(pPeBuf)[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    	while (sumsize < pRelocEntry->Size)
    	{
    		auto pBaseRelocation = (PIMAGE_BASE_RELOCATION)(pPeBuf  + sumsize + 
    			(bMemAlign ? pRelocEntry->VirtualAddress :
    				rva2faddr(pPeBuf, pRelocEntry->VirtualAddress)));
    		auto pRelocOffset = (PRELOCOFFSET)
    			((LPBYTE)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
    		DWORD item_num = (pBaseRelocation->SizeOfBlock - 
    			sizeof(IMAGE_BASE_RELOCATION)) / sizeof(RELOCOFFSET);
    		for (int i = 0; i < item_num; i++)
    		{
    			if (pRelocOffset[i].offset == 0) continue;
    			DWORD toffset = pRelocOffset[i].offset + pBaseRelocation->VirtualAddress;
    			if (!bMemAlign) toffset = rva2faddr(pPeBuf, toffset);
    
    			// 新的重定位地址 = 重定位后的地址(VA)-加载时的镜像基址(hModule VA) + 新的镜像基址(VA) + 新代码基址RVA(前面用于存放压缩的代码)
    			// 由于讲dll附加在后面,需要在dll shell中的重定位加上偏移修正
    #ifdef _WIN64
    			*(PULONGLONG)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; //重定向每一项地址
    #else
    			//printf("%08lX -> ", *(PDWORD)(pPeBuf + toffset));
    			*(PDWORD)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; //重定向每一项地址
    			//printf("%08lX\n", *(PDWORD)(pPeBuf + toffset));
    #endif
    		}
    		pBaseRelocation->VirtualAddress += offset; //重定向页表基址
    		sumsize += sizeof(RELOCOFFSET) * item_num + sizeof(IMAGE_BASE_RELOCATION);
    		all_num += item_num;
    	}
    	return all_num;
    }
    
    DWORD CPEedit::shiftOft(LPBYTE pPeBuf, DWORD offset, bool bMemAlign, bool bResetFt)
    {
    	auto pImportEntry = &getImageDataDirectory(pPeBuf)[IMAGE_DIRECTORY_ENTRY_IMPORT];
    	DWORD dll_num = pImportEntry->Size / sizeof(IMAGE_IMPORT_DESCRIPTOR);//导入dll的个数,含最后全为空的一项
    	DWORD func_num = 0;//所有导入函数个数,不包括全0的项
    	auto pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) (pPeBuf +
    		(bMemAlign ? pImportEntry->VirtualAddress : 
    		rva2faddr(pPeBuf, pImportEntry->VirtualAddress)));//指向第一个dll
    	for (int i = 0; i < dll_num; i++)
    	{
    		if (pImportDescriptor[i].OriginalFirstThunk == 0) continue;
    		auto pOFT = (PIMAGE_THUNK_DATA)(pPeBuf + (bMemAlign ?
    			pImportDescriptor[i].OriginalFirstThunk: 
    			rva2faddr(pPeBuf, pImportDescriptor[i].OriginalFirstThunk)));
    		auto pFT = (PIMAGE_THUNK_DATA)(pPeBuf + (bMemAlign ?
    			pImportDescriptor[i].FirstThunk :
    			rva2faddr(pPeBuf, pImportDescriptor[i].FirstThunk)));
    		DWORD item_num = 0;
    		for (int j = 0; pOFT[j].u1.AddressOfData != 0; j++)
    		{
    			item_num++; //一个dll中导入函数的个数,不包括全0的项
    			if ((pOFT[j].u1.Ordinal >> 31) != 0x1) //不是用序号
    			{
    				pOFT[j].u1.AddressOfData += offset;
    				if (bResetFt) pFT[j].u1.AddressOfData = pOFT[j].u1.AddressOfData;
    			}
    		}
    		pImportDescriptor[i].OriginalFirstThunk += offset;
    		pImportDescriptor[i].FirstThunk += offset;
    		pImportDescriptr[i].Name += offset;
    		func_num += item_num;
    	}
    	return func_num;
    }
    

    (2) 调整exe的PE头

    我们需要把一些信息调到壳上,还有最后一定要关掉ASLR,因为壳内跳转到OEP是硬编码的,不能让基址变化。

    void CSimpleDpack::adjustPackpeHeaders(DWORD offset)
    {
    	// 设置被加壳程序的信息, oep, reloc, iat
    	if (m_pShellIndex == NULL) return;
    	auto packpeImageSize = m_packpe.getOptionalHeader()->SizeOfImage;
    	// m_pShellIndex->DpackOepFunc 之前已经reloc过了,变成了正确的va了(shelldll是release版)
    	m_packpe.setOepRva((size_t)m_pShellIndex->DpackOepFunc -
    		m_packpe.getOptionalHeader()->ImageBase + offset);
    	m_packpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_IMPORT] = {
    		m_shellpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + packpeImageSize + offset,
    		m_shellpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_IMPORT].Size };
    	m_packpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_IAT] = {
    		m_shellpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress + packpeImageSize + offset,
    		m_shellpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_IMPORT].Size};
    	m_packpe.getImageDataDirectory()[IMAGE_DIRECTORY_ENTRY_BASERELOC] = { 0,0 };
    
    	// pe 属性设置
    	m_packpe.getFileHeader()->Characteristics |= IMAGE_FILE_RELOCS_STRIPPED; //禁止基址随机化
    }
    

    (3) 保存PE文件

    最后就是根据索引合并各个缓存区了,这里我们把shellcode和压缩数据都放到了最后一个区段,之后把PE缓存区根据FileAlignment保存即可。

    DWORD CSimpleDpack::savePe(const char* path)//失败返回0,成功返回文件大小
    {
    	/*
    		pack区域放到后面,由于内存有对齐问题,只允许pack一整个区段
    		先改pe头,再分配空间,支持若原来pe fileHeader段不够,添加段
    		将区段头与区段分开考虑
    	*/
    	// dpack头初始化
    	IMAGE_SECTION_HEADER dpackSect = {0};
    	strcpy((char*)dpackSect.Name, ".dpack");
    	dpackSect.Characteristics = IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_EXECUTE;
    	dpackSect.VirtualAddress = m_dpackTmpbuf[m_dpackSectNum - 1].OrgRva;
    	
    	// 准备dpack buf
    	DWORD dpackBufSize = 0;
    	for (int i = 0; i < m_dpackSectNum; i++) dpackBufSize += m_dpackTmpbuf[i].DpackSize;
    	LPBYTE pdpackBuf = new BYTE[dpackBufSize];
    	LPBYTE pCurBuf = pdpackBuf;
    	memcpy(pdpackBuf, m_dpackTmpbuf[m_dpackSectNum - 1].PackedBuf, 
    		m_dpackTmpbuf[m_dpackSectNum - 1].DpackSize); // 壳代码
    	pCurBuf += m_dpackTmpbuf[m_dpackSectNum - 1].DpackSize;
    	for (int i = 0; i < m_dpackSectNum -1 ; i++)
    	{
    		memcpy(pCurBuf, m_dpackTmpbuf[i].PackedBuf,
    			m_dpackTmpbuf[i].DpackSize); 
    		pCurBuf += m_dpackTmpbuf[i].DpackSize;
    	}
    
    	// 删除被压缩区段和写入pe
    	int remvoeSectIdx[MAX_DPACKSECTNUM] = {0};
    	int removeSectNum = 0;
    	for (int i = 0; i < m_packpe.getFileHeader()->NumberOfSections; i++)
    	{
    		if (m_packSectMap[i] == true) remvoeSectIdx[removeSectNum++] = i;
    	}
    	m_packpe.removeSectionDatas(removeSectNum, remvoeSectIdx);
    	m_packpe.appendSection(dpackSect, pdpackBuf, dpackBufSize);
    	delete[] pdpackBuf;
    	return m_packpe.savePeFile(path);
    }
    

    5. x64适配

    由于64位的相关教程比较少,这里来说说如何同时支持64位和32位。

    其实64位和32位结构很相似,也就是涉及到VAsize是ULONGLONG类型,大部分名称微软已经帮我们用宏重定向了64还是32位结构;还有一个麻烦事,在visual studio里面64位程序是没法开启内联汇编的。

    关于64位数据类型不一样的地方,我们可以用宏_WIN64来区分是否64此程序,这样我们编译64位加壳程序后就能解析64位程序加壳了。比如说:

    #ifdef _WIN64
    			*(PULONGLONG)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; 
    #else
    			//printf("%08lX -> ", *(PDWORD)(pPeBuf + toffset));
    			*(PDWORD)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; 
    			//printf("%08lX\n", *(PDWORD)(pPeBuf + toffset));
    #endif
    

    关于64位visual studio无法内联汇编,我们要:

    • 把汇编单独放在.asm文件里,extern g_value:QWORDfunc proto c[:argtyp1, :argtype2 ...]声明调用c++程序全局变量或函数
    • 然后用命令行ml64 /Fo $(IntDir)%(fileName).obj /c ..\src\%(fileName).asm生成.obj
    • c++代码中extern "C" 来声明调用外部函数
    extern g_orgOep:QWORD;
    AfterUnpack proto c;
    
    .code
    JmpOrgOep PROC 
        push g_orgOep;
        ret;
    JmpOrgOep ENDP
    end
    

    至此,我们的程序可以同时支持64位和32位了。

yurisbbs by @devseed since 2020, powered by nodebb.