Come iniettare codice nel kernel Linux senza codice sorgente e simboli di debug
Immagina questo scenario: hai un binario del kernel Linux già pronto per un dispositivo mobile o una board ARM64 embedded. Nessun codice sorgente disponibile, le informazioni di debug sono state rimosse durante la build, e hai bisogno di intercettare una system call o aggiungere la tua logica direttamente a livello kernel.
L'approccio usuale in questi casi è lungo e tedioso. Devi fare reverse engineering del binario in IDA Pro o Ghidra, trovare manualmente gli offset delle funzioni, e scrivere patch fragili. Uno sviluppatore con il nickname bmax121 ha deciso di semplificare questo processo e ha rilasciato il progetto KernelPatch.
Perché KernelPatch è necessario
KernelPatch è un framework per patchare e agganciare il kernel Linux quando tutto ciò che hai a disposizione è un'immagine del kernel stripped come vmlinux o boot.img.
Quando un kernel viene compilato senza informazioni di debug, gli strumenti di debug tradizionali come kprobes si rifiutano di funzionare senza workaround aggiuntivi. KernelPatch risolve questo problema autonomamente. Recupera la tabella dei simboli direttamente dal binario del kernel e fornisce un'API comoda per modificare il codice eseguibile.
Il progetto è utile per reverse engineer, ricercatori di sicurezza Android e sviluppatori di software di basso livello per sistemi embedded.
Cosa può fare il framework
Lo sviluppatore ha integrato nel progetto diverse funzionalità importanti per lavorare con il kernel:
- Ricerca simboli senza codice sorgente. Lo strumento analizza le strutture interne
kallsymsall'interno dell'immagine già pronta. Non ha bisogno di fileSystem.mapesterni o codice sorgente. Un approccio simile viene usato nell'utilitàvmlinux-to-elf, da cui l'autore ha preso in prestito alcune idee. - Due modalità di iniezione del codice. Puoi applicare una patch statica all'immagine (per esempio, sovrascrivere
boot.imgsu disco) oppure caricare codice dinamicamente a runtime. - Intercettazione di funzioni tramite inline hook. Per l'architettura ARM64, l'intercettazione delle chiamate si basa sul lavoro di
android-inline-hook. Il framework gestisce correttamente le istruzioni di indirizzamento relativo (b,bl,adrp) in modo da non compromettere l'esecuzione del codice originale. - System call hooks. Sostituire gli indirizzi nella tabella
sys_call_tablerichiede solo un paio di comandi. - Modulo SU integrato. Per i dispositivi Android, il progetto include la propria implementazione di superuser.
Come funziona internamente
Per caricare i moduli utente (KPM — Kernel Patch Modules) al volo, il framework ha bisogno di memoria all'interno del kernel. A questo scopo, KernelPatch include un allocatore TLSF compatto. Aiuta ad allocare pagine di memoria eseguibili (ROX) direttamente nello spazio degli indirizzi del kernel.
Quando si esegue un inline hook, il framework legge le prime istruzioni della funzione target, forma un cosiddetto trampolino da esse, e scrive un jump verso il tuo handler. Quando il tuo codice termina l'esecuzione, il controllo torna alla funzione originale tramite il trampolino.
Il framework legge le prime istruzioni della funzione target, crea un cosiddetto trampolino da esse, e scrive un salto verso il tuo handler. Quando il tuo codice termina l'esecuzione, il controllo torna alla funzione originale tramite il trampolino.
Requisiti di sistema e limitazioni
Il progetto ha confini chiari di applicabilità che è importante conoscere in anticipo:
- Solo l'architettura ARM64 è supportata. Non esistono ancora versioni per x86_64 o RISC-V.
- Intervallo di versioni del kernel — da Linux 3.18 a 6.6.
- Il requisito principale è che il kernel sia compilato con il flag
CONFIG_KALLSYMS=y.
Se il vendor del firmware ha completamente rimosso CONFIG_KALLSYMS durante la fase di build, KernelPatch non sarà in grado di analizzare automaticamente gli indirizzi delle funzioni. Fortunatamente, questo flag è lasciato abilitato nella maggior parte dei firmware di dispositivi Android e ARM.
Esempio pratico di utilizzo
Diciamo che hai bisogno di tracciare tutti i lanci di processi tramite la chiamata sys_execve su un dispositivo Android di test, ma non vuoi ricompilare il kernel dal codice sorgente del vendor.
In precedenza, avresti dovuto costruire un modulo caricabile del kernel completo (LKM), gestire i problemi di verifica della firma digitale e l'assenza di un modulo loader nel kernel.
Con KernelPatch, il processo è diverso:
- Scrivi un piccolo codice C con la logica di intercettazione.
- Compilalo in formato KPM.
- Passa all'utilità KernelPatch il tuo
boot.imge il modulo KPM compilato. - Ottieni un
boot.imgmodificato e flashalo tramite fastboot.
Dopo l'avvio del dispositivo, ogni chiamata a execve passa prima attraverso il tuo handler.
Il README del progetto è piuttosto laconico, e tutta la documentazione dettagliata dovrà essere studiata dal codice sorgente e dai file nella cartella doc/. Tuttavia, il progetto è in fase di sviluppo attivo e ha già raccolto oltre 14.000 stelle su GitHub.
A proposito, se non hai bisogno del framework stesso per i tuoi esperimenti, ma piuttosto di un root pronto all'uso per Android basato su di esso, l'autore consiglia di guardare il suo progetto adiacente — APatch. Questa è un'alternativa di alto livello a Magisk e KernelSU, che utilizza KernelPatch come motore sottostante.
Per i ricercatori di sicurezza e gli sviluppatori ARM64, KernelPatch è una scoperta eccellente, che fa risparmiare decine di ore nell'analisi di kernel closed-source.
Progetti correlati