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 3 Então, temos um buffer sobre o qual colocamos o "canário". Acima disso, o valor do
EBP salvo pelo ponteiro do ponto de interrupção
salvo e o endereço de retorno é colocado acima dele. Se você se lembrar, o estouro vai de baixo para cima; portanto, antes de chegar ao endereço de retorno, ele primeiro destruirá o "canário".
Público: por que isso afetará o "canário"?
Professor: porque é assumido que o atacante não sabe "pular" arbitrariamente na memória. Os ataques tradicionais de estouro de memória começam com um hacker examinando o limite de tamanho do buffer, após o qual o estouro começa na linha inferior. Mas você está certo - se um invasor puder entrar diretamente na barra de endereço de retorno, nenhum “canário” nos ajudará. No entanto, com um ataque tradicional de estouro de buffer, tudo deve acontecer exatamente dessa maneira - de baixo para cima.
Assim, a idéia principal de usar um "canário" é que permitimos que uma exploração mal-intencionada transborde o buffer de memória. Temos um código de tempo de execução que, ao retornar de uma função, verifica o "canário" para garantir que ele tenha o valor correto.
Público: Um invasor pode reescrever o endereço de retorno e alterar o "canário"? Como ele pode verificar se foi modificado, mas continua cumprindo sua função?
Professor: sim, talvez. Portanto, você deve ter algum pedaço de código que realmente verifique isso antes que a função retorne. Ou seja, nesse caso, é necessário ter o suporte de um compilador, que realmente expandirá a
convenção de chamada . Portanto, essa parte da sequência de retorno ocorre antes de considerarmos a validade desse valor para garantir que o "canário" não tenha sido destruído. Somente depois disso podemos pensar em outra coisa.
Público: Um invasor não pode saber ou adivinhar o que significa "canário"?
Professor: é exatamente disso que eu vou falar! Qual é o problema com este circuito? E se, por exemplo, colocarmos o valor A em cada programa? Ou um ramo inteiro de 4 valores de A? Obviamente, qualquer hacker pode descobrir o tamanho do buffer, sua capacidade e, assim, determinar a posição do "canário" em qualquer sistema. Portanto, podemos usar diferentes tipos de quantidades que colocamos em nosso "canário" para evitar isso.
Há uma coisa que você pode fazer com o nosso "canário". Será um tipo muito engraçado de "canário", que usa funções do programa C e processa caracteres especiais, o chamado tipo determinístico de "canário".

