壳的原理

PE文件到运行时经过的几步重要步骤:

  • 将硬盘中的PE文件复制到内存中;
  • 按内存对齐值对齐;
  • 加载dll等模块;
  • 修复IAT,重定位表;
  • 进入OEP(Original Entry Point)开始执行

壳的原理则是修改OEP (可选PE头) 指向自身代码的地址,执行完后返回真正的OEP;

壳位于PE文件所处位置需要是可执行区段内,如.text;

计算OEP偏移地址用于jmp指令返回公式:

jmp E9 xxxxxxxx = OEP - 此指令下一指令地址

添加shellcode到PE

使用010editor:

  1. 添加一个空白区段在PE末尾 (1000h同时满足文件内存对齐);
  2. 添加一个区段头;
  3. 修正新加区段的属性(通过最后一个区段头开始以及大小计算新加区段的各属性);
  4. 修改 numberofsections (PE头);
  5. 修改 sizeofimage (可选PE头);
  6. 将shellcode粘贴于新区段处;
  7. 修改OEP于shellcode处;

如果遇到区段头无空余部分问题,可将PE头和区段头之间内容平移向上覆盖掉DOS存根,并且改掉 lfanew 偏移,之后可添加;

亦或者直接扩大最后一个区段,并修改属性;

或者合并区段:

  1. 读取PE文件模拟内存对齐对每个区段进行拉伸(防止其他区段合并后偏移错误);
  2. 只保留第一个区段头信息,其他填充0;
  3. 修改第一个区段头属性;
  4. 修改numberofsections;
  5. 将更改后的PE文件保存为新文件;

这样做会导致原文件扩大,但在内存中的大小不会改变,此时有足够的空间塞入壳代码;

加壳

为壳代码写入准备

如之前插入区段的方式与思路用代码操作PE,此时创建一个叫MyShell的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyShell
{
private:
char* fileBuff;
DWORD fileSize;
PIMAGE_DOS_HEADER pDosHeader;
PIMAGE_NT_HEADERS pNtHeader;
PIMAGE_FILE_HEADER pFileHeader;
PIMAGE_OPTIONAL_HEADER pOptionHeader;
PIMAGE_SECTION_HEADER pSectionHeader;

public:
MyShell();
~MyShell();
BOOL LoadFile(const char* path);
BOOL SaveFile(const char* path);
BOOL InitFileInfo();
BOOL InsertSection(const char* sectionName, DWORD codeSize, char* codeBuff, DWORD dwCharateristics);
DWORD GetAlignSize(DWORD realSize, DWORD alignSize);
BOOL EncodeSections();
DWORD GetOep();
void SetOep(DWORD OEP);
};

第一步,读取文件并创建buffer保存PE镜像;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOOL MyShell::LoadFile(const char* path)
{
//打开文件
HANDLE hFile = CreateFileA(path, GENERIC_READ, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
//获取镜像
fileSize = GetFileSize(hFile, 0);
fileBuff = new char[fileSize] {};
if (ReadFile(hFile, fileBuff, fileSize, 0, 0) == FALSE)
{
MessageBoxA(0, "文件获取失败!", "异常", 0);
return FALSE;
}
InitFileInfo();
CloseHandle(hFile);

return TRUE;
}

第二步,解析PE(各个头);

1
2
3
4
5
6
7
8
9
10
11
//基于 fileBuff 镜像
BOOL MyShell::InitFileInfo()
{
pDosHeader = (PIMAGE_DOS_HEADER)fileBuff;
pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + fileBuff);
pFileHeader = &(pNtHeader->FileHeader);
pOptionHeader = &(pNtHeader->OptionalHeader);
pSectionHeader = (PIMAGE_SECTION_HEADER)(pFileHeader->SizeOfOptionalHeader + (DWORD)pOptionHeader);

return TRUE;
}

