logo-unlock-security

Tampering the RAM: a new way to break into any VM

Tampering the RAM: a new way to break into any VM

Nonostante l’avvento dei container sono ancora molte le realtà che utilizzano le Virtual Machine (VM). Molti distribuiscono delle immagini preconfigurate contenenti le loro applicazioni web proteggendo i sorgenti da occhi indiscreti.

Alcuni si affidano a delle credenziali forti nel login, altri permettono l’accesso solo tramite SSH con gli opportuni certificati, altri disabilitano le shortcut del GRUB per ottenere shell con privilegi di root, altri nascondono il GRUB e il login avviando solamente i servizi web, altri ancora cifrano il disco per non essere montato come esterno.

Quando tutte queste misure di protezione vengono messe in campo allo stesso momento l’unica superficie di attacco che rimane esposta è l’applicazione web stessa; e se viene tolta anche quella, la VM si può considerare inviolabile… oppure no?

Il Page Cache in teoria

Prima di poter spiegare la tecnica di attacco è doveroso spiegare i fondamenti del funzionamento della "Page Cache" di Linux che ne è alla base.

Il Page Cache è una parte del Virtual File System (VFS) che ha lo scopo di migliorare le prestazioni dell’I/O nelle operazioni di lettura/scrittura.

Si parla di caching delle pagine in quanto il kernel Linux lavora con unità di memoria chiamate pagine. Quindi, invece che tenere traccia e gestire ogni singolo bit di informazione cui il sistema accede viene gestita l’intera pagina che contiene quell’informazione. Le pagine hanno generalmente dimensione pari a 4 KB e dato che rappresentano l’unità fondamentale del Page Cache, significa che ogni richiesta di lettura di anche un solo bit comporterà la lettura di 4 KB di dati.

Di seguito un diagramma semplificato che mostra le operazioni fondamentali che il Page Cache compie.

Workflow del page caching a fronte di una richiesta di lettura/scrittura

Come si vede dalla figura precedente, un’operazione di lettura si compone di tre momenti fondamentali:

  1. Un’applicazione user-space richiede al kernel la lettura di dati dal disco utilizzando alcune system call come read(), pread(), vread(), mmap(), ecc.
  2. Il kernel Linux controlla che le pagine corrispondenti ai dati richiesti siano già presenti nella cache e in caso positivo (Cache HIT) le restituisce direttamente al chiamante. In questo caso il disco non viene mai acceduto.
  3. Nel caso le pagine richieste non siano già presenti in cache (Cache MISS), il kernel fa in modo di riservare in cache lo spazio sufficiente ad ospitarle, poi effettua una lettura dei dati dal disco. Questi vengono poi memorizzati in cache e restituiti al chiamante.

Da questo momento in poi ogni successiva richiesta di accesso al file genererà un Cache HIT, quindi senza la necessità di coinvolgere nuovamente il disco.

Diamo ancora uno sguardo a cosa succede quando viene richiesta la lettura di un file.

Dettaglio del meccanismo di file caching durante un'operazione di lettura

In operazioni di I/O che utilizzano la syscall read(), il kernel memorizza le pagine corrispondenti ai dati di cui si richiede l’accesso sia nella Page Cache che in un’altra cache applicativa. Quando si utilizza la syscall mmap(), invece, questa duplicazione non avviene e il kernel utilizza solamente la Page Cache.

Detto questo, cosa ci aspettiamo di vedere all’interno di un file vmem?

A seconda che l’accesso ai file sia avvenuto tramite una read() o mmap() ci aspettiamo di trovare una o più copie delle pagine contenenti i dati richiesti da tutti i processi del sistema, più eventualmente le pagine vicine che il kernel precarica per noi per ridurre gli accessi al disco. Oltre a questo chiaramente saranno presenti anche tutte le strutture dati utilizzate dal kernel, i processi in esecuzione, aree di memoria non utilizzate, ecc.

Il Page Cache in pratica

