首页
社区
课程
招聘
[原创]压缩壳的简单实现
发表于: 21小时前 420

[原创]压缩壳的简单实现

21小时前
420

压缩壳的简单实现

0x01 实现原理

实现一个压缩壳,需要做三步操作,三步过程涉及比较深入的PE结构理解,最终的实现目标如下图所示

其中过程分为以下三步

1、对原有数据压缩

对源程序使用算法进行压缩,并将数据放至目标程序的节表数据二的位置

2、对原有数据还原

编写壳代码,对自身节表二的数据进行压缩算法还原,并将生成的PE文件放至内存或者节表数据一的位置

3、对还原后的数据加载至内存并运行

编写LoadPE加载程序,对加载至内存的代码进行加载

0x02 程序入口编写

需要编写一个工具程序,将源目标程序变为目标程序的样子,该程序可以为控制台,也可以为其他程序,本文为了方便,编写为MFC程序

1、创建MFC程序

此处选择基于对话框的设计,使用MFC选择在静态库中使用MFC,使用静态库可以将其他库打包进入该程序,相对于共享链接DLL更干净,迁移更方便

2、添加对应的窗体功能

  • Mfc Edit Browser Control组件,用于选择打开的文件路径,ID此处命名为MEDIT_FILE_PATH

  • Button Control,用于加壳的入口,此处命名为BTN_PACK,并右键该按钮,添加点击事件

3、添加窗体代码

void CPackerDlg::OnBnClickedPack()
{
    // TODO: 在此添加控件通知处理程序代码
    CString strPath;
    GetDlgItemText(MEDIT_FILE_PATH, strPath);

    CString strNewPath = strPath.Left(strPath.ReverseFind('.')) + "_pack.exe";
    
    //加壳
    CPacker packer;
    if (packer.Pack(strPath, strNewPath))
    {
        AfxMessageBox("OK",MB_OK,0);
    }
    else
    {
        AfxMessageBox("False");
    }
}

0x03 分析EXE程序,将将源程序进行压缩

1、分析EXE程序

对传入的文件进行解析,并将其存入变量

bool CPacker::AnalysisPE(CString strSrc)
{
    //创建文件句柄
    m_hFile = CreateFile(strSrc,
        GENERIC_READ,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    //获取原始PE文件大小
    m_dwsrcPESize = GetFileSize(m_hFile, NULL);
    //创建原始文件的映射文件
    m_hFileMap = CreateFileMapping(
        __in      m_hFile,
        __in_opt  NULL,
        __in      PAGE_READONLY,
        __in      0,
        __in      0,
        __in_opt  NULL
    );
    //将映射对象挂载到当前进程虚拟地址空间
    m_srcPE = (LPBYTE) MapViewOfFile(
        __in  m_hFileMap,
        __in  FILE_MAP_READ,
        __in  0,
        __in  0,
        __in  0
    );
    //解析原始文件的DOS头部,PE头部和节表数据
    m_pDosHeader = (PIMAGE_DOS_HEADER)m_srcPE;
    m_pNTHeader = (PIMAGE_NT_HEADERS)(m_srcPE + m_pDosHeader->e_lfanew);
    m_pSecHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)&m_pNTHeader->OptionalHeader
        + m_pNTHeader->FileHeader.SizeOfOptionalHeader);
    return true;
}

2、获取压缩数据

使用WinAPI的Compress函数对原始数据进行压缩,得到压缩数据m_pComData

bool CPacker::GetCompressData()
{
    //创建压缩算法
    COMPRESSOR_HANDLE h_Compress;
    CreateCompressor(
        COMPRESS_ALGORITHM_XPRESS_HUFF,
        NULL,
        &h_Compress
    );

    //对原始数据进行压缩,多出的 28 字节是程序自定义压缩文件头,用来存放魔数、大小、校验等解压所需信息,感兴趣的同学可以自行搜索
    m_pComData = new BYTE[m_dwsrcPESize + 28];
    Compress(
        h_Compress,
        m_srcPE,			//源PE指针
        m_dwsrcPESize,		//源PESize
        m_pComData,			//压缩后数据指针
        m_dwsrcPESize + 28,		//压缩后缓冲区数据大小
        &m_dwComDataSize	//压缩后真实数据大小
    );

    return true;
}

0x04、壳代码的编写

壳代码主要做两件事

  • 完成PE的解压缩

  • 进行PE的加载

