C is the language the rest of the stack is written in. The kernel of every operating system you’ve ever used (Linux, the BSDs, the Mach core of macOS, the NT kernel that Windows is built on, the kernels embedded in routers and printers and your car’s infotainment system) is C, or C with a thin layer of C++ on top. Most of the system libraries you link against on any Unix-like system are C. The Python interpreter is C. The Ruby and PHP and Java reference implementations are C. Roughly half a century after Dennis Ritchie wrote the first version at Bell Labs in 1972, C is still the lingua franca of systems programming, and on engagement, the operator who can read C reads the source of most of the world’s running infrastructure.
For the operator, C earns its place for three reasons: most exploitable bugs in production software are C bugs (buffer overflows, use-after-free, format-string vulnerabilities, integer overflows that turn into heap corruption); shellcode is written in C and assembly because C is what produces the right kind of position-independent native binary; and a lot of the operator-side tooling that has to be small, fast, and statically linkable (loaders, droppers, kernel-driver implants) is C or one of its near-relatives. The other operator-relevant languages (Python for tooling, Go and Rust for implants, PowerShell on Windows targets) have replaced C in many domains, but they haven’t replaced it in the domains where the bytes have to be exactly the bytes you wrote.
This walkthrough covers enough C syntax to read other people’s code, then the offensive applications, then how C stacks up against Rust, Go, and Python for operator work in 2026.
Language background#
C was written by Dennis Ritchie at Bell Labs starting in 1972, evolving from Ken Thompson’s B language, which had itself evolved from Martin Richards’s BCPL. The motivation was pragmatic: Thompson was rewriting Unix to be portable across hardware, and the language had to be expressive enough to write an operating system in while compiling down to efficient machine code. C delivered on both, and Unix Version 4 (1973) was the first kernel written in it.
The standards lineage matters when you read older code. K&R C (1978, the first edition of Kernighan and Ritchie’s The C Programming Language) was the de facto standard for the first decade. ANSI C (1989, retroactively renamed C89, then C90 when ISO adopted it) introduced function prototypes and standardized the library. C99 added variable-length arrays, designated initializers, and the long long type. C11 added thread support, atomics, and _Generic. C17 was a bugfix release. C23 (formally ISO/IEC 9899:2024) added nullptr, true Boolean keywords, #embed, and some other modernizations. Most production code in 2026 targets C99 or C11; embedded and kernel work often uses extensions specific to GCC or Clang.
The mainstream compilers are GCC (the GNU Compiler Collection, originally Richard Stallman 1987), Clang (LLVM-based, Chris Lattner 2007), and MSVC (Microsoft’s compiler, used for Windows kernel and driver work). They produce mostly-compatible code; differences in extensions, warning levels, and exact undefined-behavior handling occasionally bite portability.
Variables and types#
Every C variable has a fixed type known at compile time. The basic types are integer (char, short, int, long, long long, plus signed/unsigned variants of each), floating-point (float, double, long double), and the absence-of-value void.
int age = 30; // typically 32 bits on 64-bit systems
float pi = 3.141592f; // 32-bit IEEE 754
double e = 2.718281828459045; // 64-bit IEEE 754
char initial = 'A'; // 8 bits, treated as small int
unsigned long long size = 0xFF; // 64 bits on most platforms
The fixed-width types in <stdint.h> (int8_t, int16_t, int32_t, int64_t, plus unsigned versions) are usually what the operator wants when sizes matter. The native int is “whatever size the platform thinks is fast,” which is convenient until you’re writing a binary protocol parser and discover that int is 16 bits on some embedded targets.
Beyond the basics, C builds up from four composition mechanisms:
- Arrays: a fixed-size sequence of elements of the same type.
int values[10]is ten contiguous ints. Arrays decay to pointers when passed to functions, which is one of the language’s most-used and most-bug-producing features. - Pointers: variables that hold a memory address.
int *p = &agemakesppoint toage. Dereferencing with*preads the value; pointers are the source of most C’s expressive power and most of its security vulnerabilities. - Structs: composite types that group related fields under one name.
struct { int x; int y; } point;declares a struct containing two ints. The layout in memory is field-by-field with potential padding for alignment. - Unions: like structs, but all fields share the same memory location.
union { int as_int; float as_float; }lets you reinterpret the same four bytes two ways. Used heavily in binary protocol code and reverse engineering.
Operators#
C’s operator set is what every C-family language inherited:
// Arithmetic: + - * / % (integer / truncates toward zero)
// Relational: == != < > <= >= (return 1 for true, 0 for false)
// Logical: && || ! (short-circuit evaluation)
// Bitwise: & | ^ << >> ~ (essential for binary protocols and shellcode)
// Assignment: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
Two quirks that bite C programmers and their security:
- Integer overflow on signed types is undefined behavior.
INT_MAX + 1doesn’t wrap predictably; the compiler is allowed to assume it doesn’t happen, and modern optimizers will silently delete code that “couldn’t” execute because it would require signed overflow. Many real-world vulnerabilities (CVE-2015-7547 in glibc’sgetaddrinfo, for example) are integer-overflow bugs that the compiler optimized in surprising ways. - The boolean result of comparisons is
int, not a separate type.if (x = 5)compiles fine (assignment, then test of the result), which is why every C codebase has a coding-standard rule about putting constants on the left side of comparisons (if (5 == x)) so a missing=becomes a compile error instead of a silent bug.
Control flow#
The usual C-family constructs:
// if / else if / else
if (number > 0) {
printf("positive\n");
} else if (number < 0) {
printf("negative\n");
} else {
printf("zero\n");
}
// switch (no implicit fall-through if you remember the break)
switch (day) {
case 1: printf("Monday\n"); break;
case 2: printf("Tuesday\n"); break;
// ...
default: printf("invalid\n");
}
// for loop
for (int i = 0; i < 5; i++) {
printf("%d\n", i);
}
// while loop
while (count > 0) {
count--;
}
// do-while (runs the body at least once)
do {
count++;
} while (count < 10);Switch fall-through (omitting break) is a common source of bugs and a deliberate feature in some patterns (Duff’s device being the canonical example). Modern compilers warn when fall-through looks accidental. C23 added [[fallthrough]] as an explicit attribute to mark intentional fall-through, mirroring what C++17 had since 2017.
Functions#
Functions are declared with a return type, name, parameter list, and body. Recursion works the obvious way:
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
int main(void) {
printf("5! = %d\n", factorial(5));
return 0;
}The other thing worth knowing about C functions for the operator: the calling convention is part of the binary interface. On 64-bit Linux (System V AMD64), the first six integer arguments go in RDI, RSI, RDX, RCX, R8, R9, and the return value comes back in RAX. On 64-bit Windows, the first four arguments go in RCX, RDX, R8, R9. This is what you need to know to read disassembled C function calls in Ghidra or IDA, and it’s what shellcode has to set up correctly when invoking imported functions.
C on engagement#
The offensive applications of C cluster into a few areas where its low-level nature is the right tool.
Writing exploits against C vulnerabilities#
Memory-safety bugs in C-language software produce most of the world’s exploitable vulnerabilities. Buffer overflows, use-after-free, double-free, format string vulnerabilities, type confusion, integer overflows that cascade into heap corruption. Writing exploits against these requires reading the vulnerable C code, identifying the corruption primitive, and crafting input that turns that primitive into control flow.
The classic vulnerable pattern:
#include <stdio.h>
#include <string.h>
void vulnerable_function(char *input) {
char buffer[64];
strcpy(buffer, input); // no bounds check
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
vulnerable_function(argv[1]);
return 0;
}strcpy writes until it hits a null terminator. The 64-byte buffer on the stack is sized for input that the function doesn’t validate. Input longer than 64 bytes plus saved-frame-pointer overwrites the return address; carefully crafted input redirects execution to attacker-controlled bytes elsewhere in memory.
Against this raw vulnerable code with no protections enabled, the historical Aleph One pattern works: fill the buffer with NOP sled + shellcode, overflow up to the saved return address, overwrite the return address with a pointer to somewhere in the NOP sled. The shellcode runs when the function returns.
Modern compilers turn this on by default in 2026:
- Stack canaries (
-fstack-protector-strongin GCC/Clang). A random value placed between the buffer and the saved return address; if the canary is overwritten, the program aborts before returning. Defeats naive overflow. - NX / DEP / W^X. Stack pages are non-executable, so shellcode placed on the stack can’t execute as code. Defeats naive shellcode injection; pushes attackers to ROP (return-oriented programming) instead.
- ASLR (Address Space Layout Randomization). Stack, heap, and library addresses randomize per process. Defeats naive shellcode placement; requires an information leak to reliably target.
- CFI (Control Flow Integrity, in newer Clang and MSVC). Validates indirect function calls against allowed targets, defeats some ROP chains.
- FORTIFY_SOURCE in glibc (
-D_FORTIFY_SOURCE=2or=3). Replaces dangerous functions likestrcpywith bounds-checked variants when the buffer size is known at compile time.
These defenses don’t make the vulnerable code safe; they make the exploitation chain longer and require additional primitives (info leaks, partial overwrites, ROP gadgets). The historical Phrack 49 Aleph One exploit chain is now the “first thirty minutes” of an exploit course; the actual modern exploit work is in the bypasses for each of the above.
Crafting shellcode#
Shellcode is the payload that runs after the vulnerability is triggered. The constraint set is unusual: position-independent (because it has to run wherever it lands in memory), self-contained (no library imports), and often subject to character restrictions (no null bytes if it’s going to be transported as a C string; ASCII-only if it has to pass through a text-input field).
The canonical Linux x86 (32-bit) shellcode that executes /bin/sh via execve():
char shellcode[] =
"\x31\xc0" // xor %eax, %eax ; clear EAX
"\x50" // push %eax ; null-terminator
"\x68\x2f\x2f\x73\x68" // push $0x68732f2f ; "//sh"
"\x68\x2f\x62\x69\x6e" // push $0x6e69622f ; "/bin"
"\x89\xe3" // mov %esp, %ebx ; EBX = "/bin//sh"
"\x50" // push %eax ; argv[1] = NULL
"\x53" // push %ebx ; argv[0] = path
"\x89\xe1" // mov %esp, %ecx ; ECX = argv
"\x99" // cdq ; EDX = 0 (envp)
"\xb0\x0b" // mov $0x0b, %al ; syscall 11 = execve
"\xcd\x80"; // int $0x80 ; syscall
int main(void) {
void (*func)(void) = (void (*)(void))shellcode;
func();
}Each instruction is a hand-chosen variant of what a normal execve() call would compile to, with the additional constraint that the byte sequence contains no null terminators (which would truncate it if delivered as a C string). The shellcode is x86-specific and Linux-specific; the int $0x80 syscall interface is the 32-bit Linux ABI. Modern Linux x86-64 uses the syscall instruction and different register conventions; Windows uses entirely different syscall mechanisms via the Win32 API. Each target needs its own shellcode.
To run this for testing, the binary needs to be compiled with -z execstack (allowing execution from the data section) and -no-pie -fno-stack-protector (disabling modern protections). None of these are defaults in 2026, which is the point: the example is pedagogical.
Building operator tooling in C#
Beyond exploits, C is the natural language for operator tools that have to be small, fast, or capable of doing things higher-level languages abstract away:
- Custom loaders and droppers that need a tiny statically-linked binary footprint.
- Kernel driver implants on Windows (signed-driver attack surface) and Linux (LKM-style rootkits).
- Binary analysis tools built on top of libraries like
radare2,capstone(disassembler), andunicorn(emulator), all of which are themselves C with bindings to higher-level languages. - Network-protocol fuzzers that need fine-grained control over wire bytes; AFL (Michał Zalewski) and libFuzzer are both C frameworks.
- Kernel and firmware reverse engineering where the target is C, so the operator-side tools that interact with it are usually C too.
The modern alternative for most of this work is Rust, covered in the comparison below.
C versus the other operator languages#
How C stacks up against the languages an operator actually uses for offensive work, organized around what matters per role:
| Aspect | C | Rust | Go | Python |
|---|---|---|---|---|
| Memory safety | None; manual mgmt, easy to corrupt | Borrow checker; safe by default | GC; safe by default | GC; safe by default |
| Performance | Fastest; baseline for everything else | Comparable to C | ~2-3x slower than C | Interpreted, ~50x slower |
| Binary size | Tiny (KB range for stripped binaries) | Small (~200KB-2MB typical) | Medium (5-15 MB typical) | N/A (interpreted) |
| Static linking | Trivial (musl, dietlibc for minimal builds) | Trivial | Trivial | N/A |
| Cross-compilation | Easy on POSIX, harder for Windows from Linux | Easy across all targets | Trivial across all targets | N/A |
| Shellcode / inline asm | Native; the language shellcode is conceived in | Supported but awkward | Not supported | Not supported |
| Reading other people’s code | Required for OS, kernel, library work | Required for modern systems work | Required for some C2 frameworks | Required for nearly all tool scripts |
| Vulnerability density | High; most CVEs are C | Low; type system prevents most classes | Low; GC and bounds checking | Low; interpreted overhead |
| Operator use cases | Exploits, shellcode, kernel implants | Modern droppers, loaders, signed binaries | Cross-platform implants, beacons | Operator-host tooling, automation |
| Compiler-supplied protections | Stack canaries, NX, FORTIFY_SOURCE, CFI | Built-in memory safety | Built-in memory safety | N/A (interpreted) |
The practical takeaway: C is irreplaceable for understanding what other C is doing (kernel reading, exploit development, reverse engineering), and is the natural language for shellcode and tiny operator-side binaries. For new offensive tooling that has to be safe and portable, Rust has been quietly eating C’s lunch since around 2020 (the Linux kernel started accepting Rust code in 2022; major projects like the BlackLotus successor research and modern bootkit work are increasingly Rust). Go is the cross-platform implant default. Python is what wraps all of them on the operator host.
Worth knowing: C continues to write itself into the world’s infrastructure faster than memory-safe alternatives can replace it. The CISA “Case for Memory Safe Roadmaps” (December 2023) and similar pushes from Microsoft, Google, and the Linux Foundation have not changed the basic dynamic that there is still an enormous installed base of C and it isn’t going anywhere this decade.
References and further reading#
- The C Programming Language (Kernighan and Ritchie, 2nd ed. 1988) is still the canonical C book. Short, dense, written by the people who designed the language. Reads like a reference but is meant as a tutorial; everything more recent assumes you’ve absorbed it.
- Hacking: The Art of Exploitation (Jon Erickson, 2nd ed. 2008) is the operator-side introduction. C, assembly, shellcode, and exploitation all in one volume. Older than most current exploit techniques but the foundation it teaches is the foundation.
- The Shellcoder’s Handbook: Discovering and Exploiting Security Holes (Anley, Heasman, Lindner, Richarte, 2nd ed. 2007) is the deeper reference. Covers Windows, Linux, and various Unix variants with detailed exploitation walkthroughs.
- Practical Reverse Engineering (Dang, Gazet, Bachaalany, 2014) for the Windows-side reverse engineering perspective. Pairs with the modern Ghidra / IDA / Binary Ninja tooling.
- Smashing the Stack for Fun and Profit (Aleph One, Phrack 49, 1996) is the historical paper that started the field. Pre-NX, pre-ASLR, pre-stack-canaries, but the conceptual model it lays out is still how operators think about stack-based memory corruption.
- The Linux man pages for
execve(2),mmap(2),mprotect(2), andsyscall(2)if you’re writing anything that interacts with the kernel directly.
What this comes down to#
C is the language you read other people’s code in, and the language you write code in when the bytes have to be exactly the bytes you wrote. For modern offensive work the trend is toward Rust and Go for new tooling, but the operator who can’t read C is permanently locked out of half the field: kernel reading, exploit development, reverse engineering, embedded work, anything where the underlying surface is a C-language program.
The investment is two weekends with K&R and a willingness to crash a few processes in gdb. After that, every other systems language becomes easier to learn (because they all borrowed C’s syntax) and the C-language attack surface that runs most of the world’s infrastructure becomes accessible. Most operators learn C the same way: they get tired of not understanding what they’re decompiling.