Per dimostrare praticamente il funzionamento del Page Cache avremo bisogno di:

  • sync – un tool per forzare la scrittura delle pagine "dirty" della cache su disco
  • /proc/sys/vm/drop_caches – un file su procfs per la cancellazione forzata della cache
  • vmtouch – un tool per la diagnostica della Page Cache

Preparazione dell’ambiente

Creiamo un file di prova che utilizzeremo per testare il funzionamento della cache durante le operazioni di lettura.

$ dd if=/dev/random of=file count=128 bs=1M

Puliamo tutte le cache per poter partire da una situazione pulita.

$ sync; echo 3 | sudo tee /proc/sys/vm/drop_caches

Ci possiamo assicurare che il file appena creato sia stato eliminato dalla cache utilizzando il tool VMTouch e assicurandoci che il numero di “Resident Pages” sia uguale a zero.

$ vmtouch file
           Files: 1
     Directories: 0
  Resident Pages: 0/32768  0/128M  0%
         Elapsed: 0.002936 seconds

Lettura di un file – read() syscall

Creiamo un semplice programma C che legge 2 byte del file di test.

#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

int main(int argc, char *argv[])
{
  int fd = open("file", O_RDONLY);
  uint8_t buf[2] = {0};
  read(fd, buf, sizeof(buf));
  close(fd);

  return 0;
}

Compiliamo ed eseguiamo il programma. Essendo la pagina l’unità fondamentale ci aspettiamo che in cache non ci siano solamente i due byte letti, ma l’intera pagina che contiene quei due byte.

$ vmtouch file 
           Files: 1
     Directories: 0
  Resident Pages: 12/32768  48K/128M  0.0366%
         Elapsed: 0.00183 seconds

Contrariamente a quanto ci aspettavamo, in cache non è presente una sola pagina (4 KB), ma ben 12 (48 KB). Questo avviene perché il kernel implementa la cosiddetta “read ahead logic” per cui vengono caricati in memoria dati non esplicitamente richiesti a cui si farà probabilmente accesso nel prossimo futuro in modo da ridurre il numero di accessi al disco.

Questo comportamento può essere controllato utilizzando ad esempio la syscall posix_fadvise(); vediamo come.

#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

int main(int argc, char *argv[])
{
  int fd = open("file", O_RDONLY);
  posix_fadvise(fd, 0, 0, POSIX_FADV_RANDOM);
  uint8_t buf[2] = {0};
  read(fd, buf, sizeof(buf));
  close(fd);

  return 0;
}

Puliamo la cache, compiliamo ed eseguiamo il programma, poi verifichiamo lo stato della cache.

$ vmtouch file 
           Files: 1
     Directories: 0
  Resident Pages: 1/32768  4K/128M  0.00305%
         Elapsed: 0.001783 seconds

Lettura di un file – mmap() syscall

Quanto abbiamo appena fatto può essere realizzato anche utilizzando la syscall mmap().


#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  int fd = open("file", O_RDONLY);
  uint8_t *buf = mmap(NULL, 2, PROT_READ, MAP_SHARED, fd, 0);
  printf("0x%02x\n", buf[0]);
  munmap(buf, 2);
  close(fd);

  return 0;
}

Quando eseguiamo il programma però ci accorgiamo che la “read ahead logic” di mmap() è ancora più aggressiva e legge 32 pagine a fronte di una richiesta di soli 2 byte.

$ vmtouch file 
           Files: 1
     Directories: 0
  Resident Pages: 32/32768  128K/128M  0.0977%
         Elapsed: 0.001935 seconds

Similmente al caso precedente, si può operare sul comportamento di mmap() utilizzando la syscall madvise().


#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
  int fd = open("file", O_RDONLY);
  uint8_t *buf = mmap(NULL, 2, PROT_READ, MAP_SHARED, fd, 0);
  madvise(buf, 2, MADV_RANDOM);
  printf("0x%02x\n", buf[0]);
  munmap(buf, 2);
  close(fd);

  return 0;
}

Possiamo verificare come ora venga caricata una sola pagina.

