首页
社区
课程
招聘
[原创]I Pack You:实现基本的软件壳框架
发表于: 1天前 506

[原创]I Pack You:实现基本的软件壳框架

1天前
506

I Pack You:实现基本的软件壳框架

记录一下自己开发简易软件壳的过程,参考的开源软件和书籍贴在文末。

这只是一个简易的加壳工具,实现了加壳的基本框架,后面会增加更多的功能填充这个框架。

实现思路

这个壳全部使用c/c++进行编写,没有使用汇编。因为在x64下,visual c++不支持内联汇编的语法了。如果要使用汇编,需要将汇编代码作为一个模块供c/c++使用。

软件壳分为两个部分:加壳程序和stub。其中stub以dll的形式存在,使用链接器指令将一些节区融合在一起(参考《加密与解密》)。加壳程序对目标PE文件进行加密和变换后,会提取Stub.dll的.text节区,写入目标文件的特定节区中。通过在dll中导出一些变量,实现加壳程序和stub之间的通信。

另外对于Stub.dll,我们需要修改vs默认的编译选项使得.text的代码移植到目标PE文件后可以正常运行,这点后面会说。

异常处理

为了优化程序结构,我自定义了一些异常类供程序使用:

#pragma once

#include <Windows.h>

#include <string>
#include <stdexcept>

class MyException : public std::runtime_error
{
public:
    explicit MyException(const std::string &msg) : std::runtime_error(msg) {}
};

class not_pe_format : public MyException
{
public:
    explicit not_pe_format(const std::string &msg) : MyException(msg) {}
};

class winapi_failed : public MyException
{
public:
    explicit winapi_failed(const std::string &msg)
        : MyException(std::string(msg).append(" : " + std::to_string(GetLastError()))) {}
};

winapi_failed类的构造函数中会自动将自定义的错误消息和win32错误码合并,在throw的时候就可以直接知道错误码了。

本来还实现了一个通用的win32错误码格式化工具类的,感觉没什么用,砍掉了。也贴出来给大家参考一下:

#include "ErrorPrinter.h"

// ANSI版本
void ErrorPrinter::PrintErrorA(const std::string& context, DWORD errorCode)
{
    // 获取Windows系统错误描述
    LPSTR lpMsgBuf = nullptr;
    DWORD bufLen = FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr,
        errorCode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPSTR)&lpMsgBuf,
        0,
        nullptr);
    
    std::cerr << "错误: " << context << std::endl;
    std::cerr << "错误代码: " << errorCode 
              << " (0x" << std::hex << errorCode << std::dec << ")" << std::endl;
    
    if (bufLen > 0 && lpMsgBuf != nullptr)
    {
        // 移除结尾的换行符
        std::string message(lpMsgBuf, bufLen);
        while (!message.empty() && 
               (message.back() == '\n' || message.back() == '\r' || message.back() == ' '))
        {
            message.pop_back();
        }
        
        std::cerr << "错误描述: " << message << std::endl;
        LocalFree(lpMsgBuf);
    }
    else
    {
        std::cerr << "无法获取错误描述" << std::endl;
    }
}

// Unicode/Wide版本
void ErrorPrinter::PrintErrorW(const std::wstring& context, DWORD errorCode)
{
    // 获取Windows系统错误描述
    LPWSTR lpMsgBuf = nullptr;
    DWORD bufLen = FormatMessageW(
        FORMAT_MESSAGE_ALLOCATE_BUFFER |
        FORMAT_MESSAGE_FROM_SYSTEM |
        FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr,
        errorCode,
        MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
        (LPWSTR)&lpMsgBuf,
        0,
        nullptr);
    
    std::wcerr << L"错误: " << context << std::endl;
    std::wcerr << L"错误代码: " << errorCode 
               << L" (0x" << std::hex << errorCode << std::dec << L")" << std::endl;
    
    if (bufLen > 0 && lpMsgBuf != nullptr)
    {
        // 移除结尾的换行符
        std::wstring message(lpMsgBuf, bufLen);
        while (!message.empty() && 
               (message.back() == L'\n' || message.back() == L'\r' || message.back() == L' '))
        {
            message.pop_back();
        }
        
        std::wcerr << L"错误描述: " << message << std::endl;
        LocalFree(lpMsgBuf);
    }
    else
    {
        std::wcerr << L"无法获取错误描述" << std::endl;
    }
}

PEFile模块

主要用于获取目标PE文件的各种信息以及对其进行修改。

#pragma once

#include <Windows.h>

#include <string>

// 直接分配SizeOfImage大小的内存,模仿加载器对PE文件进行读取

class PEFile
{
public:
    PEFile();

    ~PEFile();

    void Open(const std::wstring& fileName);

    PIMAGE_DOS_HEADER GetDosHeader() const;

    PIMAGE_NT_HEADERS GetNtHeaders() const;