Imagine que você usou o caractere 0 para o “canário” .O valor binário de zero é o byte zero, o caractere zero em ASCII. Um valor de -1 significa um retorno à posição anterior e assim por diante. Muitas funções param ou alteram a operação quando encontram caracteres ou valores como 0, CR, LF, -1. Imagine que você, como hacker, usa alguma função de gerenciamento de string para subir no buffer, encontrar o caractere 0 no "canário" e o processo é interrompido! Se você usar a função “retorno de carro” -1, que geralmente é usada como terminador de linha, o processo também será interrompido. Então -1 é outro sinal mágico.
Há mais uma coisa que pode ser usada no "canário" - esses são valores aleatórios difíceis de adivinhar para o atacante. O poder do valor aleatório baseia-se em como é difícil para um invasor adivinhar. Por exemplo, se um invasor perceber que há apenas 3 bits de entropia no seu sistema, ele poderá usar um ataque de força bruta. Portanto, as possibilidades de usar números aleatórios para proteger contra ataques são bastante limitadas.
Público: Geralmente, eu leio de outro buffer e escrevo o que li nesse buffer desta pilha. Nessa situação, parece que o valor aleatório do "canário" é inútil, porque leio os dados de outro buffer e sei onde está o "canário". Eu tenho outro buffer que controlo e que nunca verifico. E nesse buffer eu posso colocar muito do que eu quero colocar. Não preciso de um "canário" aleatório, porque posso reescrevê-lo com segurança. Portanto, não vejo como isso realmente funciona - no cenário que você propôs, quando a função para ao ler dados do buffer.
Professor: Entendo sua pergunta - você quer dizer que usamos um "canário" determinístico, mas não usamos uma das funções da biblioteca padrão que pode ser "enganada" por nossos personagens 0, CR, LF, -1. Então sim, na situação que você descreveu, um "canário" não é necessário.
A idéia é que você possa preencher esse buffer com bytes de qualquer lugar, mas qualquer coisa que permita adivinhar esses valores ou obtê-los aleatoriamente levará à falha.
Público: É possível usar algo como o número de segundos ou milissegundos como números aleatórios e usá-los em um "canário"?
Professor: As chamadas de dados não contêm tantos acidentes quanto você pensa. Como o programa possui logs ou uma função que você pode chamar para descobrir quando o programa foi baixado e outras coisas semelhantes. Mas, em geral, você está certo - na prática, se você pode usar um dispositivo de hardware, geralmente de baixo nível, com melhores tempos de sistema, esse tipo de abordagem pode funcionar.
Público: mesmo que consigamos visualizar os logs sobre o início do estouro de buffer, ainda é importante a que horas recusamos a solicitação. E se não conseguirmos controlar o tempo que a solicitação de um computador ao servidor leva, é duvidoso que o tempo exato possa ser determinado de maneira determinística.
Professor: muito bem, eu já disse que o mal está nos detalhes, esse é exatamente o caso. Em outras palavras, se você tiver alguma maneira de, por exemplo, determinar o tipo de canal de temporização, poderá descobrir que a quantidade de entropia, ou o número de aleatoriedade, preenche não um carimbo de data / hora inteiro, mas muito menos. Portanto, um invasor pode determinar a hora e o minuto em que você fez isso, mas não um segundo.
Público: para constar, tentar reduzir sua própria aleatoriedade é uma má idéia?
Professor: absolutamente certo!
Público: ou seja, normalmente, apenas precisamos usar tudo o que nossos sistemas suportam, certo?
Professor: sim, é verdade. É como a invenção de nosso próprio sistema de criptografia, que é outra coisa popular que nossos graduados às vezes querem fazer. Mas nós não somos a NSA, não somos matemáticos, então isso geralmente falha. Então você está absolutamente certo sobre isso.
Mas mesmo se você usar a aleatoriedade do sistema, ainda poderá obter menos bits de entropia do que o esperado. Deixe-me dar um exemplo de randomização de fase de endereços. É nesse princípio que a abordagem dos
canários de pilha funciona . Como estamos envolvidos na segurança de computadores, você provavelmente está se perguntando em que casos os "canários" não conseguem lidar com a tarefa deles e se existem maneiras de falhar no "canário".
Uma delas é um ataque reescrevendo ponteiros de função. Porque se um golpe é atingido no ponteiro de função, o "canário" não pode fazer nada.
Suponha que você tenha um código no formato
int * ptr ... .. , o ponteiro inicial, não importa como, então você tem o buffer bu
buf [128] , a função
gets (buf) e, no fundo, um ponteiro ao qual é atribuído algum valor :
* ptr = 5 .
Observo que não tentamos atacar o endereço de retorno da função que contém esse código. Como você pode ver, quando o buffer estourar, o endereço do ponteiro localizado acima dele será danificado. Se um invasor pode danificar esse ponteiro, ele pode atribuir 5 a um dos endereços que ele controla. Todos podem ver que o "canário" não vai ajudar aqui? Porque não atacamos o caminho ao longo do qual a função retorna.
Público: o ponteiro pode estar localizado abaixo do buffer?
Professor: pode, mas a ordem das variáveis específicas depende de muitas coisas diferentes, da maneira como o compilador organiza o conteúdo, do tamanho da coluna de hardware e assim por diante. Mas você está certo, se o estouro do buffer subir e o ponteiro estiver localizado abaixo do buffer, ele não poderá danificá-lo.
Público-alvo: por que você não pode associar o "canário" à função "canário", como fez com o endereço de retorno?
Professor: este é um momento interessante! Você pode fazer essas coisas. De fato, você pode tentar imaginar um compilador que, sempre que tiver um ponteiro, ele sempre tenta adicionar um complemento para algumas coisas. No entanto, verificar todas essas coisas será muito caro. Porque toda vez que você deseja usar qualquer ponteiro ou chamar qualquer função, você deve ter um código que verifique se esse "canário" está correto. Basicamente, você poderia fazer algo semelhante, mas isso faz sentido? Vemos que os "canários" não ajudam nessa situação.
E mais uma coisa que discutimos anteriormente é que, se o atacante puder adivinhar a aleatoriedade, então, em princípio, os "canários" aleatórios não funcionarão. A criação de recursos de segurança com base na aleatoriedade é um tópico separado e muito complexo, por isso não entraremos em detalhes.
Público-alvo: o canary contém menos bits que um endereço de retorno? Porque, caso contrário, você não conseguia se lembrar desse endereço e verificar se ele mudou?
Professor: vamos ver. Você está falando sobre esse esquema quando o "canário" está localizado acima do buffer e quer dizer que o sistema não pode ser seguro se for impossível olhar para o endereço de retorno e verificar se ele foi alterado.

