Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / security

Creating Shellcode from any Code Using Visual Studio and C++

4.91/5 (31 votes)
7 Jun 2021CPOL9 min read 54.1K   1.2K  
Learn how to convert any code to a stable shellcode using Visual Studio 2019 and VC++ in easy steps!
In this tutorial, we use Visual Studio 2019 and C++ to generate independent x64 machine code that can be used as shellcode, This operation needs extreme skills, but with this tutorial, you can generate your shellcode very easily...

Introduction

Shellcode is one of the most popular things in attacking and hacking, it's described in many different ways, but let's see what Wikipedia says:

Wikipedia:

In hacking, a shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability.
It is called "shellcode" because it typically starts a command shell from which the attacker can control the compromised machine, but any piece of code that performs a similar task can be called shellcode.

Yep. It's very correct but I describe shellcode as a piece of code that can be allocated and executed dynamically at runtime and it has no dependency on any static API of operating system or programming runtimes.

It may be wrong but this is the best name I could find for what we are going to do in this article, so...

Let's start!

Background

The reasons I ended up doing this kind of stuff are:

  1. Improving my programming skills and my understanding of how a computer works
  2. Improving security of my applications and games
  3. It's damn fun!

Yes of course! it can be used for bad and malicious purposes but you know... It's a sword! Protect or harm... it's up to you!

By using this technique, you can convert critical parts of your code like license checking to shellcode and encrypt it in your application. When you need to approve a license, you just decrypt the code to a heap-based space and execute it, after you finished, you dispose and release the code from memory... this makes the code harder to debug and prevent static program analysis.

Preparing Project and Environment

1. Required Tools & Software

  • Visual Studio 2019
  • VC++ Build Tools
  • CFF Explorer ( PE Viewer/Editor )
  • HxD (Hex Editor)

2. Creating Empty Projects

  1. Open Visual Studio 2019
  2. Create two empty C++ projects.
  3. Name one code_gen and other one code_tester
  4. Set code_gen Configuration Type to "Dynamic Library (.dll)"
  5. Set code_tester Configuration Type to "Application (.exe)"
  6. Set projects to x64 and Release mode.

3. Configuring Dynamic-Library to API Independent PE File

Currently, our code_gen is depended on CRT (C Runtime) and Windows Kernel. Follow the steps to make it fully independent.

  1. Add a .cpp (not .c) file to code_gen and write this basic code in it:
    C++
    // code.cpp
    extern "C" bool _code()
    {
        return true;
    }
  2. Go to code_gen project settings and configure the following options:
    • Advanced > Use Debug Libraries: No
    • Advanced > Whole Program Optimization: No Whole Program Optimization
    • C/C++ > General > Debug Information Format: None
    • C/C++ > General > SDL checks: No (/sdl-)
    • C/C++ > Code Generation > Enable C++ Exceptions: No
    • C/C++ > Code Generation > Runtime Library: Multi-threaded (/MT)
    • C/C++ > Code Generation > Security Check: Disable Security Check (/GS-)
    • C/C++ > Language > Conformance mode: No
    • C/C++ > Language > C++ Language Standard: ISO C++17 Standard (/std:c++17)
    • Linker > Input > Additional Dependencies: Empty
    • Linker > Input > Additional Dependencies > Uncheck Inherit parent or project defaults
    • Linker > Input > Ignore All Default Libraries: Yes (/NODEFAULTLIB)
    • Linker > Debugging > Generate Debug Info: No
    • Linker > Debugging > Generate Map File: Yes (/MAP)
    • Linker > Debugging > SubSystem: Native (/SUBSYSTEM:NATIVE)
    • Linker > Optimization > References: No (/OPT:NOREF)
    • Linker > Advanced > Entry Point: _code
    • Linker > Advanced > No Entry Point: Yes (/NOENTRY)
    NOTE:

    By changing entry point property to _code, we prevent resource only DLL generation, also we tell the compiler not to use CRT entrypoint.

4. Configuring Tester Application

Tester doesn't need any special configs, currently, we will focus only on code generation, manipulation and execution.

Add a main.cpp file to code_tester and write this basic code in it:

C++
// main.cpp

#include <windows.h>
#include <iostream>

using namespace std;

int main()
{
    return EXIT_SUCCESS;
}
In the next part of this article, I will teach you how to create applications with your own custom structure.

Alright, now we're all set and ready! Also, you can download the basic setup source here if you're a lazy genius kind of person. ;)

Basic Approach

Let's start from something small and basic using only math, change the _code function to this:

C++
extern "C" int _code(int x, int y)
{
    return x * y + (x + y);
}

Now compile and you should get the DLL, if not... check all the steps again and make sure your configuration is all correct.

Image 1

