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:
- I comandi sottostanti utilizzati da checksec per trovare informazioni sulle proprietà di sicurezza
- 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 comandofile
, che ora mostra l'output comestripped
:$ 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.