Curso MIT "Segurança de sistemas de computadores". Palestra 2: “Controle de ataques de hackers”, parte 1

Instituto de Tecnologia de Massachusetts. Curso de Aula nº 6.858. "Segurança de sistemas de computador". Nikolai Zeldovich, James Mickens. 2014 ano


Computer Systems Security é um curso sobre o desenvolvimento e implementação de sistemas de computador seguros. As palestras abrangem modelos de ameaças, ataques que comprometem a segurança e técnicas de segurança baseadas em trabalhos científicos recentes. Os tópicos incluem segurança do sistema operacional (SO), recursos, gerenciamento de fluxo de informações, segurança de idiomas, protocolos de rede, segurança de hardware e segurança de aplicativos da web.

Palestra 1: “Introdução: modelos de ameaças” Parte 1 / Parte 2 / Parte 3
Palestra 2: “Controle de ataques de hackers” Parte 1 / Parte 2 / Parte 3

James Mickens: Na palestra anterior, aprendemos tudo sobre ataques de estouro de buffer e hoje continuaremos discutindo alguns métodos para iniciar esses ataques. A idéia básica por trás de um ataque de buffer overflow é a seguinte.



Antes de tudo, observo que esses ataques afetam várias circunstâncias diferentes. A primeira circunstância que eles usam é que o software do sistema geralmente é escrito em C.

Por software do sistema, quero dizer bancos de dados, compiladores, servidores de rede e similares. Você pode se lembrar de seu shell de comando favorito. Todo esse "software" geralmente é escrito em C. Por que em C? Porque, em primeiro lugar, é mais rápido e, em segundo lugar, C é considerado um montador de alto nível que melhor se adapta às necessidades de uma variedade de plataformas de hardware. Portanto, todos os sistemas críticos são escritos nessa linguagem de programação de baixo nível. O problema com o software escrito em C é que ele realmente usa endereços de memória bruta e não possui nenhuma ferramenta ou módulo de software para verificá-los. Em alguns casos, isso pode levar a conseqüências desastrosas.

Por que não há verificação de índice de matriz em C, ou seja, nenhuma verificação de borda? Uma razão é que o hardware não. E as pessoas que escrevem em C geralmente desejam a velocidade de execução do programa mais rápida possível. Outra razão é que em C, como discutiremos mais adiante, é realmente muito difícil definir a semântica do que é um ponteiro e até que ponto ele deve agir. Portanto, em alguns casos, seria muito difícil automatizar processos de software em C.
Vamos discutir algumas tecnologias que realmente estão tentando criar algum tipo de gerenciamento automático de memória. Mas, como veremos, nenhum desses métodos é completamente "à prova de balas".

Além disso, os ataques de estouro de buffer usam o conhecimento x86 da arquitetura, por exemplo, em qual direção a pilha está crescendo. O que é uma convenção de chamada para funções? Quando você acessa a função C, como é a pilha? E quando você seleciona um objeto na pilha, como são essas estruturas selecionadas principais?

Vejamos um exemplo simples. Isso é muito semelhante ao que você viu na última palestra. Então, aqui temos uma solicitação de leitura padrão e, em seguida, obtemos um buffer; aqui, vem o int i canônico, seguido pelo infame comando gets . E abaixo temos outras coisas necessárias.



Então, como discutimos na palestra na semana passada, isso é problemático, certo? Porque esta operação obtém não verifica os limites do buffer. Se o usuário preencher os dados com o buffer e usarmos essa função não segura aqui, podemos realmente estourar o buffer. Podemos reescrever todo o conteúdo da pilha. Deixe-me lembrá-lo como fica.

Na parte inferior é a matriz "i". Um buffer está localizado acima dele, tem o primeiro endereço na parte inferior e o último no topo. De qualquer forma, acima do buffer, temos o valor salvo do indicador de gap - valor salvo EBP. Acima está o endereço de retorno da função, e ainda mais algumas coisas do quadro anterior.

E não se esqueça que aqui, na parte inferior, à esquerda de "i", temos um ponteiro de pilha ESP que entra lá e um novo ponteiro de quebra vem na seção EBP salva. O endereço de retorno inclui ESP e o restante do quadro anterior inclui um ponto de interrupção.



Deixe-me lembrá-lo de que a maneira como a pilha transborda é que os dados são acumulados para cima, na direção dessa seta à direita. Quando a operação get é iniciada, começamos a gravar bytes no buffer; no final, ele começa a sobrescrever tudo o que está localizado a montante. Basicamente, tudo deve parecer familiar para você.

O que um invasor faz para tirar proveito disso? Basicamente, ele insere uma longa sequência de dados. Portanto, a idéia principal é que essa técnica possa ser usada para atacar.

