|
@@ -1,161 +1,43 @@
|
|
|
## Light Hardened Container
|
|
|
|
|
|
#### Perché
|
|
|
-Voglio esporre su internet alcuni servizi da una board (Odroid C2) e farlo a modino, ogni servizio dentro un container.
|
|
|
+Voglio esporre su internet alcuni servizi da una single board (Odroid C2) e farlo a modino, ogni servizio dentro un container.
|
|
|
Giocando con i vari sistemi per creare container quello piú simile ai miei desideri é `runc`.
|
|
|
|
|
|
#### Come
|
|
|
Uso [alpine linux](https://alpinelinux.org) come base perché la rootfs é scandalosamente piccola (1.9Mb), supporta arm64 (che é l'architettura della mia board casalinga), supporta un sistema di pacchetti degno di questo nome (apk) ed e' orientata alla sicurezza.
|
|
|
|
|
|
L'assunto di base é che i servizi che girano dentro il container sono bucabili e quindi bisogna limitare i danni di un sicuro pwn.
|
|
|
-Per fare questo ho pensato di far girare la root di ogni container dentro una partizione read-only.
|
|
|
+Per fare questo ho pensato di far girare la root (/) di ogni container dentro una partizione read-only con un utente specifico per ogni servizio.
|
|
|
Per __i dati__ necessari ai vari servizi, faccio un bind di una directory da una partizione no-exec in modo che
|
|
|
anche se il servizio viene bucato, l'attaccante puó scrivere solamente sulla partizione dei dati (da cui non puo'
|
|
|
peró avviare niente).
|
|
|
|
|
|
-Per evitare di far usare interpreti e altre utilities utili all'attaccante, il container deve contenere esclusivamente i files strettamente necessari per far girare il servizio. Come fare? Partendo dal binario che vogliamo lanciare (sia esso apache, nginx, dovecot o tor) prendiamo innanzitutto le librerie da cui dipende con un `ldd` (anche se sarebbe stato figo usare una distro [tutta statica](http://sta.li)) e poi scopriamo tutti i file da cui dipende cercando le syscall a `open` mentre gira (con `strace`).
|
|
|
+Per evitare di far usare interpreti e altre utilities utili all'attaccante, il container deve contenere esclusivamente i files strettamente necessari per far girare il servizio, mi buchi ma vuoi usare `ls`? non puoi. vuoi usare `netcat`? non c'é. ok puoi compilarlo statico per `arm64`, auguri, dove lo uploadi? il fs e' read-ony, e in /data e' noexec. Insomma diventa fastidioso.
|
|
|
+Come fare? ~~Partendo dal binario che vogliamo lanciare (sia esso apache, nginx, dovecot o tor) prendiamo innanzitutto le librerie da cui dipende con un `ldd` (anche se sarebbe stato figo usare una distro [tutta statica](http://sta.li)) e poi scopriamo tutti i file da cui dipende cercando le syscall a `open` mentre gira (con `strace`).~~ Ci serve una lista dei file a cui accede il nostro servizio, servirebbe un proxy fs, quale utilizzo migliore per un filesystem fuse?
|
|
|
+Cercando in giro trovo ben due implementazioni di proxy fs, [BigBrotherFS](https://www.cs.nmsu.edu/~pfeiffer/fuse-tutorial/) e [https://rflament.github.io/loggedfs/](Loggedfs) che supporta anche i filtri delle syscall quindi piú adatto a questo uso.
|
|
|
+Le syscall che ci interessano sono le `open` e le `readlink`, quindi il [file di configurazione di loggedfs](config.xml) filtrerá solo queste call, mostrandoci nel file di log solamente i file e i symlink a cui il nostro servizio ha avuto accesso.
|
|
|
|
|
|
-Nella pratica, immaginiamo di voler far girare [caddy](https://caddyserver.com/) in questa maniera, ecco come sarebbe il setup a mano:
|
|
|
+Per fare tutte queste cose ho scritto uno script bash per evitare di rifare queste cose tutte le volte, vediamo come usarlo:
|
|
|
|
|
|
+## Uso
|
|
|
|
|
|
-Creo una partizione per __i dati__ di 30Mb (a titolo di esempio lo facciamo in loop) e ne faccio il mount (per ora senza noexec)
|
|
|
+Immaginiamo di voler far girare [caddy](https://caddyserver.com/) in questa maniera, diciamo che abbiamo una partizione cifrata dove vogliamo
|
|
|
+mettere i nostri servizi in `/var/lhc/` con dentro due directory, `containers` e `data`, possiamo lanciare `lhc caddy /var/lhc/containers/ /var/lhc/data`.
|
|
|
+lhc fará le seguenti operazioni.
|
|
|
|
|
|
-```bash
|
|
|
-dd if=/dev/zero of=./testfs/datafs bs=1024 count=30720
|
|
|
-losetup /dev/loop0 ./testfs/datafs
|
|
|
-mkfs.ext4 /dev/loop0
|
|
|
-mkdir data
|
|
|
-mount /dev/loop0 data
|
|
|
-```
|
|
|
+1. crea le dir `/var/lhc/containers/caddy` e `/var/lhc/data/caddy`
|
|
|
+2. in `/var/lhc/containers/caddy` mette la rootfs di alpine
|
|
|
+3. crea un utente `caddy` nell'host
|
|
|
+4. prepara un file di configurazione per lanciare `runc` con i dati di cui sopra
|
|
|
+5. monta `/var/lhc/containers/caddy` con loggedfs filtrando le chiamate `open` e `readlink`
|
|
|
+6. lancia una `chroot` dentro `/var/lhc/containers/caddy`
|
|
|
+6a. qui si aspetta che noi installiamo e configuriamo il servizio
|
|
|
+7. quando la chroot esce, prende tutti i file aperti, sposta il container originale in `/var/lhc/containers/caddy.orig` e dentro `/var/lhc/containers/caddy` mette solo i file che sono stati aperti.
|
|
|
|
|
|
-stessa cosa per la partizione dei container (per ora non la monto in read-only)
|
|
|
+il grosso del lavoro che rimane é il punto __6a__, ovvero l'installazione e la configurazione del servizio:
|
|
|
|
|
|
-```bash
|
|
|
-dd if=/dev/zero of=./testfs/containerfs bs=1024 count=30720
|
|
|
-losetup /dev/loop1 ./testfs/containerfs
|
|
|
-mkfs.ext4 /dev/loop1
|
|
|
-mkdir containers
|
|
|
-mount /dev/loop1 containers
|
|
|
-```
|
|
|
+### Apache setup
|
|
|
|
|
|
-creo le dir per il container
|
|
|
|
|
|
-```bash
|
|
|
-mkdir containers/caddy
|
|
|
-mkdir data/caddy
|
|
|
-```
|
|
|
-
|
|
|
-e ci scompatto dentro la rootfs di alpine linux: `tar xvf rootfs/alpine-minirootfs-3.5.1-aarch64.tar.gz -C containers/caddy`
|
|
|
-ora sará sufficiente `run spec` per creare un file di configurazione di runc e modificare i parametri del file che ci interessano:
|
|
|
-
|
|
|
-
|
|
|
-```js
|
|
|
-// prima di tutto il path della rootfs (quindi cercando 'root')
|
|
|
-"root": {
|
|
|
- "path": "./containers/caddy",
|
|
|
- // per ora non lo mettiamo read-only perché dobbiamo prima installare
|
|
|
- // quello che ci interessa, per il deploy questo bisogna metterlo a true
|
|
|
- "readonly": false
|
|
|
-}
|
|
|
-
|
|
|
-// poi dentro la voce "mounts" aggiungiamo la nostra partizione per i dati
|
|
|
-"mounts": [
|
|
|
- {
|
|
|
- "type": "bind",
|
|
|
- "destination": "/data",
|
|
|
- "source": "./data/caddy",
|
|
|
- "options": ["rbind", "rw", "noexec"]
|
|
|
- },
|
|
|
-```
|
|
|
-
|
|
|
-
|
|
|
-a questo punto possiamo far partire il container con `runc run caddy` e siamo dentro,
|
|
|
-non rimane che configurare il servizio che ci serve, in questo caso caddy.
|
|
|
-Potrei fare velocemente con un `apk update` e `apk add caddy` ma il pacchettizzato
|
|
|
-manca di alcune funzionalitá quindi scelgo di scaricarlo direttamente dal sito:
|
|
|
-
|
|
|
-```
|
|
|
-apk update
|
|
|
-apk add wget
|
|
|
-wget 'https://caddyserver.com/download/build?os=linux&arch=arm64&features=git,filemanager,cors,expires,minify' -O caddy.tar.gz
|
|
|
-
|
|
|
-```
|
|
|
-da adesso in poi, dobbiamo cercare di configurare il servizio per usare /data/ come path per tutti i file che vuole utilizzare in scrittura. Per fare ció possiamo fare in tanti modi, iniziamo a vedere cosa succede ad avviarlo manualmente:
|
|
|
-
|
|
|
-
|
|
|
-```bash
|
|
|
-```
|
|
|
-
|
|
|
-Ora ci serve far partire il container direttamente con `caddy` e non con la shell
|
|
|
-
|
|
|
-Ultima cosa, non voglio avere una shell dentro il container o altri tool (metti che appunto ci bucano, mica vogliamo fargli un favore), ma come fare? ci serve qualcosa che trovi tutti i file aperti da `caddy` e quello che mi viene in mente é `strace`, quindi da dentro il container installiamo strace (`apk add strace`) e lanciamo nginx con strace redirigendo `stderr` su un file (`strace nginx 2> strace.log`). Dobbiamo cercare di "attivare" tutte le funzionalitá del servizio in modo che becchiamo tutti i file che possono essere utilizzati da `nginx` (in questo caso quello che mi viene in mente é di lanciare una richiesta HTTP a nginx per dire, tipo dall'host faccio un `wget localhost`).
|
|
|
-Ora killiamo lo strace `kill -9 strace` e cerchiamo le syscallscall a open dentro il log:
|
|
|
-```bash
|
|
|
-nginx5:~# grep open strace.log
|
|
|
-open("/etc/ld-musl-x86_64.path", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
|
|
|
-open("/lib/libpcre.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
|
|
|
-open("/usr/local/lib/libpcre.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
|
|
|
-open("/usr/lib/libpcre.so.1", O_RDONLY|O_CLOEXEC) = 3
|
|
|
-open("/lib/libssl.so.39", O_RDONLY|O_CLOEXEC) = 3
|
|
|
-open("/lib/libcrypto.so.38", O_RDONLY|O_CLOEXEC) = 3
|
|
|
-open("/lib/libz.so.1", O_RDONLY|O_CLOEXEC) = 3
|
|
|
-open("/etc/localtime", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
|
|
|
-open("/var/lib/nginx/logs/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 3
|
|
|
-open("/etc/ssl/openssl.cnf", O_RDONLY) = -1 ENOENT (No such file or directory)
|
|
|
-open("/etc/nginx/nginx.conf", O_RDONLY) = 4
|
|
|
-open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 5
|
|
|
-open("/etc/group", O_RDONLY|O_CLOEXEC) = 5
|
|
|
-open("/", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 5
|
|
|
-open("/etc", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 6
|
|
|
-open("/etc/nginx", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 7
|
|
|
-open("/etc/nginx/modules", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 8
|
|
|
-open("/etc/nginx/mime.types", O_RDONLY) = 5
|
|
|
-open("/", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 5
|
|
|
-open("/etc", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 6
|
|
|
-open("/etc/nginx", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 7
|
|
|
-open("/etc/nginx/conf.d", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 8
|
|
|
-open("/etc/nginx/conf.d/default.conf", O_RDONLY) = 5
|
|
|
-open("/data/error.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 4
|
|
|
-open("/var/log/nginx/access.log", O_WRONLY|O_CREAT|O_APPEND, 0644) = 5
|
|
|
-open("/data/nginx.pid", O_RDWR|O_CREAT|O_TRUNC, 0644) = 8
|
|
|
-nginx5:~#
|
|
|
-```
|
|
|
-
|
|
|
-giá guardando gli errori (ENOENT) possiamo vedere alcune cosette, in particolare che ssl non funzionerá (probabilmente manca il pacchetto, ma a noi ora non interessa) e che mi sono dimenticato di cambiare qualche path dei log. comunque, i file mostrati qui sopra sono gli unici che devono rimanere dentro il container per fare in modo che tutti continui a funzionare, quindi estrapoliamo tutti i path:
|
|
|
-
|
|
|
-```bash
|
|
|
-nginx:~# grep open strace.log | sed "s/.*\"\(.*\)\".*/\1/" | sort | uniq > needed_files
|
|
|
-nginx:~# cat needed_files
|
|
|
-/
|
|
|
-/data/error.log
|
|
|
-/data/nginx.pid
|
|
|
-/etc
|
|
|
-/etc/group
|
|
|
-/etc/ld-musl-x86_64.path
|
|
|
-/etc/localtime
|
|
|
-/etc/nginx
|
|
|
-/etc/nginx/conf.d
|
|
|
-/etc/nginx/conf.d/default.conf
|
|
|
-/etc/nginx/mime.types
|
|
|
-/etc/nginx/modules
|
|
|
-/etc/nginx/nginx.conf
|
|
|
-/etc/passwd
|
|
|
-/etc/ssl/openssl.cnf
|
|
|
-/lib/libcrypto.so.38
|
|
|
-/lib/libpcre.so.1
|
|
|
-/lib/libssl.so.39
|
|
|
-/lib/libz.so.1
|
|
|
-/usr/lib/libpcre.so.1
|
|
|
-/usr/local/lib/libpcre.so.1
|
|
|
-/var/lib/nginx/logs/error.log
|
|
|
-/var/log/nginx/access.log
|
|
|
-
|
|
|
-```
|
|
|
-
|
|
|
-ora, preparo un'altra directory con solo questi files `mkdir containers/nginx-prod` e copio dentro solo i files di cui sopra:
|
|
|
-
|
|
|
-```bash
|
|
|
-CONTAINER_ROOT=`pwd`/containers/nginx
|
|
|
-for f in `cat containers/nginx/root/only_needed` ; do echo $f; if [ -f $CONTAINER_ROOT$f ]; then mkdir -p containers/nginx-prod/`dirname $f` ; cp $CONTAINER_ROOT$f containers/nginx-prod/`dirname $f`; fi; done
|
|
|
-
|
|
|
-```
|
|
|
+### Caddy setup
|