该部分是加壳技术的基础,创建新区段后,将壳代码装入该区段,并在程序运行时先行执行,如果新区段的创建注入完成了,可以说加壳也成功了一半。
思路分析
经过前面的学习我们会发现,所谓PE文件也只是由一些基础的数据结构构成的,只是他们的变量名太长,整体结构稍许复杂让我们觉得比较恼火,其本质并不难。所以对PE文件的修改,我们只需要遵循其本质规律与特征即可。
在添加区块之前,我们要修改PE文件中的一些相关“配置”,区块的有关信息分别存储在PE文件头的FILE_HEADER的区块数量、区块表中的区块数据,OPTINONAL_HEADER的映像大小也与之相关。
具体方法
基础信息获取
要处理PE文件,DOS头,PE头,FILE头等信息免不了要获取,于其后面在每个函数中都写一遍,不如提前把获取信息的过程封装为函数。
PIMAGE_DOS_HEADER pDH = (PIMAGE_DOS_HEADER)ImageBase; //DOS头的获取只要一行代码,就不写函数了
PIMAGE_NT_HEADERS GetNtHeader(PIMAGE_DOS_HEADER pDh) {//获取NT头
PIMAGE_NT_HEADERS pNtH = (PIMAGE_NT_HEADERS)((DWORD)pDh + pDh->e_lfanew);
return pNtH;
}
PIMAGE_FILE_HEADER GetFileHeader(PIMAGE_NT_HEADERS pNth) {//获取FILEHEADER
PIMAGE_FILE_HEADER pFH = &pNth->FileHeader;
return pFH;
}
PIMAGE_OPTIONAL_HEADER GetOptionalHeader(PIMAGE_NT_HEADERS pNth) {//获取OPTIONALHEADER
PIMAGE_OPTIONAL_HEADER pOh = &pNth->OptionalHeader;
return pOh;
}
PIMAGE_SECTION_HEADER GetFirstSection(PIMAGE_NT_HEADERS pNth) {//获取第一个区块
PIMAGE_SECTION_HEADER pSech= (PIMAGE_SECTION_HEADER)((UINT_PTR)pNth + sizeof(IMAGE_NT_HEADERS));
return pSech;
}
PE头文件的修改
PE文件头FILE_HEADER中NumberOfSection++。
pFh->NumberOfSections++;//区块数目+1
##选择区块添加位置
如果添加在最前面,需要将后面的区块全部向后移动,不仅增加了内存开销,还需要修改所有区块表的信息,相当麻烦,所以为了方便,最好将新区块添加到最后。
在最后添加区块需要先获得头区块表,然后根据FILE_HEADER中的NumberOfSection遍历区块表,找到最后的区块表,定位我们需要添加信息的区块表。
PIMAGE_SECTION_HEADER GetLastSection(LPVOID ImageBase) {//新区块需加在最后一个区块后 获取最后的区块表
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)ImageBase; //DOS头获取
PIMAGE_NT_HEADERS pNtH = (PIMAGE_NT_HEADERS)((DWORD)pdh + pdh->e_lfanew);//NT头获取
PIMAGE_FILE_HEADER pFh = &pNtH->FileHeader;
PIMAGE_SECTION_HEADER pSecHed = (PIMAGE_SECTION_HEADER)((UINT_PTR)pNtH + sizeof(IMAGE_NT_HEADERS));
DWORD SectionNumber = pFh->NumberOfSections;//区块数目
PIMAGE_SECTION_HEADER LastSection = pSecHed + (SectionNumber-1);
return LastSection;
}
注意,此时返回的是已有内容的最后一块区块,我们如果要对新区块进行修改和添加的话,需要将获得的Section++。
增加区块表的信息
这部分我们需要手动设置区块表的信息,首先是区块名称,通过以下方式修改:
memcpy(NewSection->Name, SectionName, 8);//设置新区块名称
SectionName为自行设置的参数,其类型为const char*,因为区块名称的规定为8位ASCII码,故设置拷贝的最大值为8。
在修改其他信息之前,需要对一些成员进行对齐处理,编写函数如下:
SIZE_T Section_Align(_In_ SIZE_T File_Size,_In_ SIZE_T Alignment){//对齐区段
return ((File_Size % Alignment == 0) ?//三目运算符,满足返回第一个,否则返回第二个
File_Size : File_Size / (Alignment - 1) * Alignment);
}
余下需要修改的成员如下:
VirtualSize 设置区块的实际大小,即添加区块在磁盘中的大小
SizeOfRawData 区块对齐后的大小
VirtualAddress 区块虚拟内存偏移,即新增区块的上一区块的虚拟地址+偏移还要再+0x1000
PointerToRawData 区块文件偏移,上一个区块的偏移加大小
Characteristics 属性设置为0xE00000E0
NewSection->Misc.VirtualSize = FileSize;//设置区块实际大小
NewSection->SizeOfRawData = Section_Align(FileSize, pOh->FileAlignment);//区段对齐后的大小
NewSection->VirtualAddress = (NewSection - 1)->VirtualAddress +//区段虚拟内存偏移
Section_Align((NewSection - 1)->SizeOfRawData, pOh->SectionAlignment) + 0x1000;
NewSection->PointerToRawData = (NewSection - 1)->PointerToRawData + //区段文件偏移
(NewSection - 1)->SizeOfRawData;//上一个区段的偏移加大小
NewSection->Characteristics= 0xE00000E0;//获取读写执行权限
修改PE头中映像大小信息
添加区块只给区块表信息是不够的,PE头中记录了文件的大小,而新区块的添加必然会引起整个文件大小的变化,所以应对OPTIONAL_HEADER中的SizeOfImage进行修改,新文件的大小为原文件大小+新区块对齐后的大小
pOh->SizeOfImage += NewSection->SizeOfRawData;//增加映像大小
区块数据添加
经过上述步骤,新区块表已构建完成,如果此时保存文件并运行就会发现如下结果。
这是因为区块表的文件与区块不匹配,导致PE文件结构不完整而发生的。所以我们还需要根据区块表,对新文件进行更新。
为后面加壳做基础,这里我们使用exe文件作为新区块的数据,将其添加到另一个exe文件的新区块中。大致思路为根据两exe文件大小申请内存,再开辟大小为两文件之和的新内存,将两文件内存写入并保存。完整函数如下:
LPVOID ApplyMemory(LPVOID ImageBase,DWORD FileSize) {//根据文件大小申请内存
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)ImageBase; //DOS头获取
PIMAGE_NT_HEADERS pnh = GetNtHeader(pdh);//NT头获取
DWORD dwImageBaseAddr = pnh->OptionalHeader.ImageBase;
//为了安全性,暂时将该申请的内存区域设置成可读可写,等一会再根据需要重新设置
//必须要设置MEM_RESERVE,不然不能申请0x00400000地址
LPVOID returnAddr = VirtualAlloc((LPVOID)dwImageBaseAddr, FileSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (GetLastError() == 0) {
printf("[+] 正在根据pe的加载基地址 申请内存,基地址为 0x%p\n", (LPVOID)dwImageBaseAddr);
return returnAddr;
}
else {
returnAddr = VirtualAlloc(NULL, FileSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("[+] pe的加载基地址不能用,正在重新申请地址中,基地址为 0x%p\n", (LPVOID)returnAddr);
return returnAddr;
}
}
void Fuse(DWORD FileSize,DWORD ShellSize,LPVOID FileBase,LPVOID ShellBase) {
//文件融合 输入文件大小与基址,将其写入到D盘
cout << "--------开始融合----------" << endl;
//融合两段内存
PIMAGE_DOS_HEADER pdh1 = (PIMAGE_DOS_HEADER)FileBase;
PIMAGE_DOS_HEADER pdh2 = (PIMAGE_DOS_HEADER)ShellBase; //DOS头获取
PIMAGE_NT_HEADERS pnh1 = GetNtHeader(pdh1);
PIMAGE_NT_HEADERS pnh2 = GetNtHeader(pdh2);
DWORD Size1 = pnh1->OptionalHeader.SizeOfImage;
DWORD Size2 = pnh2->OptionalHeader.SizeOfImage;//获取两映像大小
DWORD ImageBase = pnh1->OptionalHeader.ImageBase;
LPVOID NewAddr = ApplyMemory(FileBase, FileSize+ShellSize);
LPVOID NewStart = (LPVOID)((DWORD)NewAddr + (DWORD)FileSize);
CopyMemory(NewAddr, FileBase, FileSize);
CopyMemory(NewStart, ShellBase, ShellSize);
cout << "拷贝完成" << endl;
Write_File(NewAddr, FileSize+ShellSize, "D:\\test.exe");
}
实验
使用载体文件test1和壳文件shell进行实验,文件大小如图:
两文件的功能都是弹窗,载体文件弹原始程序,壳文件弹加载壳,弹窗效果如下:
使用融合函数融合两文件,输出结果如下:
可以看到输出结果为两文件之和大小的文件,使用lordPE查看区块,结果如下:
可以看到新添加的shell区块已存在,如果文件可以运行则说明添加成功,运行结果如下:
能正常运行,说明没问题了。
本来想的是完成了这一步就完成了加壳的关键一步,结果只是添加了新区块,添加的数据也不能直接运行,后续可能还需要修复重定位表,甚至还要搞什么IAT-HOOK,怪不得逆向路子广,虽然本质只是对数据结构的处理,但没有系统学习的话复杂的底层着实劝退。
附录
#include <stdio.h>
#include <windows.h>
#include <Commdlg.h>
#include <iostream>
#include<typeinfo>
#include <psapi.h>
#include <imagehlp.h>
#pragma comment(lib, "imagehlp.lib ")
#pragma comment(lib,"psapi.lib")
using namespace std;
HANDLE GetHandle() { //获取选择可执行文件的的句柄
TCHAR szFilePath[MAX_PATH];//要分析的文件名及路径
OPENFILENAME ofn;//定义结构,调用打开对话框选择要分析的文件及其保存路径
HANDLE hFile;// 文件句柄
//必要的初始换
memset(szFilePath, 0, MAX_PATH);//szfilepath后面max_path个字节 用0替换
//cout << "Changed path:" << szFilePath << endl;
memset(&ofn, 0, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = NULL;
ofn.hInstance = GetModuleHandle(NULL);
ofn.nMaxFile = MAX_PATH;
ofn.lpstrInitialDir = L".";
ofn.lpstrFile = szFilePath;
ofn.lpstrTitle = L"选择 PE文件打开";
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
ofn.lpstrFilter = L"*.exe\0*.exe\0*.dll\0*.dll\0\0";//过滤器选择exe和DLL
if (!GetOpenFileName(&ofn))//调用打开对话框,选择要分析的文件
{
MessageBox(NULL, L"打开文件错误", NULL, MB_OK);
return 0;
}
hFile = CreateFile(szFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
return hFile;
}
PIMAGE_NT_HEADERS GetNtHeader(PIMAGE_DOS_HEADER pDh) {//获取NT头
PIMAGE_NT_HEADERS pNtH = (PIMAGE_NT_HEADERS)((DWORD)pDh + pDh->e_lfanew);
return pNtH;
}
PIMAGE_FILE_HEADER GetFileHeader(PIMAGE_NT_HEADERS pNth) {//获取FILEHEADER
PIMAGE_FILE_HEADER pFH = &pNth->FileHeader;
return pFH;
}
PIMAGE_OPTIONAL_HEADER GetOptionalHeader(PIMAGE_NT_HEADERS pNth) {//获取OPTIONALHEADER
PIMAGE_OPTIONAL_HEADER pOh = &pNth->OptionalHeader;
return pOh;
}
LPVOID GetImageBase(HANDLE hFile) { //根据句柄创建基址
HANDLE hMapping;// 映射文件句柄
LPVOID ImageBase;// 映射基址
//选择要分析的文件后,经过3步打开并映射选择的文件到虚拟内存中
//1.创建文件内核对象,其句柄保存于hFile,将文件在物理存储器的位置通告给操作系统
if (!hFile)
{
MessageBox(NULL, L"打开文件错误", NULL, MB_OK);
return 0;
}
//2.创建文件映射内核对象(分配虚拟内存),句柄保存于hFileMapping
hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
if (!hMapping)
{
CloseHandle(hFile);
return FALSE;
}
//3.将文件数据映射到进程的地址空间,返回的映射基址保存在ImageBase中
ImageBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
if (!ImageBase)
{
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
//showMemoryInfo(hFile);
return ImageBase;
}
PIMAGE_SECTION_HEADER GetFirstSection(PIMAGE_NT_HEADERS pNth) {//获取第一个区块
PIMAGE_SECTION_HEADER pSech= (PIMAGE_SECTION_HEADER)((UINT_PTR)pNth + sizeof(IMAGE_NT_HEADERS));
return pSech;
}
int IsPEFile(HANDLE hFile) {//判断是否为pe文件 是则返回格式 非PE文件返回0
LPVOID ImageBase = GetImageBase(hFile);//exe返回1 dll返回2
PIMAGE_DOS_HEADER pDH = NULL;//指向IMAGE_DOS结构的指针
PIMAGE_NT_HEADERS pNtH = NULL;//指向IMAGE_NT结构的指针
PIMAGE_FILE_HEADER pFH = NULL;//指向IMAGE_FILE结构的指针
PIMAGE_OPTIONAL_HEADER pOH = NULL;//指向IMAGE_OPTIONALE结构的指针
//IMAGE_DOS Header结构指针
pDH = (PIMAGE_DOS_HEADER)ImageBase;
//IMAGE_NT Header结构指针
pNtH = GetNtHeader(pDH);
//IMAGE_File Header结构指针
pFH = GetFileHeader(pNtH);
if (pDH->e_magic != IMAGE_DOS_SIGNATURE)//如果dos头不为MZ
return 0;
else if (pNtH->Signature != IMAGE_NT_SIGNATURE) //DOS头不为PE
return 0;
else if (pFH->Characteristics == 34)
{
if (pFH->NumberOfSections == 1)//只有一个区块为已加壳
return 0;
cout << "该文件为可执行文件" << endl;
return 1;
}
else if (pFH->Characteristics == 8450)
{
if (pFH->NumberOfSections == 1)//只有一个区块为已加壳
return 0;
cout << "该文件为动态链接库文件" << endl;
return 2;
}
cout << "characteristics为" << pFH->Characteristics << endl;
cout << "无法识别的文件,可能是快捷方式。" << endl;
return 3;
}
LPVOID ApplyMemory(LPVOID ImageBase,DWORD FileSize) {
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)ImageBase; //DOS头获取
PIMAGE_NT_HEADERS pnh = GetNtHeader(pdh);//NT头获取
DWORD dwImageBaseAddr = pnh->OptionalHeader.ImageBase;
//为了安全性,暂时将该申请的内存区域设置成可读可写,等一会再根据需要重新设置
//必须要设置MEM_RESERVE,不然不能申请0x00400000地址
LPVOID returnAddr = VirtualAlloc((LPVOID)dwImageBaseAddr, FileSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (GetLastError() == 0) {
printf("[+] 正在根据pe的加载基地址 申请内存,基地址为 0x%p\n", (LPVOID)dwImageBaseAddr);
return returnAddr;
}
else {
returnAddr = VirtualAlloc(NULL, FileSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
printf("[+] pe的加载基地址不能用,正在重新申请地址中,基地址为 0x%p\n", (LPVOID)returnAddr);
return returnAddr;
}
}
void Write_File(LPVOID ImageBase, DWORD FileSize,const char* FilePath)//内存写入文件
{
FILE* pFile = fopen(FilePath, "wb");
if (!pFile)
{
cout << "无法打开exe文件。" << endl;
return;
}
size_t WriteSize = fwrite(ImageBase, FileSize, 1, pFile);
cout << "写入文件大小:" << WriteSize << endl;
//关闭文件
fclose(pFile);
return;
}
LPVOID CopyProcess(HANDLE hFile) {//返回复制好的PE文件结构
DWORD FileSize = GetFileSize(hFile, NULL);
LPVOID ImageBase = GetImageBase(hFile);
LPVOID OperationBase = ApplyMemory(ImageBase,FileSize);//用于后续操作的内存空间
CopyMemory(OperationBase, ImageBase, FileSize);
return OperationBase;
}
PIMAGE_SECTION_HEADER GetLastSection(LPVOID ImageBase) {//新区块需加在最后一个区块后 获取最后的区块表
PIMAGE_DOS_HEADER pdh = (PIMAGE_DOS_HEADER)ImageBase; //DOS头获取
PIMAGE_NT_HEADERS pNtH = (PIMAGE_NT_HEADERS)((DWORD)pdh + pdh->e_lfanew);//NT头获取
PIMAGE_FILE_HEADER pFh = &pNtH->FileHeader;
PIMAGE_SECTION_HEADER pSecHed = (PIMAGE_SECTION_HEADER)((UINT_PTR)pNtH + sizeof(IMAGE_NT_HEADERS));
DWORD SectionNumber = pFh->NumberOfSections;//区块数目
PIMAGE_SECTION_HEADER LastSection = pSecHed + (SectionNumber-1);
return LastSection;
}
SIZE_T Section_Align(_In_ SIZE_T File_Size,_In_ SIZE_T Alignment){//对齐区段
return ((File_Size % Alignment == 0) ?//三目运算符,满足返回第一个,否则返回第二个
File_Size : File_Size / (Alignment - 1) * Alignment);
}
LPVOID UpdateSectionInfo(LPVOID Image,DWORD FileSize) {//更新区块表信息
cout << "---------更新区块---------" << endl;
PIMAGE_DOS_HEADER pDh = (PIMAGE_DOS_HEADER)Image;
PIMAGE_NT_HEADERS pNh = GetNtHeader(pDh);
PIMAGE_FILE_HEADER pFh = GetFileHeader(pNh);
PIMAGE_OPTIONAL_HEADER pOh = GetOptionalHeader(pNh);
pFh->NumberOfSections++;//区块数目+1
PIMAGE_SECTION_HEADER LastSection = GetLastSection(Image);//最后一个区块
PIMAGE_SECTION_HEADER NewSection = LastSection;//最后区块后的新区块内容
const char* SectionName = ".Shell";//设置区块名称
memcpy(NewSection->Name, SectionName, 8);//设置新区块名称
NewSection->Misc.VirtualSize = FileSize;//设置区块实际大小
NewSection->SizeOfRawData = Section_Align(FileSize, pOh->FileAlignment);//区段对齐后的大小
NewSection->VirtualAddress = (NewSection - 1)->VirtualAddress +//区段虚拟内存偏移
Section_Align((NewSection - 1)->SizeOfRawData, pOh->SectionAlignment) + 0x1000;
NewSection->PointerToRawData = (NewSection - 1)->PointerToRawData + //区段文件偏移
(NewSection - 1)->SizeOfRawData;//上一个区段的偏移加大小
NewSection->Characteristics= 0xE00000E0;//获取读写执行权限
pOh->SizeOfImage += NewSection->SizeOfRawData;//增加映像大小
cout << "区块信息更新完成" << endl;
return Image;
}
void Fuse(DWORD FileSize,DWORD ShellSize,LPVOID FileBase,LPVOID ShellBase) {
//文件融合 输入文件大小与基址,将其写入到D盘
cout << "--------开始融合----------" << endl;
//融合两段内存
PIMAGE_DOS_HEADER pdh1 = (PIMAGE_DOS_HEADER)FileBase;
PIMAGE_DOS_HEADER pdh2 = (PIMAGE_DOS_HEADER)ShellBase; //DOS头获取
PIMAGE_NT_HEADERS pnh1 = GetNtHeader(pdh1);
PIMAGE_NT_HEADERS pnh2 = GetNtHeader(pdh2);
DWORD Size1 = pnh1->OptionalHeader.SizeOfImage;
DWORD Size2 = pnh2->OptionalHeader.SizeOfImage;//获取两映像大小
DWORD ImageBase = pnh1->OptionalHeader.ImageBase;
LPVOID NewAddr = ApplyMemory(FileBase, FileSize+ShellSize);
LPVOID NewStart = (LPVOID)((DWORD)NewAddr + (DWORD)FileSize);
CopyMemory(NewAddr, FileBase, FileSize);
CopyMemory(NewStart, ShellBase, ShellSize);
cout << "拷贝完成" << endl;
Write_File(NewAddr, FileSize+ShellSize, "D:\\test.exe");
}
int main() {
HANDLE hFile1 = GetHandle();
HANDLE hFile2 = GetHandle();
if (IsPEFile(hFile1) != 0&& IsPEFile(hFile2)!=0) {
LPVOID FileBase = CopyProcess(hFile1);
LPVOID ShellBase = CopyProcess(hFile2);
DWORD FileSize = GetFileSize(hFile1, NULL);
DWORD ShellSize = GetFileSize(hFile2, NULL);
UpdateSectionInfo(FileBase, ShellSize);
Fuse(FileSize,ShellSize,FileBase, ShellBase);
}
return 0;
}
PS
虽然目前实现了区块的添加和文件的合并,但在特殊情况下可能会导致原有exe文件不可用,目前发现的特殊情况只有载体文件大小小于壳文件的情况,具体成因尚不清楚。
参考文章
加壳器的编写_dohxxx的博客-CSDN博客
C++实现PE文件添加新节区(加壳器)_Pluviophile12138的博客-CSDN博客