Por que esse código é vulnerável a ataques de estouro de buffer?

148
int func(char* str)
{
   char buffer[100];
   unsigned short len = strlen(str);

   if(len >= 100)
   {
        return (-1);
   }

   strncpy(buffer,str,strlen(str));
   return 0;
}

Esse código é vulnerável a um ataque de estouro de buffer e estou tentando descobrir o porquê. Estou pensando que tem a ver com lenser declarado um em shortvez de um int, mas não tenho muita certeza.

Alguma ideia?

Jason
fonte
3
Existem vários problemas com este código. Lembre-se de que as seqüências C são terminadas em nulo.
Dmitri Chubarov
4
@DmitriChubarov, o cancelamento não nulo da sequência será um problema apenas se a sequência for usada após a chamada para strncpy. Nesse caso, não é.
R: Sahu
43
Os problemas nesse código fluem diretamente do fato de que strlené calculado, usado para a verificação de validade e , em seguida, é absurdamente calculado novamente - é uma falha DRY. Se o segundo strlen(str)fosse substituído por len, não haveria possibilidade de estouro de buffer, independentemente do tipo de len. As respostas não abordam esse ponto, elas apenas conseguem evitá-lo.
Jim Balter
3
@CiaPan: Se você passar uma string não terminada em nulo, strlen mostrará um comportamento indefinido.
precisa saber é o seguinte
3
@ JimBalter Nah, acho que vou deixá-los lá. Talvez alguém tenha o mesmo equívoco tolo e aprenda com ele. Sinta-se à vontade para sinalizá-los se eles o irritarem, alguém pode aparecer e excluí-los.
Asad Saeeduddin

Respostas:

192

Na maioria dos compiladores, o valor máximo de um unsigned shorté 65535.

Qualquer valor acima que é contornado, então 65536 se torna 0 e 65600 se torna 65.

Isso significa que seqüências longas do comprimento certo (por exemplo, 65600) passam na verificação e sobrecarregam o buffer.


Use size_tpara armazenar o resultado de strlen(), não unsigned shorte comparar lencom uma expressão que codifica diretamente o tamanho de buffer. Então, por exemplo:

char buffer[100];
size_t len = strlen(str);
if (len >= sizeof(buffer) / sizeof(buffer[0]))  return -1;
memcpy(buffer, str, len + 1);
orlp
fonte
2
@PatrickRoberts Teoricamente, sim. Mas você deve ter em mente que 10% do código é responsável por 90% do tempo de execução, portanto, não deixe o desempenho passar antes da segurança. E lembre-se de que, com o tempo, o código muda, o que pode subitamente significar que a verificação anterior se foi.
orlp
3
Para evitar o estouro de buffer, basta usar lencomo o terceiro argumento do strncpy. Usar strlen novamente é burro em qualquer caso.
Jim Balter
15
/ sizeof(buffer[0])- observe que sizeof(char)em C é sempre 1 (mesmo que um caractere contenha um zilhão de bits), então isso é supérfluo quando não há possibilidade de usar um tipo de dados diferente. Ainda assim ... parabéns por uma resposta completa (e obrigado por responder aos comentários).
Jim Balter
3
@ rr-: char[]e char*não são a mesma coisa. Existem muitas situações em que a char[]será implicitamente convertido em a char*. Por exemplo, char[]é exatamente o mesmo que char*quando usado como o tipo para argumentos de função. No entanto, a conversão não ocorre para sizeof().
Dietrich Epp
4
Controle @ Porque, se você alterar o tamanho de, bufferem algum momento, a expressão será atualizada automaticamente. Isso é essencial para a segurança, porque a declaração de bufferpode estar a algumas linhas da verificação do código real. Portanto, é fácil alterar o tamanho do buffer, mas esqueça de atualizar em todos os locais onde o tamanho é usado.
Orlp 29/04
28

O problema está aqui:

strncpy(buffer,str,strlen(str));
                   ^^^^^^^^^^^