第三步,插入区段和区段头,设置属性并修改PE某些字段;

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
BOOL MyShell::InsertSection(const char* sectionName, DWORD codeSize, char* codeBuff, DWORD dwCharateristics)
{
//判断节区头是否剩下两个头的空位
DWORD SectionCount = pFileHeader->NumberOfSections;
DWORD EndOfSectionHeader = (DWORD)pSectionHeader + SectionCount * IMAGE_SIZEOF_SECTION_HEADER;
DWORD BeginOfSection = (DWORD)fileBuff + pOptionHeader->SizeOfHeaders;
if (BeginOfSection - EndOfSectionHeader < IMAGE_SIZEOF_SECTION_HEADER * 2)
{
MessageBoxA(0, "插入失败,节区头大小不足!", "异常", 0);
return FALSE;
}
//获取新PE文件大小并建立新buff存放新PE
DWORD newFileSize = GetAlignSize(fileSize + codeSize, pOptionHeader->FileAlignment);
char* newFileBuff = new char[newFileSize] {};
memcpy_s(newFileBuff, newFileSize, fileBuff, fileSize);
fileSize = newFileSize;
delete[] fileBuff;
fileBuff = newFileBuff;
InitFileInfo();
//新增区段添加区段头并添加属性:名字,内存大小,文件大小,内存地址,文件偏移,权限
PIMAGE_SECTION_HEADER lastSectionHeader = pSectionHeader + (SectionCount - 1);
PIMAGE_SECTION_HEADER newSectionHeader = lastSectionHeader + 1;
strcpy_s((char *)newSectionHeader->Name, 8, sectionName);
newSectionHeader->Misc.VirtualSize = GetAlignSize(codeSize, pOptionHeader->SectionAlignment);
newSectionHeader->SizeOfRawData = GetAlignSize(codeSize, pOptionHeader->FileAlignment);
newSectionHeader->VirtualAddress = lastSectionHeader->VirtualAddress + GetAlignSize(lastSectionHeader->Misc.VirtualSize, pOptionHeader->SectionAlignment);
newSectionHeader->PointerToRawData = lastSectionHeader->PointerToRawData + lastSectionHeader->SizeOfRawData;
newSectionHeader->Characteristics = dwCharateristics;
newSectionHeader->PointerToLinenumbers = 0;
newSectionHeader->PointerToRelocations = 0;
newSectionHeader->NumberOfLinenumbers = 0;
newSectionHeader->NumberOfRelocations = 0;
//修改numberofsections以及sizeofimage
pFileHeader->NumberOfSections++;
pOptionHeader->SizeOfImage += GetAlignSize(codeSize, pOptionHeader->SectionAlignment);

//添加shellcode
char* sectionBuff = newSectionHeader->PointerToRawData + fileBuff;
memcpy(sectionBuff, codeBuff, codeSize);

return TRUE;
}

//内存对齐
DWORD MyShell::GetAlignSize(DWORD realSize, DWORD alignSize)
{
if (realSize % alignSize == 0)
return realSize;
return (realSize / alignSize + 1) * alignSize;
}

第四步,保存文件;

