Curso MIT "Segurança de sistemas de computadores". Aula 3: Estouros de Buffer: Explorações e Proteção, Parte 2

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
Aula 3: “Estouros de Buffer: Explorações e Proteção” Parte 1 / Parte 2 / Parte 3

Curiosamente, um invasor não pode pular para um endereço específico, apesar de usarmos principalmente endereços codificados. O que ele faz é chamado de "ataque de pilha", e se você é uma pessoa má, será muito divertido para você. Com esse ataque, um hacker começa a alocar dinamicamente toneladas de código de shell e simplesmente inseri-lo aleatoriamente na memória. Isso é especialmente eficaz se você usar linguagens de alto nível dinamicamente, como JavaScript. Assim, o leitor de tags está em um loop estreito e simplesmente gera um grande número de linhas de código de shell e preenche várias delas.

O invasor não pode determinar a localização exata das linhas, ele simplesmente seleciona 10 MB de linhas de código de shell e faz um salto arbitrário. E se ele puder, de alguma forma, controlar um dos ponteiros ret , existe a chance de ele "aterrar" no código do shell.



Você pode usar um truque chamado NOP slide , NOP sled ou NOP ramp , em que NOP é instruções de não operação ou comandos vazios, ociosos. Isso significa que o fluxo de execução do comando do processador "desliza" para o destino final desejado sempre que o programa vai para o endereço de memória em qualquer lugar do slide.

Imagine que se você tiver uma linha de código do shell e for para um local aleatório nessa linha, isso pode não funcionar, porque não permite que você implante o ataque da maneira correta.

Mas talvez o material que você coloca no heap seja basicamente apenas uma tonelada de NOP e, no final, você tenha um código shell. Isso é realmente muito inteligente, porque significa que agora você pode realmente chegar ao lugar certo onde está pulando. Porque se você pular para um desses NOPs , isso acontecerá "boom, boom, boom, boom, boom, boom, boom, boom" e, então, você entrará no código do shell.

Parece que as pessoas sugerem isso, o que você provavelmente vê em nossa equipe. Eles inventam algo assim, e esse é o problema. Portanto, essa é outra maneira de contornar algumas coisas aleatórias, simplesmente tornando robusta a randomização de seus códigos, se isso fizer sentido.

Então, discutimos alguns tipos de aleatoriedade que você pode usar. Existem algumas idéias estúpidas que também surgiram nas pessoas. Portanto, agora você sabe que, quando deseja fazer uma chamada do sistema, por exemplo, usando a função syscall libc , passa basicamente qualquer número único que represente a chamada do sistema que deseja fazer. Talvez a função de garfo seja 7, o sono seja 8 ou algo assim.

Isso significa que, se um invasor puder descobrir o endereço dessa instrução syscall e acessá -lo de alguma forma, ele poderá substituir o número de chamada do sistema que deseja usar diretamente. Você pode imaginar que toda vez que o programa é executado, você cria uma atribuição dinâmica de números de syscall a syscalls válidos, a fim de complicar a captura do invasor.



Existem até algumas sugestões de vanguarda para alterar o hardware, para que o equipamento contenha a chave de criptografia xor , que é usada para funções dinâmicas xor . Imagine que toda vez que você compila um programa, todos os códigos de instruções recebem uma certa chave xor . Essa chave é armazenada no registro do equipamento quando você baixa o programa inicialmente; depois disso, sempre que você executa a instrução, o equipamento executa automaticamente a operação xor com ele antes de continuar com esta instrução. O lado bom dessa abordagem é que agora, mesmo que um invasor possa gerar código shell, ele não reconhecerá essa chave. Portanto, será muito difícil descobrir o que exatamente precisa ser guardado na memória.

Público: mas se ele conseguir o código, também poderá usar o xor para transformar o código novamente em uma instrução.

Professor: sim, esse é o problema canônico, certo. Isso é um pouco semelhante ao que acontece durante os ataques do BROP , quando parecemos aleatoriamente a localização do código, mas o invasor pode "senti-lo" e descobrir o que está acontecendo. Pode-se imaginar que, por exemplo, se um invasor souber alguma sub-sequência de código que ele espera encontrar em um arquivo binário, ele tentará usar a operação xor para esse arquivo para extrair a chave.

