Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
blackbox-fuzzing — Fuzzing di dispositivi IoT usando il router TL-WR902AC come esempio | Kitploit
Strumenti/GitHubGitHub/otsmr/blackbox-fuzzing
Sicurezza IoTAnalisi delle VulnerabilitàExploitReverse EngineeringFuzzingAnalisi di BinariPaper e RicercaApprendimento e FormazioneAnalisi del Firmware
GitHubotsmr/blackbox-fuzzing

blackbox-fuzzing

Fuzzing di dispositivi IoT usando il router TL-WR902AC come esempio

1321749 mesi faRevisionato da Kitploit

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi
Vedi RepositorySito web

Blackbox-Fuzzing di dispositivi IoT usando il router TL-WR902AC come esempio

Questa è la versione HTML della mia tesina, che può essere scaricata come PDF qui.

Introduzione

Il fuzzing è diventato "uno dei modi più efficaci" per trovare bug nel software. Con questa o simili affermazioni iniziano molti articoli recenti sul fuzzing [google-scholar]. L'obiettivo principale della nostra precedente tesina sul tema "Internet of Vulnerable Things" era trovare un bug legato alla memoria e poi scrivere un exploit per questa vulnerabilità. Siamo riusciti a trovare una vulnerabilità facendo reverse engineering del firmware, ma non è stato trovato alcun bug legato alla memoria. Trovare un buffer overflow facendo reverse engineering di un binario a mano richiede non solo molto tempo, ma anche molta esperienza. Il fuzzing, allo stesso tempo, mira a essere il "modo più efficace" per trovare tali vulnerabilità legate alla memoria. Google, ad esempio, ha introdotto OSS-Fuzz, che fuzza continuamente software open source e ha già trovato oltre 10.000 vulnerabilità in 1.000 progetti [oss-fuzz].

L'obiettivo di questa tesina è ancora una volta trovare una vulnerabilità legata alla memoria, ma questa volta usando il fuzzing. La vulnerabilità obiettivo dovrebbe essere sfruttabile tramite la rete senza conoscere le credenziali di amministratore. Questo documento descrive il percorso per raggiungere tale obiettivo. A tal fine, la tesina è separata in due parti. La prima parte si concentra su come trovare un bersaglio valido, quali strumenti possono essere usati e come dovrebbe essere composto un buon bersaglio di fuzzing. La seconda parte descrive invece come sviluppare e fare debug di un harness in grado di fuzzare una funzione specifica di un binario. Successivamente, l'harness sviluppato viene usato da AFL++ per fuzzare la funzione obiettivo. Di seguito viene fornito un breve background e qual è lo stato dell'arte attuale per quanto riguarda il fuzzing di dispositivi IoT.

Tutti i file creati nell'ambito di questa tesina sono pubblicati integralmente anche su GitHub e possono essere accessibili tramite il seguente URL: otsmr/blackbox-fuzzing.

Stato dell'arte

Il fuzzing di dispositivi IoT non è facile come fuzzare un progetto open source. Spesso il codice sorgente è proprietario, il che rende impossibile il fuzzing gray-box, che strumenta il codice sorgente per ottenere le migliori prestazioni di fuzzing [afl-persistent]. Inoltre, l'architettura della CPU spesso non è supportata nativamente dai fuzzer, il che richiede un emulatore come QEMU [qemu], che rallenta anche la velocità di fuzzing [afl-persistent]. Un altro problema sono le periferiche hardware, che complicano lo sviluppo di un approccio generale. L'articolo "Embedded Fuzzing: A Review of Challenges, Tools, and Solutions" [embedded-fuzzing] fornisce una panoramica delle diverse strategie di fuzzing, come l'embedded fuzzing basato su hardware. La maggior parte di queste strategie richiede il codice sorgente del programma target, ad esempio quando si porta il codice sorgente dei fuzzer, come AFL, su dispositivi IoT basati su ARM per eseguire il fuzzer sull'hardware IoT. Eseguire il fuzzer sull'hardware del dispositivo presenta anche problemi di prestazioni, perché spesso hanno CPU di fascia bassa, più lente delle normali CPU desktop. Un altro approccio presentato in questo articolo è l'embedded fuzzing basato su emulazione, in cui viene eseguito in un emulatore un singolo programma target per effettuare un fuzzing guidato dalla copertura, oppure l'intero sistema.

