A Simple Windows Shellcode

2024-03-01

1. What Is A shellcode?

The name shellcode came from its original use to spawn a system shell in exploits after attackers successfully exploit vulnerabilities in software and redirect execution to the injected code. In general, a shellcode is a set of instructions that can be loaded and executed at any memory address (i.e. Position-independent code). Therefore, it cannot contain hard-coded addresses and must use reliable techniques to load or resolve addresses of the APIs/functions it needs.

Security researchers often spawn calc.exe from a shellcode as a proof of concept in Windows exploits. This blog presents how such a shellcode is written from head to toe and describes the techniques in detail.

You will need to have basic knowledge about x86 assembly, you can checkout my Hello World x86 if you haven’t done so

2. A Simple Shellcode

As usual, I will present the results before explaining what is behind the scenes. Because who doesn’t like to see results!?

The results are presented in two different forms:

  • C Source Code With Inline x86 Assembly
  • C Source Code That Executes The Compiled Shellcode.

2.1 C Source Code With Inline x86 Assembly

The inline x86 assembly (the portion between __asm { and }) shown in the block of code below is the shellcode we want to build. I added comments to explain each step, however, you might still want to check out Section 3 of this blog for details of the techniques used by the shellcode.

  1int main()
  2{
  3    __asm {
  4        ; Create a new stack frame
  5        mov ebp, esp   ; set base pointer to stack pointer
  6        sub esp, 0x20  ; reverve 32 bytes of stack space for local variables
  7
  8        ; Get kernel32.dll base address
  9        xor ebx, ebx             ; ebx = 0x00000000
 10        mov ebx, fs:[ebx + 0x30] ; ebx = &PEB
 11        mov ebx, [ebx + 0x0C]    ; ebx = &LDR
 12        mov ebx, [ebx + 0x1C]    ; ebx = &1st entry in InitOrderModuleList : ntdll.dll
 13        mov ebx, [ebx]           ; ebx = &2nd entry in InitOrderModuleList : kernelbase.dll
 14        mov ebx, [ebx]           ; ebx = &3rd entry in InitOrderModuleList : kernel32.dll
 15        mov eax, [ebx + 0x08]    ; eax = &kernel32.dll
 16        mov [ebp - 0x04], eax     ; [ebp - 0x04] = &kernel32.dll
 17
 18
 19        ; Get address of IMAGE_EXPORT_DIRECTORY within kernel32.dll
 20        mov ebx, [eax + 0x3C] ; ebx = Offset of NT_Header
 21        add ebx, eax          ; ebx = &NT_Header = Offset + &kernel32.dll
 22        mov ebx, [ebx + 0x78] ; ebx = RVA Image_Export_Directory = [&NT_Header + 0x78]
 23        add ebx, eax          ; ebx = &ExportTable = RVA + &kernel32.dll
 24
 25        ; Get address of AddressOfNames within the IMAGE_EXPORT_DIRECTORY
 26        mov edi, [ebx + 0x20] ; edi = RVA AddressOfNames
 27        add edi, eax          ; edi = &AddressOfNames = RVA + &kernel32.dll
 28        mov [ebp - 0x08], edi  ; [ebp - 0x08] = &AddressOfNames
 29
 30        ; Get address of AddressOfNameOrdinals within the IMAGE_EXPORT_DIRECTORY
 31        mov ecx, [ebx + 0x24] ; ecx = RVA AddressOfNameOrdinals
 32        add ecx, eax          ; ecx = &AddressOfNameOrdinals = RVA + &kernel32.dll
 33        mov [ebp - 0x0C], ecx  ; [ebp - 0x0C] = &AddressOfNameOrdinals
 34
 35        ; Get address of AddressOfFunctions within the IMAGE_EXPORT_DIRECTORY
 36        mov edx, [ebx + 0x1C] ; edx = RVA AddressOfFunctions
 37        add edx, eax          ; edx = &AddressOfFunctions = RVA + &kernel32.dll
 38        mov [ebp - 0x10], edx  ; [ebp - 0x10] = &AddressOfFunctions
 39
 40        ; Get NumberOfNames of the IMAGE_EXPORT_DIRECTORY
 41        mov edx, [ebx + 0x18] ; EDX = NumberOfNames
 42        mov [ebp - 0x14], edx  ; [ebp - 0x14] = NumberOfNames
 43
 44
 45        ; Prepare the stack string 'WinExec\x00'
 46        mov edx, 0x63657841   ; "cexA"
 47        shr edx, 8            ; shift-right 8 bits to create the string commented below
 48        push edx              ; "\x00cex"
 49        push 0x456E6957       ; "EniW"
 50        mov [ebp - 0x18], esp  ; [ebp - 0x18] = address of the stack string 'WinExec\x00'
 51        call my_GetProcAddress; return eax = &WinExec
 52
 53        ; Call WinExec(CmdLine, ShowState);
 54        ;   CmdLine   = "calc.exe"
 55        ;   ShowState = 0x00000001 = SW_SHOWNORMAL (displays a window)
 56        xor ecx, ecx    ; ecx = 0
 57        push ecx        ; pushing string terminator 0x00
 58        push 0x6578652e ; "exe."
 59        push 0x636c6163 ; "clac"
 60        mov ebx, esp    ; ebx = address of the stack string "calc.exe\x00"
 61        inc ecx         ; ecx = 0x00000001
 62        push ecx        ; uCmdShow  = 0x00000001 (SW_SHOWNORMAL) # 2nd argument
 63        push ebx        ; lpcmdLine = "calc.exe\x00"             # 1st argument
 64        call eax        ; call WinExec
 65
 66        ; Prepare the stack string  'ExitProcess\x00'
 67        xor ecx, ecx           ; ecx = 0
 68        mov ecx, 0x73736541    ; "sseA"
 69        shr ecx, 8             ; shift-right 8 bits to create the string commented below
 70        push ecx               ; "\x00sse"
 71        push 0x636F7250        ; "corP"
 72        push 0x74697845        ; "tixE"
 73        mov [ebp - 0x18], esp   ; [ebp - 0x18] =  address of the stack string "ExitProcess\x00"
 74        call my_GetProcAddress ; return eax = &ExitProcess
 75
 76        ; Call ExitProcess(ExitCode)
 77        xor edx, edx ; edx = 0
 78        push edx     ; ExitCode = 0
 79        call eax     ; ExitProcess(ExitCode)
 80
 81
 82        ; Get address by name of function
 83        ; input:  [ebp - 0x18] & target_function_name
 84        ;         [ebp - 0x04] : &kernel32.dll
 85        ;         [ebp - 0x08] : &AddressOfNames
 86        ;         [ebp - 0x0c] : &AddressOfNameOrdinals
 87        ;         [ebp - 0x10] : &AddressOfFunctions
 88        ;         [ebp - 0x14] : NumberOfNames
 89        ; output: eax = address of target_function_name if name_found
 90        ;         eax = 0 if not found.
 91
 92        my_GetProcAddress:
 93            xor eax, eax             ; eax = 0 (counter)
 94            mov edx, [ebp - 0x14]    ; edx = NumberOfNames
 95
 96        str_match_loop :
 97            mov edi, [ebp - 0x8]     ; edi = &AddressOfNames
 98            mov esi, [ebp - 0x18]    ; esi = &target_function_name
 99            xor ecx, ecx             ; ecx = 0
100            cld                      ; clear the direction flag to compare strings from left to right
101            mov edi, [edi + eax * 4] ; edi = RVA current_function_name = [&AddressOfNames + (Counter * 4)]
102            add edi, [ebp - 0x4]     ; edi = &current_function_name = RVA + &kernel32.dll
103            add cx, 0x8              ;  cx = len(target_function_name) + 1 (End NULL Byte)
104            repe cmpsb               ; compare first cx bytes of[&current_function_name] to target_function_name
105            jz name_found            ; If string at[&current_function_name] == target_function_name, then name_found
106            inc eax                  ; Else: counter ++
107            cmp eax, edx             ; Check eax == NumberOfNames ?
108            jb str_match_loop        ; If eax != NumberOfNames, loop: the next function name.
109            xor eax, eax             ; Else: AddressOfNames exhausted and target_function_name not found.Set eax = 0 to return
110            ret
111
112        name_found :
113            mov ecx, [ebp - 0xC]     ; ecx = &AddressOfNameOrdinals
114            mov edx, [ebp - 0x10]    ; edx = &AddressOfFunctions
115            mov ax, [ecx + eax * 2]  ;  ax = OrdinalNumber = [&AddressOfNameOrdinals + (counter * 2)]
116            mov eax, [edx + eax * 4] ; eax = RVA target_function_name = [&AddressOfFunctions + (OrdinalNumber * 4)]
117            add eax, [ebp - 0x4]     ; eax = &target_function_name = RVA + &kernel32.dll
118            ret
119    }
120    return 0;
121}

