0%

x64以及x86加壳程序实现

1.加壳原理

​ 加壳主要分为两部分工作,第一部分是主题程序,主要将原PE文件读入内存,然后对该文件各部分进行加工,主要包括压缩各区段数据,将输入表、重定位变形,将外壳部分与处理好的主题文件拼合。另一部分是外壳,主要包括加壳后程序执行时的引导段,它模拟PE装载器处理输入表、重定位表,最后跳转到原程序执行。

image-20220301104747990

.pediy部分,以ShellStart为界,之前的部分以非压缩形式存在,之后的部分以压缩方式存在。新程序入口点指向ShellStart0开始的部分,外壳先执行这部分,这部分主要是在内存中将ShellStart开始处真正的代码解压缩,并初始化一些数据。初始化完成后继续转到ShellStart执行,该部分开始处的代码是真正的外壳部分,主要功能是还原程序(.text, .data等区块数据),一个重要功能则是阻止破解者的跟踪和脱壳。所以一般来说这段代码会比较长,里面会有各种调试器、反DUMP代码。

2.PE文件知识

image-20220221151706528

2.1 IMAGE_DOS_HEADER

PE文件第一个字节位于MS-DOS头部,称作IMAGE_DOS_HEADER,结构如下。

关键字段:e_magic值为5A4Dh, e_lfanew是真正的PE文件头的RVA,位于从文件开始偏移3Ch处。

image-20220221155116123

2.2 IMAGE_NT_HEADERS

紧跟着的是IMAGE_NT_HEADERS(PE头)。

image-20220221171131327

image-20220221171410919

1.signature

​ ASCII码字符为“PE00”

image-20220221172235941

2.IMAGE_FILE_HEADER

image-20220221194013255

SizeofOptionalHeader:对于32位PE文件,这个域通常是00E0h,64位PE通常为00F0h。

3.IMAGE_OPTIONAL_HEADER

下图为32位PE文件的相应结构体,64与32有一些区别,譬如PE32的BaseOfdata域不存在与PE32+,PE32的Magic域为010Bh,PE32+为020Bh,以及部分变量的类型不同。

DataDirectory字段是数据目录表,由数个相同的IMAGE_DATA_DIRECTORY组成,指向输入表、输出表、资源块数据。

image-20220222112716490

image-20220222112729300

2.3 区块

紧跟IMAGE_NT_HEADERS的是区块表,是一个IMAGE_SECTION_HEADER结构数组。每个这样的结构都包含了所关联的区块信息,该数组的数目由IMAGE_NT_HEADERS.FileHeader.NumberOfSection指出。

image-20220228145316376

常见的区块表如下

image-20220228145743086

=====================================================================================

实现部分

Note:由于该实现代码最终需要集成在某个项目工具上,所以这里并没有判断文件是否为PE文件以及是多少位的文件。

1. 加壳程序处理

​ 判断文件格式也比较简单:1.判断文件的第一个字是否为IMAGE_DOS_SIGNATURE,即5A4Dh;

​ 2.通过e_lfanew找到IMAGE_NT_HEADERS,判断Signature字段是否为00004550h,即IMAGE_NT_SIGNATURE,如果是就认为是PE文件。

​ 3.判断是否可以加壳,如果只有一个区块或者如果入口点的值大于第二个区块的虚拟地址,就认为已经被加壳。

​ 4.校验FileHeader结构中的Characteristics字段的值,判断是EXE还是DLL。

加壳的主要流程:

  • 加密代码段,增加区段
  • 加密/压缩被加壳程序,将stub代码移植到新区段
  • 将原始程序OEP记录下来并设置到新区段,修改OEP,在原始OEP之前执行解密代码
  • 去掉随机基址,保存为新文件

​ 外壳部分的代码都封装到一个DLL中,使用加壳部分的代码将DLL中的代码和数据植入加壳后的PE文件。

image-20220301161223807

image-20220301161934264

​ 在加壳过程中,有一个加壳器程序和stub.dll两个文件,加壳器程序会把原文件(要加壳的文件)以文件方式读取到堆内存,它还是以文件对齐粒度(200h)对齐的,而stub.dll是以不处理的方式读取到了内存中,它是以内存粒度(1000h)对齐的。
使用LoadLibraryExA加载DLL并且第三个参数使用DONT_RESOLVE_DLL_REFERENCES的时候,他就不会对这个文件进行重定位等操作,是以原始形态加载到内存。

stub.dll里的.text段里面的数据需要先进行重定位修复,修复完成后再移植过去,这样壳区段才能正常运行起来。