Se a string for maior que o comprimento do buffer de destino, o strncpy ainda a copiará. Você está baseando o número de caracteres da sequência como o número a ser copiado em vez do tamanho do buffer. A maneira correta de fazer isso é a seguinte:

strncpy(buffer,str, sizeof(buff) - 1);
buffer[sizeof(buff) - 1] = '\0';

O que isso faz é limitar a quantidade de dados copiados para o tamanho real do buffer menos um para o caractere final nulo. Em seguida, definimos o último byte no buffer para o caractere nulo como uma proteção adicional. A razão para isso é que o strncpy copiará até n bytes, incluindo o nulo final, se strlen (str) <len - 1. Caso contrário, o nulo não será copiado e você terá um cenário de falha, porque agora seu buffer não possui terminação. corda.

Espero que isto ajude.

EDIT: Após um exame mais aprofundado e contribuições de outras pessoas, segue uma codificação possível para a função:

int func (char *str)
  {
    char buffer[100];
    unsigned short size = sizeof(buffer);
    unsigned short len = strlen(str);

    if (len > size - 1) return(-1);
    memcpy(buffer, str, len + 1);
    buffer[size - 1] = '\0';
    return(0);
  }

Como já sabemos o comprimento da string, podemos usar o memcpy para copiar a string do local referenciado por str no buffer. Observe que, na página de manual do strlen (3) (em um sistema FreeBSD 9.3), o seguinte é indicado:

 The strlen() function returns the number of characters that precede the
 terminating NUL character.  The strnlen() function returns either the
 same result as strlen() or maxlen, whichever is smaller.

O que eu interpreto é que o comprimento da string não inclui o nulo. É por isso que copio len + 1 bytes para incluir o nulo, e o teste verifica se o comprimento <tamanho do buffer - 2. Menos um porque o buffer inicia na posição 0 e menos outro para garantir que haja espaço para o nulo.

EDIT: Acontece que o tamanho de algo começa com 1 enquanto o acesso começa com 0; portanto, o -2 anterior estava incorreto porque retornaria um erro para qualquer coisa> 98 bytes, mas deveria ser> 99 bytes.

EDIT: Embora a resposta sobre um curto não assinado esteja geralmente correta, já que o comprimento máximo que pode ser representado é 65.535 caracteres, isso realmente não importa, porque se a string for maior que isso, o valor será contornado. É como pegar 75.231 (que é 0x000125DF) e mascarar os 16 bits principais, fornecendo 9695 (0x000025DF). O único problema que vejo com isso são os primeiros 100 caracteres após 65.535, pois a verificação de comprimento permitirá a cópia, mas copiará até os 100 primeiros caracteres da string em todos os casos e terminará nula . Portanto, mesmo com o problema abrangente, o buffer ainda não será excedido.

Isso pode ou não representar, por si só, um risco de segurança, dependendo do conteúdo da string e para o que você a está usando. Se é apenas texto simples legível por humanos, geralmente não há problema. Você acabou de obter uma string truncada. No entanto, se for algo como uma URL ou mesmo uma sequência de comandos SQL, você poderá ter um problema.

Daniel Rudy
fonte
2
É verdade, mas isso está além do escopo da questão. O código mostra claramente a função que está sendo transmitida por um ponteiro de char. Fora do escopo da função, não nos importamos.
Daniel Rudy
"o buffer no qual str está armazenado" - esse não é um buffer overflow , que é o problema aqui. E toda resposta tem esse "problema", que é inevitável, dada a assinatura de func... e todas as outras funções C já escritas que usam seqüências terminadas em NUL como argumentos. Expor a possibilidade de a entrada não ter terminação NUL é completamente sem noção.
Jim Balter
"isso está além do escopo da questão" - que infelizmente está além da capacidade de algumas pessoas de compreender.
Jim Balter
"O problema está aqui" - você está certo, mas ainda falta o problema principal, que é o teste ( len >= 100) com um valor, mas o comprimento da cópia com um valor diferente ... isso é uma violação do princípio DRY. Simplesmente chamar strncpy(buffer, str, len)evita a possibilidade de estouro de buffer e faz menos trabalho do que strncpy(buffer,str,sizeof(buffer) - 1)... embora aqui seja apenas um equivalente mais lento memcpy(buffer, str, len).
Jim Balter
@ JimBalter Está além do escopo da questão, mas discordo. Entendo que os valores usados ​​pelo teste e o que é usado no strncpy são dois diferentes. No entanto, a prática geral de codificação diz que o limite de cópias deve ser sizeof (buffer) - 1, portanto, não importa qual o comprimento de str na cópia. O strncpy irá parar de copiar bytes quando atingir um nulo ou copiar n bytes. A próxima linha garante que o último byte no buffer seja um caractere nulo. O código é seguro, eu mantenho minha declaração anterior.
Daniel Daniel
11

Mesmo que você esteja usando strncpy, o comprimento do corte ainda depende do ponteiro da string passado. Você não tem idéia de quanto tempo essa cadeia é (a localização do terminador nulo em relação ao ponteiro, ou seja). Portanto, ligar strlensozinho abre a vulnerabilidade. Se você quer ser mais seguro, use strnlen(str, 100).

O código completo corrigido seria:

int func(char *str) {
   char buffer[100];
   unsigned short len = strnlen(str, 100); // sizeof buffer

   if (len >= 100) {
     return -1;
   }

   strcpy(buffer, str); // this is safe since null terminator is less than 100th index
   return 0;
}
Patrick Roberts
fonte
@ user3386109 strlenTambém não teria acesso depois do final do buffer?
Patrick Roberts
2
@ user3386109 o que você está apontando torna a resposta do orlp tão inválida quanto a minha. Não vejo por strnlenque não resolve o problema se o que o orlp está sugerindo está supostamente correto de qualquer maneira.
Patrick Roberts
1
"Acho que o strnlen não resolve nada aqui" - é claro que sim; impede transbordar buffer. "como str poderia apontar para um buffer de 2 bytes, nenhum dos quais é NUL." - isso é irrelevante, pois é verdade em qualquer implementação de func. A questão aqui é sobre o estouro de buffer, não o UB, porque a entrada não é terminada por NUL.
Jim Balter
1
"O segundo parâmetro passado para o strnlen deve ser o tamanho do objeto que o primeiro parâmetro aponta, ou o strnlen é inútil" - isso é um absurdo completo e absoluto. Se o segundo argumento para strnlen for o comprimento da sequência de entrada, então strnlen será equivalente a strlen. Como você obteria esse número e, se o tivesse, por que precisaria ligar para str [n] len? Não é para isso que serve o strnlen.
Jim Balter
1
+1 Embora esta resposta é imperfeito porque não é equivalente ao código do OP - strncpy NUL-pads e não NUL terminar, enquanto NUL-termina strcpy e não NUL-pad, que faz resolver o problema, ao contrário do comentários ridículos e ignorantes acima.
Jim Balter
4

A resposta com a embalagem está correta. Mas há um problema que acho que não foi mencionado se (len> = 100)

Bem, se Len tivesse 100, copiaríamos 100 elementos e não teríamos \ 0 à direita. Isso claramente significaria que qualquer outra função, dependendo da sequência final correta, andaria muito além da matriz original.

A string problemática de C é IMHO insolúvel. É melhor você sempre ter alguns limites antes da ligação, mas mesmo isso não ajudará. Não há verificação de limites e, portanto, os estouros de buffer sempre podem e infelizmente acontecerão ....

