Por que é tão difícil tornar C menos propenso a estouros de buffer?


23

Estou fazendo um curso na faculdade, onde um dos laboratórios é realizar explorações de buffer overflow no código que eles nos fornecem. Isso varia de explorações simples, como alterar o endereço de retorno de uma função em uma pilha para retornar a uma função diferente, até o código que altera um estado de registro / memória de programas, mas depois retorna à função que você chamou, o que significa que o A função que você chamou é completamente alheia à exploração.

Eu fiz algumas pesquisas sobre isso, e esses tipos de explorações são usadas em praticamente todos os lugares até agora, em coisas como rodar homebrew no Wii e o jailbreak sem restrições para iOS 4.3.1

Minha pergunta é por que esse problema é tão difícil de resolver? É óbvio que esta é uma das principais explorações usadas para hackear centenas de coisas, mas parece que seria muito fácil de corrigir simplesmente truncando qualquer entrada além do comprimento permitido e limpando toda a entrada que você receber.

EDIT: Outra perspectiva que eu gostaria que as respostas fossem consideradas - por que os criadores de C não corrigem esses problemas reimplementando as bibliotecas?

Respostas:


35

Eles consertaram as bibliotecas.

Qualquer biblioteca padrão moderno C contém variantes mais seguras de strcpy, strcat, sprintfe assim por diante.

Nos sistemas C99 - que é a maioria dos Unixes - você os encontrará com nomes como strncate snprintf, o "n" indicando que é necessário um argumento do tamanho de um buffer ou do número máximo de elementos a serem copiados.

Essas funções podem ser usadas para lidar com muitas operações com mais segurança, mas, em retrospecto, sua usabilidade não é ótima. Por exemplo, algumas snprintfimplementações não garantem que o buffer seja nulo. strncatleva vários elementos para copiar, mas muitas pessoas passam por engano o tamanho do buffer de destino.

No Windows, um muitas vezes encontra o strcat_s, sprintf_s, o sufixo "_s" indicando "seguro". Eles também chegaram à biblioteca padrão C no C11 e fornecem mais controle sobre o que acontece no caso de um estouro (truncamento x afirmação, por exemplo).

Muitos fornecedores fornecem ainda mais alternativas não padronizadas, como asprintfna GNU libc, que alocará um buffer do tamanho apropriado automaticamente.

A idéia de que você pode "apenas consertar C" é um mal-entendido. Corrigir C não é o problema - e já foi feito. O problema é consertar décadas de código C escritos por programadores ignorantes, cansados ​​ou apressados, ou códigos que foram transportados de contextos em que a segurança não importava para contextos em que a segurança importa. Nenhuma alteração na biblioteca padrão pode corrigir esse código, embora a migração para compiladores e bibliotecas padrão mais recentes geralmente ajude a identificar os problemas automaticamente.


11
+1 para apontar o problema para os programadores, não para o idioma.
Nicol Bolas

8
@ Nicol: Dizer "o problema [são] os programadores" é injustamente reducionista. O problema é que, durante anos (décadas), C tornou mais fácil escrever código inseguro do que código seguro, principalmente porque nossa definição de "seguro" evoluiu mais rapidamente do que qualquer padrão de linguagem e esse código ainda existe. Se você quiser reduzir isso a um único substantivo, o problema é "1970-1999 libc", não "os programadores".

1
Ainda é responsabilidade dos programadores usar as ferramentas que eles têm agora para corrigir esses problemas. Tire um meio dia ou mais e dê uma olhada no código-fonte para essas coisas.
Nicol Bolas

1
@ Nicol: Embora seja trivial detectar um potencial estouro de buffer, geralmente não é trivial ter certeza de que é uma ameaça real e menos trivial para descobrir o que deve acontecer se o buffer for transbordado. O tratamento de erros geralmente não é considerado, não é possível implementar "rapidamente" uma melhoria, pois você pode alterar o comportamento de um módulo de maneiras inesperadas. Acabamos de fazer isso em uma base de códigos herdada de vários milhões de linhas e, embora valha a pena exercitar, isso custa muito tempo (e dinheiro).
mattnz