​ 首先根据stub.dll的重定位表获取出stub.dll中.text段需要重定位的数据,然后把该数据

  1. 减去原始基址(Nt->OptionalHeader.ImageBase)
  2. 减去原始代码段RVA(Nt->OptionalHeader.ImageBase)
  3. 加上新基址(exe目标文件)
  4. 加上新RVA(exe中新添加的区段RVA)
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
void FixStubReloc(char* hModule,DWORD dwNewBase,DWORD dwNewSecRva)
{
//获取重定位va
auto pReloc = (PIMAGE_BASE_RELOCATION)
(GetOptHeader(hModule)->DataDirectory[5].VirtualAddress
+ hModule);

//获取.text区段的Rva
DWORD dwTextRva = (DWORD)GetSecHeader(hModule, ".text")->VirtualAddress;

//修复重定位
while (pReloc->SizeOfBlock)
{
struct TypeOffset
{
WORD offset : 12;
WORD type : 4;
};
TypeOffset* pTyOf = (TypeOffset*)(pReloc + 1);
DWORD dwCount = (pReloc->SizeOfBlock - 8) / 2;
for (size_t i = 0; i < dwCount; i++)
{
if(pTyOf[i].type != 3)
continue;
//要修复的Rva
DWORD dwFixRva = pTyOf[i].offset + pReloc->VirtualAddress;
//要修复的地址
DWORD* pFixAddr = (DWORD*)(dwFixRva + (DWORD)hModule);

DWORD dwOld;
VirtualProtect(pFixAddr, 4, PAGE_READWRITE, &dwOld);
*pFixAddr -= (DWORD)hModule; //减去原始基址
*pFixAddr -= dwTextRva; //减去原始代码段Rva
*pFixAddr += dwNewBase; //加上新基址
*pFixAddr += dwNewSecRva; //加上新Rva
VirtualProtect(pFixAddr, 4, dwOld, &dwOld);
}
//指向下一个重定位块
pReloc = (PIMAGE_BASE_RELOCATION)
((DWORD)pReloc + pReloc->SizeOfBlock);
}
}

《加密与解密》如下处理也同样

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
void CPE::FixReloc(PBYTE lpImage, PBYTE lpCode, DWORD dwCodeRVA)
{
// 1. 获取DOS头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage;
// 2. 获取NT头
PIMAGE_NT_HEADERS64 pNt = (PIMAGE_NT_HEADERS64)((ULONGLONG)lpImage + pDos->e_lfanew);
// 3. 获取数据目录表
PIMAGE_DATA_DIRECTORY pRelocDir = pNt->OptionalHeader.DataDirectory;
pRelocDir = &(pRelocDir[IMAGE_DIRECTORY_ENTRY_BASERELOC]);
// 4. 获取重定位目录
PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((ULONGLONG)lpImage + pRelocDir->VirtualAddress);
typedef struct {
WORD Offset : 12; // (1) 大小为12Bit的重定位偏移
WORD Type : 4; // (2) 大小为4Bit的重定位信息类型值
}TypeOffset, * PTypeOffset; // 这个结构体是A1Pass总结的

// 循环获取每一个MAGE_BASE_RELOCATION结构的重定位信息
while (pReloc->VirtualAddress)
{
PTypeOffset pTypeOffset = (PTypeOffset)(pReloc + 1);
ULONGLONG dwSize = sizeof(IMAGE_BASE_RELOCATION);
ULONGLONG dwCount = (pReloc->SizeOfBlock - dwSize) / 2;
for (ULONGLONG i = 0; i < dwCount; i++)
{
if (*(PULONGLONG)(&pTypeOffset[i]) == NULL)
break;
ULONGLONG dwRVA = pReloc->VirtualAddress + pTypeOffset[i].Offset;
PULONGLONG pRelocAddr = (PULONGLONG)((ULONGLONG)lpImage + dwRVA);
// 修复重定位信息 公式:需要修复的地址-原映像基址-原区段基址+现区段基址+现映像基址
ULONGLONG dwRelocCode = *pRelocAddr - pNt->OptionalHeader.ImageBase - pNt->OptionalHeader.BaseOfCode +
dwCodeRVA + m_dwImageBase;
*pRelocAddr = dwRelocCode;
}
pReloc = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pReloc + pReloc->SizeOfBlock);
}
}
1
2
3
4
5
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
// WORD TypeOffset[1];
} IMAGE_BASE_RELOCATION;

VirtualAddress:指向PE文件中需要重定位数据的RAV,由于每个重定位结构体只负责描述0x1000字节大小区域的重定位信息,因此这个字段的值总是0x1000的倍数。
SizeOfBlock:描述IMAGE_BASE_RELOCATION结构体与重定位数组TypeOffset的体积总大小(IMAGE_SIZEOF_BASE_RELOCATION+2*n)(n==代码中的dwCount)

TypeOffset 是一个数组,数组每项大小为两个字节(16位),它由高 4位和低 12位组成,高 4位代表重定位类型,低 12位是重定位地址,它与 VirtualAddress 相加即是指向PE 映像中需要修改的那个代码的地址。

