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 = ¤t_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[¤t_function_name] to target_function_name
105 jz name_found ; If string at[¤t_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:
- Obtaining the address of Kernel32.dll:
- Thread Environment Block (
TEB
) –> Process Environment Block (PEB
) –>PEB_LDR_DATA
–>LDR_DATA_TABLE_ENTRY
(kernel32.dll)
- Thread Environment Block (
- 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
- Querying API address by name:
- Matching function name (
AddressOfNames
,NumberOfNames
) –> Getting the corresponding ordinal (AddressOfNameOridnals
) –> Getting the address of the API (AddressOfFunctions
)
- Matching function name (
I visualised the steps in the remaining of this section.
3.1 Address of Kernel32.dll
3.2 Kernel32.dll’s Image Export Directory
3.3 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.