SECURE CODING IN C AND C++
Chapter 2: Strings — Exam-Ready Notes
HIGHEST EXAM WEIGHT CHAPTER | Buffer Overflows + Stack Attacks + Mitigations
SECTION 1 — C STRING MODEL (Foundation)
Every string vulnerability in this chapter traces back to one root: C has no built-in string type. Strings are raw
arrays with a convention.
1.1 The Null-Terminated Byte String (NTBS)
• A C string = contiguous sequence of chars terminated by '\0' (null character)
• A pointer to a string points to its FIRST character — not the whole string
• String LENGTH = number of bytes BEFORE the null terminator (null not counted)
• String SIZE = length + 1 (must include null terminator in all allocations)
strlen("Hello") = 5. malloc for a copy needs strlen+1 = 6 bytes. This +1 is an off-
📌 NOTE
by-one exam trap.
1.2 Wide Strings vs Narrow Strings
Property Narrow String (char) Wide String (wchar_t)
Type char — 1 byte wchar_t — 2 bytes (Windows) or 4
bytes (Linux)
Literal "Hello" L"Hello"
Length fn strlen() — returns bytes wcslen() — returns CHARACTER
count, NOT bytes
Alloc rule malloc(strlen(s)+1) malloc((wcslen(s)+1) * sizeof(wchar_t))
Copy fn strcpy / strncpy wcscpy / wcsncpy
Null term '\0' (1 byte) L'\0' (2 or 4 bytes)
Using strlen() on a wchar_t string is catastrophic. On Linux each wchar_t = 4
⚠ TRAP bytes, so L'0' = 0x30 0x00 0x00 0x00. strlen() hits the second byte (0x00) and
returns 1 instead of 10. You allocate a 2-byte buffer for a 40-byte string.
1.3 String Literals — Storage Rules
• String literals are stored in READ-ONLY memory (text/rodata segment)
• char *s = "Hello"; — s points to read-only memory. Modifying s[0] = 'h' → UNDEFINED BEHAVIOR
(segfault)
• char s[] = "Hello"; — compiler copies literal into a writable stack array. Modification is SAFE
• In C++, string literals are const char[] — modification is a compile-time ERROR
CERT STR30-C: Do not attempt to modify string literals. Always declare as const
✅ RULE
char * or copy into char[].
1.4 Character Types — The int Rule
Functions fgetc(), getc(), getchar() return int — NOT char. This is not an accident.
Why int? Explanation
char range issue signed char: -128 to 127. unsigned char: 0-255. Neither can represent both all
characters AND EOF(-1) unambiguously.
EOF = -1 If stored in signed char, EOF(-1) = valid byte value 0xFF = char 255.
Ambiguous.
int solves it int holds all unsigned char values (0-255) AND EOF(-1) with no overlap.
Classification fns isalpha(), isdigit() etc. accept int for same reason — must handle EOF safely.
char c = getchar(); — WRONG. If char is signed, EOF may equal a valid
⚠ TRAP
character. Loop may never terminate. Always: int c = getchar();
SECTION 2 — THE 6 DANGEROUS FUNCTIONS
This table is the single most important reference in Chapter 2 for code analysis questions.
Function Why Dangerous Safe Replacement Key Rule
gets(buf) No size parameter at all. fgets(buf, sizeof(buf), stdin) Never use gets(). Ever.
Impossible to use safely.
REMOVED from C11.
strcpy(dst,src No destination size check. strncpy(dst,src,sizeof(dst)-1) + Always bound to
) Copies until src null manual null sizeof(dst)-1
terminator. dst overflows if
src is longer.
strcat(dst,src) No remaining-space check. strncat(dst,src,sizeof(dst)- Calculate remaining
Appends without verifying dst strlen(dst)-1) space explicitly
has room.
sprintf(buf,fm No output size limit. Also snprintf(buf,sizeof(buf),"%s",v Format arg must
t,...) vulnerable to format string ar) ALWAYS be a string
attack if fmt is user input. literal
scanf("%s",b Reads until whitespace. No scanf("%127s",buf) with field Field width = sizeof(buf)-
uf) bounds check on destination. width 1
printf(var) If var contains %x %n %p, printf("%s",var) Format arg must
printf() interprets them as ALWAYS be a string
instructions not data. literal
printf(user_data) is a FORMAT STRING VULNERABILITY — not a buffer
⚠ TRAP overflow. The fix is NOT bounds checking. The fix is printf("%s", user_data).
These are two completely different vulnerability classes.
SECTION 3 — FOUR COMMON STRING MANIPULATION ERRORS
3.1 Improperly Bounded String Copies
Occurs when data is copied from a source of unknown length into a fixed-length buffer.
char response[8];
gets(response); // attacker enters 100 chars → overflow
• gets() implementation keeps writing *p++ = c until \n or EOF — no stop condition on destination
• Input > 7 chars overwrites: local variables → saved frame pointer → return address
• Attacker controls return address → redirects execution to shellcode
Replace gets() with fgets(). Replace strcpy() with strncpy() + explicit null
✅ RULE
termination.
3.2 Off-by-One Errors
Off-by-one = writing or reading exactly one position past the valid array boundary.
Off-by-One Pattern Code Example Problem
Missing +1 for null malloc(strlen(s)) Allocates N bytes for N+1 needed. Null
terminator terminator overwrites adjacent memory.
Loop uses <= instead of for(i=1; i<=11; i++) Iterates one past end. Accesses index 11
< on a 10-element array.
Loop starts at 1 not 0 for(i=1; ...) Skips index 0. Leaves first element
uninitialized.
Wrong size in strcpy_s strcpy_s(s1, sizeof(s2), s2) Passes size of SOURCE not
DESTINATION. s1 overflows if s2 > s1.
Null write out of bounds dest[strlen+1] = '\0' Writes null one position past valid
allocation.
Off-by-one errors are subtle. Always ask: does my allocation include +1 for the
📌 NOTE
null terminator? Does my loop use < (strict) not <=?
3.3 Null-Termination Errors
A string without a null terminator causes string functions to read past the intended end, consuming garbage
memory until they find an accidental 0x00.
strncpy(a, "0123456789abcdef", sizeof(a)); // sizeof(a)=16, string=16 chars
// strncpy fills all 16 bytes but adds NO null terminator if src >= dest size
strcpy(c, a); // reads past a[] — undefined behavior
• strncpy() does NOT guarantee null termination if source length >= dest size
• Fix: always manually null-terminate after strncpy:
strncpy(ntbs, source, sizeof(ntbs)-1);
ntbs[sizeof(ntbs)-1] = '\0'; // explicit null termination — always required
strncpy() is NOT a safe drop-in for strcpy(). It doesn't null-terminate when src
⚠ TRAP
length >= dest size. You must add the null terminator yourself.
3.4 String Truncation
When a destination buffer is too small, the string is silently cut off. The program continues with incomplete
data — logic errors, authentication bypasses, data corruption.
• Less immediately dangerous than overflow, but can cause security logic failures
• Example: username truncated to 8 chars may match a different user's record in a database
• snprintf() truncates silently — always check its return value
snprintf() return value = number of chars that WOULD have been written. If return
✅ RULE
>= sizeof(buf), output was truncated. Always check this.
SECTION 4 — BUFFER OVERFLOWS (Core Attack Mechanism)
This is the mechanism behind most Chapter 2 vulnerabilities. You must be able to explain it step by step.
4.1 Process Memory Organization
Segment Contains Key Properties
Text / Code Machine instructions, string literals Read-only. Attempting to write causes
(read-only data) segfault.
Data Initialized global & static variables Read-write. Persists entire program lifetime.
BSS Uninitialized global & static variables Zero-initialized by OS loader. Same lifetime as
Data.
Heap Dynamically allocated memory Grows upward. Managed by allocator. Manual
(malloc/new) lifetime.
Stack Local variables, function args, return Grows downward. Automatic lifetime. Target of
addresses, frame pointers stack smashing.
Variable placement rule: local variable → stack. static/global initialized → data.
📌 NOTE static/global uninitialized → BSS. malloc result → heap. Stack grows toward
lower addresses; heap grows toward higher addresses.
4.2 Stack Frame Structure
When a function is called, a FRAME is pushed onto the stack containing (from high to low address):
Stack Position Contents Why It Matters for Attacks
Higher addresses Function arguments pushed by Can be read/corrupted by large overflow
caller
↓ Return address (saved EIP/RIP) THE primary target — controls where
execution goes after return
↓ Saved frame pointer (EBP/RBP) Controls stack frame of calling function
↓ Local variables including buffers Overflow starts here and grows UPWARD
toward return address
Lower addresses (top of stack) Buffer declared here — overflow writes toward
higher addresses
Stack grows downward (toward lower addresses). But buffer writes grow upward
📌 NOTE (toward higher addresses). This collision is what makes stack buffer overflows
dangerous — a buffer write naturally heads toward the return address.
4.3 Stack Smashing — Step by Step
Stack smashing = overwriting data in the stack segment beyond a buffer's boundary.
Step What Happens Code Example
1. Declare Fixed-size local buffer allocated on char response[8]; — 8 bytes reserved
buffer stack
2. Input written with no size check gets(response); — reads unlimited input
Unbounded
write
3. Overflow Input > 7 chars writes past buffer end AAAAAAAAAAAAAAAAA — 17 A's
4. Adjacent stack data overwritten Saved EBP overwritten with 'AAAA'
Corruption
5. Return Return address replaced with attacker EIP now points to attacker-chosen address
addr value
overwrite
6. Control Function returns — CPU jumps to Execution redirected to shellcode or libc
hijack attacker address
The program CRASHES if the overwritten return address is invalid or points to
⚠ TRAP non-executable memory. The program is HIJACKED if it points to valid shellcode
or a known library function. Both are failures — one is noisier.
4.4 Code Injection vs Arc Injection
Attack Type Mechanism Attacker Provides Detectability
Code Injection Return address → shellcode The malicious code AND Blocked by
in buffer/heap its address NX/DEP/W^X (non-
executable stack)
Arc Injection Return address → existing Only the address of Harder to block —
(ret2libc) library function (e.g. system()) existing code uses legitimate code
Return-Oriented Chain of ret-ending gadgets Addresses of gadgets + Bypasses NX — uses
Programming (ROP) their arguments existing code
fragments
Arc injection is preferred by attackers when NX/DEP is active because it reuses
📌 NOTE existing executable code. The attacker just needs to know where functions like
system('/bin/sh') live in memory — which ASLR tries to prevent.
SECTION 5 — FORMAT STRING VULNERABILITIES
Distinct from buffer overflows. Root cause: user input treated as format instructions, not data.
5.1 How Format String Attacks Work
• printf() reads the format string as a mini-program: %d = read int, %s = read string, %n = write count
• If user controls the format string, they insert their own instructions
• printf() trusts the format string completely — it reads arguments from the stack regardless of whether
they were actually pushed
Specifier Normal Use Attack Use Impact
%d / %x Print int / hex Stack read Leak stack values — info disclosure
%p Print pointer address Stack read Leak memory addresses — breaks
ASLR
%s Print string Read arbitrary address as Crash or leak memory contents
string
%n Write char count to Write to arbitrary address ARBITRARY MEMORY WRITE →
pointer code execution
'%n' turns printf() into a WRITE primitive. The attacker can write any value to any
🔴 DANGER memory address — overwriting return addresses, GOT entries, or function
pointers. This is why user input must NEVER be the format argument.
5.2 Vulnerable vs Secure Pattern
// VULNERABLE — user_input IS the format string
printf(user_input);
sprintf(buf, user_input);
snprintf(buf, size, user_input);
// SECURE — user_input is DATA, literal string is FORMAT
printf("%s", user_input);
snprintf(buf, sizeof(buf), "%s", user_input);
The format argument must ALWAYS be a string literal you wrote, never a
✅ RULE
variable. This applies to printf, fprintf, sprintf, snprintf, and all variants.
5.3 snprintf() Is Not Fully Safe
• snprintf() prevents buffer overflow via the size parameter — correct
• BUT snprintf() is still vulnerable to format string attack if format arg is user input
• snprintf() return value must be checked — negative = error, >= size = truncation occurred
• Failure causes include: insufficient buffer AND encoding errors AND implementation errors
⚠ TRAP Students write 'snprintf() is safe'. It is safer — not safe. It still requires a literal
format string and return value checking.
SECTION 6 — MITIGATION STRATEGIES
Every mitigation has a specific mechanism and specific limitation. Know both for full marks.
6.1 Runtime Mitigations — Compiler and OS Level
Mitigation Mechanism Limitation / Bypass
Stack Canaries (- Compiler places random value (canary) Only detects sequential overflows that
fstack-protector) between local vars and return address. overwrite canary. Cannot stop
Checks it before return. Mismatch = targeted overwrites (e.g. overwriting a
abort. local function pointer without touching
canary).
ASLR (Address OS randomizes base addresses of stack, Entropy is limited. Brute-forceable on
Space Layout heap, libraries at each execution. 32-bit. Info leaks (format string %p)
Randomization) Shellcode/ret2libc addresses change defeat ASLR by revealing actual
every run. addresses.
NX / DEP / W^X Memory pages are either Writable OR Does not stop arc injection or ROP —
eXecutable — never both. Stack/heap these use existing executable code.
marked non-executable. Code injection
fails.
Stack-Smashing GCC implementation of stack canaries. Same limitations as canaries.
Protector (ProPolice) Reorders local variables to put buffers Reordering helps but doesn't eliminate
below other variables, reducing all overflow paths.
corruption impact.
PaX Linux kernel patch enforcing W^X strictly Requires kernel patch. Compatibility
plus ASLR with higher entropy. issues with some applications.
EXAM RULE: Mitigations REDUCE risk — they do not ELIMINATE vulnerabilities.
📌 NOTE Stack canaries detect stack smashing but don't fix the underlying unbounded
copy. The root cause fix is always: bounded copy + input validation.
6.2 Code-Level Mitigations
Mitigation How to Apply Example
Input Validation Reject or sanitize input before if (strlen(input) >= sizeof(buf)) reject;
processing. Check length, character set,
format.
Object Size Checking Use __builtin_object_size() or similar to GCC provides
verify destination at runtime. __builtin_object_size(buf,0) for size
hints
C11 Annex K Use _s variants: strcpy_s, strncpy_s, strcpy_s(dst, sizeof(dst), src);
Bounds-Checking gets_s — take explicit destination size.
Dynamic Allocation Allocate buffers sized to actual input at char *p = malloc(strlen(input)+1);
runtime rather than using fixed arrays. strcpy(p,input);
C++ std::string Use std::string instead of char arrays. std::string s = argv[0]; — no overflow
Manages memory automatically, no possible
manual sizing.
6.3 Notable Real Vulnerabilities from Chapter 2
Vulnerability Root Cause System Affected
Blaster/RPC DCOM Unbounded while loop — no null check or Windows XP/2000 —
(2003) destination bounds in hostname extraction 8M+ machines
Kerberos (MIT) Buffer overflow in string handling in the KDC (Key Authentication
Distribution Center) infrastructure worldwide
Remote Login (rlogin) Improperly bounded string copy in login handling UNIX remote access
SECTION 7 — CERT C SECURE CODING RULES FOR STRINGS
These rules are directly cited in your PPT. Know the rule ID, what it says, and what code violates it.
Rule ID Rule Name Violation Example Why Dangerous
STR30-C Do not modify string literals char *s="Hi"; s[0]='h'; Undefined behavior —
read-only memory write
→ segfault
STR31-C Guarantee storage has malloc(strlen(s)) — missing Off-by-one → null
sufficient space for char data +1 terminator overwrites
+ null terminator adjacent memory
STR32-C Do not pass non-null- strncpy fills buffer fully with strlen reads past buffer
terminated strings to string no \0; then passes to strlen end → undefined
functions behavior
STR04-C Use plain char for characters Using signed char[] where Implicit conversion
in the basic character set const char* expected warnings; potential sign
extension bugs
EXP33-C Do not read uninitialized or Loop with no null check on Reads past string end →
out-of-bounds memory source string undefined behavior, info
leak
ARR30-C Do not form out-of-bounds Pointer arithmetic past array Out-of-bounds access →
pointers or array subscripts end corruption or crash
MSC00-C Compile cleanly at high Implicit conversions between Warnings often indicate
warning levels char types real bugs — -Wall -
Wextra required
SECTION 8 — CODE ANALYSIS PATTERNS (Exam Drills)
These are the exact patterns your examiner will test. Recognize them instantly.
Pattern 1 — gets() → Stack Buffer Overflow
void get_y_or_n(void) {
char response[8];
gets(response); // VULNERABLE: no bounds check
}
• Vulnerability: gets() writes unlimited input into 8-byte buffer
• Impact: Return address overwrite → code injection or arc injection
• Fix: fgets(response, sizeof(response), stdin)
Pattern 2 — strcpy() with user input → Overflow
char prog_name[128];
strcpy(prog_name, argv[0]); // VULNERABLE if argv[0] > 127 chars
printf(prog_name); // ALSO VULNERABLE: format string attack
• Vulnerability 1: strcpy no destination bounds → stack overflow
• Vulnerability 2: printf(var) → format string — %n gives arbitrary write
• Vulnerability 3: argv[0] may be NULL → null pointer dereference
• Fix: null check argv[0] + strncpy with sizeof-1 + null terminate + printf("%s",var)
Pattern 3 — strncpy() false safety → Missing null terminator
char a[16];
strncpy(a, "0123456789abcdef", sizeof(a)); // source = 16 chars = sizeof(a)
strcpy(c, a); // VULNERABLE: a is NOT null terminated
• strncpy fills all 16 bytes but adds NO null terminator when src length >= dest size
• strcpy(c,a) reads past a[] until it finds an accidental 0x00 — undefined behavior
• Fix: strncpy(a, src, sizeof(a)-1); a[sizeof(a)-1] = '\0';
Pattern 4 — Wide string malloc mistake
wchar_t src[] = L"Hello";
wchar_t *dst = malloc(wcslen(src) + 1); // VULNERABLE: missing sizeof(wchar_t)
wcscpy(dst, src);
• wcslen() returns CHARACTERS. malloc() needs BYTES. On Linux: 5+1=6 bytes allocated, 24 bytes
needed
• wcscpy writes 24 bytes into 6-byte buffer → heap overflow
• Fix: malloc((wcslen(src) + 1) * sizeof(wchar_t))
Pattern 5 — Off-by-one in loop
char *dest = malloc(strlen(s1)); // MISSING +1 for null terminator
for (i=1; i<=11; i++) { // WRONG: starts at 1, goes to 11
dest[i] = s1[i]; // skips index 0, writes to index 11
}
• Error 1: malloc(strlen) not malloc(strlen+1) — null terminator overwrites adjacent heap
• Error 2: loop starts at i=1, skips dest[0] — uninitialized first byte
• Error 3: i<=11 should be i<strlen(s1) — writes one past end
• Fix: malloc(strlen+1), loop from i=0, condition i<strlen(s1), dest[strlen(s1)]='\0'
SECTION 9 — RAPID REVISION CHEAT SHEET
Read this the night before. 2-minute recall of the entire chapter.
Topic Key Facts
C string model Null-terminated byte array. No bounds checking. Programmer responsible for
all sizing.
strlen vs size strlen = chars before \0. Size = strlen+1. ALWAYS +1 when allocating a string
copy.
wchar_t allocation malloc((wcslen(s)+1) * sizeof(wchar_t)). Never wcslen+1 alone.
Sizeof(wchar_t)=4 on Linux.
gets() REMOVED from C11. No bounds check. Replace with fgets(buf, sizeof(buf),
stdin).
strcpy() No dest size check. Fix: strncpy(dst,src,sizeof(dst)-1) + dst[sizeof(dst)-1]='\0'
strncpy() trap Does NOT null-terminate if src >= dest size. Always add manual null
termination.
sprintf() trap No size limit AND format string vulnerable. Fix:
snprintf(buf,sizeof(buf),"%s",var)
printf(var) trap Format string vulnerability — NOT buffer overflow. Fix: printf("%s",var). Format
arg = literal.
%n specifier Writes to memory address. Turns printf into arbitrary write primitive. Most
dangerous specifier.
Stack layout Low addr: buffer → local vars → saved EBP → return addr → args → High
addr
Stack smashing Overflow buffer → overwrite return addr → control hijack → code/arc injection
Code injection Attacker provides shellcode. Blocked by NX/DEP/W^X.
Arc injection Return-to-libc. Uses existing code. Bypasses NX. Needs system() address.
ROP Chains ret-ending gadgets. Bypasses NX + canaries. Needs gadget
addresses.
Stack canary Random value before return addr. Detects sequential overflow. Bypass: info
leak or targeted write.
ASLR Randomizes addresses. Bypass: format string %p leak or brute force (32-bit
only).
NX/W^X/DEP Stack/heap non-executable. Stops code injection. Does NOT stop arc injection
or ROP.
CERT STR31-C Guarantee storage has sufficient space including null terminator.
CERT STR32-C Do not pass non-null-terminated strings to string functions.
off-by-one rule < not <=. strlen+1 not strlen. sizeof-1 for strncpy bound.
Chapter 2 Complete — Next: Chapter 3 Pointer Subterfuge