2.Stub.dll处理

将工程设置release版本,如果不想代码被优化,可以禁止优化。
大概流程如下(后三部分没有):

  1. 将数据段,只读数据段和代码段进行合并
  2. 编写代码获取API的地址
  3. 加入混淆指令,反调试
  4. 解密/解压缩
  5. 加密IAT等等
1
2
3
4
5
6
7
8
//把数据段融入代码段
#pragma comment(linker,"/merge:.data=.text")
//把只读数据段融入代码段
#pragma comment(linker,"/merge:.rdata=.text")
//设置代码段为可读可写可执行
#pragma comment(linker,"/section:.text,RWE")

extern "C" __declspec(dllexport) StubConf g_Sc = { 0 };

为方便迁移代码和数据,将两段融合,.text段也就是壳代码,因为加完壳后,在壳代码中无法使用导入表,因此,需要自己动态获取需要使用的API函数的地址。
只要获取到LoadLibraryExA和GetProcAddress两个函数的地址,我们就可以根据LoadLibraryExA来获取任意模块dll的基地址,再使用GetProcAddress函数获取到任意API函数的地址了。
根据kernel32基址可获取到GetProcAddress地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//获取内核模块基址
void GetKernel()
{
__asm
{
push esi;
mov esi, fs:[0x30]; //得到PEB地址
mov esi, [esi + 0xc]; //指向PEB_LDR_DATA结构的首地址
mov esi, [esi + 0x1c];//InInitializationOrder的地址
mov esi, [esi]; //得到第2个条目kernelBase的链表
mov esi, [esi]; //得到第3个条目kernel32的链表(win10系统)
mov esi, [esi + 0x8]; //kernel32.dll地址
mov g_hKernel32, esi;
pop esi;
}
}