    PIMAGE_OPTIONAL_HEADER GetOptHeader() const;

    ULONGLONG GetPEImageBase() const;

    PIMAGE_DATA_DIRECTORY GetDataDirectory(UINT index) const;

    PIMAGE_SECTION_HEADER GetSectionHeaders();

    // 返回新节区的RVA
    DWORD AddSection(const std::string& name, DWORD dwSize);

    void SetEntryPointer(DWORD dwEP);

    // 写入新文件,一般最后调用
    void WriteToFile();

private:
    bool IsPEFile() const;

    void ReadAsLoader();

private:
    std::wstring m_fileName;

    HANDLE m_hFile;
    PBYTE m_pImageBase;

    PBYTE m_pOverlayData;
};

该模块模仿了Windows程序加载器对PE文件的加载操作。这样做的好处是可以直接使用RVA进行地址的计算,而不必理会FOA。另外映射的时候还需要注意读取文件末尾的OverlayData。

先前我考虑过使用file-mapping来映射PE文件,因为file-mapping可以像加载器一样映射PE文件,这样就不必手动映射每一个节区。这需要在CreateFileMapping的fdwProtect参数中传递 | SEC_IMAGE。使用file-mapping还可以在取消映射之后自动dump回磁盘中。对于大文件的处理,file-mapping可以提升效率。这样做虽然省下了手动映射的步骤,但是正如前面所说,使用SEC_IMAGE会使得MapViewOfFile像加载器一样映射(加载)PE文件,对于.text、.idata等类似的段,我们不能直接对其进行修改,需要使用VirtualProtect修改页面保护属性之后才可以修改这些段。所以我还是决定自己手动映射PE文件。

当然在映射之前,应该检查一下PE文件的有效性,一般来说,检查MZ和PE就可以。

注意PEFile的成员变量中,并没有直接保存PE文件的各种字段地址,只保存了映射的首地址,因为我们需要对PE文件进行频繁的操作,一些字段的地址可能会发生变化,如果忘记了更新对应的成员变量,后续的操作就会发生错误。所以最好是只保存一个基址,其它的字段在需要的时候通过函数动态获取。

新建节区以存储stub

存储位置 实现方式 技术特点 优缺点分析
新增节区
在原有 PE 节区末尾新增一个或多个自定义节(如 .upx0, .pack)。 1. 代码与数据混存:Stub 的解密逻辑、压缩数据、IAT 修复代码通常打包在一起。
2. 节属性伪装:将代码节标记为可读/写/执行(RWX),或拆分为数据节(RWD)和代码节(RX)。
优点:易于定位和管理,不破坏原文件结构。
缺点:新增的节名和异常属性(如 RWX)容易被识别为壳特征。
节区缝隙填充
利用 PE 节区对齐(Section Alignment)产生的“缝隙”(Slack Space)。 1. 空间复用:文件对齐(File Alignment)通常为 512 字节,而内存对齐为 4096 字节,中间存在大量未使用的零空间。
2. 隐写术:将 Stub 的指令或数据拆散,填充到这些缝隙中。
优点:极难通过静态文件大小或节区列表发现,抗查杀能力强。
缺点:开发复杂度高,可利用空间有限。
覆盖资源节
替换或加密原程序的资源段(.rsrc),将 Stub 藏在资源数据中。 1. 借壳生蛋:利用资源目录结构的复杂性,将 Stub 代码伪装成图标、位图或版本信息的数据。
2. 运行时解压:程序启动时,Stub 先从资源中解压出真正的资源数据,再执行自身。
优点:绕过对代码节的直接扫描。
缺点:资源操作逻辑复杂,易导致程序兼容性问题。
覆盖代码节
直接覆盖原程序的代码节(.text),将原代码压缩/加密后附在文件末尾。 1. 鸠占鹊巢:程序入口点(OEP)指向被覆盖的 Stub 代码。
2. 尾部存储:原程序的全部代码和数据被当作“附加数据”处理。
优点:文件结构改动最小,看起来像一个正常的 PE 文件。
缺点:原代码节必须有足够空间容纳 Stub,否则需要扩展节区,容易被检测。

我们采用新增节区的方式。因为对齐的需要,文件头末尾通常留有一定空间,这为我们追加新的节区提供了便利。当然最好还是检查一下是否有空闲空间。新的节区将用于存储stub的.text段。