1、完成PE的解压缩

创建的PE文件,可能有基址随机化,无重定位修正机制等问题,为了提升壳代码的健壮性,需要用地址无关代码。

我们需要做以下几步

  • 通过运行环境获取Kernel32的地址
  • 获取GetProcess的函数地址
  • 通过GetProcess获取需要用到的函数地址
  • 通过函数进行解压缩操作
1.1、获取Kernel32的库地址

进程在加载时,FS寄存器指向当前活动线程的TEB结构,通过TEB的[0x30]获取到对应的TEB结构,再通过TEB拿到加载的Kernel32的基址

HMODULE GetKernel32() {
    HMODULE hKer;
    __asm {
        // 获取当前进程PEB结构体的内存地址
        mov eax, dword ptr fs:[0x30]
        // 获取PEB_LDR_DATA 结构体地址
        mov eax, dword ptr[eax + 0x0C]
        // 获取InLoadOrderModuleList链表头
        mov eax, dword ptr[eax + 0x0C]
        // ntdll.dll 的模块结构体首地址
        mov eax, dword ptr[eax]
        // kernel32.dll 的模块结构体首地址
        mov eax, dword ptr[eax]
        // kernel32.dll 的内存基址
        mov eax, dword ptr[eax + 0x18]
        mov hKer, eax
    }
    return hKer;
}
1.2、获取在内存中各个函数的真实地址
  • 创建函数指针
// 创建函数声明
// GetProceAddress函数
typedef FARPROC (WINAPI* PFN_GetProcAddress) (
    HMODULE hModule,
    LPCSTR lpProcName
    );
// LoadLibrary函数
typedef HMODULE(WINAPI* PFN_LoadLibraryA)(
    _In_ LPCSTR lpLibFileName
    );
// CreateDeCompressor函数
typedef BOOL(WINAPI* PFN_CreateDeCompressor)(
    DWORD Algorithm,
    PCOMPRESS_ALLOCATION_ROUTINES AllocationRoutines,
    PDECOMPRESSOR_HANDLE DeCompressHandle
    );
// DeCompress函数
typedef BOOL(WINAPI* PFN_DeCompress)(
    DECOMPRESSOR_HANDLE DeCompressorHandle,
    LPCVOID CompressedData,
    SIZE_T CompressedDataSize,
    PVOID UnCompressedBuffer,
    SIZE_T UnComressedBufferSize,
    PSIZE_T UnCompressedDataSize
    );
// VirtualAlloc函数
typedef LPVOID (WINAPI* PFN_VirtualAlloc)(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD flAllocationType,
    DWORD flProtect
);
// VirtualProtect函数
typedef BOOL(WINAPI* PFN_VirtualProtect)(
    _In_  LPVOID lpAddress,
    _In_  SIZE_T dwSize,
    _In_  DWORD flNewProtect,
    _Out_ PDWORD lpflOldProtect
);

此处为了使用方便,建了一个函数指针的结构体

struct Environment
{
    PFN_GetProcAddress pfnGetProcAddress;
    PFN_LoadLibraryA pfnLoadLibraryA;
    PFN_CreateDeCompressor pfnDeCompressor;
    PFN_DeCompress pfnDeCompress;
    PFN_VirtualAlloc pfnVirtualAlloc;
    PFN_VirtualProtect  pfnVirtualProtect;
};
  • 将函数指针指向对应的真实函数地址

Kernel32函数基址和想要的名称我们有了,剩下的就是找到对应的函数,需要用到导出表的知识