2.2 C Source Code That Executes The Compiled Shellcode

To obtain the compiled shellcode provided in this section, you can compile the inline x86 assembly code using masm or compile the entire C source code provided in Section 2.1 and extract the portion of the shellcode from the compiled executable. Feel free to choose any method that is available and easy for you.

 1#include <stdio.h>
 2#include <windows.h>
 3
 4int main() {
 5    unsigned char shellcode[] = \
 6      "\x8B\xEC\x83\xEC\x20\x33\xDB\x64\x8B\x5B"
 7      "\x30\x8B\x5B\x0C\x8B\x5B\x1C\x8B\x1B\x8B"
 8      "\x1B\x8B\x43\x08\x89\x45\xFC\x8B\x58\x3C"
 9      "\x03\xD8\x8B\x5B\x78\x03\xD8\x8B\x7B\x20"
10      "\x03\xF8\x89\x7D\xF8\x8B\x4B\x24\x03\xC8"
11      "\x89\x4D\xF4\x8B\x53\x1C\x03\xD0\x89\x55"
12      "\xF0\x8B\x53\x18\x89\x55\xEC\xBA\x41\x78"
13      "\x65\x63\xC1\xEA\x08\x52\x68\x57\x69\x6E"
14      "\x45\x89\x65\xE8\xE8\x36\x00\x00\x00\x33"
15      "\xC9\x51\x68\x2E\x65\x78\x65\x68\x63\x61"
16      "\x6C\x63\x8B\xDC\x41\x51\x53\xFF\xD0\x33"
17      "\xC9\xB9\x41\x65\x73\x73\xC1\xE9\x08\x51"
18      "\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74"
19      "\x89\x65\xE8\xE8\x05\x00\x00\x00\x33\xD2"
20      "\x52\xFF\xD0\x33\xC0\x8B\x55\xEC\x8B\x7D"
21      "\xF8\x8B\x75\xE8\x33\xC9\xFC\x8B\x3C\x87"
22      "\x03\x7D\xFC\x66\x83\xC1\x08\xF3\xA6\x74"
23      "\x08\x40\x3B\xC2\x72\xE4\x33\xC0\xC3\x8B"
24      "\x4D\xF4\x8B\x55\xF0\x66\x8B\x04\x41\x8B"
25      "\x04\x82\x03\x45\xFC\xC3";
26
27    // Allocate executable memory
28    void* exec_mem = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
29
30    if (exec_mem == nullptr) {
31        printf("Memory allocation failed!\n");
32        return 1;
33    }
34
35    // Copy the shellcode to the allocated memory
36    memcpy(exec_mem, shellcode, sizeof(shellcode));
37
38    // Cast the allocated memory to a function pointer
39    int (*funcPtr)() = (int(*)())exec_mem;
40
41    // Execute the function
42   funcPtr();
43
44
45    // Free the allocated memory
46    VirtualFree(exec_mem, 0, MEM_RELEASE);
47
48    return 0;
49}