DWORD PEFile::AddSection(const std::string& name, DWORD dwSize)
{
    PIMAGE_NT_HEADERS pNtHeaders = GetNtHeaders();
    DWORD dwSizeOfRawData = 0, dwVirtualSize = 0;
    // 计算对齐后的内存大小和文件大小
    // 计算节区对齐后的原始大小
    if (dwSize % pNtHeaders->OptionalHeader.FileAlignment)
    {
        dwSizeOfRawData = (dwSize / pNtHeaders->OptionalHeader.FileAlignment + 1) *
                          pNtHeaders->OptionalHeader.FileAlignment;
    }
    else
    {
        dwSizeOfRawData = (dwSize / pNtHeaders->OptionalHeader.FileAlignment) *
                          pNtHeaders->OptionalHeader.FileAlignment;
    }

    // 计算节区对齐后的内存大小
    if (dwSize % pNtHeaders->OptionalHeader.SectionAlignment)
    {
        dwVirtualSize = (dwSize / pNtHeaders->OptionalHeader.SectionAlignment + 1) *
                        pNtHeaders->OptionalHeader.SectionAlignment;
    }
    else
    {
        dwVirtualSize = (dwSize / pNtHeaders->OptionalHeader.SectionAlignment) *
                        pNtHeaders->OptionalHeader.SectionAlignment;
    }

    // 添加节表项
    PIMAGE_SECTION_HEADER pLastSecHeader = &(GetSectionHeaders()[pNtHeaders->FileHeader.NumberOfSections - 1]);
    PIMAGE_SECTION_HEADER pNewSectionHeader = pLastSecHeader + 1;

    // 填充新的节表项
    std::memset(pNewSectionHeader, 0, sizeof(IMAGE_SECTION_HEADER));
    std::memcpy(pNewSectionHeader->Name, name.c_str(), IMAGE_SIZEOF_SHORT_NAME); // 设置新节区的名字

    pNewSectionHeader->Misc.VirtualSize = dwVirtualSize;
    pNewSectionHeader->VirtualAddress = pNtHeaders->OptionalHeader.SizeOfImage;
    pNewSectionHeader->SizeOfRawData = dwSizeOfRawData;

    LARGE_INTEGER fileSize;
    GetFileSizeEx(m_hFile, &fileSize);
    pNewSectionHeader->PointerToRawData = fileSize.QuadPart;
    pNewSectionHeader->Characteristics = 0xE0000040;

    // 修正nt头
    pNtHeaders->FileHeader.NumberOfSections++;
    pNtHeaders->OptionalHeader.SizeOfHeaders += sizeof(IMAGE_SECTION_HEADER);
    DWORD dwSizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage; // 用于拷贝原来的数据
    pNtHeaders->OptionalHeader.SizeOfImage += dwVirtualSize;

    // 分配更大的内存空间
    auto pNewImageBase = new byte[dwSizeOfImage + dwVirtualSize]();
    std::memcpy(pNewImageBase, m_pImageBase, dwSizeOfImage);

    // 清理原内存空间
    DWORD dwNewSectionRVA = pNewSectionHeader->VirtualAddress;
    delete[] m_pImageBase;

    // 更新指针
    m_pImageBase = pNewImageBase;

    return dwNewSectionRVA;
}

写回文件

我们将修改后的PE文件作为原文件的副本写回磁盘中,需要注意的是别忘了写回overlay数据:

void PEFile::WriteToFile()
{
    if (m_pImageBase == nullptr)
    {
        throw std::runtime_error("PEFile: 没有加载PE文件");
    }

    // 构建新文件名:在原文件名基础上添加 "_pack"
    std::wstring newFileName = m_fileName;

    // 查找最后一个点(文件扩展名分隔符)
    size_t dotPos = newFileName.find_last_of(L".");
    if (dotPos != std::wstring::npos)
    {
        // 在扩展名前插入 "_pack"
        newFileName.insert(dotPos, L"_pack");
    }
    else
    {
        // 如果没有扩展名,直接在末尾添加 "_pack"
        newFileName += L"_pack";
    }

    // 创建新文件
    HANDLE hNewFile = CreateFileW(
        newFileName.c_str(),
        GENERIC_WRITE,
        0,
        nullptr,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        nullptr);

    if (hNewFile == INVALID_HANDLE_VALUE)
    {
        throw winapi_failed("CreateFileW Failed for new file");
    }

    try
    {
        PIMAGE_DOS_HEADER pDosHeader = GetDosHeader();
        PIMAGE_NT_HEADERS pNtHeaders = GetNtHeaders();
        PIMAGE_SECTION_HEADER pSecHeaders = GetSectionHeaders();

        DWORD bytesWritten = 0;

        // 1. 写入DOS头和PE头
        DWORD sizeOfHeaders = pNtHeaders->OptionalHeader.SizeOfHeaders;
        if (!WriteFile(hNewFile, m_pImageBase, sizeOfHeaders, &bytesWritten, nullptr))
        {
            throw winapi_failed("WriteFile Failed for headers");
        }

        // 2. 写入各个节区
        for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++)
        {
            // 定位到当前写入位置(节区的文件偏移)
            LARGE_INTEGER filePos;
            filePos.QuadPart = pSecHeaders[i].PointerToRawData;

            if (SetFilePointerEx(hNewFile, filePos, nullptr, FILE_BEGIN) == FALSE)
            {
                throw winapi_failed("SetFilePointerEx Failed for section positioning");
            }

            // 写入节区数据
            DWORD dataSize = pSecHeaders[i].SizeOfRawData;
            DWORD virtualAddress = pSecHeaders[i].VirtualAddress;

            if (dataSize > 0)
            {
                if (!WriteFile(hNewFile, m_pImageBase + virtualAddress, dataSize, &bytesWritten, nullptr))
                {
                    throw winapi_failed("WriteFile Failed for section data");
                }
            }
        }

        // 3. 写入额外数据(Overlay)
        if (m_pOverlayData != nullptr)
        {
            // 计算Overlay数据的起始位置
            PIMAGE_SECTION_HEADER pLastSec = &pSecHeaders[pNtHeaders->FileHeader.NumberOfSections - 1];
            DWORD overlayOffset = pLastSec->PointerToRawData + pLastSec->SizeOfRawData;

            LARGE_INTEGER overlayPos;
            overlayPos.QuadPart = overlayOffset;

            if (SetFilePointerEx(hNewFile, overlayPos, nullptr, FILE_BEGIN) == FALSE)
            {
                throw winapi_failed("SetFilePointerEx Failed for overlay positioning");
            }

            // 计算Overlay数据大小
            LARGE_INTEGER originalFileSize;
            if (!GetFileSizeEx(m_hFile, &originalFileSize))
            {
                throw winapi_failed("GetFileSizeEx Failed for overlay size calculation");
            }

            DWORD overlaySize = static_cast<DWORD>(originalFileSize.QuadPart - overlayOffset);

            if (overlaySize > 0)
            {
                if (!WriteFile(hNewFile, m_pOverlayData, overlaySize, &bytesWritten, nullptr))
                {
                    throw winapi_failed("WriteFile Failed for overlay data");
                }
            }
        }

        // 4. 设置文件结束位置
        LARGE_INTEGER finalSize;
        finalSize.QuadPart = 0;
        if (SetFilePointerEx(hNewFile, finalSize, nullptr, FILE_END) == FALSE)
        {
            throw winapi_failed("SetFilePointerEx Failed for setting file end");
        }

        if (!SetEndOfFile(hNewFile))
        {
            throw winapi_failed("SetEndOfFile Failed");
        }

        // 刷新文件缓冲区
        if (!FlushFileBuffers(hNewFile))
        {
            throw winapi_failed("FlushFileBuffers Failed");
        }

        CloseHandle(hNewFile);

        // 输出成功信息
        std::cout << "Written!" << std::endl;
    }
    catch (...)
    {
        // 发生异常时关闭文件句柄
        CloseHandle(hNewFile);
        throw;
    }
}

Packer模块

这个模块实现了加壳操作。

class Packer
{
public:
    Packer(const std::string& stubDllName);
    ~Packer();

    void Pack(std::shared_ptr<PEFile> targetFile);

private:
    // 转移并加密导入表,将导入表移动到dwRVA处
    void HideImportTable();

    // 先不考虑aslr的情况
    void FixStubReloc(DWORD dwNewSecRVA);

    PIMAGE_SECTION_HEADER FindStubTextHeader();

    void WriteStub(DWORD dwStubSecRVA);

    void SetParam();

    void SaveOEP();

    void SetEP(DWORD dwStubSecRVA);
    
private:
    std::shared_ptr<PEFile> m_targetFile;
    HMODULE m_hStubDll;

    PBYTE m_pStubDll;
};

隐藏导入表

关于导入表可以参考我的这两篇文章:

PE加密壳处理导入表的逻辑是破坏静态结构、延迟动态重建。其根本目的是切断静态分析工具直接获取API调用线索的路径,同时保证程序在运行时能正常执行。

处理导入表的方式有很多种,这里说说一些常见的做法:

处理阶段 目标 技术手段 对分析/运行的影响 对抗对象
静态破坏
(磁盘文件)
隐藏、误导
切断静态分析线索
1. 清空/截断IID:抹去PE头中原始导入描述符。
2. 加密IAT:加密IAT中的地址数据。
3. 替换为壳表:仅保留壳自身所需API的导入表。
静态分析工具失效
(如PEiD, LordPE)无法直接列出程序真实调用的API,增加逆向起点难度。
逆向分析者、
静态扫描工具
动态重建
(内存运行)
延迟、按需恢复
保证程序正常运行
1. 模拟加载器:壳代码调用GetProcAddress,手动填充内存IAT。
2. 延迟加载:仅在API首次被调用前才解析其地址。
3. API哈希:存储API的哈希值而非字符串,运行时比对获取地址。
程序功能正常
运行时代码能通过IAT正确调用系统API,但重建时机可控,增加动态分析复杂度。
动态调试、
脱壳时机捕捉
高级混淆
(抗修复)
干扰、绑定
防止自动化脱壳修复
1. IAT Hook/代理跳板:IAT填入壳代码地址,调用前先执行壳逻辑。
2. 移动IAT:将IAT重定位到堆或新节区,破坏标准PE结构。
3. 代码虚拟化:将call [IAT]等指令转换为虚拟机字节码,消除直接调用痕迹。
自动化修复失败
脱壳工具(如ImportREC)抓取的IAT可能指向壳代码,导致修复后的程序无法运行。
自动化脱壳工具、
IAT修复脚本

