Encuentre y corrija vulnerabilidades de archivos binarios en Linux, con la utilidad checksec y el compilador gcc



Imagen: Imágenes de libros de Internet Archive. Modificado por Opensource.com. CC BY-SA 4.0



Después de compilar el mismo código fuente, podemos terminar con diferentes binarios. Depende de qué banderas pasamos a manos del compilador. Algunas de estas banderas le permiten habilitar o deshabilitar una serie de propiedades del binario relacionadas con la seguridad.



Algunos de ellos están habilitados o deshabilitados por el compilador de forma predeterminada. Así es como pueden surgir vulnerabilidades en archivos binarios que no conocemos.



Checksec es una sencilla utilidad para determinar qué propiedades se incluyeron en el momento de la compilación. En este artículo te diré:



  • cómo utilizar la utilidad checksec para encontrar vulnerabilidades;
  • cómo utilizar el compilador gcc para corregir las vulnerabilidades encontradas.


Instalación de checksec



Para el sistema operativo Fedora y otros sistemas basados ​​en RPM:



$ sudo dnf install checksec
      
      





Para sistemas basados ​​en Debian, use apt.



Inicio rápido con checksec



La utilidad checksec consta de un único archivo de secuencia de comandos, que, sin embargo, es bastante grande. Gracias a esta transparencia, puede averiguar qué comandos del sistema para buscar vulnerabilidades en binarios se ejecutan bajo el capó:



$ file /usr/bin/checksec

/usr/bin/checksec: Bourne-Again shell script, ASCII text executable, with very long lines

$ wc -l /usr/bin/checksec

2111 /usr/bin/checksec
      
      





Ejecutemos checksec en la utilidad de exploración de directorios (ls):



$ checksec --file=/usr/bin/ls

<strong>RELRO           STACK CANARY      NX            PIE             RPATH     RUNPATH      Symbols       FORTIFY Fortified       Fortifiable    FILE</strong>

Full RELRO      Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols        Yes   5       17              /usr/bin/ls

      
      





Al ejecutar el comando en la terminal, recibirá un informe sobre qué propiedades útiles tiene este binario y cuáles no.  



La primera línea es el encabezado de la tabla, que enumera las distintas propiedades de seguridad: RELRO, STACK CANARY, NX, etc. La segunda línea muestra los valores de estas propiedades para el binario de utilidad ls.



¡Hola binario!



Compilaré un binario del código C más simple:



#include <stdio.h>

int main()

{

        printf(«Hello World\n»);

        return 0;

}

      
      





Tenga en cuenta que hasta ahora no he pasado una sola bandera al compilador, con la excepción de -o (no viene al caso, pero simplemente dice dónde mostrar el resultado de la compilación):



$ gcc hello.c -o hello

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped

$ ./hello

Hello World
      
      





Ahora ejecutaré la utilidad checksec para mi binario. Algunas propiedades son diferentes de las propiedades.



ls (     ):

$ checksec --file=./hello

<strong>RELRO           STACK CANARY      NX            PIE             RPATH     RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE</strong>

Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   85) Symbols       No    0       0./hello

      
      





Checksec le permite utilizar una variedad de formatos de salida, que puede especificar con la opción --output. Elegiré el formato JSON y haré que la salida sea más descriptiva con la utilidad jq :



$ checksec --file=./hello --output=json | jq

{

  «./hello»: {

    «relro»: «partial»,

    «canary»: «no»,

    «nx»: «yes»,

    «pie»: «no»,

    «rpath»: «no»,

    «runpath»: «no»,

    «symbols»: «yes»,

    «fortify_source»: «no»,

    «fortified»: «0»,

    «fortify-able»: «0»

  }

}

      
      





Análisis (checksec) y eliminación (gcc) de vulnerabilidades



El archivo binario creado anteriormente tiene varias propiedades que determinan, digamos, el grado de vulnerabilidad. Compararé las propiedades de este archivo con las propiedades del binario ls (también enumerado arriba) y explicaré cómo hacer esto usando la utilidad checksec. 



