SimpleDpack_C++编写32位与64位shellcode压缩壳_PE结构与壳的原理讲解
本帖最后由 小木曾雪菜 于 2022-1-24 18:51 编辑# SimpleDpack_C++编写32位与64位shellcode压缩壳_PE结构与壳的原理讲解
by devseed,此篇教程同时发在论坛和[我的博客](https://blog.schnee.moe/posts/SimpleDpack/)上,完整源码见我的(https://github.com/YuriSizuku/SimpleDpack)
## 0. 前言
正好赶上元宵节写完了,祝大家元宵节快乐~
写教程不易,希望大家来给我的github点个star~
5年前,初入逆向,看着PE结构尤其是IAT一头雾水,对于脱壳原理理解不深刻,于是就用c++自己写了个简单的加壳工具((https://github.com/YuriSizuku/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`。
```c
|DOS header // e_lfanew
|NT header
|file header // NumberOfSections, SizeOfOptionalHeader(x86=0xe0, x64=0xf0)
|optional header
|... //AddressOfEntryPoint(oep), ImageBase, SizeOfImage, SizeOfHeaders
|data directory //IMAGE_DIRECTORY_ENTRY_EXPORT, ..._IMPORT, ..._IAT
|section headers
```
具体细节可以看此图(来源于网络)
## (2) DataDirectory
在`OptionalHeader`的最后,有`DataDirectory`,定义了PE文件各 Directory的`RVA`和`size`,如下:
```c
typedef struct _IMAGE_OPTIONAL_HEADER {
...
IMAGE_DATA_DIRECTORY DataDirectory;
} 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`指向`序号表`。
```c
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`结构如下图,
之后可以根据上表段内偏移来看查看`函数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`
```c
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; // 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 | a9490h | a8290h |
| &FirstThunk | 91c58h | 90a58h |
加载前`OFT`表和`FT(IAT)`表内容相同,都是指向`IMAGE_IMPORT_BY_NAME`结构,里面有序号和函数名。`b1148h`的file offset为`b1148h-91000h + 8fe00h = aff48h`,如下图所示。
#### .3IMAGE_DIRECTORY_ENTRY_IAT
即为我们所说的`IAT`表,多在`.rdata`。
`IAT`表存储了各DLL函数的运行时地址`VA`(IAT在data directory中的声明并不是必要的,主要在运行时调用)。
* 各个`IMAGE_IMPORT_DESCRIPTORFT`的`FirstThunk`数组指针(RVA)始终指向`IAT`表内的元素,因此`FT`表就是`IAT`表
* 程序运行前,`IAT`表的值与`OFT`表的值一样(即上一节说的运行前`FT`表与`OFT`表内的值一样)
* 编译器会把动态库函数用`call `这种内存间接寻址,即
`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`即为需要重定向基地址的位置
```C
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;
} 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 0The 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,程序运行前需要重定向到对应基址。
### (3) Section header
`section header`为PE头的最后一部分,里面储存的各个区段的`File Offset`,`RVA`,`Size`,`Characteristics`等。`RVA`和`File Offset`地址转换要来查此表。关于区段头注意:
* 这里`SizeOfRawData`(文件中的大小)可以为零(比如说动态生成的数据,区段之留个头声明,文件里不需要对应的数据),
* `SizeOfRawData`必须是`FileAlignment`的整数倍,`VirtualSize`为实际内存空间(不包括`MemoryAlign`后的)
* 各区段之间在内存上不能有空隙(比如说我中间删除一个区段,修改了文件指针与内存指针,但是内存上两个区段地址没有接上,就没法运行了)。
数据结构如下:
```c
typedef struct _IMAGE_SECTION_HEADER { //0x28 bytes, the last is all zero
BYTEName; // 8bytes, 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
WORDNumberOfRelocations;
WORDNumberOfLinenumbers;
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文件头
这部分主要就是根据结构,和偏移,来用指针指向对应的数据,详见(https://github.com/YuriSizuku/SimpleDpack/blob/master/src/PeInfo.cpp)类。
``` cpp
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.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.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`表的载入等。
对于压缩壳,我们壳内的索引需要有
* 原来区段位置大小
* 压缩的缓存区位置大小、压缩类型
* 源程序的`OEP`、`IAT`
落实到代码上,在(https://github.com/YuriSizuku/SimpleDpack/blob/master/src/dpackType.h)中
```c
#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的文件头
}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; //变换区段索引, 以全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`的参数与解压大小等放到压缩数据头即可。其他方面如加密、反调试、花指令什么的暂不考虑,不过在这个我定义的框架下也很好添加。
```c
#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`方便的调整基址了。
在我们这个简单的压缩壳中,主要的是四部分:
* 分配解压后的内存(如果把区段头信息也删除了,需要自己分配)
* 解压缩各区段数据(暂不考虑`TLS`,`rsrc`的压缩)
* 初始化原始的`IAT`
* 跳转到原OEP
为了方便扩展,比如说加密,添加`stolen oep`等,前后分别加上`BeforeUnpack()`,`AfterUnpack()`空函数。此部分的完整代码在(https://github.com/YuriSizuku/SimpleDpack/blob/master/src/simpledpackshell.cpp)、(https://github.com/YuriSizuku/SimpleDpack/blob/master/src/shellcode64.asm)。
```c
#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`即可
```c
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.OrgSize == 0) continue;
LPBYTE tVa = (LPBYTE)imagebase + g_dpackShellIndex.SectionIndex.OrgRva;
DWORD tSize = g_dpackShellIndex.SectionIndex.OrgSize;
VirtualQueryEx(hProcess, tVa, &mi, tSize);
if(mi.State == MEM_FREE)
{
DWORD flProtect = PAGE_EXECUTE_READWRITE;
switch (g_dpackShellIndex.SectionIndex.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`宏来作为压缩标志。
``` c
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.DpackSectionType)
{
caseDPACK_SECTION_RAW:
{
if (g_dpackShellIndex.SectionIndex.OrgSize == 0) continue;
VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex.OrgRva),
g_dpackShellIndex.SectionIndex.OrgSize,
PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((void*)(imagebase + g_dpackShellIndex.SectionIndex.OrgRva),
(void*)(imagebase + g_dpackShellIndex.SectionIndex.DpackRva),
g_dpackShellIndex.SectionIndex.OrgSize);
VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex.OrgRva),
g_dpackShellIndex.SectionIndex.OrgSize,
oldProtect, &oldProtect);
break;
}
case DPACK_SECTION_DLZMA:
{
LPBYTE buf = new BYTE.OrgSize];
if (!dlzmaUnpack(buf,
(LPBYTE)(g_dpackShellIndex.SectionIndex.DpackRva + imagebase),
g_dpackShellIndex.SectionIndex.DpackSize))
{
MessageBox(0, "unpack failed", "error", 0);
ExitProcess(1);
}
VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex.OrgRva),
g_dpackShellIndex.SectionIndex.OrgSize,
PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy((void*)(imagebase + g_dpackShellIndex.SectionIndex.OrgRva),
buf, g_dpackShellIndex.SectionIndex.OrgSize);
VirtualProtect((LPVOID)(imagebase + g_dpackShellIndex.SectionIndex.OrgRva),
g_dpackShellIndex.SectionIndex.OrgSize,
oldProtect, &oldProtect);
delete[] buf;
break;
}
default:
break;
}
}
}
```
>
### (3) 初始化源程序的IAT
`DPACK_SHELL_INDEX`这个结构记载了原程序`IAT`,我们需要`LoadLibrary`和`GetProcAddress`手动得到函数的地址,再写入源`IAT`中。
```c
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.OriginalFirstThunk==0) continue;
tName=(LPBYTE)(imagebase+pImport.Name);
tHomule=LoadLibrary((LPCSTR)tName);
pfThunk=(PIMAGE_THUNK_DATA)(imagebase+pImport.FirstThunk);
poThunk=(PIMAGE_THUNK_DATA)(imagebase+pImport.OriginalFirstThunk);
for(j=0;poThunk.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.u1.Ordinal >>31) != 0x1) //不是用序号
{
pFuncName=(PIMAGE_IMPORT_BY_NAME)(imagebase+poThunk.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.u1.Ordinal & 0x0000ffff));
#else
tVa = (DWORD)GetProcAddress(tHomule, (LPCSTR)(poThunk.u1.Ordinal & 0x0000ffff));
#endif
}
if (tVa == NULL)
{
MessageBox(NULL, "IAT load error!", "error", NULL);
ExitProcess(1);
}
pfThunk.u1.Function = tVa;//注意间接寻址
}
VirtualProtect((LPVOID)(pfThunk),item_num * sizeof(IMAGE_THUNK_DATA),
oldProtect,&oldProtect);
}
}
```
### (4) 跳转到源OEP
这个最简单的方法就是用push和ret实现了,我们用g_orgOep来表示源OEP的地址。
```c
#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,并保存。
```c
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().VirtualAddress;
DWORD shellEndtRva = m_shellpe.getSectionHeader().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;
}
```
下面挑重点说一些操作,加壳程序完整代码在(https://github.com/YuriSizuku/SimpleDpack/blob/master/src/SimpleDpack.cpp),对PE进行修改的代码见(https://github.com/YuriSizuku/SimpleDpack/blob/master/src/PeEdit.cpp)。
### (1) shellcode的处理
由于我们的shellcode在DLL中,因此可以直接`LoadLibrary`载入,`GetProcAddress`可以获取`g_dpackShellIndex`这个我们导出的壳的索引结构。对`shellcode`进行重定向和`IAT`的处理如下:
```c
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);
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.offset == 0) continue;
DWORD toffset = pRelocOffset.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);
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.OriginalFirstThunk == 0) continue;
auto pOFT = (PIMAGE_THUNK_DATA)(pPeBuf + (bMemAlign ?
pImportDescriptor.OriginalFirstThunk:
rva2faddr(pPeBuf, pImportDescriptor.OriginalFirstThunk)));
auto pFT = (PIMAGE_THUNK_DATA)(pPeBuf + (bMemAlign ?
pImportDescriptor.FirstThunk :
rva2faddr(pPeBuf, pImportDescriptor.FirstThunk)));
DWORD item_num = 0;
for (int j = 0; pOFT.u1.AddressOfData != 0; j++)
{
item_num++; //一个dll中导入函数的个数,不包括全0的项
if ((pOFT.u1.Ordinal >> 31) != 0x1) //不是用序号
{
pOFT.u1.AddressOfData += offset;
if (bResetFt) pFT.u1.AddressOfData = pOFT.u1.AddressOfData;
}
}
pImportDescriptor.OriginalFirstThunk += offset;
pImportDescriptor.FirstThunk += offset;
pImportDescriptr.Name += offset;
func_num += item_num;
}
return func_num;
}
```
### (2) 调整exe的PE头
我们需要把一些信息调到壳上,还有最后一定要关掉`ASLR`,因为壳内跳转到OEP是硬编码的,不能让基址变化。
```c
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() = {
m_shellpe.getImageDataDirectory().VirtualAddress + packpeImageSize + offset,
m_shellpe.getImageDataDirectory().Size };
m_packpe.getImageDataDirectory() = {
m_shellpe.getImageDataDirectory().VirtualAddress + packpeImageSize + offset,
m_shellpe.getImageDataDirectory().Size};
m_packpe.getImageDataDirectory() = { 0,0 };
// pe 属性设置
m_packpe.getFileHeader()->Characteristics |= IMAGE_FILE_RELOCS_STRIPPED; //禁止基址随机化
}
```
### (3) 保存PE文件
最后就是根据索引合并各个缓存区了,这里我们把shellcode和压缩数据都放到了最后一个区段,之后把PE缓存区根据`FileAlignment`保存即可。
``` cpp
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.OrgRva;
// 准备dpack buf
DWORD dpackBufSize = 0;
for (int i = 0; i < m_dpackSectNum; i++) dpackBufSize += m_dpackTmpbuf.DpackSize;
LPBYTE pdpackBuf = new BYTE;
LPBYTE pCurBuf = pdpackBuf;
memcpy(pdpackBuf, m_dpackTmpbuf.PackedBuf,
m_dpackTmpbuf.DpackSize); // 壳代码
pCurBuf += m_dpackTmpbuf.DpackSize;
for (int i = 0; i < m_dpackSectNum -1 ; i++)
{
memcpy(pCurBuf, m_dpackTmpbuf.PackedBuf,
m_dpackTmpbuf.DpackSize);
pCurBuf += m_dpackTmpbuf.DpackSize;
}
// 删除被压缩区段和写入pe
int remvoeSectIdx = {0};
int removeSectNum = 0;
for (int i = 0; i < m_packpe.getFileHeader()->NumberOfSections; i++)
{
if (m_packSectMap == true) remvoeSectIdx = 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位程序加壳了。比如说:
```c
#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:QWORD`、`func proto c[:argtyp1, :argtype2 ...]`声明调用c++程序全局变量或函数
* 然后用命令行`ml64 /Fo $(IntDir)%(fileName).obj /c..\src\%(fileName).asm`生成`.obj`,
* c++代码中`extern "C"` 来声明调用外部函数
```assembly
extern g_orgOep:QWORD;
AfterUnpack proto c;
.code
JmpOrgOep PROC
push g_orgOep;
ret;
JmpOrgOep ENDP
end
```
至此,我们的程序可以同时支持64位和32位了。
这个不知道为什么,markdown后面排版会变得混乱,就移除了一些代码块。
用完以后不能运行了
本帖最后由 小木曾雪菜 于 2021-3-22 16:32 编辑
Hmily 发表于 2021-3-22 14:51
我这测试好像没什么区别,能具体告诉我是哪里有问题不?
比如说 “(4) 编程实现解析PE文件头” 代码块无法显示出来,"(3) 保存PE文件"等也有问题。
之前的一些代码块显示有问题,后面排版就乱了, 在这个帖子里我就删掉了出问题的代码块,上传的md文档是原文件 后面有些代码好像溢出了代码块,是粘贴出问题了吗? qzhsjz 发表于 2021-2-26 18:04
后面有些代码好像溢出了代码块,是粘贴出问题了吗?
好像是论坛markdown插件出了点问题,我typora写的markdown复制过来后面排版就乱了 小木曾雪菜 发表于 2021-2-26 18:07
好像是论坛markdown插件出了点问题,我typora写的markdown复制过来后面排版就乱了
markdown不太兼容吧 谢谢分享。
技术贴收藏了。 技术贴收藏 看到系统提示关注人员已更新,点赞评分收藏三连后看了不到十秒钟一脸蒙逼的出去,仰望高端dalao{:301_998:} 小木曾雪菜 发表于 2021-2-26 18:07
好像是论坛markdown插件出了点问题,我typora写的markdown复制过来后面排版就乱了
能把原始的放网盘或者传上来吗,我来分析下原因。 Hmily 发表于 2021-3-18 07:40
能把原始的放网盘或者传上来吗,我来分析下原因。
好的h大,我上传了md文档。
链接: https://pan.baidu.com/s/1H8jAVA4lyRnu54WSsoN7Cg 提取码: d88j 小木曾雪菜 发表于 2021-3-20 11:26
好的h大,我上传了md文档。
链接: https://pan.baidu.com/s/1H8jAVA4lyRnu54WSsoN7Cg 提取码: d88j
我这测试好像没什么区别,能具体告诉我是哪里有问题不?