3. Explanations

The main purpose of the shellcode is to spawn “calc.exe”. The shellcode can call the API function WinExec("calc.exe", SW_SHOWNORMAL) (See Microsoft Learn Document ). Keep in mind that a shellcode cannot use hardcoded addresses. Therefore it has to obtain the address to the API WinExec before being able to use the API.

It requires a bit of in-depth knowledge about the Windows operating system to resolve the address of WinExec. I will keep it simple by showing you the chain of actions needed to get the API’s address. You can do your research about the individual items to understand the details.

I suggest reading from the end of the chain (start from the goal) shown below to understand why the previous step is needed, and then you can follow the steps from start to end when you match them with the result source code provided in Section 2 of this blog.

Here we GO:

  1. Obtaining the address of Kernel32.dll:
  2. Obtaining the address of Kernel32.dll’s Image Export Directory (Export Address Table - EAT):
    • IMAGE_DOS_HEADER (kernel32.dll) –> IMAGE_NT_HEADERS –> IMAGE_OPTIONAL_HEADER32 –> IMAGE_DATA_DIRECTORY (Export) –> IMAGE_EXPORT_DIRECTORY (NumberOfNames, AddressOfFunctions, AddressOfNames, AddressOfNameOridnals).
    • Declarations of the structures above can be found here
  3. Querying API address by name:
    • Matching function name (AddressOfNames, NumberOfNames) –> Getting the corresponding ordinal (AddressOfNameOridnals) –> Getting the address of the API (AddressOfFunctions)