Essencialmente, discutimos todos os tipos de ataques de randomização sobre os quais eu queria falar hoje. Antes de prosseguirmos com a programação, vale a pena discutir quais desses métodos de proteção são utilizados na prática. Acontece que o GCC e o Visual Studio incluem a abordagem de canários de pilha por padrão . Esta é uma comunidade muito popular e muito famosa. Se você observar o Linux e o Windows, eles também tiram proveito de coisas como memória não executável e randomização do espaço de endereço. É verdade que o sistema de limites largos não é tão popular entre eles, provavelmente devido ao custo de memória, processador, alarmes falsos etc., sobre os quais já falamos. Então, basicamente, examinamos como as coisas vão impedir o problema de estouro de buffer.

Agora vamos falar sobre ROP , programação orientada a reverso. Hoje eu já contei o que ele representa em termos de randomizar o espaço de endereço e impedir que os dados sejam executados - é leitura, gravação e execução. Na verdade, essas são coisas muito poderosas. Porque a randomização impede a possibilidade de um invasor entender onde estão nossos endereços codificados. E a capacidade de impedir a execução de dados garante que, mesmo se você colocar o código do shell na pilha, um invasor não pode simplesmente pular para executá-lo.

Tudo isso parece bastante progressivo, mas os hackers estão constantemente desenvolvendo métodos de ataque contra essas soluções de defesa progressiva.

Então, qual é a essência da programação reversa?

E se, em vez de apenas criar um novo código durante um ataque, um invasor pudesse combinar os trechos de código existentes e depois combiná-los de maneira anormal? Afinal, sabemos que o programa contém toneladas desse código.



Felizmente ou infelizmente, tudo depende de que lado você está. Se você puder encontrar alguns trechos interessantes de código e combiná-los, poderá obter algo como a linguagem Turing , onde o atacante pode essencialmente fazer o que quiser.

Vejamos um exemplo muito simples que lhe parecerá familiar a princípio, mas que rapidamente se tornará algo louco.

Digamos que temos o seguinte programa. Então, vamos ter algum tipo de função e, o que é conveniente para o atacante, aqui está essa agradável função shell de execução . Portanto, isso é apenas uma chamada para o sistema, ele executará o comando bin / bash e isso terminará. Em seguida, temos um processo de estouro de buffer canônico ou, desculpe, uma função que anunciará a criação de um buffer e, em seguida, usará uma dessas funções não seguras para preencher o buffer com bytes.



Portanto, sabemos que aqui o estouro de buffer ocorre sem problemas. Mas o interessante é que temos essa função shell de execução , mas é difícil obtê-la de maneiras baseadas em estouros de buffer. Como um invasor pode chamar esse comando shell de execução ?

Primeiro, o atacante pode desmontar o programa, iniciar o GDB e descobrir o endereço dessa coisa no arquivo executável. Você provavelmente está familiarizado com esses métodos no trabalho de laboratório. Em seguida, durante um estouro de buffer, um invasor pode pegar esse endereço, colocá-lo no estouro de buffer gerado e verificar se a função retorna ao shell de execução .

Para deixar claro, eu vou desenhar. Portanto, você tem uma pilha assim: na parte inferior, há um buffer excedido, acima dele é um indicador de intervalo salvo, acima dele é o endereço de retorno para prosess_msg . Em baixo, à esquerda, temos um novo ponteiro de pilha que inicia a função, acima dela, um novo ponteiro de quebra, depois o ponteiro de pilha que será usado, e ainda mais alto é o ponteiro de quebra do quadro anterior. Tudo parece bem familiar.



Como eu disse, durante o ataque, o GDB foi usado para descobrir qual é o endereço do shell de execução . Assim, quando o buffer transborda, podemos simplesmente colocar o endereço do shell de execução aqui e à direita. Esta é realmente uma extensão bastante simples do que já sabemos fazer. Essencialmente, isso significa que, se tivermos um comando que inicie o shell e se pudermos desmontar o arquivo binário para descobrir onde está esse endereço, podemos simplesmente colocá-lo nessa matriz de estouro localizada na parte inferior da pilha. É bem simples.

Portanto, esse foi um exemplo extremamente frívolo, porque o programador, por algum motivo maluco, colocou essa função aqui, apresentando ao atacante um verdadeiro presente.
Agora, suponha que, em vez de chamar isso de run_shell , chamemos de run_boring e simplesmente execute o comando / bin / ls . No entanto, não perdemos nada, porque teremos a string char * bash_path no topo , o que nos dirá o caminho para este bin / bash .