$ vmtouch file 
           Files: 1
     Directories: 0
  Resident Pages: 1/32768  4K/128M  0.00305%
         Elapsed: 0.001792 seconds

Lo stato di pausa di una VM

Quando utilizziamo un sistema di virtualizzazione possiamo mettere in pausa l’esecuzione della VM. A differenza della sospensione in cui lo stato della VM viene salvato su disco, con la pausa viene salvato in RAM.

La funzione di pausa è utile quando si vuole interrompere temporaneamente l’esecuzione di un’operazione sulla VM per poi riprenderla in un secondo momento.

I file della virtualizzazione

Quando utilizziamo una VM vengono creati nel sistema host alcuni file tra cui i principali sono:

  • vmdk – disco virtuale equivalente al disco della VM
  • vmsd – metadata e altre informazioni sugli snapshot della VM
  • vmx – informazioni di configurazione e impostazioni dell’hardware della VM
  • vmsn – stato della VM relativo a un particolare snapshot
  • vmem – contenuto della memoria quando si mette in pausa la VM

Il file vmem è particolarmente interessante perché ci permette di analizzare il contenuto della RAM di una VM in pausa; contenuto che non cambierà fintanto che la VM resterà in pausa.

Tampering della RAM

Se mettiamo insieme quanto detto finora sul funzionamento della Page Cache durante le operazioni di lettura e sullo stato di pausa di una VM, otteniamo la nuova tecnica di attacco argomento di questo articolo.

L'attacco si compone di 3 parti:

  1. Avviamo una VM e interagiamo con essa per forzare la lettura di un file di interesse
  2. Mettiamo in pausa la VM e modifichiamo il contenuto della Page Cache tramite il file vmem
  3. Togliamo la VM dallo stato di pausa e forziamo nuovamente la lettura del file di interesse

Vediamo questi tre passi utilizzati in un caso reale per capirne la reale efficacia. Utilizzeremo una VM scaricata da HackMyVM, ma avremmo potuto utilizzare qualunque altra VM.

Vogliamo ottenere una shell di root e lo faremo in 3 passi:

  1. Enumeriamo gli utenti forzando la lettura del file /etc/passwd
  2. Enumeriamo gli hash della password forzando la lettura del file /etc/shadow
  3. Eseguiamo un bypass del login e una privilege escalation facendo tampering della RAM

Enumerazione degli utenti

Una volta avviata la VM tutto ciò che abbiamo è la schermata di login.

Schermata di login su una VM Linux

Il primo obiettivo è quello di enumerare tutti gli utenti del sistema. Sappiamo che in Linux la lista degli utenti insieme ad altre loro proprietà è presente nel file /etc/passwd a cui chiaramente non abbiamo accesso.

Quello che sappiamo è che davanti abbiamo il binario /usr/bin/login. Volendo semplificare, possiamo dire che si tratta di un programma che viene eseguito indirettamente dal processo init tramite getty o un display manager. Quando eseguito richiede all’utente il nome utente e la password con cui eseguire l’accesso. Lo username inserito viene controllato che esista nel file /etc/passwd prima di eventualmente controllare l’esattezza della password.

Sapendo questo ci basta mettere in pausa la VM non appena vediamo la schermata di login e aprire con un hex editor il file vmem corrispondente. A questo punto cerchiamo le corrispondenze della stringa :x:1000:1000:: cioè la riga del file /etc/passwd che fa riferimento all’utente con ID 1000 (generalmente utilizzato come primo ID per gli utenti creati su Linux).

Enumerazione degli utenti tramite ricerca nel file VMEM di una VM in pausa

In questo modo siamo stati in grado di leggere il contenuto del file /etc/passwd e ottenere la lista completa degli utenti del sistema tra cui troviamo l'utente moksha:


moksha:x:1000:1000:moksha,,,:/home/moksha:/bin/bash

Enumerazione degli hash delle password

Supponiamo di voler effettuare il login con l’utente moksha, quindi di voler cercare nel file vmem una corrispondenza per la stringa moksha:$, cioè l’inizio della riga nel file /etc/shadow corrispondente all’hash della password dell’utente moksha.