I visualised the steps in the remaining of this section.

3.1 Address of Kernel32.dll

Address of Kernel32.dll

3.2 Kernel32.dll’s Image Export Directory

Kernel32.dll Image Export Directory

3.3 Querying API Address By Name

Querying API address by name

Note: the function name “target” has the ordinal number of 2 in this example. Choosing a fixed ordinal allows me to visualise how the address of the function “target” is obtained.

4. Appended (Structure Declarations)

The declaration of the objects/structures used in this blog is summarized in this section for easy reference.

4.1 Thread Environment Block (TEB)

typedef struct _TEB {
  PVOID Reserved1[12];
  PPEB  ProcessEnvironmentBlock;
  PVOID Reserved2[399];
  BYTE  Reserved3[1952];
  PVOID TlsSlots[64];
  BYTE  Reserved4[8];
  PVOID Reserved5[26];
  PVOID ReservedForOle;
  PVOID Reserved6[4];
  PVOID TlsExpansionSlots;
} TEB, *PTEB;

4.2 Process Environment Block (PEB)

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;

4.3 LIST_ENTRY

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink;
  struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

4.4 PEB_LDR_DATA

typedef struct _PEB_LDR_DATA {
  BYTE       Reserved1[8];
  PVOID      SsHandle;
  LIST_ENTRY InLoadOrderModuleList;
  LIST_ENTRY InMemoryOrderModuleList;
  LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

4.5 LDR_DATA_TABLE_ENTRY

typedef struct _LDR_DATA_TABLE_ENTRY {
	LIST_ENTRY InLoadOrderLinks;
    LIST_ENTRY InMemoryOrderLinks;
    LIST_ENTRY InInitializationOrderLinks;
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

4.6 IMAGE_DOS_HEADER

typedef struct _IMAGE_DOS_HEADER {
    WORD  e_magic;      /* 00: MZ Header signature */
    WORD  e_cblp;       /* 02: Bytes on last page of file */
    WORD  e_cp;         /* 04: Pages in file */
    WORD  e_crlc;       /* 06: Relocations */
    WORD  e_cparhdr;    /* 08: Size of header in paragraphs */
    WORD  e_minalloc;   /* 0a: Minimum extra paragraphs needed */
    WORD  e_maxalloc;   /* 0c: Maximum extra paragraphs needed */
    WORD  e_ss;         /* 0e: Initial (relative) SS value */
    WORD  e_sp;         /* 10: Initial SP value */
    WORD  e_csum;       /* 12: Checksum */
    WORD  e_ip;         /* 14: Initial IP value */
    WORD  e_cs;         /* 16: Initial (relative) CS value */
    WORD  e_lfarlc;     /* 18: File address of relocation table */
    WORD  e_ovno;       /* 1a: Overlay number */
    WORD  e_res[4];     /* 1c: Reserved words */
    WORD  e_oemid;      /* 24: OEM identifier (for e_oeminfo) */
    WORD  e_oeminfo;    /* 26: OEM information; e_oemid specific */
    WORD  e_res2[10];   /* 28: Reserved words */
    DWORD e_lfanew;     /* 3c: Offset to extended header */
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

4.7 IMAGE_NT_HEADERS

typedef struct _IMAGE_NT_HEADERS {
  DWORD Signature; /* "PE"\0\0 */	/* 0x00 */
  IMAGE_FILE_HEADER FileHeader;		/* 0x04 */
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;	/* 0x18 */
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

4.8 IMAGE_NT_HEADERS64

typedef struct _IMAGE_NT_HEADERS64 {
  DWORD Signature;
  IMAGE_FILE_HEADER FileHeader;
  IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;

4.9 IMAGE_OPTIONAL_HEADER

typedef struct _IMAGE_OPTIONAL_HEADER {

  /* Standard fields */

  WORD  Magic; /* 0x10b or 0x107 */	/* 0x00 */
  BYTE  MajorLinkerVersion;
  BYTE  MinorLinkerVersion;
  DWORD SizeOfCode;
  DWORD SizeOfInitializedData;
  DWORD SizeOfUninitializedData;
  DWORD AddressOfEntryPoint;		/* 0x10 */
  DWORD BaseOfCode;
  DWORD BaseOfData;

  /* NT additional fields */

  DWORD ImageBase;
  DWORD SectionAlignment;		/* 0x20 */
  DWORD FileAlignment;
  WORD  MajorOperatingSystemVersion;
  WORD  MinorOperatingSystemVersion;
  WORD  MajorImageVersion;
  WORD  MinorImageVersion;
  WORD  MajorSubsystemVersion;		/* 0x30 */
  WORD  MinorSubsystemVersion;
  DWORD Win32VersionValue;
  DWORD SizeOfImage;
  DWORD SizeOfHeaders;
  DWORD CheckSum;			/* 0x40 */
  WORD  Subsystem;
  WORD  DllCharacteristics;
  DWORD SizeOfStackReserve;
  DWORD SizeOfStackCommit;
  DWORD SizeOfHeapReserve;		/* 0x50 */
  DWORD SizeOfHeapCommit;
  DWORD LoaderFlags;
  DWORD NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; /* 0x60 */
  /* 0xE0 */
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

4.10 IMAGE_OPTIONAL_HEADER64

typedef struct _IMAGE_OPTIONAL_HEADER64 {
  WORD  Magic; /* 0x20b */
  BYTE MajorLinkerVersion;
  BYTE MinorLinkerVersion;
  DWORD SizeOfCode;
  DWORD SizeOfInitializedData;
  DWORD SizeOfUninitializedData;
  DWORD AddressOfEntryPoint;
  DWORD BaseOfCode;
  ULONGLONG ImageBase;
  DWORD SectionAlignment;
  DWORD FileAlignment;
  WORD MajorOperatingSystemVersion;
  WORD MinorOperatingSystemVersion;
  WORD MajorImageVersion;
  WORD MinorImageVersion;
  WORD MajorSubsystemVersion;
  WORD MinorSubsystemVersion;
  DWORD Win32VersionValue;
  DWORD SizeOfImage;
  DWORD SizeOfHeaders;
  DWORD CheckSum;
  WORD Subsystem;
  WORD DllCharacteristics;
  ULONGLONG SizeOfStackReserve;
  ULONGLONG SizeOfStackCommit;
  ULONGLONG SizeOfHeapReserve;
  ULONGLONG SizeOfHeapCommit;
  DWORD LoaderFlags;
  DWORD NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

4.11 IMAGE_DATA_DIRECTORY

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

4. 12 IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY {
	DWORD	Characteristics;
	DWORD	TimeDateStamp;
	WORD	MajorVersion;
	WORD	MinorVersion;
	DWORD	Name;
	DWORD	Base;
	DWORD	NumberOfFunctions;
	DWORD	NumberOfNames;
	DWORD	AddressOfFunctions;
	DWORD	AddressOfNames;
	DWORD	AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORY,*PIMAGE_EXPORT_DIRECTORY;

5. In Memory Of Geoff Chappell

Geoff Chappell’s website is one of the most detailed and reliable sources for studying Windows Internals. Geoff’s website has benefited many new students and seasoned researchers. Geoff passed away on Sep 4, 2023. I am deeply thankful for his knowledge and hope he rests in peace.

Geoff’s website (https://www.geoffchappell.com/) might stop working in the future. If you cannot access his amazing works through the original website, you can use the website’s Internet archives. One of the archives, https://geoffchappellmirror.github.io/, is used in this blog.

6. References



More posts like this

Hello World x86

2024-02-01 | #category_assembly #topic_calling_convention #topic_decompiler #topic_inline_assembly #topic_machine_code #topic_stack #topic_stack_string #topic_x86

Hello, World!

Programming is one of the most important skills in computer science. Typically, beginners take their first step into a new programming language by writing the iconic “Hello, World!” program. To maintain this tradition and encourage beginners to establish a strong foundation for their future work, my first blog post will be about a “Hello, World!” program, this time written in x86 assembly.

Continue reading 