FARPROC GetAddressFromModuleBase(HMODULE hModuleBase, LPCSTR lpProcName) {
    //解析DLL,找到对应的导出目录表
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModuleBase;
    PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((LPBYTE)pDosHeader + pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)&pNTHeader->OptionalHeader + pNTHeader->FileHeader.SizeOfOptionalHeader);

    PIMAGE_DATA_DIRECTORY pDataDir = (PIMAGE_DATA_DIRECTORY)&(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);

    PIMAGE_EXPORT_DIRECTORY pExpDir = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)hModuleBase + pDataDir->VirtualAddress);
    
    // 获取导出名称表,名称序号表和导出地址表
    DWORD* pNames = (DWORD*)((DWORD)hModuleBase + pExpDir->AddressOfNames);
    WORD* pOrdinals = (WORD*)((DWORD)hModuleBase + pExpDir->AddressOfNameOrdinals);
    DWORD* pFuncs = (DWORD*)((DWORD)hModuleBase + pExpDir->AddressOfFunctions);

    // 针对名称找到对应的RVA地址
    // 判断 lpProcName 是序号(高16位为0)还是名称指针
    if (((DWORD_PTR)lpProcName & 0xFFFF0000) == 0) {
        // 按序号查找
        WORD ordinal = (WORD)(DWORD_PTR)lpProcName;
        if (ordinal < pExpDir->Base ||
            ordinal >= pExpDir->Base + pExpDir->NumberOfFunctions)
            return NULL;
        DWORD index = ordinal - pExpDir->Base;
        DWORD funcRva = pFuncs[index];
        if (funcRva >= pDataDir->VirtualAddress &&
            funcRva < pDataDir->VirtualAddress + pDataDir->Size)
            return NULL; // 转发函数
        return (FARPROC)((DWORD_PTR)hModuleBase + funcRva);
    }
    else {
        // 按名称查找
        for (DWORD i = 0; i < pExpDir->NumberOfNames; i++) {
            char* funcName = (char*)((DWORD_PTR)hModuleBase + pNames[i]);
            if (CmpStr(funcName, lpProcName) == TRUE) {
                DWORD funcRva = pFuncs[pOrdinals[i]];
                if (funcRva >= pDataDir->VirtualAddress &&
                    funcRva < pDataDir->VirtualAddress + pDataDir->Size)
                    return NULL;
                return (FARPROC)((DWORD_PTR)hModuleBase + funcRva);
            }
        }
    }

return NULL;
}

进行函数调用

VOID InitEnvironment(Environment* pEnv) {
    // 获取Kernel32基址
    HMODULE hKer = GetKernel32();
    // 保存局部变量,写数组方式可以存在栈上,全局变量和局部变量都会和地址相关
    char szGetProcAddress[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0' };
    char szLoadLibraryA[] = { 'L','o','a','d','L','i','b','r','a','r','y','A','\0' };
    char szVirtualAlloc[] = { 'V','i','r','t','u','a','l','A','l','l','o','c','\0' };
    char szVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t','\0' };
    char szCabinetDll[] = { 'C','a','b','i','n','e','t','.','d','l','l','\0' };
    char szCreateCompressor[] = { 'C','r','e','a','t','e','D','e','c','o','m','p','r','e','s','s','o','r','\0' };
    char szDeCompress[] = { 'D','e','c','o','m','p','r','e','s','s','\0' };

    // 获取GetProcAddress、LoadLibrary、VirtualAlloc和VirtualProtect的函数地址
    pEnv->pfnGetProcAddress = (PFN_GetProcAddress)GetAddressFromModuleBase(hKer, szGetProcAddress);
    pEnv->pfnLoadLibraryA = (PFN_LoadLibraryA)GetAddressFromModuleBase(hKer, szLoadLibraryA);
    pEnv->pfnVirtualAlloc = (PFN_VirtualAlloc)GetAddressFromModuleBase(hKer, szVirtualAlloc);
    pEnv->pfnVirtualProtect = (PFN_VirtualProtect)GetAddressFromModuleBase(hKer, szVirtualProtect);
    // 加载Cabinet库和里边的解压缩函数
    HMODULE hCabinet = pEnv->pfnLoadLibraryA(szCabinetDll);
    pEnv->pfnDeCompressor = (PFN_CreateDeCompressor)GetAddressFromModuleBase(hCabinet, szCreateCompressor);
    pEnv->pfnDeCompress = (PFN_DeCompress)GetAddressFromModuleBase(hCabinet, szDeCompress);
}
1.3、进行解压缩

首先需要获取当前程序的模块基址,和Kernel32的模块基址获取类似,通过TEB找到PEB,再通过PEB找到模块基址

HMODULE GetUserModuleBase() {
    HMODULE hUser;
    __asm {
        mov eax, dword ptr fs : [0x30]
        mov eax, dword ptr[eax + 0x0C]
        mov eax, dword ptr[eax + 0x0C]
        mov eax, dword ptr[eax + 0x18]
        mov hUser, eax
    }
    return hUser;
}

获取完用户的基址后,再对数据节的代码进行解压缩

// 初始化上一步的函数
Environment env;
InitEnvironment(&env);
// 定位程序的数据节
LPBYTE pImageBase = (LPBYTE)GetUserModuleBase();
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pImageBase;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((LPBYTE)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)&pNTHeader->OptionalHeader + pNTHeader->FileHeader.SizeOfOptionalHeader);