Gli approcci sopra menzionati prendono tutti di mira direttamente un binario utilizzando un emulatore o strumentando il codice sorgente. Questi approcci richiedono una configurazione di fuzzing che spesso deve essere creata appositamente per un singolo dispositivo IoT e sono difficili da generalizzare. Per questo, i ricercatori hanno creato un programma IoTFuzzer che si propone come framework di fuzzing automatizzato con l'obiettivo di "trovare vulnerabilità di corruzione della memoria senza accesso alle relative immagini del firmware [iotfuzzer]." IoTFuzzer si basa sull'osservazione che la maggior parte dei dispositivi IoT ha un'app mobile per controllarli, e queste app contengono informazioni sul protocollo usato per comunicare con il dispositivo. Il programma identifica e riutilizza quindi la logica specifica del programma per mutare i casi di test e testare efficacemente i target IoT [iotfuzzer].

Contesto

Harness

Un harness descrive una sequenza di chiamate API che elaborano gli input forniti dal fuzzer. A differenza di una normale applicazione, che spesso non necessita di un harness, una libreria che implementa funzioni riutilizzabili deve essere chiamata con i parametri corretti e anche nella sequenza giusta, così da poter gestire lo stato tra più chiamate a funzioni condivise. Fuzzare casualmente la libreria senza costruire la macchina a stati difficilmente avrà successo e, al contrario, creerà molti crash falsi positivi quando le dipendenze della libreria non vengono rispettate. Questo può accadere, ad esempio, quando un controllo sulla dimensione del buffer viene saltato dal fuzzer, provocando un buffer overflow spurio.

In questo documento verranno fuzzate applicazioni normali, ma a causa delle dipendenze hardware legate all'uso di socket e del multi-threading, dobbiamo creare un harness anche per loro. L'harness viene caricato nel contesto del binario e può chiamare funzioni interne del programma target, come mostrato nel Codice 10.

Corpus

Il termine "corpus" descrive campioni di input validi o casi di test e funge da riferimento di base per generare nuovi dati di input durante il processo di fuzzing. Nel Codice 10 potrebbe essere, ad esempio, una richiesta HTTP. I fuzzer sfruttano quindi questo corpus per creare casi di test mutati o diversificati, aiutando nell'individuazione di vulnerabilità software attraverso l'esplorazione di vari scenari di input.

Trovare un bersaglio valido

La parte più dispendiosa in termini di tempo del black box fuzzing è trovare una funzione potenzialmente vulnerabile nel firmware. Il primo passo è trovare binari interessanti che, ad esempio, siano accessibili tramite la rete, utilizzino funzioni non sicure o non abbiano funzionalità di sicurezza come lo stack canary abilitato, che costituisce una protezione contro i buffer overflow. Il nostro precedente articolo ([iovt]) ha già descritto come estrarre il firmware dal router preso di mira e come trovare un binario potenzialmente pericoloso. Per questo è stato utilizzato lo strumento EMBA [emba]. EMBA classifica tutti i binari trovati nel firmware in base al numero di funzioni non sicure come strcpy, all'accesso di rete e alle protezioni di sicurezza come lo stack canary o il NX-Bit, che diventano interessanti quando si sfrutta un buffer overflow, come si può vedere nel Codice 1.

```txt [+] STRCPY - top 10 results: 235 : libcmm.so : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | 77 : wscd : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | Networking | [snip] 28 : httpd : common linux file: yes | RELRO | No Canary | NX enabled | No Symbols | Networking | 27 : cli : common linux file: no | No RELRO | No Canary | NX disabled | No Symbols | No Networking | ```

Codice 1: risultato di EMBA relativo a usi non sicuri della funzione strcpy.

Poiché l'obiettivo di questo articolo è trovare una vulnerabilità di memoria che possa essere sfruttata tramite la rete senza conoscere le credenziali dell'amministratore, la funzione vulnerabile deve essere chiamabile tramite la rete e dovrebbe interagire direttamente con l'input fornito dall'utente. Ma avere un'interazione di rete non significa che il binario sia anche direttamente accessibile tramite la rete. Per scoprire quali binari sono in ascolto, possiamo usare la shell root UART, già stabilita in [iovt].

```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```

Codice 2: Utilizzo della shell root UART per eseguire netstat

Reversing del binario

Il primo binario che sembra promettente è wscd. Il binario ha le chiamate strcpy più insicure (eccetto che per la libreria libcmm.so) e l'interazione di rete, che nel caso di wscd significa che si collega a un dispositivo UPnP e non ascolta su una porta specifica. Ha, come mostrato in seguito, una funzione facile da fuzzare, motivo per cui questo binario è stato scelto come esempio in questo documento per spiegare la procedura generale. Prima del reversing, possiamo usare la shell root UART per scoprire se il binario è in esecuzione e come è stato avviato.

