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

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》
  • 《软件加密技术内幕》
相关推荐
CHANG_THE_WORLD5 小时前
PE 文件 的 重定位表
pe文件
booksyhay12 天前
串口调试助手注册机制研究(一)
逆向工程·串口调试助手·uartassist
带娃的IT创业者14 天前
当不可能成为可能:我将 Mac OS X 移植到了 Nintendo Wii
逆向工程·mac os x·极客·nintendo wii·操作系统移植·powerpc·硬件破解
三维频道17 天前
柔性材料3D数字化:蓝光扫描在内衣胸垫设计与质检中的应用
人工智能·3d·逆向工程·蓝光3d扫描仪·服装数字化·内衣设计·柔性材料检测
带娃的IT创业者19 天前
逆向工程与数字考古:以3万美元收购Friendster为例的技术重构实战
重构·数据清洗·逆向工程·数字考古·架构重构·friendster·技术迁移
曼岛_1 个月前
[逆向工程]160个CrackMe入门实战之aLoNg3x.2解析(七)
逆向工程
曼岛_1 个月前
[逆向工程]160个CrackMe入门实战之Andrnalin.1解析(八)
逆向工程
Pure_White_Sword1 个月前
[NSSRound#6 Team]void(V1)
网络安全·ctf·reverse·逆向工程
Pure_White_Sword1 个月前
[广东省大学生攻防大赛 2022]pyre
网络安全·ctf·reverse·逆向工程