LPBYTE pComData = pImageBase + pSecHeader[1].VirtualAddress;
DWORD dwComDataSize = pSecHeader[1].PointerToRelocations;
DWORD dwSrcPESize = pSecHeader[1].PointerToLinenumbers;

//解压缩
LPBYTE pPEBuffer = (LPBYTE)env.pfnVirtualAlloc(
    NULL,
    dwSrcPESize,
    MEM_COMMIT,
    PAGE_READWRITE
);

DECOMPRESSOR_HANDLE hDeCompressor;
env.pfnDeCompressor(
    COMPRESS_ALGORITHM_XPRESS_HUFF,
    NULL,
    &hDeCompressor);

DWORD dwDeCompressDataSize;
env.pfnDeCompress(
    hDeCompressor,
    pComData,					//压缩数据
    dwComDataSize,				//压缩数据大小
    pPEBuffer,					//解压缩缓冲区
    dwSrcPESize,				//解压缩缓冲区大小
    &dwDeCompressDataSize
);

2、将解压缩后的数据进行LoadPE操作

LoadPE是模仿操作系统将PE文件按照特定格式加载进内存的操作,并将EIP执行新PE的入口点

PE加载分为两种,一种为进程镂空操作,另一种为反射式加载,这边采用反射式加载的方式,申请一块新内存,并手动解析PE,将其运行。

LoadPE分为以下几步

