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 3Palestra 2: “Controle de ataques de hackers”
Parte 1 /
Parte 2 /
Parte 3Aula 3: “Estouros de Buffer: Explorações e Proteção”
Parte 1 /
Parte 2 /
Parte 3 Bem-vindo à palestra sobre explorações para estouros de buffer. Hoje encerraremos a discussão sobre os
limites de
Baggy e passaremos para outros métodos de proteção contra estouro de buffer.

A seguir, falaremos sobre os materiais impressos da palestra de hoje, dedicados à
Programação Orientada a Retorno Cego (BROP) - programação cega de orientação reversa. Essa é uma técnica de exploração que pode ser realizada mesmo que o atacante não possua o binário de destino. Essas explorações visam destruir "canários" nas pilhas de sistemas de 64 bits. Então, se você era como eu quando li esses materiais pela primeira vez, deveria ter sentido vontade de assistir ao filme de Christopher Nolan. Foi apenas uma explosão cerebral!
Vamos considerar como esses gadgets funcionam corretamente. Portanto, espero que até o final da palestra você possa entender todas essas altas tecnologias descritas nos materiais da palestra. Mas primeiro, como eu disse, encerraremos a discussão sobre os
limites de
Baggy . Considere um exemplo muito simples.
Suponha que vamos atribuir um ponteiro
p e alocar um tamanho de 44 bytes para ele. Suponha também que o tamanho do slot seja 16 bytes.
O que acontece quando atribuímos a função
malloc ? Você já sabe que, nesse caso, o sistema
Baggy bounds tentará complementar essa distribuição com um logaritmo
2n . Portanto, para o nosso ponteiro de 44 bytes, 64 bytes de memória serão alocados. Mas como o tamanho do slot é de 16 bytes, criaremos 64/16 = 4 tabelas de limites de 16 bytes cada. Cada uma dessas entradas será colocada no log de distribuição de tamanho.
Em seguida, atribua outro ponteiro
char * q = p + 60 . Vemos que esse valor está fora dos limites, porque o tamanho de
p é 44 bytes, e aqui é 60 bytes. Mas
Baggy bounds funciona para que, neste caso, nada de
ruim aconteça, embora o programador não deva ter feito isso.
Agora vamos assumir que a próxima coisa que fazemos é atribuir outro ponteiro, que será igual a
char * r = q + 16 . Agora, isso realmente causará um erro, porque o tamanho do deslocamento será 60 + 16 = 76, que é 12 bytes maior que os 4 slots (4x16 = 64 bytes)
alocados pelo sistema
Baggy . E esse excesso é realmente mais da metade do slot.

Se você se lembrar, nesse caso, o sistema
Baggy bounds responderá imediatamente a um erro crítico de sincronização, o que causará a falha do programa e o interromperá.
Então, vamos imaginar que temos apenas duas linhas:
char * p = malloc (44)
char * q = p + 60E não há terceira linha com o código. Em vez disso, faremos o seguinte:
char * s = q + 8Nesse caso, o ponteiro terá um valor de 60 + 8 = 68 bits, que será 4 bytes a mais do que os limites atribuídos pelos
limites de Baggy . De fato, isso não causará um erro crítico, embora o valor vá além dos limites. O que fizemos aqui é definir um bit de ordem superior para o ponteiro. Portanto, se alguém tentar desdiferenciá-lo posteriormente, isso levará a um erro crítico neste momento.