E se o endereço de retorno for capturado pelo invasor, ele poderá determinar para onde a função saltará após o estouro. Ou seja, a única coisa que um hacker pode fazer é interceptar o endereço de retorno e pular para onde ele quiser. Os atacantes geralmente executam código com privilégios para controlar o processo de interceptação.

Portanto, se esse processo era de alta prioridade, por exemplo, era executado como root ou administrador, não importa o que chamamos de superusuário do seu sistema operacional favorito, agora este programa, controlado por um invasor, pode fazer o que quiser, usando os privilégios dessa prioridade. Assim, um hacker pode ler arquivos ou enviar spam se danificar um servidor de email. Pode até derrotar os firewalls, porque a idéia de um firewall é a de que existem máquinas “boas” por trás e máquinas “ruins” fora dele. Normalmente, os computadores dentro do firewall “confiam” um no outro e, se você conseguir invadir pelo menos um computador dentro de uma rede protegida por um firewall, será ótimo. Porque agora você pode simplesmente ignorar as muitas verificações que esses computadores costumam fazer em relação às máquinas "alienígenas", porque elas a consideram uma pessoa confiável.

Há uma coisa que você teria que pensar e que eu pensava como estudante:

“Ótimo, eles nos mostraram como sobrecarregar o buffer, mas por que o sistema operacional não pode impedir isso? Ela não age como alguém como os Guardiões da Galáxia, que protege o bem das coisas más que acontecem por aí? ”

É importante observar que o sistema operacional não monitora você constantemente. E o hardware observa, extrai instruções e as descriptografa e faz muitas coisas parecidas. Mas, em uma primeira aproximação, o que o sistema operacional faz? Basicamente, configura tabelas de páginas que permitem que o aplicativo funcione, e se você solicitar ao sistema operacional, por exemplo, para enviar um pacote de rede, ou se desejar fazer algum tipo de solicitação IPC ou algo semelhante, você será direcionado ao sistema operacional para obter ajuda. Mas o sistema operacional não segue todas as instruções que seu aplicativo executa. Em outras palavras, quando esse buffer está cheio, o sistema operacional não monitora de maneira alguma como a memória dessa pilha é usada. Todo esse espaço de endereço pertence a você, como iniciador do processo, e isso não se aplica ao sistema operacional. Você pode fazer o que quiser com isso, e o sistema operacional não pode ajudá-lo com problemas.

Mais adiante, discutiremos algumas das coisas que o sistema operacional pode fazer com relação ao hardware para se defender contra esse tipo de ataque. Deixe-me lembrá-lo novamente - na verdade, apenas o hardware monitora o que você está fazendo e reage a ele. Assim, você pode tirar proveito de alguns tipos especiais de proteção, discutiremos mais a fundo.

É assim que um estouro de buffer se parece. Como vamos consertar todas essas coisas?

Uma maneira de evitar estouros de buffer é simplesmente evitar erros no código C. Essa é uma abordagem construtiva, porque se o seu programa não tiver erros, o invasor não poderá usá-los. No entanto, isso é mais fácil dizer do que fazer. Existem algumas coisas muito simples que os programadores podem fazer para fornecer uma "higiene" de segurança. Por exemplo, recursos como o get, que sabemos agora, podem ser chamados de "go-tos" ou "capturar o sistema operacional", o que é uma violação de segurança.

Portanto, quando você compila seu código usando um compilador moderno, como o GCC ou o Visual Studio, eles indicam as desvantagens de tais funções. Eles dirão: "Ei, você está usando uma coisa perigosa, é melhor considerar o uso da função fgets ou de outras funções que possam realmente acompanhar a conformidade com as fronteiras". Essa é uma das coisas simples que os programadores podem fazer.

Mas observe que muitos aplicativos realmente manipulam buffers sem recorrer a todas essas funções. Isso é muito comum em servidores de rede que definem seus próprios procedimentos de análise e, em seguida, verifique se os dados são recuperados do buffer da maneira que eles desejam. Assim, limitando-se simplesmente à seleção das funções de comando corretas, não será possível resolver completamente o problema.

Outra circunstância que dificulta essa abordagem do problema é que nem sempre é óbvio que o problema é causado por um erro em um programa escrito em C. Se você já trabalhou em um programa de larga escala que foi escrito em C, você sabe , como acontece com os identificadores de função que possuem 18 estrelas acima do ponteiro void *. Eu acho que apenas Zeus sabe o que isso pode significar, certo? Com linguagens como C, mesmo um programador pode achar muito difícil entender se ocorreu ou não um erro.

Em geral, um dos principais tópicos de nossas palestras será que o idioma C é um produto do diabo. E usamos apenas porque sempre queremos ser mais rápidos do que todos os outros, certo? Porém, à medida que o hardware se torna cada vez mais rápido, usamos linguagens mais avançadas para escrever códigos volumosos de sistema. No entanto, nem sempre faz sentido escrever seu código C, mesmo que você pense que será mais rápido. Discutiremos esse problema mais tarde.