2.1、解析PE
// 基于PE的基址,进行解析
LPVOID pFileMap = (PVOID)pPEBuffer;
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pPEBuffer;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((BYTE*)pDosHeader + pDosHeader->e_lfanew);
DWORD dwHeaderSize = pNTHeader->OptionalHeader.SizeOfHeaders;
DWORD dwImageSize = pNTHeader->OptionalHeader.SizeOfImage;
PIMAGE_DATA_DIRECTORY pDataDir = (PIMAGE_DATA_DIRECTORY)pNTHeader->OptionalHeader.DataDirectory;
PIMAGE_SECTION_HEADER pSecHeader = IMAGE_FIRST_SECTION(pNTHeader);
DWORD dwBaseRelocationRva = pDataDir[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress;
DWORD dwBaseRelocationSize = pDataDir[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
DWORD dwNumSecSize = (DWORD)pNTHeader->FileHeader.NumberOfSections;

解析完成后,获取到对应的ImageSize,并使用VirtualAlloc分配内存,尝试使用PE自带的ImageBase,如果能分配成功,则分配至该内存,否则随机分配

// 分配新内存:先尝试PE指定的ImageBase,失败则由OS自选
LPVOID hNewBase = pEnv->pfnVirtualAlloc(
    (LPVOID)pNTHeader->OptionalHeader.ImageBase,
    dwImageSize,
    MEM_COMMIT | MEM_RESERVE,
    PAGE_EXECUTE_READWRITE
);
if (hNewBase == NULL) {
    hNewBase = pEnv->pfnVirtualAlloc(
        NULL,
        dwImageSize,
        MEM_COMMIT | MEM_RESERVE,
        PAGE_EXECUTE_READWRITE
    );
}

基于新的PE基址,算出偏移值和新的入口点地址

DWORD dwRelocBase = (DWORD)hNewBase - pNTHeader->OptionalHeader.ImageBase;
DWORD dwPEEntry = (DWORD)hNewBase + pNTHeader->OptionalHeader.AddressOfEntryPoint;
2.2、拷贝文件头数据
// 拷贝PE头到新内存
memcpy(hNewBase, pDosHeader, dwHeaderSize);
2.3、拷贝节数据
// 逐节从文件偏移拷贝到新内存RVA
for (DWORD i = 0; i < dwNumSecSize; i++)
{
    PIMAGE_SECTION_HEADER pSection = pSecHeader + i;
    // SizeOfRawData为0无需拷贝(如.bss节)
    if (pSection->SizeOfRawData == 0)
        continue;
    LPVOID dwDst = (char*)hNewBase + pSection->VirtualAddress;
    LPVOID dwSrc = (char*)pFileMap + pSection->PointerToRawData;
    // SizeOfRawData可能因文件对齐大于VirtualSize,取小值防止越界
    DWORD dwCopySize = pSection->SizeOfRawData;
    if (dwCopySize > pSection->Misc.VirtualSize)
        dwCopySize = pSection->Misc.VirtualSize;
    memcpy(dwDst, dwSrc, dwCopySize);
}
2.4、修复导入表

通过NTHeader和导入表地址算出导出节表头

PIMAGE_SECTION_HEADER GetSectionHeader(DWORD RVA, PIMAGE_NT_HEADERS pNTHeader) {
    PIMAGE_SECTION_HEADER pSecHeader = IMAGE_FIRST_SECTION(pNTHeader);
    DWORD dwNumOfSections = pNTHeader->FileHeader.NumberOfSections;
    for (DWORD i = 0; i < dwNumOfSections; i++)
    {
        if (RVA >= pSecHeader->VirtualAddress && RVA < pSecHeader->VirtualAddress + pSecHeader->Misc.VirtualSize) {
            return pSecHeader;
        }
        pSecHeader++;
    }
    return NULL;
}

进行导入表修复

// 导入表修复
DWORD dwImportAddr = pDataDir[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
// 无导入表时RVA为0,跳过避免GetSectionHeader返回NULL崩溃
// DWORD dwDiff = GetSectionHeader(dwImportAddr, pNTHeader)->VirtualAddress - GetSectionHeader(dwImportAddr, pNTHeader)->PointerToRawData;
if (dwImportAddr != 0)
{
    PIMAGE_SECTION_HEADER pImpSecHdr = GetSectionHeader(dwImportAddr, pNTHeader);
    if (pImpSecHdr != NULL)
    {
        // RVA转文件偏移的基值:SectionRawBase + (RVA - SectionRvaBase)
        DWORD dwImpSecRawBase = pImpSecHdr->PointerToRawData;
        DWORD dwImpSecRvaBase = pImpSecHdr->VirtualAddress;
        // dwDiff = dwImportAddr - dwDiff;

        // PIMAGE_IMPORT_DESCRIPTOR pItem = PIMAGE_IMPORT_DESCRIPTOR((DWORD)pFileMap + dwDiff);
        PIMAGE_IMPORT_DESCRIPTOR pItem = PIMAGE_IMPORT_DESCRIPTOR((DWORD)pFileMap + dwImpSecRawBase + (dwImportAddr - dwImpSecRvaBase));
        while (true)
        {
            if (pItem->Name == NULL || pItem->FirstThunk == NULL)
                break;

            // HMODULE hDll = pEnv->pfnLoadLibraryA((LPCSTR)((DWORD)pFileMap + pItem->Name - dwDiff));
            HMODULE hDll = pEnv->pfnLoadLibraryA((LPCSTR)((DWORD)pFileMap + dwImpSecRawBase + (pItem->Name - dwImpSecRvaBase)));
            if (hDll == NULL)
                break;

            PDWORD pINT = (PDWORD)pItem->FirstThunk;
            if (pItem->OriginalFirstThunk != NULL)
                pINT = (PDWORD)pItem->OriginalFirstThunk;
            // pINT = (PDWORD)((DWORD)pFileMap + (DWORD)pINT - dwDiff);
            pINT = (PDWORD)((DWORD)pFileMap + dwImpSecRawBase + ((DWORD)pINT - dwImpSecRvaBase));

            DWORD dwIATAddr = pItem->FirstThunk + (DWORD)hNewBase;

            while (*pINT != NULL)
            {
                DWORD dwFuncAddr = 0;
                if (*pINT & 0x80000000) {
                    dwFuncAddr = (DWORD)pEnv->pfnGetProcAddress(hDll, (LPCSTR)LOWORD(*pINT));
                } else {
                    DWORD dwNameRva = sizeof(WORD) + *(PDWORD)pINT;
                    LPCSTR lpName = (LPCSTR)((DWORD)pFileMap + dwImpSecRawBase + (dwNameRva - dwImpSecRvaBase));
                    dwFuncAddr = (DWORD)pEnv->pfnGetProcAddress(hDll, lpName);
                }
                *(DWORD*)dwIATAddr = dwFuncAddr;
                pINT++;
                dwIATAddr += 4;
            }
            pItem++;
        }
    }
}
2.5、修复重定位表
// 重定位表修复
// 无重定位表时跳过(EXE通常无重定位),避免GetSectionHeader返回NULL崩溃
if (dwBaseRelocationRva != 0 && dwBaseRelocationSize != 0)
{
    // PIMAGE_SECTION_HEADER pBaseSecHdr = GetSectionHeader((DWORD)pBaseRelocation, pNTHeader);
    PIMAGE_SECTION_HEADER pBaseSecHdr = GetSectionHeader(dwBaseRelocationRva, pNTHeader);
    if (pBaseSecHdr != NULL)
    {
        // pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pFileMap + (DWORD)pBaseRelocation - pBaseSecHdr->VirtualAddress);
        // RVA转文件偏移:SectionRawBase + (RVA - SectionRvaBase)
        PIMAGE_BASE_RELOCATION pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pFileMap + pBaseSecHdr->PointerToRawData + (dwBaseRelocationRva - pBaseSecHdr->VirtualAddress));
        DWORD dwReadRelocSize = 0;
        for (; dwReadRelocSize < dwBaseRelocationSize; )
        {
            DWORD dwPageOff = pBaseRelocation->VirtualAddress;
            LPWORD pRelocData = (LPWORD)((DWORD)pBaseRelocation + sizeof(IMAGE_BASE_RELOCATION));
            DWORD dwItemCount = (pBaseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

            for (size_t i = 0; i < dwItemCount; i++)
            {
                WORD wData = pRelocData[i];
                if (wData & 0x3000)
                {
                    DWORD dwAddr = (DWORD)hNewBase + dwPageOff + (wData & 0x0FFF);
                    *(DWORD*)dwAddr += dwRelocBase;
                }
            }
            dwReadRelocSize += pBaseRelocation->SizeOfBlock;
            pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pBaseRelocation + pBaseRelocation->SizeOfBlock);
        }
    }
}

最后跳转至入口点

DWORD OEP = EnrtyFromPEBuffer(pPEBuffer, &env);
__asm {
    jmp OEP
}

0x05、组装PE文件

写一个壳代码的PE文件分为以下几步

  • 创建文件
  • 写PE头
  • 写空节
  • 写数据节(将原始PE压缩后的数据)
  • 写代码节(第三节的壳代码,将数据进行解压缩,并进行LoadPE操作)
1、创建文件

使用WinAPI直接创建即可

//创建文件
HANDLE hFile = CreateFile(
    strNewPE,
    GENERIC_WRITE,
    0,
    NULL,
    CREATE_ALWAYS,
    FILE_ATTRIBUTE_NORMAL,
    NULL
);
2、写PE头

PE头主要关注PE内容和对应的写入长度,需要对原始PE文件做解析,获取原始的Header大小和原始的PE文件

m_dwNewPEHeaderSize = m_pNTHeader->OptionalHeader.SizeOfHeaders;
m_pNewPEHeader = new BYTE[m_dwNewPEHeaderSize];
ZeroMemory(m_pNewPEHeader, m_dwNewPEHeaderSize);
CopyMemory(m_pNewPEHeader, m_pDosHeader, m_dwNewPEHeaderSize);

同时需要对新的PE头进行修改部分数据

  • 节的数量
  • PE入口地址
  • 映像文件大小
  • 对数据目录表置空
  • 节表数据重写
//对新的PE进行解析
PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)m_pNewPEHeader;
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((LPBYTE)pDosHeader + pDosHeader->e_lfanew);
PIMAGE_SECTION_HEADER pSecHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)&pNTHeader->OptionalHeader
    + pNTHeader->FileHeader.SizeOfOptionalHeader);
DWORD dwsrcSecNum = pNTHeader->FileHeader.NumberOfSections;
// 修改节数量
pNTHeader->FileHeader.NumberOfSections = 3;
// 修改入口点
pNTHeader->OptionalHeader.AddressOfEntryPoint = m_pNewSecHeader[2].VirtualAddress;
// 修改镜像大小
pNTHeader->OptionalHeader.SizeOfImage = m_pNewSecHeader[2].VirtualAddress + m_pNewSecHeader[2].Misc.VirtualSize;
// 重置数据目录表
ZeroMemory(pNTHeader->OptionalHeader.DataDirectory, sizeof(pNTHeader->OptionalHeader.DataDirectory));
//重置节表
ZeroMemory(pSecHeader, dwsrcSecNum*40);



// 节表数据修改
// 空节
strcpy((char*)m_pNewSecHeader[0].Name, ".CodeSec");
m_pNewSecHeader[0].Misc.VirtualSize = m_pNTHeader->OptionalHeader.SizeOfImage;

m_pNewSecHeader[0].VirtualAddress = m_pSecHeader[0].VirtualAddress;
m_pNewSecHeader[0].SizeOfRawData = 0;
m_pNewSecHeader[0].PointerToRawData = 0;
m_pNewSecHeader[0].Characteristics =
    IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ;