E a última coisa que faremos é atribuir outro ponteiro:
char * t = s - 32De fato, fizemos isso - retornamos o ponteiro para a borda. Portanto, se inicialmente foi além, agora retornamos ao volume alocado originalmente que criamos para o ponteiro. Portanto, agora
t não terá um bit de ordem superior em sua composição e pode ser facilmente desreferenciado.
Público: como o programa sabe que
r tem um excesso maior que a metade da pilha?
Professor Mickens: observe que quando criamos
r , obtemos um código de ferramenta que funcionará em todas essas operações com ponteiros. Então, podemos dizer onde
q estará localizado e sabemos que ele está dentro dos
limites de Baggy . Portanto, quando realizamos esta operação
q + 16 , as ferramentas
Baggy bounds sabem de onde esse valor inicial veio. E então, se ocorrer um deslocamento desse tamanho original, os
limites Baggy determinarão facilmente que o deslocamento é maior que ½ do tamanho do slot.
Em princípio, quando você executa operações com ponteiros, deve verificar se eles excederam o tamanho alocado ou não. Em algum momento, você tem um ponteiro localizado dentro dos limites dos limites de
Baggy e, em seguida, algo acontece que o faz ir além dos limites. Então, quando isso acontecer, descobriremos que algum tipo de crochê “saiu” do nosso código.
Espero que isso seja compreensível. Foi uma visão geral muito breve da lição de casa, mas espero que você possa entendê-la facilmente.
Portanto, temos um ponteiro que se parece com isso:
char * p = malloc (256) , adicionamos o ponteiro
char * q = p + 256 , após o qual tentaremos desreferenciar esse ponteiro.
Então o que vai acontecer? Bem, observe que 256 é uma sequência
2n , portanto estará dentro dos
limites de Baggy . Portanto, quando adicionamos outros 256 bits, isso significa que fazemos outra passagem para o final das bordas dos
limites Baggy . Como no exemplo anterior, essa linha é boa o suficiente, mas leva ao fato de que um bit de ordem superior será definido para
q . Portanto, quando tentamos desreferenciar isso, tudo vai explodir e precisar chamar nosso agente de seguros. Isso está claro?

A partir desses 2 exemplos, você pode entender como o sistema
Baggy bounds funciona . Como mencionei na última palestra, você realmente não precisa instrumentar cada operação de ponteiro se puder usar a análise de código estático para descobrir que um conjunto específico de operações de ponteiro é seguro. Adiarei uma discussão mais aprofundada de algumas análises estáticas, mas basta dizer que você nem sempre precisa executar essas ações matemáticas, já verificamos isso antes.
Outra questão mencionada no Piazza: como garantir a compatibilidade dos
limites Baggy com as bibliotecas anteriores que não são de ferramentas. A idéia é que, quando os
limites Baggy inicializam tabelas de borda, eles estabelecem que todos os registros devem estar dentro de 31 bits. Portanto, quando lemos a tabela de limites, cada registro nela representa um valor no formato
2n + 31 . Assim, inicializando os limites iniciais do tamanho 31 bits, assumimos que cada ponteiro terá o tamanho máximo possível de
2n + 31 . Deixe-me dar um exemplo muito simples que deixará isso claro.
Suponha que tenhamos um espaço de memória que usamos para um monte. Esse espaço de memória é composto de dois componentes. No topo, temos um heap que foi alocado usando código que não é de ferramenta, e abaixo é um heap que foi alocado com código de ferramenta. Então, o que os
limites Baggy farão? Como você se lembra, este sistema tem o conceito de um slot, cujo tamanho é 16 bits. Portanto, a tabela de limites consistirá em 2 seções, iniciadas a partir de 31 bits.
No entanto, ao executar o código da ferramenta, ele realmente usará o algoritmo
Baggy bounds para definir os valores apropriados para esta linha da tabela.

