In prior levels, you knew the address of win()
because the binary was always loaded into the memory at the same place.
In this level, we'll explore challenges when the executable that you are overflowing is Position Independent!
A Position Independent Executable is loaded into a random location in memory.
Because of this, you cannot know exactly where the win()
function is located.
So how can you solve this?
On x86 (and most other modern architectures), memory is mapped into a process' memory space page by page.
A memory page is a contiguous block of 0x1000 (4096) bytes starting at a page address aligned to 0x1000 for performance and memory management reasons (more on this much later in the pwn.college curriculum!).
For example, the following are all examples of potential page addresses:
0x5f7be1ec2000
0x7ee1382c9000
0x6513a3b67000
Do you see how the last three digits (e.g., 12 bits, or 1.5 bytes, or affectionately known as 3 "nibbles") are all 0
?
We can use this to partially predict addresses in the binary.
For an example, let's assume that our win()
function is located 0x1337
bytes past the start of the binary (so, if the binary were not position independent, it would likely be located at 0x401337
).
This means that, for example, if our PIE binary were loaded at page address 0x6513a3b67000
, it would have its win
function at 0x6513a3b68337
.
If it were loaded at 0x5f7be1ec2000
, its win
function would be at 0x5f7be1ec3337
, and so on.
So, realistically, know the last three nibbles of any address in the binary as these nibbles never change due to the page-alignment (to 0x1000
bytes).
This gives us a workaround: we can overwrite the least significant byte of the saved return address, which we can know from debugging the binary, to retarget the return to main to any instruction that shares the other 7 bytes.
Since that last byte will be constant between executions (due to page alignment), this will always work.
If the address we want to redirect execution to is a bit farther away from the saved return address, and we need to write two bytes, then one of those nibbles (the fourth least-significant one) will be a guess, and it will be incorrect 15 of 16 times.
This is okay: we can just run our exploit a few times until it works (statistically, ~50% chance after 11 times and ~90% chance after 36 times).