
看起来令人生畏?试试演示文稿版本,它总结了本 README。YouTube 视频即将推出!
加壳器是一种在其自身地址空间(有时是另一个进程的地址空间)内解压缩并启动另一个程序的程序。它有时以攻击分析环境(如调试器和虚拟沙箱)的载体而闻名。它主要用于以下几件事:
从根本上说,加壳器只有几个基本步骤:
同样简单的是,加壳器只由几个部分组成:
构建加壳器可能很棘手,因为存根需要以某种方式构建并导入加壳器可执行文件中。对于 Windows 来说,学习 Visual Studio 的构建系统(超越简单编译)可能是一项繁重的任务。幸运的是,CMake 提供了一个简单、跨平台的构建系统,支持 Visual Studio,并且高度可定制!
本教程旨在教授以下内容:
如果你已经熟悉 C++ 和 CMake,可以随意跳过直接进入打包部分。否则,请继续阅读!
首先,让我们以某种方式证明这个构建系统有效,并且能创建正确的加壳可执行文件。安装好 CMake 和 Visual Studio 后,使用你选择的终端导航到本仓库的根目录,并执行以下命令:``` $ mkdir build $ cd build $ cmake ../
这将创建构建 packer 教程代码所需的项目文件。然后运行:```
$ cmake --build ./ --config Release
这将以 Release 模式构建 packer 项目。然后,你可以运行以下测试:```
$ ctest -C Release ./
如果一切顺利,test\_pack 和 test_unpack 应该会成功。如果你想亲自查看打包的结果,你应该会看到:```
$ ./packed.exe
I'm just a little guy!
让我们来聊聊实现这一切所涉及的所有关键部分。
因此,我们了解了加壳器的主要组件,但如何在开发周期中立即测试我们的加壳器呢?我们需要在整体项目中添加第三个二进制文件,以便正确测试我们二进制的加壳与脱壳过程。总体而言,我们需要三个项目:
我们还需要一个压缩库,以确保二进制文件能够压缩进 stub 可执行文件中。这里使用 zlib 会很合适。
我们的项目层级结构,初始阶段应该如下所示:``` packer/ +---+ CMakeLists.txt + dummy/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + stub/ | | | +---+ CMakeLists.txt | + src/ | | | +---+ main.cpp | + src/ | | | +---+ main.cpp | + zlib-1.2.13/ | +---+ CMakeLists.txt + ...
每个文件夹中的 main.cpp 目前可以简单地写成这样:```cpp
#include <iostream>
int main(int argc, char *argv[]) {
std::cout << "I'm just a little guy!" << std::endl;
return 0;
}
现在,我们应该确定我们要处理的是一个简单的依赖链,CMake 通过一些配置可以很好地为我们解析它:
让我们从 根项目(即 packer)开始进行我们的 CMake 配置。
CMake 通常要求指定一个最低版本,因为它已经存在很长时间,并支持长期使用旧版本。之后,我们可以将 packer 项目声明为一个 C++ 项目。```cmake
cmake_minimum_required(VERSION 3.24)
project(packer CXX)
我们还希望将我们的打包器声明为"MultiThreaded"(即 /MT)而不是"MultiThreadedDLL"(即 /MD),这样我们就不必担心运行时 DLL 依赖。```cmake
# this line will mark our packer as MultiThreaded, instead of MultiThreadedDLL
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
变量设置语句中尖括号内的语句称为生成器表达式,它帮助我们在需要的地方解析配置时数据,你会在整个文件中经常看到它。这个生成器表达式的作用是:当检测到配置为 Debug 时,输出字符串 Debug,否则不输出任何内容。这样,当选择 Debug 编译配置时,会提供 MultiThreadedDebug 运行时库;而当选择非调试编译配置(如 Release)时,则提供 MultiThreaded。请参阅条件生成器表达式以深入理解其中的原理。有关 CMAKE_MSVC_RUNTIME_LIBRARY 变量的更多信息,请参阅 CMake 文档。
CMake 允许你将源代码组织成层级结构,就像 Visual Studio 通过其界面创建项目时自动做的那样;它还会在你的文件夹中递归搜索匹配的文件名。在我们的配置文件中,我们对头文件(.hpp)、代码文件(.cpp)和资源脚本(.rc)设置了全局递归。对于我们的示例,我们实际上只需要 main.cpp,但对于更大的项目,了解这一点会很有用。```cmake
file(GLOB_RECURSE SRC_FILES ${PROJECT_SOURCE_DIR}/src/.cpp) file(GLOB_RECURSE HDR_FILES ${PROJECT_SOURCE_DIR}/src/.hpp) file(GLOB_RECURSE RC_FILES ${PROJECT_SOURCE_DIR}/src/*.rc)
source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Header Files" FILES ${HDR_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Source Files" FILES ${SRC_FILES}) source_group(TREE "${PROJECT_SOURCE_DIR}" PREFIX "Resource Files" FILES ${RC_FILES})
这里我应当说明,更准确地说,CMake 是一种 **构建系统**(make system)。它会根据给定的编译器,为当前环境生成相应的“构建系统”。在 Linux 上,这会为检测到(或指定)的编译器生成一个 makefile。而在 Windows 上,它会生成一个与我们所用 Visual Studio 版本兼容的 Visual Studio 工程,这意味着一旦你创建了 MSVC 构建系统,之后就可以直接使用 Visual Studio 完成一切操作——只要你愿意!归根结底,你是在用 CMake 来配置 Visual Studio。因此,你现在所做的,本质上就是在 Visual Studio 中创建项目里的文件树,就像 GUI 在创建项目时替你完成的那样。
接下来,我们需要添加我们所依赖的项目:```cmake
# this will add zlib as a build target
add_subdirectory(${PROJECT_SOURCE_DIR}/zlib-1.2.13)
# this will add our stub project
add_subdirectory(${PROJECT_SOURCE_DIR}/stub)
# this will add our test dummy project
add_subdirectory(${PROJECT_SOURCE_DIR}/dummy)
我之前提到过一点,packer 项目依赖 stub 项目。无论如何,packer 需要保留 stub 并对其进行操作,最终得到我们打包后的可执行文件。我们可以使用 Windows 资源文件 来最终将我们的 stub 可执行文件嵌入到 packer 二进制文件中,无论构建配置如何!除此之外,我们还可以使用 CMake 为我们生成这些文件,这样我们对构建出的可执行文件的引用在 CMake 项目中就是可靠的!现在让我们生成资源文件,以便我们可以将它们包含到项目中:```cmake
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.hpp" CONTENT "#pragma once\n#define IDB_STUB 1000\n") file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/$/stub.rc" CONTENT "#include <winresrc.h>\n#include "stub.hpp"\nIDB_STUB STUB "$<TARGET_FILE:stub>"\n")
`CMAKE_CURRENT_BINARY_DIR` 是包含当前构建目录的字符串。Visual Studio 会根据配置将二进制文件转储到不同文件夹中,因此我们使用 `$<CONFIG>` 生成器表达式来获取当前构建配置。我们还使用 `$<TARGET_FILE:stub>` 生成器表达式来输出存根二进制文件编译后的可执行文件名。当我们将这些文件(生成的 RC 文件和生成的头文件)包含到项目中时,就可以成功地将存根二进制文件集成到打包器项目中。