Crear un complemento para Clang Static Analyzer para buscar desbordamientos de enteros


Autor del artículo: 0x64rem


Entrada


Hace un año y medio, tuve la idea de realizar mi phaser como parte de la tesis en la universidad. Comencé a estudiar materiales sobre gráficos de flujo de control, gráficos de flujo de datos, ejecución simbólica, etc. Luego vino la búsqueda de herramientas, una muestra de diferentes bibliotecas (Angr, Triton, Pin, Z3). Al final no sucedió nada concreto, hasta que este verano fui al programa Summer of Hack 2019 de Digital Security , donde me ofrecieron la extensión del Analizador estático de Clang como tema para el proyecto. Me pareció que este tema me ayudaría a poner mi conocimiento teórico en los estantes, comenzar a implementar algo sustancial y obtener recomendaciones de mentores experimentados. A continuación, te contaré cómo fue el proceso de escribir el complemento y describiré el curso de mis pensamientos durante el mes de pasantía.


Analizador estático de Clang


Para el desarrollo, Clang ofrece tres opciones de interfaz para la interacción:


  • LibClang es una interfaz C de alto nivel que le permite interactuar con AST, pero no completamente. Una buena opción si necesita interacción con otro idioma (por ejemplo, la implementación de enlaces ) o una interfaz estable.
  • Complementos de Clang : bibliotecas dinámicas llamadas en tiempo de compilación. Le permite manipular completamente el AST.
  • LibTooling : una biblioteca para crear herramientas separadas basadas en Clang. También da acceso completo a la interacción con AST. El código resultante se puede ejecutar fuera del entorno de compilación del proyecto verificado.

Como vamos a ampliar las capacidades de Clang Static Analyzer, elegimos la implementación del complemento. Puede escribir código para el complemento en C ++ o Python.


Para este último, hay carpetas que le permiten analizar el código fuente, iterar sobre los nodos del árbol de sintaxis abstracta resultante, también tienen acceso a las propiedades de los nodos y pueden asignar el nodo a la línea del código fuente. Tal conjunto es adecuado para un simple corrector. Vea el repositorio llvm para más detalles.


Mi tarea requiere un análisis detallado del código, por lo que se eligió C ++ para el desarrollo. Lo siguiente es una introducción a la herramienta.


Clang Staic Analyzer (en adelante CSA) es una herramienta para el análisis estático de código C / C ++ / Objective-C basado en la ejecución simbólica. Se puede llamar al analizador a través de la interfaz de Clang agregando los indicadores -cc1 y -analyze al comando de compilación, o mediante un binario de exploración-compilación separado. Además del análisis en sí, CSA hace posible generar informes visuales html.


# ,      clang' clang -cc1 --help #  CSA  №1 clang++ -cc1 -x c++ -load path/to/Checker.so -analyze -analyzer-checker=test.Me -analyzer-config $BUILD_OPTIONS Checker.cpp 

  #  CSA  №2 scan-build -load-plugin path/to/Checker.so -enable-checker test.Me $BUILD_COMMAND 

  #       DivideZero clang++ -cc1 -analyze -analyzer-checker=core.DivideZero -o reports div-by-zero-test.cpp 


CSA tiene una excelente biblioteca para analizar el código fuente usando AST (Abstract Syntax Tree), CFG (Control Flow Graph). Desde las estructuras puede ver más lejos las declaraciones de variables, sus tipos, el uso de operadores binarios y unarios, puede obtener expresiones simbólicas, etc. Mi complemento utilizará la funcionalidad de las clases AST, esta elección se justificará aún más. La siguiente es una lista de clases que se utilizó en la implementación del complemento, la lista ayudará a obtener una comprensión primaria de las características de CSA:


  • Stmt: esto incluye operaciones binarias.


  • Decl - declaración de variables.


  • Expr: almacena las partes izquierda, derecha de las expresiones, su tipo.


  • ASTContext: información sobre el árbol, el nodo actual.


  • Administrador de origen: información sobre el código real que corresponde a la parte del árbol.


  • RecursiveASTVisitor, ASTMatcher: clases para atravesar un árbol.


    Repito que CSA brinda al desarrollador la oportunidad de examinar en detalle la estructura del código, y las clases enumeradas anteriormente son solo una pequeña parte de las disponibles. Definitivamente recomiendo consultar la documentación de su versión de Clang si no sabe cómo extraer datos; muy probablemente, algo adecuado ya ha sido escrito.



Búsqueda de desbordamiento de enteros