Portanto, a primeira abordagem para resolver o problema é evitar erros no código do programa C, e a segunda é criar ferramentas que ajudem os programadores a encontrar esses erros. Um exemplo dessa ferramenta é a análise de código estático. Mais tarde falaremos sobre isso em detalhes, e agora direi que a análise estática é uma maneira de analisar o código-fonte do seu programa antes mesmo de iniciar, e ajuda a detectar possíveis problemas.

Imagine que você tem essa função, vamos chamá-la de void foo (int, * p) , que contém dados inteiros e um ponteiro. Digamos que ele declara um valor de deslocamento inteiro int off . Esta função declara outro ponteiro e adiciona um deslocamento a ela: int * z = p + desativado . Mesmo agora, ao escrever uma função, a análise de código estático pode nos dizer que essa variável de deslocamento não é inicializada.



Assim, analisando o programa, é possível responder à pergunta se nossa função funcionará corretamente. E neste exemplo, é muito simples ver a resposta "não, não" porque não há inicialização de deslocamento. A análise estática é um software e, à medida que você usa o compilador popular para criar seu código, ele diz: “Ei, amigo, isso não é inicializado. Tem certeza de que deseja fazer exatamente isso? Este é um dos exemplos mais simples de uso da análise estática.

Outro exemplo considera o caso quando temos um ramo de uma função, ou seja, sua execução sob uma determinada condição.



Portanto, se o deslocamento for maior que 8, se (desativado> 8) , isso levará a uma chamada para alguma barra de função (desativada) . Portanto, essa condição nos diz qual é o valor do deslocamento. Mesmo ignorando o fato de que o deslocamento não foi inicializado, ao analisar esse ramo da função, ainda descobrimos que ele pode ser maior que 8. Portanto, quando começamos a realizar uma análise estática da barra, descobrimos que o deslocamento pode ter apenas determinados valores. Observo mais uma vez que esta é uma introdução muito superficial à análise estática, depois consideraremos essa ferramenta em mais detalhes. Mas este exemplo mostra como você pode detectar alguns tipos de erros, mesmo sem executar o código.

Então, mais uma coisa em que você pode pensar é que faz a mesma coisa que a análise estática. Isso é um fuzz de software. A ideia dele é que você pegue todas as funções no código do programa e insira valores aleatórios nelas. Assim, todas as opções para os valores e formatos do seu código se sobrepõem. Ou seja, o Fuzzing é uma ferramenta para procurar automaticamente vulnerabilidades, enviando dados ou dados inválidos no formato errado à entrada do programa. Por exemplo, você insere os valores 2, 4, 8 e 15 no teste de unidade e recebe uma mensagem de que o número 15 provavelmente está incorreto, pois todos os números são pares, mas são ímpares.

De fato, você precisa observar quantas ramificações do programa como um todo afetam seu código de teste, porque esses são geralmente os locais onde os "bugs" estão ocultos. Os programadores não pensam nesses "cantos e recantos" e, como resultado, passam em alguns testes de unidade, você pode dizer a maioria desses testes. No entanto, eles não examinam todos os "cantos e recantos" do programa, e é aí que a análise estática pode ajudar. Novamente, usando coisas como o conceito de restrição. Por exemplo, em nossa seção de programa, esta é uma condição para ramificar uma função que define um deslocamento de mais de oito. Assim, podemos descobrir que esse deslocamento é estático. E se usarmos a geração automática de dados de entrada do Fuzzing com base nessa restrição, podemos garantir que um dos valores de entrada para o deslocamento seja menor que 8, um seja 8 e um seja mais que 8. Isso está claro?



Portanto, essa é a principal idéia por trás do conceito de criação de ferramentas para ajudar os programadores a encontrar erros. Mesmo a análise parcial de código pode ser muito útil ao trabalhar com a linguagem C. Muitas das ferramentas que examinaremos servem para evitar estouros de buffer ou verificar a inicialização de variáveis ​​que não são capazes de detectar todos os problemas do código do programa. Mas eles podem ser úteis na melhoria da segurança desses programas. A desvantagem dessas ferramentas é que elas não estão completas. O progresso em perspectiva não é um progresso completo. Portanto, você precisa explorar ativamente o problema da proteção contra explorações, tanto em programas escritos em C quanto em outros programas. Examinamos duas abordagens para solucionar o problema de proteção de buffer overflow, mas existem outras abordagens.

