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

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

PE文件第一个字节位于MS-DOS头部,称作IMAGE_DOS_HEADER,结构如下。
关键字段:e_magic值为5A4Dh, e_lfanew是真正的PE文件头的RVA,位于从文件开始偏移3Ch处。

紧跟着的是IMAGE_NT_HEADERS(PE头)。
、

1.signature
ASCII码字符为“PE00”


SizeofOptionalHeader:对于32位PE文件,这个域通常是00E0h,64位PE通常为00F0h。
下图为32位PE文件的相应结构体,64与32有一些区别,譬如PE32的BaseOfdata域不存在与PE32+,PE32的Magic域为010Bh,PE32+为020Bh,以及部分变量的类型不同。
DataDirectory字段是数据目录表,由数个相同的IMAGE_DATA_DIRECTORY组成,指向输入表、输出表、资源块数据。


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

常见的区块表如下

=====================================================================================
实现部分
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文件。


在加壳过程中,有一个加壳器程序和stub.dll两个文件,加壳器程序会把原文件(要加壳的文件)以文件方式读取到堆内存,它还是以文件对齐粒度(200h)对齐的,而stub.dll是以不处理的方式读取到了内存中,它是以内存粒度(1000h)对齐的。
使用LoadLibraryExA加载DLL并且第三个参数使用DONT_RESOLVE_DLL_REFERENCES的时候,他就不会对这个文件进行重定位等操作,是以原始形态加载到内存。
stub.dll里的.text段里面的数据需要先进行重定位修复,修复完成后再移植过去,这样壳区段才能正常运行起来。
首先根据stub.dll的重定位表获取出stub.dll中.text段需要重定位的数据,然后把该数据
- 减去原始基址(Nt->OptionalHeader.ImageBase)
- 减去原始代码段RVA(Nt->OptionalHeader.ImageBase)
- 加上新基址(exe目标文件)
- 加上新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) { auto pReloc = (PIMAGE_BASE_RELOCATION) (GetOptHeader(hModule)->DataDirectory[5].VirtualAddress + hModule);
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; 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; *pFixAddr += dwNewBase; *pFixAddr += dwNewSecRva; 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) { PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)lpImage; PIMAGE_NT_HEADERS64 pNt = (PIMAGE_NT_HEADERS64)((ULONGLONG)lpImage + pDos->e_lfanew); PIMAGE_DATA_DIRECTORY pRelocDir = pNt->OptionalHeader.DataDirectory; pRelocDir = &(pRelocDir[IMAGE_DIRECTORY_ENTRY_BASERELOC]); PIMAGE_BASE_RELOCATION pReloc = (PIMAGE_BASE_RELOCATION)((ULONGLONG)lpImage + pRelocDir->VirtualAddress); typedef struct { WORD Offset : 12; WORD Type : 4; }TypeOffset, * PTypeOffset;
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; } 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版本,如果不想代码被优化,可以禁止优化。
大概流程如下(后三部分没有):
- 将数据段,只读数据段和代码段进行合并
- 编写代码获取API的地址
- 加入混淆指令,反调试
- 解密/解压缩
- 加密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]; mov esi, [esi + 0xc]; mov esi, [esi + 0x1c]; mov esi, [esi]; mov esi, [esi]; mov esi, [esi + 0x8]; 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; VOID* EnvironmentPointer; struct _CLIENT_ID ClientId; VOID* ActiveRpcHandle; VOID* ThreadLocalStoragePointer; struct _PEB* ProcessEnvironmentBlock; ULONG LastErrorValue; ULONG CountOfOwnedCriticalSections; VOID* CsrClientThread; VOID* Win32ThreadInfo; ULONG User32Reserved[26]; ULONG UserReserved[5]; VOID* WOW32Reserved; ULONG CurrentLocale; ULONG FpSoftwareStatusRegister; ...
|
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; UCHAR ReadImageFileExecOptions; UCHAR BeingDebugged; union { UCHAR BitField; struct { UCHAR ImageUsesLargePages:1; UCHAR IsProtectedProcess:1; UCHAR IsLegacyProcess:1; UCHAR IsImageDynamicallyRelocated:1; UCHAR SkipPatchingUser32Forwarders:1; UCHAR SpareBits:3; }; }; VOID* Mutant; VOID* ImageBaseAddress; struct _PEB_LDR_DATA* Ldr; ...
|
在偏移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
| struct _PEB_LDR_DATA { ULONG Length; UCHAR Initialized; VOID* SsHandle; struct _LIST_ENTRY InLoadOrderModuleList; struct _LIST_ENTRY InMemoryOrderModuleList; struct _LIST_ENTRY InInitializationOrderModuleList; VOID* EntryInProgress; UCHAR ShutdownInProgress; VOID* ShutdownThreadId; };
|
这里有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
| struct _LDR_DATA_TABLE_ENTRY { struct _LIST_ENTRY InLoadOrderLinks; struct _LIST_ENTRY InMemoryOrderLinks; struct _LIST_ENTRY InInitializationOrderLinks; VOID* DllBase; VOID* EntryPoint; ULONG SizeOfImage; struct _UNICODE_STRING FullDllName; struct _UNICODE_STRING BaseDllName; ULONG Flags; USHORT LoadCount; USHORT TlsIndex;
|
_PEB_LDR_DATA中的3个字段InLoadOrderModuleList、InMemoryOrderModuleList、和InInitializationOrderModuleList,它们分别指向**_LDR_DATA_TABLE_ENTRY** 结构体上的InLoadOrderModuleLinks、InMemoryOrderModuleLinks、和InInitializationOrderModuleLinks字段。
而偏移0x18 ,VOID* DllBase 就是dll在该进程的基地址。我们要的就是这个,但是要找的kernel32的。
1 2
| 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* pTeb = NtCurrentTeb();
PULONGLONG pPeb = (PULONGLONG) * (PULONGLONG)((ULONGLONG)pTeb + 0x60);
PULONGLONG pLdr = (PULONGLONG) * (PULONGLONG)((ULONGLONG)pPeb + 0x18);
PULONGLONG pInLoadOrderModuleList = (PULONGLONG)((ULONGLONG)pLdr + 0x10);
PULONGLONG pModuleExe = (PULONGLONG)*pInLoadOrderModuleList;
PULONGLONG pModuleNtdll = (PULONGLONG)*pModuleExe;
PULONGLONG pModuleKernel32 = (PULONGLONG)*pModuleNtdll;
dwKernel32Addr = pModuleKernel32[6]; return dwKernel32Addr; }
|
用Windbg随便附加一个进程试试(**Note:**在X64环境下进行的)



然后是获取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(); PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwBase; #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 PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory; pExportDir = &(pExportDir[IMAGE_DIRECTORY_ENTRY_EXPORT]); DWORD dwOffset = pExportDir->VirtualAddress; PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(dwBase + dwOffset); DWORD dwFunCount = pExport->NumberOfFunctions; DWORD dwFunNameCount = pExport->NumberOfNames; DWORD dwModOffset = pExport->Name;
PDWORD pEAT = (PDWORD)(dwBase + pExport->AddressOfFunctions);
PDWORD pENT = (PDWORD)(dwBase + pExport->AddressOfNames);
PWORD pEIT = (PWORD)(dwBase + pExport->AddressOfNameOrdinals);
for (DWORD dwOrdinal = 0; dwOrdinal < dwFunCount; dwOrdinal++) { if (!pEAT[dwOrdinal]) continue;
DWORD dwID = pExport->Base + dwOrdinal; 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):《加密与解密》