Memória mal gerenciada I

David García    10 junio, 2020
Memória mal gerenciada I

Se tivéssemos que escolher uma vulnerabilidade particularmente prejudicial, provavelmente seria a execução arbitrária de código e, mais ainda, se ela puder ser explorada remotamente. As consequências podem ser fatais, como já vimos em muitos desses episódios (Conficker para analistas de malware , MS08-067 e EternalBlue para pentesters , WannaCry para todos, etc.).

La ejecución de código arbitrario ha sido y sigue siendo uno de los errores de programación que más pérdidas y reparaciones ha causado a lo largo y ancho de la historia del silicio. Por cierto, lo llamamos arbitrario porque, en realidad, la CPU ya está ejecutando código; la gracia de lo arbitrario es que se deja al arbitrio del atacante decidir qué código se ejecuta, puesto que es quien toma el control del proceso. De eso trata una explotación de este tipo: desviar la ejecución normal y determinada de un proceso a un agente extraño introducido en aquel de forma arbitraria por un atacante a través de un exploit.

A execução de códigos arbitrários em sido e continua sendo um dos erros de programação que causaram mais perdas e reparos ao longo da história do silício. A propósito, chamamos isso de arbitrário porque, na verdade, a CPU já está executando o código; a graça do arbitrário é que fica no arbítrio do invasor ecidir qual código será executado, pois é ele quem assume o controle do processo. É disso que trata uma exploração (exploit) desse tipo: desviar a execução normal e determinada de um processo para um agente estranho arbitrariamente introduzido nele por um atacante por meio de uma exploração (exploit).

Como exatamente isso acontece?

Existem muitas maneiras de executar código (daqui em diante entenderemos arbitrário). A definição, a propósito, não se limita aos executáveis ​​nativos. Um cross-site scripting não deixa de ser uma injeção de código estranho que, novamente, desvia a execução de um script para o snippet de código injetado.

Um dos fatores na execução do código no nível nativo é o derivado das falhas no gerenciamento de memória. Examinaremos os tipos mais comuns de erros, focando em como eles ocorrem e como os sistemas operacionais e as linguagens de programação estão evoluindo para reduzir o efeito que essas falhas têm quando são exploradas maliciosamente.

Voltando ao tempo, nem todos os idiomas tinham um gerenciamento manual do uso que eles faziam da memória. De fato, John McCarthy, um dos pais da Inteligência Artificial e criador do LISP, inventou o conceito de automatic garbage collection (memória liberada durante a execução de um processo) nos anos sessenta.

No entanto, apesar do fato de os recoletores de memória facilitarem a vida dos programadores (abstraindo-os do gerenciamento manual), era uma sobrecarga no consumo de recursos que alguns sistemas não podiam se permitir. Para se ter uma ideia, seria como se o rastreamento de voos de uma torre de controle do aeroporto em tempo real parasse por alguns segundos para eliminar a memória liberada.

É por isso que linguagens como C ou C ++ mantêm um peso enorme ao programar aplicativos de sistema. São linguagens sem coletor de lixo (embora seja possível utilizá-las por meio de bibliotecas) nas quais o peso do gerenciamento de memória recai inteiramente sobre o programador. E, é claro, quando você deixa o trabalho de uma máquina nas mãos de um ser humano … Pelo contrário, liberar os recursos que um coletor consome supõe um aumento enorme no desempenho e na resposta do programa e isso se traduz em um custo menor em hardware.

É tão difícil gerenciar a memória manualmente?

Obviamente, é uma pergunta muito aberta e a resposta dependerá do nosso nível de familiaridade com esse tipo de programação e as facilidades que a linguagem nos fornece, adicionadas ao uso de ferramentas e tecnologias externas implementadas no compilador.

Vamos dar um exemplo: suponha que desejemos associar uma string de texto a uma variável. Uma operação que é trivial em linguagens com gerenciamento automático de memória, por exemplo, em Python (é um código de exemplo, não vamos nos preocupar em corrigi-lo):

Bem, isso na linguagem C tem algumas adições interessantes. Primeiro de tudo, não sabemos o comprimento da string. Essa quantidade não é «padrão» com a string, precisamos encontrá-la ou adicioná-la como um parâmetro para a função. Segundo, como não temos seu tamanho, também não sabemos que memória precisaremos para armazená-lo e, terceiro: quem é o responsável por avisar quando não precisamos mais dessa memória?

Vamos ver um trecho de código (existem várias maneiras de implementar isso, mais seguro e melhor, mas isso servirá para ilustrar o que queremos dizer, por exemplo, usando strdup, «% ms» etc.):

Como vemos, nem começamos a manipular a cadeia quando já escrevemos código para detectar o fim de uma cadeia, reservar memória, monitorar os limites da matriz na pilha etc.

No entanto, o importante é examinar a linha 28, essa função «free», usada para dizer ao sistema para liberar a parte da memória que tínhamos reservado na função «ler». Aqui a situação é clara: não usamos mais essa memória e a devolvemos.

Em um código de exemplo, é fácil usar a memória, mas e se continuarmos usando essa memória reservada 200 linhas de código posteriormente? E se tivermos que passar esse ponteiro por várias funções? Como é claro quem está encarregado da memória, a função chamada ou quem chama essa função?

Nas entradas a seguir, veremos alguns cenários que se tornam vulnerabilidades devido a esse tipo de supervisão: liberação double free, uso de memória não inicializada, vazamentos de memória (memory leak) ou fuga de memória e ponteiros pendentes ou ponteiros soltos.


David García
@dgn1729

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *