Native RCE — from source review
TL;DR: Auditing C/C++ for memory-safety bugs (the path to native RCE) means reading for: (1) buffer-length math errors, (2) signed/unsigned confusion, (3) integer overflow in size calculations, (4) double-free / use-after-free in object lifetimes, (5) format-string usage, (6) printf-family with untrusted format args, (7) tainted indices into arrays. The classes are old; the bugs ship because the code is still big and the compiler can’t prove safety. Companion to decompiler-driven-source-review and stack-buffer-overflow.
What you grep first
1
2
3
4
5
6
7
8
9
10
11
12
13
# Bounded/unbounded copies
grep -rn 'strcpy\|strcat\|sprintf\|vsprintf\|gets\b' .
grep -rn 'memcpy\|memmove\|memset\|memcmp\|bcopy' .
# Length calc patterns
grep -rn 'malloc\|calloc\|realloc\|alloca' .
grep -rn 'sizeof' .
# printf family
grep -rn 'printf\|fprintf\|sprintf\|snprintf\|syslog' .
# Indexing
grep -rn '\[.*\]' . # too broad; use a smarter walker
For real audits, semgrep / CodeQL rules are better than grep. But knowing the patterns by eye is what makes you fast.
Bug class 1 — bounded copy with wrong bound
1
2
char dst[64];
memcpy(dst, src, src_len); // src_len attacker-controlled
or
1
strncpy(dst, src, sizeof(src)); // sizeof(src) is pointer size, not buffer size
Patterns to flag:
sizeof(ptr)whereptris a pointer (not an array) — yields 8 on 64-bit, not the buffer size.len = strlen(src) + 1; dst = malloc(len); strcpy(dst, src);— fine untilsrcwas tampered to remove the null; then it walks the heap.
Bug class 2 — integer overflow in size calc
1
2
void *buf = malloc(count * sizeof(struct item)); // count * sizeof can overflow
for (i = 0; i < count; ++i) buf[i] = ...;
If count is attacker-controlled and count * sizeof(struct item) wraps to a small value, you malloc tiny and write large → heap overflow. The classic Stagefright bug shape.
Mitigations: calloc(count, sizeof(...)) which checks the multiplication, or explicit overflow check.
Variants:
1
2
if (len1 + len2 > MAX) error(); // len1 + len2 can wrap and pass
memcpy(dst, src, len1 + len2);
Bug class 3 — signed/unsigned confusion
1
2
3
int len;
if (len > buf_size) error(); // len is signed; negative len passes
memcpy(buf, src, len); // memcpy takes size_t; negative becomes huge
Greps:
1
grep -rnE 'int\s+(size|len|count|n)|\bint\s+\w+\s*=\s*read\(' .
Any int holding a size that goes into size_t-typed APIs is a candidate. Should be size_t or carefully bounded.
Bug class 4 — off-by-one
1
2
char buf[64];
for (i = 0; i <= 64; ++i) buf[i] = 0; // writes one past end
Or:
1
2
3
char dst[64];
strncpy(dst, src, 64);
dst[64] = '\0'; // BOF by one byte
These look benign but a one-byte heap overflow into the next chunk’s metadata is a known exploitation primitive.
Bug class 5 — format-string injection
1
2
3
printf(user_input); // BAD
fprintf(stderr, msg); // msg attacker-controlled — same bug
syslog(LOG_INFO, user_input); // same
%n writes to the stack; %s reads from the stack; arbitrary read/write primitive in the right shape. See format-string-bugs.
Modern compilers warn (-Wformat-security) but the warning is often disabled in releases of legacy code.
Bug class 6 — double-free / use-after-free
1
2
3
free(obj->buf);
... // code path that calls into something that re-enters
free(obj->buf); // double free
Or:
1
2
3
if (cond) free(ptr);
... // ptr still used downstream
*ptr = x; // UAF
Audit object lifecycles: every malloc/new paired with exactly one free/delete. Functions that might free their argument should be annotated and consistently used.
Bug class 7 — tainted index
1
table[user_idx] = value; // user_idx unchecked
If user_idx is signed or unbounded, attacker writes outside the array. Equally bad with negative indices.
Bug class 8 — TOCTOU on filesystem
1
2
3
if (access("file", R_OK) == 0) {
fd = open("file", O_RDONLY); // file may have changed
}
Classic Time-Of-Check / Time-Of-Use. Fix: open once, fstat/fcntl on the descriptor.
Bug class 9 — uninitialised reads
1
2
3
struct ctx c;
strcpy(c.name, input);
send_struct(&c); // other fields are uninitialised stack bytes → info leak
Use memset or calloc to zero before populate.
Bug class 10 — out-of-tree dependencies
A bug in an embedded library (libpng, libjpeg, libxml2, openssl, zlib) lives in the binary unless the build is updated. Audit:
- Vendored copies of upstream libs in the source tree.
- Versions older than current security release.
- Pinned-but-old in build manifests.
Tools that find these for you
- Compiler warnings:
-Wall -Wextra -Wformat-security -Wconversion -Wpointer-arith. - clang-tidy rules:
clang-analyzer-core.*,bugprone-*. - CodeQL: built-in C/C++ queries for many of these classes.
- Coverity / Klocwork for commercial.
- AFL / libFuzzer / Honggfuzz for finding bugs you missed.
But the reviewer’s eye finds bugs static analysis misses — primarily because the logic of the bug isn’t expressible as a pattern. See whitebox-to-exploit-methodology.
From bug to RCE
A memory-safety bug is a primitive, not a shell. To turn it into RCE you need:
- A way to control the value written/read.
- A way to control where the write/read lands (heap shaping, vtable placement).
- Bypasses for active mitigations (ASLR → leak; DEP → ROP; canary → leak; CFI → corruption that doesn’t violate CFI).
See exploit-primitives-for-mitigated-targets and the windows/linux exploit-dev notes for the next layer.
Source-audit checklist
- Every length arithmetic uses checked addition / multiplication.
- Every size variable is
size_tor bounded. - Loops use
<with the right bound (no<= sizeof(arr)). - No
printf(varname)with untrusted format. - Every alloc has exactly one free, and freed pointers are nulled.
- Indexing operations are bounds-checked.
- Open file descriptors after permission check, not re-open.
- Structs sent over the wire are zeroed before populate.
- Bundled libs match current security releases.