1
2
3
4
5
6
7
8
9
10
11
12
BOOL MyShell::SaveFile(const char* path)
{
HANDLE hFile = CreateFileA(path, GENERIC_WRITE, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
if (WriteFile(hFile, fileBuff, fileSize, 0, 0) == FALSE)
{
MessageBoxA(0, "保存文件失败!", "异常", 0);
return FALSE;
}
CloseHandle(hFile);

return TRUE;
}

至此,用代码实现了之前用010手动粘贴shellcode之前的所有步骤;

对原程序编码加密

加壳难度逐级递进,此时先考虑对.text段的加密,因为.data段有IAT表等东西需要处理;

1
2
3
4
5
6
7
8
9
BOOL MyShell::EncodeSections()
{
int key = 0xBC;
char* pData = pSectionHeader->PointerToRawData + fileBuff;
for (int i = 0; i < pSectionHeader->SizeOfRawData; i++)
pData[i] ^= key;

return TRUE;
}

制作壳代码

之前写的壳代码是需要手动从ida里扣的,这里的壳代码写在dll文件里,使用link命令合并区段只剩代码段,此时第一节区便是需要的shellcode;

1
2
3
4
5
//代码段数据段合并
#pragma comment(linker,"/merge:.data=.text")
#pragma comment(linker,"/merge:.rdata=.text")
//设置属性
#pragma comment(linker,"/section:.text,RWE")

壳代码的特点:

  • 拥有解密功能;
  • 拥有保护功能;

难点:

  • 壳代码是后期写入文件里,系统无法修复壳代码的iat,需要动态调用API;
  • 壳代码如果有全局变量,会涉及到重定位,需要修复壳的重定位表;

其具体构造如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
_declspec(naked) void Code()
{
//保存寄存器环境
__asm pushad
//实现逻辑
GetAPI();
DecodeSections();
MyCode();
//恢复寄存器环境并跳入真正OEP
__asm popad
__asm jmp g_OepInfo.oldOEP;
}

原OEP以及新OEP的传递需要用到dll的结构体导出,以便exe和dll交换OEP信息,其结构体如下所示:

1
2
3
4
5
6
7
8
9
typedef struct _OEPINFO
{
DWORD newOEP;
DWORD oldOEP;
}OEPINFO, * POEPINFO;

extern "C" _declspec(dllexport) OEPINFO g_OepInfo;

OEPINFO g_OepInfo = { (DWORD)Code };

在之前的MyShell类里给出了OEP的set和get方法:

1
2
3
4
5
6
7
8
9
10
11
12
DWORD MyShell::GetOep()
{
return pOptionHeader->AddressOfEntryPoint + pOptionHeader->ImageBase;
}

//输入的OEP实际上为Code函数距离其节区开始的偏移
void MyShell::SetOep(DWORD OEP)
{
DWORD SectionCount = pFileHeader->NumberOfSections;
PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + (SectionCount - 1);
pOptionHeader->AddressOfEntryPoint = pLastSectionHeader->VirtualAddress + OEP;
}

动态调用API

首先是GetAPI的实现,如何动态获取API地址呢,在上一篇ShellCode中说明了需要利用PEB来获取kernel32的基址从而找到LoadLibrary以获取所有可使用的API;

代码如下:

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
//获取kernel32或者kernelbase
DWORD GetEssentialModule()
{
DWORD dwBase = 0;
__asm
{
mov eax, dword ptr fs : [0x30]
mov eax, [eax + 0xc]
mov eax, [eax + 0x1c]
mov eax, [eax]
mov eax, [eax + 0x8]
mov dwBase, eax
}

return dwBase;
}

//根据导出表寻址函数
DWORD MyGetProcAddress(DWORD hModule, LPCSTR funcName)
{
//获取NT头
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + (DWORD)hModule);
//获取导出表
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)((pNtHeader->OptionalHeader.DataDirectory[0].VirtualAddress) + (DWORD)hModule);
//名称表,序号表,地址表
DWORD* nameTable = (DWORD*)(exportTable->AddressOfNames + (DWORD)hModule);
WORD* oridinalTable = (WORD*)(exportTable->AddressOfNameOrdinals + (DWORD)hModule);
DWORD* addressTable = (DWORD*)(exportTable->AddressOfFunctions + (DWORD)hModule);
//获取函数地址
for (int i = 0; i < exportTable->NumberOfNames; i++)
{
//获取函数名
char* name = (char*)(nameTable[i] + (DWORD)hModule);
if (!strcmp(name, funcName))
return addressTable[oridinalTable[i]] + (DWORD)hModule;
}

return 0;
}

//获取之后所要用到的API
void GetAPI()
{
DWORD kernelBase = GetEssentialModule();
//获取LoadlibraryEx
g_MyLoadLibraryExA = (MyLoadLibraryExA)MyGetProcAddress(kernelBase, "LoadLibraryExA");
//动态加载kernel32.dll
HMODULE kernel32Base = g_MyLoadLibraryExA("kernel32.dll", 0, 0);
g_MyGetProcAddress = (MYGetProcAddress)MyGetProcAddress((DWORD)kernel32Base, "GetProcAddress");
g_MyGetModuleHandleA = (MyGetModuleHandleA)g_MyGetProcAddress(kernel32Base, "GetModuleHandleA");
g_MyVirtualProtect = (MyVirtualProtect)g_MyGetProcAddress(kernel32Base, "VirtualProtect");
HMODULE user32Base = g_MyLoadLibraryExA("user32.dll", 0, 0);
g_MyMessageBoxA = (MyMessageBoxA)g_MyGetProcAddress(user32Base, "MessageBoxA");
}

对以上使用到的全局变量定义为如下:

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
typedef HMODULE (WINAPI * MyLoadLibraryExA)(
LPCSTR lpLibFileName,
HANDLE hFile,
DWORD dwFlags
);

MyLoadLibraryExA g_MyLoadLibraryExA = NULL;

typedef FARPROC (WINAPI * MYGetProcAddress)(
HMODULE hModule,
LPCSTR lpProcName
);

MYGetProcAddress g_MyGetProcAddress = NULL;

typedef HMODULE (WINAPI * MyGetModuleHandleA)(
LPCSTR lpModuleName
);