Sim e não Observe que, se ocorrer um ataque de estouro de buffer, qualquer coisa acima dele será sobrescrito; portanto, isso ainda poderá causar problemas. Mas, basicamente, se essas coisas são imutáveis de certa forma, você pode fazer algo assim. Mas o problema é que, em muitos casos, manipular o endereço de retorno é algo bastante complicado. Porque você pode imaginar que uma função especial pode ser chamada de lugares diferentes, e assim por diante. Nesse caso, corremos um pouco à frente e, se houver tempo no final da palestra, voltaremos a isso.
São situações em que um "canário" pode falhar. Existem outros locais onde a falha é possível, por exemplo, ao atacar as funções
malloc e
free . A função malloc aloca um bloco de memória de um determinado tamanho em bytes e retorna um ponteiro para o início do bloco. O conteúdo do bloco de memória alocado não é inicializado, permanece com valores indefinidos. E a função
livre libera memória previamente alocada dinamicamente.
Este é um ataque único no estilo de C. Vamos ver o que acontece aqui. Imagine que você tem dois ponteiros aqui, p e q, para os quais usamos
malloc para alocar 1,024 bytes de memória para cada um desses ponteiros. Suponha que façamos a função
strcpy para p com algum tipo de erro de buffer controlado por um invasor. É aqui que o estouro ocorre. E então executamos o comando
free q e
free p . Este é um código bastante simples, certo?

Temos dois ponteiros para os quais alocamos memória, usamos um deles para uma determinada função, ocorre um estouro de buffer e liberamos a memória dos dois ponteiros.
Suponha que as linhas de memória de peq estejam localizadas próximas uma da outra no espaço da memória. Nesse caso, coisas ruins podem acontecer, certo? Como a função
strcpy é usada para copiar o conteúdo de
str2 para
str1 .
Str2 deve ser um ponteiro para uma string que termina com zero e
strcpy retorna um ponteiro para
str1 . Se as linhas
str1 e
str2 se sobrepuserem, o comportamento da função
strcpy será indefinido.
Portanto, a função
strycpy que processa a memória
p pode ao mesmo tempo afetar a memória alocada para
q . E isso pode causar problemas.
É possível que você tenha feito algo assim em seu próprio código inadvertidamente quando usou algum tipo de ponteiro estranho. E tudo parece funcionar, mas quando você precisa chamar a função
livre , ocorre um incômodo. E um invasor pode tirar proveito disso, explicarei por que isso acontece.
Imagine que, dentro da implementação das funções
free e
malloc , o bloco realçado se parece com isso.
Vamos supor que no topo do bloco haja dados visíveis do aplicativo e abaixo tenhamos o tamanho da variável. Esse tamanho não é o que o aplicativo vê diretamente, mas um tipo de "contabilidade" conduzida por
free ou
malloc , para que você saiba o tamanho do buffer de memória alocado. Um bloco livre está localizado próximo ao bloco realçado. Suponha que um bloco livre tenha alguns metadados parecidos com este: temos o tamanho do bloco acima, há espaço livre abaixo dele, o ponteiro de trás e o ponteiro de avanço abaixo dele. E na parte inferior do bloco, o tamanho é mostrado novamente.

