I Pack You:实现基本的软件壳框架
记录一下自己开发简易软件壳的过程,参考的开源软件和书籍贴在文末。
这只是一个简易的加壳工具,实现了加壳的基本框架,后面会增加更多的功能填充这个框架。
实现思路
这个壳全部使用c/c++进行编写,没有使用汇编。因为在x64下,visual c++不支持内联汇编的语法了。如果要使用汇编,需要将汇编代码作为一个模块供c/c++使用。
软件壳分为两个部分:加壳程序和stub。其中stub以dll的形式存在,使用链接器指令将一些节区融合在一起(参考《加密与解密》)。加壳程序对目标PE文件进行加密和变换后,会提取Stub.dll的.text节区,写入目标文件的特定节区中。通过在dll中导出一些变量,实现加壳程序和stub之间的通信。
另外对于Stub.dll,我们需要修改vs默认的编译选项使得.text的代码移植到目标PE文件后可以正常运行,这点后面会说。
异常处理
为了优化程序结构,我自定义了一些异常类供程序使用:
cpp
#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错误码格式化工具类的,感觉没什么用,砍掉了。也贴出来给大家参考一下:
cpp
#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文件的各种信息以及对其进行修改。
cpp
#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段。
cpp
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数据:
cpp
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模块
这个模块实现了加壳操作。
cpp
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使用。**后续会使用其它技术对导入表进行处理。
cpp
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进行修正:
cpp
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代码,我们使用下面的指令融合区段并修改区段属性:
cpp
#pragma comment(linker, "/merge:.data=.text")
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/section:.text,RWE")
初始化
由于我们破坏的导入表,程序运行所需的模块都没有被加载的内存中,壳代码首先要做的便是搭建基础的运行环境。
原理可以参考我的这篇文章:64位Windows中使用PEB来获取进程加载的模块
头文件:
cpp
#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来处理异常,否则初始化失败的时候程序会卡死一段时间,虽然无伤大雅,但也让人难受:
cpp
#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进行修复,需要注意的是在修复的时候需要修改对应地址的页属性:
cpp
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
cpp
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》
- 《软件加密技术内幕》