Portanto, a terceira abordagem é o uso de uma linguagem segura para a memória, ou uma linguagem que garanta a segurança da memória. Essas linguagens incluem Python, Java, c #. Não quero colocar o Perl em pé de igualdade com eles, porque é usado por "pessoas más". Dessa forma, você pode usar uma linguagem segura de memória e parece que essa é a coisa mais óbvia que você pode fazer. Eu acabei de explicar que basicamente C é um codificador de assembly de alto nível, mas fornece ponteiros brutos e outras coisas indesejadas. Por que não usar apenas uma dessas linguagens de programação de alto nível?

Existem várias razões para isso. Primeiramente, nessas linguagens, existem muitos elementos do código herdado de C. Tudo está bem se você iniciar um novo projeto e usar uma das linguagens de alto nível que garantam a segurança da memória. Mas e se você recebesse um grande arquivo binário ou uma grande distribuição de código-fonte que foi escrita em C e mantida por 10 a 15 anos, esse foi um projeto de gerações, quero dizer que até nossos filhos continuarão trabalhando nele ? Nesse caso, você não será capaz de dizer: “Acabei de reescrever tudo em C # e mudar o mundo!”.

E o problema não está apenas na linguagem C, existem sistemas que você deve ter ainda mais medo, pois eles usam códigos Fortran e COBOL, coisas da Guerra Civil. Por que isso está acontecendo? Porque, como engenheiros, queremos pensar que podemos construir tudo sozinhos, e será incrível, será como eu quero, e chamarei minhas variáveis ​​como quero.

Mas no mundo real isso não acontece. Você aparece no trabalho e possui esse sistema que já existe, olha a base do código e pensa por que ele não faz o que é necessário? E então eles lhe dizem: "Escute, faremos tudo o que você quiser, mas apenas na segunda versão do programa, e agora você terá que fazer o que temos que funcionar, porque, caso contrário, os clientes receberão seu dinheiro de volta".

Então, como lidamos com o enorme problema do uso forçado de código legado? Como você sabe, uma das vantagens dos sistemas com definição incorreta de limites é que eles funcionam perfeitamente com esse código desatualizado. Esse é um dos motivos pelos quais você não pode se livrar do problema de estouro de buffer simplesmente mudando para idiomas que fornecem uso seguro de memória.



E se precisarmos de acesso de baixo nível ao hardware? Por exemplo, para atualizar drivers e outras coisas.

Portanto, outro problema surge se você precisar de acesso de baixo nível ao equipamento, o que acontece ao gravar drivers para alguns dispositivos. Nesse caso, você realmente precisa das vantagens que C oferece, por exemplo, a capacidade de examinar registros e elementos de função semelhantes.

Além disso, a necessidade de usar C surge quando você está preocupado com o desempenho do sistema. , , , . , , . , , memory-safe . , JIT. , Java, Java Script. , , «». , . , «» x86.

, , -. , , JVM, - Java. , - . , - JVM , . , , . . , , .

, , 86. JIT- , . JIT- , .

, JavaScript , , «» 32- , . JIT-, «» . , , JIT-, , , .



«» , asm.js – JavaScript, , , . , , , JavaScript , . JavaScript, JavaScript, C ++.

, -, IO. . , , , , . «» , .

. , - . , C C++, , . Python, , , . . .

, , , .

, . , . , . , «» , . , C C++, .

, ? , , ? ?

. , – . , - , . , . , -, , . IP , , . , - . , , . , , «» .

, , , , , , . , - , . , , , . , , , , , , .



, . stack canaries, « », , . « » , , , . , , .

Vamos desenhar um diagrama da nossa pilha. Precisamos garantir que o atacante primeiro "entre no canário" antes de chegar ao endereço de retorno. E se pudermos detectar isso antes de retornar da função, poderemos detectar o "mal".

28:30 min

Continuação:

Curso MIT "Segurança de sistemas de computadores". Palestra 2: “Controle de ataques de hackers”, parte 2


A versão completa do curso está disponível aqui .

Obrigado por ficar conosco. Você gosta dos nossos artigos? Deseja ver materiais mais interessantes? Ajude-nos fazendo um pedido ou recomendando a seus amigos, um desconto de 30% para os usuários da Habr em um análogo exclusivo de servidores básicos que inventamos para você: Toda a verdade sobre o VPS (KVM) E5-2650 v4 (6 núcleos) 10GB DDR4 240GB SSD 1Gbps de US $ 20 ou como dividir o servidor? (as opções estão disponíveis com RAID1 e RAID10, até 24 núcleos e até 40GB DDR4).

Dell R730xd 2 vezes mais barato? Somente nós temos 2 TVs Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 a partir de US $ 249 na Holanda e nos EUA! Leia sobre Como criar um prédio de infraestrutura. classe usando servidores Dell R730xd E5-2650 v4 custando 9.000 euros por um centavo?

Source: https://habr.com/ru/post/pt414505/


All Articles