SimpleDpack_C++编写shellcode压缩壳

Posted 看雪学院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SimpleDpack_C++编写shellcode压缩壳相关的知识,希望对你有一定的参考价值。

本文为看雪论坛精华文章

看雪论坛作者ID:devseed



 

5年前,初入逆向,看着PE结构尤其是IAT一头雾水,对于脱壳原理理解不深刻,于是就用c++自己写了个简单的加壳工具( SimpleDpack )。 最近 重构了一下代码,规范了命名和拆分了几个函数,使得结构清晰,稍微拓展一下支持64位。

虽然这个toy example程序本身意义不大,但是通过这个程序可以来熟悉PE结构和加壳原理,深刻理解各种指针和内存分布等操作,对于初学者非常有帮助。于是我打算以此例来讲解Windows PE结构,谈谈加壳原理、编写shellcode等方法,解决方案和一些技巧等。


SimpleDpack_C++编写shellcode压缩壳

一、分析PE64结构

SimpleDpack_C++编写shellcode压缩壳


来讲述PE结构的教程虽然已经有很多了,但好多都是偏向于理论,很多东西不去文件中自己看看很不容易理解。这里将结合PE实例来分析其结构与作用。由于32位程序PE结构分析很多了,此处以64位程序为例分析 。其实pe64也就ImageBase 、VA如IAT和OFT等、堆栈大小等是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]

SimpleDpack_C++编写shellcode压缩壳


2、DataDirectory


在OptionalHeader的最后,有DataDirectory[16],定义了PE文件各 Directory的RVA和size,如下:
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举个例子,可以手动的计算段内偏移。
 

SimpleDpack_C++编写shellcode压缩壳


IMAGE_EXPORT_DIRECTORY结构如下图:
SimpleDpack_C++编写shellcode压缩壳
 
之后可以根据上表段内偏移来看查看函数RVA表,函数名称RVA表,如下:
 
SimpleDpack_C++编写shellcode压缩壳


(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表示了, 如下图所示:

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

SimpleDpack_C++编写shellcode压缩壳


(3)IMAGE_DIRECTORY_ENTRY_IAT


即为我们所说的IAT表,多在.rdata。
 
IAT表存储了各DLL函数的运行时地址VA(IAT在data directory中的声明并不是必要的,主要在运行时调用)。

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

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

编译器会把动态库函数用call [imagebase + iat + offset]这种内存间接寻址,即 call -> IAT(FT) -> func_addr。

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

SimpleDpack_C++编写shellcode压缩壳

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


(4)IMAGE_DIRECTORY_ENTRY_BASERELOC


重定向表,多在.reloc。
 
记载了需要重定向的地址,在DLL中或是开启ASLR后,基址改变,通过此表来修改地址以匹配新的基址。
  • reloc内包含多个BASE_RELOCATION块。

  • 每个BASE_RELOCATION块内头描述了此块reloc的VirtualAddress(RVA)和SizeOfBlock。

  • 之后块内若干个两字节的TypeOffset,低12位为offset,高4位是type,64位也是两字节。

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,程序运行前需要重定向到对应基址。  

SimpleDpack_C++编写shellcode压缩壳


3、Section header


section header为PE头的最后一部分,里面储存的各个区段的File Offset,RVA,Size,Characteristics等。RVA和File 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;}



SimpleDpack_C++编写shellcode压缩壳

二、壳的数据结构设计

SimpleDpack_C++编写shellcode压缩壳

上一节说了好多,其实并不难,就是PE结构有一些地方比较绕,因此来分析了实际PE文件的几个部分。熟悉了PE结构,接下来开始谈谈加壳相关的了。
 
加壳主要有两部分:负责压缩修改等写入exe的加壳程序、嵌入exe的负责解压还原等操作的壳程序本身。

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

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

对于压缩壳,我们壳内的索引需要有
  • 原来区段位置大小

  • 压缩的缓存区位置大小、压缩类型

  • 源程序的OEP、IAT


落实到代码上,在 dPackType.h 中。
#include <Windows.h>#ifndef _DPACKPROC_H#define _DPACKPROC_H#define MAX_DPACKSECTNUM 16 // 最多可pack区段数量#include "lzmalzmalib.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;}



SimpleDpack_C++编写shellcode压缩壳

三、壳的shellcode编写

SimpleDpack_C++编写shellcode压缩壳

shellcode一般都是用汇编去编写,但是我们要同时去做32位和64位程序,就要写两份汇编了。因此我们采取用c来编写shellcode,必要的地方加入汇编即可。同时,为了方便将shellcode附加到源程序上,我们采取将shellcode编译为DLL,这样就可以通过reloc方便的调整基址了。
 
在我们这个简单的压缩壳中,主要的是四部分:
  • 分配解压后的内存(如果把区段头信息也删除了,需要自己分配)

  • 解压缩各区段数据(暂不考虑TLS,rsrc的压缩)

  • 初始化原始的IAT

  • 跳转到原OEP


为了方便扩展,比如说加密,添加stolen oep等,前后分别加上BeforeUnpack(),AfterUnpack()空函数。此部分的完整代码在 simpledpackshell.cpp shellcode64.asm
#ifdef _WIN64void 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、分配解压内存


直接用VirtualQueryEx和VirtualAllocEx即可。
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_RAW和DPACK_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,我们需要LoadLibrary和GetProcAddress手动得到函数的地址,再写入源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


SimpleDpack_C++编写shellcode压缩壳

四、加壳程序的编写

SimpleDpack_C++编写shellcode压缩壳


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

a. 加载要加壳的exe文件,获取PE文件头的相关信息,对区段进行压缩,放入临时缓冲区。

b. 加载shellcode的DLL,将索引信息写入DPACK_SHELL_INDEX,对shellcode的地址重定向(exe的imagebase + shellcode附加在exe后面的偏移)。

c. 对IAT的位置加上shellcode附加在exe后面的偏移。

d. 将shellcode代码附加到exe代码后面,修改OEP、IAT等索引信。

e. 修正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
", *(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);}


SimpleDpack_C++编写shellcode压缩壳

五、x64适配

SimpleDpack_C++编写shellcode压缩壳


由于64位的相关教程比较少,这里来说说如何同时支持64位和32位。
 
其实64位和32位结构很相似,也就是涉及到VA或size是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
", *(PDWORD)(pPeBuf + toffset));#endif

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

把汇编单独放在 .asm 文件里, extern g_value:QWORD func 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; .codeJmpOrgOep PROC push g_orgOep; ret;JmpOrgOep ENDPend

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



SimpleDpack_C++编写shellcode压缩壳

- End -



SimpleDpack_C++编写shellcode压缩壳


看雪ID:devseed

https://bbs.pediy.com/user-home-838741.htm

   *本文由看雪论坛 devseed 原创,转载请注明来自看雪社区。




# 往期推荐












SimpleDpack_C++编写shellcode压缩壳
公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



SimpleDpack_C++编写shellcode压缩壳

球分享

SimpleDpack_C++编写shellcode压缩壳

球点赞

球在看



点击“阅读原文”,了解更多!

以上是关于SimpleDpack_C++编写shellcode压缩壳的主要内容,如果未能解决你的问题,请参考以下文章

Window中的shellcode编写框架(入门篇)

Windows 下 ShellCode 编写初步

《黑客攻防-系统实战》--shellcode

《黑客攻防-系统实战》--shellcode

ShellCode加载器编写

MIPS下shellcode编写