No momento, existem duas abordagens principais para a busca de vulnerabilidades em aplicativos - análise estática e dinâmica. Ambas as abordagens têm seus prós e contras. O mercado chega à conclusão de que ambas as abordagens devem ser usadas - elas resolvem problemas ligeiramente diferentes com resultados diferentes. No entanto, em alguns casos, o uso da análise estática é limitado - por exemplo, quando não há código fonte. Neste artigo, falaremos sobre uma tecnologia bastante rara, mas muito útil, que permite combinar as vantagens de abordagens estáticas e dinâmicas - análise estática de código executável.
Vamos de longeDe acordo com a empresa de antivírus da McAfee, os danos globais causados pelo cibercrime em 2017 totalizaram cerca de US $ 600 bilhões, o que equivale a 0,8% do PIB global. Vivemos na era da tecnologia da informação, cujas especificidades têm sido a rápida integração da rede global e das tecnologias da Internet em todas as esferas da atividade humana. Agora, os crimes cibernéticos não são mais fora do comum.
As estatísticas mostram um aumento exponencial do crime cibernético.
A vulnerabilidade dos aplicativos se transformou em um problema sério: de acordo com o Departamento de Segurança Interna dos EUA, mais de 90% dos ataques cibernéticos bem-sucedidos são implementados usando várias vulnerabilidades nos aplicativos. Os métodos mais famosos de exploração de vulnerabilidades são:
- Injeção de SQL
- estouro de buffer
- script crossite
- Usando uma configuração não segura.
A análise de software (software) quanto à presença de recursos não declarados (NDV) e vulnerabilidades é a principal tecnologia para garantir a segurança do aplicativo.
Falando em tecnologias clássicas e bem estabelecidas para análise de software para vulnerabilidades e NDV (para conformidade com os requisitos de segurança da informação), podemos distinguir:
- análise de código estático (Static Application Security Testing);
- análise de código dinâmico (Dynamic Application Security Testing).
Existe o IAST (análise interativa), no entanto, é essencialmente dinâmico (no processo de análise, um agente adicional observa o que acontece durante a execução do aplicativo). O RASP (Autodefesa de Aplicativo de Tempo de Execução), que também é mencionado às vezes em várias ferramentas de análise, é provavelmente uma ferramenta de proteção.
A análise dinâmica (o método "Black Box") é uma verificação do programa durante sua execução. As vantagens a seguir podem ser diferenciadas dessa abordagem.
- Como as vulnerabilidades estão no programa executável e o erro é detectado usando sua operação, a geração de falsos positivos é menor que a da análise estática.
- Nenhum código fonte é necessário para executar a análise.
Mas também há desvantagens.
- Cobertura incompleta do código e, portanto, há riscos de vulnerabilidades ausentes. Por exemplo, a análise dinâmica não pode encontrar vulnerabilidades associadas ao uso de criptografia fraca ou indicadores como "bomba temporária".
- A necessidade de executar o aplicativo, o que em alguns casos pode ser difícil. O lançamento do aplicativo pode exigir configuração complexa e configuração de várias integrações. Além disso, para que os resultados sejam tão precisos quanto possível, é necessário reproduzir o "ambiente de combate", mas é difícil realizar isso sem prejudicar o software.
A análise estática (o método “Caixa Branca”) é um tipo de teste de programa no qual o programa não é executado.
Listamos os benefícios.
- Cobertura total do código, o que leva à busca por mais vulnerabilidades.
- Não há dependência do ambiente em que o programa será executado.
- A capacidade de implementar testes nos estágios iniciais de gravação de código de um módulo ou programa na ausência de arquivos executáveis. Isso permite que você já integre de maneira flexível uma solução semelhante ao SDLC (Ciclo de vida do desenvolvimento de software ou ciclo de vida do desenvolvimento de software) no início do desenvolvimento.
A única desvantagem do método é a presença de falsos positivos: a necessidade de avaliar se o analisador indica um erro real ou é provável que esse falso positivo.
Como podemos ver, ambos os métodos de análise têm vantagens e desvantagens. No entanto, é possível de alguma maneira usar as vantagens desses métodos, minimizando as desvantagens? Sim, se você aplicar a análise binária - a pesquisa de vulnerabilidades nos arquivos executáveis pela análise estática.
Análise binária ou tecnologia de análise de arquivos executáveis
A análise binária permite a análise estática sem código fonte, por exemplo, no caso de terceiros contratados. Além disso, a cobertura do código será completa, em contraste com a aplicação do método de análise dinâmica. Usando a análise binária, você pode verificar as bibliotecas de terceiros usadas no processo de desenvolvimento para as quais não há código fonte. Além disso, usando a análise binária, você pode realizar uma verificação de controle da versão, comparando os resultados da análise do código fonte do repositório e o código executável do servidor de combate.
No processo de análise binária, a imagem binária é transformada em uma representação intermediária (representação interna ou modelo de código) para posterior análise. Depois disso, algoritmos de análise estática são aplicados à representação interna. Como resultado, o modelo atual é complementado com as informações necessárias para a detecção adicional de vulnerabilidades e NDV. Na próxima etapa, a aplicação das regras para busca de vulnerabilidades e NDV.
Escrevemos mais sobre o esquema de análise estática
em um artigo anterior . Diferentemente da análise de código-fonte, que usa elementos da teoria da compilação (análise lexical e sintática) para construir o modelo, a análise binária usa a teoria da tradução reversa para desmontar, descompilar e desobstruir o modelo.
Um pouco sobre os termos
Estamos falando de analisar arquivos executáveis que não possuem informações de depuração. Com informações de depuração, a tarefa é bastante simplificada, mas se houver informações de depuração, o código-fonte provavelmente será e a tarefa se tornará irrelevante.
Neste artigo, chamamos a análise de bytecode Java também de análise binária, embora isso não esteja totalmente correto. Fazemos isso para simplificar o texto. Obviamente, a tarefa de analisar o bytecode da JVM é mais simples do que analisar o código C / C ++ binário e Objective-C / Swift. Mas o esquema geral de análise é semelhante no caso de bytecode e código binário. As principais dificuldades descritas no artigo estão relacionadas especificamente à análise do código binário.
Descompilação é o processo de recuperar o código fonte do código binário. Você pode falar sobre os elementos da tradução reversa - desmontagem (obtenção do código do assembler a partir de uma imagem binária), tradução do assembler em um código de três endereços ou outra representação, restaurando construções do nível do código-fonte.
Ofuscação - transformações que preservam a funcionalidade do código fonte, mas dificultam a descompilação e a compreensão da imagem binária resultante. Desobstrução é a transformação inversa. A ofuscação pode ser aplicada no nível do código-fonte e no nível do código binário.
Como assistir os resultados?
Vamos começar um pouco do final, mas a questão de visualizar os resultados da análise binária é geralmente feita primeiro.
É importante que um especialista analise o código binário para mapear vulnerabilidades e o NDV para o código-fonte. Para fazer isso, no estágio final, o processo de desofuscação (desenrolar) é iniciado se conversões confusas foram aplicadas e o código binário foi descompilado na fonte. Ou seja, as vulnerabilidades podem ser demonstradas no código descompilado.
No processo de descompilação, mesmo que descompilemos o bytecode da JVM, algumas das informações não são restauradas corretamente, portanto, a análise ocorre em uma representação próxima ao código binário. Nesse sentido, surge a pergunta: como, localizando vulnerabilidades no código binário, localizá-las na fonte? A solução para o problema do bytecode da JVM foi descrita
em nosso artigo sobre a pesquisa de vulnerabilidades no bytecode Java . A solução para o código binário é semelhante, ou seja, uma questão técnica.
Vamos repetir a ressalva importante - estamos falando sobre análise de código binário sem informações de depuração. Na presença de informações de depuração, a tarefa é bastante simplificada.
A principal pergunta que temos sobre a exibição dos resultados é se o código descompilado é suficiente para entender e localizar a vulnerabilidade.
Abaixo estão algumas reflexões sobre este assunto.
- Se estamos falando sobre o bytecode da JVM, em geral a resposta é "yes" - a qualidade de descompilação para o bytecode é ótima. Quase sempre você pode descobrir qual é a vulnerabilidade.
- O que pode interferir na localização qualitativa da vulnerabilidade é uma ofuscação simples, como renomear nomes de funções e funções. No entanto, na prática, muitas vezes acontece que é mais importante entender a vulnerabilidade do que determinar em qual arquivo está. A localização é necessária quando alguém pode corrigir a vulnerabilidade, mas, nesse caso, o desenvolvedor também entenderá onde está a vulnerabilidade do código descompilado.
- Quando falamos sobre a análise do código binário (por exemplo, C ++), é claro, tudo é muito mais complicado. Não existe uma ferramenta que recupere completamente o código C ++ aleatório. No entanto, a peculiaridade do nosso caso é que não precisamos compilar o código posteriormente: precisamos de qualidade suficiente para entender a vulnerabilidade.
- Na maioria das vezes, é possível obter uma qualidade de descompilação suficiente para entender a vulnerabilidade encontrada. Para fazer isso, você precisa resolver muitos problemas complexos, mas pode resolvê-los (abaixo, falaremos brevemente sobre isso).
- Para C / C ++, é ainda mais difícil localizar a vulnerabilidade - os nomes dos caracteres são perdidos de várias maneiras durante o processo de compilação, você não pode restaurá-los.
- A situação no Objective-C é um pouco melhor - existem nomes de funções e é mais fácil localizar a vulnerabilidade.
- As questões da ofuscação se destacam. Há várias transformações complexas que podem complicar a descompilação e o mapeamento de vulnerabilidades. Na prática, um bom descompilador pode lidar com a maioria das conversões confusas (lembre-se de que precisamos de qualidade de código suficiente para entender a vulnerabilidade).
Como conclusão - geralmente mostra a vulnerabilidade para que possa ser entendida e verificada.
Complexidades e detalhes da análise binária
Aqui não falaremos sobre o bytecode: todas as coisas interessantes sobre ele já foram ditas acima. O mais interessante é a análise do código binário real. Aqui vamos falar sobre a análise de C / C ++, Objective-C e Swift como um exemplo.
Dificuldades significativas surgem mesmo ao desmontar. O estágio mais importante é a divisão da imagem binária em subprogramas. Em seguida, selecione as instruções do montador nas sub-rotinas - uma questão técnica. Escrevemos sobre isso
em detalhes
em um artigo para a revista “Issues of Cybersecurity No. 1 (14) - 2016” , aqui descreveremos brevemente.
Como exemplo, falaremos sobre a arquitetura x86. As instruções nele não têm um comprimento fixo. Em imagens binárias, não há uma divisão clara em seções de código e dados: tabelas de importação, tabelas de funções virtuais podem estar na seção de códigos, tabelas de transição podem estar nos intervalos entre os blocos de funções base na seção de códigos. Portanto, é necessário poder separar o código dos dados e entender onde as rotinas começam e onde terminam.
Os mais comuns são dois métodos para resolver o problema de determinar os endereços iniciais dos subprogramas. No primeiro método, os endereços dos subprogramas são determinados pelo prólogo padrão (para a arquitetura x86 é push ebp; mov ebp, esp). No segundo método, uma seção do código é percorrida recursivamente a partir do ponto de entrada com o reconhecimento das instruções de chamada da sub-rotina. O desvio é feito através do reconhecimento de instruções de ramificação. As combinações dos métodos descritos também são usadas quando uma travessia recursiva é iniciada a partir dos endereços de partida encontrados pelo prólogo.
Na prática, verifica-se que essas abordagens fornecem uma porcentagem bastante baixa de código reconhecido, pois nem todas as funções têm um prólogo padrão e existem chamadas e transições indiretas.
Algoritmos básicos podem ser aprimorados pelas seguintes heurísticas.
- Em uma grande base de imagens de teste, encontre uma lista mais precisa de prólogos (novos prólogos ou variações dos padrões).
- Você pode encontrar automaticamente tabelas de funções virtuais e, a partir delas, selecionar os endereços iniciais dos subprogramas.
- Os endereços iniciais dos subprogramas e algumas outras construções podem ser encontrados com base nas seções do código binário associado ao mecanismo de tratamento de exceções.
- Você pode verificar os endereços iniciais pesquisando esses endereços na imagem e reconhecendo as instruções de chamada.
- Para procurar limites, você pode fazer um desvio recursivo da sub-rotina com o reconhecimento de instruções no endereço inicial. Há uma dificuldade com transições indiretas e funções sem retorno. A análise da tabela de importação e o reconhecimento de construções de comutadores podem ajudar.
Outra coisa importante que precisa ser feita durante a conversão reversa, para procurar normalmente uma vulnerabilidade posteriormente, é reconhecer funções padrão em uma imagem binária. As funções padrão podem ser vinculadas estaticamente à imagem ou até estar embutidas. O principal algoritmo de reconhecimento é uma pesquisa por assinatura com variações; para a solução, você pode oferecer o algoritmo Aho-Korasik adaptado. Para coletar assinaturas, é necessário pré-analisar as imagens da biblioteca coletadas com diferentes condições e selecioná-las como bytes imutáveis.
O que vem a seguir
Na seção anterior, examinamos o estágio inicial da tradução reversa de uma imagem binária - desmontagem. O estágio, de fato, é inicial, mas determinante. Nesse estágio, você pode perder parte do código, que terá um efeito dramático nos resultados da análise.
Então, muitas coisas interessantes acontecem. Diga brevemente sobre as principais tarefas. Não falaremos em detalhes: o know-how, sobre o qual não podemos escrever explicitamente aqui, ou soluções técnicas e de engenharia não muito interessantes estão nos detalhes.
- Convertendo código de montagem em uma representação intermediária na qual a análise pode ser executada. Você pode usar vários bytecodes. Para idiomas C, o LLVM parece ser uma boa escolha. O LLVM é ativamente suportado e desenvolvido pela comunidade; a infraestrutura, inclusive útil para análises estáticas, é atualmente impressionante. Nesta fase, há um grande número de detalhes aos quais você precisa prestar atenção. Por exemplo, você precisa detectar quais variáveis são endereçadas na pilha para não multiplicar entidades na exibição resultante. Você precisa configurar a exibição ideal dos conjuntos de instruções do assembler nas instruções do bytecode.
- Restaure estruturas de alto nível (por exemplo, loops, ramificações). Quanto mais preciso for possível restaurar as construções originais do código do montador, melhor será a qualidade da análise. A restauração de tais construções ocorre usando elementos da teoria dos grafos no CFG (controle de fluxo gráfico) e algumas outras representações gráficas do programa.
- Execução de algoritmos de análise estática. Há detalhes. Em geral, não é muito importante obtermos a representação interna da fonte ou do binário - todos também precisamos criar CFG, aplicar algoritmos de análise de fluxo de dados e outros algoritmos típicos da estática. Existem alguns recursos ao analisar a visualização obtida do binário, mas eles são mais técnicos.
Conclusões
Falamos sobre como você pode fazer análises estáticas quando não há código fonte. De acordo com a experiência de comunicação com os clientes, verifica-se que a tecnologia é muito procurada. No entanto, a tecnologia é rara: o problema da análise binária não é trivial, sua solução requer algoritmos complexos de alta tecnologia de análise estática e tradução reversa.
Este artigo foi escrito em colaboração com Anton Prokofiev, analista do Solar appScreener