NivaarExam PrepOfficial exam papers ↗

19-Soft-B3 Security · December 2018

Question 6 of 7: Stack Buffer Overflow in a C Program

Nivaar worked solution (AI-drafted; not reviewed by a licensed engineer)

Notes on this paper

National Exams, December 2018 — 04-Soft-B3, Security/Safety (closed book, 3 hours, no calculator). FIVE of the seven questions constitute a complete paper (the first five as answered in the answer book are marked, each of equal value); this solution answers all seven as a full study resource. Most questions call for essay-format answers; clarity and organisation of the answer are important. Question 6 asks for a security analysis of a short C program.

Reference texts. Stallings & Brown, Computer Security: Principles and Practice, 4th ed., Ch. 2–3 (Cryptographic Tools, One-Time Pad, Stream/Block Ciphers, Modes of Operation), Ch. 21 (Public-Key Infrastructure, Certificate Authorities), Ch. 10 (Key Management, Diffie–Hellman, RSA), Ch. 3 (Hash Functions, MAC), Ch. 23 (Digital Signatures), Ch. 3 & 24 (User Authentication, Two-Factor, SSO, Password Storage), Ch. 9 (Firewalls, DMZ), Ch. 8 (Intrusion Detection, Honeypots), Ch. 10 (Buffer Overflow), Ch. 1 (Security Concepts — CIA Triad); Anderson, Security Engineering, 3rd ed., Ch. 4 (Access Control), Ch. 1 (Security Concepts).

Question 6: Stack Buffer Overflow in a C Program (20 marks)

Question text not reproduced: the examination questions are © Engineers and Geoscientists BC. Open the official past paper (linked at the top of this page) to read the question, then follow the worked solution below.

Given. The listed C program, with the line numbering from the exam:

 1: int foo(char *arg)
 2: {
 3:   int i, len;
 4:   char buf[24];
 5:
 6:   len = strlen(arg);
 7:
 8:   if (len-1 > 24)
 9:     len = 24;
10:   for (i = 0; i < len; i++)
11:     buf[i] = arg[i];
12:   return 0;
13: }
14:
15: int main(int argc, char *argv[])
16: {
17:   char string[12];
18:
19:   strncpy(string, argv[1], 12);
20:   foo(argv[2]);
21:   return 0;
22: }

buf is a fixed 24-byte stack array (valid indices 0–23); the length clamp on line 8–9 compares len−1 (not len) against 24; the copy loop on line 10–11 runs while i < len (a correctly strict bound, unlike some variants of this question); argv[2], an attacker-controlled command-line argument, is passed into foo with no length check of its own.

Find. (a) Whether the program is vulnerable, and if so the worst-case number of bytes an attacker can write past the end of buf, and at exactly what input length that worst case occurs; (b) a corrected version that removes the vulnerability.

Approach. Trace the clamp condition and the loop's iteration count as an explicit function of strlen(arg), evaluate it at and around the clamp's own boundary (not just "very long input"), and compare against buf's true capacity, sizeof(buf) = 24.

  1. Model the write count as a function of len. Let $L = \text{strlen(arg)}$. Line 8–9 clamps: $L_{\text{eff}} = 24$ if $L - 1 > 24$ (i.e. $L > 25$), else $L_{\text{eff}} = L$. The loop for (i = 0; i < len_eff; i++) writes indices $0 \ldots L_{\text{eff}}-1$, i.e. exactly $$\text{bytes\_written}(L) = L_{\text{eff}}$$ — the loop bound itself is correctly strict here, so the only defect is in the clamp comparison.
  2. Check the boundary the clamp is supposed to guard. buf holds 24 bytes, indices 0–23, so writing 24 bytes (indices 0–23) exactly fills it and writing 25 or more overflows it. For $L \le 24$: $L - 1 \le 23$, not $>24$, so $L_{\text{eff}} = L \le 24$ — bytes_written $\le 24$, no overflow (at $L=24$ the buffer is exactly filled, zero slack, but not exceeded).
  3. Find the clamp's own off-by-one. The clamp test is len−1 > 24, i.e. it only clamps once $L > 25$ — NOT once $L \ge 24$, which is what a correct guard against a 24-byte buffer requires. At $L = 25$: $L - 1 = 24$, and $24 > 24$ is false, so len is not clamped at all; $L_{\text{eff}} = 25$. $$\text{overflow}(25) = 25 - 24 = \boxed{1 \text{ byte}}$$ A 25-character argv[2] slips past the clamp's own off-by-one and writes one byte (index 24) beyond the end of buf.
  4. Confirm this is the unique worst case. At $L = 26$: $L-1 = 25 > 24$ is true, so the clamp finally engages, $L_{\text{eff}} = 24$; bytes_written $= 24$, overflow $= 0$ — and this holds for every $L \ge 26$, since the clamp always forces $L_{\text{eff}} = 24$ once it triggers. A sweep of every input length from 0 to 2000 confirms the maximum overflow is exactly 1 byte, occurring only at $L = 25$; both shorter and longer inputs than 25 produce zero overflow.
  5. Conclude on the vulnerability (part a). Yes — this is a stack buffer overflow (CWE-121/CWE-193) caused by an off-by-one in the clamp's comparison (len−1 > 24 instead of len > 24, or correctly, len > sizeof(buf)-1). It is a narrow, one-byte overflow reachable only by a single exact input length, but it is still attacker-controlled memory corruption of whatever stack data sits immediately after buf — commonly a saved register, another local variable, or (depending on compiler layout and stack protections) the frame's canary/return address.
  6. Fix it (part b). Clamp against the buffer's own declared size, correctly (no stray −1, and using sizeof rather than the unrelated literal 24), and copy strictly fewer than that many bytes, reserving one slot for a NUL terminator: if (len > sizeof(buf) - 1) len = sizeof(buf) - 1; then for (i = 0; i < len; i++) buf[i] = arg[i]; buf[len] = '\0';. With $\text{sizeof(buf)} = 24$, this gives $L_{max} = 23$, and $$\text{bytes\_written}_{fixed} = L_{max} + 1 = 23 + 1 = 24 = \text{sizeof(buf)}$$ exactly filling buf (data plus terminator) and never exceeding it, for zero bytes of overflow regardless of how long arg is.
QuantityVulnerable versionCorrected version
buf capacity24 bytes24 bytes
Clamp testlen-1 > 24 (wrong, triggers only at len>25)len > sizeof(buf)-1 = 23
Loop boundi < len (correctly strict)i < len, plus explicit buf[len]='\0'
Worst-case bytes written25 (at len = 25)24
Maximum overflow1 byte, exactly at len = 250 bytes, any len

Two secondary observations, not the primary vulnerability: strncpy(string, argv[1], 12) in main never writes past string's 12 bytes, but if argv[1] is 12 characters or longer, strncpy does not NUL-terminate it, leaving string non-terminated for any later use; and neither argv[1] nor argv[2] is checked against argc before use, so too few command-line arguments causes main to dereference a NULL/out-of-bounds pointer.