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 or ExitProcess for free.
  • We have to manually:
    1. Find the memory address of those functions ourselves (e.g., from kernel32.dll),
    2. Put the right inputs (arguments) onto the stack,
    3. 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 (.exe0x6578652e)
    • ASCII: 'e' = 0x65, 'x' = 0x78, 'e' = 0x65, '.' = 0x2e
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 of WinExec
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 into eax 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 using VirtualAlloc which is more popular and suspicious and which is more closely investigated by the blue teamers.

See Run Shellcode for more Examples and techniques.