**为了尽快搭建整个加壳框架,我们只是简单地将导入表进行加密,并清空导入表的数据目录项和所在的空间,再将加密后的导入表放到.pack节区中进行保存,供stub修复iat使用。**后续会使用其它技术对导入表进行处理。

void Packer::HideImportTable()
{
    PIMAGE_DATA_DIRECTORY pImportTable = m_targetFile->GetDataDirectory(IMAGE_DIRECTORY_ENTRY_IMPORT);

    // 清空导入表数据目录项
    DWORD dwImportTableRVA = pImportTable->VirtualAddress;
    DWORD dwImportTableSize = pImportTable->Size;

    pImportTable->VirtualAddress = 0;
    pImportTable->Size = 0;

    PBYTE pImageBase = reinterpret_cast<PBYTE>(m_targetFile->GetDosHeader());

    // 复制原导入表
    auto pNewImportTable = new byte[dwImportTableSize]();
    std::memcpy(pNewImportTable, pImageBase + dwImportTableRVA, dwImportTableSize);
    
    // 销毁原导入表
    std::memset(pImageBase + dwImportTableRVA, 0, dwImportTableSize);

    // 加密导入表
    for (int i = 0;i < dwImportTableSize;i++)
    {
        pNewImportTable[i] ^= key;
    }

    // 转移导入表到stub中
    PIMPT pImpt = reinterpret_cast<PIMPT>(GetProcAddress(m_hStubDll, "g_importTable"));
    if (!pImpt)
    {
        delete[] pNewImportTable;
        throw winapi_failed("getting g_importTable failed");
    }
    
    if (IMPT_SIZE >= dwImportTableSize)
    {
        std::memcpy(pImpt->pData, pNewImportTable, dwImportTableSize);
        pImpt->dwSize = dwImportTableSize;

        pImpt->dwRVA = dwImportTableRVA;
        pImpt->dwSize = dwImportTableSize;
    }
    else
    {
        delete[] pNewImportTable;
        throw std::runtime_error("the size of impt too small");
    }

    delete[] pNewImportTable;
}

修复Stub的重定位表项

关于重定位表的结构,可以看看我的另一篇文章PE文件之重定位表

stub.dll中的一些操作使用的是VA,这些VA原来是加载器进行修正的,但是目标PE文件中并没有Stub.dll的重定位表。我考虑过合并stub和目标PE文件的重定位表,这样加载器就可以帮我们处理stub的重定位信息。但是这样做有些麻烦:

  • 首先是BASERELOC指定的空间中没有足够的容量装得下stub的重定位表。不过好在PE文件中会有许多空洞,我们可以把BASERELOC移植到cave中。
  • 假定有了足够的空间容纳stub的重定位表,在合并之前还需要剔除不属于.text的重定位信息。由于重定位信息是以页面为单位进行组织的,要判断一个地址是否在.text中还是有些麻烦的。
  • 假定剔除了无关的重定位地址,在合并之前还需要修改stub重定位表的页基址(因为.text是追加到目标PE文件中的),并判断需要移植的重定位表的页基址是否和目标PE文件的重定位表的页基址冲突。

所以我决定直接在加壳的时候就手动修复stub的重定位信息。由于我们将Stub.dll的.text复制到了.pack中,所以在stub中进行的直接寻址需要根据.pack节区的rva进行修正:

void Packer::FixStubReloc(DWORD dwNewSecRVA)
{
    auto pDosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(m_pStubDll);
    auto pNtHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>((PBYTE)pDosHeader + pDosHeader->e_lfanew);
    PIMAGE_SECTION_HEADER pSecHeaders = IMAGE_FIRST_SECTION(pNtHeaders);
    
    // 定位到数据目录项中的重定位表
    PIMAGE_DATA_DIRECTORY pDataDirs = pNtHeaders->OptionalHeader.DataDirectory;
    auto pRelocDir = &pDataDirs[IMAGE_DIRECTORY_ENTRY_BASERELOC];
    // 得到重定位表
    auto pRelocTable = reinterpret_cast<PIMAGE_BASE_RELOCATION>(m_pStubDll + pRelocDir->VirtualAddress);

    // 新的加载基址和原来的加载基址
    ULONGLONG newImageBase = m_targetFile->GetPEImageBase();
    ULONGLONG originImageBase = pNtHeaders->OptionalHeader.ImageBase;

    // 得到.text节区的起始地址
    PIMAGE_SECTION_HEADER pTextHeader = FindStubTextHeader();

    typedef struct
    {
        WORD OffsetInPage : 12;
        WORD Type : 4;
    } TypeOffset, *PTypeOffset;

    /*
    下面开始对修正stub中的重定位信息,直接对重定位表项指向的地址进行修复
    我们只关心.text中需要重定位的地址,因为我们只追加了.text节。
    */

    while (pRelocTable->VirtualAddress) // 遍历重定位表
    {
        DWORD dwPageRVA = pRelocTable->VirtualAddress;
        PTypeOffset pOffsets = reinterpret_cast<PTypeOffset>(pRelocTable + 1);
        // 得到该页内需要重定位的偏移地址数目
        DWORD dwOffsetNums = (pRelocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(TypeOffset);

        for (size_t i = 0; i < dwOffsetNums; i++)
        {
            if (*(PULONGLONG)(&pOffsets[i]) == NULL)
                break;
            ULONGLONG dwRVA = dwPageRVA + pOffsets[i].OffsetInPage;
            PULONGLONG pRelocAddr = (PULONGLONG)((ULONGLONG)m_pStubDll + dwRVA);
            // 修复重定位信息   公式:需要修复的地址-原映像基址-原区段基址+现区段基址+现映像基址
            ULONGLONG dwRelocCode = *pRelocAddr - originImageBase - pTextHeader->VirtualAddress +
                                    dwNewSecRVA + newImageBase;
            *pRelocAddr = dwRelocCode;
        }
        pRelocTable = (PIMAGE_BASE_RELOCATION)((ULONGLONG)pRelocTable + pRelocTable->SizeOfBlock);
    }
}

Stub.dll

融合区段

为了便于Packer提取stub代码,我们使用下面的指令融合区段并修改区段属性:

#pragma comment(linker, "/merge:.data=.text")
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,RWE")

初始化

由于我们破坏的导入表,程序运行所需的模块都没有被加载的内存中,壳代码首先要做的便是搭建基础的运行环境。

原理可以参考我的这篇文章:64位Windows中使用PEB来获取进程加载的模块

头文件:

#pragma once

#include <Windows.h>

// 偏移量定义
#define OFFSET_PEB_IN_TEB 0x60
#define OFFSET_ImageBaseAddress_IN_PEB 0x10
#define OFFSET_LDR_IN_PEB 0x18
#define OFFSET_InLoadOrderModuleList_IN_LDR 0x10
#define OFFSET_InLoadOrderLinks_IN_LDR_DATA_TABLE_ENTRY 0x10
#define OFFSET_DllBase_IN_LDR_DATA_TABLE_ENTRY 0x30
#define OFFSET_BaseDllName_IN_LDR_DATA_TABLE_ENTRY 0x58

typedef HMODULE(WINAPI *fnLoadLibraryA)(LPCSTR);
typedef FARPROC(WINAPI *fnGetProcAddress)(HMODULE, LPCSTR);
typedef INT(WINAPI *fnMessageBoxW)(HWND, LPCWSTR, LPCWSTR, UINT);
typedef VOID(WINAPI *fnExitProcess)(UINT);
typedef BOOL(WINAPI *fnVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldProtect);
typedef SIZE_T(WINAPI *fnVirtualQuery)(LPCVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer, SIZE_T dwLength);

// 需要用到的模块基址
extern ULONGLONG g_ImageBase;
extern ULONGLONG g_kernel32ImageBase;
extern HMODULE g_hUser32;

// 常用函数
extern fnLoadLibraryA g_MyLoadLibrary;
extern fnGetProcAddress g_MyGetProcAddress;

extern fnMessageBoxW g_MyMessageBoxW;
extern fnExitProcess g_MyExitProcess;
extern fnVirtualProtect g_MyVirtualProtect;
extern fnVirtualQuery g_MyVirtualQuery;

int MyStrCmp(const char *p, const char *q);

bool Init();

在实现的时候使用了PEB中的ImageAddressBase字段来作为基址,如果使用PE中的ImageBase,如果启用了ARLS机制,壳代码就会报内存访问错误。另外我们使用了SEH来处理异常,否则初始化失败的时候程序会卡死一段时间,虽然无伤大雅,但也让人难受:

#include "pch.h"

#include "init.h"

// 需要用到的模块基址
ULONGLONG g_ImageBase;
ULONGLONG g_kernel32ImageBase;
HMODULE g_hUser32;

// 常用函数
fnLoadLibraryA g_MyLoadLibrary;
fnGetProcAddress g_MyGetProcAddress;
fnMessageBoxW g_MyMessageBoxW;
fnExitProcess g_MyExitProcess;
fnVirtualProtect g_MyVirtualProtect;
fnVirtualQuery g_MyVirtualQuery;

// 获取当前模块和kernel32模块的基址
static ULONGLONG GetKernel32Addr()
{
    _TEB *pTeb = NtCurrentTeb();
    ULONGLONG pPeb = *(PULONGLONG)((ULONGLONG)pTeb + OFFSET_PEB_IN_TEB);

    // 初始化sg_ImageBase
    sg_ImageBase = *(PULONGLONG)(pPeb + OFFSET_ImageBaseAddress_IN_PEB);

    // 获取Ldr
    PULONGLONG pLdr = (PULONGLONG) * (PULONGLONG)(pPeb + OFFSET_LDR_IN_PEB);

    // 获取InLoadOrderModuleList
    PLIST_ENTRY InLoadOrderModuleList = (PLIST_ENTRY)((ULONGLONG)pLdr + OFFSET_InLoadOrderModuleList_IN_LDR);

    // 直接取第三个模块(假设:exe->ntdll->kernel32)
    PLIST_ENTRY pModuleExe = InLoadOrderModuleList->Flink; // 第一个:exe模块
    PLIST_ENTRY pModuleNtdll = pModuleExe->Flink;		   // 第二个:ntdll
    PLIST_ENTRY pModuleKernel32 = pModuleNtdll->Flink;	   // 第三个:kernel32

    // 转换为LDR_DATA_TABLE_ENTRY指针
    // LIST_ENTRY是嵌入在LDR_DATA_TABLE_ENTRY结构中的,计算结构体起始地址
    ULONGLONG ullKernel32Base = *(PULONGLONG)((ULONGLONG)pModuleKernel32 + OFFSET_DllBase_IN_LDR_DATA_TABLE_ENTRY);

    sg_kernel32ImageBase = ullKernel32Base;

    // 获取DllBase
    return ullKernel32Base;
}

int MyStrCmp(const char *p, const char *q)
{
    while (*p == *q && *p != '\0')
    {
        p++;
        q++;
    }
    return *p - *q;
}

static ULONGLONG InitGetProcAddress(ULONGLONG ullKernel32Base)
{
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)ullKernel32Base;
    PIMAGE_NT_HEADERS64 pNtHeader = (PIMAGE_NT_HEADERS64)(ullKernel32Base + pDosHeader->e_lfanew);

    // 获取导出表
    PIMAGE_DATA_DIRECTORY pExpDir = &(pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]);
    PIMAGE_EXPORT_DIRECTORY pExpTable = (PIMAGE_EXPORT_DIRECTORY)(ullKernel32Base + pExpDir->VirtualAddress);

    // 遍历导出表,得到GetProcAddress

    DWORD dwNumberOfNames = pExpTable->NumberOfNames;
    // 导出函数地址表VA
    PDWORD pdwAddrOfFuncs = (PDWORD)(ullKernel32Base + pExpTable->AddressOfFunctions);
    // 函数名称地址表VA
    PDWORD pdwAddrOfNames = (PDWORD)(ullKernel32Base + pExpTable->AddressOfNames);
    // 函数序号地址表
    PWORD pdwAddrOfOrdinals = (PWORD)(ullKernel32Base + pExpTable->AddressOfNameOrdinals);

    const char targetName[] = "GetProcAddress";
    for (int i = 0; i < dwNumberOfNames; i++)
    {
        LPCSTR name = (LPCSTR)(ullKernel32Base + pdwAddrOfNames[i]);
        if (!MyStrCmp(name, targetName))
        {
            g_MyGetProcAddress = (fnGetProcAddress)(ullKernel32Base + pdwAddrOfFuncs[pdwAddrOfOrdinals[i]]);
            return ullKernel32Base + pdwAddrOfFuncs[pdwAddrOfOrdinals[i]];
        }
    }

    return 0;
}

