contenuto di proc/mappe

Rilevamento del caricamento dinamico nelle applicazioni Android con /proc/maps

TL; DR: Attraverso il caricamento dinamico, gli autori di malware possono caricare di nascosto codice dannoso nella loro applicazione per evitare il rilevamento. Possiamo rilevare tale caricamento tramite il file generato dal kernel /proc/[PID]/maps dell'applicazione.
Di recente, abbiamo creato un semplice script che ci consente di rilevare il caricamento dinamico nelle app Android. Questo ci ha offerto una buona opportunità per discutere del caricamento dinamico in generale in questo blog. 

Caricamento dinamico e collegamento:

Per comprendere il resto del blog, è importante esaminare le basi del collegamento e del caricamento ed evidenziare le differenze tra collegamento e caricamento e statico e dinamico.

Cosa sta collegando:

Il processo di compilazione ha più parti. Il collegamento è l'ultimo passaggio prima di ottenere un eseguibile eseguibile. Un programma è in genere più di un semplice file autonomo e si basa su altre librerie o file per funzionare. Pertanto, non è sufficiente compilare il codice in codice macchina per poterlo eseguire, ma anche in qualche modo link i diversi file in un eseguibile coerente.

Come si fa in pratica? Dopo la compilazione e l'assemblaggio, l'assembler emette i file oggetto, che di solito corrispondono a ciascun modulo del programma. Tali file possono essere rilocabili o eseguibili. I file oggetto del primo tipo devono essere sottoposti a collegamento prima di poter essere eseguiti, ma quelli del secondo tipo possono essere eseguiti immediatamente. Nell'ecosistema GNU/Linux, l'estensione abituale per un file oggetto è .o. I file oggetto condivisi, noti come librerie, sono file oggetto riposizionabili destinati a essere utilizzati da molti programmi diversi e non possono essere eseguiti da soli. In GNU/Linux hanno l'estensione .so (Shared Object), mentre in Windows hanno l'estensione .dll (Dynamic-link Library).

Il linker quindi prende i file, risolve i simboli (funzioni e variabili) e li indirizza agli indirizzi di memoria corretti annotando tutto nella tabella dei simboli dell'eseguibile. Per motivi di prestazioni, anche si trasferisce il tuo codice in modo che i pezzi di codice correlati finiscano per essere mappati agli indirizzi di memoria vicini, indipendentemente da come hai organizzato originariamente il tuo programma, poiché la leggibilità umana non è più un problema e le prestazioni sono la priorità principale. 

I molti nomi diversi dell'eseguibile:

Sia in Linux che in Windows quelle estensioni apparentemente diverse hanno gli stessi formati sottostanti degli eseguibili. In Windows, sono PE (Portable Executable) e di solito hanno le estensioni .exe e .dll, mentre in Linux sono ELF (Executable and Linkable File) e hanno le estensioni .bin, .so o .o (Sì , anche i file oggetto menzionati in precedenza sono ELF).

Collegamento dinamico:

Diciamo che qualcosa è stato collegato dinamicamente quando invece di eseguire quel processo durante la compilazione, viene eseguito in fase di caricamento o di esecuzione. Il che ci porta al caricamento: un caricatore copia semplicemente il contenuto dell'output del linker in memoria ed esegue il programma. Quando viene utilizzato il collegamento dinamico, il processo di collegamento si verifica appena prima del caricamento, durante l'esecuzione del programma, il che spesso crea confusione tra i due.
Il caricamento dinamico, d'altra parte, significa che parti del codice possono essere caricate in memoria in qualsiasi momento durante il runtime. I due processi possono e spesso vengono eseguiti insieme.

Allora perché questo è un problema?

Il caricamento dinamico è sicuramente utile. Ad esempio, invece di caricare tutte le librerie che il programma utilizzerà al momento del caricamento, è possibile caricarle solo quando è necessario utilizzarle, utilizzando quindi meno memoria, o caricarle solo in modo condizionale in determinati casi.

Ma rappresenta anche un modo semplice per gli sviluppatori di malware di nascondere il loro codice dannoso. Possono inserire tutto il loro codice legittimo nell'APK e spostare tutto il codice nefasto in un DEX (Dalvik Executable) che l'applicazione scaricherà e quindi caricherà dinamicamente durante l'uso e quindi farà apparire la loro applicazione innocua all'ispezione statica di base del APK.

Come vengono caricate le classi DEX:

Android offre un'opzione per caricare dinamicamente i file .dex utilizzando una classe chiamata DexClassLoader. Per caricare una classe, dobbiamo semplicemente scrivere:

// Init the loader
DexClassLoader dexClassLoader = new DexClassLoader(path_to_dex, null, null, parent_class);

// Load the class:
Class dynamic_class = dexClassLoader.loadClass("DynamicClass");

// Load a method we could call it
Method method = dynamic_class.getMethod("method1");

E poi possiamo usare invoke() del metodo per usare il metodo.

