逆向-PE文件添加新区块

该部分是加壳技术的基础,创建新区段后,将壳代码装入该区段,并在程序运行时先行执行,如果新区段的创建注入完成了,可以说加壳也成功了一半。

思路分析

经过前面的学习我们会发现,所谓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;//增加映像大小

区块数据添加

经过上述步骤,新区块表已构建完成,如果此时保存文件并运行就会发现如下结果。
exe不可用
这是因为区块表的文件与区块不匹配,导致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博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值