Portanto, o mais interessante é que um invasor que deseja executar o sl pode "analisar" o programa e encontrar a localização do run_boring , e isso não é nada divertido. Mas, na verdade, temos uma linha na memória que aponta para o caminho do shell, além disso, sabemos algo mais interessante. Isto é, mesmo que o programa não chame o sistema com o argumento / bin / ls , ele ainda fará algum tipo de chamada.

Portanto, sabemos que o sistema deve estar de alguma forma conectado com este programa - sistema (“/ bin / ls”) . Portanto, podemos usar essas duas operações nulas para realmente associar o sistema a esse argumento char * bash_path . A primeira coisa que fazemos é entrar no GDB e descobrir onde esse sistema (“/ bin / ls”) está localizado na imagem do processo binário. Então, basta acessar o GDB , digitar print_system e obter informações sobre o deslocamento. Isso é bem simples e você pode fazer o mesmo com o bash_path . Ou seja, você simplesmente usa o GDB para descobrir onde essa coisa mora.

Depois de ter feito, você precisa fazer outra coisa. Porque agora realmente precisamos descobrir como invocar o sistema usando o argumento que escolhemos. E a maneira como fazemos isso consiste essencialmente em falsificar o quadro de chamada do sistema. Se você se lembra, um quadro é o que o compilador e o hardware usam para implementar a chamada de pilha.

Queremos organizar na pilha algo como o que descrevi nesta figura. Na verdade, vamos falsificar um sistema que deveria estar na pilha, mas pouco antes de ele realmente executar seu código.

Então, aqui temos o argumento do sistema, esta é a linha que queremos executar. Na parte inferior, temos uma linha na qual o sistema deve retornar quando a linha com o argumento for concluída. O sistema espera que a pilha tenha a mesma aparência antes do início da execução.



Costumávamos assumir que não há argumentos quando você passa a função, mas agora parece um pouco diferente. Só precisamos garantir que o argumento esteja no código de estouro que estamos criando. Só precisamos garantir que esse quadro de chamada falsa esteja nesse array. Assim, nosso trabalho será o seguinte. Lembre-se de que o estouro da pilha vai de baixo para cima.



Primeiro, vamos colocar o endereço do sistema aqui. Além disso, colocaremos um endereço de retorno indesejado . Este é o local onde o sistema retornará após a conclusão. Este endereço será um conjunto aleatório de bytes. Acima dele, colocaremos o endereço bash_path . O que acontece quando o buffer estourar agora?

Depois que o prosess_msg chegar à linha de chegada, ele dirá: "OK, este é o lugar para onde eu deveria voltar"! O código do sistema continua a funcionar, ele se move mais alto e vê o quadro de chamadas falsas que criamos. Para o sistema, nada de impressionante acontecerá, ele dirá: "sim, aqui está, o argumento que quero executar é bin / bash ", ele executa e está pronto - o invasor capturou o shell!

O que fizemos agora? Aproveitamos o conhecimento da convenção de chamada , convenção de chamada , como uma plataforma para criar quadros de pilha falsos ou nomes de quadros falsos, eu diria. Usando esses quadros de chamada falsos, podemos executar qualquer função mencionada e que já esteja definida pelo aplicativo.

A próxima pergunta que devemos fazer é: e se o programa não tiver essa linha char * bash_path ? Noto que essa linha está quase sempre presente no programa. No entanto, suponha que vivamos em um mundo invertido, e ele ainda não está lá. Então, o que podemos fazer para colocar essa linha em um programa?

A primeira coisa que você pode fazer para isso é especificar o endereço correto para bash_path , colocando-o mais alto, aqui neste compartimento de nossa pilha, inserindo três elementos, cada um com 4 bytes de tamanho:

/ 0
/ pat
/ bin



Mas, de qualquer forma, nosso ponteiro vem aqui e - bum! - A coisa está feita. Dessa forma, agora você pode chamar argumentos simplesmente colocando-os no seu código shell. Aterrorizante, não é? E tudo isso é construído antes de um ataque completo do BROP . Porém, antes de apontar um ataque completo do BROP , você precisa entender como simplesmente encadear as coisas que já estão dentro do código. Quando eu tenho esse endereço de retorno despejado aqui, queremos apenas acessar o shell. Mas se você for um invasor, poderá direcionar esse endereço ou endereço de retorno para algo que realmente possa ser usado. E se você fizesse isso, poderia colocar várias funções seguidas em uma linha, vários sinais de uma função em uma linha. Esta é realmente uma opção muito poderosa.