Friedrich
fonte
A string problemática é solucionável: basta usar as funções apropriadas. I. e. não strncpy() e amigos, mas a alocação de memória funciona como strdup()e amigos. Eles estão no padrão POSIX-2008, portanto são bastante portáteis, embora não estejam disponíveis em alguns sistemas proprietários.
cmaster - reinstate monica
"qualquer outra função dependendo da sequência final correta" - bufferé local para esta função e não é usada em nenhum outro lugar. Em um programa real, teríamos que examinar como ele é usado ... às vezes a terminação NUL não está correta (o uso original do strncpy era criar entradas de diretório de 14 bytes do UNIX - preenchidas com NUL e não terminadas com NUL). "A string problemática de C é IMHO insolúvel" - enquanto C é uma linguagem incrível que foi superada por uma tecnologia muito melhor, um código seguro pode ser escrito nela se for usada disciplina suficiente.
Jim Balter
Sua observação me parece equivocada. if (len >= 100)é a condição para quando a verificação falha , não quando passa, o que significa que não há um caso em que exatamente 100 bytes sem terminador NUL sejam copiados, pois esse comprimento está incluído na condição de falha.
Patrick Roberts
@ cmaster. Neste caso você está errado. Não é solucionável, porque sempre se pode escrever além dos limites. Sim, é um comportamento indefeso, mas não há como evitá-lo completamente.
Friedrich
@Jim Balter. Isso não importa. Eu posso escrever potencialmente sobre os limites desse buffer local e, portanto, sempre será possível corromper algumas outras estruturas de dados.
Friedrich
3

Além dos problemas de segurança envolvidos na chamada strlenmais de uma vez, geralmente não se deve usar métodos de string em strings cujo comprimento é precisamente conhecido [para a maioria das funções de strings, existe apenas um caso muito restrito em que elas devem ser usadas - em strings para as quais um máximo o comprimento pode ser garantido, mas o comprimento exato não é conhecido]. Uma vez que o comprimento da string de entrada é conhecido e o tamanho do buffer de saída, deve-se descobrir qual o tamanho de uma região que deve ser copiada e, em seguida, usar memcpy()para realmente executar a cópia em questão. Embora seja possível que strcpytenha um desempenho superior memcpy()ao copiar uma sequência de apenas 1 a 3 bytes, em muitas plataformas memcpy()é provável que seja mais do que o dobro da velocidade ao lidar com sequências maiores.

Embora existam algumas situações em que a segurança teria um custo de desempenho, essa é uma situação em que a abordagem segura também é a mais rápida. Em alguns casos, pode ser razoável escrever código que não seja seguro contra entradas que se comportam de maneira estranha, se o código que fornece as entradas pode garantir que elas serão bem comportadas e se a proteção contra entradas mal comportadas impediria o desempenho. Garantir que os comprimentos de sequência sejam verificados apenas uma vez melhora o desempenho e a segurança, embora uma coisa extra possa ser feita para ajudar a proteger a segurança, mesmo ao rastrear manualmente o comprimento da sequência: para cada sequência que se espera que tenha um nulo final, escreva o nulo final explicitamente. do que esperar que a string de origem a tenha. Assim, se alguém estivesse escrevendo um strdupequivalente:

char *strdupe(char const *src)
{
  size_t len = strlen(src);
  char *dest = malloc(len+1);
  // Calculation can't wrap if string is in valid-size memory block
  if (!dest) return (OUT_OF_MEMORY(),(char*)0); 
  // OUT_OF_MEMORY is expected to halt; the return guards if it doesn't
  memcpy(dest, src, len);      
  dest[len]=0;
  return dest;
}

Observe que a última instrução geralmente poderia ser omitida se o memcpy tivesse processado len+1bytes, mas outro segmento era modificar a cadeia de origem, o resultado poderia ser uma cadeia de destino não terminada por NUL.

supercat
fonte
3
Você pode explicar os problemas de segurança envolvidos na ligação strlenmais de uma vez ?
Bogdan Alexandru
1
@BogdanAlexandru: Depois que alguém chama strlene toma alguma ação com base no valor retornado (que era provavelmente o motivo para chamá-lo em primeiro lugar), uma chamada repetida (1) sempre gera a mesma resposta que a primeira, nesse caso, é simplesmente um desperdício de trabalho, ou (2) às vezes (porque outra coisa - talvez outro segmento - modificou a sequência nesse meio tempo) produz uma resposta diferente; nesse caso, o código que faz algumas coisas com o comprimento (por exemplo, alocar um buffer) pode assumir um tamanho diferente do código que faz outras coisas (copiar para o buffer).
Supercat