bool Init()
{
    __try
    {
        // 获取kernel32基址
        sg_kernel32ImageBase = GetKernel32Addr();
        if (!sg_kernel32ImageBase)
        {
            __leave;
        }

        // 获取GetProcAddress
        g_MyGetProcAddress = (fnGetProcAddress)InitGetProcAddress(sg_kernel32ImageBase);
        if (!g_MyGetProcAddress)
        {
            __leave;
        }

        // 获取其他需要的函数
        g_MyLoadLibrary = (fnLoadLibraryA)g_MyGetProcAddress((HMODULE)sg_kernel32ImageBase, "LoadLibraryA");
        if (!g_MyLoadLibrary)
        {
            __leave;
        }

        sg_hUser32 = g_MyLoadLibrary("user32");
        if (!sg_hUser32)
        {
            __leave;
        }
        g_MyMessageBoxW = (fnMessageBoxW)g_MyGetProcAddress(sg_hUser32, "MessageBoxW");
        if (!g_MyMessageBoxW)
        {
            __leave;
        }

        g_MyExitProcess = (fnExitProcess)g_MyGetProcAddress((HMODULE)sg_kernel32ImageBase, "ExitProcess");
        if (!g_MyExitProcess)
        {
            __leave;
        }

        g_MyVirtualProtect = (fnVirtualProtect)g_MyGetProcAddress(
            (HMODULE)sg_kernel32ImageBase, "VirtualProtect");
        if (!g_MyExitProcess)
        {
            __leave;
        }

        g_MyVirtualQuery = (fnVirtualQuery)g_MyGetProcAddress((HMODULE)sg_kernel32ImageBase, "VirtualQuery");
        if (!g_MyVirtualQuery)
        {
            __leave;
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        return false;
    }

    return true;
}

修复IAT

这里模仿加载器对IAT进行修复,需要注意的是在修复的时候需要修改对应地址的页属性:

static void FixImportTable()
{
    // 解密导入表
    for (int i = 0; i < g_importTable.dwSize; i++)
    {
        g_importTable.pData[i] ^= key;
    }

    // 修复IAT
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)g_importTable.pData;
    while (pImportDesc->Name)
    {
        const char *modName = (const char *)(sg_ImageBase + pImportDesc->Name);

        HMODULE hMod = g_MyLoadLibrary(modName);
        PIMAGE_THUNK_DATA pFirstThunks = (PIMAGE_THUNK_DATA)(sg_ImageBase + pImportDesc->FirstThunk);

        // 计算这个DLL的IAT大小(以NULL结束符为界)
        DWORD iatSize = 0;
        PIMAGE_THUNK_DATA pTemp = pFirstThunks;
        while (pTemp->u1.AddressOfData != 0)
        {
            iatSize += sizeof(IMAGE_THUNK_DATA);
            pTemp++;
        }
        iatSize += sizeof(IMAGE_THUNK_DATA); // 包含NULL结束符

        // 修改这个IAT区域的保护
        DWORD dwOldProtect = 0;
        if (!g_MyVirtualProtect(pFirstThunks, iatSize, PAGE_READWRITE, &dwOldProtect))
        {
            // 尝试以页为单位
            MEMORY_BASIC_INFORMATION mbi = {0};
            g_MyVirtualQuery(pFirstThunks, &mbi, sizeof(mbi));

            // 对齐到页边界
            LPVOID pageStart = (LPVOID)((ULONG_PTR)pFirstThunks & ~0xFFF);
            SIZE_T pageCount = ((iatSize + 0xFFF) / 0x1000) + 1;

            if (!g_MyVirtualProtect(pageStart, pageCount * 0x1000, PAGE_READWRITE, &dwOldProtect))
            {
                g_MyMessageBoxW(nullptr, L"FixImportTable", L"Unable to change page", MB_OK);
                g_MyExitProcess(12);
            }
        }

        while (pFirstThunks->u1.AddressOfData)
        {
            // 检查是通过序号还是名称导入的
            if (IMAGE_SNAP_BY_ORDINAL64(pFirstThunks->u1.Ordinal))
            {
                DWORD dwFuncOrinal = (pFirstThunks->u1.Ordinal) & 0xFFFF;
                ULONGLONG ullFuncAddr = (ULONGLONG)g_MyGetProcAddress(hMod, MAKEINTRESOURCEA(dwFuncOrinal));
                pFirstThunks->u1.Function = ullFuncAddr;
            }
            else
            {
                DWORD dwFuncNameRVA = pFirstThunks->u1.AddressOfData;
                PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)(sg_ImageBase + dwFuncNameRVA);
                ULONGLONG ullFuncAddr = (ULONGLONG)g_MyGetProcAddress(hMod, pFuncName->Name);
                pFirstThunks->u1.Function = ullFuncAddr;
            }
            pFirstThunks++;
        }
        pImportDesc++;
    }
}

执行OEP

typedef void (*FUNC)();
FUNC ep = (FUNC)(g_param.dwEP + g_ImageBase);
ep();

修改编译选项

使用vs默认的编译选项编译dll时,cl会自动添加一些安全检查函数,这并不是我们想要的,因为它们可能会访问其它节区。

首先禁用安全检查:

图片描述

然后修改基本运行时检查:

图片描述

总结

至此,一个基本的软件壳框架就搭好了,可以使用一个PE文件作为输入进行测试。当然,像notepad这样经过微软签名的程序加壳后就不能运行,因为我们的壳破坏了签名。后续我将为其添加反调试、反dump、动态解密、反静态分析等功能。

参考

开源软件:

参考书籍:

  • 《Windows PE权威指南》
  • 《加密与解密》
  • 《Windows环境下32位汇编语言程序设计》
  • 《Windows内核原理与实现》
  • 《逆向工程核心原理》
  • 《Windows核心编程》
  • 《精通Windows API》
  • 《软件加密技术内幕》

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 2
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回