MyGetModuleHandleA g_MyGetModuleHandleA = NULL;

typedef BOOL (WINAPI * MyVirtualProtect)(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);

MyVirtualProtect g_MyVirtualProtect = NULL;

typedef int (WINAPI * MyMessageBoxA)(
HWND hWnd,
LPCSTR lpText,
LPCSTR lpCaption,
UINT uType
);

MyMessageBoxA g_MyMessageBoxA = NULL;

之后,在解密部分实现里,就可以使用API来操作数据了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL DecodeSections()
{
int key = 0xBC;
//写入后,得到exe的镜像基址,并获取其节区
HMODULE hModule = g_MyGetModuleHandleA(0);
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + (DWORD)hModule);
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeader);
char* sectionBuff = (char *)(pSectionHeader->VirtualAddress + (DWORD)hModule);
//解密
DWORD oldProtect;
g_MyVirtualProtect(sectionBuff, pSectionHeader->SizeOfRawData, PAGE_EXECUTE_READWRITE, &oldProtect);
for (int i = 0; i < pSectionHeader->SizeOfRawData; i++)
sectionBuff[i] ^= key;
g_MyVirtualProtect(sectionBuff, pSectionHeader->SizeOfRawData, oldProtect, &oldProtect);

return TRUE;
}

void MyCode()
{
g_MyMessageBoxA(0, "壳代码执行!", "提示", 0);
}

此时便有了dll文件,在之前的MyShell类的main函数中做出如下拼装:

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
#define CHARACTERISTICS 0xE00000E0

typedef struct _OEPINFO
{
DWORD newOEP;
DWORD oldOEP;
}OEPINFO, * POEPINFO;

int main()
{
MyShell myShell;

if (argc < 2)
{
printf("\nUsage: %s + ./file_you_want_pack\n\n", argv[0]);
return 0;
}
char* path = argv[1];

//载入目标exe并对节区加密
myShell.LoadFile(path);
myShell.EncodeSections();
//载入上面编写好的dll文件
HMODULE hModule = LoadLibraryA("ShellCode.dll");
//获取OEP信息结构
POEPINFO pOepInfo = (POEPINFO)GetProcAddress(hModule, "g_OepInfo");
pOepInfo->oldOEP = myShell.GetOep();

//获取dll的节区位置,这是需要的shellcode
PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + (DWORD)hModule);
PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader);
char* buff = (char*)(pDllSectionHeader->VirtualAddress + (DWORD)hModule);
//将shellcode“粘贴”进目标exe文件
myShell.InsertSection("BcShell", pDllSectionHeader->Misc.VirtualSize, buff, CHARACTERISTICS);

//设置新的OEP指向
myShell.SetOep(pOepInfo->newOEP - (DWORD)hModule - pDllSectionHeader->VirtualAddress);

myShell.SaveFile(path);

return 0;
}

至此一个加壳项目就 “完成” 了;

但这里任然保留了一个难点还没攻克:重定位表,在shellcode里编写的对全局变量引用的地址和在写入目标exe后所对应的地址是有问题的;

修复重定位表

重定位表结构

重定位表记录编译之前立即数地址所对应内容的位置,用于编译期间修复立即数,防止基址变化引起的立即数定位错误(类似于IAT在编译期间会修复原本指向名称为准确的地址);

其位于datadirectory[5];

重定位表中只有两个字段:VirtualAddress,SizeOfBlock,都为DWORD类型;

一个程序可能有多张重定位表;

其结构如下图所示:

relocate

其中sizeofblock为整个结构的大小(DWORD区域和WORD区域);

virtualaddress存放的内容一般为0x1000的整数倍;

word类型区域存放数据加上virtualaddress的数据则是某个立即数的准确rva;

这些rva转换为va之后,存的是立即数,而不是立即数对应的变量值;

word类型区域存放数据:0001 0000 0000 0000 ,高4位用于标识:0011为有效,其他位才是用来加virtualaddress的数据;

关于0x1000是对于4KB内存页的对齐,节省内存空间才这么设计的上述结构;

修复开始

对于将要修复的重定位表是针对于注入后的壳代码而言的,通过一个共同点:立即数地址对节区的偏移不变;

思路

  • 从dll中拿到原本的重定位表里的所有rva,利用这个rva和节区rva计算不变的相对偏移offset;
  • 通过offset再拿到镜像中这些立即数的存放PA地址,此时也就得到了立即数;
  • 再用同样的方法用立即数获取对应变量在镜像中新的立即数地址,并利用得到的PA地址来替换这些立即数;