Para comenzar a implementar el complemento, debe elegir la tarea que resolverá. Para este caso, el sitio web llvm proporciona listas de posibles inspectores ; también puede modificar los comprobadores estables o alfa existentes. Durante la revisión del código de los verificadores disponibles, quedó claro que para un desarrollo más exitoso de libclang es mejor escribir su verificador desde cero, por lo que la elección se realizó a partir de una lista de ideas no realizadas . Como resultado, se eligió la opción para crear un verificador para la detección de desbordamiento de enteros. Clang ya tiene funcionalidad para prevenir esta vulnerabilidad (las banderas -ftrapv, -fwrapv y similares están indicadas para su uso), está integrado en el compilador, y ese escape se vierte en advertencias, y no se suele mirar allí. Todavía hay UBSan , pero estos son desinfectantes, no todos los usan, y este método trata de identificar problemas en tiempo de ejecución, y el complemento CSA funciona en tiempo de compilación, analizando las fuentes.


Lo siguiente es la recopilación de material sobre la vulnerabilidad seleccionada. El desbordamiento de enteros solía ser algo simple y no serio. De hecho, la vulnerabilidad es entretenida y puede tener consecuencias impresionantes.
Los desbordamientos de enteros son un tipo de vulnerabilidad que podría provocar que los datos de tipo entero en el código tomen valores inesperados. Desbordamiento: si la variable se ha vuelto más grande de lo previsto, Desbordamiento: menos que su tipo original. Dichos errores pueden aparecer tanto por el programador como por el compilador.


En C ++, durante una operación de comparación aritmética, los valores enteros se convierten al mismo tipo, más a menudo a uno más grande en términos de profundidad de bits. Y tales fantasmas ocurren en todas partes y constantemente, pueden ser explícitos o implícitos. Hay varias reglas por las cuales ocurren los fantasmas [1]:


  • Conversión de un tipo firmado a un tipo con un bit firmado pero más grande: simplemente agregue el orden superior.
  • Convertir un entero con signo en un entero sin signo de la misma capacidad: lo negativo se convierte en positivo y adquiere un nuevo significado. Un ejemplo de un error similar en DirectFB es CVE-2014-2977 .
  • Convertir un entero con signo en un entero sin signo de mayor capacidad de bits: primero, la capacidad de bits se expandirá, luego, si el número es negativo, entonces cambiará incorrectamente el valor. Por ejemplo: 0xff (-1) se convierte en 0xffffffff.
  • Un entero sin signo con un signo de la misma capacidad de bit: un número puede cambiar el valor, dependiendo del valor del bit alto.
  • Un entero sin signo con un entero con un signo de mayor capacidad: primero, aumenta la capacidad de un número sin signo, luego la conversión a uno con signo.
  • Conversión descendente: los bits simplemente se truncan. Esto puede hacer que los valores sin signo sean negativos, etc. Un ejemplo de tal vulnerabilidad en PHP .

Es decir El desencadenante de la vulnerabilidad puede ser una entrada insegura del usuario, aritmética incorrecta, conversión de tipo incorrecta causada por un programador o compilador durante la optimización. La opción de bomba de tiempo también es posible, cuando un código es inofensivo con una versión del compilador, pero con el lanzamiento de un nuevo algoritmo de optimización "explota" y causa un comportamiento inesperado. En la historia, ya ha habido un caso así con la clase SafeInt (muy irónico) [5, 6.5.2].


Los desbordamientos de enteros abren un vector ancho: es posible forzar la ejecución para que tome una ruta diferente (si el desbordamiento afecta las declaraciones condicionales), se produce un desbordamiento del búfer. Para mayor claridad, puede familiarizarse con CVE específicos, ver sus causas, consecuencias. Naturalmente, es mejor buscar el desbordamiento de enteros en productos de código abierto, para que no solo lea la descripción, sino que también vea el código.


  • CVE-2019-3560 : el desbordamiento de enteros en Fizz (un proyecto que implementa TLS para Facebook) podría explotar una vulnerabilidad DoS utilizando un paquete de red reducido.
  • CVE-2018-14618 : desbordamiento de búfer en Curl causado por desbordamiento de enteros debido a la longitud de la contraseña.
  • CVE-2018-6092 : en los sistemas de 32 bits, una vulnerabilidad en WebAssembly para Chrome permitió que RCE se implementara a través de una página HTML especial.