We only need .dll and .map file, our DLL file contains the assembled x64 machine code and our map file contains information about the addresses used by the linker, but the most important thing inside the map file is the address and offset of our code inside the virtual memory space that our code gets mapped.

  1. Open code_gen.dll with CFF Explorer.

    Image 2

    As you can see, our DLL has no import/export address table (IAT/EAT) and we have only three sections, we don't need the last two of them, one contains debug directory data and one contains default resources like file version. The code we are looking for is inside .text section which contains machine codes.

    The only information we need from section area (PE Viewer) is .text section Virtual Address and Raw Address.

  2. Open code_gen.dll inside HxD or any other hex editor, Press Ctrl+G or select Search->Goto... and enter the raw address... That's the code we're looking for! Pretty easy, right?

    Image 3

    C3 opcode means RETURN which shows it's the end of our function, we don't need the zero bytes at all.

  3. Select the bytes and select Edit -> Copy as -> C from menu bar and then paste it inside main.cpp.

    The code should look like this:

    C++
    #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. Now we should create our function type definition, add this code in global scope:
    C++
    typedef int(*_code_t)(int, int);
  5. It's time to set our raw code access flag to executable so CPU can execute it, add this code in main:
    C++
    DWORD old_flag;
    VirtualProtect(_code_raw, sizeof _code_raw, PAGE_EXECUTE_READWRITE, &old_flag);
  6. And the last step is execution which is pretty simple, add this code before return:
    C++
    _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. Build code_tester and run it, Bam! It's working! Result should look like:
    Result of code_tester.exe:

    Result of function : 601700

Well, this was a very basic approach without any allocation, encryption/decryption, compression/decompression, etc., but good enough to give you an understanding of what's happening here.

Now... let's go to the next level!

Advanced Approach

In the basic approach, we had the actual code because it was very basic but when it comes to more complex codes like compression, encryption, license checking, etc., it's not easy like this anymore, code can get its offset at any address in the binary and that's why we need .map file.

Also in basic approach, we didn't use any c runtime or Windows API but in the real world, we need them a lot... so we have to solve this issue as well. (Explained in Part 2)

Alright, in this part of the article, we create two shellcodes, one for encrypting buffer and one for decrypting buffer.

  1. Clone tiny-aes-c repo to your computer, copy only aes.c and aes.h in code_gen project.
  2. Change the code.cpp to this:
    C++
    // code.cpp
    extern "C"
    {
        #include "aes.h"
    
        bool _encrypt(void* data, size_t size)
        {
            // Encryption Code Area //
            return true;
        }
    }
    NOTE:

    You can avoid using extern "C" if you change code.cpp to code.c but you can't use any C++ feature in future, anyway most of the C++ libraries are based on C and they are all compatible with this method.

    tiny-aes-c is based on C language and there's only some C runtime functions that the compiler will optimize to pure machine codes, this means our shellcode is already heavily optimized by compiler and this is a good thing!

  3. Write the encryption code like this and don't use any data on stack, you must not have a .data section in your DLL file after compile, If you do check all the codes and put stack-based data inside the functions.

    Here's the code for encryption:

    C++
    // 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. Compile and open code_gen.dll with CFF Explorer:

    Image 4

    As you see, there's a .rdata section added to the DLL, this section is generated by tiny-aes-c for sbox and rsbox lookup tables and it's not possible to make the machine code work without these data.

    Manually merging two sections data and re-addressing every value in the machine code is pretty much hard and needs a lots of time but...

    There's a one-line of magical compiler directive that helps us here! Add the following code at the top of code.cpp:

    C++
    #pragma comment(linker, "/merge:.rdata=.text")
  5. Compile again and open code_gen.dll with CFF Explorer:

    Image 5

    Boom! It's fixed, now our code directly gets feed from allocated data in lower addresses above itself.

    Image 6

  6. Open code_gen.dll inside HxD or any other hex editor, Press Ctrl+G or select Search->Goto... and enter the .text section raw address. Press Ctrl+E or select Edit->Select Block... and enter the .text section raw size, then copy the buffer as c array and paste it inside a header file and add it to code_tester project like shellcode_encrypter_raw.h and name the array same as file.
    NOTE:

    For ease of code extraction you can directly right click on the section in CFF Explorer and click on dump section, sometimes it produce extra size so I stick to the manual way.

  7. Change code_tester main.cpp file to this:
    C++
    // 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. Ok, the next step is finding the address offset of _encrypt function in the shellcode, open code_gen.map with a text editor. (I use Notepad++.)
  9. Search for _encrypt and you must find this line:
    C++
    0001:000012c0       _encrypt                   00000001800022C0 f   code.obj
    

    Now we know our function offset in virtual offset but we need its actual offset, our virtual offset is 0x22C0.

  10. Head back to the CFF Explorer and check for .text section virtual address which is 0x1000, Now the only thing we need to do is subtract them which gives us 0x12C0 and this is our offset, replace the value inside the code:
    C++
    #define FUNCTION_OFFSET 0x12C0 
  11. Compile and test it using command line:
    C++
    code_tester.exe some_image.jpg -e some_image_encrypted.jpg

    [ NUCLEAR EXPLOSION ! ]

    The result of EXE is OK and the generated file is completely encrypted using AES-256!

  12. Alright, to generate decrypter shellcode, follow the exact steps except:
    1. Use AES_CBC_decrypt_buffer instead of AES_CBC_encrypt_buffer
    2. Change type definition to this:
    C++
    typedef bool(*_crypt)(void*, size_t);

    Here's how the main.cpp code should look:

    C++
    // 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. Compile and test it using command line:
    C++
    code_tester.exe some_image_encrypted.jpg -d some_image_decrypted.jpg

And that's it! Now you have two small shellcodes that do the encryption/decryption for you!

You can dispose the code after you used them. Also, you can compress and encrypt them using different keys and decrypt them just on the fly when you need them.

Conclusion

This is the end of Part 1. In Part 2, we will create an EXE/DLL packer/protector from scratch using only C++ by the same technique but we get involved in much much complex stuff like resolving, obfuscation, call redirections and etc.

I hope you enjoyed the article. Feel free to ask your questions in the comments.

Stay tuned!

History

  • 7th June, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)