Para cada elemento, además le mostraré cómo eliminar las vulnerabilidades encontradas.



1. Símbolos de depuración



Empezaré simple. Ciertos símbolos se incluyen en el binario en tiempo de compilación. Estos símbolos se utilizan en el desarrollo de software: son necesarios para depurar y corregir errores.



Los símbolos de depuración generalmente se eliminan de la versión del binario que los desarrolladores lanzan para uso general. Esto no afecta el funcionamiento del programa de ninguna manera. Esta limpieza (indicada por la tira de palabras ) se realiza a menudo para ahorrar espacio, ya que el archivo se vuelve más claro después de eliminar los caracteres. Y en el software propietario, estos caracteres a menudo se eliminan también porque los atacantes tienen la capacidad de leerlos en formato binario y utilizarlos para sus propios fines.



Checksec muestra que los símbolos de depuración están presentes en mi binario, pero no en el archivo ls. 



$ checksec --file=/bin/ls --output=json | jq | grep symbols

    «symbols»: «no»,

$ checksec --file=./hello --output=json | jq | grep symbols

    «symbols»: «yes»,
      
      







La ejecución del comando de archivo puede mostrar lo mismo. Los personajes no se eliminan.



$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, <strong>not stripped</strong>
      
      





Cómo funciona checksec



Ejecutemos este comando con la opción --debug:



$ checksec --debug --file=./hello

      
      





Dado que la utilidad checksec es una secuencia de comandos larga, puede utilizar las funciones de Bash para examinarla. Vamos a mostrar los comandos que ejecuta el script para mi archivo de saludo:



$ bash -x /usr/bin/checksec --file=./hello
      
      





Preste especial atención a echo_message, la salida de un mensaje sobre si el binario contiene símbolos de depuración:



+ readelf -W --symbols ./hello

+ grep -q '\.symtab'

+ echo_message '\033[31m96) Symbols\t\033[m  ' Symbols, ' symbols=«yes»' '«symbols»:«yes»,'
      
      





La utilidad checksec usa el comando readelf con la bandera especial --symbols para leer un archivo binario. Imprime todos los símbolos de depuración en el binario. 



$ readelf -W --symbols ./hello
      
      





En el contenido de la sección .symtab, puede averiguar la cantidad de símbolos encontrados:



$ readelf -W --symbols ./hello | grep -i symtab
      
      





Cómo eliminar los símbolos de depuración después de la compilación



La utilidad de la tira nos ayudará con esto.



$ gcc hello.c -o hello

$
 

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>not stripped</strong>

$
 

$ strip hello

$
 

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=322037496cf6a2029dcdcf68649a4ebc63780138, for GNU/Linux 3.2.0, <strong>stripped</strong>
      
      





Cómo eliminar símbolos de depuración en tiempo de compilación



Al compilar, use la bandera -s:



$ gcc -s hello.c -o hello

$

$ file hello

hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=247de82a8ad84e7d8f20751ce79ea9e0cf4bd263, for GNU/Linux 3.2.0, <strong>stripped</strong>

      
      





También puede verificar que los símbolos se hayan eliminado utilizando la utilidad checksec:



$ checksec --file=./hello --output=json | jq | grep symbols

    «symbols»: «no»,

      
      





2. Canarias



Canary (informantes) son valores "secretos" que se almacenan en la pila entre el búfer y los datos de control. Se utilizan para proteger contra ataques de desbordamiento del búfer: si se cambian estos valores, vale la pena hacer sonar la alarma. Cuando se inicia una aplicación, se crea su propia pila para ella. En este caso, es solo una estructura de datos con operaciones push y pop. Un atacante podría preparar datos maliciosos y escribirlos en la pila. En este caso, el búfer puede desbordarse y la pila puede dañarse. En el futuro, esto provocará un bloqueo del programa. El análisis de los valores canarios le permite comprender rápidamente que se ha producido un hackeo y tomar medidas.