Quando um ponteiro sai do topo do espaço da memória, ele é sempre definido no máximo possível
2n + 31 limites. Isso significa que os
limites Baggy nunca consideram que uma operação de ponteiro que "veio" de uma biblioteca não-ferramenta pode ir além dos limites.
A idéia é que, no código da ferramenta, sempre faremos essas comparações para ponteiros, mas se definirmos os limites de escrita do ponteiro para um código não-ferramenta do formato
2n + 31 , nunca teremos um erro de desreferência. Ou seja, temos uma boa interação entre as
entradas de código de
Baggy e os registros não instrumentais das bibliotecas anteriores.
Isso significa que temos esse sistema, o que é bom, porque não trava o programa ao usar bibliotecas que não são de ferramentas, mas tem um problema. O problema é que nunca podemos determinar os limites dos ponteiros que são gerados por código que não é de ferramenta. Como nunca definiremos um bit de ordem alta quando, por exemplo, esse ponteiro tiver muito ou pouco espaço. Portanto, não podemos realmente garantir a segurança da memória para operações que ocorrem ao usar código não instrumental. Você também não pode determinar quando passamos um ponteiro que ultrapassou os limites de tamanho, do código instrumental para o código não instrumental. Nesse caso, algo inimaginável pode acontecer. Se você tiver um ponteiro desse tipo retirado do código da ferramenta, ele terá um bit de ordem superior definido como 1. Portanto, parece que ele tem dimensões gigantescas.
Sabemos que, se apenas colocarmos esse código no código da ferramenta, podemos limpar esse sinalizador em alguns pontos quando ele retornar às bordas. Mas se passarmos esse endereço enorme para o código não instrumental, ele poderá fazer algo inimaginável. Pode até retornar esse ponteiro aos limites, mas nunca teremos a oportunidade de limpar esse bit de alta ordem. Portanto, ainda podemos ter problemas, mesmo ao usar o circuito mostrado aqui.
Público: se tivermos código de ferramenta para alocar memória, ele usa a mesma função
malloc que o código de atributo?
Professor: Essa é uma pergunta um pouco delicada. Se considerarmos o caso aqui, isso é estritamente observado, pois temos duas áreas de memória, cada uma das quais obedece às regras estabelecidas para ele. Mas, em princípio, isso dependerá do código que usa a linguagem de programação selecionada. Imagine que, em C ++, por exemplo, você pode atribuir seu próprio qualificador. Portanto, depende de certos detalhes do código.
Público: como o qualificador pode verificar se o limite está definido para 31 bits ou não?
Professor: nos níveis mais baixos, os algoritmos de distribuição funcionam para que quando você chama um sistema desconhecido, o ponteiro se move para cima. Portanto, se você tiver vários alocadores, todos tentarão alocar memória, cada um com sua própria parte da memória, que eles reservam para si mesmos, basicamente, corretamente. Portanto, na vida real, pode ser mais fragmentado do que em um nível alto.
Portanto, tudo o que examinamos acima estava relacionado à operação dos
limites Baggy em sistemas de 32 bits. Considere o que acontece ao usar sistemas de 64 bits. Nesses sistemas, você pode realmente se livrar da tabela de limites, porque podemos armazenar algumas informações sobre os limites no próprio ponteiro.
Considere como um ponteiro comum se parece nos limites de Baggy. É composto por 3 partes. 21 bits são alocados para a primeira parte zero, outros 5 bits são alocados para o tamanho, esse é o tamanho principal do log e outros 38 são os bits do endereço usual.

A razão pela qual isso não limita enormemente o tamanho do endereço do programa que você está usando é que a maioria dos bits de ordem superior do sistema operacional e / ou equipamento localizados nas duas primeiras partes do ponteiro não permite que o aplicativo seja usado por vários motivos. Portanto, como se viu, não estamos reduzindo muito o número de aplicativos usados no sistema. É assim que um ponteiro comum se parece.
O que acontece quando temos apenas um desses indicadores? Bem, em um sistema de 32 bits, tudo o que podemos fazer é apenas definir um bit de ordem superior e esperar que isso nunca tenha mais da metade do tamanho do slot. Mas agora que temos todo esse espaço de endereço extra, é possível colocar o deslocamento fora das bordas OOB (fora dos limites) diretamente nesse ponteiro. Assim, podemos fazer algo como o mostrado na figura, dividindo o ponteiro em 4 partes e redistribuindo seu tamanho.
Assim, podemos obter 13 bits para os limites de deslocamento, ou seja, anote o quão longe esse ponteiro OOB estará de onde deveria estar. Então, novamente, você pode definir o tamanho real do objeto indicado aqui como 5 e o restante da parte zero, que agora será 21-13 = 8 bits. E então segue nosso endereço parte de 38 bits. Neste exemplo, você vê as vantagens de usar sistemas de 64 bits.