Il file /proc/[PID]/maps:

In Unix, tutto è un file, e anche se non è realmente uno, viene gestito e vi si accede come tale. Ciò include le strutture dati del kernel e Linux non fa eccezione alla regola. Il kernel Linux ci consente di accedere e leggere le sue strutture dati attraverso /proc/ pseudo-file system. Ogni processo ha quindi la propria cartella in /proc/[PID]. I file e le sottocartelle qui contengono molte informazioni utili e importanti sul processo, ma oggi ci concentreremo su un solo file: /proc/[PID]/maps.

/proc/[PID]/maps visualizza un grafico della memoria mappata di un processo. Quando diciamo memoria mappata, intendiamo un segmento di memoria virtuale che ha una corrispondenza uno a uno con un file. Questa mappatura consente a un'applicazione di modificare e accedere ai file leggendo e scrivendo direttamente nella memoria. Ciò significa che quando un programma accede a un file, questo finirà per essere registrato nel suo file /proc/[PID]/maps.

/proc/[PID]/maps ci mostra anche quali permessi ha il processo per ogni segmento. Questo può aiutarci a determinare quali file il processo ha modificato e quali file ha letto.

Ecco come appare un breve segmento di un normale file /proc/PID/maps:

7f9cefbf7000-7f9cefbf8000 r--p 00000000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf8000-7f9cefbf9000 r-xp 00001000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbf9000-7f9cefbfa000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfa000-7f9cefbfb000 r--p 00002000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfb000-7f9cefbfc000 rw-p 00003000 103:03 1589169            /usr/lib/libXcomposite.so.1.0.0
7f9cefbfc000-7f9cefc08000 r--p 00000000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc08000-7f9cefc1b000 r-xp 0000c000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc1b000-7f9cefc24000 r--p 0001f000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc24000-7f9cefc25000 r--p 00027000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc25000-7f9cefc26000 rw-p 00028000 103:03 1579223            /usr/lib/libxcb.so.1.1.0
7f9cefc26000-7f9cefc27000 r--p 00000000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc27000-7f9cefc28000 r-xp 00001000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc28000-7f9cefc29000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc29000-7f9cefc2a000 r--p 00002000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2a000-7f9cefc2b000 rw-p 00003000 103:03 1577111            /usr/lib/libX11-xcb.so.1.0.0
7f9cefc2b000-7f9cefc47000 r--p 00000000 103:03 1584005            /usr/lib/libX11.so.6.3.0
7f9cefc47000-7f9cefcd1000 r-xp 0001c000 103:03 1584005            /usr/lib/libX11.so.6.3.0

Ogni riga nel file registra un singolo segmento di memoria nello spazio di indirizzi di memoria virtuale contiguo allocato al processo.

  • Indirizzo – La prima colonna mostra l'indirizzo iniziale e finale del segmento.
  • Permessi – Questa colonna mostra le autorizzazioni di cui dispone il processo per il segmento. r/w/x sono le solite read/write/execute, mentre l'ultima lettera è s or p, che significa rispettivamente condiviso o privato.
  • Offset – Questo è l'offset dall'inizio del file, per poter calcolare l'indirizzo iniziale dei dati mappati. A volte, un segmento non viene mappato da un file (nel qual caso la colonna del percorso avrà un identificatore per la natura del segmento, come spiegato di seguito), nel qual caso l'offset viene semplicemente lasciato come 0.
  • Dispositivo – Quando il segmento è stato mappato da un file, in questa colonna viene visualizzato il numero esadecimale del dispositivo in cui è memorizzato il file.
  • Inode (nodo indice) – Se il segmento proviene da un file, questo è il file numero di inode del file.
  • sentiero – Questo è il percorso del file, se presente. Questo può essere [heap], [stack] o [contro] se il segmento è la struttura omonima.

Il nostro copione umile:

Nelle applicazioni Android offensive, il caricamento dinamico del codice viene solitamente eseguito dalla home directory dell'applicazione, quindi i file caricati in memoria da quel dizionario (o altre posizioni comuni) dovrebbero apparire nel file delle mappe. Per automatizzare l'attività di controllo di quel file, abbiamo costruito uno script molto semplice che utilizza regex per cercare la stringa '/data/data' nel file delle mappe di un dato PID nel dispositivo connesso e restituisce le righe che corrispondono. /data/data è ovviamente la home directory delle app in cui sono archiviati file e dati. È l'unica posizione da cui un'app può caricare i file DEX.

Interagire con ADB tramite Python a volte è stata una sfida, poiché farlo utilizzando la shell può essere scomodo, ma abbiamo trovato un utile set di strumenti chiamato pwntools che forniscono esattamente quella funzionalità. Offrono una vasta gamma di funzionalità progettate per aiutare con l'hacking e la prototipazione. Sicuramente dai un'occhiata.

Puoi vedere lo script completo che usiamo a questo Github.

Salta al contenuto