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也就ImageBase
、VA
如IAT
和OFT
等、堆栈大小等是ULONGLONG,其他和pe32基本保持一致。
(1) PE文件头总览 Windows PE的数据结构定义在winnt.h
头文件里,大体可以归纳下列几点:
NT header
包括file header
和optional header
,
optional header
,末尾含有16个元素的data directory
数组;
IMAGE_OPTIONAL_HEADER64
,里面ImageBase
、还有堆栈尺寸类型是ULONGLONG
紧随着NT header
的是各section的headers,数量为fi le header
里面的NumberOfSections
。
1 2 3 4 5 6 7 |DOS header |NT header |file header |optional header |... |data directory[16 ] |section headers[n]
具体细节可以看此图(来源于网络)
(2) DataDirectory 在OptionalHeader
的最后,有DataDirectory[16]
,定义了PE文件各 Directory的RVA
和size
,如下:
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 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 #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 #define IMAGE_DIRECTORY_ENTRY_TLS 9 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 #define IMAGE_DIRECTORY_ENTRY_IAT 12 #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14
.1 IMAGE_DIRECTORY_ENTRY_EXPORT DLL导出表头,一般在.rdata
、.edata
。
里面有三个表的指针(RVA),都是数组形式存储(每个表里的项地址上是连续的),
AddressOfFunctions
指向函数RVA表
AddressOfNames
指向函数名RVA表
(存储字符串指针)、
AddressOfNameOrdinals
指向序号表
。
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } 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
结构如下图,
之后可以根据上表段内偏移来看查看函数RVA表
,函数名称RVA表
,如下:
.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
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 typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; DWORD OriginalFirstThunk; } DUMMYUNIONNAME; DWORD TimeDateStamp; DWORD ForwarderChain; DWORD Name; DWORD FirstThunk; } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; DWORD Function; DWORD Ordinal; DWORD AddressOfData; } u1; } IMAGE_THUNK_DATA32; typedef struct _IMAGE_THUNK_DATA64 { union { ULONGLONG ForwarderString; ULONGLONG Function; ULONGLONG Ordinal; ULONGLONG AddressOfData; } u1; } IMAGE_THUNK_DATA64; typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; CHAR Name[1 ]; } 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
加载前OFT
表和FT(IAT)
表内容相同,都是指向IMAGE_IMPORT_BY_NAME
结构,里面有序号和函数名。b1148h
的file offset为b1148h-91000h + 8fe00h = aff48h
,如下图所示。
.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
一样 。
我们在IDA中找到一处调用IAT第一项的call,即下图call cs:PatBlt
。由于是64位汇编,call和jmp只能是对于于此RIP的+-2g地址空间跳转。即此处call (44 FF 15)后四字节(8e bb 06 00)为下一条指令地址和间接寻址内存的相对地址,即6bb8eh+260cah = 91c58h
,正好是IAT的第一项地址。
.4 IMAGE_DIRECTORY_ENTRY_BASERELOC 重定向表,多在.reloc
。
记载了需要重定向的地址,在DLL中或是开启ASLR后,基址改变,通过此表来修改地址以匹配新的基址。
reloc内包含多个BASE_RELOCATION
块
每个BASE_RELOCATION
块内头描述了此块reloc
的VirtualAddress
(RVA)和SizeOfBlock
之后块内若干个两字节的TypeOffset
,低12位为offset
,高4位是type
,64位也是两字节。
RVA+offset
即为需要重定向基地址的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; } IMAGE_BASE_RELOCATION; typedef struct TypeOffset // after one base_relation , it has multi typeoffset { WORD offset : 12 ; WORD type : 4 ; }TypeOffset,*PTypeOffset
如下图,RVA=b20a0h
中存储的地址需要重定向,因为F0 CE 02 80 01 00 00 00 00
是以18000000000h
为基址的VA,程序运行前需要重定向到对应基址。
section header
为PE头的最后一部分,里面储存的各个区段的File Offset
,RVA
,Size
,Characteristics
等。RVA
和File Offset
地址转换要来查此表。关于区段头注意:
这里SizeOfRawData
(文件中的大小)可以为零(比如说动态生成的数据,区段之留个头声明,文件里不需要对应的数据),
SizeOfRawData
必须是FileAlignment
的整数倍,VirtualSize
为实际内存空间(不包括MemoryAlign
后的)
各区段之间在内存上不能有空隙(比如说我中间删除一个区段,修改了文件指针与内存指针,但是内存上两个区段地址没有接上,就没法运行了)。
数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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_SWECTION_HEADER;
(4) 编程实现解析PE文件头 这部分主要就是根据结构,和偏移,来用指针指向对应的数据,详见CPEinfo 类。
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 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的负责解压还原等操作的壳程序本身。
对于压缩壳,我们壳内的索引需要有
原来区段位置大小
压缩的缓存区位置大小、压缩类型
源程序的OEP
、IAT
落实到代码上,在dPackType.h 中
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 #include <Windows.h> #ifndef _DPACKPROC_H #define _DPACKPROC_H #define MAX_DPACKSECTNUM 16 #include "lzma\lzmalib.h" typedef struct _DLZMA_HEADER { size_t RawDataSize; size_t DataSize; char LzmaProps[LZMA_PROPS_SIZE]; }DLZMA_HEADER, *PDLZMA_HEADER; typedef struct _DPACK_ORGPE_INDEX //源程序被隐去的信息,此结构为明文表示,地址全是rva { #ifdef _WIN64 ULONGLONG ImageBase; #else DWORD ImageBase; #endif DWORD OepRva; 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 //源信息与压缩变换后信息索引表是{ DWORD OrgRva; DWORD OrgSize; DWORD DpackRva; DWORD DpackSize; DWORD Characteristics; DWORD DpackSectionType; }DPACK_SECTION_ENTRY, * PDPACK_SECTION_ENTRY; typedef struct _DPACK_SHELL_INDEX //DPACK 变换头{ union { PVOID DpackOepFunc; DWORD DpackOepRva; }; DPACK_ORGPE_INDEX OrgIndex; WORD SectionNum; DPACK_SECTION_ENTRY SectionIndex[MAX_DPACKSECTNUM]; 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
的参数与解压大小等放到压缩数据头即可。其他方面如加密、反调试、花指令什么的暂不考虑,不过在这个我定义的框架下也很好添加。
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 #include <Windows.h> #include "dpackType.h" size_t dlzmaPack (LPBYTE pDstBuf,LPBYTE pSrcBuf,size_t srcSize) { size_t dstSize = -1 ; 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; 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
方便的调整基址了。
在我们这个简单的压缩壳中,主要的是四部分:
分配解压后的内存(如果把区段头信息也删除了,需要自己分配)
解压缩各区段数据(暂不考虑TLS
,rsrc
的压缩)
初始化原始的IAT
跳转到原OEP
为了方便扩展,比如说加密,添加stolen oep
等,前后分别加上BeforeUnpack()
,AfterUnpack()
空函数。此部分的完整代码在simpledpackshell.cpp 、shellcode64.asm 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #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) 分配解压内存 直接用VirtualQueryEx
和VirtualAllocEx
即可
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 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
宏来作为压缩标志。
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 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
中。
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 57 58 59 60 61 62 63 64 65 void LoadOrigionIat (PVOID arg) { DWORD i,j; DWORD dll_num = g_dpackShellIndex.OrgIndex.ImportSize /sizeof (IMAGE_IMPORT_DESCRIPTOR); DWORD item_num=0 ; DWORD oldProtect; HMODULE tHomule; 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); PIMAGE_THUNK_DATA pfThunk; PIMAGE_THUNK_DATA poThunk; 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 { #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的地址。
1 2 3 4 5 6 7 8 9 10 #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代码后面,修改OEP
、IAT
等索引信息
修正exe的pe头,将压缩区段的RawSize改为0,并保存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 DWORD CSimpleDpack::packPe (const char * dllpath, int dpackSectionType) { if (m_packpe.getPeBuf () == NULL ) return 0 ; initDpackTmpbuf (); DWORD packsize = packSection (dpackSectionType); DWORD shellsize = loadShellDll (dllpath); DWORD packpeImgSize = m_packpe.getOptionalHeader ()->SizeOfImage; DWORD shellStartRva = m_shellpe.getSectionHeader ()[0 ].VirtualAddress; DWORD shellEndtRva = m_shellpe.getSectionHeader ()[3 ].VirtualAddress; adjustShellReloc (packpeImgSize); adjustShellIat (packpeImgSize); initShellIndex (shellEndtRva); makeAppendBuf (shellStartRva, shellEndtRva, packpeImgSize); adjustPackpeHeaders (0 ); return packsize + shellEndtRva - shellStartRva; }
下面挑重点说一些操作,加壳程序完整代码在CSimpleDpack ,对PE进行修改的代码见CPEedit 。
(1) shellcode的处理 由于我们的shellcode在DLL中,因此可以直接LoadLibrary
载入,GetProcAddress
可以获取g_dpackShellIndex
这个我们导出的壳的索引结构。对shellcode
进行重定向和IAT
的处理如下:
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 DWORD CPEedit::shiftReloc (LPBYTE pPeBuf, size_t oldImageBase, size_t newImageBase, DWORD offset, bool bMemAlign) { 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); #ifdef _WIN64 *(PULONGLONG)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; #else *(PDWORD)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; #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); DWORD func_num = 0 ; auto pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR) (pPeBuf + (bMemAlign ? pImportEntry->VirtualAddress : rva2faddr (pPeBuf, pImportEntry->VirtualAddress))); 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++; 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是硬编码的,不能让基址变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void CSimpleDpack::adjustPackpeHeaders (DWORD offset) { if (m_pShellIndex == NULL ) return ; auto packpeImageSize = m_packpe.getOptionalHeader ()->SizeOfImage; 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 }; m_packpe.getFileHeader ()->Characteristics |= IMAGE_FILE_RELOCS_STRIPPED; }
(3) 保存PE文件 最后就是根据索引合并各个缓存区了,这里我们把shellcode和压缩数据都放到了最后一个区段,之后把PE缓存区根据FileAlignment
保存即可。
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 DWORD CSimpleDpack::savePe (const char * path) { 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; 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; } 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位结构很相似,也就是涉及到VA
或size
是ULONGLONG类型,大部分名称微软已经帮我们用宏重定向了64还是32位结构;还有一个麻烦事,在visual studio里面64位程序是没法开启内联汇编的。
关于64位数据类型不一样的地方,我们可以用宏_WIN64
来区分是否64此程序,这样我们编译64位加壳程序后就能解析64位程序加壳了。比如说:
1 2 3 4 5 6 7 #ifdef _WIN64 *(PULONGLONG)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; #else *(PDWORD)(pPeBuf + toffset) += newImageBase - oldImageBase + offset; #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"
来声明调用外部函数
1 2 3 4 5 6 7 8 9 extern g_orgOep:QWORD; AfterUnpack proto c; .code JmpOrgOep PROC push g_orgOep; ret; JmpOrgOep ENDP end
至此,我们的程序可以同时支持64位和32位了。