//数据节
strcpy((char*)m_pNewSecHeader[1].Name, ".DataSec");
m_pNewSecHeader[1].Misc.VirtualSize = GetAlign(m_dwComDataSize, m_pNTHeader->OptionalHeader.SectionAlignment);
m_pNewSecHeader[1].VirtualAddress = m_pNewSecHeader[0].VirtualAddress + m_pNewSecHeader[0].Misc.VirtualSize;
m_pNewSecHeader[1].SizeOfRawData = m_dwComDataSize;
m_pNewSecHeader[1].PointerToRawData = m_pNTHeader->OptionalHeader.SizeOfHeaders;
m_pNewSecHeader[1].Characteristics =
    IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ;
m_pNewSecHeader[1].PointerToRelocations = m_dwComDataSize;
m_pNewSecHeader[1].PointerToLinenumbers = m_dwsrcPESize;


//代码(壳)节
strcpy((char*)m_pNewSecHeader[2].Name, ".PackSec");
m_pNewSecHeader[2].Misc.VirtualSize = GetAlign(m_dwDeComCodeSize, m_pNTHeader->OptionalHeader.SectionAlignment);
m_pNewSecHeader[2].VirtualAddress = m_pNewSecHeader[1].VirtualAddress + m_pNewSecHeader[1].Misc.VirtualSize;
m_pNewSecHeader[2].SizeOfRawData = m_dwDeComCodeSize;
// .PackSec 文件偏移对齐到 FileAlignment
m_pNewSecHeader[2].PointerToRawData = GetAlign(m_pNewSecHeader[1].PointerToRawData + m_pNewSecHeader[1].SizeOfRawData, m_pNTHeader->OptionalHeader.FileAlignment);
m_pNewSecHeader[2].Characteristics =
    IMAGE_SCN_MEM_WRITE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ;

CopyMemory(pSecHeader, m_pNewSecHeader, sizeof(m_pNewSecHeader));
3、写空节(.CodeSec)

空节无数据,需要在PE头将其内存映射准备好即可

4、写数据节(.DataSec)

将0x03节的压缩数据放至该节,并使用文件对其的方式进行对齐

//写数据节
WriteFile(hFile, m_pComData, m_dwComDataSize, &dwBytesWrited, NULL);
// 填充到 .PackSec 文件偏移(对齐到 FileAlignment)
DWORD dwPadSize = m_pNewSecHeader[2].PointerToRawData - (m_pNewSecHeader[1].PointerToRawData + m_pNewSecHeader[1].SizeOfRawData);
if (dwPadSize > 0) {
    LPBYTE pPad = new BYTE[dwPadSize];
    ZeroMemory(pPad, dwPadSize);
    WriteFile(hFile, pPad, dwPadSize, &dwBytesWrited, NULL);
    delete[] pPad;
}
5、写壳节(.PackSec)

壳代码需要将0x04章节的数据生成干净的代码

需要做以下步骤,确保代码入口点为自己的入口代码

  • 开启release
  • 随机基址打开
  • 链接器->高级->添加入口函数
  • 链接器->高级->关闭数据执行保护(关闭DEP)
  • C++->代码生成->禁用安全检查(关闭GS)
  • 确保自己的函数位于代码段的开头,函数处添加extern "C" __declspec(code_seg(".text$a")) Entry()

最终的代码节如图所示

将生成的代码拷贝为C++代码

const uint8_t data[1496] = {
0x55, 0x8B, 0xEC, 0x83, 0xEC, 0x44, 0x8D, 0x45, 0xBC, 0x50, 0xE8, 0x51, 0x04, 0x00, 0x00, 0x83,
0xC4, 0x04, 0xE8....}

m_dwDeComCodeSize = sizeof(data);
m_pDeComCode = new BYTE[m_dwDeComCodeSize];
memcpy(m_pDeComCode, data, m_dwDeComCodeSize);

//写代码节
WriteFile(hFile, m_pDeComCode, m_dwDeComCodeSize, &dwBytesWrited, NULL);
CloseHandle(hFile);

完成后,即可完成一个简单的加壳程序

0x06 演示效果

