Shellcode
Creation

A walkthrough of shellcode creation concepts / A how to without the handholding.

Introduction

When I first started dabbling in malware and general computer based tom-foolery I knew what shellcode WAS, but always struggled with one simple question that wasn't quite explained by anyone:

// Question

"so... how u make that shit then?"

Use msfvenom! Okay, sure that's the pragmatic approach and will work in 99% of cases, however just getting shellcode is not the goal. I want the knowledge college. I wanna be the pro hackermans, get mad skillz, alladat.

That's what this post is going to be about. How to create shellcode (it's in the title...) and how every step along the way works, why it works and yada yada yada.

Oh yeah, keep it legal. The defense is op af and you will get owned real bad.

What is Shellcode

Put simply shellcode is raw bytes that the cpu will execute. Typically presented in hex because binary is unwieldy and hurts our human brains. I will use the Linux exit function to illustrate going from C code to shellcode.

C raw code
int main(int argc, char* argv[]) {
    //Do something 
    //Do something 
    exit(0) // Or return 0, for you l33t coders out there.
}

A compiler may generate assembly equivalent to:

x86 asm compiled code
; exit(0) on Linux x86-64
mov   rax, 60        ; syscall number for exit 
xor   rdi, rdi       ; exit code 0
syscall  ; invoke the kernel

Assembled, each instruction becomes raw bytes:

hex encoded bytes
; exit(0) on Linux x86-64
48 C7 C0 3C 00 00 00   ; mov rax, 60
48 31 FF               ; xor rdi, rdi
0F 05                  ; syscall

Lets break down how we got to these funny numbers. To start you will notice the first two instructions prefixed with 48, this is known as a register extension prefix, and is indicating to the cpu to expect 64-bit operand width. Next we will have the opcodes, these are the action that the cpu will take. For the first instruction the opcode is C7, corresponding to "Move Immediate"* or in ASM mov. If you are a particularly smart cookie you might guess that what comes next is telling the cpu where its moving something to. This byte is called the ModR/M byte, C0 in this instance meaning the rax register. Yes, 3C 00 00 00 is 60, but in little endian format.

// For turbo nerds

Technically C7 /0 corresponds to MOV r/m64, imm32, but I refuse to elaborate on that for now.

Concatenated into shellcode string notation:

c shellcode representation
char sc[] = "\x48\xC7\xC0\x3C\x00\x00\x00\x48\x31\xFF\x0F\x05";

The next step is to place that string into executable memory, call it and let the computer do its thing. The CPU sees no difference between these representations, they are the same bytes. The art of maldev is how you get it there and how you call it without some nosey "EDR" deleting your beautifully crafted puppycat.jpg.exe.

Writing The Code

When you write code, you likely do not mind how the compiler will structure the binary, this however, is very important to how we are actually going to extract our shellcode as the compiler bakes in a lot of noise:

PE32 typical compiled executable

                    +--------------------------------------------------+
                    | DOS Header                                       |
                    | "MZ"                                             |
                    +--------------------------------------------------+
                    | DOS Stub                                         |
                    | "This program cannot be run in DOS mode."        |
                    +--------------------------------------------------+
                    | PE Header                                        |
                    | "PE\0\0"                                         |
                    +--------------------------------------------------+
                    | Optional Header                                  |
                    | ImageBase, EntryPoint, Section Info, etc         |
                    +--------------------------------------------------+
                    | .text                                            |
                    | Executable code                                  |
                    | main(), CRT startup, compiler generated code     |
                    +--------------------------------------------------+
                    | .rdata                                           |
                    | Read-only data, strings, imports                 |
                    +--------------------------------------------------+
                    | .data                                            |
                    | Initialized global variables                     |
                    +--------------------------------------------------+
                    | .pdata                                           |
                    | Exception handling metadata                      |
                    +--------------------------------------------------+
                    | .rsrc                                            |
                    | Icons, manifests, version info                   |
                    +--------------------------------------------------+
                    | .reloc                                           |
                    | Relocation table                                 |
                    +--------------------------------------------------+
                    | Import Address Table                             |
                    | kernel32.dll, ntdll.dll, etc                     |
                    +--------------------------------------------------+

Our shellcode is usually hiding somewhere inside .text, surrounded by runtime initialization code, metadata, imports, relocations and other compiler-generated shit we neither need nor want. The cleaner our code is, the easier extraction becomes.

Sectioning

The easiest way to separate our code from the junk is to have it placed in a specific section that we can force to only contain our code. Lucky for us, modern compilers will allow us to do just that:

C section placement example
#include "myheader.h"

// GCC/Clang 
__attribute__((section(".mysection")))

// MSVC
#pragma code_seg(".mysection") 

int innocentfn(CONST PBYTE pl, CONST SIZE_T plsize) {
    // do stuff
}

With this our code will be contained within the .mysection section rather than .text which will aid us later when extracting.

Position Independence