4
@NicolBolas: Não tenho certeza em que tipo de loja você trabalha, mas o último local em que escrevi C para uso em produção exigiu alterar o documento de design detalhado, revisá-lo, alterar o código, alterar o plano de teste, revisar o plano de teste, revisar o plano de teste e realizar uma execução completa teste do sistema, revisando os resultados do teste e certificando novamente o sistema no local do cliente. Isto é para um sistema de telecomunicações em um continente diferente, escrito para uma empresa que não existe mais. A última vez que soube, a fonte estava em um arquivo RCS em uma fita QIC que ainda deveria ser legível, se você encontrar uma unidade de fita adequada.
TMN

19

Não é realmente impreciso dizer que C é realmente "propenso a erros" por design . Além de alguns erros graves como gets, a linguagem C não pode realmente ser de outra maneira sem perder o recurso principal que atrai as pessoas para C em primeiro lugar.

C foi projetado como uma linguagem de sistemas para atuar como uma espécie de "montagem portátil". Uma característica importante da linguagem C é que, diferentemente das linguagens de nível superior, o código C geralmente mapeia muito de perto o código de máquina real. Em outras palavras, ++igeralmente é apenas uma incinstrução, e muitas vezes você pode ter uma idéia geral do que o processador estará fazendo em tempo de execução, observando o código C.

Mas a adição de verificação implícita de limites acrescenta uma sobrecarga extra - que o programador não solicitou e pode não querer. Essa sobrecarga vai muito além do armazenamento extra necessário para armazenar o comprimento de cada matriz ou das instruções extras para verificar os limites da matriz em todos os acessos à matriz. E a aritmética dos ponteiros? Ou então, se você tem uma função que recebe um ponteiro? O ambiente de tempo de execução não tem como saber se esse ponteiro se enquadra nos limites de um bloco de memória legitimamente alocado. Para acompanhar isso, você precisará de uma arquitetura de tempo de execução séria que possa verificar cada ponteiro em uma tabela de blocos de memória alocados no momento; nesse momento, já estamos entrando no território de tempo de execução gerenciado no estilo Java / C #.


12
Honestamente, quando as pessoas perguntam por que C não é "seguro", fico me perguntando se elas reclamariam que a montagem não é "segura".
Ben Brocka

5
A linguagem C é muito parecida com a montagem portátil em uma máquina PDP-11 da Digital Equipment Corporation. Ao mesmo tempo, as máquinas Burroughs tinha limites de matriz verificação na CPU, então eles estavam realmente fácil de obter programas direito em cheques de matriz na vida de hardware em hardware em Rockwell Collins. (Usado principalmente na aviação.)
Tim Williscroft

15

Eu acho que o verdadeiro problema não é que estes tipos de erros são difíceis de corrigir, mas que eles são tão fáceis de fazer: Se você usar strcpy, sprintfe amigos no caminho (aparentemente) simples que o trabalho pode, então você provavelmente abriu a porta para um estouro de buffer. E ninguém notará isso até que alguém o explore (a menos que você tenha ótimas análises de código). Agora, adicione o fato de que existem muitos programadores medíocres e que estão sob pressão de tempo a maior parte do tempo - e você tem uma receita para um código tão cheio de estouros de buffer que será difícil corrigi-los, simplesmente porque há muitos deles e eles estão se escondendo tão bem.


3
Você realmente não precisa de "muito boas análises de código". Você só precisa banir o sprintf ou redefinir o sprintf para algo que use sizeof () e erros no tamanho de um ponteiro, etc. Você nem precisa de revisões de código, pode fazer esse tipo de coisa com a confirmação do SCM ganchos e grep.

1
@ JoeWreschnig: sizeof(ptr)é 4 ou 8, geralmente. Essa é outra limitação em C: não há como determinar o comprimento de uma matriz, dado apenas o ponteiro para ela.
MSalters

@MSalters: Sim, uma matriz de int [1] ou char [4] ou o que quer que seja um falso positivo, mas na prática você nunca está lidando com buffers desse tamanho com essas funções. (Não estou falando teoricamente aqui - trabalhei em uma grande base de código C por quatro anos que usava essa abordagem. Nunca atingi a limitação do sprintfing em um caractere [4].)

