
Autor do artigo: 0x64rem
Entrada
Há um ano e meio, tive a ideia de realizar meu phaser como parte da tese na universidade. Comecei a estudar materiais sobre gráficos de controle de fluxo, gráficos de fluxo de dados, execução simbólica etc. A seguir, veio a busca por ferramentas, uma amostra de diferentes bibliotecas (Angr, Triton, Pin, Z3). Nada concreto aconteceu no final, até este verão, fui para o programa Summer of Hack 2019 da Digital Security , onde me ofereceram a extensão do Analisador estático de Clang como tema para o projeto. Pareceu-me que esse tópico me ajudaria a colocar meu conhecimento teórico nas prateleiras, a começar a implementar algo substancial e a receber recomendações de mentores experientes. A seguir, mostrarei como foi o processo de elaboração do plug-in e descreverei o curso de meus pensamentos durante o mês do estágio.
Analisador estático de clang
Para o desenvolvimento, o Clang fornece três opções de interface para interação:
- LibClang é uma interface C de alto nível que permite que você interaja com o AST, mas não totalmente. Uma boa opção se você precisar de interação com outro idioma (por exemplo, a implementação de ligações ) ou uma interface estável.
- Clang Plugins - bibliotecas dinâmicas chamadas em tempo de compilação. Permite manipular completamente o AST.
- LibTooling - uma biblioteca para criar ferramentas separadas com base no Clang. Também oferece acesso total à interação com o AST. O código resultante pode ser executado fora do ambiente de construção do projeto verificado.
Como vamos expandir os recursos do Clang Static Analyzer, escolhemos a implementação do plug-in. Você pode escrever o código para o plug-in em C ++ ou Python.
Para o último, existem ligantes que permitem analisar o código-fonte, iterar sobre os nós da árvore de sintaxe abstrata resultante, também têm acesso às propriedades dos nós e podem mapear o nó para a linha do código-fonte. Esse conjunto é adequado para um simples verificador. Veja o repositório llvm para mais detalhes.
Como minha tarefa requer uma análise detalhada do código, o C ++ foi escolhido para desenvolvimento. A seguir, é apresentada uma introdução à ferramenta.
O Clang Staic Analyzer (doravante CSA) é uma ferramenta para análise estática do código C / C ++ / Objective-C com base na execução simbólica. O analisador pode ser chamado pelo front-end do Clang adicionando os sinalizadores -cc1 e -analyze ao comando build ou por meio de um binário de scan-build separado. Além da análise em si, o CSA possibilita a geração de relatórios visuais de html.

