Ricerca nel sito web

Identificare le proprietà di sicurezza su Linux utilizzando checksec


Scopri come utilizzare checksec per identificare le proprietà di sicurezza di un eseguibile, capire cosa significano e sapere come utilizzarle.

La compilazione del codice sorgente produce un file binario. Durante la compilazione, puoi fornire flag al compilatore per abilitare o disabilitare determinate proprietà sul binario. Alcune di queste proprietà sono rilevanti per la sicurezza.

Checksec è un piccolo strumento ingegnoso (e uno script di shell) che, tra le altre funzioni, identifica le proprietà di sicurezza integrate in un binario quando è stato compilato. Un compilatore potrebbe abilitare alcune di queste proprietà per impostazione predefinita e potrebbe essere necessario fornire flag specifici per abilitarne altre.

Questo articolo spiega come utilizzare checksec per identificare le proprietà di sicurezza su un binario, tra cui:

  1. I comandi sottostanti utilizzati da checksec per trovare informazioni sulle proprietà di sicurezza
  2. Come abilitare le proprietà di sicurezza utilizzando GNU Compiler Collection (GCC) durante la compilazione di un file binario di esempio

Installa checksec

Per installare checksec su Fedora e altri sistemi basati su RPM, utilizzare:

$ sudo dnf install checksec

Per le distribuzioni basate su Debian, utilizzare il comando apt equivalente.

Lo script della shell

Checksec è uno script di shell a file singolo, anche se piuttosto grande. Un vantaggio è che puoi leggere rapidamente lo script e comprendere tutti i comandi di sistema in esecuzione per trovare informazioni su file binari o eseguibili:

$ 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

Prendi checksec per un giro con un binario che probabilmente esegui quotidianamente: l'onnipresente comando ls. Il formato del comando è checksec --file= seguito dal percorso assoluto del binario ls:

$ 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

Quando lo esegui in un terminale, vedi una codifica a colori che mostra cosa è buono e cosa probabilmente non lo è. Dico "probabilmente" perché anche se qualcosa è in rosso, non significa necessariamente che le cose siano orribili: potrebbe semplicemente significare che i fornitori della distribuzione hanno fatto alcuni compromessi durante la compilazione dei file binari.

La prima riga fornisce varie proprietà di sicurezza che solitamente sono disponibili per i binari, come RELRO, STACK CANARY, NX e così via (lo spiego in dettaglio sotto). La seconda riga mostra lo stato di queste proprietà per il file binario specificato (ls, in questo caso). Ad esempio, NX abilitato significa che alcune proprietà sono abilitate per questo binario.

Un esempio binario

Per questo tutorial, utilizzerò il seguente programma "hello world" come esempio binario.

#include <stdio.h>

int main()
{
	printf("Hello World\n");
	return 0;
}
 

Tieni presente che non ho fornito a gcc alcun flag aggiuntivo durante la compilazione:

$ 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

Esegui il file binario tramite checksec. Alcune proprietà sono diverse rispetto al comando ls sopra (sullo schermo potrebbero essere visualizzate in rosso):

$ 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
$

Modifica del formato di output

Checksec consente vari formati di output, che puoi specificare con --output. Sceglierò il formato JSON e collegherò l'output all'utilità jq per una stampa gradevole.

Per proseguire, assicurati di avere jq installato perché questo tutorial utilizza questo formato di output per eseguire rapidamente il grep per proprietà specifiche dall'output e riportare yes o no su ciascuno:

$ 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"
  }
}

Passeggiando tra le proprietà di sicurezza

Il binario sopra include diverse proprietà di sicurezza. Confronterò quel binario con il binario ls sopra per esaminare cosa è abilitato e spiegare come checksec ha trovato queste informazioni.

1. Simboli

Inizierò prima con quello facile. Durante la compilazione, alcuni simboli vengono inclusi nel file binario, principalmente per il debug. Questi simboli sono necessari quando si sviluppa software e sono necessari più cicli per il debug e la correzione delle cose.

Questi simboli vengono solitamente rimossi (rimossi) dal file binario finale prima che venga rilasciato per uso generale. Ciò non influisce in alcun modo sull'esecuzione del binario; funzionerà proprio come con i simboli. La rimozione viene spesso eseguita per risparmiare spazio, poiché il codice binario è leggermente più leggero una volta rimossi i simboli. Nel software closed source o proprietario, i simboli spesso vengono rimossi perché la loro presenza in un codice binario rende piuttosto semplice dedurre il funzionamento interno del software.

Secondo checksec, i simboli sono presenti in questo binario, ma non erano nel binario ls. Puoi trovare queste informazioni anche eseguendo il comando file sul programma: vedrai not stripped nell'output verso la fine:

$ 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

Come ha fatto checksec a trovare queste informazioni? Bene, fornisce una comoda opzione --debug per mostrare quali funzioni sono state eseguite. Pertanto, l'esecuzione del seguente comando dovrebbe mostrarti quali funzioni sono state eseguite all'interno dello script di shell:

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

In questo tutorial, cerco i comandi sottostanti utilizzati per trovare queste informazioni. Poiché si tratta di uno script di shell, puoi sempre utilizzare le funzionalità di Bash. Questo comando restituirà ogni comando eseguito dallo script di shell:

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

Se scorri l'output, dovresti vedere un echo_message seguito dalla categoria della proprietà di sicurezza. Ecco cosa segnala checksec sulla presenza di simboli nel file binario:

+ readelf -W --symbols ./hello
+ grep -q '\.symtab'
+ echo_message '\033[31m96) Symbols\t\033[m  ' Symbols, ' symbols="yes"' '"symbols":"yes",'

Per semplificare ciò, checksec utilizza l'utilità readelf per leggere il codice binario e fornisce uno speciale flag --symbols che elenca tutti i simboli all'interno del codice binario. Quindi cerca un valore speciale, .symtab, che fornisce il conteggio delle voci (simboli) trovate. Puoi provare i seguenti comandi sul binario di test che hai compilato sopra:

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

Come eliminare i simboli

È possibile eliminare i simboli dopo la compilazione o durante la compilazione.

  • Post compilazione: dopo la compilazione, puoi utilizzare l'utilità strip sul binario per rimuovere i simboli. Conferma che ha funzionato utilizzando il comando file, che ora mostra l'output come stripped:

    $ 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
    $ 

Come rimuovere i simboli durante la compilazione

Invece di rimuovere manualmente i simboli dopo la compilazione, puoi chiedere al compilatore di farlo per te fornendo l'argomento -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, stripped
$ 

Dopo aver eseguito nuovamente checksec, puoi vedere che i symbols vengono visualizzati come no:

$ checksec --file=./hello --output=json | jq | grep symbols
    "symbols": "no",
$ 

2. Canarino

I canarini sono valori noti che vengono inseriti tra un buffer e i dati di controllo sullo stack per monitorare gli overflow del buffer. Quando un'applicazione viene eseguita, le vengono assegnati due tipi di memoria.  Uno di questi è uno stack, che è semplicemente una struttura dati con due operazioni: push, che mette i dati nello stack, e pop, che rimuove i dati dallo stack in ordine inverso. Un input dannoso potrebbe traboccare o corrompere lo stack con input appositamente predisposti e causare l'arresto anomalo del programma:

$ checksec --file=/bin/ls --output=json | jq | grep canary
    "canary": "yes",
$ 
$ checksec --file=./hello --output=json | jq | grep canary
    "canary": "no",
$ 

Come fa checksec a scoprire se il binario è abilitato con un canary? Utilizzando il metodo sopra, puoi restringere il campo eseguendo il seguente comando all'interno dello script di shell:

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

Abilita canarino

Per proteggersi da questi casi, il compilatore fornisce il flag -stack-protector-all, che aggiunge codice extra al binario per verificare tali buffer overflow:

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

$ checksec --file=./hello --output=json | jq | grep canary
    "canary": "yes",

Checksec mostra che la proprietà è ora abilitata. Puoi verificarlo anche 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. TORTA

PIE sta per eseguibile indipendente dalla posizione. Come suggerisce il nome, è il codice che viene inserito da qualche parte nella memoria per l'esecuzione indipendentemente dal suo indirizzo assoluto:

$ checksec --file=/bin/ls --output=json | jq | grep pie
    "pie": "yes",

$ checksec --file=./hello --output=json | jq | grep pie
    "pie": "no",

Spesso PIE è abilitato solo per le librerie e non per i programmi a riga di comando autonomi. Nell'output seguente, hello viene mostrato come LSB eseguibile, mentre la libreria standard libc (.so) il file è contrassegnato come oggetto condiviso LSB:

$ 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 tenta di trovare queste informazioni con:

$ readelf -W -h ./hello | grep EXEC
  Type:                              EXEC (Executable file)

Se provi lo stesso comando su una libreria condivisa invece di EXEC, vedrai un DYN:

$ readelf -W -h /lib64/libc-2.32.so | grep DYN
  Type:                              DYN (Shared object file)

Abilita TORTA

Per abilitare PIE su un programma di test, inviare i seguenti argomenti al compilatore:

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

Puoi verificare che PIE sia abilitato utilizzando checksec:

$ checksec --file=./hello --output=json | jq | grep pie
    "pie": "yes",
$ 

Dovrebbe essere visualizzato come eseguibile PIE con il tipo modificato da 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 sta per "non eseguibile". Spesso è abilitato a livello di CPU, quindi un sistema operativo con NX abilitato può contrassegnare alcune aree di memoria come non eseguibili. Spesso gli exploit buffer-overflow mettono il codice nello stack e poi provano ad eseguirlo. Tuttavia, rendendo quest'area scrivibile non eseguibile è possibile prevenire tali attacchi. Questa proprietà è abilitata per impostazione predefinita durante la normale compilazione utilizzando gcc:

$ checksec --file=/bin/ls --output=json | jq | grep nx
    "nx": "yes",

$ checksec --file=./hello --output=json | jq | grep nx
    "nx": "yes",

Checksec determina queste informazioni con il comando seguente. RW verso la fine significa che lo stack è leggibile e scrivibile; poiché non esiste E, non è eseguibile:

$ readelf -W -l ./hello | grep GNU_STACK
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x10

Disabilita NX a scopo dimostrativo

Non è consigliato, ma puoi disabilitare NX durante la compilazione di un programma utilizzando l'argomento -z execstack:

$ gcc -z execstack hello.c -o hello

$ checksec --file=./hello --output=json | jq | grep nx
    "nx": "no",

Dopo la compilazione, lo stack diventa eseguibile (RWE), che consente l'esecuzione di codice dannoso:

$ readelf -W -l ./hello | grep GNU_STACK
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RWE 0x10

5. RELAZIONE

RELRO sta per rilocazione di sola lettura. Un binario ELF (Executable Linkable Format) utilizza una Global Offset Table (GOT) per risolvere le funzioni in modo dinamico. Se abilitata, questa proprietà di sicurezza rende il GOT all'interno del binario di sola lettura, impedendo alcune forme di attacchi di riposizionamento:

$ checksec --file=/bin/ls --output=json | jq | grep relro
    "relro": "full",

$ checksec --file=./hello --output=json | jq | grep relro
    "relro": "partial",

Checksec trova queste informazioni utilizzando il comando seguente. Qui, una delle proprietà RELRO è abilitata; pertanto, il binario mostra "parziale" durante la verifica tramite checksec:

$ readelf -W -l ./hello | grep GNU_RELRO
  GNU_RELRO      0x002e10 0x0000000000403e10 0x0000000000403e10 0x0001f0 0x0001f0 R   0x1

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

Abilita RELRO completo

Per abilitare RELRO completo, utilizzare i seguenti argomenti della riga di comando durante la compilazione con gcc:

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

$ checksec --file=./hello --output=json | jq | grep relro
    "relro": "full",

Ora è abilitata anche la seconda proprietà, rendendo il programma completamente 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. Fortificare

Fortify è un'altra proprietà di sicurezza, ma non rientra nell'ambito di questo articolo. Lascerò imparare come checksec verifica la fortificazione nei file binari e come è abilitato con gcc come esercizio da affrontare.

$ 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"

Altre funzionalità di checksec

L'argomento della sicurezza è infinito e, sebbene non sia possibile trattare tutto qui, voglio menzionare alcune altre funzionalità del comando checksec con cui è un piacere lavorare.

Esegui contro più file binari

Non è necessario fornire ciascun file binario a checksec individualmente. Invece, puoi fornire un percorso di directory in cui risiedono più file binari e checksec li verificherà tutti per te in una volta sola:

$ checksec --dir=/usr/bin

Processi

Oltre ai binari, checksec funziona anche sui programmi durante l'esecuzione. Il comando seguente trova le proprietà di sicurezza di tutti i programmi in esecuzione sul sistema. Puoi utilizzare --proc-all se vuoi che controlli tutti i processi in esecuzione oppure puoi selezionare un processo specifico utilizzando il suo nome:

$ checksec --proc-all

$ checksec --proc=bash

Proprietà del nocciolo

Oltre alle applicazioni userland di checksec descritte in questo articolo, puoi anche usarlo per controllare le proprietà del kernel integrate nel tuo sistema:

$ checksec --kernel

Provaci

Checksec è un buon modo per capire quali proprietà dello spazio utente e del kernel sono abilitate. Esamina dettagliatamente ciascuna proprietà di sicurezza e cerca di comprendere i motivi per abilitare ciascuna funzionalità e i tipi di attacchi che previene.