5
@BlackJack: A maioria dos programadores não é estúpida - se você forçá-los a passar do tamanho, eles passam pelo correto. É apenas a maioria também não vai passar o tamanho, a menos que forçado. Você pode escrever uma macro que retornará o comprimento de uma matriz se for estática ou com tamanho automático, mas erros se for indicado um ponteiro. Em seguida, defina sprintf para chamar snprintf com essa macro fornecendo o tamanho. Agora você tem uma versão do sprintf que funciona apenas em matrizes com tamanhos conhecidos e força o programador a chamar snprintf com um tamanho especificado manualmente.

1
Um exemplo simples dessa macro seria o #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / (sizeof(a) != sizeof(void *))que acionará uma divisão do tempo de compilação por zero. Outro inteligente que vi pela primeira vez no Chromium é o #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]) / !(sizeof(a) % sizeof((a)[0]))que troca alguns poucos falsos positivos por alguns falsos negativos - infelizmente é inútil para char []. Você pode usar várias extensões do compilador para torná-lo ainda mais confiável, por exemplo, blogs.msdn.com/b/ce_base/archive/2007/05/08/… .

7

É difícil corrigir estouros de buffer porque C praticamente não fornece ferramentas úteis para solucionar o problema. É uma falha de linguagem fundamental que os buffers nativos não oferecem proteção e é praticamente, se não completamente, impossível substituí-los por um produto superior, como o C ++ fez com std::vectore std::array, e é difícil, mesmo no modo de depuração, encontrar estouros de buffer.


13
"Falha na linguagem" é uma afirmação muito tendenciosa. O fato de as bibliotecas não fornecerem verificação de limites era uma falha; que o idioma não é uma escolha consciente para evitar sobrecarga. Essa escolha faz parte do que permite que construções de nível superior std::vectorsejam implementadas com eficiência. E vector::operator[]faz a mesma escolha por velocidade sobre segurança. A segurança vectorvem de facilitar a distribuição do tamanho, que é a mesma abordagem adotada pelas bibliotecas C modernas.

1
@ Charles: "C simplesmente não fornece nenhum tipo de buffer de expansão dinâmica como parte da biblioteca padrão." Não, isso não tem nada a ver com isso. Primeiro, C os fornece via realloc(C99 também permite dimensionar matrizes de pilha usando um tamanho determinado por tempo de execução, mas constante, por qualquer variável automática, quase sempre preferível char buf[1024]). Segundo, o problema não tem nada a ver com a expansão de buffers, tem a ver se os buffers carregam ou não tamanho com eles e verificam esse tamanho quando você os acessa.

5
@ Joe: O problema não é tanto que as matrizes nativas estão quebradas. É que eles são impossíveis de substituir. Para começar, vector::operator[]faz checagem de limites no modo de depuração - algo que matrizes nativas não podem fazer - e, em segundo lugar, não há como C trocar o tipo de matriz nativa por outro que possa fazer checagem de limites, porque não há modelos e nenhum operador sobrecarga. No C ++, se você quiser passar de T[]para std::array, você pode praticamente trocar apenas um typedef. Em C, não há como conseguir isso, nem escrever uma classe com funcionalidade equivalente, muito menos interface.
DeadMG

3
@ Joe: Exceto que nunca pode ser estaticamente dimensionado e você nunca pode torná-lo genérico. É impossível escrever qualquer biblioteca em C que realiza o mesmo papel que std::vector<T>e std::array<T, N>fazer em C ++. Não haveria maneira de projetar e especificar qualquer biblioteca, nem mesmo a Standard, que pudesse fazer isso.
DeadMG

1
Não sei ao certo o que você quer dizer com "nunca pode ser estaticamente dimensionado". Como eu usaria esse termo, std::vectortambém nunca pode ser estaticamente dimensionado. Quanto ao genérico, você pode torná-lo tão genérico quanto o bom C precisar ser - um pequeno número de operações fundamentais no void * (adicionar, remover, redimensionar) e tudo o mais escrito especificamente. Se você vai reclamar que C não possui genéricos no estilo C ++, isso está muito fora do escopo do manuseio seguro de buffer.