$ checksec --file=/bin/ls --output=json | jq | grep canary

    «canary»: «yes»,

$

$ checksec --file=./hello --output=json | jq | grep canary

    «canary»: «no»,

$

 ,    canary,  checksec   :

$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'
      
      





Enciende canary



Para hacer esto, al compilar, usamos el indicador -stack-protector-all:



$ gcc -fstack-protector-all hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep canary

    «canary»: «yes»,

      
      





Ahora checksec puede decirnos con la conciencia tranquila que el mecanismo canario está encendido:



$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)

    83: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@@GLIBC_2.4

$

      
      





3. PIE



La propiedad PIE habilitada permite que el código ejecutable se coloque arbitrariamente en la memoria independientemente de su dirección absoluta:



PIE (Ejecutable independiente de posición): código ejecutable independiente de la posición. La capacidad de predecir dónde y qué áreas de la memoria se encuentran en el espacio de direcciones de un proceso favorece a los atacantes. Los programas de usuario se cargan y ejecutan desde una dirección de memoria virtual de proceso predefinida a menos que se compilen con la opción PIE. El uso de PIE permite que el sistema operativo cargue secciones de código ejecutable en fragmentos arbitrarios de memoria, lo que hace que sea mucho más difícil de descifrar.



$ checksec --file=/bin/ls --output=json | jq | grep pie

    «pie»: «yes»,

$ checksec --file=./hello --output=json | jq | grep pie

    «pie»: «no»,

      
      





A menudo, la propiedad PIE solo se incluye al compilar bibliotecas. En el resultado a continuación, hello está marcado como ejecutable LSB y el archivo libc (.so) de la biblioteca estándar está marcado como objeto compartido LSB:



$ file hello

hello: ELF 64-bit <strong>LSB executable</strong>, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=014b8966ba43e3ae47fab5acae051e208ec9074c, for GNU/Linux 3.2.0, not stripped

$ file /lib64/libc-2.32.so

/lib64/libc-2.32.so: ELF 64-bit <strong>LSB shared object</strong>, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=4a7fb374097fb927fb93d35ef98ba89262d0c4a4, for GNU/Linux 3.2.0, not stripped

      
      





Checksec obtiene esta información así:



$ readelf -W -h ./hello | grep EXEC

  Type:                              EXEC (Executable file)

      
      





Si ejecuta el mismo comando para la biblioteca, verá DYN en lugar de EXEC:



$ readelf -W -h /lib64/libc-2.32.so | grep DYN

  Type:                              DYN (Shared object file)

      
      





Enciende PIE



Al compilar el programa, debe especificar los siguientes indicadores:



$ gcc -pie -fpie hello.c -o hello
      
      





Para asegurarse de que la propiedad PIE esté habilitada, ejecute el siguiente comando:



$ checksec --file=./hello --output=json | jq | grep pie

    «pie»: «yes»,

$
      
      





Ahora nuestro archivo binario (hola) cambiará su tipo de EXEC a DYN:



$ file hello

hello: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=bb039adf2530d97e02f534a94f0f668cd540f940, for GNU/Linux 3.2.0, not stripped

$ readelf -W -h ./hello | grep DYN

  Type:                              DYN (Shared object file)

      
      





4. NX



Las herramientas del sistema operativo y del procesador le permiten configurar de manera flexible los derechos de acceso a las páginas de memoria virtual. Al habilitar la propiedad NX (No Execute), podemos evitar que los datos se interpreten como instrucciones del procesador. A menudo, en los ataques de desbordamiento de búfer, los atacantes insertan código en la pila y luego intentan ejecutarlo. Sin embargo, al evitar que el código se ejecute en estos segmentos de memoria, estos ataques pueden evitarse. En la compilación normal con gcc, esta propiedad está habilitada de forma predeterminada:



$ checksec --file=/bin/ls --output=json | jq | grep nx

    «nx»: «yes»,

$ checksec --file=./hello --output=json | jq | grep nx

    «nx»: «yes»,

      
      





Checksec vuelve a utilizar el comando readelf para obtener información sobre la propiedad NX. En este caso, RW significa que la pila es de lectura / escritura. Pero dado que esta combinación no contiene el carácter E, existe una prohibición de ejecutar código desde esta pila:



$ readelf -W -l ./hello | grep GNU_STACK

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10

      
      





Desactivar NX 



No se recomienda deshabilitar la propiedad NX, pero puede hacerlo así:



$ gcc -z execstack hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep nx

    «nx»: «no»,

      
      





Después de la compilación, veremos que los permisos de la pila han cambiado a RWE:



$ readelf -W -l ./hello | grep GNU_STACK

  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
      
      





5. RELRO



En los binarios vinculados dinámicamente, se utiliza una GOT (Tabla de compensación global) especial para llamar a funciones desde bibliotecas. Esta tabla está referenciada por los binarios ELF (Executable Linkable Format). Cuando la protección RELRO (solo lectura de reubicación) está habilitada, el GOT pasa a ser de solo lectura. Esto le permite protegerse contra algunos tipos de ataques que modifican los registros de la tabla:



$ checksec --file=/bin/ls --output=json | jq | grep relro

    «relro»: «full»,

$ checksec --file=./hello --output=json | jq | grep relro

    «relro»: «partial»,

      
      





En este caso, sólo una de las propiedades RELRO está habilitada, por lo que checksec genera el valor "parcial". Checksec usa el comando readelf para mostrar la configuración. 



$ readelf -W -l ./hello | grep GNU_RELRO

  GNU_RELRO      0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R   0x1

$ readelf -W -d ./hello | grep BIND_NOW
      
      





Encienda la protección completa (FULL RELRO)



Para hacer esto, al compilar, necesita usar las banderas apropiadas:



$ gcc -Wl,-z,relro,-z,now hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep relro

    «relro»: «full»,

      
      





Eso es todo, ahora nuestro binario ha recibido el título honorífico de FULL RELRO:



$ readelf -W -l ./hello | grep GNU_RELRO

  GNU_RELRO      0x002dd0 0x0000000000403dd0 0x0000000000403dd0 0x000230 0x000230 R   0x1

$ readelf -W -d ./hello | grep BIND_NOW

 0x0000000000000018 (BIND_NOW)    
      
      



      

Otras características de checksec



El tema de la seguridad se puede estudiar sin cesar. Incluso hablando de la sencilla utilidad checksec en este artículo, no puedo cubrir todo. Sin embargo, mencionaré algunas posibilidades más interesantes.



Comprobando varios archivos



No es necesario ejecutar un comando por separado para cada archivo. Puede ejecutar un comando para varios binarios a la vez:



$ checksec --dir=/usr/bin
      
      





Comprobación de procesos



La utilidad checksec también le permite analizar la seguridad de los procesos. El siguiente comando muestra las propiedades de todos los programas en ejecución en su sistema (necesita usar la opción --proc-all para hacer esto): 



$ checksec --proc-all
      
      





También puede seleccionar un proceso para verificar especificando su nombre:



$ checksec --proc=bash
      
      





Verificación de kernel



Del mismo modo, puede analizar las vulnerabilidades en el kernel de su sistema.



$ checksec --kernel
      
      





Prevenido vale por dos



Estudie las propiedades de seguridad en detalle e intente comprender exactamente a qué afecta cada una de ellas y qué tipos de ataques puede prevenir. ¡Checksec para ayudarte!






Los servidores en la nube de Macleod son rápidos y seguros.



Regístrese usando el enlace de arriba o haciendo clic en el banner y obtenga un 10% de descuento durante el primer mes de alquiler de un servidor de cualquier configuración.






All Articles