When creating shellcode there is a necessity that the code can run in any place in memory that it happens to land. Meaning references to absolute addresses are banned, everything must be position independent without assuming anything about the current memory layout, because once you copy the shellcode elsewhere, those addresses still point to the old image base.

An example of code that is not explicitly position independent:

C absolute addressing style
#include <stdio.h>

int secretValue = 1337;

/* pointer stored in writable memory (relocation target) */
int *globalPtr = &secretValue;

void printInfo(void){
    printf("%d\n", *globalPtr);
}

int main(void){
    /* assumes globalPtr resolves correctly via loader fixups */
    printInfo();
    return 0;
}

This style of code demonstrates a hard dependency on a fixed address via a global pointer. The main idea is that the pointer value is assumed stable and stored in a relocatable slot. This will probably break when you remove it from a PE/ELF context, which is exactly what we are going to do.

And here is code that is accomplishing the same thing while avoiding absolute addressing:

C relative addressing style
#include <stdio.h>

static int secretValue = 1337;

static int* getPtr(void){
    /* compiler typically uses RIP-relative addressing here */
    return &secretValue;
}

void printInfo(void){
    int *ptr = getPtr();

    printf("%d\n", *ptr);
}

int main(void){
    printInfo();
    return 0;
}
// Homework

The astute ones among you may notice that calling printf() would not quite work in shellcode, since you would not be able to use the Import Address Table to find the printf function. And you would be correct. Your homework is to research how to dynamically resolve functions so that you can use them in shellcode. Have fun.

Compiling and Linking

Now we have reached the easy part, it really is just two commands until you move onto extraction. But there is some nuance to dig into on this.

We will use MSVC tools as an example for this:

cmd compile
cl.exe /c /GS- /O2 shellcode.c

Let's look at the options and what they are doing. The /c option is instructing the compiler to only create a .obj file and stop, since we want to handle the linking manually to manage the structure of the binary. The /GS- option disables the stack canary that MSVC adds to functions to prevent overflows. This requires a call to __security_check_cookie, which is an external C Runtime (CRT) function that will cause your shellcode to crash. The /O2 option is for optimizing the code. This will also help reduce the number of null bytes, which we will touch on later.

Next is to manually link the .obj file into a binary:

cmd link
link.exe /ENTRY:FunctionName /NODEFAULTLIB /FIXED shellcode.obj

This is where the nuance really lies, to start: /ENTRY:FunctionName is defining a manual entry point, by default programs start at main or WinMain, which are actually called by the CRT startup code. Since we are ditching the CRT, we tell the linker to start execution at our function instead. /NODEFAULTLIB does exactly what it suggests, disables the inclusion of default libraries. /FIXED Prevents the linker from creating a .reloc section. Since our code is position-independent by design, we nix the relocation table.

Extraction

Now we have a clean binary, parsing the PE at this point is trivial with a python library pefile:

Python PE section shellcode extractor
import pefile
import sys

def save_shellcode(exe_path):
    try:
        pe = pefile.PE(exe_path)
        target_section = None

        for section in pe.sections:
            if b".shellcode" in section.Name:
                target_section = section
                break

        if not target_section:
            print("[-] Could not find .shellcode section!")
            return

        raw_code = target_section.get_data()[: target_section.Misc_VirtualSize]

        c_string = ''.join(f'\\x{b:02x}' for b in raw_code)

        print("\n[CSTRING]\n")
        print(f"\"{c_string}\"")

    except Exception as e:
        print(f"[-] Error: {e}")


if __name__ == "__main__":
    save_shellcode("shellcode.exe")

The script iterates over the PE's section table looking for a section named .shellcode straightforward enough. The interesting part is this line:

Python PE section shellcode extractor
raw_code = target_section.get_data()[:target_section.Misc_VirtualSize]

get_data() pulls the sections raw bytes, but it returns data sized to the sections raw data size, that is, the size after the linker has padded it to meet the file alignment boundary (probably 512 bytes). That padding is just \x00 appended to fill the alignment gap.

Misc_VirtualSize on the other hand, is the field the linker writes to record the sections actual content size before any padding is applied.

By slicing get_data() to Misc_VirtualSize, you're stripping the alignment padding and keeping only the bytes that you want.

Null-Bytes

One immediate problem you will run into when converting raw shellcode into a cstring is the presence of null bytes. In C strings are terminated by a null byte, meaning any \x00 inside your shellcode will truncate execution prematurely if it is treated as a string literal.

In practice, this means raw output like: \x48\x00\xC7... shouldn't be embedded as a normal string. The string will end at the first null byte and you will silently lose the rest of your payload.

The simplest and easiest way to work around this is to store the shellcode in unsigned char[] and execute it as a buffer. Encoding, encryption, substitution, etc. are the more interesting solutions, but those belong in a post about evasion, not creation. This one's long enough.

Conclusion

That's pretty much it. That's how u make that shit. If you want to see examples of this being used to generate custom shellcode, feel free to check out the first maldev project I will totally finish one day stubcore.


← Back to Writeups Next Post →