This is a challenging rendition of the format binary where we performed an arbitrary write to change data. In this case, we will modify the return pointer to get code execution.
Static Analysis
As usual, let's check security on the binary:
$ checksec bbpwn
[*] '/home/joybuzzer/Documents/vunrotc/public/binex/03-formats/bbpwn/src/bbpwn'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
We see that there is no canary and no PIE. This means that this code is subject to buffer overflows.
We perform our routine checks in search of anything outstanding.
The original writeup for this challenges uses radare2. gdb commands were added later.
gef➤ info functions
All defined functions:
Non-debugging symbols:
...
0x0804870b flag()
0x08048724 main
...
The most important thing to notice in this disassembly is that there is a format string bug at 0x080487d7. The address we write to is directly passed as the argument to printf.
We'll then check where our input is on the stack when we run it:
$ ./bbpwn
Hello baby pwner, whats your name?
%x %x %x %x %x %x %x %x %x %x %x %x %x %x %x
Ok cool, soon we will know whether you pwned it or not. Till then Bye 8048914 ff8e99e8 f7c3a439 f7c0c2f4 f7f365fc f781ab0d ff8e9cb4 0 0 25207825 78252078 20782520 25207825 78252078 20782520
We see that we start writing at the 10th offset.
Plan of Attack
There is no canary to leak. Nothing else happens after the format string bug is triggered, meaning we need to perform some arbitrary write. Overwriting the return pointer of main() is not always a great choice because the stack frame is unpredictable.
A better solution is to overwrite the address of another function with the address of flag, our desired function. That way, when we call the function, it will call flag instead. This is what we call a GOT overwrite and will be discussed further at the end of the binary exploitation section.
The reason that the plan of attack is possible is because RELRO is not fully on. RELRO, or RElocation Linked Read-Only, is a security feature that makes the GOT read-only. This means that we cannot overwrite the GOT. However, because RELRO is only Partial, we can overwrite the GOT.
Understanding the Payload
We choose fflush as a good candidate for overwriting because it is called right after the format string bug is triggered. This means that we can overwrite the return pointer of fflush with the address of flag.
Checking the got table, we can find the address of fflush:
For the radare2 output, we can also use pdf @ sym.imp.fflush to see the address of the function. This shows us the PLT entry, which jumps us to the address in the GOT (reloc.fflush).
In the got table, fflush is at 0x0804a028. We can verify this by checking the address for an instruction:
Therefore, we need to change the value at 0x0804a028 to 0x0804870b.
Let's begin to simulate changing the value at the fflush entry in the got table. We want to overwrite the value, one byte at a time, until we get the desired value. Consider the following payload:
This means that we're going to write the number of bytes thus far to the address 0x0804a028, 0x0804a029, and 0x0804a02b. If we run gdb and stop execution right after the printf, we can see what the values are:
p =process('./bbpwn')gdb.attach(p, gdbscript='b *(main+184)')
We see that the current value is 0x52 at the lowest byte. Remember that we can only add to the value, meaning to get 0x0b at that byte, we need to reach 0x10b. This takes 0x10b-0x52=185 bytes. Therefore, we can append %185x into our payload so that many bytes are written first.
Why does this work?
Note the difference in the format specifier. We are writing %185x and not%185$x. Rather than writing the value of the 185th argument, we are writing the argument provided as a 185-byte value. As a proof-of-concept, consider the following code:
Remember that %n prints the number of bytes written thus far. If we write spaces, we add another byte to the count. In theory, we could subtract one from the hex format specifier, but this is less confusing.
The lower byte is now 0x0b as desired. Now, let's do the second and third bytes similarly. Our current bytes are 0x010b, and we need this to be 0x0487. 0x0847-0x010b=892. We can add %892x to the format string to write that many bytes. This changes the value at that address to:
Finally, to modify the fourth bit, we need to write 0x08 to the fourth byte, which requires 0x108-0x87=129 bytes to be written. This will spill over to the next DWORD, but that's okay because it doesn't prevent us from pulling off this exploit.
Putting it all Together
Putting together the payload, we have the following exploit: