Step E.4: Exercise 3
03_chirp [15 pts]
Resources
Challenge Instructions
There have been a number of defensive upgrades made to binary security over the years designed to protect against memory corruption vulnerabilities like the stack-based buffer overflow.
In this challenge, we’ll cross-examine a few in particular: stack canaries and PIE.
What are stack canaries?
When you’ve been performing basic buffer overflow exploits to hijack the control flow of a binary, this has typically been done by simply overwriting the ret address stored in the stack that the function’s instructor pointer would reference.
Stack canaries frustrate this process by doing a couple things:
- At compile time, the source code is slightly modified around these ret operations; the compilers inject some additional operations in order to protect/preserve the integrity of the address referenced by the
retoperation. - The way that the address is protected is that a runtime-determined random value (aka the “canary”) is inserted in-between that address and the rest of the calling function’s frame. When the function terminates, that random value is checked by the binary to determine if it’s changed; if it has, then the binary presumes that an overflow has taken place and terminates the whole process.
There are a few rules/behaviors that stack canaries follow that are worth being mindful of.
- In our 64-bit system, stack canaries are always sized 8 bytes.
- Stack canaries are set when the program starts up and do not change after. The same stack canary is even passed along to any child processes it spawns (i.e. they do not create their own stack canaries). Stack canaries are only reset when the process restarts.
- The relative position of stack canaries within the stack do not change run-over-run (though the absolute address may change due to things like ASLR); stack canaries will always be positioned between all other variables within a function and the return address.
What is PIE?
Position Independent Executables (PIE) is a hardening measure that binaries can likewise embrace at compile-time. In brief, PIE randomizes the address space of the binary’s assembly at runtime making it practically impossible to statically reference particular instructions in our exploits.
Below is a screenshot of the same binary objdump compiled with and without PIE:

In the case of the PIE-compiled binary, instead of addresses the objdump dumps offsets.
Like stack canaries, PIE has a few rules/behaviors that are worth being mindful of:
- At runtime, a PIE base address is randomly set and all of the binary’s instructions are offset from that base address. Each individual instruction is not seeded with its own randomized value, but are instead offset from this base address.
- The PIE base address typically ends in 000 due to memory pages being the units of randomization, sized at 0x1000 bytes. For example, it might look like: 0x5b930239a000.
Memory Leaks & Format Strings
On their face, both of these protections may seem insurmountable:
- We don’t know the value of the canary at runtime, so overflowing it with anything other than itself will prevent our exploit.
- We don’t know the value of the PIE base address at runtime, so even if we were to correctly set the canary, we couldn’t reliably execute other code in the binary.
The big condition on both of the above is that we cannot know these values. If we did, then we could overcome both of these problems. This is why vulnerabilities like memory leaks are very concerning.
Memory leaks can manifest in a number of ways, but one of the most common is by way of format-string vulnerabilities. When printf() is called to print a string, it doesn’t simply print the string; it goes to an address on the stack and prints the contents that address points to. If printf() implicitly trusts user input…
| Implicit Trust | Explicitly defined |
printf(userinput) |
printf("%s", userinput) |
…then the user can forcefully decide what type of data is being rendered by printf(). As an example, this might look like…
userinput = "%x" → printf(userinput) → printf("%x")
This is a problem! By passing a format specifier (%x) as a placeholder value but not specifying what’s meant to go there (i.e. printf("%x", somevariable)) the printf() function by default reads values off of the top of the stack instead. We can leak multiple sequential values (e.g. “%x %x %x %x…”) or even particular values (i.e. the 5th value via “%4$x”).
Note: the “%x” format specifier returns hex representations of whatever’s next off the stack, but there are other format specifiers we might choose to use instead that may be more useful/insightful.
Using this mechanism, you can leak the stack memory at runtime which – as we mentioned earlier, sets us up to compromise the binary in its entirety.
Guidance
- Your goal for this challenge is to exploit a format-string vulnerability in order to leak both the stack canary AND a PIE address, then perform a buffer overflow.
- You can use GDB to identify any canaries and the PIE base address. This is useful in affirming that you’ve correctly identified/calculated them through a memory leak. The GDB commands are:
canarypiebase
- By default GDB disables ASLR, which affects PIE behavior. If you don’t change this, you might be stumped as to why the PIE base address always appears as something like 0x555555554000. If you want a more representative experience, you need to turn this feature off before running the binary in ASLR with the following GDB command:
set disable-randomization off
- Because setting breakpoints at random addresses is practically impossible, you can instead make use of setting breakpoints by offset in GDB via:
breakrva 0x<offset>- As an example, we can break on the “call” operation from the earlier screenshot with
breakrva 0x17f0.
- Addresses that start with 0x7ff generally (but not always) are associated with external library functions (i.e. libc). These are NOT useful in calculating your PIE base as they are separately randomized.