```txt $ ps PID USER VSZ STAT COMMAND 962 admin 1096 S wscd -i ra0 -m 1 -w /var/tmp/wsc_upnp/ 1018 admin 1080 S wscd_5G -i rai0 -m 1 -w /var/tmp/wsc_upnp_5G/ ```

Codice 3: Utilizzo del comando ps per visualizzare tutti i programmi in esecuzione.

Con ps non vediamo solo che il binario è in esecuzione, ma anche quali sono gli argomenti, i quali sono importanti per verificare se una potenziale funzione viene effettivamente chiamata. Il significato di questi argomenti può essere ricavato dalla guida della CLI, che viene visualizzata quando si invoca il binario senza alcun argomento.

```txt $ chroot root /qemu-mipsel-static /usr/bin/wscd Usage: wscd [-i infName] [-a ipaddress] [-p port] [-f descDoc] [-w webRootDir] -m UPnPOpMode -D [-d debugLevel] -h -i: Interface name this daemon will run wsc protocol(if not set, will use the default interface name - ra0) e.g.: ra0 -w: Filesystem path where descDoc and web files related to the device are stored e.g.: /etc/xml/ -m: UPnP system operation mode 1: Enable UPnP Device service(Support Enrolle or Proxy functions) 2: Enable UPnP Control Point service(Support Registratr function) 3: Enable both UPnP device service and Control Point services. [...] ```

Codice 4: Opzioni del binario wscd.

Come mostrato in Codice 4 wscd viene avviato con "Enabled UPnP Device service", il che sembra promettente. Dopo aver verificato che il binario è effettivamente in esecuzione sul router, il binario può essere analizzato utilizzando Ghidra per cercare funzioni sospette. Per il fuzzing, le funzioni di parsing sono particolarmente interessanti perché di solito sono complesse e spesso l'input analizzato contiene campi di lunghezza per i dati contenuti, come nel caso del pacchetto TCP che contiene la lunghezza del payload.

Figura 1: Utilizzo di Ghidra per cercare funzioni di parsing.

Un altro vantaggio delle funzioni di parsing è che spesso non interagiscono con altre parti del codice né richiedono interazione con l'utente attraverso la rete. Pertanto la funzione di parsing può essere chiamata direttamente con l'input senza modificare il binario o sovrascrivere altre funzioni, così la funzione può essere sottoposta a fuzzing.

Prima di iniziare il fuzzing della funzione, è necessario verificare se la funzione viene effettivamente attivata, perché la funzione è interessante solo quando viene chiamata con un input controllato dall'utente. A questo scopo, Ghidra può essere utilizzato per cercare riferimenti alla funzione target. Nel caso della funzione parser_parse ci sono più modi. Poiché sappiamo come viene avviato il programma, le chiamate possono essere ridotte a un singolo albero di chiamate di funzione, come mostrato in Codice 5.

```c main() if ((WscUPnPOpMode & 1) != 0) // Argument -m 1 WscUPnPDevStart() UpnpDownloadXmlDoc() -> my_http_Download() -> http_Download() if (http_MakeMessage()) http_RequestAndResponse() http_RecvMessage() ```

Codice 5: Albero delle chiamate della funzione parser_parse

Dopo aver trovato una funzione target, possiamo ora creare un setup di fuzzing per testare la funzione, che viene descritto nella parte successiva. Ma prima vengono presentate altre funzioni potenzialmente vulnerabili.

Altre potenziali funzioni vulnerabili

Per questo articolo, più binari potenzialmente vulnerabili sono stati analizzati manualmente alla ricerca di funzioni sospette. La seguente è una breve sintesi di altri possibili target trovati.

Il binario httpd è il backend per l'interfaccia web di amministrazione. Il binario è accessibile via rete sulla porta 80. Una funzione interessante in httpd è la funzione httpd_parser_main. Durante l'analisi dell'implementazione del parser con Ghidra sono state identificate diverse parti di codice sospette. Una delle parti sospette è l'analisi del Content-Type. Nella seguente sezione si può trovare una richiesta HTTP di base.```txt POST / HTTP/1.1\r\n Content-Type: multipart/form-data; boundary=X;\r\n Host: example.com\r\n \r\n \r\n DATA\r\n

root@kitploit:~
Di seguito è riportato uno snippet della funzione `httpd_parser_main` che analizza il `Content-Type` dalla
richiesta http fornita dall'utente.


<div id="c6"></div>```c
// user_input_ptr points to
//  "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");

if (user_input_ptr == cursor) {
 cursor = strstr(user_input_ptr,"boundary=");
 user_input_ptr = cursor + 9;

 // user_input_ptr points now to "X;\r\nHost: example.com\r\n..."

 if (cursor != (char *)0x0) {

  do {
    while (cursor = user_input_ptr, *cursor == " ") {
      user_input_ptr = cursor + 1;
    }
    user_input_ptr = cursor + 1;
  } while (*cursor == "\t");

  // cursor points now to "X;\r\nHost: example.com\r\n..."

  // strchr returns a pointer to the first occurrence of ";" in the user request.
  // If ";" is not found, the function returns a null pointer.
  user_input_ptr = strchr(cursor, ";");
  if (user_input_ptr != (char *)0x0) {
    // The character ";" is replaced by an null byte to terminate the string
    *user_input_ptr = "\0";
    // cursor points now to "X\0\r\nHost: example.com\r\n..."
  }

  // DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
  strcpy(&DAT_00444050, cursor);
  // DAT_00444050 contains now "X"
 }
}

Codice 6: Albero di chiamata della funzione parser_parse

La vulnerabilità in questo codice è la chiamata alla funzione strcpy e il presupposto che Content-Type termini con un punto e virgola. Poiché strcpy copia il buffer fino al successivo byte nullo, e come mostrato in Codice 6 il byte nullo viene aggiunto solo quando viene trovato un punto e virgola. Rimuovendo il punto e virgola, il successivo byte nullo si trova alla fine del buffer di input, ad esempio alla fine della richiesta HTTP. Quindi la variabile globale DAT_00444050 può andare in overflow, andando poi a sovrascrivere dati oltre l'indirizzo 0x0044414f. La parte difficile non è solo trovare una variabile globale interessante oltre questo indirizzo che possa essere sovrascritta, ma anche il fatto che non possono essere usati byte nulli a causa di strcpy. Ma quando c'è un errore del genere, probabilmente ce ne sono altri da trovare.

Il binario tdpd è usato dall'app mobile ed è accessibile via UDP sulla rete locale. tdpd ha quasi le stesse funzioni di tmpd, che per lo più non vengono mai chiamate. La funzione principale ascolta solo i messaggi sulla porta UDP e risponde sempre con informazioni di base sul router, come il nome o il modello. Non c'è quasi alcuna interazione con l'input fornito dall'utente, che quindi non è interessante da fuzzare.

Un'altra interessante coppia di binari è upnpd e ushare. Entrambi i binari gestiscono messaggi UPnP che quindi richiedono di fare il parsing di XML. Poiché è possibile trovare una stringa di copyright nel binario, si può presumere che questi programmi non siano stati sviluppati da TP-Link.```sh $ strings usr/bin/ushare | grep "(C)" Benjamin Zores (C) 2005-2007, for GeeXboX Team.

root@kitploit:~
Entrambi i binari caricano le librerie condivise `libupnp.so` e `libixml.so` che hanno le stesse
funzioni del progetto open source `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/). Poiché
l'obiettivo di questo documento è il fuzzing black box, questi binari vengono ignorati. Tuttavia, il fuzzing gray box di questa
libreria potrebbe avere potenziale, dato che nel 2021 è stata trovata una perdita di memoria in `libixml.so`
[\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249).

Il binario **tmpd** è il backend dell'app mobile. La parte interessante è che il router e
l'app mobile comunicano tramite un protocollo binario personalizzato. Di seguito viene mostrato un messaggio dal
client al server.

<div id="c7"></div>```txt
00000000  01 00 05 00 00 08 00 00  00 00 00 17 50 7b 6e fe  |............P{n.|
00000010  01 01 02 00 00 00 00 00                           |........        |

Codice 7: Messaggio dall'app mobile al router.

Per comprendere il protocollo binario, il binario tmpd è stato sottoposto a reverse engineering utilizzando Ghidra. Con queste informazioni, il messaggio in Codice 7 può essere scomposto come segue:```txt 01 00 05 00 : Version 00 08 00 00 : Size (8 Bytes) 00 00 00 17 : Datatype 50 7b 6e fe : Checksum (CRC32) 01 01 : Options 02 00 : Function id 00 00 00 00 : Function parameters

root@kitploit:~
<p class="text-align: center">Codice 8: Protocollo binario personalizzato scomposto.</p>

Questo sembra promettente perché tali protocolli binari devono essere analizzati. Ma la parte più sospetta del protocollo binario non è il campo di lunghezza, ma l'uso dell'ID di funzione e dei parametri di funzione.


<figure id="f2">
  <p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
  <figcaption>
    <p style="text-align: center">Figura 2: Funzione decompilata da tmpd che analizza l'ID di funzione e i relativi parametri.</p>
  </figcaption>
</figure>

[Figura 2](#f2) mostra una parte della funzione parser decompilata del protocollo personalizzato. Alla riga 16, l'ID di funzione viene estratto, e la funzione corrispondente viene poi chiamata alla riga 29. Il comportamento sospetto è che la funzione viene chiamata con parametri estratti senza alcun controllo dal buffer di input controllato dall'utente. Potremmo ora provare a trovare una funzione nella tabella di salto mostrata in [Figura 3](#f3) dove questo potrebbe essere pericoloso, come quando il parametro viene usato per indicizzare un buffer o interpretato come stringa. Invece di eseguire manualmente il reverse engineering e cercare tra le oltre 100 funzioni, il che richiederebbe tempo, possiamo usare un fuzzer che farebbe questo automaticamente.

<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">Figura 3: Funzione decompilata da tmpd che analizza l'ID di funzione
e i suoi parametri.</p></figcaption>
</figure>

Purtroppo, il binario `tmpd` è raggiungibile solo localmente tramite la rete, come mostrato nel [Codice 2](#c2). Per connettersi a questo binario, l'app si connette prima al router via SSH in modalità `direct-tcpip`, che inoltra semplicemente i pacchetti al processo locale. E la connessione SSH è protetta dalle credenziali dell'amministratore. Ma come descritto in [\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf), la connessione SSH può essere facilmente compromessa perché la chiave host del server non viene mai verificata dall'app. Scartando ogni pacchetto instradato verso internet, l'amministratore può essere indotto ad accedere al router mentre viene eseguito un attacco man-in-the-middle per rubare le credenziali.

## Fuzzing con AFL++ e QEMU

In questa sezione, viene sviluppato un harness che ha come target una delle funzioni trovate in precedenza. Dopo lo sviluppo dell'harness, viene utilizzato il fuzzer all'avanguardia AFL++ [\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) per eseguire il fuzzing sulla funzione target. Poiché i binari sono compilati per l'architettura `mipsel`, si utilizza l'emulatore QEMU per eseguire il binario. La configurazione di fuzzing di base usata in questo articolo è in gran parte ispirata al post del blog "Firmware Fuzzing 101" di Adam Van Prooyen [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101).

### Ambiente di fuzzing 

Per creare facilmente un ambiente di fuzzing riproducibile, Docker è la scelta migliore. Abbiamo creato un Dockerfile che installa tutti gli strumenti necessari, come un cross-compiler per l'architettura CPU `mipsel` o `gdb-multiarch`, che può essere usato per eseguire il debug dell'harness.

Inoltre, AFLplusplus viene scaricato e compilato insieme a QEMU, che viene costruito in una versione con piccole modifiche per consentire l'esecuzione di binari non instrumentati sotto afl-fuzz.```docker
FROM debian:latest

RUN apt update && apt install -y \
      curl \
      vim \
      gcc-mipsel-linux-gnu \
      openssh-server \
      qemu-user-static \
      gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static

# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh

RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh

WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]

Dockerfile che installa gli strumenti necessari.

L'immagine può quindi essere creata utilizzando docker build.```sh docker build -t fuzz .

root@kitploit:~
Quando l'immagine viene creata, può essere facilmente utilizzata con `docker run`, che
avvia poi il container.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz

Using the option -d will start the container in the background. With docker exec multiple shells can be started inside the container, which is helpful to start the executable in one session using QEMU and in the other session gdb-multiarch.```sh docker exec -it fuzz /bin/bash

root@kitploit:~
### Sovrascrivere la funzione main

Nella sezione precedente è stato identificato un potente bersaglio di fuzzing. Il problema è che durante l'esecuzione del binario non si raggiungerà mai la chiamata di funzione, perché la funzione `parser_parse` viene chiamata solo se un pacchetto TCP viene ricevuto su un socket. Questo sarebbe non solo negativo per le prestazioni, ma anche difficile da configurare. Per questo motivo il punto di ingresso del fuzzer dovrebbe trovarsi in una posizione diversa dalla normale funzione main. A questo scopo può essere usata la variabile d'ambiente `LD_PRELOAD`, che consente di iniettare un harness con accesso alle funzioni interne. Come descrive la pagina man di `ld.so`, responsabile del collegamento delle librerie condivise necessarie a un eseguibile in fase di esecuzione, `LD_PRELOAD` può essere utilizzata "per sovrascrivere selettivamente funzioni in altri oggetti condivisi
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)."

La funzione `__uClibc_main` è la più adatta a questo scopo. Per sovrascrivere questa funzione, è necessario creare un file C che contenga una funzione con lo stesso nome.```c
void __uClibc_main(void *main, int argc, char** argv) {
    // Harness code, e.g. call the function parser_append
    printf("My custom __uClibc_main was called!");
}

Il file C può quindi essere cross-compilato in un oggetto condiviso nell'architettura mipsel utilizzando mipsel-linux-gnu-gcc. L'opzione -fPIC abilita il "Position Independent Code", il che significa che il codice macchina non dipende dall'essere collocato a un indirizzo specifico, utilizzando l'indirizzamento relativo invece di quello assoluto.```txt $ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC

root@kitploit:~
La libreria condivisa appena creata può quindi essere caricata aggiungendo la variabile d'ambiente `LD_PRELOAD` al comando QEMU.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!

Con il comando chroot le directory corrente e root possono essere cambiate per il comando fornito. Questo è utile perché l'eseguibile wscd apre altri file, come le librerie condivise dal firmware. Possiamo osservare questo comportamento aggiungendo l'argomento -strace a QEMU.```txt chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt 38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000 38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory) 38180 open("/parser_parse_hook.o",O_RDONLY) = 3 38180 fstat(3,0x7ffff920) = 0 38180 close(3) = 0 38180 munmap(0x7f7e6000,4096) = 0 38180 open("/lib/libpthread.so.0",O_RDONLY) = 3 38180 open("/lib/libc.so.0",O_RDONLY) = 3 [...]

root@kitploit:~
Come possiamo vedere, l'eseguibile apre diverse librerie nella cartella `/lib/` sul firmware e non
sull'host.

### Sviluppo e debug dell'harness

Dopo che il setup è stato creato, possiamo iniziare a sviluppare un harness. Come descritto nella sezione
background, l'harness è il driver tra il fuzzer e la funzione target. L'harness carica
l'input di fuzzing, che viene salvato da AFL++ in un file. Con il percorso del file come parametro, l'harness
chiama quindi il target di fuzzing; in questo caso, sarebbe `parser_append`. Le funzioni possono essere chiamate
usando l'indirizzo.

<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
  // Verify that a filename is provided
  if (argc != 2) exit(1);

  // Create function pointer to the fuzz target
  int (*parser_request_init)(void *, int) = (void *) 0x00412564;
  int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;

  // Open the fuzz input file
  int fd = open(argv[1], O_RDONLY);
  char fuzz_buf[2048 + 1];
  int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
  if (fuzz_buf_len < 0) exit(1);
  fuzz_buf[fuzz_buf_len] = 0;

  // Call the target functions
  uint8_t parsed_data[220]; 
  parser_request_init(parsed_data, 8);
  int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
  printf("Response is %d\n", status);
  exit(0);
}

Codice 10: Codice harness con il fuzz target `parser_append` nel binario wscd.

Come mostrato nel Codice 10, la funzione parser_parse non viene chiamata direttamente, ma tramite la funzione parser_append. Prima che questa funzione venga chiamata, deve essere chiamata la funzione di inizializzazione parser_request_init, che inizializza la struct di output della funzione parser_parse.

Mentre nel caso di parser_parse l'harness è piuttosto semplice da impostare, altri target richiedono harness più sofisticati, come la funzione httpd_parser_main. Ad esempio, prima di chiamare il target, deve essere chiamata la funzione http_init_main, che termina con un SIGSEGV. Per scoprire dove viene causato questo segmentation fault, è utile eseguire il debug del codice con un debugger come gdb. Per fare ciò, QEMU può essere avviato con l'opzione -g, che avvia un gdb-server sulla porta indicata.```sh chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd corpus/httpd/simple.txt

root@kitploit:~
Poiché il binario è nell'architettura `mipsel`, è necessario usare `gdb-multiarch`. Dopo che gdb è
stato avviato, il seguente script di inizializzazione può essere caricato con gdb usando `sources <path to script>`.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc

A causa della chroot, lo script ha prima modificato il percorso del prefisso assoluto in modo che, quando il binario carica un oggetto condiviso, gdb trovi il file. Poi viene impostato il file target, perché il gdb-server di QEMU non supporta il trasferimento di file, quindi gdb prova a caricare i file dal disco. Dopo aver configurato gdb, lo script si connette al gdb-server con target remote e crea un breakpoint all'inizio della funzione target. Con display, l'output è semplicemente migliorato, così durante lo stepping verranno mostrate le successive quattro righe di assembly. Usando si possiamo avanzare di un'istruzione alla volta, il che è utile quando l'harness ha un segmentation fault usando il corpus predefinito, cosa che dovrebbe sempre funzionare. Come mostrato nel Code 11, il binario ha un segmentation fault nella funzione fprintf.

```sh (gdb) si 0x004059b0 in http_parser_makeHeader () 1: x/4i \$pc => 0x4059b0 : jalr t9 0x4059b4 : addiu a1,a1,16248 0x4059b8 : li v0,200 0x4059bc : lw gp,16(sp) (gdb) ni 0x7f56a8ac in fprintf () from /share/root/lib/libc.so.0 1: x/4i \$pc => 0x7f56a8ac \: bal 0x7f56db80 0x7f56a8b0 \: nop 0x7f56a8b4 \: lw ra,36(sp) 0x7f56a8b8 \: jr ra 0x7f56a8bc \: addiu sp,sp,40 (gdb) n Single stepping until exit from function fprintf, which has no line number information.