Porque se simplesmente definirmos o endereço de retorno para o salto, depois disso o programa geralmente falha, o que talvez não desejemos. Portanto, vale a pena vincular algumas dessas coisas para fazer coisas mais interessantes com o programa.

Suponha que nosso objetivo seja chamar o sistema um número arbitrário de vezes. Não queremos fazer isso apenas uma vez, faremos isso várias vezes arbitrariamente. Então, como isso pode ser feito?

Para fazer isso, usamos duas informações que já sabemos como obter. Sabemos como obter o endereço do sistema - você só precisa procurar no GDB e encontrá-lo lá. Também sabemos como encontrar o endereço dessa linha, bin / bash . Agora, para iniciar esse ataque usando várias chamadas para o sistema, precisamos usar gadgets. Isso nos aproxima do que está acontecendo no BROP .

Então, o que precisamos agora é encontrar o endereço dessas duas operações de código: pop% eax e ret . O primeiro remove o topo da pilha e o coloca no registro eax , e o segundo o coloca no ponteiro da instrução eip . Isso é o que chamamos de gadget. Parece um pequeno conjunto de instruções de montagem que um invasor pode usar para criar ataques mais ambiciosos.



Esses gadgets são ferramentas padrão usadas pelos hackers para encontrar coisas como arquivos binários. Também é fácil encontrar um desses gadgets, supondo que você tenha uma cópia do binário e não nos incomodamos com a randomização. Essas coisas são muito fáceis de encontrar, bem como o endereço do sistema e assim por diante.

Então, se temos um desses gadgets, por que podemos usá-lo? Claro, fazer o mal! Para fazer isso, você pode fazer o seguinte.

Suponha que alteremos nossa pilha para que fique assim, a exploração, como antes, é direcionada de baixo para cima. A primeira coisa que fazemos é colocar o endereço do sistema aqui e, acima dele, colocamos o endereço do gadget pop / ret . Ainda mais alto, colocamos o endereço do bash_path e repetimos tudo: de cima, colocamos novamente o endereço do sistema, o endereço do gadget pop / ret e o endereço do bash_path .



O que acontecerá aqui agora? Será um pouco complicado, então as notas desta palestra estão disponíveis na Internet e, por enquanto, você pode apenas ouvir o que está acontecendo aqui, mas quando eu entendi isso pela primeira vez, foi como entender que o Papai Noel não existia!

Começaremos do local em que a entrada de entrada está localizada, de volta ao sistema em que a instrução ret removerá o item da pilha usando o comando pop ; agora, a parte superior do ponteiro da pilha está aqui. Portanto, removemos o elemento usando pop , retornamos o procedimento ret , que transfere o controle para o endereço de retorno selecionado na pilha, e esse endereço de retorno é colocado lá com o comando call . Então, fazemos novamente uma chamada para o sistema, e esse processo pode ser repetido várias vezes.



É claro que podemos relacionar essa sequência para executar um número arbitrário de coisas. Essencialmente, o kernel recebe o que é chamado de programação orientada a reverso. Observe que não realizamos nada nesta pilha. Fizemos o que nos permitiu impedir a execução de dados sem destruir nada. Demos um salto inesperado para fazer o que queremos. Na verdade, é muito, muito, muito, inteligente.

E o interessante é que, em um nível alto, identificamos esse novo modelo de computação. , , , . , , . , - . , , . , . . , . , , stack canaries.

, «» , . , , «» ret address saved %ebp , - , «». , ret , , «», , - . stack canaries .

, «». , . , «»?

, , , .
, , , «» , «» «».

, , , «» , , .
, - , «» , . , ? ?
, fork . , fork . , , , fork , , , , «» . , stack canaries .

«»? . , , , «». «» . .



, , – , «». , , 0. , «», . , :

«, «»! , 0. «»! 1 – «», 2 – . , 2- . , , «».



, , , .

«», , , . , , «».

57:10

:

Curso MIT "Segurança de sistemas de computadores". Aula 3: Estouros de Buffer: Explorações e Proteção, Parte 3


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/pt418093/


All Articles