Para no reinventar la rueda, se consideró el código para detectar el desbordamiento de enteros en el analizador estático CppCheck . Su enfoque es el siguiente:


  1. Determine si una expresión es un operador binario.
  2. En caso afirmativo, verifique si ambos argumentos son de tipo entero.
  3. Determinar el tamaño de los tipos.
  4. Compruebe mediante cálculos si el valor puede ir más allá de sus límites máximos o mínimos.
    Pero en esta etapa no dio claridad. Resulta muchas historias diferentes, y de esta sistematización de la información se hace más difícil. Todo en su lugar pone la lista de CWE . En total, hay 9 tipos de desbordamiento de enteros asignados en el sitio:
    • 190 - desbordamiento de enteros
    • 191 - flujo inferior entero
    • 192 - error de coerción entero
    • 193 - off-by-one
    • 194 - Extensión de señal inesperada
    • 195 - Error de conversión de firmado a no firmado
    • 196 - Error de conversión de sin firmar a firmado
    • 197 - Error de truncamiento numérico
    • 198 - Uso de orden de bytes incorrecto

Consideramos el motivo de cada opción y entendemos que se producen desbordamientos con conversiones explícitas / implícitas incorrectas. Y porque cualquier conversión se muestra en la estructura del árbol de sintaxis abstracta, usaremos AST para el análisis. En la figura a continuación (Fig. 3), se puede ver que cualquier operación que causa una conversión en el árbol es un nodo separado, y, deambulando por el árbol, podemos verificar todas las conversiones de tipos basadas en una tabla con transformaciones que pueden causar un error.


Signo gSigno lRegístrate eUnsign gUnsign lUnsign e
Letrero+-+---
Unsign+----+


Más específicamente, el algoritmo suena así: vamos alrededor de Casts y miramos IntegralCast (conversiones enteras). Si encuentra un nodo adecuado, mire a los descendientes en busca de una operación binaria o Decl (declaración de variable). En el primer caso, debe verificar el signo y la profundidad de bits que utiliza la operación binaria. En el segundo caso, compare solo el tipo de declaración.


Implementación del verificador


Vayamos a la implementación. Necesitamos un esqueleto para un corrector, que puede ser una biblioteca independiente o puede ensamblarse como parte de Clang. En el código, la diferencia será pequeña. Si ya está planeando escribir su propio complemento, le recomiendo que lea inmediatamente un pequeño pdf: "Analizador estático de Clang: Una guía para desarrolladores" , las cosas básicas están bien descritas allí, aunque es posible que algo ya no sea relevante, la biblioteca se actualiza regularmente, pero usted agarrar de inmediato.