Observe que aqui temos o tamanho usual para um ponteiro regular, em ambos os casos esse tamanho é de 64 bits e sua descrição é elementar. E isso é bom, porque ao usar ponteiros "grossos", precisaríamos de muitas palavras para descrevê-los.
Também observo que o código que não é de ferramenta pode ser facilmente aplicado aqui, porque funciona e usa o mesmo tamanho que ponteiros comuns. Podemos colocar essas coisas em uma
estrutura , por exemplo, e o tamanho dessa
estrutura permanecerá inalterado. Portanto, isso é muito bom quando temos a oportunidade de trabalhar em um mundo de 64 bits.
Público: por que, no segundo caso, o deslocamento está localizado na frente do tamanho, e não como no caso anterior, e o que acontecerá se o tamanho do deslocamento for grande?
Professor: Eu acho que em alguns casos temos certos problemas limitantes nos quais teremos que trabalhar. Por exemplo, um problema ocorrerá se houver mais bits. Mas, basicamente, acho que não há uma razão para você não conseguir ler algumas dessas coisas. A menos que determinadas condições estritas, nas quais não penso agora, devam ter estipulado o tamanho da peça zero, caso contrário, poderá haver problemas com o hardware.
Portanto, você ainda pode iniciar um estouro de buffer no sistema
Baggy bounds , pois a aplicação das abordagens acima não resolve todos os problemas, certo? Outro problema que você pode encontrar se tiver um código não instrumental, porque não conseguiremos detectar nenhum problema no código não instrumental. Você também pode encontrar vulnerabilidades de memória que surgem de um sistema de alocação de memória dinâmica. Se você se lembra, em uma palestra anterior, vimos esse estranho ponteiro para
libertar malloc , e os
limites de Baggy não podiam impedir a ocorrência de tais coisas.
Também discutimos na última palestra o fato de que os ponteiros de código não têm limites que seriam associados a eles. Suponha que tenhamos uma estrutura na qual o buffer de memória esteja localizado na parte inferior, e o ponteiro na parte superior, e o buffer exceda. Assumimos que o excesso de buffer ainda esteja dentro dos
limites de Baggy . Então você deve redefinir esse ponteiro de função. Caso contrário, se tentarmos usá-lo, ele poderá ser enviado para código malicioso para atacar uma parte controlada da memória. E, neste caso, os limites não nos ajudarão, porque não temos bordas associadas, associadas a esses ponteiros de função.
Então, qual é o preço do uso de
limites Baggy ? De fato, temos apenas 4 componentes desse preço.

O primeiro é o espaço. Porque se você usar um ponteiro "grosso", é óbvio que você deve aumentar os ponteiros. Se você estiver usando o sistema
Baggy bounds que acabamos de falar, salve uma tabela de borda. E essa tabela possui um tamanho de slot que permite controlar o tamanho dessa tabela até você esgotar as possibilidades de memória alocada para ela.
Além disso, você também aumentou a carga na CPU, que é forçada a executar todas essas operações instrumentais com ponteiros. Como para cada um ou quase todos os ponteiros, é necessário verificar os limites usando os mesmos modos de operação, o que atrasará a execução do seu programa.
Há também um problema com alarmes falsos. Já discutimos o que pode acontecer quando um programa gera ponteiros que vão além dos limites, mas nunca tenta desreferê-los. A rigor, isso não é um problema.
Os limites folgados sinalizarão esses sinalizadores "fora dos
limites " se ultrapassarem 1/2 do tamanho do slot, pelo menos na solução de 32 bits.
O que você verá na maioria das ferramentas de segurança é que os alarmes falsos reduzem a probabilidade de as pessoas usarem essas ferramentas. Porque, na prática, todos esperamos que nos preocupemos com a segurança, mas o que realmente excita as pessoas? Eles querem carregar suas fotos estúpidas no Facebook, querem acelerar o processo de upload e assim por diante. Portanto, se você realmente deseja que suas ferramentas de segurança estejam com demanda, elas devem ter zero alarmes falsos. Tentar capturar todas as vulnerabilidades geralmente leva a alarmes falsos, o que incomodará desenvolvedores ou usuários.
Também leva a despesas improdutivas que você precisa de suporte do compilador. Porque você precisa adicionar todas as ferramentas ao sistema, ignorando a verificação do ponteiro e assim por diante.
Portanto, para usar o sistema
Baggy bounds , precisamos pagar um preço, que consiste em uso excessivo de espaço, aumento na carga da CPU, alarmes falsos e a necessidade de usar um compilador.
Isso
conclui a discussão sobre os
limites de
Baggy .
Agora podemos pensar em duas outras estratégias de mitigação de estouro de buffer. De fato, eles são muito mais fáceis de explicar e entender.
Uma dessas abordagens é chamada
de memória não executável . Sua idéia principal é que o hardware de troca indicará 3 bits de
R ,
W e
X - leia, escreva e execute - para cada página que você tiver na memória. , , . 2 , , , , .
, . , , , , - . , . «
W X » , , , , , . , , . . , , . ?
- , . , . , , .
, , , , . , , . .
, . –
just-in-time , .
-, JavaScript . JavaScript, , - - «» , - «» , x86 . , .
. , ,
just-in-time W ,
X . , , .
— . , , , .

, , . GDB, , . , . , . .
, . , , , , , .
: , , — . , , , , , , , - . , , .
, , , GDB , , , , . .
, , , , . - , - , . , . , , .
, ? , . , , . , , , , - , .
27:10
:
Curso MIT "Segurança de sistemas de computadores". 3: « : », 2.
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?