对MyShell类新增函数:

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
//修复插入后壳代码的个别立即数
BOOL MyShell::RepairRelocate(DWORD imageBase)
{
//获取dll重定位表
PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)imageBase;
PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + imageBase);
PIMAGE_OPTIONAL_HEADER pDllOptionHeader = &(pDllNtHeader->OptionalHeader);
PIMAGE_BASE_RELOCATION pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllOptionHeader->DataDirectory[5].VirtualAddress + imageBase);
PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader);

//遍历重定位表
while (pDllRelocate->SizeOfBlock)
{
//取word类型个数
DWORD reCount = (pDllRelocate->SizeOfBlock - 8) / 2;
char* begin = (char *)pDllRelocate + 8;

//遍历每个小数
for (int i = 0; i < reCount; i++)
{
WORD* pRelocRva = (WORD*)begin;
//有效位判断
if ((*pRelocRva & 0x3000) == 0x3000)
{
//取DLL内立即数rva
DWORD relocRva = (*pRelocRva & 0xfff) + pDllRelocate->VirtualAddress;
//计算offset
DWORD offset = relocRva - pDllSectionHeader->VirtualAddress;
//计算镜像中立即数地址
DWORD SectionCount = pFileHeader->NumberOfSections;
PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + (SectionCount - 1);
DWORD destAddr = offset +(DWORD)(fileBuff + pLastSectionHeader->PointerToRawData);

//计算变量相对节区offset
offset = (*(DWORD*)destAddr - imageBase) - pDllSectionHeader->VirtualAddress;
//计算变量于镜像内新VA并修改
DWORD aimVA = offset + (pLastSectionHeader->VirtualAddress + pOptionHeader->ImageBase);
*(DWORD*)destAddr = aimVA;
}
begin += 2;
}
pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllRelocate->SizeOfBlock + (DWORD)pDllRelocate);
}

return TRUE;
}

对于以上实现的加壳项目只适用于32位且固定基址的程序;

固定基址的原因:全局变量的VA计算用到了dll的imagebase,OEP的提取也用到了imagebase;

动态基址问题

思路是将dll的重定位表也扔到加壳程序里,利用操作系统对壳代码修复重定位表;

此时壳代码可以畅通无阻地运行,则可以在壳代码中动态的获取程序基址计算OEP,修复原程序重定位表;

思路:

  • 塞入dll壳代码与重定位表;
  • 修复壳代码对应固定基址时立即数;
  • 修复重定位表(重定位表的virtualAddress相对加壳程序而言);
  • 修改加壳程序datadirectory[5]字段;
  • 修复原程序重定位表;

步骤一,更改了main中对insertSection的输入:

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
	//获取OEP信息结构
POEPINFO pOepInfo = (POEPINFO)GetProcAddress(hModule, "g_OepInfo");
pOepInfo->oldOEP = myShell.GetOep();
------

//保存旧重定位表信息
DWORD oldRelocSize = 0;
char * oldReloc = myShell.SaveOldReloc(&oldRelocSize);
//保存PE文件名
PPENAME PeName = (PPENAME)GetProcAddress(hModule, "g_PeName");
strcpy_s(PeName->name, path);

//获取dll的节区位置,这是需要的shellcode
PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + (DWORD)hModule);
PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader);
char* buff = (char*)(pDllSectionHeader->VirtualAddress + (DWORD)hModule);
//获取导入表
myShell.GetImportTable();
//获取dll重定位表
char* pDllRelocate = (char *)(pDllNtHeader->OptionalHeader.DataDirectory[5].VirtualAddress + (DWORD)hModule);
//计算插入Buff
DWORD finalSize = oldRelocSize + pDllSectionHeader->Misc.VirtualSize + pDllNtHeader->OptionalHeader.DataDirectory[5].Size + myShell.GetImportTableSize();
char* finalBuff = new char[finalSize];
char* p = finalBuff;
memcpy(p, buff, pDllSectionHeader->Misc.VirtualSize);
p += pDllSectionHeader->Misc.VirtualSize;
if (oldReloc)
memcpy(p, oldReloc, oldRelocSize);
p += oldRelocSize;
memcpy(p, pDllRelocate, pDllNtHeader->OptionalHeader.DataDirectory[5].Size);