上面代码部分为32位获取Kernel32.dll地址,可以在WinDbg中调试,定位TEB与PEB,定位Ldr,定位LDR_DATA_TABLE_ENTRY然后确定kernel32.dll基址。(这里罗嗦一下,具体说一下

TEB结构体部分成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct _TEB
{
struct _NT_TIB NtTib; //0x0
VOID* EnvironmentPointer; //0x1c
struct _CLIENT_ID ClientId; //0x20
VOID* ActiveRpcHandle; //0x28
VOID* ThreadLocalStoragePointer; //0x2c
struct _PEB* ProcessEnvironmentBlock; //0x30
ULONG LastErrorValue; //0x34
ULONG CountOfOwnedCriticalSections; //0x38
VOID* CsrClientThread; //0x3c
VOID* Win32ThreadInfo; //0x40
ULONG User32Reserved[26]; //0x44
ULONG UserReserved[5]; //0xac
VOID* WOW32Reserved; //0xc0
ULONG CurrentLocale; //0xc4
ULONG FpSoftwareStatusRegister; //0xc8
...

PEB包含在其中,fs寄存器指向当前活动线程的PEB结构,所以fs:[0x30]可以获取到PEB地址,PEB结构体部分成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct _PEB
{
UCHAR InheritedAddressSpace; //0x0
UCHAR ReadImageFileExecOptions; //0x1
UCHAR BeingDebugged; //0x2
union
{
UCHAR BitField; //0x3
struct
{
UCHAR ImageUsesLargePages:1; //0x3
UCHAR IsProtectedProcess:1; //0x3
UCHAR IsLegacyProcess:1; //0x3
UCHAR IsImageDynamicallyRelocated:1; //0x3
UCHAR SkipPatchingUser32Forwarders:1; //0x3
UCHAR SpareBits:3; //0x3
};
};
VOID* Mutant; //0x4
VOID* ImageBaseAddress; //0x8
struct _PEB_LDR_DATA* Ldr; //0xc
...

在偏移0xC的位置,存在struct _PEB_LDR_DATA* Ldr; ,这个_PEB_LDR_DATA结构体指针存储进程已加载的模块信息,就是包含了加载的DLL的信息。

_PEB_LDR_DATA结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
//0x30 bytes (sizeof)
struct _PEB_LDR_DATA
{
ULONG Length; //0x0
UCHAR Initialized; //0x4
VOID* SsHandle; //0x8
struct _LIST_ENTRY InLoadOrderModuleList;//模块链表,以加载顺序排序 //0xc
struct _LIST_ENTRY InMemoryOrderModuleList;//模块链表,以内存位置排序 //0x14
struct _LIST_ENTRY InInitializationOrderModuleList;//模块链表,以初始化顺序排序 //0x1c
VOID* EntryInProgress; //0x24
UCHAR ShutdownInProgress; //0x28
VOID* ShutdownThreadId; //0x2c
};

这里有3个_LIST_ENTRY结构体,它们每个的意义是不一样的。其实这三个链表都可以找到Kernel32地址。

1
2
3
4
5
操作系统规定,每当为本进程装入一个dll模块时,
就要为其分配、创建一个_LDR_DATA_TABLE_ENTRY数据结构,
并将其挂入InLoadOrderModuleList和InMemoryOrderModuleList,
完成对这个模块的动态连接以后,就把它挂入InInitializationOrderModuleList队列,
以便依次调用模块的初始化函数。

_LDR_DATA_TABLE_ENTRY 部分成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//0x78 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24
struct _UNICODE_STRING BaseDllName; //0x2c
ULONG Flags; //0x34
USHORT LoadCount; //0x38
USHORT TlsIndex; //0x3a

_PEB_LDR_DATA中的3个字段InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList,它们分别指向**_LDR_DATA_TABLE_ENTRY** 结构体上的InLoadOrderModuleLinks、InMemoryOrderModuleLinks、和InInitializationOrderModuleLinks字段。

而偏移0x18 ,VOID* DllBase 就是dll在该进程的基地址。我们要的就是这个,但是要找的kernel32的。

1
2
//获取 DllBase
mov esi, [eax + 0x8]; Poniter to DllBase

64位的地址会有些不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ULONGLONG GetKernel32Addr()
{
ULONGLONG dwKernel32Addr = 0;
// 获取TEB的地址
_TEB* pTeb = NtCurrentTeb();
// 获取PEB的地址
PULONGLONG pPeb = (PULONGLONG) * (PULONGLONG)((ULONGLONG)pTeb + 0x60);
// 获取PEB_LDR_DATA结构的地址
PULONGLONG pLdr = (PULONGLONG) * (PULONGLONG)((ULONGLONG)pPeb + 0x18);
//模块链表的头指针InLoadOrderModuleList
PULONGLONG pInLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);
// 获取链表中第一个模块信息,exe模块
PULONGLONG pModuleExe = (PULONGLONG)*pInLoadOrderModuleList;
// 获取链表中第二个模块信息,ntdll模块
PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
// 获取链表中第三个模块信息,Kernel32模块
PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
// 获取kernel32基址
dwKernel32Addr = pModuleKernel32[6];
return dwKernel32Addr;
}

用Windbg随便附加一个进程试试(**Note:**在X64环境下进行的)

image-20220302104143424

image-20220302104311490

image-20220302104504487

然后是获取GetProcAddress函数地址,遍历kernel32模块输出表,找到函数地址即可

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
ULONGLONG MyGetProcAddress()
{
ULONGLONG dwBase = GetKernel32Addr();
// 1. 获取DOS头
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase;
// 2. 获取NT头
#ifdef _WIN64
PIMAGE_NT_HEADERS64 pNt = (PIMAGE_NT_HEADERS64)(dwBase + pDos->e_lfanew);
#else
PIMAGE_NT_HEADERS32 pNt = (PIMAGE_NT_HEADERS32)(dwBase + pDos->e_lfanew);
#endif
// 3. 获取数据目录表
PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory;
pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]);
DWORD dwOffset = pExportDir->VirtualAddress;
// 4. 获取导出表信息结构
PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + dwOffset);
DWORD dwFunCount = pExport->NumberOfFunctions;
DWORD dwFunNameCount = pExport->NumberOfNames;
DWORD dwModOffset = pExport->Name;
// Get Export Address Table
PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions);
// Get Export Name Table
PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames);
// Get Export Index Table
PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);

for (DWORD dwOrdinal = 0; dwOrdinal < dwFunCount; dwOrdinal++)
{
if (!pEAT[dwOrdinal]) // Export Address offset
continue;

// 1. 获取序号
DWORD dwID = pExport->Base + dwOrdinal;
// 2. 获取导出函数地址
ULONGLONG dwFunAddrOffset = pEAT[dwOrdinal];

for (DWORD dwIndex = 0; dwIndex < dwFunNameCount; dwIndex++)
{
// 在序号表中查找函数的序号
if (pEIT[dwIndex] == dwOrdinal)
{
// 根据序号索引到函数名称表中的名字
ULONGLONG dwNameOffset = pENT[dwIndex];
char* pFunName = (char*)((ULONGLONG)dwBase + dwNameOffset);
if (!strcmp(pFunName, "GetProcAddress"))
{// 根据函数名称返回函数地址
return dwBase + dwFunAddrOffset;
}
}
}
}
return 0;
}

Stub项目应该设置为静态编译模式(配置属性-c/c++-代码生成-运行库),将MD改为MT

x86加壳实现地址

x64加壳实现地址

参考链接(x86):https://bbs.pediy.com/thread-250960.htm

参考书籍(x64):《加密与解密》