A análise do código binário, ou seja, o código executado diretamente pela máquina, é uma tarefa não trivial. Na maioria dos casos, se for necessário analisar o código binário, ele é restaurado primeiro desmontando e depois descompilando em alguma representação de alto nível, e então eles analisam o que aconteceu.
Aqui, deve-se dizer que o código que foi restaurado, de acordo com a representação do texto, tem pouco em comum com o código que foi originalmente escrito pelo programador e compilado em um arquivo executável. É impossível recuperar exatamente o arquivo binário recebido de linguagens de programação compiladas, como C / C ++, Fortran, pois essa é uma tarefa não-formalizada por algoritmos. No processo de conversão do código fonte que o programador escreveu no programa que a máquina executa, o compilador realiza conversões irreversíveis.
Nos anos 90 do século passado, acreditava-se amplamente que o compilador, como um moedor de carne, mói o programa original, e a tarefa de restaurá-lo é semelhante à tarefa de restaurar um carneiro de uma linguiça.

No entanto, não é tão ruim. No processo de obtenção de salsichas, a ovelha perde sua funcionalidade, enquanto o programa binário a salva. Se a lingüiça resultante pudesse correr e pular, as tarefas seriam semelhantes.

Portanto, uma vez que o programa binário tenha mantido sua funcionalidade, podemos dizer que é possível restaurar o código executável para uma representação de alto nível, de modo que a funcionalidade do programa binário, que não possui uma representação inicial, e o programa cuja representação textual recebemos sejam equivalentes.
Por definição, dois programas são funcionalmente equivalentes se, nos mesmos dados de entrada, completarem ou falharem ao concluir sua execução e, se a execução for concluída, produzirem o mesmo resultado.
A tarefa de desmontar geralmente é resolvida no modo semiautomático, ou seja, o especialista faz a recuperação manual usando ferramentas interativas, por exemplo, o desmontador interativo
IdaPro ,
radare ou outra ferramenta. Além disso, também no modo semiautomático, é realizada a descompilação.
HexRays ,
SmartDecompiler ou outro descompilador, adequado para resolver esta tarefa de descompilação, é usado como uma ferramenta de descompilação para ajudar um especialista.
A restauração da representação textual original do programa a partir do código de bytes pode ser bastante precisa. Para linguagens interpretadas, como Java ou linguagens da família .NET, cuja tradução é realizada em código de bytes, a tarefa de descompilação é resolvida de maneira diferente. Não estamos considerando esse problema neste artigo.
Assim, você pode analisar programas binários através da descompilação. Normalmente, essa análise é realizada para entender o comportamento de um programa para substituí-lo ou modificá-lo.
Da prática de trabalhar com programas herdados
Alguns softwares, escritos há 40 anos na família de idiomas de baixo nível C e Fortran, controlam os equipamentos de produção de petróleo. A falha deste equipamento pode ser crítica para a produção, portanto, mudar o software é altamente indesejável. No entanto, nos últimos anos, os códigos-fonte foram perdidos.
O novo funcionário do departamento de segurança da informação, cujas responsabilidades eram entender como funciona, descobriu que o programa de monitoramento de sensores grava algo no disco com alguma regularidade e que grava e como essas informações podem ser usadas não está claro. Ele também teve a ideia de que o monitoramento da operação do equipamento possa ser exibido em uma tela grande. Para isso, foi necessário entender como o programa funciona, qual e em que formato ele grava no disco, como essas informações podem ser interpretadas.
Para resolver o problema, foi aplicada a tecnologia de descompilação, seguida pela análise do código restaurado. Primeiro desmontamos os componentes do software, um de cada vez, depois localizamos o código responsável pela entrada / saída de informações e gradualmente começamos a nos recuperar desse código, dadas as dependências. Em seguida, a lógica do programa foi restaurada, o que possibilitou responder a todas as perguntas do serviço de segurança em relação ao software analisado.
Se você precisar analisar um programa binário para restaurar a lógica de sua operação, restaure parcial ou completamente a lógica de conversão de dados de entrada em dados de saída, etc., é conveniente fazer isso usando um descompilador.
Além de tais tarefas, na prática há problemas de análise de programas binários para requisitos de segurança da informação. Além disso, o cliente nem sempre entende que essa análise consome muito tempo. Parece, faça a descompilação e execute o código resultante com um analisador estático. Mas, como resultado de uma análise qualitativa, quase nunca é bem-sucedida.
Primeiro, as vulnerabilidades encontradas devem ser capazes não apenas de encontrar, mas também de explicar. Se a vulnerabilidade foi encontrada em um programa em uma linguagem de alto nível, o analista ou a ferramenta de análise de código mostra quais fragmentos do código contêm certas falhas, cuja presença causou a vulnerabilidade. E se não houver código fonte? Como mostrar qual código causou a vulnerabilidade?
O descompilador recupera o código “cheio” de artefatos de recuperação e é inútil mapear a vulnerabilidade revelada a esse código, mas nada está claro. Além disso, o código restaurado é mal estruturado e, portanto, se presta mal a ferramentas de análise de código. Também é difícil explicar a vulnerabilidade em termos de um programa binário, porque a pessoa para quem a explicação é feita deve ser bem versada na representação binária de programas.
Em segundo lugar, uma análise binária de acordo com os requisitos de segurança da informação deve ser realizada com a compreensão do que fazer com o resultado resultante, pois é muito difícil corrigir uma vulnerabilidade em um código binário, mas não há código-fonte.
Apesar de todos os recursos e dificuldades de conduzir uma análise estática de programas binários de acordo com os requisitos de segurança da informação, há muitas situações em que essa análise é necessária. Se, por algum motivo, não houver código-fonte e o programa binário executar uma funcionalidade crítica para os requisitos de segurança da informação, ele deverá ser verificado. Se forem encontradas vulnerabilidades, esse aplicativo deve ser enviado para revisão, se possível, ou um "shell" adicional deve ser feito, o que permitirá controlar o movimento de informações confidenciais.
Quando a vulnerabilidade estava oculta em um arquivo binário
Se o código que o programa executa possui um alto nível de criticidade, mesmo que o código-fonte do programa esteja em um idioma de alto nível, é útil auditar o arquivo binário. Isso ajudará a eliminar os recursos que o compilador pode introduzir, executando transformações otimizadas. Portanto, em setembro de 2017, a transformação de otimização realizada pelo compilador Clang foi amplamente discutida. O resultado foi uma chamada para uma
função que nunca deveria ser chamada.
#include <stdlib.h> typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); }
Como resultado de transformações de otimização, o compilador receberá esse código do assembler. O exemplo foi compilado no Linux X86 com o sinalizador -O2.
.text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9
Há um
comportamento indefinido no código fonte. A função NeverCalled () é chamada devido às transformações de otimização que o compilador executa. Durante o processo de otimização, ele provavelmente executa uma análise de
aliases e, como resultado, a função Do () recebe o endereço da função NeverCalled (). E como o método main () chama a função Do (), que não está definida, que é um comportamento indefinido (comportamento indefinido), o resultado é o seguinte: a função EraseAll () é chamada, que executa o comando rm -rf /.
O exemplo a seguir: como resultado das transformações de otimização do compilador, perdemos a verificação de um ponteiro para NULL antes de retirá-lo da referência.
#include <cstdlib> void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; }
Como a linha 3 desreferencia o ponteiro, o compilador assume que o ponteiro é diferente de zero. Em seguida, a linha 4 foi excluída como resultado da otimização
"remoção de código inacessível" , porque a comparação é considerada redundante e, em seguida, a linha 3 foi excluída pelo compilador como resultado da otimização
" eliminação de código morto". Somente a linha 5. O código do assembler resultante da compilação do gcc 7.3 no Linux x86 com o sinalizador -O2 é mostrado abaixo.
.text .p2align 4,,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret
Os exemplos acima do trabalho de otimização do compilador são o resultado de um comportamento indefinido do UB no código. No entanto, esse é um código bastante normal que a maioria dos programadores consideraria seguro. Hoje, os programadores gastam tempo eliminando comportamentos indefinidos no programa, enquanto há 10 anos eles não prestavam atenção a ele. Como resultado, o código herdado pode conter vulnerabilidades do UB.
Os analisadores de código-fonte estático mais modernos não detectam erros relacionados ao UB. Portanto, se o código executa uma crítica funcional aos requisitos de segurança da informação, é necessário verificar o código-fonte e o código que será executado.