7

O problema não é com o C linguagem .

Na IMO, o único grande obstáculo a ser superado é que C é simplesmente ensinado mal . Décadas de más práticas e informações erradas foram institucionalizadas em manuais de referência e notas de aula, envenenando as mentes de cada nova geração de programadores desde o início. Os alunos recebem uma breve descrição das funções de E / S "fáceis", como gets1 ou, scanfe então são deixadas para seus próprios dispositivos. Eles não são informados sobre onde ou como essas ferramentas podem falhar ou como evitar essas falhas. Eles não são informados sobre o uso fgetsestrtol/strtodporque essas são consideradas ferramentas "avançadas". Então eles são desencadeados no mundo profissional para causar estragos. Não que muitos dos programadores mais experientes saibam melhor, porque receberam a mesma educação com danos cerebrais. É enlouquecedor. Eu vejo muitas perguntas aqui e no Stack Overflow e em outros sites onde fica claro que a pessoa que está fazendo a pergunta está sendo ensinada por alguém que simplesmente não sabe do que está falando , e é claro que você não pode simplesmente dizer "seu professor está errado", porque ele é professor e você é apenas um cara na Internet.

E então você tem a multidão que despreza qualquer resposta começando com "bem, de acordo com o padrão do idioma ..." porque eles estão trabalhando no mundo real e, segundo eles, o padrão não se aplica ao mundo real . Eu posso lidar com alguém que apenas tem uma educação ruim, mas quem insiste em ser ignorante é apenas uma desgraça para a indústria.

Não haveria problemas de estouro de buffer se o idioma fosse ensinado corretamente, com ênfase na escrita de código seguro. Não é "difícil", não é "avançado", é apenas ter cuidado.

Sim, isso tem sido um discurso retórico.


1 Que, felizmente, finalmente foi retirado da especificação da linguagem, embora ocorra para sempre 40 anos de código legado.


1
Enquanto eu concordo principalmente com você, acho que você ainda está sendo um pouco injusto. O que consideramos "seguro" também é uma função do tempo (e vejo que você é um desenvolvedor de software profissional há muito mais tempo do que eu, por isso tenho certeza de que você está familiarizado com isso). Daqui a dez anos, alguém estará tendo a mesma conversa sobre por que diabos todos em 2012 usaram implementações de tabelas de hash compatíveis com DoS, não sabíamos nada sobre segurança? Se houver um problema no ensino, é um problema que focamos demais no ensino das "melhores práticas", e não que a melhor prática em si evolua.

1
E sejamos honestos. Você pode escrever um código seguro com apenas sprintf, mas isso não significa que o idioma não tenha falhas. C era falho e é falho - como qualquer idioma - e é importante admitirmos essas falhas para que possamos continuar a corrigi-las.

@JoeWreschnig - Embora eu concorde com o ponto mais amplo, acho que há uma diferença qualitativa entre implementações de tabelas de hash compatíveis com DoS e excedentes de buffer. O primeiro pode ser atribuído a circunstâncias que evoluem ao seu redor, mas o segundo não tem desculpas; saturações de buffer são erros de codificação, ponto final. Sim, C não tem guardas de lâmina e cortará você se você for descuidado; podemos discutir se isso é uma falha no idioma ou não. Isso é ortogonal ao fato de que muito poucos os alunos recebem qualquer instrução de segurança quando estão aprendendo a língua.
21712 John Bode

5

O problema é tanto a falta de visão gerencial quanto a incompetência do programador. Lembre-se, um aplicativo de 90.000 linhas precisa de apenas uma operação insegura para ser completamente insegura. Está quase além da possibilidade que qualquer aplicativo escrito sobre o manuseio de strings fundamentalmente inseguro seja 100% perfeito - o que significa que será inseguro.

O problema é que os custos de insegurança não são cobrados do destinatário certo (a empresa que vende o aplicativo quase nunca precisará reembolsar o preço de compra) ou não são claramente visíveis no momento em que as decisões são tomadas ("Temos que enviar em março, não importa o quê! "). Estou bastante certo de que se você considerasse os custos e os custos de longo prazo para os usuários, e não para o lucro da sua empresa, escrever em C ou em idiomas relacionados seria muito mais caro, provavelmente tão caro que é claramente a escolha errada em muitos campos onde hoje em dia a sabedoria convencional diz que é uma necessidade. Mas isso não mudará a menos que seja introduzida uma responsabilidade muito mais rigorosa por software - que ninguém na indústria deseja.


-1: Culpar a administração como a raiz de todo mal não é particularmente construtivo. Ignorando a história um pouco menos. A resposta é quase resgatada pela última frase.
mattnz

Uma responsabilidade mais estrita por software pode ser introduzida por usuários interessados ​​em segurança e dispostos a pagar por ela. Pode-se argumentar que poderia ser introduzido com severas penalidades por violações de segurança. Uma solução baseada no mercado funcionaria se os usuários estivessem dispostos a pagar pela segurança, mas não estão.
David Thornley

4

Um dos grandes poderes do uso de C é que ele permite manipular a memória da maneira que você achar melhor.

Uma das grandes fraquezas do uso de C é que ele permite manipular a memória da maneira que você achar melhor.

Existem versões seguras de quaisquer funções não seguras. No entanto, programadores e compiladores não impõem estritamente seu uso.


2

por que os criadores de C não corrigem esses problemas reimplementando as bibliotecas?

Provavelmente porque o C ++ já fez isso e é compatível com o código C. Portanto, se você deseja um tipo de string seguro no seu código C, basta usar std :: string e escrever seu código C usando um compilador C ++.

O subsistema de memória subjacente pode ajudar a evitar estouros de buffer, introduzindo blocos de proteção e verificação de validade deles - para que todas as alocações tenham 4 bytes de 'fefefefe' adicionados, quando esses blocos são gravados, o sistema pode lançar um wobbler. Não é garantido impedir uma gravação na memória, mas mostrará que algo deu errado e precisa ser corrigido.

Eu acho que o problema é que as velhas rotinas strcpy etc ainda estão presentes. Se eles foram removidos em favor do strncpy etc, isso ajudaria.


1
Remover completamente o strcpy etc. tornaria os caminhos de atualização incremental ainda mais difíceis, o que, por sua vez, resultaria em pessoas que não atualizariam. Da maneira como é feito agora, você pode mudar para um compilador C11, começar a usar variantes _s, banir variantes não _s e corrigir o uso existente, por qualquer período de tempo praticamente viável.

-2

É simples entender por que o problema do estouro não está resolvido. C foi falho em algumas áreas. Na época, essas falhas eram vistas como toleráveis ​​ou até mesmo como um recurso. Agora, décadas depois, essas falhas não podem ser corrigidas.

Algumas partes da comunidade de programação não querem esses buracos. Basta olhar para todas as guerras de chamas que começam com strings, matrizes, ponteiros, coleta de lixo ...


5
LOL, resposta terrível e equivocada.
Heath Hunnicutt

1
Para explicar por que essa é uma resposta ruim: C realmente tem muitas falhas, mas permitir que os estouros de buffer etc. tenham muito pouco a ver com eles, mas com os requisitos básicos de linguagem. Não seria possível projetar um idioma para executar o trabalho de C e não permitir estouros de buffer. Partes da comunidade não querem abrir mão dos recursos que C lhes permite, geralmente por um bom motivo. Também existem divergências sobre como evitar alguns desses problemas, mostrando que não temos um entendimento completo do design da linguagem de programação, nada mais.
David Thornley

1
@DavidThornley: Pode-se projetar uma linguagem para fazer o trabalho de C, mas torná-la de maneira que as maneiras idiomáticas normais de fazer as coisas permitiriam pelo menos que um compilador verifique estouros de buffer razoavelmente eficientemente, caso o compilador opte por fazê-lo. Há uma enorme diferença entre ter memcpy()disponível e ser apenas um meio padrão de copiar eficientemente um segmento de matriz.
Supercat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.