使用 Visual Studio 和 C++ 创建 Shellcode
学习如何使用 Visual Studio 2019 和 VC++ 以简单的步骤将任何代码转换为稳定的 Shellcode!
引言
Shellcode 是攻击和黑客活动中最受欢迎的事物之一,它被描述为许多不同的方式,但让我们看看维基百科怎么说。
Wikipedia在黑客攻击中,shellcode 是一小段代码,用作软件漏洞利用中的载荷。
它被称为“shellcode”,因为它通常会启动一个命令 shell,攻击者可以从中控制受感染的机器,但任何执行类似任务的代码都可以称为 shellcode。
是的。这是非常正确的,但我将 shellcode 描述为一段可以在运行时动态分配和执行的代码,并且它不依赖于操作系统或编程运行时的任何静态 API。
这可能是错误的,但这是我在本文中要做的最好的名称,所以……
我们开始吧!
背景
我最终做这种事情的原因是:
- 提高我的编程技能和对计算机工作原理的理解。
- 提高我的应用程序和游戏的安全性。
- 这太有趣了!
是的,当然!它可以用于坏的和恶意的目的,但你知道……这是一把剑!保护还是伤害……取决于你!
通过使用这种技术,您可以将代码的关键部分(如许可证检查)转换为 shellcode 并将其加密到您的应用程序中。当您需要批准许可证时,只需将代码解密到堆空间并执行它,完成后,您就可以从内存中处理并释放该代码……这使得代码更难调试并防止静态程序分析。
准备项目和环境
1.所需工具和软件
- Visual Studio 2019
- VC++ 构建工具
- CFF Explorer (PE 查看器/编辑器)
- HxD (十六进制编辑器)
2.创建空项目
- 打开 Visual Studio 2019
- 创建两个空的 C++ 项目。
- 将一个命名为
code_gen
,另一个命名为code_tester
。 - 将
code_gen
的配置类型设置为“动态库 (*.dll)”。 - 将
code_tester
的配置类型设置为“应用程序 (*.exe)”。 - 将项目设置为x64 和 Release模式。
3.配置动态库为 API 独立 PE 文件
目前,我们的code_gen
依赖于 CRT (C Runtime) 和 Windows 内核。请按照以下步骤使其完全独立。
- 向
code_gen
添加一个.cpp 文件(而不是 .c 文件)并在其中编写基本代码。// code.cpp extern "C" bool _code() { return true; }
- 转到
code_gen
项目设置并配置以下选项:- 高级 > 使用调试库:否
- 高级 > 全程序优化:无全程序优化
- C/C++ > 常规 > 调试信息格式:无
- C/C++ > 常规 > SDL 检查:否 (/sdl-)
- C/C++ > 代码生成 > 启用 C++ 异常:否
- C/C++ > 代码生成 > 运行时库:多线程 (/MT)
- C/C++ > 代码生成 > 安全检查:禁用安全检查 (/GS-)
- C/C++ > 语言 > 兼容模式:否
- C/C++ > 语言 > C++ 语言标准:ISO C++17 标准 (/std:c++17)
- 链接器 > 输入 > 其他依赖项:空
- 链接器 > 输入 > 其他依赖项 > 取消勾选继承父项或项目默认值
- 链接器 > 输入 > 忽略所有默认库:是 (/NODEFAULTLIB)
- 链接器 > 调试 > 生成调试信息:否
- 链接器 > 调试 > 生成映射文件:是 (/MAP)
- 链接器 > 调试 > 子系统:原生 (/SUBSYSTEM:NATIVE)
- 链接器 > 优化 > 引用:否 (/OPT:NOREF)
- 链接器 > 高级 > 入口点:_code
- 链接器 > 高级 > 无入口点:是 (/NOENTRY)
注意通过将入口点属性更改为 _code,我们阻止了仅资源 DLL 的生成,并且我们告诉编译器不要使用 CRT 入口点。
4.配置测试器应用程序
测试器不需要任何特殊配置,目前我们将只关注代码生成、操作和执行。
向code_tester
添加一个main.cpp文件并在其中编写基本代码。
// main.cpp
#include <windows.h>
#include <iostream>
using namespace std;
int main()
{
return EXIT_SUCCESS;
}
在这篇文章的下一部分,我将教您如何从头开始创建具有自定义结构的应用程序。
好了,现在我们都准备好了!此外,如果您是那种懒惰的天才,可以从此处下载基本设置源。 ;)
基本方法
让我们从一些简单基础的数学开始,将_code
函数更改为如下内容:
extern "C" int _code(int x, int y)
{
return x * y + (x + y);
}
现在编译,您应该会得到 DLL,如果没有……请再次检查所有步骤,并确保您的配置都正确。
我们只需要.dll和.map文件,我们的 DLL 文件包含汇编后的 x64 机器代码,而我们的 map 文件包含有关代码在代码映射的虚拟内存空间中使用的地址的信息。但 map 文件中最重要的是我们代码在虚拟内存空间中的地址和偏移量。
- 使用 CFF Explorer 打开code_gen.dll。
正如您所见,我们的 DLL 没有导入/导出地址表(IAT/EAT),我们只有三个节,我们不需要后两个。一个包含调试目录数据,一个包含文件版本等默认资源。我们要找的代码在.text 节中,它包含机器码。
我们从节区域(PE 查看器)中需要的唯一信息是.text 节的虚拟地址和原始地址。
- 在 HxD 或任何其他十六进制编辑器中打开code_gen.dll,按Ctrl+G或选择Search->Goto...并输入原始地址……这就是我们要找的代码!很简单,对吧?
C3操作码表示RETURN,这表明这是我们函数的结束,我们根本不需要零字节。
- 选择字节,然后在菜单栏中选择Edit -> Copy as -> C,然后将其粘贴到main.cpp中。
代码看起来应该像这样:
#include <windows.h> #include <iostream> using namespace std; unsigned char _code_raw[9] = { 0x8D, 0x42, 0x01, 0x0F, 0xAF, 0xC1, 0x03, 0xC2, 0xC3 }; int main() { return EXIT_SUCCESS; }
- 现在我们应该创建函数类型定义,在全局作用域中添加这段代码:
typedef int(*_code_t)(int, int);
- 是时候将我们的原始代码访问标志设置为可执行了,以便 CPU 可以执行它,在 main 中添加这段代码:
DWORD old_flag; VirtualProtect(_code_raw, sizeof _code_raw, PAGE_EXECUTE_READWRITE, &old_flag);
- 最后一步是执行,这很简单,在 return 前添加这段代码:
_code_t fn_code = (_code_t)(void*)_code_raw; int x = 500; int y = 1200; printf("Result of function : %d\n", fn_code(x, y));
- 构建
code_tester
并运行它,砰!它工作了!结果应该看起来像:code_tester.exe 的结果函数结果:601700
好吧,这是一个非常基本的方法,没有任何分配、加密/解密、压缩/解压缩等,但足以让您了解这里发生了什么。
现在……让我们进入下一个级别!
高级方法
在基本方法中,我们实际上拥有代码,因为它非常基础,但当涉及到更复杂的代码(如压缩、加密、许可证检查等)时,它就不再像这样简单了,代码可以在二进制文件的任何地址获取其偏移量,这就是为什么我们需要.map文件。
同样,在基本方法中,我们没有使用任何 C 运行时或 Windows API,但在现实世界中,我们需要它们很多……所以我们必须解决这个问题。(第二部分解释)
好了,在本篇文章中,我们将创建两个 shellcode,一个用于加密缓冲区,一个用于解密缓冲区。
- 将tiny-aes-c仓库克隆到您的计算机,只将aes.c和aes.h复制到
code_gen
项目中。 - 将code.cpp更改为如下:
// code.cpp extern "C" { #include "aes.h" bool _encrypt(void* data, size_t size) { // Encryption Code Area // return true; } }
注意如果您将code.cpp更改为code.c,则可以避免使用 extern "C",但将来您将无法使用任何 C++ 功能。不过,大多数 C++ 库都基于 C,并且它们都与此方法兼容。
tiny-aes-c基于 C 语言,并且只有一些 C 运行时函数会被编译器优化为纯机器码,这意味着我们的 shellcode 已经被编译器进行了高度优化,这是一个好事!
- 像这样编写加密代码,并且不要使用堆栈上的任何数据,编译后您的 DLL 文件中不得包含 .data 节。如果包含,请检查所有代码并将基于堆栈的数据放入函数内。
这是加密代码:
// code.cpp extern "C" { #include "aes.h" bool _encrypt(void* data, size_t size) { // Allocate data on heap struct AES_ctx ctx; unsigned char key[32] = { 0xBB, 0x17, 0xCA, 0x8C, 0x69, 0x7F, 0xA1, 0x89, 0x3B, 0xCF, 0xA8, 0x12, 0x34, 0x6F, 0xB6, 0xE8, 0x79, 0x89, 0xDA, 0xD0, 0x0B, 0xA9, 0xA1, 0x1B, 0x5B, 0x38, 0xD0, 0x4A, 0x20, 0x4D, 0xB8, 0x0E}; unsigned char iv[16] = { 0xA3, 0xF3, 0xD4, 0xC5, 0x5E, 0xCD, 0x41, 0xA6, 0x22, 0xC9, 0x8D, 0xE5, 0xA3, 0xBB, 0x29, 0xF1}; // Initialize encrypt context AES_init_ctx_iv(&ctx, key, iv); // Encrypt buffer AES_CBC_encrypt_buffer(&ctx, (uint8_t*)data, size); return true; } }
- 编译并使用 CFF Explorer 打开code_gen.dll。
正如您所见,DLL 中添加了一个.rdata 节。该节由
tiny-aes-c
为sbox
和rsbox
查找表生成,并且没有这些数据就无法使机器码工作。手动合并两个节的数据并重新定位机器码中的每个值需要大量时间,但……
有一个单行的神奇编译器指令可以帮助我们!在code.cpp的顶部添加以下代码:
#pragma comment(linker, "/merge:.rdata=.text")
- 再次编译并使用 CFF Explorer 打开code_gen.dll。
轰!它已修复,现在我们的代码直接从其上方较低地址分配的数据中获取。
- 在 HxD 或任何其他十六进制编辑器中打开code_gen.dll,按Ctrl+G或选择Search->Goto...并输入.text节的原始地址。按Ctrl+E或选择Edit->Select Block...并输入.text节的原始大小,然后将缓冲区复制为 C 数组并粘贴到头文件中,并将其添加到
code_tester
项目中,例如shellcode_encrypter_raw.h,并将数组命名为与文件名相同。注意为了方便提取代码,您可以直接在 CFF Explorer 中右键单击节并单击“转储节”,有时它会产生额外的大小,所以我坚持手动方式。
- 将
code_tester
的main.cpp文件更改为如下:// main.cpp #include <windows.h> #include <stdio.h> #include <fstream> #include <vector> #include "shellcode_encrypter_raw.h" using namespace std; typedef bool(*_encrypt)(void*, size_t); #define ENC_SC_RAW shellcode_encrypter_raw #define FUNCTION_OFFSET 0 int main(int argc, char* argv[]) { // Check for commands count if (argc != 4) return EXIT_FAILURE; // Get commands values char* input_file = argv[1]; char* process_mode = argv[2]; char* output_file = argv[3]; // Change code protection DWORD old_flag; VirtualProtect(ENC_SC_RAW, sizeof ENC_SC_RAW, PAGE_EXECUTE_READWRITE, &old_flag); // Declaring encrypt function _encrypt encrypt = (_encrypt)(void*)&ENC_SC_RAW[FUNCTION_OFFSET]; // Read input file to vector buffer ifstream input_file_reader(argv[1], ios::binary); vector<uint8_t> input_file_buffer(istreambuf_iterator<char>(input_file_reader), {}); // Add padding to input file data for (size_t i = 0; i < 16; i++) input_file_buffer.insert(input_file_buffer.begin(), 0x0); for (size_t i = 0; i < 16; i++) input_file_buffer.push_back(0x0); // Encrypting file buffer if (strcmp(process_mode, "-e") == 0) encrypt(input_file_buffer.data(), input_file_buffer.size()); // Save encrypted buffer to output file fstream file_writter; file_writter.open(output_file, ios::binary | ios::out); file_writter.write((char*)input_file_buffer.data(), input_file_buffer.size()); file_writter.close(); // Code successfully executed printf("OK"); return EXIT_SUCCESS; }
- 好的,下一步是找到 shellcode 中
_encrypt
函数的地址偏移量,用文本编辑器打开code_gen.map。(我使用 Notepad++。) - 搜索
_encrypt
,您应该会找到这一行:0001:000012c0 _encrypt 00000001800022C0 f code.obj
现在我们知道函数在虚拟偏移量中的偏移量,但我们需要它的实际偏移量。我们的虚拟偏移量是0x22C0。
- 返回 CFF Explorer 并查找.text节的虚拟地址,即0x1000。现在唯一需要做的就是减去它们,得到0x12C0,这就是我们的偏移量,将值替换到代码中。
#define FUNCTION_OFFSET 0x12C0
- 编译并使用命令行进行测试:
code_tester.exe some_image.jpg -e some_image_encrypted.jpg
[核爆炸!]
EXE 的结果是OK,生成的文件使用 AES-256 完全加密!
- 好了,要生成解密器 shellcode,请遵循完全相同的步骤,除了:
- 使用
AES_CBC_decrypt_buffer
而不是AES_CBC_encrypt_buffer
。 - 将类型定义更改为如下:
typedef bool(*_crypt)(void*, size_t);
这是main.cpp代码应该的样子:
// main.cpp #include <windows.h> #include <stdio.h> #include <fstream> #include <vector> #include "shellcode_encrypter_raw.h" #include "shellcode_decrypter_raw.h" using namespace std; typedef bool(*_crypt)(void*, size_t); #define ENC_SC_RAW shellcode_encrypter_raw #define DEC_SC_RAW shellcode_decrypter_raw #define FUNCTION_OFFSET 0x12C0 int main(int argc, char* argv[]) { // Check for commands count if (argc != 4) return EXIT_FAILURE; // Get commands values char* input_file = argv[1]; char* process_mode = argv[2]; char* output_file = argv[3]; // Validate process mode if (strcmp(process_mode, "-e") != 0 && strcmp(process_mode, "-d") != 0) return EXIT_FAILURE; // Change code protection DWORD old_flag; VirtualProtect(ENC_SC_RAW, sizeof ENC_SC_RAW, PAGE_EXECUTE_READWRITE, &old_flag); VirtualProtect(DEC_SC_RAW, sizeof DEC_SC_RAW, PAGE_EXECUTE_READWRITE, &old_flag); // Declaring encrypt function _crypt encrypt = (_crypt)(void*)&ENC_SC_RAW[FUNCTION_OFFSET]; _crypt decrypt = (_crypt)(void*)&DEC_SC_RAW[FUNCTION_OFFSET]; // Read input file to vector buffer ifstream input_file_reader(argv[1], ios::binary); vector<uint8_t> input_file_buffer(istreambuf_iterator<char>(input_file_reader), {}); // Add padding to input file data if(strcmp(process_mode, "-d") == 0) goto SKIP_PADDING; for (size_t i = 0; i < 16; i++) input_file_buffer.insert(input_file_buffer.begin(), 0x0); for (size_t i = 0; i < 16; i++) input_file_buffer.push_back(0x0); // Encrypting/Decrypting file buffer SKIP_PADDING: if (strcmp(process_mode, "-e") == 0) encrypt(input_file_buffer.data(), input_file_buffer.size()); if (strcmp(process_mode, "-d") == 0) decrypt(input_file_buffer.data(), input_file_buffer.size()); // Save encrypted buffer to output file fstream file_writter; file_writter.open(output_file, ios::binary | ios::out); if (strcmp(process_mode, "-e") == 0) file_writter.write((char*)input_file_buffer.data(), input_file_buffer.size()); if (strcmp(process_mode, "-d") == 0) file_writter.write((char*)&input_file_buffer[16], input_file_buffer.size() - 32); file_writter.close(); // Code successfully executed printf("OK"); return EXIT_SUCCESS; }
- 使用
- 编译并使用命令行进行测试:
code_tester.exe some_image_encrypted.jpg -d some_image_decrypted.jpg
就这样!现在您有了两个执行加密/解密的小型 shellcode!
您可以在使用它们之后处理掉代码。此外,您可以使用不同的密钥压缩和加密它们,并在需要时即时解密它们。
结论
第一部分到此结束。在第二部分中,我们将使用相同的技术从头开始创建一个 EXE/DLL 打包器/保护器,但我们将深入研究更复杂的内容,如解析、混淆、调用重定向等。
希望您喜欢这篇文章。欢迎在评论区提问。
敬请期待!
历史
- 2021 年 6 月 7 日:初始版本