
Fuzzer per driver del kernel Linux
Questo repository contiene tutti i sorgenti (inclusi gli script di configurazione) necessari per mettere in funzione difuze.
Ubuntu >= 14.04.5 LTS
Fare riferimento al readme
Come spiegato nel nostro articolo, ci sono due componenti principali di difuze: Interface Recovery e Fuzzing Engine
Il meccanismo di recupero dell'interfaccia si basa su passi di analisi LLVM. Ogni fase del recupero dell'interfaccia è scritta come passi individuali. Segui le istruzioni seguenti per mettere in funzione il Recupero dell'Interfaccia.
Questo passaggio si occupa di installare LLVM e c2xml:
Prima, assicurati di avere libxml (richiesto per c2xml):
sudo apt-get install libxml2-dev
sudo pip install lxml
Successivamente, abbiamo creato un singolo script che scarica e compila tutti gli strumenti necessari.
cd helper_scripts
python setup_difuze.py --help
usage: setup_difuze.py [-h] [-b TARGET_BRANCH] [-o OUTPUT_FOLDER]
optional arguments:
-h, --help mostra questo messaggio di aiuto ed esci
-b TARGET_BRANCH Branch (cioè versione) di LLVM da configurare. Default: release_38 ad esempio release_38
-o OUTPUT_FOLDER Cartella dove tutto deve essere configurato.
Esempio:
python setup_difuze.py -o difuze_deps
Per completare la configurazione devi anche modificare la variabile d'ambiente PATH locale. Lo script di configurazione ti darà le modifiche esatte da fare.
Questo dipende dal completamento con successo della Configurazione. Abbiamo un singolo script che compila tutto, prego.
cd InterfaceHandlers
./build.sh
Questo dipende dal completamento con successo della Compilazione. Per eseguire i componenti di Recupero dell'Interfaccia sui driver del kernel, dobbiamo prima i driver in bitcode LLVM.
Prima, abbiamo bisogno di un kernel compilabile. Significa che dovresti essere in grado di compilare il kernel usando la normale configurazione di compilazione, cioè make.
Prima catturiamo l'output del comando make, da questo output estraiamo il comando di compilazione esatto.
makebear make <all the options to make>
bear make -j8Questo genererà un file compile_commands.json nella directory corrente.
Basta passare V=1 e reindirizzare l'output al file.
Esempio:
make V=1 O=out ARCH=arm64 > makeout.txt 2>&1
NOTA: NON USARE PROCESSI MULTIPLI cioè -j. Eseguire in modalità multi-processo rovinerà il file di output poiché più processi cercano di scrivere sul file di output.
Tutto qui. Successivamente, nel passaggio seguente il nostro script prende il makeout.txt generato ed esegue il Recupero dell'Interfaccia su tutti i driver riconosciuti.
Tutti i vari passaggi del Recupero dell'Interfaccia sono racchiusi in un unico script helper_scripts/run_all.py
Come eseguire:
cd helper_scripts
python run_all.py --help
usage: run_all.py [-h] [-l LLVM_BC_OUT] [-a CHIPSET_NUM] [-m MAKEOUT]
[-c COMPJSON] [-g COMPILER_NAME] [-n ARCH_NUM] [-o OUT]
[-k KERNEL_SRC_DIR] [-isclang] [-clangp CLANG_PATH]
[-llvmlinkp LLVMLINK_PATH] [-skb] [-skl] [-skp] [-skP]
[-ske] [-skI] [-ski] [-skv] [-skd] [-f IOCTL_FINDER_OUT]
optional arguments:
-h, --help mostra questo messaggio di aiuto ed esci
-l LLVM_BC_OUT Directory di destinazione dove devono essere memorizzati tutti i file bitcode generati.
-a CHIPSET_NUM Numero del chipset. I numeri validi sono: 1(mediatek)|2(qualcomm)|3(huawei)|4(samsung)
-m MAKEOUT Percorso del file makeout.txt.
-c COMPJSON Percorso del file compile_commands.json generato da Bear.
-g COMPILER_NAME Nome del compilatore usato in makeout.txt, necessario per filtrare i comandi di compilazione. Es: aarch64-linux-android-gcc
-n ARCH_NUM Architettura di destinazione, 32 bit (1) o 64 bit (2).
-o OUT Percorso della cartella out. Questa è la cartella che potrebbe essere usata come directory di output durante la compilazione di alcuni kernel.
-k KERNEL_SRC_DIR Directory base dei sorgenti del kernel.
-isclang flag per indicare che clang è stato usato per compilare il kernel
-clangp CLANG_PATH Percorso assoluto del binario clang (se non fornito, verrà usato quello disponibile nel PATH)
-llvmlinkp LLVMLINK_PATH Percorso assoluto del binario llvm-link (se non fornito, verrà usato quello disponibile nel PATH)
-skb Salta la compilazione LLVM (default: non saltato).
-skl Salta Dr Linker (default: non saltato).
-skp Salta il parsing degli header (default: non saltato).
-skP Salta la generazione dei file preprocessati (default: non saltato).
-ske Salta l'identificazione dei punti di ingresso (default: non saltato).
-skI Salta la generazione degli include (default: non saltato).
-ski Salta l'esecuzione di IoctlCmdParser (default: non saltato).
-skv Salta l'elaborazione degli ioctl V4L2 (default: non saltato).
-skd Salta il finder del nome del dispositivo (default: non saltato).
-f IOCTL_FINDER_OUT Percorso della cartella di output dove deve essere memorizzato l'output dell'ioctl command finder.
Lo script compila, collega ed esegue il Recupero dell'Interfaccia su tutti i driver riconosciuti, quindi potrebbe richiedere tempo considerevole (45 min-90 min).
Lo script sopra esegue i seguenti compiti in modalità multiprocessore per utilizzare tutti i core della CPU:
Tutti i file bitcode generati verranno posizionati nella cartella fornita all'argomento -l.
Questo passaggio richiede tempo considerevole, a seconda del numero di core che hai.
Quindi, se hai già eseguito questo passaggio, puoi saltarlo passando -skb.
Questo esegue il collegamento, passa attraverso tutti i file bitcode e identifica i file bitcode correlati che devono essere collegati e li collega (usando llvm-link) in un file bitcode consolidato (che verrà memorizzato accanto al file bitcode corrispondente).
Simile al passaggio precedente, puoi saltare questo passaggio passando -skl.
Questo passaggio cerca le dichiarazioni dei punti di ingresso nei file header e memorizza la loro configurazione nel file: hdr_file_config.txt sotto la directory di compilazione LLVM.
Per saltare: -skp
Questo passaggio identifica tutti i punti di ingresso in tutti i file bitcode consolidati dei driver.
L'output verrà memorizzato nel file: entry_point_out.txt sotto la directory di compilazione LLVM.
Esempio di contenuto nel file entry_point_out.txt:
IOCTL:msm_lsm_ioctl:/home/difuze/kernels/pixel/msm/sound/soc/msm/qdsp6v2/msm-lsm-client.c:msm_lsm_ioctl.txt:/home/difuze/pixel/llvm_out/sound/soc/msm/qdsp6v2/llvm_link_final/final_to_check.bc
IOCTL:msm_pcm_ioctl:/home/difuze/kernels/pixel/msm/sound/soc/msm/qdsp6v2/msm-pcm-lpa-v2.c:msm_pcm_ioctl.txt:/home/difuze/pixel/llvm_out/sound/soc/msm/qdsp6v2/llvm_link_final/final_to_check.bc
Per saltare: -ske
Questo passaggio eseguirà il componente principale del Recupero dell'Interfaccia (IoctlCmdParser) su tutti i punti di ingresso nel file entry_point_out.txt. L'output per ogni punto di ingresso verrà memorizzato nella cartella fornita per l'opzione -f.
Per saltare: -ski
Ora mostreremo un esempio dal punto in cui hai i sorgenti del kernel fino al punto in cui ottieni i risultati del Recupero dell'Interfaccia.
Abbiamo caricato un kernel mediatek 33.2.A.3.123.tar.bz2. Scarica ed estrai prima il file sopra.
Diciamo che hai estratto il file sopra in una cartella chiamata: ~/mediatek_kernel
Installa Bear e segui i passaggi seguenti:
cd ~/mediatek_kernel
source ./env.sh
cd kernel-3.18
# the following step may not be needed depending on the kernel
mkdir out
make O=out ARCH=arm64 tubads_defconfig
# generating compile_commands.json
bear make -j8 O=out ARCH=arm64
cd <repo_path>/helper_scripts
python run_all.py -l ~/mediatek_kernel/llvm_bitcode_out -a 1 -c ~/mediatek_kernel/kernel-3.18/compile_commands.json -n 2 -o ~/mediatek_kernel/kernel-3.18/out -k ~/mediatek_kernel/kernel-3.18 -f ~/mediatek_kernel/ioctl_finder_out
Il comando sopra richiede abbastanza tempo (30 min - 1 ora).
Prima di tutto, tutti i risultati dell'analisi saranno nella cartella: ~/mediatek_kernel/ioctl_finder_out (argomento fornito all'opzione -f), per ogni punto di ingresso verrà creato un file .txt che contiene tutte le informazioni sull'interfaccia recuperata.
Se sei interessato solo alle informazioni sull'interfaccia e non ti interessa altro, ti consigliamo di usare lo script parse_interface_output.py. Questo script converte l'output disordinato del passaggio di Recupero dell'Interfaccia in bei file json con un formato pulito e coerente.
cd <repo_path>/helper_scripts
python parse_interface_output.py <ioctl_finder_out_dir> <output_directory_for_json_files>
Qui <ioctl_finder_out_dir> dovrebbe essere uguale alla cartella che hai fornito all'opzione -f e <output_directory_for_json_files> è la cartella dove devono essere creati i file json.
Puoi usare i file json corrispondenti per il recupero dell'interfaccia del corrispondente ioctl.
-g (solo se usi makeout.txt)Per fornire il valore per l'opzione -g devi conoscere il nome del binario *-gcc usato per compilare il kernel.
Un modo semplice per saperlo è grep per gcc in makeout.txt e vedrai i comandi del compilatore dai quali puoi conoscere il nome del binario *-gcc.
Per il nostro esempio sopra, se fai grep gcc makeout.txt per la compilazione di esempio, vedrai molte righe come quella sotto:
aarch64-linux-android-gcc -Wp,-MD,fs/jbd2/.transaction.o.d -nostdinc -isystem ...
Quindi, il valore per -g dovrebbe essere aarch64-linux-android-gcc.
Se il kernel da compilare è a 32 bit, molto probabilmente il binario sarà arm-eabi-gcc
Per chipset Qualcomm (o msm), potresti vedere *gcc-wrapper.py invece di *.gcc, nel qual caso dovresti fornire *gcc-wrapper.py.
-aA seconda del tipo di chipset, devi fornire il numero corrispondente.
-oQuesto è il percorso della cartella fornito all'opzione O= per il comando make durante la compilazione del kernel.
Non tutti i kernel necessitano di un percorso out separato. Puoi compilare il kernel senza fornire l'opzione O, in tal caso NON dovresti fornire un valore per quell'opzione durante l'esecuzione di run_all.py.
Per i kernel compilati usando clang, oltre alle opzioni sopra, specifica le seguenti opzioni (supponendo che tu abbia usato compile_commands.json):
-isclang -clangp <PATH_TO_THE_CLANG_USED_TO_BUILD_THE_KERNEL> -llvmlinkp <PATH_TO_THE_LLVM_LINK (will be in the same folder as clang)>
Prima di poter iniziare il fuzzing dobbiamo elaborare un po' l'output con i nostri parser di qualità da ricerca (scusa).
Questi si trovano qui. Lo script principale da eseguire sarà run_all.py:
$ python run_all.py --help
usage: run_all.py [-h] -f F -o O [-n {manual,auto,hybrid}] [-m M]
run_all options
optional arguments:
-h, --help mostra questo messaggio di aiuto ed esci
-f F Nome del file dell'output dell'analisi ioctl OPPURE l'intera directory di output creata dal sistema
-o O Directory di output per salvare i risultati. Se questa directory non esiste verrà creata
-n {manual,auto,hybrid} Specifica le opzioni per il nome del dispositivo. Puoi scegliere manual (specifica ogni nome manualmente), auto (salta tutto ciò per cui non identifichiamo un nome), o hybrid (se abbiamo rilevato un nome, lo usiamo, altrimenti chiediamo all'utente)
-m M Abilita l'output multi-dispositivo. La maggior parte degli ioctl ha un solo nodo dispositivo applicabile, ma alcuni possono averne più di uno. (0 per disabilitare)
Dovrai passare -f con la directory di output dell'analisi ioctl, ad esempio ~/mediatek_kernel/ioctl_finder_out.
-o è dove vuoi salvare i risultati post-elaborati. Questi saranno file XML facilmente digeribili (jpits).
-n Specifica al sistema in che misura vuoi fare affidamento sul nostro recupero del nome del dispositivo.
Se non vuoi fare alcun lavoro/caccia al nome, puoi specificare auto.
Questo ovviamente ha il costo di saltare qualsiasi dispositivo per cui non recuperiamo un nome. Se vuoi essere paranoico e non fidarti di nessuno dei nostri sforzi di recupero (totalmente ragionevole) puoi usare l'opzione manual per nominare tu stesso ogni singolo dispositivo.
hybrid quindi è una combinazione di entrambi: nomineremo il dispositivo per te quando possiamo, e ci affideremo a te quando falliamo.
-m A volte gli ioctl possono corrispondere a più di un dispositivo (questo è comune con gli ioctl v4l2/subdev per esempio). Il supporto per questo è abilitato per impostazione predefinita, ma richiede l'interazione dell'utente per specificare il numero di dispositivi per ogni dispositivo. Se questo è troppo fastidioso per te, puoi disabilitare il prompt passando -m 0 (supporremo un singolo dispositivo per ogni ioctl).
Dopo l'esecuzione, dovresti avere, nella tua cartella out, una cartella per ogni ioctl.
MangoFuzz è il nostro semplice fuzzer prototipo e si basa su Peach (specificamente MozPeach).
Non è un fuzzer particolarmente sofisticato, ma trova bug. È stato anche costruito per essere facilmente espandibile. Ci sono 2 componenti in questo fuzzer, il fuzz engine e l'executor. L'executor si trova qui, e il fuzz engine si trova qui.
L'executor viene eseguito sul telefono, in ascolto dei dati che il fuzz engine gli invierà.
Basta compilarlo per l'architettura del tuo telefono, eseguire adb push sul telefono, ed eseguirlo con la porta su cui vuoi che ascolti!
L'interfacciamento con MangoFuzz è abbastanza semplice. Avrai bisogno di un oggetto Engine e un oggetto Parser, in cui inserirai il tuo engine.
Da qui, fai il parsing dei jpits con il tuo Parser, e poi esegui l'Engine. Facile!
Abbiamo fornito alcuni semplici script di esecuzione per iniziare.
Per eseguire contro driver specifici puoi usare runner.py su una delle cartelle ioctl nella directory di output (creata dai nostri script di post-elaborazione).
e.g. ./runner.py -f honor8/out/chb -num 1000. Questo dice a MangoFuzz di eseguire 1000 iterazioni contro tutte le coppie di valori di comandi ioctl relative all'ioctl/driver chb.
Se invece vogliamo eseguire contro un intero dispositivo (telefono), puoi usare dev_runner.py. e.g. ./dev_runner.py -f honor8/out -num 100.
Questo continuerà a iterare sui file driver, passando casualmente da uno all'altro per 100 iterazioni ciascuno.
Nota che prima che il fuzz engine possa comunicare con il telefono, dovrai usare ADB per impostare il port forwarding, ad esempio adb forward tcp:2022 tcp:2022