1、静态PE效果

节数量

数据目录表

节表

有三个节,分别为代码节,数据节和壳节

节数据有两个节

1、数据节(原始PE的压缩数据)

2、代码节(壳代码)

2、动态运行效果

内存布局

EIP指针指向.PackSec的入口

参照源码进行对照,前两个为初始化环境变量和获取用户模块基址

跟进初始换环境变量函数

获取kernel32模块基址

跟进调试,成功获取Kernel32基址

将局部变量存入栈

下边获取各个函数地址

GetProcAddress

LoadLibrary

VirtualAlloc

VirtualProtect

使用LoadLibrary加载cabinet库

获取CreateDecompressor函数

获取Decompress函数

下一步获取模块基址

内存如图所示

下一步解析PE数据,找到压缩数据入口和对应的大小,并对压缩数据进行解压缩

DosHeader + 3C的位置得到NTHeader

通过NTHeader的可选头位置+可选头大小得到节表起始位置

获取压缩数据的起始位置,大小

使用函数分配内存,并使用解压函数,将解压后的数据放至内存

成功分配内存,内存地址为0x001D0000

该内存暂时为空数据

后边分别调用创建解压器工具和进行解压操作

DeCompressor创建成功

下一步调用DeCompress函数,将数据段中的数据解压缩至0x001D0000位置处

下一步为模拟操作系统进行加载PE操作

加载PE分为一下几步

  • 分配内存
  • 在内存中按照格式映射镜像
  • 修复导入表
  • 修复重定位表
  • 将指针执行代码入口点

1、分配内存

首先尝试在原始PE的ImageBase上分配内存,分配失败则自动分配

调试中,发现需要分配的内存地址为0x00400000

该地址分配失败

自动分配的地址为0x00560000

目前该内存区域为空

2、在内存中按照格式映射镜像

下一步,我们需要对文件头和节数据进行依次拷贝


文件头包括Dos头,DosStub,NT头和节表几块

根据映射图,这几块内存在内存和文件中的位置一致,我们只需要获取到SizeOfHeaders这个字段值,并将文件头开始指针,和长度进行拷贝即可

该PE的SizeOfHeaders值为0x400,可以看到0x400以上的内容都为文件头的数据

进行memcpy的调用,源地址为PE文件映射首地址0x001D0000,目的地址为新内存加载地址0x00560000,大小为0x400

拷贝后的内存映射情况

下一步为节数据的拷贝

我们需要根据节表的VirtualAddress、SizeOfRawData和PointerToRawData三个字段进行计算,将PointerToRawData的指向的起始数据,拷贝SizeOfRawData的长度到内存基址+VirtualAddress的位置

继续调试跟进内存

第一个节为.textbass段,数据大小为0,不拷贝数据

第二个节为.text段,需要拷贝的内存地址为0x00560000+11000h = 0x00571000,拷贝大小为6800h

观察内存变化

和文件中的数据相同

依次循环节表,将全部节数据拷贝过去

3、修复导入表和重定位表

将PE文件映射进内存后,并不能够直接使用,需要修复多个表,包括全局变量,使用的库函数,使用绝对地址的汇编代码,本文简单修复一下必要的导入表和重定位表,tls表,延迟导入表,绑定导入表等感兴趣的同学可以尝试一下

导入表的修复,需要遍历导入描述符表,遍历获取对应的Name,并通过LoadLibrary加载DLL,获取新的模块基址,并通过导入名称表(INT)按函数名 / 序号获取函数真实地址,逐一填入 IAT 对应位置

LoadLibrary函数加载Kernel32.dll的模块基址

成功获取到模块基址

后续通过INT的序号或者名字把IAT填充

使用GetProcAddress函数,依次将IAT填充

获取GetFileSize的函数地址

通过汇编,查看IAT地址位置ds:[edi]处,查看到0x75BC33A0为第一个函数CreateFileA的函数地址,0x75BC35D0为第二个函数GetFileSize的地址

按照这个逻辑,逐渐填充所有用到的IAT地址

下一步为重定位表的修复

计算的偏移值为上方的dwRelocBase的值

定位到重定位表,依次修复即可

5、将指针执行代码入口点

最后返回入口点,并使用汇编跳转至入口点


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 4
活跃值: (107)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
源代码程序,欢迎各位大牛指导!
上传的附件:
20小时前
0
游客
登录 | 注册 方可回帖
返回