------
//将shellcode以及dll重定位表“粘贴”进目标exe文件
char* sectionBuff = myShell.InsertSection("BcShell", finalSize, finalBuff, CHARACTERISTICS);
delete[] finalBuff;

虚线内为更改部分,这使得插入的节区大小可以满足shellcode以及重定位表和原程序自己导入表和原重定位表的大小;

对于新增加的函数:SaveOldReloc()和 GetImportTable(),前者有以下说明,后者放在下一个小标题讲解;

1
2
3
4
5
6
7
char * MyShell::SaveOldReloc(DWORD * size)
{
*size = pOptionHeader->DataDirectory[5].Size;
char* reloc = Rva2Foa(pOptionHeader->DataDirectory[5].VirtualAddress) + fileBuff;

return reloc;
}

由于要修改加壳程序的datadirectory[5]字段,所以要先把原来的保存起来,以修复原程序的重定位表;

步骤二三四由之前的 RepairRelocate() 函数修改而来,首先对类定义了一些成员和方法:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
DWORD inRelocSize;
PIMAGE_BASE_RELOCATION inRelocTable;
------

DWORD MyShell::Foa2Rva(DWORD foa)
{
DWORD rva = 0;
for (PIMAGE_SECTION_HEADER p = pSectionHeader; p->Name != NULL; p++)
{
if (foa >= p->PointerToRawData && foa < p->PointerToRawData + p->SizeOfRawData)
{
rva = foa + p->VirtualAddress - p->PointerToRawData;
break;
}
}

return rva;
}

DWORD MyShell::Rva2Foa(DWORD rva)
{
DWORD foa = 0;
for (PIMAGE_SECTION_HEADER p = pSectionHeader; p->Name != NULL; p++)
{
if (rva >= p->VirtualAddress && rva < p->VirtualAddress + p->Misc.VirtualSize)
{
foa = rva - p->VirtualAddress + p->PointerToRawData;
break;
}
}

return foa;
}

BOOL MyShell::GetInRelocTable(char* sectionBuff, DWORD offset)
{
inRelocTable = (PIMAGE_BASE_RELOCATION)(sectionBuff + offset);
PIMAGE_BASE_RELOCATION pInR = inRelocTable;

while (pInR->SizeOfBlock)
{
inRelocSize++;
pInR++;
}

return TRUE;
}

//修改后的
BOOL MyShell::RepairRelocate(DWORD imageBase, char* sectionBuff,DWORD offset)
{
//获取dll重定位表
PIMAGE_DOS_HEADER pDllDosHeader = (PIMAGE_DOS_HEADER)imageBase;
PIMAGE_NT_HEADERS pDllNtHeader = (PIMAGE_NT_HEADERS)(pDllDosHeader->e_lfanew + imageBase);
PIMAGE_OPTIONAL_HEADER pDllOptionHeader = &(pDllNtHeader->OptionalHeader);
PIMAGE_BASE_RELOCATION pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllOptionHeader->DataDirectory[5].VirtualAddress + imageBase);
PIMAGE_SECTION_HEADER pDllSectionHeader = IMAGE_FIRST_SECTION(pDllNtHeader);

//shellcode段RVA
DWORD shellCodeRVA = Foa2Rva(sectionBuff - fileBuff);
PIMAGE_BASE_RELOCATION pInR = inRelocTable;

//遍历重定位表
while (pDllRelocate->SizeOfBlock)
{
//取word类型个数
DWORD reCount = (pDllRelocate->SizeOfBlock - 8) / 2;
char* begin = (char *)pDllRelocate + 8;

//遍历每个小数
for (int i = 0; i < reCount; i++)
{
WORD* pRelocRva = (WORD*)begin;
//有效位判断
if ((*pRelocRva & 0x3000) == 0x3000)
{
//取DLL内立即数rva
DWORD relocRva = (*pRelocRva & 0xfff) + pDllRelocate->VirtualAddress;
//计算offset
DWORD offset = relocRva - pDllSectionHeader->VirtualAddress;
//计算镜像中立即数地址
DWORD SectionCount = pFileHeader->NumberOfSections;
PIMAGE_SECTION_HEADER pLastSectionHeader = pSectionHeader + (SectionCount - 1);
DWORD destAddr = offset +(DWORD)(fileBuff + pLastSectionHeader->PointerToRawData);

//计算变量相对节区offset
offset = (*(DWORD*)destAddr - imageBase) - pDllSectionHeader->VirtualAddress;
//计算变量于镜像内新VA并修改
DWORD aimVA = offset + (pLastSectionHeader->VirtualAddress + pOptionHeader->ImageBase);
*(DWORD*)destAddr = aimVA;
}
begin += 2;
}
//修复壳代码重定位表
pInR->VirtualAddress = pDllRelocate->VirtualAddress - pDllSectionHeader->VirtualAddress + shellCodeRVA;
pDllRelocate = (PIMAGE_BASE_RELOCATION)(pDllRelocate->SizeOfBlock + (DWORD)pDllRelocate);
pInR++;
}
//修改目标data目录指向注入重定位表
DWORD ss = 0;
SaveOldReloc(&ss);
DWORD tableNewRva = Foa2Rva((DWORD)sectionBuff + offset - (DWORD)fileBuff - ss);
pOptionHeader->DataDirectory[5].VirtualAddress = tableNewRva;
pOptionHeader->DataDirectory[5].Size += pDllOptionHeader->DataDirectory[5].Size;

