
Una libreria leggera di strumentazione dinamica
Copyright 2020 Google LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
## Cos'è TinyInst?
TinyInst è una libreria di instrumentazione dinamica leggera che può essere utilizzata per instrumentare solo i moduli selezionati del processo, lasciando il resto del processo in esecuzione in modo nativo. È pensata per essere facile da capire, facile da modificare e facile da utilizzare. Non è progettata per essere compatibile con tutti i target (maggiori dettagli in seguito).
### Come si confronta con [DynamoRIO](https://dynamorio.org/) e [PIN](https://software.intel.com/en-us/articles/pintool)?
TinyInst non è pensato come sostituto di framework di instrumentazione complessi come DynamoRIO e PIN, ma piuttosto come alternativa per scenari in cui una soluzione più leggera sarebbe sufficiente. TinyInst presuppone che il target sia ben educato (nel senso spiegato sotto), cosa che non vale per i framework più complessi. Di conseguenza, probabilmente non riuscirai a eseguire TinyInst contro malware come [era stato fatto con DynamoRIO in precedenza](https://www.slideshare.net/MaximShudrak/fuzzing-malware-for-fun-profit-applying-coverageguided-fuzzing-to-find-bugs-in-modern-malware). D'altra parte, se un target non funziona con altri framework a causa del modulo che non deve essere instrumentato, e il modulo instrumentato è ben educato, potrebbe funzionare con TinyInst. Poiché con TinyInst la maggior parte del processo viene eseguita in modo nativo, il tempo di avvio del processo sarà più breve e potrebbe superare altre soluzioni nei casi in cui il processo target trascorre molto tempo nei moduli in cui l'instrumentazione non è necessaria.
### Come si confronta con [Mesos](https://github.com/gamozolabs/mesos) e [TrapFuzz](https://github.com/googleprojectzero/p0tools/tree/master/TrapFuzz)?
TinyInst è una soluzione completa di riscrittura binaria, quindi è possibile modificare qualsiasi comportamento nel modulo target. Questo le consente, ad esempio, di estrarre edge coverage invece dei soli blocchi di base. Inoltre, TinyInst non dipende da altro software, come IDA Pro, per identificare i blocchi di base.
### Quali sistemi operativi supporta TinyInst?
TinyInst funziona su Windows (x86 e x64), macOS (x64 e ARM64), Linux (x64 e ARM64) e Android (ARM64). Consulta il README nella directory corrispondente per ciascun sistema operativo per note e limitazioni aggiuntive.
### Quali target sono compatibili con TinyInst?
TinyInst presuppone che tutti i moduli instrumentati siano ben educati nel senso che
- Non è presente codice auto-modificante
- L'indirizzo di ritorno sullo stack non viene mai acceduto direttamente dal programma
OR/AND (a seconda delle impostazioni)
- Nessun dato viene mai memorizzato prima della cima dello stack (su indirizzi inferiori a quelli puntati da ESP/RSP). Questa condizione può essere rilassata in "nessun dato prima di (ESP/RSP - arbitrary_offset)" usando il flag `-stack_offset`.
TinyInst richiede inoltre che DEP/NX sia abilitato per il processo target. Se non lo è già, puoi usare il flag `-force_dep` per forzarlo. Tuttavia, nell'improbabile caso in cui il target abbia realmente bisogno di DEP disattivato per funzionare correttamente, forzarlo potrebbe causare comportamenti anomali.
### Qual è l'overhead prestazionale?
Secondo le prime misurazioni sulla decodifica delle immagini, su un target a 64 bit ben educato con le impostazioni predefinite di TinyInst, l'overhead prestazionale era di circa il 15% senza client e di circa il 20% con l'esempio di client per la raccolta della copertura. Nota che questo non include il timeout introdotto dall'instrumentazione iniziale dei moduli. Vedi i suggerimenti sulle prestazioni qui sotto per maggiori dettagli.
## Compilare TinyInst
1. Apri un terminale e configura il tuo ambiente di build (ad es. su Windows, esegui vcvars64.bat / vcvars32.bat)
2. Vai nella directory che contiene il sorgente
3. Esegui i seguenti comandi (modifica il generatore in base alla versione dell'IDE e alla piattaforma per cui vuoi compilare):
#### Windows```
mkdir build
cd build
cmake -G "Visual Studio 16 2019" -A x64 ..
cmake --build . --config Release
mkdir build cd build cmake -G Xcode .. cmake --build . --config Release
#### Linux```
mkdir build
cd build
cmake ..
cmake --build . --config Release
mkdir build cd build cmake -DCMAKE_TOOLCHAIN_FILE=</path/to/android/ndk>build/cmake/android.toolchain.cmake -DANDROID_NDK=</path/to/android/ndk> -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM= .. cmake --build . --config Release
Nota #1: la build a 64 bit funzionerà anche su target a 32 bit sui sistemi operativi Windows e Linux
Nota #2: Riscontri problemi nella creazione di una build a 32 bit su Windows a 64 bit perché l'ambiente non è configurato correttamente e mancano delle librerie? Apri il file .sln generato in Visual Studio e compila da lì invece di eseguire cmake --build. Nota anche che la build a 64 bit funzionerà su target a 32 bit, quindi potrebbe non essere necessario creare una build a 32 bit.
## Utilizzo di TinyInst
TinyInst è pensato principalmente per essere usato come libreria all'interno di altri programmi.
Un client TinyInst è scritto come sottoclasse della classe TinyInst. Il client può quindi fare override dei metodi API di cui ha bisogno. I metodi API sono definiti di seguito.
Dopo la creazione del client, deve essere inizializzato con le opzioni della riga di comando chiamando
`void init(int argc, char **argv);`
Le opzioni della riga di comando sono definite di seguito e un client può anche definirne di proprie. Dopodiché, per eseguire e controllare un programma strumentato, si possono usare le seguenti funzioni.
`DebuggerStatus Run(int argc, char **argv, uint32_t timeout);`
`DebuggerStatus Attach(unsigned int pid, uint32_t timeout);`
Queste funzioni eseguono un programma (usando la riga di comando specificata) oppure si agganciano a un programma già in esecuzione. Se non viene specificato un metodo target, il target continuerà a essere eseguito finché il programma non termina, non va in crash, o fino alla scadenza del timeout (espresso in millisecondi). Se è definito un metodo target, TinyInst restituirà il controllo ogni volta che il metodo target viene invocato e ogni volta che il metodo target restituisce, consentendo al chiamante di eseguire attività aggiuntive.
Quando `Run` e `Attach` restituiscono mentre il processo target è ancora vivo, si possono usare le seguenti funzioni per terminare il processo o continuare l'esecuzione.
`DebuggerStatus Kill();`
`DebuggerStatus Continue(uint32_t timeout);`
TinyInst include un binario di copertura di esempio, che può essere invocato usando
`<options> -- <target command line>`
Esempio su Windows:
`litecov.exe -instrument_module notepad.exe -coverage_file coverage.txt -- notepad.exe`
## API di strumentazione
### Callback di eventi del debugger
Questi callback sono solo informativi e il client non deve emettere codice strumentato durante la loro esecuzione. I client devono chiamare lo stesso handler definito nella superclasse prima di gestire questi eventi direttamente.
`OnProcessCreated`
Chiamato quando il processo target viene creato o quando ci si aggancia a esso.
`OnProcessExit`
Chiamato quando il processo target termina.
`OnProcessEntrypoint`
Chiamato quando viene raggiunto l'entrypoint del processo (binario principale)
`OnTargetMethodReached`
Se il metodo target è definito, chiamato quando il metodo target viene raggiunto per la prima volta.
`OnModuleLoaded`
Chiamato quando un modulo viene caricato. Chiamato per ogni modulo, non solo per quelli strumentati.
`OnModuleUnloaded`
Chiamato quando un modulo viene scaricato. Chiamato per ogni modulo, non solo per quelli strumentati.
`OnException`
Chiamato quando viene incontrata un'eccezione. Il client deve restituire true (se l'eccezione è stata gestita) oppure il risultato dello stesso metodo sulla classe padre.
### Callback di strumentazione
Durante questi callback, il client può aggiungere codice al target chiamando `WriteCode()`. Nota che il client è responsabile del salvataggio e del ripristino di qualsiasi contesto (come registri e flag alterati nel codice inserito).
`InstrumentBasicBlock`
Può essere usato per inserire codice che verrà eseguito su uno specifico blocco di base
`InstrumentEdge`
Può essere usato per inserire codice che verrà eseguito su uno specifico arco. Nota: per motivi di prestazioni, questo callback viene emesso solo su archi non deterministici (cioè salti condizionali) e salti/chiamate indiretti (es. `call rax`). Per archi in cui il blocco di base successivo è sempre noto dato il blocco di base precedente (es. `jmp offset`, `call offset`), non verrà emesso alcun callback.
`InstrumentInstruction`
Può essere usato per modificare l'istruzione o inserire codice prima di essa. A seconda del codice restituito, l'istruzione originale verrà emessa o meno dopo il callback.
### Altri callback
`OnModuleEntered`
Chiamato quando il flusso di controllo viene trasferito da un altro modulo a un modulo strumentato
`OnModuleInstrumented`
Chiamato quando un modulo viene strumentato. Questo avviene generalmente quando viene raggiunto l'entrypoint del processo (se il metodo target non è definito) o quando viene raggiunto il metodo target (se è definito). Il client può inizializzare qui i propri dati relativi alla strumentazione
`OnModuleUninstrumented`
Chiamato quando i dati di strumentazione non sono più validi e devono essere cancellati. Nota che questo non è la stessa cosa dello scaricamento del modulo: per impostazione predefinita, la strumentazione persiste attraverso scaricamenti/ricaricamenti del modulo. Questo callback può essere usato per cancellare qualsiasi dato relativo alla strumentazione nel client.
### API Hook
Oltre all'API generica documentata sopra, TinyInst implementa anche un'API di hooking più adatta a ispezionare e modificare il comportamento di singole funzioni. Questa API è documentata in una [pagina separata](https://github.com/googleprojectzero/TinyInst/blob/master/hook.md).
## Opzioni della riga di comando
### Relative alla strumentazione
`-instrument_module [module name]` specifica quale modulo strumentare; è possibile specificare più opzioni `-instrument_module` per strumentare più moduli.
`-instrument_transitive [module name]` simile a `-instrument_module`, con l'eccezione che solo il codice raggiunto da altri moduli strumentati verrà eseguito strumentato. Usato principalmente come ottimizzazione per chiamate come module1->module2->module1, dove non è importante strumentare l'intero modulo module2, ma le voci module2->module1 causano rallentamenti.
`-indirect_instrumentation [none|local|global|auto]` quale strumentazione usare per salti/chiamate indiretti
`-patch_return_addresses` - sostituisce l'indirizzo di ritorno con il valore originale, fa sì che i ritorni vengano strumentati usando qualunque metodo `-indirect_instrumentation` sia specificato
`-generate_unwind` - Genera dati di stack unwinding per il codice strumentato (per una gestione delle eccezioni C++ più veloce). Nota che potrebbe non funzionare correttamente su alcune versioni meno recenti di Windows.
`-persist_instrumentation_data` (default = true) Non strumenta nuovamente il modulo in caso di scaricamenti/ricaricamenti del modulo. Funziona solo se il modulo viene caricato allo stesso indirizzo di prima.
`-instrument_cross_module_calls` (default=true) Se vengono specificati più moduli `-instrument_module` e uno chiama l'altro, salta al codice strumentato dell'altro modulo senza causare un'eccezione (che provocherebbe rallentamenti).
`-stack_offset` (default=0) Quando si salva il contesto sullo stack, lascia invariati questo numero di byte in cima allo stack (prima dello stack pointer).
`-patch_module_entries [off|data|code|all]` Tenta di risolvere i rallentamenti dovuti a un numero eccessivo di voci nei moduli cercando puntatori a entrypoint rilevati in precedenza e sostituendoli con le loro controparti strumentate. Il valore del flag controlla dove cercare questi puntatori. Attenzione: abilitare questa opzione potrebbe introdurre instabilità nel target.
### Relative al debug
`-trace_debug_events` - stampa gli eventi del debugger (moduli caricati, eccezioni, ecc.)
`-trace_basic_blocks` - stampa i blocchi di base mentre vengono eseguiti
`-trace_module_entries` - stampa tutte le voci nel codice strumentato
`-trace_syscalls` - [solo Linux/Android] Consente al client di ricevere eventi di inizio/fine syscall tramite i callback `OnSyscall()` / `OnSyscallEnd()`.
`-full_address_map` - Mantiene una mappa a livello di istruzioni degli indirizzi nel codice strumentato verso gli indirizzi nel codice originale. Richiede molta memoria, ma è utile per il debug.
### Metodo target e persistenza
TinyInst consente all'utente di definire un metodo target. Se viene definito un metodo target, nessun codice verrà strumentato (tutto verrà eseguito nativamente) finché il metodo target non viene raggiunto per la prima volta. Inoltre, TinyInst interromperà l'esecuzione all'ingresso e all'uscita del metodo target.
`-target_module` - modulo che contiene il metodo target
`-target_method` - nome del metodo target. Funziona solo se il metodo target è esportato o se si dispone dei simboli per il modulo target.
`-target_offset` - da usare quando il metodo target non può essere specificato per nome. Indirizzo relativo del metodo target rispetto alla base del modulo
`-loop` - se questo flag è specificato, TinyInst eseguirà il metodo target in un ciclo infinito (o finché non viene chiamato Kill() o il processo termina per un altro motivo). Gli argomenti della funzione verranno salvati e ripristinati tra le iterazioni. È usato principalmente per forzare la persistenza nel fuzzing.
`-nargs` - numero di argomenti del metodo target da salvare tra le iterazioni. Da usare insieme a `-loop`
`-callcon [ms64|stdcall|fastcall|thiscall]` - convenzione di chiamata usata dal metodo target. Da usare insieme a `-loop`
### Altri
`-target_env key=value` - [attualmente solo macOS e Linux/Android] specifica una variabile d'ambiente aggiuntiva da passare al processo target. È possibile specificare più opzioni `-target_env` per passare più variabili d'ambiente.
`-force_dep` - [solo Windows] Abilita forzatamente DEP per il processo target.
## Modulo di copertura
TinyInst include un modulo di copertura (di esempio), `LiteCov`. Il modulo di copertura può raccogliere la copertura dei blocchi di base o degli archi (controllata tramite il flag `-covtype`). In aggiunta, il modulo può estrarre la copertura "compare" (contando il numero di byte che corrispondono nelle istruzioni cmp/sub) specificando il flag `-cmp_coverage`.
Una caratteristica speciale del modulo di copertura è che il buffer di copertura nel processo target viene inizialmente allocato come sola lettura, causando un'eccezione la prima volta che viene incontrata nuova copertura. Combinato con un'opzione per ignorare un certo sottoinsieme di copertura, ciò consente di verificare rapidamente se l'esecuzione del target con un dato input ha prodotto nuova copertura o meno.
## Come funziona TinyInst?
TinyInst è costruito sopra un debugger personalizzato. Il debugger osserva il processo target per eventi come caricamento di moduli, rilevamento di breakpoint, eccezioni sollevate, ecc. Il debugger implementa anche breakpoint e persistenza se il metodo target è specificato.
Quando un modulo da strumentare viene caricato, viene inizialmente "strumentato" nel modo seguente
- Tutte le regioni eseguibili nel modulo vengono marcate come non eseguibili, mantenendo le altre permissioni (lettura/scrittura) come erano in origine. Questo causa un'eccezione ogni volta che il flusso di controllo raggiunge un modulo strumentato, eccezione che viene catturata e gestita dal debugger.
- Viene allocata una regione di memoria eseguibile entro 2GB dall'intervallo di indirizzi del modulo originale. È qui che verrà posizionato il codice strumentato/riscritto del modulo. 2GB è importante perché consente a tutte le istruzioni che usano indirizzamento nella forma [rip+offset] di essere sostituite con [rip+fixed_offset].
Ogni volta che si entra in un modulo strumentato (sia la prima volta che in qualsiasi altro momento), il blocco di base colpito viene strumentato, insieme a tutti i blocchi di base che possono essere scoperti in modo affidabile seguendo ricorsivamente i rami condizionali e anche le chiamate e i salti diretti (es. jmp offset, call offset).
Questo è sufficiente per eseguire il codice strumentato perché
- tutti i salti/chiamate diretti atterreranno nel codice strumentato nella posizione corretta
- tutti i salti/chiamate indiretti (es. call rax) atterreranno nella loro posizione originale nel codice, causando un'eccezione, che il debugger risolve sostituendo il puntatore all'istruzione con la posizione corrispondente nel codice strumentato.
Tuttavia, anche se funziona, nota che causerà un'eccezione su ogni chiamata/salto indiretto il cui target si trova in un modulo strumentato. Poiché la gestione delle eccezioni è lenta, strumentare target con molta indirezione (es. metodi virtuali in C++, puntatori a funzione) sarà lento senza ulteriore strumentazione.
### Strumentazione di chiamate e salti indiretti
TinyInst può strumentare chiamate e salti indiretti per evitare eccezioni su target indiretti (già visti). Una chiamata o un salto strumentato, invece di saltare al target originale, salterà alla testa della lista collegata di stub. Ogni stub contiene una coppia di (original_target, translated_target). Verifica se il target del salto/chiamata corrisponde a original_target e, in tal caso, il flusso di controllo viene diretto a translated_target. In caso contrario, salta allo stub successivo. Se si raggiunge la fine della lista, significa che il target del salto/chiamata non è mai stato visto prima. Questo causerà un breakpoint catturato dal debugger, che verrà risolto creando un altro stub e inserendolo nella lista.
Questo meccanismo può essere implementato in 2 modi
- lista per-callsite (locale)
- hashtable globale usata da tutti i salti/chiamate indiretti
L'hashtable globale offre prestazioni migliori. La lista locale (per-callsite) consente di ottenere archi corretti (con il corretto indirizzo di origine) su chiamate/salti indiretti.
Nota che sui Windows moderni, a causa di CFG, tutti i salti/chiamate indiretti avvengono dalla stessa posizione; quindi con binari compilati con CFG è comunque impossibile (senza una gestione speciale) ottenere archi accurati. Questo, insieme al vantaggio in termini di prestazioni, è il motivo per cui la hashlist globale è il metodo predefinito per gestire chiamate/salti indiretti in TinyInst.
### Patching degli indirizzi di ritorno
Per impostazione predefinita, quando avviene una chiamata nel codice strumentato, l'indirizzo di ritorno scritto sarà la prossima istruzione nel *codice strumentato*. Questo funziona correttamente nella maggior parte dei casi, ma causerà problemi se il processo target accede agli indirizzi di ritorno per scopi diversi dal ritorno. Un esempio notevole è lo stack unwinding durante la gestione delle eccezioni sui sistemi operativi a 64 bit. Pertanto, i target che devono catturare eccezioni non funzioneranno correttamente con TinyInst per impostazione predefinita.
Questo può essere risolto nella maggior parte dei casi aggiungendo il flag `-generate_unwind`, che fa sì che TinyInst generi e registri i metadati di stack unwinding / gestione delle eccezioni per il processo target. Nota che `-generate_unwind` potrebbe non funzionare correttamente su alcune versioni meno recenti di Windows perché richiede UNWIND_INFO versione 2.
TinyInst ha anche un'opzione (esposta tramite il flag `-patch_return_addresses`) per riscrivere gli indirizzi di ritorno nei loro valori corrispondenti nel codice non strumentato ogni volta che avviene una chiamata. Nota comunque che questa opzione introduce un overhead piuttosto elevato, poiché causa un cambio di contesto a ogni ritorno (arco all'indietro) da un modulo non strumentato a uno strumentato.
## Suggerimenti sulle prestazioni
L'overhead maggiore in TinyInst deriva da un'eccezione sollevata ogni volta che si entra in un modulo strumentato da un modulo non strumentato. Puoi vedere queste eccezioni attivarsi usando il flag `-trace_module_entries`. La strumentazione di salti/chiamate indiretti dovrebbe essere usata quando possibile e la strumentazione dei ritorni non dovrebbe essere usata quando possibile. TinyInst offre le migliori prestazioni su moduli (o gruppi di moduli) ragionevolmente autocontenuti. Ad esempio, se hai due moduli, A e B, dove A chiama B spesso ma solo B è strumentato, questo causerà molti rallentamenti. Si potrebbero ottenere prestazioni migliori strumentando sia A che B.
## Suggerimenti per il debug
Usa `-trace_basic_blocks` per vedere i blocchi di base mentre vengono eseguiti. Vedrai sia gli indirizzi nel codice strumentato sia gli indirizzi corrispondenti nel codice non strumentato.
Usa il callback OnException() per esaminare lo stato del programma quando si verifica il crash.
## Disclaimer
Questo non è un prodotto ufficiale Google.