Por que temos 2 ponteiros aqui? Como o sistema de alocação de memória, nesse caso, usa uma lista duplamente vinculada para rastrear como os blocos livres estão relacionados entre si. Portanto, ao selecionar um bloco livre, você o exclui desta lista duplamente vinculada. E então, quando você o liberar, fará alguma aritmética para o ponteiro e colocará essas coisas em ordem. Depois disso, você o adiciona a esta lista vinculada, certo?
Sempre que você ouvir sobre a aritmética dos ponteiros, pense que este é o seu "canário". Porque haverá muitos problemas. Deixe-me lembrá-lo de que tivemos um buffer overflow
p . Se assumirmos que
p e
q estão próximos um do outro, ou muito próximos no espaço da memória, pode acontecer que esse estouro de buffer possa sobrescrever alguns dados de tamanho do ponteiro alocado
q - essa é a parte inferior do nosso bloco alocado. Se você continuar seguindo meu pensamento desde o início, sua imaginação lhe dirá onde tudo começa a dar errado. De fato, em essência, o que finalmente acontece com essas operações é
q livre e
p livre - eles examinam esses metadados no bloco selecionado para fazer todas as manipulações necessárias com o ponteiro.

Ou seja, em algum momento da execução, as funções
livres receberão um certo ponteiro com base no valor do tamanho:
p = get.free.block (size) , e o tamanho é o que o invasor controla, porque realizou um estouro de buffer corretamente ?
Ele fez vários cálculos aritméticos, olhou para a função
back e os ponteiros deste bloco e agora fará algo como atualizar os ponteiros “back” e “forward” - estas são as duas últimas linhas.

Mas, na realidade, isso não deve incomodá-lo. Este é apenas um exemplo do código que ocorre neste caso. Mas o fato é que, devido ao tamanho reescrito pelo hacker, ele agora controla esse ponteiro, que passa pela função
free . E por causa disso, os dois estados aqui na linha inferior são, na verdade, atualizações de ponteiros. E como o invasor conseguiu controlar esse
p , ele realmente controla esses dois indicadores. É neste local que um ataque pode ocorrer.
Portanto, ao correr
livre e tentar fazer algo como combinar esses dois blocos, você tem uma lista duplamente vinculada. Porque se você tiver dois blocos que colidem um com o outro e os dois estiverem livres, deseje combiná-los em um grande bloco.
Mas se controlarmos o tamanho, significa que controlaremos todo o processo a partir das quatro linhas acima. Isso significa que, se entendermos como o estouro funciona, poderemos gravar dados na memória da maneira que escolhermos. Como eu disse, essas coisas geralmente acontecem com seu próprio código, se você não é inteligente com um ponteiro. Quando você comete algum erro duplo livre como
free q e
free p ou outra coisa, sua função falha. Como você mexeu com os metadados que vivem em cada um desses blocos selecionados e, em algum momento, esse cálculo indicará algum tipo de valor "lixo", após o qual você estará "morto". Mas se você é um invasor, pode escolher esse valor e usá-lo em seu proveito.
Vamos para outra abordagem para evitar ataques de estouro de buffer. Essa abordagem é verificar os limites. O objetivo da verificação de limites é garantir que, ao usar um ponteiro específico, ele se refira apenas ao que é um objeto de memória. E esse ponteiro está dentro dos limites permitidos desse objeto de memória. Essa é a principal idéia da verificação. — . , C, . , : , , ?
, – . 1024 , :
char [1024] ,
char *y = & [108].
? ? Difícil dizer. , , . , , - .
- , , , . . , , , . , , . , , .
, ,
struct union . , . :
integer ,
struct ,
int .
,
union , . ,
integer ,
struct , .
, , - :
int p: & (u,s,k) , : u, s, k.

, , , , . , ,
union integer ,
struct . , , , . .
p' ,
p ,
p' , .

, , . , ,
union . , - -
union , , , . , , X. , , , , . , , . .
, . ,
p p' , . .
? Electric fencing – . , , , , .

, - , . , , . , . , , , .
- C C++, , , . - , , - . , . , «» — , , , . , , .
, guard page – ! , .
59:00
:
MIT « ». 2: « », 2A versão completa do curso está disponível
aqui .
, . ? ? ,
30% entry-level , : VPS (KVM) E5-2650 v4 (6 Cores) 10GB DDR4 240GB SSD 1Gbps $20 ? ( RAID1 RAID10, 24 40GB DDR4).
Dell R730xd 2 ? 2 Intel Dodeca-Core Xeon E5-2650v4 128GB DDR4 6x480GB SSD 1Gbps 100 $249 ! . c Dell R730xd 5-2650 v4 9000 ?