O CSA possui uma excelente biblioteca para analisar o código-fonte usando AST (Abstract Syntax Tree), CFG (Control Flow Graph). A partir das estruturas, é possível ver mais as declarações de variáveis, seus tipos, o uso de operadores binários e unários, você pode obter expressões simbólicas etc. Meu plug-in usará a funcionalidade das classes AST, essa opção será justificada ainda mais. A seguir, é apresentada uma lista de classes usadas na implementação do plug-in. A lista ajudará a obter um entendimento primário dos recursos do CSA:
Stmt - isso inclui operações binárias.
Decl - declaração de variáveis.
Expr - armazena as partes esquerda e direita das expressões, seu tipo.
ASTContext - informações sobre a árvore, o nó atual.
Gerenciador de origem - informações sobre o código real que corresponde à parte da árvore.
RecursiveASTVisitor, ASTMatcher - classes para atravessar uma árvore.
Repito que o CSA oferece ao desenvolvedor a oportunidade de examinar em detalhes a estrutura do código, e as classes listadas acima são apenas uma pequena parte do disponível. Definitivamente, recomendo consultar a documentação da sua versão do Clang se você não souber extrair nenhum dado; provavelmente, algo adequado já foi escrito.
Pesquisa de estouro inteiro
Para começar a implementar o plug-in, você precisa escolher a tarefa que ele resolverá. Nesse caso, o site llvm fornece listas de possíveis verificadores ; você também pode modificar os verificadores estáveis ou alfa existentes. Durante a revisão do código dos verificadores disponíveis, ficou claro que, para um desenvolvimento mais bem-sucedido da libclang, é melhor escrever seu verificador do zero, para que a escolha tenha sido feita a partir de uma lista de idéias não realizadas . Como resultado, a opção foi escolhida para criar um verificador para detecção de estouro inteiro. O Clang já possui funcionalidade para evitar essa vulnerabilidade (os sinalizadores -ftrapv, -fwrapv e similares são indicados para seu uso), ele é incorporado ao compilador e esse escape é derramado em avisos, e geralmente não é procurado lá. Ainda existe o UBSan , mas esses são desinfetantes, nem todos os usam, e esse método é sobre a identificação de problemas em tempo de execução, e o plug-in CSA funciona em tempo de compilação, analisando as fontes.
A seguir, é apresentada a coleção de materiais sobre a vulnerabilidade selecionada. O excesso de número inteiro costumava ser algo simples e não sério. De fato, a vulnerabilidade é divertida e pode ter conseqüências impressionantes.
Estouros de número inteiro são um tipo de vulnerabilidade que pode resultar em dados do tipo número inteiro no código assumindo valores inesperados. Estouro - se a variável se tornar maior do que o pretendido, Estouro insuficiente - menor que seu tipo original. Esses erros podem aparecer por causa do programador e por causa do compilador.
Em C ++, durante uma operação de comparação aritmética, os valores inteiros são convertidos para o mesmo tipo, mais frequentemente para um maior em termos de profundidade de bits. E esses fantasmas ocorrem em todos os lugares e constantemente, podem ser explícitos ou implícitos. Existem várias regras pelas quais fantasmas ocorrem [1]:
- Convertendo de um assinado para um tipo com um bit assinado, mas maior: basta adicionar a ordem superior.
- Convertendo um número inteiro assinado em um número inteiro não assinado da mesma capacidade: o negativo é convertido em positivo e assume um novo significado. Um exemplo de erro semelhante no DirectFB é o CVE-2014-2977 .
- Convertendo um número inteiro assinado em um número inteiro não assinado com uma maior capacidade de bits: primeiro, a capacidade de bits será expandida; se o número for negativo, o valor será alterado incorretamente. Por exemplo: 0xff (-1) se torna 0xffffffff.
- Um número inteiro não assinado com um sinal da mesma capacidade de bits: um número pode alterar o valor, dependendo do valor do bit alto.
- Um número inteiro não assinado com um número inteiro com um sinal de maior capacidade: primeiro, a capacidade de um número não assinado aumenta, depois a conversão para um número assinado.
- Conversão descendente: os bits são truncados. Isso pode tornar os valores não assinados negativos e assim por diante. Um exemplo dessa vulnerabilidade no PHP .
I.e. o gatilho da vulnerabilidade pode ser entrada não segura do usuário, aritmética incorreta, conversão incorreta de tipos causada por um programador ou compilador durante a otimização. A opção de bomba-relógio também é possível, quando um pedaço de código é inofensivo em uma versão do compilador, mas com o lançamento de um novo algoritmo de otimização "explode" e causa comportamento inesperado. Na história, já houve esse caso com a classe SafeInt (muito irônica) [5, 6.5.2].
Estouros de número inteiro abrem um vetor amplo: é possível forçar a execução a seguir um caminho diferente (se o estouro afetar instruções condicionais), causar um estouro de buffer. Para maior clareza, você pode se familiarizar com CVEs específicas, ver suas causas, conseqüências. É natural procurar um melhor estouro de número inteiro em produtos de código aberto, para que você possa ler não apenas a descrição, mas também o código.
- CVE-2019-3560 - O excesso de número inteiro no Fizz (um projeto que implementa o TLS para Facebook) pode explorar uma vulnerabilidade de DoS usando um pacote de rede apertado.
- CVE-2018-14618 - Estouro de buffer no Curl causado por estouro de número inteiro devido ao tamanho da senha.
- CVE-2018-6092 - Em sistemas de 32 bits, uma vulnerabilidade no WebAssembly for Chrome permitia que o RCE fosse implementado por meio de uma página HTML especial.
Para não reinventar a roda, foi considerado o código para detectar o excesso de número inteiro no analisador estático CppCheck . Sua abordagem é a seguinte:
- Determine se uma expressão é um operador binário.
- Se sim, verifique se os dois argumentos são do tipo inteiro.
- Determine o tamanho dos tipos.
- Verifique por meio de cálculos se o valor pode ir além de seus limites máximo ou mínimo.
Mas, nesta fase, não deu clareza. Acontece muitas histórias diferentes, e dessa sistematização de informações se torna mais difícil. Tudo em seu lugar coloca a lista da CWE . No total, existem 9 tipos de estouro de número inteiro alocados no site:
- 190 - fluxo inteiro de fluxo
- 191 - underflow inteiro
- 192 - erro de coerção de número inteiro
- 193 - fora de um
- 194 - Extensão inesperada de sinal
- 195 - Erro de conversão assinado não assinado
- 196 - Erro de conversão não assinado em assinado
- 197 - Erro de truncamento numérico
- 198 - Uso de pedidos de bytes incorretos
Consideramos o motivo de cada opção e entendemos que os estouros ocorrem com projeções explícitas / implícitas incorretas. E porque Se qualquer conversão for exibida na estrutura da árvore de sintaxe abstrata, usaremos o AST para análise. Na figura abaixo (Fig. 3), pode-se ver que qualquer operação que cause uma conversão na árvore é um nó separado e, vagando pela árvore, podemos verificar todas as conversões de tipo com base em uma tabela com transformações que podem causar um erro.

