Native RCE — from source review

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) where ptr is 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 until src was 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_t or 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.

References