使用 C++ 从零开始创建你自己的 x64 PE 打包器/保护器
本文将为您带来一个非常详细但易于学习的经验,教您如何仅使用 VC++ 创建自己的 x64 PE 打包器/保护器!
引言
还记得人们喜欢用 Pied、exeinfo、die、RDG 等 **PE 检测器**来检测开发人员使用了什么打包器/保护器吗?
曾几何时,PE 打包器/保护器非常流行,人们用它们来有效地减小二进制文件大小,并为代码添加一些保护层。
随着逆向工程技术和工具的进步,保护器变得非常脆弱且容易被攻破,但正邪之间的战争仍在继续……
然而,PE 打包器在安全性和减小代码大小方面仍然非常有用,但制作打包器是一项非常困难和复杂的任务。它需要非常精确的底层编程知识,这使得很少有人能成功完成它。
本文将教您如何仅使用 VC++ 创建自己的 PE 打包器,好消息是**无需汇编知识!**
背景
您可能会问,既然市面上已经有数百个打包器,为什么我还需要自定义打包器?要回答这个问题,您需要了解 PE 打包器的工作原理。
PE 打包器/保护器获取一个 PE 文件,分析它并提取输入 PE 文件的所有信息,然后修改 PE 文件并使用自己的结构重新创建它,它可能会将您的所有节压缩到一个新节中,并将其解压缩代码作为入口点,当 PE 启动时,它将动态解压缩数据到内存空间并恢复原始入口点并调用它,它也可能加密代码,因此原始原始代码只能在运行时访问和读取。
现在,打包器的结构总是相同的,一段时间后,当所有人都能够访问它时,它就成为一个容易攻击的目标,他们开始打包不同的 PE 文件,然后在其中搜索签名,签名成为一个可以用来为打包器创建解包器/反保护器的标记。
例如,如果您获取一个使用 ASPack 打包的 EXE 文件,您可以使用 **OllyDbg** 脚本或可下载的解包工具(如 ASPackDie)轻松地一键解包!
因此,创建自定义打包器的目的是:
- 只有你拥有这个打包器,而且它只用于你的产品,这使得分析更加困难,因为它具有独特性。
- 打包器只在你手中,攻击者无法从公共网站下载它来分析其功能。
- 你控制程序如何恢复和启动、压缩/加密算法等。
- 你可以使用额外的反向工程技术和任何你想要的东西!
- 当前版本受到攻击时,你可以快速更改签名和结构。
- 你可以隐藏攻击者可能用于分析的有用信息。
此外,在本教程中,我们不打算开发常规的 PE 打包器,而是根据输入 PE 文件创建一个新的 PE 文件,就像链接器一样,而不是操作现有的 exe/dll 文件。
注意:本文是关于如何使用 Visual Studio 构建 Shellcode 的前一篇文章的第二部分。
准备开发环境
1. 所需工具与软件
- Visual Studio 2019
- VC++ 构建工具 (C++ 17+ 支持)
- CFF Explorer (PE 查看器/编辑器)
- HxD (十六进制编辑器)
2. 创建空项目
-
打开 Visual Studio 2019
-
创建两个空的 C++ 项目。
-
一个命名为
pe_packer
,另一个命名为unpacker_stub
-
将
pe_packer
的配置类型设置为**“应用程序 (.exe)”** -
将
unpacker_stub
的配置类型设置为**“应用程序 (.exe)”** -
设置
unpacker_stub
独立于 CRT(C 运行时)和 Windows 内核。如果您不知道如何设置,请阅读上一篇文章。此外,本文中的 unpacker_stub 是一个 exe,因此您需要删除 /NOENTRY 选项。 -
将项目设置为 x64 和 Release 模式。
-
为这两个项目添加两个 .cpp 文件,一个用于打包器,一个用于解包器,代码设置如下:
// packer.cpp (pe_packer project) #include <Windows.h> #include <iostream> #include <fstream> using namespace std; int main(int argc, char* argv[]) { if (argc != 3) return EXIT_FAILURE; char* input_pe_file = argv[1]; char* output_pe_file = argv[2]; return EXIT_SUCCESS; }
// unpacker.cpp (unpacker_stub project) #include <Windows.h> // Entrypoint void func_unpack() { }
好了,现在我们万事俱备,可以开始开发了!
注意:为了加快打包器测试速度,您可以创建一个名为 pe_packer_tester.bat 的文件,其中包含以下内容:
"%cd%\pe_packer.exe" "%cd%\input_pe.exe" "%cd%\output_pe.exe"
您可以在此处下载基本设置源代码。
打包器:解析 + 验证输入 PE
好的,现在用户已将一个输入路径 (input_pe_file
) 和一个输出 PE 路径 (output_pe_file
) 传递给我们的打包器,第一步是验证输入文件并确保它是一个有效的 PE 文件,并且还确保它符合我们打包器所需的标准。
要执行验证,我们需要解析 PE 文件。
// Reading Input PE File
ifstream input_pe_file_reader(argv[1], ios::binary);
vector<uint8_t> input_pe_file_buffer(istreambuf_iterator<char>(input_pe_file_reader), {});
// Parsing Input PE File
PIMAGE_DOS_HEADER in_pe_dos_header = (PIMAGE_DOS_HEADER)input_pe_file_buffer.data();
PIMAGE_NT_HEADERS in_pe_nt_header = (PIMAGE_NT_HEADERS)(input_pe_file_buffer.data() + in_pe_dos_header->e_lfanew);
然后我们这样验证属性
bool isPE = in_pe_dos_header->e_magic == IMAGE_DOS_SIGNATURE;
bool is64 = in_pe_nt_header->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 &&
in_pe_nt_header->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC;
bool isDLL = in_pe_nt_header->FileHeader.Characteristics & IMAGE_FILE_DLL;
bool isNET = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size != 0;
添加检查和操作后,打包器代码应如下所示:
// packer.cpp
#include <Windows.h>
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
// Macros
#define BOOL_STR(b) b ? "true" : "false"
#define CONSOLE_COLOR_DEFAULT SetConsoleTextAttribute(hConsole, 0x09);
#define CONSOLE_COLOR_ERROR SetConsoleTextAttribute(hConsole, 0x0C);
int main(int argc, char* argv[])
{
// Setup Console
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTitle("Custom x64 PE Packer by H.M v1.0");
FlushConsoleInputBuffer(hConsole);
CONSOLE_COLOR_DEFAULT;
// Validate Arguments Count
if (argc != 3) return EXIT_FAILURE;
// User Inputs
char* input_pe_file = argv[1];
char* output_pe_file = argv[2];
// Reading Input PE File
ifstream input_pe_file_reader(argv[1], ios::binary);
vector<uint8_t> input_pe_file_buffer(istreambuf_iterator<char>(input_pe_file_reader), {});
// Parsing Input PE File
PIMAGE_DOS_HEADER in_pe_dos_header = (PIMAGE_DOS_HEADER)input_pe_file_buffer.data();
PIMAGE_NT_HEADERS in_pe_nt_header = (PIMAGE_NT_HEADERS)(input_pe_file_buffer.data() + in_pe_dos_header->e_lfanew);
// Validte PE Infromation
bool isPE = in_pe_dos_header->e_magic == IMAGE_DOS_SIGNATURE;
bool is64 = in_pe_nt_header->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 &&
in_pe_nt_header->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC;
bool isDLL = in_pe_nt_header->FileHeader.Characteristics & IMAGE_FILE_DLL;
bool isNET = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR].Size != 0;
// Log Validation Data
printf("[Validation] Is PE File : %s\n", BOOL_STR(isPE));
printf("[Validation] Is 64bit : %s\n", BOOL_STR(is64));
printf("[Validation] Is DLL : %s\n", BOOL_STR(isDLL));
printf("[Validation] Is COM or .Net : %s\n", BOOL_STR(isNET));
// Validate and Apply Action
if (!isPE)
{
CONSOLE_COLOR_ERROR;
printf("[Error] Input PE file is invalid. (Signature Mismatch)\n");
return EXIT_FAILURE;
}
if (!is64)
{
CONSOLE_COLOR_ERROR;
printf("[Error] This packer only supports x64 PE files.\n");
return EXIT_FAILURE;
}
if (isNET)
{
CONSOLE_COLOR_ERROR;
printf("[Error] This packer currently doesn't support .NET/COM assemblies.\n");
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
打包器:开发 PE 生成器
好的,现在我们知道输入 PE 文件是有效的,是时候创建一个 PE 生成器来生成一个有效的空 PE 文件了,为此我们使用 Windows API。
创建 DOS 头
每个 PE 文件都以 DOS 头开始,其中包含魔数(文件签名)和有关 PE 文件的基本信息,例如主头在哪里或重定位表的地址,要创建 DOS 头,我们需要初始化一个 IMAGE_DOS_HEADER
结构并设置值。
// Initializing Dos Header
IMAGE_DOS_HEADER dos_h;
memset(&dos_h, NULL, sizeof IMAGE_DOS_HEADER);
dos_h.e_magic = IMAGE_DOS_SIGNATURE;
dos_h.e_cblp = 0x0090;
dos_h.e_cp = 0x0003;
dos_h.e_crlc = 0x0000;
dos_h.e_cparhdr = 0x0004;
dos_h.e_minalloc = 0x0000;
dos_h.e_maxalloc = 0xFFFF;
dos_h.e_ss = 0x0000;
dos_h.e_sp = 0x00B8;
dos_h.e_csum = 0x0000; // Checksum
dos_h.e_ip = 0x0000;
dos_h.e_cs = 0x0000;
dos_h.e_lfarlc = 0x0040;
dos_h.e_ovno = 0x0000;
dos_h.e_oemid = 0x0000;
dos_h.e_oeminfo = 0x0000;
dos_h.e_lfanew = 0x0040; // Address of the NT Header
创建 NT 头
创建 DOS 头之后,下一个头必须是 NT 头,它包含有关 PE 文件的所有重要信息。NT 头包含:
-
签名
-
文件头
-
可选头
所有这 3 部分都包含在一个结构中,即 MAGE_NT_HEADERS
,要创建它,我们只需初始化它并设置以下值:
// Initializing Nt Header
IMAGE_NT_HEADERS nt_h;
memset(&nt_h, NULL, sizeof IMAGE_NT_HEADERS);
nt_h.Signature = IMAGE_NT_SIGNATURE;
nt_h.FileHeader.Machine = IMAGE_FILE_MACHINE_AMD64;
nt_h.FileHeader.NumberOfSections = 2;
nt_h.FileHeader.TimeDateStamp = 0x00000000; // Must Update
nt_h.FileHeader.PointerToSymbolTable = 0x0;
nt_h.FileHeader.NumberOfSymbols = 0x0;
nt_h.FileHeader.SizeOfOptionalHeader = 0x00F0;
nt_h.FileHeader.Characteristics = 0x0022; // Must Update
nt_h.OptionalHeader.Magic = IMAGE_NT_OPTIONAL_HDR64_MAGIC;
nt_h.OptionalHeader.MajorLinkerVersion = 10;
nt_h.OptionalHeader.MinorLinkerVersion = 0x05;
nt_h.OptionalHeader.SizeOfCode = 0x00000200; // Must Update
nt_h.OptionalHeader.SizeOfInitializedData = 0x00000200; // Must Update
nt_h.OptionalHeader.SizeOfUninitializedData = 0x0;
nt_h.OptionalHeader.AddressOfEntryPoint = 0x00001000; // Must Update
nt_h.OptionalHeader.BaseOfCode = 0x00001000;
nt_h.OptionalHeader.ImageBase = 0x0000000140000000;
nt_h.OptionalHeader.SectionAlignment = 0x00001000;
nt_h.OptionalHeader.FileAlignment = 0x00000200;
nt_h.OptionalHeader.MajorOperatingSystemVersion = 0x0;
nt_h.OptionalHeader.MinorOperatingSystemVersion = 0x0;
nt_h.OptionalHeader.MajorImageVersion = 0x0006;
nt_h.OptionalHeader.MinorImageVersion = 0x0000;
nt_h.OptionalHeader.MajorSubsystemVersion = 0x0006;
nt_h.OptionalHeader.MinorSubsystemVersion = 0x0000;
nt_h.OptionalHeader.Win32VersionValue = 0x0;
nt_h.OptionalHeader.SizeOfImage = 0x00003000; // Must Update
nt_h.OptionalHeader.SizeOfHeaders = 0x00000200;
nt_h.OptionalHeader.CheckSum = 0xFFFFFFFF; // Must Update
nt_h.OptionalHeader.Subsystem = IMAGE_SUBSYSTEM_WINDOWS_CUI;
nt_h.OptionalHeader.DllCharacteristics = 0x0120;
nt_h.OptionalHeader.SizeOfStackReserve = 0x0000000000100000;
nt_h.OptionalHeader.SizeOfStackCommit = 0x0000000000001000;
nt_h.OptionalHeader.SizeOfHeapReserve = 0x0000000000100000;
nt_h.OptionalHeader.SizeOfHeapCommit = 0x0000000000001000;
nt_h.OptionalHeader.LoaderFlags = 0x00000000;
nt_h.OptionalHeader.NumberOfRvaAndSizes = 0x00000010;
注意:MAGE_NT_HEADERS 基于您为项目设置的 CPU 架构。
在本文中,它生成 MAGE_NT_HEADERS64。
创建节
现在我们有了 DOS 头和 NT 头,剩下的就是节了!节包含 PE 文件中的所有数据,它们也有自己的头,所以我们需要为它们初始化一个头,然后将数据写入指定的偏移量,为了创建头,我们使用 IMAGE_SECTION_HEADER
结构。
// Initializing Section [ Code ]
IMAGE_SECTION_HEADER c_sec;
memset(&c_sec, NULL, sizeof IMAGE_SECTION_HEADER);
c_sec.Name[0] = '[';
c_sec.Name[1] = ' ';
c_sec.Name[2] = 'H';
c_sec.Name[3] = '.';
c_sec.Name[4] = 'M';
c_sec.Name[5] = ' ';
c_sec.Name[6] = ']';
c_sec.Name[7] = 0x0;
c_sec.Misc.VirtualSize = 0x00001000; // Virtual Size
c_sec.VirtualAddress = 0x00001000; // Virtual Address
c_sec.SizeOfRawData = 0x00000600; // Raw Size
c_sec.PointerToRawData = 0x00000200; // Raw Address
c_sec.PointerToRelocations = 0x00000000; // Reloc Address
c_sec.PointerToLinenumbers = 0x00000000; // Line Numbers
c_sec.NumberOfRelocations = 0x00000000; // Reloc Numbers
c_sec.NumberOfLinenumbers = 0x00000000; // Line Numbers Number
c_sec.Characteristics = IMAGE_SCN_MEM_EXECUTE |
IMAGE_SCN_MEM_READ |
IMAGE_SCN_CNT_CODE ;
// Initializing Section [ Data ]
IMAGE_SECTION_HEADER d_sec;
memset(&d_sec, NULL, sizeof IMAGE_SECTION_HEADER);
d_sec.Name[0] = '[';
d_sec.Name[1] = ' ';
d_sec.Name[2] = 'H';
d_sec.Name[3] = '.';
d_sec.Name[4] = 'M';
d_sec.Name[5] = ' ';
d_sec.Name[6] = ']';
d_sec.Name[7] = 0x0;
d_sec.Misc.VirtualSize = 0x00000200; // Virtual Size
d_sec.VirtualAddress = 0x00002000; // Virtual Address
d_sec.SizeOfRawData = 0x00000200; // Raw Size
d_sec.PointerToRawData = 0x00000800; // Raw Address
d_sec.PointerToRelocations = 0x00000000; // Reloc Address
d_sec.PointerToLinenumbers = 0x00000000; // Line Numbers
d_sec.NumberOfRelocations = 0x00000000; // Reloc Numbers
d_sec.NumberOfLinenumbers = 0x00000000; // Line Numbers Number
d_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA |
IMAGE_SCN_MEM_READ;
创建 PE 文件
太棒了!现在我们一切就绪,可以开始将 PE 文件写入磁盘了,执行此操作请使用以下代码:
// Create/Open PE File
fstream pe_writter;
pe_writter.open(output_pe_file, ios::binary | ios::out);
// Write DOS Header
pe_writter.write((char*)&dos_h, sizeof dos_h);
// Write NT Header
pe_writter.write((char*)&nt_h, sizeof nt_h);
// Write Headers of Sections
pe_writter.write((char*)&c_sec, sizeof c_sec);
pe_writter.write((char*)&d_sec, sizeof d_sec);
// Add Padding
while (pe_writter.tellp() != c_sec.PointerToRawData) pe_writter.put(0x0);
// Write Code Section
pe_writter.put(0xC3); // Empty PE Return Opcode
for (size_t i = 0; i < c_sec.SizeOfRawData - 1; i++) pe_writter.put(0x0);
// Write Data Section
for (size_t i = 0; i < d_sec.SizeOfRawData; i++) pe_writter.put(0x0);
// Close PE File
pe_writter.close();
现在运行你的打包器,**见证奇迹吧!**
打包器:主要实现
好的,现在我们有了 PE 解析器和 PE 生成器,是时候开发打包器本身了,为了执行此操作,我们使用 fast-lzma2 进行压缩,使用 AES-256 进行加密,然后将数据写入 PE 文件。
注意:我选择 fast-lzma2 进行压缩,因为它速度快,压缩比高。
你可以使用 zlib 或任何你想要的压缩库。
添加所需库
-
克隆 fast-lzma2 仓库并使用静态链接将其添加到您的项目中。
-
克隆 tiny-aes-c 仓库并将其添加到您的项目中。
你也可以使用我们在文章前一部分生成的 tiny-aes-c shellcode。
像这样添加库头文件和库:
// Encryption Library
extern "C"
{
#include "aes.h"
}
// Compression Library
#include "lzma2\fast-lzma2.h"
#pragma comment(lib, "lzma2\\fast-lzma2.lib")
压缩/加密数据
最后,我们像这样压缩并加密整个输入 PE 文件
// <----- Packing Data ( Main Implementation ) ----->
printf("[Information] Initializing AES Cryptor...\n");
struct AES_ctx ctx;
const unsigned char key[32] = {
0xD6, 0x23, 0xB8, 0xEF, 0x62, 0x26, 0xCE, 0xC3, 0xE2, 0x4C, 0x55, 0x12,
0x7D, 0xE8, 0x73, 0xE7, 0x83, 0x9C, 0x77, 0x6B, 0xB1, 0xA9, 0x3B, 0x57,
0xB2, 0x5F, 0xDB, 0xEA, 0x0D, 0xB6, 0x8E, 0xA2
};
const unsigned char iv[16] = {
0x18, 0x42, 0x31, 0x2D, 0xFC, 0xEF, 0xDA, 0xB6, 0xB9, 0x49, 0xF1, 0x0D,
0x03, 0x7E, 0x7E, 0xBD
};
AES_init_ctx_iv(&ctx, key, iv);
printf("[Information] Initializing Compressor...\n");
FL2_CCtx* cctx = FL2_createCCtxMt(8);
FL2_CCtx_setParameter(cctx, FL2_p_compressionLevel, 9);
FL2_CCtx_setParameter(cctx, FL2_p_dictionarySize, 1024);
vector<uint8_t> data_buffer;
data_buffer.resize(input_pe_file_buffer.size());
printf("[Information] Compressing Buffer...\n");
size_t original_size = input_pe_file_buffer.size();
size_t compressed_size = FL2_compressCCtx(cctx, data_buffer.data(), data_buffer.size(),
input_pe_file_buffer.data(), original_size, 9);
data_buffer.resize(compressed_size);
// Add Padding Before Encryption
for (size_t i = 0; i < 16; i++) data_buffer.insert(data_buffer.begin(), 0x0);
for (size_t i = 0; i < 16; i++) data_buffer.push_back(0x0);
printf("[Information] Encrypting Buffer...\n");
AES_CBC_encrypt_buffer(&ctx, data_buffer.data(), data_buffer.size());
// Log Compression Information
printf("[Information] Original PE Size : %ld bytes\n", input_pe_file_buffer.size());
printf("[Information] Packed PE Size : %ld bytes\n", data_buffer.size());
// Calculate Compression Ratio
float ratio =
(1.0f - ((float)data_buffer.size() / (float)input_pe_file_buffer.size())) * 100.f;
printf("[Information] Compression Ratio : %.2f%%\n", (roundf(ratio * 100.0f) * 0.01f));
注意:正如我之前所说,我们不会执行大多数 PE 打包器所使用的 PE 打包器例程,我们不会
在运行时加密/压缩代码段并恢复它,我们也不会操纵输入 PE 文件。
我们使用 PE 加载器将整个 PE 文件加载并映射到内存中,并调用入口点。
将数据写入 PE 文件并更新对齐
现在我们需要将打包数据写入生成的 PE 文件,请按照以下步骤操作:
-
将这些宏添加到全局作用域中
#define file_alignment_size 512 // Default Hard Disk Block Size (0x200) #define memory_alignment_size 4096 // Default Memory Page Size (0x1000)
-
将此函数添加到全局作用域
inline DWORD _align(DWORD size, DWORD align, DWORD addr = 0) { if (!(size % align)) return addr + size; return addr + (size / align + 1) * align; }
在处理 PE 文件时,对齐是一个非常重要的操作,学习它非常有帮助!
-
使用对齐更新以下值和代码
nt_h.OptionalHeader.SectionAlignment = memory_alignment_size; nt_h.OptionalHeader.FileAlignment = file_alignment_size;
d_sec.Misc.VirtualSize = _align(data_buffer.size(), memory_alignment_size); d_sec.VirtualAddress = c_sec.VirtualAddress + c_sec.Misc.VirtualSize; d_sec.SizeOfRawData = _align(data_buffer.size(), file_alignment_size); d_sec.PointerToRawData = c_sec.PointerToRawData + c_sec.SizeOfRawData;
// Write Data Section size_t current_pos = pe_writter.tellp(); pe_writter.write((char*)data_buffer.data(), data_buffer.size()); while (pe_writter.tellp() != current_pos + d_sec.SizeOfRawData) pe_writter.put(0x0); // Releasing And Finalizing vector<uint8_t>().swap(input_pe_file_buffer); vector<uint8_t>().swap(data_buffer); CONSOLE_COLOR_SUCCSESS; printf("[Information] PE File Packed Successfully."); return EXIT_SUCCESS;
-
构建项目并测试它,您的打包器应该生成一个包含打包数据的有效工作 PE 文件。
解包器:存根实现
好的!如果你还在跟着我,是时候生成解包器机器码并将其放入代码段了,为此我们需要生成一个解包器存根,打开 unpacker.cpp 并将 fast-lzma2 和 tiny-aes-c 添加到项目中,就像你为打包器所做的那样,并设置值和密钥,现在我们需要创建一些可以从打包器修改和操作的变量:
volatile PVOID data_ptr = (void*)0xAABBCCDD;
volatile DWORD data_size = 0xEEFFAADD;
volatile DWORD actual_data_size = 0xA0B0C0D0;
为什么要用 volatile
关键字?很简单……阻止编译器对其进行优化,同时保持优化,这是双赢的 ;)
代码应如下所示
// unpacker.cpp (unpacker_stub project)
#include <Windows.h>
// Encryption Library
extern "C"
{
#include "aes.h"
}
// Compression Library
#include "lzma2\fast-lzma2.h"
// WARNING : If you faced error using pragma, try adding lib file in linker settings
#pragma comment(lib, "lzma2\\fast-lzma2.lib")
// Merge Data With Code
#pragma comment(linker, "/merge:.rdata=.text")
// Entrypoint
void func_unpack()
{
// Internal Data [ Signatures ]
volatile PVOID data_ptr = (void*)0xAABBCCDD;
volatile DWORD data_size = 0xEEFFAADD;
volatile DWORD actual_data_size = 0xA0B0C0D0;
volatile DWORD header_size = 0xF0E0D0A0;
// Initializing Resolvers
k32_init(); crt_init();
// Getting BaseAddress of Module
intptr_t imageBase = (intptr_t)GetModuleHandleA(0);
data_ptr = (void*)((intptr_t)data_ptr + imageBase);
// Initializing Cryptor
struct AES_ctx ctx;
const unsigned char key[32] = {
0xD6, 0x23, 0xB8, 0xEF, 0x62, 0x26, 0xCE, 0xC3, 0xE2, 0x4C, 0x55, 0x12,
0x7D, 0xE8, 0x73, 0xE7, 0x83, 0x9C, 0x77, 0x6B, 0xB1, 0xA9, 0x3B, 0x57,
0xB2, 0x5F, 0xDB, 0xEA, 0x0D, 0xB6, 0x8E, 0xA2
};
const unsigned char iv[16] = {
0x18, 0x42, 0x31, 0x2D, 0xFC, 0xEF, 0xDA, 0xB6, 0xB9, 0x49, 0xF1, 0x0D,
0x03, 0x7E, 0x7E, 0xBD
};
AES_init_ctx_iv(&ctx, key, iv);
// Casting PVOID to BYTE
uint8_t* data_ptr_byte = (uint8_t*)data_ptr;
// Decrypting Buffer
AES_CBC_decrypt_buffer(&ctx, data_ptr_byte, data_size);
// Allocating Code Buffer
uint8_t* code_buffer = (uint8_t*)malloc(actual_data_size);
// Decompressing Buffer
FL2_decompress(code_buffer, actual_data_size, &data_ptr_byte[16], data_size - 32);
memset(data_ptr, 0, data_size);
}
注意:我们不使用 lzma2 多线程解压,因为在 shellcode 中使用多线程是一个非常糟糕的主意!
解包器:C 运行时和 WinAPI 解析器
好的,现在如果你尝试构建 unpacker_stub 项目,你会遇到很多**无法解析的外部符号**错误。
发生这种情况是因为我们移除了所有标准库,如 msvcrt 和 kernel32。对此有一个解决方案,称为**延迟导入**。
延迟导入技术
在延迟导入中,我们即时调用系统函数以动态使用函数。要使用此技术,您将需要来自真正的天才 Justas Masiulis 的这个惊人的单个头文件库。
第一步是像这样加载一个库:
uintptr_t msvcrtLib = reinterpret_cast<uintptr_t>(LI_FIND(LoadLibraryA)(_S("msvcrt.dll")));
然后像这样调用库的函数:
LI_GET(msvcrtLib, printf)("This is a message from dynamically loaded printf.\n");
就是这样!您可以使用任何库和任何函数,而不会在您的 PE 映像中留下痕迹,但这里的问题是 fast-lzma2 中有许多函数,用 LI_GET
函数替换所有这些函数可能会非常耗时!
它还可能在库代码中产生许多问题,所以我提出了这个想法:如果我开发解析器呢?它奏效了!
开发解析器
什么是解析器?我们如何将其用作解决方案?很简单,我们重新实现了模拟的 msvrct.lib 和 kernel32.lib(可用于任何其他库)中的所有 C 运行时和 WinAPI 函数,然后我们调用所有原始函数并将它们的函数参数重定向到它们,然后返回结果,这让我们能够从任何动态库创建静态库!
例如,我们这样解析 memcpy:
// resolver.h
void crt_init();
void* ___memcpy(void* dst, const void* src, size_t size);
// resolver.cpp
uintptr_t msvcrtLib = 0;
#define _VCRTFunc(fn) LI_GET(msvcrtLib,fn)
void crt_init()
{
msvcrtLib = reinterpret_cast<uintptr_t>(LI_FIND(LoadLibraryA)(_S("msvcrt.dll")));
}
// Dynamic memcpy
void* ___memcpy(void* dst, const void* src, size_t size)
{
return _VCRTFunc(memcpy)(dst, src, size);
}
// resolver_export.cpp
#include "resolver.h"
#define RESOLVER extern "C"
RESOLVER void* __cdecl memcpy(void* dst, const void* src, size_t size)
{
return ___memcpy(dst, src, size);
}
为了减少文章的篇幅,我避免展示如何解决所有需要的函数或过程,但您可以通过提供的示例代码轻松完成,**我还在项目源代码中包含了我的解析器的预构建静态库文件**,所以请随意节省一些时间并使用它们。
静态链接到解析器
由于链接顺序问题,避免使用 pragma 链接到解析器,而是使用链接器属性:
-
进入
unpacker_stub
项目的配置,然后进入 Linker > General > Additional Library Directories,并将其更改为 ".\resolvers" -
转到 Linker > Input > Additional Dependencies 并添加 "msvrcrt.lib" 和 "kernel32.lib"
-
前往 VC++ Directories 并清除 Library Directories 和 Library WinRT Directories,以避免链接到原始库。
-
创建外部函数头文件
// Resolvers Functions extern "C" void crt_init(); extern "C" void k32_init();
-
在初始化内部值之后和初始化加密器之前初始化解析器
// Initializing Resolvers k32_init(); crt_init();
-
将节合并 pragma 更新为
// Merge Data With Code #pragma comment(linker, "/merge:.rdata=.text") #pragma comment(linker, "/merge:.data=.text")
-
转到 Linker > Command Line,并在 Additional Options 中输入 "/EMITPOGOPHASEINFO /SECTION:.text,EWR"。
-
转到 Linker > Advanced 并将 Randomized Base Address 更改为 No (/DYNAMICBASE:NO)
-
转到 Linker > Advanced 并将 Fixed Base Address 更改为 Yes (/FIXED),此选项可防止生成重定位目录,因为重定位目录会导致代码依赖于存根 PE 文件。
现在构建,奇迹发生了……**解包器存根编译成功!**
解包器:PE 加载器/映射器
是时候为解包器添加一个 PE 加载器/映射器并完成解包器存根代码了,对于此操作,我们使用纯 C 语言开发的 mmLoader 库。
将库添加到项目和文件后,将以下代码添加到解包器存根代码的末尾:
// PE Loader Library
#include "mmLoader.h"
...
// Loading PE File
DWORD pe_loader_result = 0;
HMEMMODULE pe_module = LoadMemModule(code_buffer, true, &pe_loader_result);
**就是这样!**现在构建项目,您应该会得到 unpacker_stub.exe,它只包含两个节:
-
.text:解包器机器码
-
.pdata:包含我们不需要的异常目录
使用 CFF Explorer 或十六进制编辑器提取 .text 数据,并将其转换为字节数组,如下所示:
// unpacker_stub.h (pe_packer project)
unsigned char unpacker_stub[175104] = {
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B,
0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0,
0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, 0xB7, 0xFD, 0x93, 0x26
...
您可以在此处下载完成的解包器存根的源代码。
打包器:存根生成
在 packer.cpp 中包含 unpacker_stub.h
并对代码进行以下更改。
-
添加字节模式搜索辅助函数,用于在解包器存根中查找和修补签名
#include <algorithm> ... inline DWORD _find(uint8_t* data, size_t data_size, DWORD& value) { for (size_t i = 0; i < data_size; i++) if (memcmp(&data[i], &value, sizeof DWORD) == 0) return i; return -1; }
-
将节头改为这个
// Initializing Section [ Code ] IMAGE_SECTION_HEADER c_sec; memset(&c_sec, NULL, sizeof IMAGE_SECTION_HEADER); c_sec.Name[0] = '['; c_sec.Name[1] = ' '; c_sec.Name[2] = 'H'; c_sec.Name[3] = '.'; c_sec.Name[4] = 'M'; c_sec.Name[5] = ' '; c_sec.Name[6] = ']'; c_sec.Name[7] = 0x0; c_sec.Misc.VirtualSize = _align(sizeof unpacker_stub, memory_alignment_size); c_sec.VirtualAddress = memory_alignment_size; c_sec.SizeOfRawData = sizeof unpacker_stub; c_sec.PointerToRawData = file_alignment_size; c_sec.Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_CODE; // Initializing Section [ Data ] IMAGE_SECTION_HEADER d_sec; memset(&d_sec, NULL, sizeof IMAGE_SECTION_HEADER); d_sec.Name[0] = '['; d_sec.Name[1] = ' '; d_sec.Name[2] = 'H'; d_sec.Name[3] = '.'; d_sec.Name[4] = 'M'; d_sec.Name[5] = ' '; d_sec.Name[6] = ']'; d_sec.Name[7] = 0x0; d_sec.Misc.VirtualSize = _align(data_buffer.size(), memory_alignment_size); d_sec.VirtualAddress = c_sec.VirtualAddress + c_sec.Misc.VirtualSize; d_sec.SizeOfRawData = _align(data_buffer.size(), file_alignment_size); d_sec.PointerToRawData = c_sec.PointerToRawData + c_sec.SizeOfRawData; d_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE;
-
更新生成的 PE 头,为此,请在节头之后添加以下代码:
// Update PE Image Size printf("[Information] Updating PE Information...\n"); nt_h.OptionalHeader.SizeOfImage = _align(d_sec.VirtualAddress + d_sec.Misc.VirtualSize, memory_alignment_size); // Update PE Informations nt_h.FileHeader.Characteristics = in_pe_nt_header->FileHeader.Characteristics; nt_h.FileHeader.TimeDateStamp = in_pe_nt_header->FileHeader.TimeDateStamp; nt_h.OptionalHeader.CheckSum = 0xFFFFFFFF; nt_h.OptionalHeader.SizeOfCode = c_sec.SizeOfRawData; nt_h.OptionalHeader.SizeOfInitializedData = d_sec.SizeOfRawData; nt_h.OptionalHeader.Subsystem = in_pe_nt_header->OptionalHeader.Subsystem; // Update PE Entrypoint ( Taken from .map file ) nt_h.OptionalHeader.AddressOfEntryPoint = 0x00005940;
要从 .map 文件获取入口点偏移量,只需搜索
func_unpacker
即可找到偏移量,或者您也可以使用 CFF Explorer 从 unpacker_stub.exe 中复制入口点。 -
现在我们需要找到解包器存根签名并修补它们,将 PE 写入器代码更新为以下代码:
// Create/Open PE File printf("[Information] Writing Generated PE to Disk...\n"); fstream pe_writter; pe_writter.open(output_pe_file, ios::binary | ios::out); // Write DOS Header pe_writter.write((char*)&dos_h, sizeof dos_h); // Write NT Header pe_writter.write((char*)&nt_h, sizeof nt_h); // Write Headers of Sections pe_writter.write((char*)&c_sec, sizeof c_sec); pe_writter.write((char*)&d_sec, sizeof d_sec); // Add Padding while (pe_writter.tellp() != c_sec.PointerToRawData) pe_writter.put(0x0); // Find Singuatures in Unpacker Stub DWORD data_ptr_sig = 0xAABBCCDD; DWORD data_size_sig = 0xEEFFAADD; DWORD actual_data_size_sig = 0xA0B0C0D0; DWORD header_size_sig = 0xF0E0D0A0; DWORD data_ptr_offset = _find(unpacker_stub, sizeof unpacker_stub, data_ptr_sig); DWORD data_size_offset = _find(unpacker_stub, sizeof unpacker_stub, data_size_sig); DWORD actual_data_size_offset = _find(unpacker_stub, sizeof unpacker_stub, actual_data_size_sig); DWORD header_size_offset = _find(unpacker_stub, sizeof unpacker_stub, header_size_sig); // Log Singuatures Information if (data_ptr_offset != -1) printf("[Information] Signature A Found at : %X\n", data_ptr_offset); if (data_size_offset != -1) printf("[Information] Signature B Found at : %X\n", data_size_offset); if (actual_data_size_offset != -1) printf("[Information] Signature C Found at : %X\n", actual_data_size_offset); if (header_size_offset != -1) printf("[Information] Signature D Found at : %X\n", header_size_offset); // Update Code Section printf("[Information] Updating Offset Data...\n"); memcpy(&unpacker_stub[data_ptr_offset], &d_sec.VirtualAddress, sizeof DWORD); memcpy(&unpacker_stub[data_size_offset], &d_sec.SizeOfRawData, sizeof DWORD); DWORD pe_file_actual_size = (DWORD)input_pe_file_buffer.size(); memcpy(&unpacker_stub[actual_data_size_offset], &pe_file_actual_size, sizeof DWORD); memcpy(&unpacker_stub[header_size_offset], &nt_h.OptionalHeader.BaseOfCode, sizeof DWORD); // Write Code Section printf("[Information] Writing Code Data...\n"); pe_writter.write((char*)&unpacker_stub, sizeof unpacker_stub); // Write Data Section printf("[Information] Writing Packed Data...\n"); size_t current_pos = pe_writter.tellp(); pe_writter.write((char*)data_buffer.data(), data_buffer.size()); while (pe_writter.tellp() != current_pos + d_sec.SizeOfRawData) pe_writter.put(0x0); // Close PE File pe_writter.close();
好了,现在让我们试试打包器……然后……**恭喜!你制作了你的第一个 PE 打包器!**
你可以在这里下载打包器和解包器存根的完整源代码。
打包器:动态链接支持 + 导出表创建
现在我们的打包器可以打包 EXE 文件并生成一个新的可工作的 EXE 文件,但是如果我们想打包一个带有其导出的 DLL 呢?为此,我们需要为我们的输出 PE 文件创建一个导出表,然后将调用重定向到实际的模块。
这个过程不像前面的部分那么容易,实际上它非常复杂,需要一个铁脑筋才能解决,但别担心,我绞尽脑汁为你解决了它,所以让我们开始为我们的打包器添加 DLL 支持吧!
A) 更新并使解包器存根支持 DLL
目前,我们的解包器存根代码并非为 DLL 入口点设计,我们需要对其进行修改,以确保它能正确通过 DLL 初始化例程,此外,我们还需要添加两个额外的值,这些值将在下一节中解释。
新的解包器存根应如下所示:
// unpacker.cpp (unpacker_stub project)
// WinAPI Functions
#include <Windows.h>
#include <winnt.h>
EXTERN_C IMAGE_DOS_HEADER __ImageBase;
// Resolvers Functions
EXTERN_C void crt_init();
EXTERN_C void k32_init();
// Encryption Library
extern "C"
{
#include "aes.h"
}
// Compression Library
#include "lzma2\fast-lzma2.h"
// PE Loader Library
#include "mmLoader.h"
// Merge Data With Code
#pragma comment(linker, "/merge:.rdata=.text")
#pragma comment(linker, "/merge:.data=.text")
// Cross Section Value
EXTERN_C static volatile uintptr_t moduleImageBase = 0xBCEAEFBA;
EXTERN_C static volatile FARPROC functionForwardingPtr = (FARPROC)0xCAFEBABE;
// External Functions
EXTERN_C BOOL CallModuleEntry(void* pMemModule_d, DWORD dwReason);
// Multi-Accessing Values
HMEMMODULE pe_module = 0;
// Entrypoint (EXE/DLL)
BOOL func_unpack(void*, int reason, void*)
{
// Releasing DLL PE Module
if (reason == DLL_PROCESS_DETACH)
{ CallModuleEntry(pe_module, DLL_PROCESS_DETACH); FreeMemModule(pe_module); return TRUE; };
// Handling DLL Thread Events
if (reason == DLL_THREAD_ATTACH) return CallModuleEntry(pe_module, DLL_THREAD_ATTACH);
if (reason == DLL_THREAD_DETACH) return CallModuleEntry(pe_module, DLL_THREAD_DETACH);
// Internal Data [ Signatures ]
volatile PVOID data_ptr = (void*)0xAABBCCDD;
volatile DWORD data_size = 0xEEFFAADD;
volatile DWORD actual_data_size = 0xA0B0C0D0;
volatile DWORD header_size = 0xF0E0D0A0;
// Initializing Resolvers
k32_init(); crt_init();
// Getting BaseAddress of Module
intptr_t imageBase = (intptr_t)&__ImageBase;
data_ptr = (void*)((intptr_t)data_ptr + imageBase);
// Initializing Cryptor
struct AES_ctx ctx;
const unsigned char key[32] = {
0xD6, 0x23, 0xB8, 0xEF, 0x62, 0x26, 0xCE, 0xC3, 0xE2, 0x4C, 0x55, 0x12,
0x7D, 0xE8, 0x73, 0xE7, 0x83, 0x9C, 0x77, 0x6B, 0xB1, 0xA9, 0x3B, 0x57,
0xB2, 0x5F, 0xDB, 0xEA, 0x0D, 0xB6, 0x8E, 0xA2
};
const unsigned char iv[16] = {
0x18, 0x42, 0x31, 0x2D, 0xFC, 0xEF, 0xDA, 0xB6, 0xB9, 0x49, 0xF1, 0x0D,
0x03, 0x7E, 0x7E, 0xBD
};
AES_init_ctx_iv(&ctx, key, iv);
// Casting PVOID to BYTE
uint8_t* data_ptr_byte = (uint8_t*)data_ptr;
// Decrypting Buffer
AES_CBC_decrypt_buffer(&ctx, data_ptr_byte, data_size);
// Allocating Code Buffer
uint8_t* code_buffer = (uint8_t*)malloc(actual_data_size);
// Decompressing Buffer
FL2_decompress(code_buffer, actual_data_size, &data_ptr_byte[16], data_size - 32);
memset(data_ptr, 0, data_size);
// Loading PE Module
DWORD pe_loader_result = 0;
pe_module = LoadMemModule(code_buffer, false, &pe_loader_result);
// Set Image Base
moduleImageBase = (uintptr_t)*pe_module;
functionForwardingPtr = 0;
// Call Entrypoint
return CallModuleEntry(pe_module, DLL_PROCESS_ATTACH);
}
现在让我为您解释更新的部分;)
-
我们将
func_unpack
的返回类型更改为BOOL
并添加了 3 个参数(dllmain 例程)。BOOL func_unpack(void*, int reason, void*)
-
我们应该更新获取镜像地址的方式,在 exe 中我们可以直接使用
GetModuleHandle
,但在 dll 中不行,所以我们使用__ImageBase
外部值来获取它,是的,我们可以使用func_unpack
函数的第一个参数hInstance
,但它只适用于 dll,通过使用__ImageBase
,我们可以在任何类型的 PE 文件中获取正确的值。#include <winnt.h> EXTERN_C IMAGE_DOS_HEADER __ImageBase; ... // Getting BaseAddress of Module intptr_t imageBase = (intptr_t)&__ImageBase;
-
我们需要控制动态加载模块的入口点调用,因此我们应该对 mmLoader 进行一些简单的更改,并将
CallModuleEntry
函数设置为 public,然后我们用它在从内存加载模块后手动调用入口点:// External Functions EXTERN_C BOOL CallModuleEntry(void* pMemModule_d, DWORD dwReason); ... // Changes in mmLoader.c BOOL CallModuleEntry(void* pMemModule_d, DWORD dwReason) { PMEM_MODULE pMemModule = pMemModule_d; ...
-
我们应该处理 dll 事件,以避免在分离时发生内存泄漏、崩溃或数据丢失。
// Releasing DLL PE Module if (reason == DLL_PROCESS_DETACH) { CallModuleEntry(pe_module, DLL_PROCESS_DETACH); FreeMemModule(pe_module); return TRUE; }; // Handling DLL Thread Events if (reason == DLL_THREAD_ATTACH) return CallModuleEntry(pe_module, DLL_THREAD_ATTACH); if (reason == DLL_THREAD_DETACH) return CallModuleEntry(pe_module, DLL_THREAD_DETACH);
-
我们添加了两个静态值,我们将在后续步骤中使用和访问它们,这些值可以在 PE 节之间访问。
// Cross Section Value EXTERN_C static volatile uintptr_t moduleImageBase = 0xBCEAEFBA; EXTERN_C static volatile FARPROC functionForwardingPtr = (FARPROC)0xCAFEBABE;
-
最后,我们更新了 PE 加载流程并设置了我们的值,这些值是什么?继续阅读!
// Loading PE Module DWORD pe_loader_result = 0; pe_module = LoadMemModule(code_buffer, false, &pe_loader_result); // Set Image Base moduleImageBase = (uintptr_t)*pe_module; functionForwardingPtr = 0; // Call Entrypoint return CallModuleEntry(pe_module, DLL_PROCESS_ATTACH);
不要忘记将 pe_module
移至全局作用域,以便可以在每个事件调用中访问它。编译解包器存根并在打包器项目中更新原始数组和入口点偏移量后,它应该对 exe 和 dll 都有效,现在我们来处理导出表。
B) 在打包器中为新的解包器存根值添加模式搜索
好的,现在转到 packer.cpp,在更新入口点值的那一行之后添加新的模式搜索代码
// Update PE Entrypoint ( Taken from .map file )
nt_h.OptionalHeader.AddressOfEntryPoint = 0x00005F10;
// Get Const Values Offset In Unpacker
DWORD imagebase_value_sig = 0xBCEAEFBA;
DWORD imageBaseValueOffset = _find(unpacker_stub, sizeof unpacker_stub, imagebase_value_sig);
memset(&unpacker_stub[imageBaseValueOffset], NULL, sizeof uintptr_t);
if (imageBaseValueOffset != -1)
printf("[Information] ImageBase Value Signature Found at : %X\n", imageBaseValueOffset);
DWORD forwarding_value_sig = 0xCAFEBABE;
DWORD forwarding_value_offset = _find(unpacker_stub, sizeof unpacker_stub, forwarding_value_sig);
memset(&unpacker_stub[forwarding_value_offset], NULL, sizeof FARPROC);
if (imageBaseValueOffset != -1)
printf("[Information] Function Forwading Value Signature Found at : %X\n", forwarding_value_offset);
C) 添加导出节/表/代码生成步骤
现在是时候添加一个步骤来检测我们是否正在打包一个 dll 文件了,在模式搜索之后添加以下代码:
// Create Export Table ( Section [ Export ] )
IMAGE_SECTION_HEADER et_sec;
memset(&et_sec, NULL, sizeof IMAGE_SECTION_HEADER);
bool hasExports = false; vector<uint8_t> et_buffer;
if (isDLL)
{
// We Generate Export Section, Export Table and Export Code Here
}
D) 从输入 PE 文件中提取导出信息
我们已经准备好开始处理导出,第一步是找出输入 PE 文件是否有任何导出。
if (isDLL)
{
uint8_t export_section_index = 0;
int export_section_raw_addr = -1;
// Get Export Table Information
IMAGE_DATA_DIRECTORY ex_table =
in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (ex_table.VirtualAddress != 0) hasExports = true;
printf("[Information] Has Exports : %s\n", BOOL_STR(hasExports));
if (hasExports)
{
printf("[Information] Creating Export Table...\n");
// We Have Exports on Input PE File!
}
}
现在我们获取输入 PE 导出目录的 RVA(相对虚拟地址),并计算我们的导出节所在的虚拟地址。
// Export Directory RVA
DWORD e_dir_rva = ex_table.VirtualAddress;
DWORD et_sec_virtual_address = d_sec.VirtualAddress + d_sec.Misc.VirtualSize;
printf("[Information] Input PE File Section Count : %d\n", in_pe_nt_header->FileHeader.NumberOfSections);
然后我们遍历输入 PE 节,找出哪个节包含 DLL 导出数据。
// Get Section Macro
#define GET_SECTION(h,s) (uintptr_t)IMAGE_FIRST_SECTION(h) + ((s) * sizeof IMAGE_SECTION_HEADER)
...
// Find Export Section in Input PE File
for (size_t i = 0; i < in_pe_nt_header->FileHeader.NumberOfSections; i++)
{
IMAGE_SECTION_HEADER* get_sec = (PIMAGE_SECTION_HEADER)(GET_SECTION(in_pe_nt_header, i));
IMAGE_SECTION_HEADER* get_next_sec = (PIMAGE_SECTION_HEADER)(GET_SECTION(in_pe_nt_header, i + 1));
if (e_dir_rva > get_sec->VirtualAddress &&
e_dir_rva < get_next_sec->VirtualAddress &&
(i + 1) <= in_pe_nt_header->FileHeader.NumberOfSections)
{
export_section_index = i; break;
};
}
printf("[Information] Export Section Found At %dth Section\n", export_section_index + 1);
if (export_section_index != -1)
{
// Actual Export Generation Happens Here
}
好的,在我们进入龙口之前,让我们谈谈我们将如何执行 DLL 导出生成的过程……
E) 理解 DLL 导出转发的概念
在继续之前,您需要了解 DLL 导出是如何工作的以及其背后的设计理念,DLL 导出由以下部分组成:
-
导出目录:它是一个图像目录,包含两个值:导出表的 RVA 和它的大小。从 RVA 我们可以找出哪个节包含导出表。
-
导出节:它是一个包含导出表和导出数据的节,也可以包含导出代码。
-
导出表:它是一个结构,包含有关 DLL 导出的基本信息,它们位于何处,有多少个,导出数据位于何处以及其 RVA 是多少。
-
导出数据:它包含函数**名称 RVA、名称、序数和 RVA** 的列表。
-
名称 RVA:它是一个指向以空字符串文字结尾的函数字符串名称的 RVA。
-
函数 RVA:这是一个指向函数机器码(导出代码!)的 RVA。
-
-
导出代码:它是一个函数机器码数组,此代码可以位于 .text 节或任何其他节中。在我们的打包器中,我们将使用基本的机器码在同一导出节中生成代码。
注意:在本文中我提到不需要汇编知识,但是这部分需要一点汇编知识,但由于它不复杂也不动态,我们只使用一小段预生成的机器码。
**什么是函数转发?**在编程中,函数转发意味着从一个函数调用跳转到另一个函数,而不会干扰函数参数。
它可以通过几种技术实现,如 DLL 劫持、DLL 代理、机器码重定向等。在我们的 PE 打包器中,我们生成一小段机器码(32 字节),它定位加载模块的镜像基地址,然后将其与实际函数偏移量相加,最后添加一个跳转。
这是我们将用于函数转发的汇编代码:
PUSH RCX
PUSH RAX
MOV RAX,QWORD PTR DS:[(Image Base Address)]
MOV ECX, (Function Offset)
ADD RAX,RCX
MOV QWORD PTR DS:[(Function Offset + Image Base Address)],RAX
POP RAX
POP RCX
JMP QWORD PTR DS:[(Function Offset + Image Base Address)] /* < Jump */
所以基本上,在我们将在解包器中设置动态模块的镜像基址为 `Image Base Address` 后,我们可以轻松地将 `Function Offset` 添加到其中,相加后,我们将值设置到第二个静态值持有者 `Function Offset + Image Base Address` 中,然后跳转到它,就是这样!
F) 克隆输入 PE 导出表,进行更改并重定位
好的,既然您已经知道这些齿轮是如何工作的了,是时候开始困难的部分了,在继续之前,添加这些有用的宏来简化过程:
#define GET_SECTION(h,s) (uintptr_t)IMAGE_FIRST_SECTION(h) + ((s) * sizeof IMAGE_SECTION_HEADER)
#define RVA_TO_FILE_OFFSET(rva,membase,filebase) ((rva - membase) + filebase)
#define RVA2OFS_EXP(rva) (input_pe_file_buffer.data() + \
(RVA_TO_FILE_OFFSET(rva, in_pe_exp_sec->VirtualAddress, in_pe_exp_sec->PointerToRawData)))
#define REBASE_RVA(rva) ((rva - in_pe_exp_sec->VirtualAddress + et_sec_virtual_address) - \
(e_dir_rva - in_pe_exp_sec->VirtualAddress))
现在我们像这样解析输入 PE 导出节,现在我们可以访问导出表数据了。
printf("[Information] Parsing Input PE Export Section...\n");
// Get Export Directory
PIMAGE_SECTION_HEADER in_pe_exp_sec = (PIMAGE_SECTION_HEADER)(GET_SECTION(in_pe_nt_header, export_section_index));
PIMAGE_EXPORT_DIRECTORY e_dir = (PIMAGE_EXPORT_DIRECTORY)RVA2OFS_EXP(e_dir_rva);
DWORD e_dir_size = in_pe_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size;
printf("[Information] Export Section Name : %s\n", in_pe_exp_sec->Name);
// Extracting Input Binary Export Table
PULONG in_et_fn_tab = (PULONG)RVA2OFS_EXP(e_dir->AddressOfFunctions);
PULONG in_et_name_tab = (PULONG)RVA2OFS_EXP(e_dir->AddressOfNames);
PUSHORT in_et_ordianl_tab = (PUSHORT)RVA2OFS_EXP(e_dir->AddressOfNameOrdinals);
uintptr_t in_et_data_start = (uintptr_t)in_et_fn_tab;
DWORD in_et_last_fn_name_size = strlen((char*)RVA2OFS_EXP(in_et_name_tab[e_dir->NumberOfNames - 1])) + 1;
uintptr_t in_et_data_end = (uintptr_t)(RVA2OFS_EXP(in_et_name_tab[e_dir->NumberOfNames - 1]) + in_et_last_fn_name_size);
然后我们使用宏像这样简单地重新定位它们
// Rebase Export Table Addresses
printf("[Information] Rebasing Expor Table Addresses...\n");
e_dir->AddressOfFunctions = REBASE_RVA(e_dir->AddressOfFunctions);
e_dir->AddressOfNames = REBASE_RVA(e_dir->AddressOfNames);
e_dir->AddressOfNameOrdinals = REBASE_RVA(e_dir->AddressOfNameOrdinals);
for (size_t i = 0; i < e_dir->NumberOfNames; i++) in_et_name_tab[i] = REBASE_RVA(in_et_name_tab[i]);
我们重定位它们之后,将导出目录数据复制到新的 PE 文件中。
// Generate Export Table Direcotry Data
et_buffer.resize(e_dir_size);
memcpy(et_buffer.data(), e_dir, sizeof IMAGE_EXPORT_DIRECTORY);
G) 生成导出机器码
现在我们已经准备好为我们的导出生成机器码了,为此,只需将这段小的机器码模板添加到您的源代码中,紧接在帮助函数之后:
// Machine Code
unsigned char func_forwarding_code[32] =
{
0x51, 0x50, // PUSH RCX, PUSH RAX
0x48, 0x8B, 0x05, 0x00, 0x00, 0x00, 0x00, // MOV RAX,QWORD PTR DS:[OFFSET]
0xB9, 0x00, 0x00, 0x00, 0x00, // MOV ECX,VALUE
0x48, 0x03, 0xC1, // ADD RAX,RCX
0x48, 0x89, 0x05, 0x00, 0x00, 0x00, 0x00, // MOV QWORD PTR DS:[OFFSET],RAX
0x58, 0x59, // POP RAX, POP RCX
0xFF, 0x25, 0x00, 0x00, 0x00, 0x00, // JMP QWORD PTR DS:[OFFSET]
};
在此之后,我们需要分配一个临时缓冲区,计算镜像基址 RVA、当前代码块 RVA 和偏移量,然后我们简单地设置机器码字节数组中的值并将其添加到临时缓冲区中,完成后,我们将其添加到我们的导出节中。
// Generate Export Table Codes
printf("[Information] Generating Function Forwarding Code...\n");
DWORD ff_code_buffer_size = sizeof func_forwarding_code * e_dir->NumberOfFunctions;
uint8_t* ff_code_buffer = (uint8_t*)malloc(ff_code_buffer_size);
DWORD image_base_rva = c_sec.VirtualAddress + imageBaseValueOffset;
DWORD ff_value_rva = c_sec.VirtualAddress + forwarding_value_offset;
for (size_t i = 0; i < e_dir->NumberOfFunctions; i++)
{
DWORD func_offset = in_et_fn_tab[in_et_ordianl_tab[i]];
DWORD machine_code_offset = i * sizeof func_forwarding_code;
DWORD machine_code_rva = et_buffer.size() + machine_code_offset + et_sec_virtual_address;
// Machine Code Data
int32_t* offset_to_image_base = (int32_t*)&func_forwarding_code[5];
int32_t* function_offset_value = (int32_t*)&func_forwarding_code[10];
int32_t* offset_to_func_addr = (int32_t*)&func_forwarding_code[20];
int32_t* offset_to_func_addr2 = (int32_t*)&func_forwarding_code[28];
offset_to_image_base[0] = (image_base_rva - machine_code_rva) - (5 + sizeof int32_t);
function_offset_value[0] = func_offset;
offset_to_func_addr[0] = (ff_value_rva - machine_code_rva) - (20 + sizeof int32_t);
offset_to_func_addr2[0] = (ff_value_rva - machine_code_rva) - (28 + sizeof int32_t);
memcpy(&ff_code_buffer[machine_code_offset], func_forwarding_code, sizeof func_forwarding_code);
// Update Function Address
in_et_fn_tab[i] = et_sec_virtual_address + et_buffer.size() + (i * sizeof func_forwarding_code);
}
// Copy Updated Export Table Data
DWORD et_data_size = in_et_data_end - in_et_data_start;
memcpy(&et_buffer.data()[sizeof IMAGE_EXPORT_DIRECTORY], (void*)in_et_data_start, et_data_size);
// Merge Export Table and Export Data Buffers
DWORD size_of_export_table = et_buffer.size();
et_buffer.resize(size_of_export_table + ff_code_buffer_size);
memcpy(&et_buffer.data()[size_of_export_table], (void*)ff_code_buffer, ff_code_buffer_size);
free(ff_code_buffer);
**就是这样!**我们已经生成了所有需要的东西,现在我们只需要为导出生成一个新的节头:
// Generate Export Table Section
et_sec.Name[0] = '[';
et_sec.Name[1] = ' ';
et_sec.Name[2] = 'H';
et_sec.Name[3] = '.';
et_sec.Name[4] = 'M';
et_sec.Name[5] = ' ';
et_sec.Name[6] = ']';
et_sec.Name[7] = 0x0;
et_sec.Misc.VirtualSize = _align(et_buffer.size(), memory_alignment_size);
et_sec.VirtualAddress = et_sec_virtual_address;
et_sec.SizeOfRawData = _align(et_buffer.size(), file_alignment_size);
et_sec.PointerToRawData = d_sec.PointerToRawData + d_sec.SizeOfRawData;
et_sec.Characteristics = IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE;
并更新我们的导出表目录
// Update Export Table Directory
nt_h.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress = et_sec.VirtualAddress;
nt_h.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size = e_dir_size;
并更新节计数和图像大小
// Update PE Headers
nt_h.FileHeader.NumberOfSections = 3;
// Update PE Image Size
nt_h.OptionalHeader.SizeOfImage =
_align(et_sec.VirtualAddress + et_sec.Misc.VirtualSize, memory_alignment_size);
就这样!是不是没那么难对吧?🍎
H) 将 DLL 导出写入 PE 文件
当然,我们需要做一些更改才能将导出正确写入 DLL 文件,在完成将数据节写入文件后,我们写入导出节,此外,我们还应该共享 current_pos
值。
// Write Export Section
if (et_buffer.size() != 0 && hasExports)
{
printf("[Information] Writing Export Table Data...\n");
current_pos = pe_writter.tellp();
pe_writter.write((char*)et_buffer.data(), et_buffer.size());
while (pe_writter.tellp() != current_pos + et_sec.SizeOfRawData) pe_writter.put(0x0);
}
所以最终的 PE 文件生成必须像这样:
// Create/Open PE File
printf("[Information] Writing Generated PE to Disk...\n");
fstream pe_writter;
size_t current_pos;
pe_writter.open(output_pe_file, ios::binary | ios::out);
// Write DOS Header
pe_writter.write((char*)&dos_h, sizeof dos_h);
// Write NT Header
pe_writter.write((char*)&nt_h, sizeof nt_h);
// Write Headers of Sections
pe_writter.write((char*)&c_sec, sizeof c_sec);
pe_writter.write((char*)&d_sec, sizeof d_sec);
if(nt_h.FileHeader.NumberOfSections == 3) pe_writter.write((char*)&et_sec, sizeof et_sec);
// Add Padding
while (pe_writter.tellp() != c_sec.PointerToRawData) pe_writter.put(0x0);
// Find Singuatures in Unpacker Stub
DWORD data_ptr_sig = 0xAABBCCDD;
DWORD data_size_sig = 0xEEFFAADD;
DWORD actual_data_size_sig = 0xA0B0C0D0;
DWORD header_size_sig = 0xF0E0D0A0;
DWORD data_ptr_offset = _find(unpacker_stub, sizeof unpacker_stub, data_ptr_sig);
DWORD data_size_offset = _find(unpacker_stub, sizeof unpacker_stub, data_size_sig);
DWORD actual_data_size_offset = _find(unpacker_stub, sizeof unpacker_stub, actual_data_size_sig);
DWORD header_size_offset = _find(unpacker_stub, sizeof unpacker_stub, header_size_sig);
...
// Update Code Section
printf("[Information] Updating Offset Data...\n");
memcpy(&unpacker_stub[data_ptr_offset], &d_sec.VirtualAddress, sizeof DWORD);
memcpy(&unpacker_stub[data_size_offset], &d_sec.SizeOfRawData, sizeof DWORD);
DWORD pe_file_actual_size = (DWORD)input_pe_file_buffer.size();
memcpy(&unpacker_stub[actual_data_size_offset], &pe_file_actual_size, sizeof DWORD);
memcpy(&unpacker_stub[header_size_offset], &nt_h.OptionalHeader.BaseOfCode, sizeof DWORD);
// Write Code Section
printf("[Information] Writing Code Data...\n");
current_pos = pe_writter.tellp();
pe_writter.write((char*)&unpacker_stub, sizeof unpacker_stub);
while (pe_writter.tellp() != current_pos + c_sec.SizeOfRawData) pe_writter.put(0x0);
// Write Data Section
printf("[Information] Writing Packed Data...\n");
current_pos = pe_writter.tellp();
pe_writter.write((char*)data_buffer.data(), data_buffer.size());
while (pe_writter.tellp() != current_pos + d_sec.SizeOfRawData) pe_writter.put(0x0);
// Write Export Section
if (et_buffer.size() != 0 && hasExports)
{
printf("[Information] Writing Export Table Data...\n");
current_pos = pe_writter.tellp();
pe_writter.write((char*)et_buffer.data(), et_buffer.size());
while (pe_writter.tellp() != current_pos + et_sec.SizeOfRawData) pe_writter.put(0x0);
}
// Close PE File
pe_writter.close();
**现在我们的打包器也支持 DLL 文件了!** 🎉
你可以在这里下载打包器和解包器存根的完整源代码。
打包器:文件版本生成
好的,这是文章的最后一部分了,当然我们可以做更多,添加更多功能,但我相信文章已经很长了。在文章的最后一部分,我们将对最终的 PE 文件进行一些后期处理,我们将添加文件信息和图标。
您可以使用 GitHub 上的许多库来完成本文的这一部分,我使用我自己的资源库。
-
链接到
utilities\hmrclib64_vc16.lib
,可以在下一章源代码 zip 文件中找到。 -
在压缩库头文件之后,将函数定义添加到 packer.cpp 中:
// PE Info Ediotr void HMResKit_LoadPEFile(const char* peFile); void HMResKit_SetFileInfo(const char* key, const char* value); void HMResKit_SetPEVersion(const char* peFile); void HMResKit_ChangeIcon(const char* iconPath); void HMResKit_CommitChanges(const char* sectionName);
-
像这样添加信息和图标
// Post-Process [ Add Information & Icon ] printf("[Information] Adding File Information and Icon...\n"); HMResKit_LoadPEFile(output_pe_file); HMResKit_SetFileInfo("ProductName", "Custom PE Packer"); HMResKit_SetFileInfo("CompanyName", "MemarDesign™ LLC."); HMResKit_SetFileInfo("LegalTrademarks", "MemarDesign™ LLC."); HMResKit_SetFileInfo("Comments", "Developed by Hamid.Memar"); HMResKit_SetFileInfo("FileDescription", "A PE File Packed by HMPacker"); HMResKit_SetFileInfo("ProductVersion", "1.0.0.1"); HMResKit_SetFileInfo("FileVersion", "1.0.0.1"); HMResKit_SetFileInfo("InternalName", "packed-pe-file"); HMResKit_SetFileInfo("OriginalFilename", "packed-pe-file"); HMResKit_SetFileInfo("LegalCopyright", "Copyright MemarDesign™ LLC. © 2021-2022"); HMResKit_SetFileInfo("PrivateBuild", "Packed PE"); HMResKit_SetFileInfo("SpecialBuild", "Packed PE"); HMResKit_SetPEVersion("1.0.0.1"); if (!isDLL) HMResKit_ChangeIcon("app.ico"); HMResKit_CommitChanges("[ H.M ]");
注意:本文不涉及从输入 PE 文件中提取图标和文件信息,这很容易实现,但由于我们需要解析资源节,而且我不想让文章更长,所以我只提供一个提示。
您可以查看这篇有用的文章。
您可以在此处下载打包器和解包器存根最终版本的完整源代码。
打包器:额外内容 + 改进技巧
这里有一些关于 PE 打包器改进的技巧和额外指南,你可以使用。
提示 1:更新校验和
完成整个后期处理后,是时候更新 PE 文件校验和了,它位于:
OptionalHeader.CheckSum = 0xFFFFFFFF;
有效的校验和对于从恶意软件扫描器获得更好的结果非常重要。您可以查看这篇文章,了解 PE 文件的校验和计算。
提示 2:添加代码签名和签名
我们的打包 PE 文件不遵循任何著名编译器的标准,这可能会导致一些杀毒软件出现问题。如果您是一名合格的程序员,代码签名有助于解决此问题,获取有效的证书并在后期处理代码中使用 signtool.exe
。
提示 3:清单支持
如果您想克隆输入文件的清单,以向打包的 PE 添加额外详细信息,例如何时需要管理员权限等。您应该解析资源目录并从那里提取它。
提示 4:.NET 支持
要添加 .NET 支持,您可以采用困难的方法(操作 .net PE 结构)或使用原生的 **CLR 托管**(推荐)。您可以查看我的 clr 托管文章,顺便说一下,这篇文章有点旧了,现在可以通过更好得多的方式托管 clr,也许我将来会写一篇关于它的文章,谁知道呢?:)
因此,将 .net 程序集打包到数据节中,并在解包器存根中使用 clr 托管从内存加载它,您也可以使用 .Net Core Hosting。
提示 5:多层 PE 打包
我们的打包器的一个优点是它不会干扰输入 PE 结构来生成打包的 PE 文件,这意味着您可以在输出 PE 文件上使用任何其他打包器作为第二层压缩/保护!
是的!你可以简单地创建自己的保护系统和压缩,然后也使用一个著名的打包器,这样攻击者将面临两阶段的逆向工程,这会使他们的生活变得更加困难!
此外,我们的打包器还有一个有趣的地方是,你可以用同一个 PE 打包器无限次地重复打包打包后的 PE 文件,甚至每次随机化密钥和 IV!
提示 6:更高的 PE 压缩率
请记住,打包器只能通过**大尺寸**的 PE 文件才能获得最佳效果,并且不要忘记解包器存根本身也有大小,例如,如果您用 PE 打包器打包一个 1KB 的 dll,输出会大于输入文件,但如果您打包一个 100MB 的 dll,您会得到一个非常小的打包文件,具有很高的压缩比!
无论如何,在这种情况下,您仍然可以使用 **UPX** 对最终打包的 PE 文件进行压缩,以同时压缩解包器存根。
给疯子们的疯狂提示如果你想更疯狂一点,获取 upx 源代码并自定义它,为其添加额外的加密层!:v
提示 7:代码虚拟化
为了提高 PE 文件的安全性,请尝试一些提供代码虚拟化的产品。如果您在解包器存根代码上使用虚拟机,这将使逆向工程过程变得非常困难。
额外:关于重定位、非标准 PE 文件的注意事项
我们的 PE 打包器需要添加更多部分,例如处理重定位和非序数函数导出。强烈建议不要在非标准或已签名的 PE 文件(如 d3dcompiler_47.dll)上尝试打包器。
您可以向打包器添加新功能并提交到**HMPacker GitHub 仓库。**
额外:关于多语言 PE 文件的注意事项
此打包器未在多语言 PE 文件上测试过,然而从理论上讲它应该可以正常工作,请勿在没有备份的情况下将其用于 PE 文件,要添加功能齐全的多语言支持,您需要进行一些资源克隆。
额外:Marmoset Toolbag 3 上的真实世界测试
**让我们在 AAA 软件 Marmoset Toolbag 3 上测试我们的 PE 打包器!**Marmoset 3 有四个 PE 文件:
-
toolbag.exe:主应用程序文件,大小为 19,763,288 字节
-
substance_linker.dll:库文件,大小为 378,368 字节
-
substance_sse2_blend.dll:库文件,大小为 958,976 字节
-
python36.dll:Python 库,大小为 3,555,992 字节
好的,现在让我们用我们的打包器来处理它们……
pack_marmoset.bat :
"%cd%\pe_packer.exe" "%cd%\toolbag.exe" "%cd%\toolbag_packed.exe"
"%cd%\pe_packer.exe" "%cd%\substance_sse2_blend.dll" "%cd%\substance_sse2_blend_packed.dll"
"%cd%\pe_packer.exe" "%cd%\substance_linker.dll" "%cd%\substance_linker_packed.dll"
"%cd%\pe_packer.exe" "%cd%\python36.dll" "%cd%\python36_packed.dll"
结果
**太棒了!**我们的打包器将 toolbag.exe 从 19,763,288 字节减少到 5,169,152 字节!让我们测试一下软件是否正常工作……
**它完美运行!**使用过程中没有崩溃,性能没有下降,而且非常干净……
**再次恭喜** 🍻
额外:使用杀软检查打包后的二进制文件
VirusTotal
这是一个使用 67 款杀毒软件的扫描报告,通过 VirusTotal 进行。其中只有 2 款杀毒软件将打包后的 toolbag 检测为误报,这可以通过向一个虚假节添加虚假裸露软件代码来解决。一些杀毒软件,特别是像 SecureAge APEX 这样的 AI 驱动的杀毒软件,会标记所有没有明确指令的 PE 文件,因此我们高度加密的打包文件被标记了。但是,通过添加一些原始 C++ 代码,标记就会消失。
请友善!注意:这个技巧不适用于包含真实恶意代码的应用程序,同时请做一个好人,不要利用科学对抗人民,那样不好。
AntiScan
这是使用 AntiScan 对 26 款杀毒软件的扫描结果,其中没有一款杀毒软件将打包的工具包检测为误报!
额外:仔细查看打包后的二进制文件
在文章结束之前,让我们更仔细、更技术性地看看我们的打包器生成的二进制文件。
-
所有著名的 PE 检测器都无法识别打包的工具包。
-
我们的 PE 文件具有自定义节,没有导入表,也没有从任何其他依赖项导入。
-
我们的 PE 文件具有 99% 的熵,这意味着它被高度压缩。
-
我们的 PE 打包文件与原始 PE 可执行文件相比,内存开销仅为约 12.5MB。
奖励:文章的黑暗版本
文章可以作为带有黑暗 GitHub 主题的单个 HTML 文件在此处找到。
致谢
我希望您喜欢这篇文章并从中受益。欢迎您将文章翻译成您的语言,但请不要忘记提及 CodeProject 上的原始链接和**作者姓名**。
根据 MIT 许可证授权
一份简短简单的许可协议,其条件仅要求保留版权和许可声明。根据许可协议的作品、修改和更大范围的作品可以在不同的条款下分发,且无需源代码。
由 Hamid.Memar 创作、开发和发布
2021 年 11 月 13 日