Si desea agregar su corrector a su ensamblaje de clanes, entonces necesita:


  1. Escriba el corrector en sí con aproximadamente el siguiente contenido:


     namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { //       ,    .       struct CheckerOpts { //       string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code }; } void ento::registerSuperChecker(CheckerManager &mgr) { auto checker = mgr.registerChecker<SuperChecker>(); //       ,   4    //       ,  stand-alone    . AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); SuperChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); // getCheckerIntegerOption:  ,  ,   } 

  2. Luego, en el código fuente de Clang, deberá cambiar los archivos CMakeLists.txt y Checkers.td . Vive por aquí ${llvm-source-path}/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
    y aquí ${llvm-source-path}/clang/include/clang/StaticAnalyzer/Checkers/Checkers.td .
    En el primero, solo necesita agregar el nombre del archivo con el código, en el segundo debe agregar una descripción estructural:


      #Checkers.td def SuperChecker : Checker<"SuperChecker">, HelpText<"test checker">, Documentation<HasDocumentation>; 


Si no está claro, en el archivo Checkers.td hay suficientes ejemplos de cómo y qué hacer.


Lo más probable es que no desee reconstruir Clang, y recurrirá a la opción con el ensamblado de la biblioteca (so / dll). Luego, en el código del verificador debería haber algo como esto:


 namespace { class SuperChecker : public Checker<check::PreStmt<BinaryOperator>> { //       ,    .       struct CheckerOpts { string FlagOne; int FlagTwo; }; CheckerOpts Opts; //cool code }; } void initializationFunction(CheckerManager &mgr){ SuperChecker *checker = mgr.registerChecker<SuperChecker>(); //       ,   4    AnalyzerOptions &AnOpts = mgr.getAnalyzerOptions(); TestChecker::CheckerOpts &ChOpts = checker->Opts; ChOpts.FlagOne = AnOpts.getCheckerStringOption("Inp1", "", checker); ChOpts.FlagTwo = AnOpts.getCheckerIntegerOption("Inp2", 0, checker); // getCheckerIntegerOption:  ,  ,   } extern "C" void clang_registerCheckers (CheckerRegistry &registry) { registry.addChecker(&initializationFunction, "test.Me", "SuperChecker description", "doc_link"); } extern "C" const char clang_analyzerAPIVersionString [] = "8.0.1"; 

A continuación, recopile su código, puede escribir su propio script para el ensamblaje, pero si tiene algún problema con esto (como tenía el autor :)), puede usar el Makefile en el código fuente clang y hacer el comando clangStaticAnalyzerCheckers de una manera extraña.


Luego, llame al verificador:


  • para damas incorporadas


     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 

    En esta etapa, ya tenemos algún tipo de resultado (Fig. 4), pero el código escrito solo puede detectar desbordamientos potenciales. Y eso significa una gran cantidad de falsos positivos.




Para solucionar esto, podemos:


  • Recorriendo el gráfico de un lado a otro y verificando los valores específicos de las variables para casos en los que tenemos un desbordamiento potencial.
  • Durante el recorrido AST, guarde inmediatamente valores específicos para variables y verifíquelos cuando sea necesario.
  • Usar análisis de manchas.

Para reforzar más argumentos, vale la pena mencionar que al analizar Clang, todos los archivos especificados en la directiva #include también analizan, como resultado, el tamaño del AST resultante aumenta. Como resultado, de las opciones propuestas, solo una es racional con respecto a una tarea específica:


  • Primero, lleva mucho tiempo completarlo. Caminar en un árbol, buscar y contar todo lo que necesita llevará mucho tiempo, puede resultar difícil analizar un proyecto grande con dicho código. Para recorrer el árbol en el código, usaremos la clase clang::RecursiveASTVisitor , que realiza una búsqueda de profundidad recursiva. Una estimación del tiempo de este enfoque. , donde V es el conjunto de vértices y E es el conjunto de bordes del gráfico.
  • El segundo: ciertamente puede almacenar, pero no sabemos qué necesitaremos y qué no. Además, las estructuras de los árboles, que utilizamos en el análisis, requieren mucha memoria, por lo que gastar esos recursos en otra cosa es una mala idea.
  • En tercer lugar, es una buena idea, para este método puede encontrar suficiente investigación y ejemplos. Pero en CSA no hay una mancha lista. Hay un verificador , que luego se agregó a la lista de verificadores alfa (alpha.security.taint.TaintPropagation) en las fuentes, se describe en el archivo GenericTaintChecker.cpp . El verificador es bueno, pero adecuado solo para funciones de E / S inseguras conocidas de C, solo "marca" las variables que fueron argumentos o resultados de funciones peligrosas. Además de las opciones descritas, vale la pena considerar variables globales, campos de clase, etc., para restaurar correctamente el modelo de "distribución".

El tiempo restante para la pasantía se pasó leyendo GenericTaintChecker.cpp y tratando de rehacerlo para satisfacer sus necesidades. No funcionó con éxito al final del período, pero siguió siendo una tarea de refinamiento que ya estaba más allá del alcance de la capacitación en DSec. Además, durante el desarrollo quedó claro que identificar funciones peligrosas es una tarea separada, no siempre los lugares peligrosos del proyecto provienen de algunas funciones estándar, por lo tanto, se agregó una marca al verificador para indicar una lista de funciones que se considerarán "envenenadas" / "marcadas" durante el análisis de manchas.
Además, se agregó una verificación para determinar si la variable es un campo de bits. Según las herramientas CSA estándar, el tamaño está determinado por el tipo, y si trabajamos con un campo de bits, entonces su tamaño tendrá el valor del tipo de bit de todo el campo, y no el número de bits especificado en la declaración de variable.


Cual es el resultado?


Por el momento, se ha implementado un simple comprobador que solo puede advertir sobre posibles desbordamientos de enteros. Una clase modificada para el análisis de manchas, que todavía tiene mucho trabajo por hacer. Después de eso, debe usar SMT para determinar los desbordamientos. Para esto, el solucionador Z3 SMT es adecuado, que se agregó al ensamblaje de Clang en la versión 5.0.0 (a juzgar por las notas de la versión ). Para usar el solucionador, es necesario que Clang se construya con la opción CLANG_ANALYZER_BUILD_Z3=ON , y cuando se llama directamente al complemento CSA, se transmiten las banderas -Xanalyzer -analyzer-constraints=z3 .


Repositorio de resultados de GitHub


Referencias


  1. Howard M., Leblanc D., Viega J. "Los 24 pecados de la seguridad informática"


  2. Cómo escribir un corrector en 24 horas


  3. Analizador estático de Clang: una guía para desarrolladores de Checker


  4. Manual de desarrollo del verificador CSA


  5. Dietz W. y col. Comprender el desbordamiento de enteros en C / C ++


Source: https://habr.com/ru/post/473412/


All Articles