return TRUE;
}

步骤五,实则已经实现,在步骤一将两张重定位表顺序插入shellcode之后,且在上面的代码中最后几段将导入表size更改为了两个size叠加;

用这个方法可以绕过动态基质,但是这个架构写出的壳有个bug,导致原程序加壳后变成固定基址了…虽然可以正常跑….

加密导入表

此步骤针对于程序安全性质而言;

针对加壳程序的导入表加密,对API进行保护;

步骤:

  • 转移导入表进新区段,并抹掉原导入表(填充00,并将datadirectory[1]指向一个假表);
  • 对API名称加密;
  • 对新导入表加密;
  • 于壳代码中解密并手动模拟导入表的修复(使用对应dll的导出表);

此处只给出了转移导入表部分的代码,对于后期加密部分可参考上一篇 Windows_ShellCode;

当此处实现后,因为可以由代码自己修复导入表和重定位表,则原程序的.idata段和.reloc段就随便乱改都没问题了;

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
//新增字段
DWORD importTableSize;
PIMAGE_IMPORT_DESCRIPTOR pImportTable;
------

char* MyShell::GetImportTable()
{
pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(Rva2Foa(pOptionHeader->DataDirectory[1].VirtualAddress) + (DWORD)fileBuff);
for (PIMAGE_IMPORT_DESCRIPTOR p = pImportTable; p->Name != NULL; p++)
importTableSize++;
importTableSize++;
importTableSize *= sizeof(IMAGE_IMPORT_DESCRIPTOR);

return (char *)pImportTable;
}

BOOL MyShell::MoveImportTable(char* sectionBuff, DWORD offset)
{
PIMAGE_IMPORT_DESCRIPTOR newImportTable = (PIMAGE_IMPORT_DESCRIPTOR)(sectionBuff + offset);
//移动到指定区段偏移位置
memcpy(newImportTable, pImportTable, importTableSize);
memset(pImportTable, 0x00, importTableSize);
pImportTable = newImportTable;
//修改datadirectory对应rva
pOptionHeader->DataDirectory[1].VirtualAddress = Foa2Rva((DWORD)newImportTable - (DWORD)fileBuff);

return TRUE;
}

DWORD MyShell::GetImportTableSize()
{
return importTableSize;
}

加密这里有个坑,按dll遍历函数名称填地址的时候,kernel32和kernelbase里有ntdll的函数引用,但是又有同名函数干扰,要想办法将对应dll的API地址填充正确,解决方法是判断导入表名称是否为kernel32或者kernelbase,如果是则多循环一次,多循环的一次dll则加载ntdll;

完成所有内容(包括加密导入表的所有步骤)的加壳项目:

关于加壳dll需要添加一个名字结构数组来传递模块名称,否则GetModuleHandle(0)是进程基址;

( BUG 肯定是有的( 缺陷是支持32位且节区头需要空闲 (

脱壳

脱壳手段于之前基础篇大部分都提及;

此外,esp定律一般是哄骗小学生的,大部分时候都用不到;

但基础篇尚未提及一点,在dump之后的文件虽然是可以查看其源码的,但如果需要动调,是无法实现的;

此时要让dump的程序能运行,则需要修复其导入表,因为此时导入表dump出的是实打实的地址,需要利用地址反找函数符号,重新构建导入表结构;

修复导入表一般用脚本完成,脚本实现思路如上述所示;