La compilación del código fuente produce un binario. Durante la compilación, puede proporcionar indicadores al compilador para habilitar o deshabilitar ciertas propiedades en el binario. Algunas de estas propiedades son relevantes para la seguridad.
Checksec es una pequeña herramienta ingeniosa (y script de shell) que, entre otras funciones, identifica las propiedades de seguridad que se incorporaron en un binario cuando se compiló. Un compilador puede habilitar algunas de estas propiedades de forma predeterminada y es posible que deba proporcionar indicadores específicos para habilitar otras.
Este artículo explica cómo usar checksec para identificar las propiedades de seguridad en un binario, incluyendo:
- Los comandos subyacentes que checksec utiliza para encontrar información sobre las propiedades de seguridad
- Cómo habilitar las propiedades de seguridad utilizando GNU Compiler Collection (GCC) al compilar un binario de muestra
Instalar checksec
Para instalar checksec en Fedora y otros sistemas basados en RPM, use:
$ sudo dnf install checksec
Para distribuciones basadas en Debian, use el equivalente apt
comando.
El script de shell
Checksec es un script de shell de un solo archivo, aunque bastante grande. Una ventaja es que puede leer el script rápidamente y comprender todos los comandos del sistema que se ejecutan para encontrar información sobre archivos binarios o ejecutables:
$ 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
Tome checksec para una unidad con un binario que probablemente ejecute a diario:el ubicuo ls
dominio. El formato del comando es checksec --file=
seguido de la ruta absoluta de ls
binario:
$ checksec --file=/usr/bin/ls
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 5 17 /usr/bin/ls
Cuando ejecuta esto en una terminal, ve un código de colores que muestra lo que es bueno y lo que probablemente no lo es. Digo "probablemente" porque incluso si algo está en rojo, no significa necesariamente que las cosas sean horribles; podría significar que los proveedores de la distribución hicieron algunas concesiones al compilar los binarios.
La primera línea proporciona varias propiedades de seguridad que normalmente están disponibles para binarios, como RELRO
, STACK CANARY
, NX
, y así sucesivamente (lo explico en detalle más abajo). La segunda línea muestra el estado de estas propiedades para el binario dado (ls
, en este caso). Por ejemplo, NX enabled
significa que alguna propiedad está habilitada para este binario.
Un binario de muestra
Para este tutorial, usaré el siguiente programa "hola mundo" como binario de muestra.
#include <stdio.h>
int main()
{
printf("Hello World\n");
return 0;
}
Tenga en cuenta que no proporcioné gcc
con banderas adicionales durante 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
Ejecute el binario a través de checksec. Algunas de las propiedades son diferentes que con el ls
comando anterior (en su pantalla, estos pueden aparecer en rojo):
$ checksec --file=./hello
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 85) Symbols No 0 0./hello
$
Cambiando el formato de salida
Checksec permite varios formatos de salida, que puede especificar con --output
. Elegiré el formato JSON y canalizaré la salida a jq
utilidad para una bonita impresión.
Para seguir, asegúrese de tener jq
instalado porque este tutorial usa este formato de salida para buscar rápidamente propiedades específicas de la salida e informar yes
o no
en cada:
$ 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"
}
}
Recorriendo las propiedades de seguridad
Más sobre seguridad
- La guía de codificación defensiva
- Seminario web:Automatización de la seguridad y el cumplimiento del sistema con un sistema operativo estándar
- 10 capas de seguridad de contenedores de Linux
- Libro para colorear de SELinux
- Más artículos de seguridad
El binario anterior incluye varias propiedades de seguridad. Compararé ese binario con el ls
binario anterior para examinar qué está habilitado y explicar cómo checksec encontró esta información.
1. Símbolos
Voy a empezar con el fácil primero. Durante la compilación, se incluyen ciertos símbolos en el binario, principalmente para la depuración. Estos símbolos son necesarios cuando está desarrollando software y requieren varios ciclos para depurar y arreglar cosas.
Estos símbolos generalmente se eliminan (eliminan) del binario final antes de que se publique para uso general. Esto no afecta la ejecución del binario de ninguna manera; se ejecutará tal como lo haría con los símbolos. La eliminación a menudo se realiza para ahorrar espacio, ya que el binario es algo más claro una vez que se eliminan los símbolos. En el software propietario o de código cerrado, los símbolos a menudo se eliminan porque tener estos símbolos en un binario hace que sea más fácil inferir el funcionamiento interno del software.
Según checksec, los símbolos están presentes en este binario, pero no estaban en el ls
binario. También puede encontrar esta información ejecutando el file
comando en el programa:verá not stripped
en la salida hacia el final:
$ checksec --file=/bin/ls --output=json | jq | grep symbols
"symbols": "no",
$ checksec --file=./hello --output=json | jq | grep symbols
"symbols": "yes",
$ 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
¿Cómo encontró checksec esta información? Bueno, proporciona un útil --debug
opción para mostrar qué funciones se ejecutaron. Por lo tanto, ejecutar el siguiente comando debería mostrarle qué funciones se ejecutaron dentro del script de shell:
$ checksec --debug --file=./hello
En este tutorial, busco los comandos subyacentes utilizados para encontrar esta información. Dado que es un script de shell, siempre puede utilizar las funciones de Bash. Este comando generará todos los comandos que se ejecutaron desde el script de shell:
$ bash -x /usr/bin/checksec --file=./hello
Si se desplaza por la salida, debería ver un echo_message
seguido de la categoría de la propiedad de seguridad. Esto es lo que informa checksec sobre si el binario contiene símbolos:
+ readelf -W --symbols ./hello
+ grep -q '\.symtab'
+ echo_message '\033[31m96) Symbols\t\033[m ' Symbols, ' symbols="yes"' '"symbols":"yes",'
Para simplificar esto, checksec utiliza el readelf
utilidad para leer el binario y proporciona un --symbols
especial bandera que enumera todos los símbolos dentro del binario. Luego busca un valor especial, .symtab
, que proporciona un recuento de entradas (símbolos) que encuentra. Puede probar los siguientes comandos en el binario de prueba que compiló anteriormente:
$ readelf -W --symbols ./hello
$ readelf -W --symbols ./hello | grep -i symtab
Cómo quitar símbolos
Puede eliminar los símbolos después de la compilación o durante la compilación.
- Compilación de publicaciones: Después de la compilación, puede usar la
strip
utilidad en el binario para eliminar los símbolos. Confirme que funcionó usando elfile
comando, que ahora muestra el resultado comostripped
:$ 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, not stripped
$
$ 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, stripped
$
Cómo eliminar símbolos durante la compilación
En lugar de eliminar los símbolos manualmente después de la compilación, puede pedirle al compilador que lo haga por usted proporcionando el -s
argumento:
$ 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, stripped
$
Después de volver a ejecutar checksec, puede ver que symbols
se muestran como no
:
$ checksec --file=./hello --output=json | jq | grep symbols
"symbols": "no",
$
2. Canarias
Los canarios son valores conocidos que se colocan entre un búfer y los datos de control en la pila para monitorear los desbordamientos del búfer. Cuando se ejecuta una aplicación, se le asignan dos tipos de memoria. Uno de ellos es una pila , que es simplemente una estructura de datos con dos operaciones:push
, que coloca los datos en la pila y pop
, que elimina datos de la pila en orden inverso. La entrada maliciosa podría desbordar o corromper la pila con una entrada especialmente diseñada y hacer que el programa se bloquee:
$ checksec --file=/bin/ls --output=json | jq | grep canary
"canary": "yes",
$
$ checksec --file=./hello --output=json | jq | grep canary
"canary": "no",
$
¿Cómo comprueba checksec si el binario está habilitado con un canario? Usando el método anterior, puede reducirlo ejecutando el siguiente comando dentro del script de shell:
$ readelf -W -s ./hello | grep -E '__stack_chk_fail|__intel_security_cookie'
Habilitar canary
Para protegerse contra estos casos, el compilador proporciona el -stack-protector-all
indicador, que agrega código adicional al binario para verificar tales desbordamientos de búfer:
$ gcc -fstack-protector-all hello.c -o hello
$ checksec --file=./hello --output=json | jq | grep canary
"canary": "yes",
Checksec muestra que la propiedad ahora está habilitada. También puede verificar esto con:
$ 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. pastel
PIE significa ejecutable independiente de la posición. Como sugiere el nombre, es un código que se coloca en algún lugar de la memoria para su ejecución, independientemente de su dirección absoluta:
$ checksec --file=/bin/ls --output=json | jq | grep pie
"pie": "yes",
$ checksec --file=./hello --output=json | jq | grep pie
"pie": "no",
A menudo, PIE está habilitado solo para bibliotecas y no para programas de línea de comandos independientes. En el resultado a continuación, hello
se muestra como LSB executable
, mientras que la libc
biblioteca estándar (.so
) el archivo está marcado como LSB shared object
:
$ 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
$ file /lib64/libc-2.32.so
/lib64/libc-2.32.so: ELF 64-bit LSB shared object, 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 intenta encontrar esta información con:
$ readelf -W -h ./hello | grep EXEC
Type: EXEC (Executable file)
Si prueba el mismo comando en una biblioteca compartida en lugar de EXEC
, verá un DYN
:
$ readelf -W -h /lib64/libc-2.32.so | grep DYN
Type: DYN (Shared object file)
Habilitar pastel
Para habilitar PIE en un programa de prueba, envíe los siguientes argumentos al compilador:
$ gcc -pie -fpie hello.c -o hello
Puede verificar que PIE esté habilitado usando checksec:
$ checksec --file=./hello --output=json | jq | grep pie
"pie": "yes",
$
Debería mostrarse como un ejecutable PIE con el tipo cambiado 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
NX significa "no ejecutable". A menudo está habilitado en el nivel de la CPU, por lo que un sistema operativo con NX habilitado puede marcar ciertas áreas de la memoria como no ejecutables. A menudo, los exploits de desbordamiento de búfer colocan código en la pila y luego intentan ejecutarlo. Sin embargo, hacer que esta área de escritura no sea ejecutable puede prevenir tales ataques. Esta propiedad está habilitada de forma predeterminada durante la compilación regular usando gcc
:
$ checksec --file=/bin/ls --output=json | jq | grep nx
"nx": "yes",
$ checksec --file=./hello --output=json | jq | grep nx
"nx": "yes",
Checksec determina esta información con el siguiente comando. RW
hacia el final significa que la pila es legible y escribible; ya que no hay E
, no es ejecutable:
$ readelf -W -l ./hello | grep GNU_STACK
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW 0x10
Deshabilitar NX para fines de demostración
No se recomienda, pero puede deshabilitar NX
al compilar un programa usando -z execstack
argumento:
$ gcc -z execstack hello.c -o hello
$ checksec --file=./hello --output=json | jq | grep nx
"nx": "no",
Tras la compilación, la pila se vuelve ejecutable (RWE
), que permite la ejecución de código malicioso:
$ readelf -W -l ./hello | grep GNU_STACK
GNU_STACK 0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10
5. RELRO
RELRO significa Reubicación de solo lectura. Un binario de formato enlazable ejecutable (ELF) utiliza una tabla de compensación global (GOT) para resolver funciones dinámicamente. Cuando está habilitada, esta propiedad de seguridad hace que GOT dentro del binario sea de solo lectura, lo que evita alguna forma de ataques de reubicación:
$ checksec --file=/bin/ls --output=json | jq | grep relro
"relro": "full",
$ checksec --file=./hello --output=json | jq | grep relro
"relro": "partial",
Checksec encuentra esta información usando el siguiente comando. Aquí se habilita una de las propiedades de RELRO; por lo tanto, el binario muestra "parcial" al verificar a través de checksec:
$ readelf -W -l ./hello | grep GNU_RELRO
GNU_RELRO 0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R 0x1
$ readelf -W -d ./hello | grep BIND_NOW
Habilitar RELRO completo
Para habilitar RELRO completo, use los siguientes argumentos de la línea de comandos al compilar con gcc
:
$ gcc -Wl,-z,relro,-z,now hello.c -o hello
$ checksec --file=./hello --output=json | jq | grep relro
"relro": "full",
Ahora, la segunda propiedad también está habilitada, haciendo que el programa esté lleno 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)
6. Fortificar
Fortify es otra propiedad de seguridad, pero está fuera del alcance de este artículo. Me iré aprendiendo cómo checksec verifica fortificar en binarios y cómo se habilita con gcc
como un ejercicio para que lo abordes.
$ checksec --file=/bin/ls --output=json | jq | grep -i forti
"fortify_source": "yes",
"fortified": "5",
"fortify-able": "17"
$ checksec --file=./hello --output=json | jq | grep -i forti
"fortify_source": "no",
"fortified": "0",
"fortify-able": "0"
Otras funciones de checksec
El tema de la seguridad es interminable, y aunque no es posible cubrir todo aquí, quiero mencionar algunas características más del checksec
comandos que hacen que sea un placer trabajar con ellos.
Ejecutar contra múltiples binarios
No es necesario que proporcione cada binario para checksec individualmente. En su lugar, puede proporcionar una ruta de directorio donde residen múltiples archivos binarios, y checksec los verificará todos de una vez:
$ checksec --dir=/usr/bin
Procesos
Además de los binarios, checksec también funciona en programas durante la ejecución. El siguiente comando encuentra las propiedades de seguridad de todos los programas en ejecución en su sistema. Puede usar --proc-all
si desea que verifique todos los procesos en ejecución, o puede seleccionar un proceso específico usando su nombre:
$ checksec --proc-all
$ checksec --proc=bash
Propiedades del núcleo
Además de las aplicaciones de la zona de usuario de checksec descritas en este artículo, también puede usarlas para verificar las propiedades del kernel integradas en su sistema:
$ checksec --kernel
Pruébalo
Checksec es una buena manera de entender qué propiedades de kernel y de espacio de usuario están habilitadas. Revise cada propiedad de seguridad en detalle e intente comprender las razones para habilitar cada función y los tipos de ataques que previene.