In questo momento non otterremmo alcun risultato. Questo perché il file /etc/shadow viene acceduto solamente quando viene fatto un tentativo di login.

Quindi, se tentassimo di effettuare il login con l'utente moksha e una password qualunque verrebbero letti i file /etc/passwd e /etc/shadow, quindi le pagine corrispondenti di entrambi i file verrebbero caricate in memoria nella Page Cache.

Eseguiamo un tentativo di login come specificato e mettiamo in pausa la VM.

Login con utente corretto e password errata

Fatto questo apriamo di nuovo il file vmem corrispondente alla RAM della VM e cerchiamo nuovamente il pattern moksha:$.

File /etc/shadow caricato nella memoria della VM grazie al file caching

Questa volta otteniamo una corrispondenza, quindi possiamo leggere il contenuto del file /etc/shadow e ottenere l’hash delle password di tutti gli utenti del sistema, tra cui l'utente moksha.

moksha:$y$j9T$aahAz65DkZxl.its6aXmV1$7beXMp4sTYWPV9IuR1CIbPqidBBFx7zhqq1nt2IBTl2:19361:0:99999:7:::

Bypass del login e privilege escalation

Ottenuti i nomi utente e gli hash delle password potremmo provare a ottenere le credenziali in chiaro per usarle per effettuare il login. Oppure, sfruttando ancora una volta il funzionamento del Page Cache potremmo sovrascrivere gli hash con un hash di cui conosciamo la password.

Generiamo l’hash per la password “password” specificando un salt di lunghezza pari a quello utilizzato per l’hash corrente; questo permetterà di ottenere un hash di pari lunghezza.

>>> import crypt
>>> crypt.crypt(
...   'password', # Our password of choice
...   '$y$j9T$aahAz65DkZxl.its6aXmV1$' # The salt found in the vmem file
... )
'$y$j9T$aahAz65DkZxl.its6aXmV1$oHbJ46tHyqV/QPv8c5lxYh.vHfIC1ovf7GBWpu1tIT1'
Sostituzione dell'hash della password nel file VMEM della VM

A questo punto potremmo fare di più: modificando lo user ID (UID) e il group ID (GID) da 1000 (equivalente all’utente moksha) a 0 (equivalente all’utente root) potremmo avere accesso con i massimi privilegi.

Questo si può fare cercando e sostituendo tutte le occorrenze di moksha:x:1000:1000: con moksha:x:0000:0000: facendo attenzione a utilizzare una sequenza di quattro zeri per l’UID e il GID in modo da ottenere la stessa lunghezza di stringa.

A questo punto possiamo salvare il file vmem e togliere la VM dallo stato di pausa per effettuare il login con le credenziali moksha:password e ottenere l’accesso con privilegi di root sulla VM.

Privilege escalation tramite tampering della RAM di una VM

Conclusioni

Questa tecnica non sfrutta alcuna vulnerabilità, ma dei meccanismi di funzionamento dei sistemi operativi e delle macchine virtuali. Per questo motivo non esiste attualmente un rimedio che renda inefficace la tecnica se non quello di disabilitare completamente il Page Cache causando però fortissimi rallentamenti della VM.

Alcuni sistemi di virtualizzazione come VirtualBox cercano di evitare il tampering della RAM inserendo all'inizio del file VMEM un checksum calcolato sul contenuto del file, impedendone il ripristino in caso di manomissione. È chiaro però che è sufficiente ricalcolare l'hash oppure cambiare sistema di virtualizzazione per ottenere di nuovo l'accesso alla VM nel modo spiegato.

Francesco Marano
Francesco Marano
Founder | Cyber Security Consultant
www.unlock-security.it

I'm an offensive cyber security expert with several years of experience as penetration tester and team leader.I love making software do things other than what they were designed to do!I do security research to find new bugs and new ways to get access to IT assets. I'm a speaker at events talking about my research to share my findings and improve the awareness about cyber security issues.