65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Visual Studio 和 C++ 创建 Shellcode

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (28投票s)

2021 年 6 月 7 日

CPOL

9分钟阅读

viewsIcon

56126

downloadIcon

1391

学习如何使用 Visual Studio 2019 和 VC++ 以简单的步骤将任何代码转换为稳定的 Shellcode!

引言

Shellcode 是攻击和黑客活动中最受欢迎的事物之一,它被描述为许多不同的方式,但让我们看看维基百科怎么说。

Wikipedia

黑客攻击中,shellcode 是一小段代码,用作软件漏洞利用中的载荷
它被称为“shellcode”,因为它通常会启动一个命令 shell,攻击者可以从中控制受感染的机器,但任何执行类似任务的代码都可以称为 shellcode。

是的。这是非常正确的,但我将 shellcode 描述为一段可以在运行时动态分配和执行的代码,并且它不依赖于操作系统或编程运行时的任何静态 API。

这可能是错误的,但这是我在本文中要做的最好的名称,所以……

我们开始吧!

背景

我最终做这种事情的原因是:

  1. 提高我的编程技能和对计算机工作原理的理解。
  2. 提高我的应用程序和游戏的安全性。
  3. 这太有趣了!

是的,当然!它可以用于坏的和恶意的目的,但你知道……这是一把剑!保护还是伤害……取决于你!

通过使用这种技术,您可以将代码的关键部分(如许可证检查)转换为 shellcode 并将其加密到您的应用程序中。当您需要批准许可证时,只需将代码解密到堆空间并执行它,完成后,您就可以从内存中处理并释放该代码……这使得代码更难调试并防止静态程序分析。

准备项目和环境

1.所需工具和软件

  • Visual Studio 2019
  • VC++ 构建工具
  • CFF Explorer (PE 查看器/编辑器)
  • HxD (十六进制编辑器)

2.创建空项目

  1. 打开 Visual Studio 2019
  2. 创建两个空的 C++ 项目。
  3. 将一个命名为code_gen,另一个命名为code_tester
  4. code_gen配置类型设置为“动态库 (*.dll)”
  5. code_tester配置类型设置为“应用程序 (*.exe)”
  6. 将项目设置为x64 和 Release模式。

3.配置动态库为 API 独立 PE 文件

目前,我们的code_gen依赖于 CRT (C Runtime) 和 Windows 内核。请按照以下步骤使其完全独立。

  1. code_gen添加一个.cpp 文件(而不是 .c 文件)并在其中编写基本代码。
    // code.cpp
    extern "C" bool _code()
    {
        return true;
    }
  2. 转到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 文件中最重要的是我们代码在虚拟内存空间中的地址和偏移量。

  1. 使用 CFF Explorer 打开code_gen.dll

    正如您所见,我们的 DLL 没有导入/导出地址表(IAT/EAT),我们只有三个节,我们不需要后两个。一个包含调试目录数据,一个包含文件版本等默认资源。我们要找的代码在.text 节中,它包含机器码

    我们从节区域(PE 查看器)中需要的唯一信息是.text 节的虚拟地址原始地址

  2. 在 HxD 或任何其他十六进制编辑器中打开code_gen.dll,按Ctrl+G或选择Search->Goto...并输入原始地址……这就是我们要找的代码!很简单,对吧?

    C3操作码表示RETURN,这表明这是我们函数的结束,我们根本不需要零字节。

  3. 选择字节,然后在菜单栏中选择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;
    }
  4. 现在我们应该创建函数类型定义,在全局作用域中添加这段代码:
    typedef int(*_code_t)(int, int);
  5. 是时候将我们的原始代码访问标志设置为可执行了,以便 CPU 可以执行它,在 main 中添加这段代码:
    DWORD old_flag;
    VirtualProtect(_code_raw, sizeof _code_raw, PAGE_EXECUTE_READWRITE, &old_flag);
  6. 最后一步是执行,这很简单,在 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));
  7. 构建code_tester并运行它,砰!它工作了!结果应该看起来像:
    code_tester.exe 的结果

    函数结果:601700

好吧,这是一个非常基本的方法,没有任何分配、加密/解密、压缩/解压缩等,但足以让您了解这里发生了什么。

现在……让我们进入下一个级别!

高级方法

在基本方法中,我们实际上拥有代码,因为它非常基础,但当涉及到更复杂的代码(如压缩、加密、许可证检查等)时,它就不再像这样简单了,代码可以在二进制文件的任何地址获取其偏移量,这就是为什么我们需要.map文件。

同样,在基本方法中,我们没有使用任何 C 运行时或 Windows API,但在现实世界中,我们需要它们很多……所以我们必须解决这个问题。(第二部分解释)

好了,在本篇文章中,我们将创建两个 shellcode,一个用于加密缓冲区,一个用于解密缓冲区。

  1. tiny-aes-c仓库克隆到您的计算机,只将aes.caes.h复制到code_gen项目中。
  2. 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 已经被编译器进行了高度优化,这是一个好事!

  3. 像这样编写加密代码,并且不要使用堆栈上的任何数据,编译后您的 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;
        }
    }
  4. 编译并使用 CFF Explorer 打开code_gen.dll

    正如您所见,DLL 中添加了一个.rdata 节。该节由tiny-aes-csboxrsbox查找表生成,并且没有这些数据就无法使机器码工作。

    手动合并两个节的数据并重新定位机器码中的每个值需要大量时间,但……

    有一个单行的神奇编译器指令可以帮助我们!在code.cpp的顶部添加以下代码:

    #pragma comment(linker, "/merge:.rdata=.text")
  5. 再次编译并使用 CFF Explorer 打开code_gen.dll

    轰!它已修复,现在我们的代码直接从其上方较低地址分配的数据中获取。

  6. 在 HxD 或任何其他十六进制编辑器中打开code_gen.dll,按Ctrl+G或选择Search->Goto...并输入.text节的原始地址。按Ctrl+E或选择Edit->Select Block...并输入.text节的原始大小,然后将缓冲区复制为 C 数组并粘贴到头文件中,并将其添加到code_tester项目中,例如shellcode_encrypter_raw.h,并将数组命名为与文件名相同。
    注意

    为了方便提取代码,您可以直接在 CFF Explorer 中右键单击节并单击“转储节”,有时它会产生额外的大小,所以我坚持手动方式。

  7. code_testermain.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;
    }
  8. 好的,下一步是找到 shellcode 中_encrypt函数的地址偏移量,用文本编辑器打开code_gen.map。(我使用 Notepad++。)
  9. 搜索_encrypt,您应该会找到这一行:
     0001:000012c0       _encrypt                   00000001800022C0 f   code.obj

    现在我们知道函数在虚拟偏移量中的偏移量,但我们需要它的实际偏移量。我们的虚拟偏移量是0x22C0

  10. 返回 CFF Explorer 并查找.text节的虚拟地址,即0x1000。现在唯一需要做的就是减去它们,得到0x12C0,这就是我们的偏移量,将值替换到代码中。
    #define FUNCTION_OFFSET 0x12C0 
  11. 编译并使用命令行进行测试:
    code_tester.exe some_image.jpg -e some_image_encrypted.jpg

    [核爆炸!]

    EXE 的结果是OK,生成的文件使用 AES-256 完全加密!

  12. 好了,要生成解密器 shellcode,请遵循完全相同的步骤,除了:
    1. 使用AES_CBC_decrypt_buffer而不是AES_CBC_encrypt_buffer
    2. 将类型定义更改为如下:
    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;
    }
  13. 编译并使用命令行进行测试:
    code_tester.exe some_image_encrypted.jpg -d some_image_decrypted.jpg

就这样!现在您有了两个执行加密/解密的小型 shellcode!

您可以在使用它们之后处理掉代码。此外,您可以使用不同的密钥压缩和加密它们,并在需要时即时解密它们。

结论

第一部分到此结束。在第二部分中,我们将使用相同的技术从头开始创建一个 EXE/DLL 打包器/保护器,但我们将深入研究更复杂的内容,如解析、混淆、调用重定向等。

希望您喜欢这篇文章。欢迎在评论区提问。

敬请期待!

历史

  • 2021 年 6 月 7 日:初始版本
© . All rights reserved.