Mais especificamente, o algoritmo soa assim: damos uma olhada no Casts e olhamos para o IntegralCast (conversões de número inteiro). Se você encontrar um nó adequado, observe os descendentes em busca de uma operação binária ou Decl (declaração de variável). No primeiro caso, você precisa verificar a profundidade do sinal e dos bits que a operação binária usa. No segundo caso, compare apenas o tipo de declaração.
Implementação do verificador
Vamos começar a implementação. Precisamos de um esqueleto para um verificador, que pode ser uma biblioteca independente ou pode ser montado como parte do Clang. No código, a diferença será pequena. Se você já planeja escrever seu próprio plug-in, recomendo que você leia imediatamente um pequeno pdf: "Clang Static Analyzer: A Checker Developer Guide" , as coisas básicas estão bem descritas lá, embora algo possa não ser mais relevante, a biblioteca é atualizada regularmente, mas você agarre imediatamente.
Se você deseja adicionar seu verificador ao conjunto de clang, é necessário:
Escreva o verificador em si com aproximadamente o seguinte conteúdo:
namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> {
Em seguida, no código-fonte do Clang, você precisará alterar os arquivos CMakeLists.txt
e Checkers.td
. Viva por aqui ${llvm-source-path}/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
e aqui ${llvm-source-path}/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td
.
No primeiro, você só precisa adicionar o nome do arquivo com o código; no segundo, você precisa adicionar uma descrição estrutural:
#Checkers.td def SuperChecker : Checker<"SuperChecker">, HelpText<"test checker">, Documentation<HasDocumentation>;
Se não estiver claro, no arquivo Checkers.td
há exemplos suficientes de como e o que fazer.
Provavelmente você não desejará reconstruir o Clang e recorrerá à opção com o assembly da biblioteca (so / dll). Então, no código do verificador, deve ser algo como isto:
namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> {
Em seguida, colete seu código, você pode escrever seu próprio script para montagem, mas se tiver algum problema com isso (como o autor tinha :)), use o Makefile no código-fonte do clang e faça o comando clangStaticAnalyzerCheckers de uma maneira estranha.
Em seguida, ligue para o verificador:
para damas embutidas
clang++ -cc1 -analyze -analyzer-checker=core.DivideZero test.cpp
para externo
clang++ -cc1 -load ${PATH_TO_CHECKER}/SuperChecker.so -analyze -analyzer-checker=test.Me -analyzer-config test.Me:UsrInp1="foo" test.Me:Inp1="bar" -analyzer-config test.Me:Inp2=123 test.cpp
Nesta fase, já temos algum tipo de resultado (Fig. 4), mas o código escrito pode detectar apenas possíveis estouros. E isso significa um grande número de falsos positivos.

Para corrigir isso, podemos:
- Percorrendo o gráfico de um lado para o outro e verificando os valores específicos das variáveis para os casos em que temos um potencial estouro.
- Durante o percurso AST, salve imediatamente valores específicos para variáveis e verifique-os quando necessário.
- Use análise de contaminação.
Para fundamentar argumentos adicionais, vale ressaltar que, ao analisar o Clang, todos os arquivos especificados na diretiva #include
também analisam, como resultado, o tamanho do AST resultante aumenta. Como resultado, das opções propostas, apenas uma é racional em relação a uma tarefa específica:
- Primeiro, leva muito tempo para ser concluído. Andando em uma árvore, pesquisando e contando tudo o que você precisa levará muito tempo, pode ser difícil analisar um projeto grande com esse código. Para percorrer a árvore no código, usaremos a classe
clang::RecursiveASTVisitor
, que realiza uma pesquisa de profundidade recursiva. Uma estimativa do tempo dessa abordagem será
, onde V é o conjunto de vértices e E é o conjunto de arestas do gráfico. - O segundo - você certamente pode armazenar, mas não sabemos do que precisamos e do que não precisamos. Além disso, as próprias estruturas em árvore, que usamos na análise, exigem muita memória; portanto, gastar esses recursos em outra coisa é uma má idéia.
- Terceiro, é uma boa idéia: para esse método, você pode encontrar pesquisas e exemplos suficientes. Mas na CSA não há mácula pronta. Existe um verificador , que foi adicionado posteriormente à lista de verificadores alfa (alpha.security.taint.TaintPropagation) nas fontes, descrito no arquivo
GenericTaintChecker.cpp
. O verificador é bom, mas adequado apenas para funções de E / S inseguras conhecidas de C, apenas "marca" variáveis que eram argumentos ou resultados de funções perigosas. Além das opções descritas, vale a pena considerar variáveis globais, campos de classe etc. para restaurar corretamente o modelo de "distribuição".
O tempo restante para o estágio foi gasto lendo GenericTaintChecker.cpp
e tentando refazê-lo para atender às suas necessidades. Não deu certo até o final do período, mas continuou sendo uma tarefa de aprimoramento que já estava além do escopo do treinamento no DSec. Também durante o desenvolvimento, ficou claro que a identificação de funções perigosas é uma tarefa separada, nem sempre os lugares perigosos do projeto vêm de algumas funções padrão; portanto, um sinalizador foi adicionado ao verificador para indicar uma lista de funções que serão consideradas "envenenadas" / "marcadas" durante a análise de contaminação.
Além disso, uma verificação foi adicionada para determinar se a variável é um campo de bits. Por ferramentas CSA padrão, o tamanho é determinado pelo tipo e, se trabalharmos com um campo de bits, seu tamanho terá o valor do tipo de bit de todo o campo, e não o número de bits especificado na declaração da variável.
Qual é o resultado?
No momento, um verificador simples foi implementado que pode avisar apenas sobre possíveis estouros de número inteiro. Uma classe modificada para análise de contaminação, que ainda tem muito trabalho a fazer. Depois disso, você precisa usar o SMT para determinar os estouros. Para isso, o solver Z3 SMT é adequado, que foi adicionado ao assembly Clang na versão 5.0.0 (a julgar pelas notas de versão ). Para usar o solucionador, é necessário que o Clang seja construído com a opção CLANG_ANALYZER_BUILD_Z3=ON
e, quando o plug-in CSA for chamado diretamente, os -Xanalyzer -analyzer-constraints=z3
serão transmitidos.
Repositório de Resultados do GitHub
Referências:
Howard M., Leblanc D., Viega J. "Os 24 Pecados da Segurança do Computador"
Como escrever um verificador em 24 horas
Clang Static Analyzer: Guia do desenvolvedor de um verificador
Manual de desenvolvimento do verificador CSA
Dietz W. et al. Noções básicas sobre estouro de número inteiro em C / C ++