Program received signal SIGSEGV, Segmentation fault.

root@kitploit:~
<p style="text-align: center">Codice 11: Errore di segmentazione in printf.</p>

Per investigare l'errore, Ghidra può essere utilizzato per scoprire con quali parametri viene chiamata la funzione.```c
fprintf(
 *(FILE **)(iVar1 + 0x101c),
 "HTTP/1.1 %d %s\r\n",
 *(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
 (&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);

The SIGSEGV è probabilmente causato dal fatto che il primo parametro non è un descrittore di file ma un puntatore nullo. Dove iVar1 è solo un riferimento all'input della funzione httpd_parser_main. Ciò significa che l'input del fuzzing deve avere un descrittore di file alla posizione 0x101c. Quindi l'input deve essere adattato alla seguente struct.```c typedef struct { int _a; // 4 Bytes int _b; // 4 Bytes int socket; // 4 Bytes int ip; // 4 Bytes int mac; // 4 Bytes unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes FILE * fd_out; // expected to be a valid file descriptor } HttpMainT;

root@kitploit:~
Poiché `fd_out` deve essere solo un puntatore valido a un descrittore di file, può essere facilmente impostato su `stdout`.
Eseguendo di nuovo `httpd_parser_main` si produrrà ora un output HTTP valido.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
    /usr/bin/httpd /httpd_corpus.txt

bind: No such file or directory
[ dm_shmInit ] 086:  shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close

<!DOCTYPE html>
[...]

L'harness ora funziona e può essere usato per fuzzare la funzione usando AFL++; come verrà spiegato nella prossima sezione.

Genera dati del corpus

Come accennato nella parte introduttiva, un corpus seed descrive campioni di input validi, che funge da riferimento fondamentale per generare nuovi dati di input durante il processo di fuzzing.

Questi input sono in genere scelti per rappresentare diversi aspetti dei programmi target. Il corpus seed viene utilizzato da un fuzzer per generare casi di test mutati o evoluti che vengono poi eseguiti contro il software target per scoprire bug, crash o altri problemi. Questo corpus svolge un ruolo importante nel guidare il fuzzer verso le aree rilevanti del programma e nell'aumentare la probabilità di rilevare vulnerabilità o comportamenti imprevisti. Fornendo un insieme diversificato e rappresentativo di input iniziali, il corpus seed aiuta il fuzzer a esplorare più rapidamente percorsi diversi nel target e quindi ad aumentare la copertura.

Quando si tratta di funzioni che analizzano dati di rete, questi input possono essere creati usando Wireshark per registrare diversi pacchetti.

Per la funzione httpd_parse_main sono stati creati quattro diversi corpora. Ognuno mirato a diversi percorsi del binario. Un esempio è la richiesta di login, che contiene nome utente e password. Per questo corpus, l'harness ha dovuto essere modificato perché TP-Link usa una crittografia (debole) per "proteggere" la password. Per questo, la password viene crittografata nel browser usando AES e poi decrittografata nel backend. In questo processo, la password viene generata nel browser e poi crittografata usando RSA. Poi i dati crittografati vengono firmati. Poiché un fuzzer non può creare una firma né crittografare dati, alcune funzioni sono state sovrascritte e ora decodificano semplicemente i dati da base64. Per fare ciò, i dati sono stati prima estratti in chiaro dal browser usando il debugger mostrato nella Figura 4.

Figura 4: Estrazione dei dati prima della crittografia.

Nel target, la funzione rsa_tmp_decrypt_bypart è stata quindi sovrascritta per sostituire la logica che decrittografava i dati con la semplice decodifica da base64.```c // Replacing the logic with b64_decode int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00; b64_decode(output, 0x1000, input, input_len); int * seqnumber = (int *) 0x00444db0; *seqnumber = 0x3ac28e29-input_len+12; return 0; // says it was okay }

root@kitploit:~
<p style="text-align: center">Codice 12: la funzione rsa_tmp_decrypt_bypart ora decodifica semplicemente base64
invece di decifrare i dati.</p>

Durante l'esecuzione del corpus, la funzione target restituisce sempre un documento HTML con l'errore "408
Request Timeout". Usando Ghidra e GDB è stato possibile identificare il problema. L'errore si verifica sempre
dopo la chiamata alla funzione `http_stream_fgets`. La riga problematica era il controllo del carattere di fine
riga `\n`.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {

Questa condizione impone che dopo ogni interruzione di riga debba seguire un ritorno a capo. Dopo aver aggiunto il ritorno a capo, tutti i corpora creati hanno funzionato.

Fuzz del target

Nell'ultima sezione abbiamo sviluppato più harness e li abbiamo eseguiti usando QEMU. In questa sezione, QEMU viene sostituito da AFL++, che riceve i corpora generati come input seed per fare fuzz della funzione target. Nella sezione "Ambiente di fuzzing" è stata creata un'immagine docker che scarica già AFL++ da GitHub e poi usa uno script fornito da AFL++ per compilare una versione patchata di QEMU. Quindi AFL++ può ora essere avviato con il seguente comando che accetta diversi parametri, come -Q che dice ad AFL++ di usare la versione patchata di QEMU.```sh QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@

root@kitploit:~
<p style="text-align: center">Codice 13: Fuzzing del binario <code>httpd</code> usando l'harness
e <code>afl-fuzz</code>.</p>

A differenza di prima, il comando `chroot` non è più necessario e viene sostituito dalla variabile
`QEMU_LD_PREFIX`. Questa dice a QEMU dove cercare gli oggetti condivisi. Inoltre, la variabile `LD_PRELOAD`
viene sostituita dalla versione specifica di AFL `AFL_PRELOAD`. L'ultimo argomento nel comando sono
i due caratteri `@`. Verranno sostituiti da AFL++ con un percorso di file che contiene l'input di fuzzing.
Quando viene avviato, AFL++ mostra l'avanzamento usando l'interfaccia a terminale mostrata nella [Figura 5](#f5).

<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">Figura 5: La schermata di stato di AFL++.</p></figcaption>
</figure>

La schermata di stato di `AFL++` fornisce informazioni essenziali sul processo di fuzzing in corso. La documentazione di
`AFL++` offre una buona panoramica dei termini usati nella schermata di stato
[\[afl-screen\]](https://aflplus.plus/docs/status_screen/).  Quando si esegue il debug del corpus con le
seguenti variabili d'ambiente, l'interfaccia può essere disabilitata e con `AFL_DEBUG` un logging dettagliato
viene abilitato, che mostra l'input corrente del fuzzer e lo `stdout` dal programma target.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI

Come mostrato nella Figura 5, il fuzzing di un binario può richiedere parecchio tempo. Secondo la documentazione, "si dovrebbe prevedere che venga eseguito per giorni o settimane" e "ad alcuni job sarà consentito di essere eseguiti per mesi." Per ridurre il tempo necessario, la velocità di esecuzione dovrebbe essere superiore a 100 execs/sec. Quando, ad esempio, il target httpd_main_parser è stato fuzzato, la velocità di esecuzione era all'inizio di circa 30/sec. Per migliorare la velocità, il binario target è stato analizzato alla ricerca di funzioni sospette, che probabilmente sono la causa del rallentamento. Una delle funzioni sospette era rsa_gdpr_generate_key perché è noto che generare una chiave RSA sia lento. Dopo aver sovrascritto la funzione, la velocità è migliorata a 600 esecuzioni al secondo.

Un indicatore che aiuta a capire quando fermare il fuzzing è il contatore di cicli. AFL++ evidenzierà il numero in verde quando "il fuzzer non ha visto alcuna azione per un po' di tempo", il che aiuta a prendere la decisione di fermare il fuzzer.

Ma il numero più interessante è probabilmente "total crashes". Questo mostra quando il programma va in crash a causa dell'input di fuzzing corrente e probabilmente si tratta di un bug correlato alla memoria. Per verificare che sia un vero bug, è possibile usare di nuovo gdb per trovare la posizione del bug.

Conclusione

Il fuzzing può essere il modo più efficace per trovare vulnerabilità di sicurezza. In questa tesina, sono state fuzzate tre diverse funzioni, ma non ne è stata trovata nessuna. Mentre la configurazione del fuzzing black box in sé non è così complessa e dispendiosa in termini di tempo, trovare un target efficace e sviluppare un harness funzionante lo sono. La maggior parte delle volte, l'harness deve essere sottoposto a debug, e poi la logica sottostante nel binario deve essere sottoposta a reverse engineering, il che richiede di nuovo molto tempo.

Scarica lo strumento