Knot Resolver: Storia di un DNS Application Firewall

Qualche mese fa ho iniziato a testare seriamente Knot Resolver (kres) che, come riportato nella documentazione ufficiale, è "l'OpenResty dei DNS". Knot Resolver, sviluppato da cz.nic, è infatti un DNS caching resolver (anche indicato come full resolver) scritto in C e LuaJIT, supporta DNS privacy e DNSSEC e ha diversi moduli tra cui "DAF" acronimo di DNS Application Firewall e che permette di filtrare query DNS applicando azioni diverse a seconda del tipo di regola.

Tutti ormai conoscono il public DNS di CloudFlare 1.1.1.1 il cui servizio è gratuito con un focus sulle performance e sulla privacy, oltre che sulla sicurezza. Non tutti sanno che, però, CloudFlare usa Knot Resolver per erogare il servizio! Come riportato sul loro blog:

... In the end, we decided to build the system around the Knot Resolver from CZ NIC. This is a modern resolver that was originally released about two and a half years ago. By selecting the Knot Resolver, we also increase software diversity. The tipping point was that it had more of the core features we wanted, with a modular architecture similar to OpenResty. The Knot Resolver is in active use and development. [https://blog.cloudflare.com/dns-resolver-1-1-1-1/]

La cosa che mi ha colpito inizialmente è che la configurazione è, in realtà, uno script Lua (e, come potete immaginare, questo ha subito attirato la mia attenzione). Scrivere la configurazione del resolver direttamente in Lua permette di intervenire sulla query DNS, e sulla response (o answer), in miliardi di modi differenti! Prima di iniziare vi propongo un piccolo esempio per farvi capire meglio le potenzialità di questo progetto:

net = { '0.0.0.0' }

user('nobody','nogroup')

modules = { 'hints' }

hints['foo.bar'] = '127.0.0.1'

Questo è un esempio di configurazione possibile, vediamolo riga per riga:

  • la prima, net = { '0.0.0.0' }, stabilisce che kres ascolterà su tutte le interfacce disponibili
  • la seconda riga user('nobody','nogroup') definisce l'utente e il gruppo con cui eseguirà il daemon
  • la terza riga include moduli modules = { 'hints' }. modules è una Lua table il cui contenuto è una lista di moduli Lua da includere. hints è un modulo che permette la risoluzione statica di record in un formato "/etc/hosts like" oppure definiti manualmente. Funziona con record di tipo A, AAAA e PTR.
  • la quarta riga definisce la risoluzione statica dell'host foo.bar in 127.0.0.1

Effettuando una query verso kres richiedendo la risoluzione del hostname foo.bar otteniamo questo risultato:

$ dig foo.bar @localhost

; <<>> DiG 9.10.6 <<>> foo.bar @localhost
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 22961
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;foo.bar.			IN	A

;; ANSWER SECTION:
foo.bar.		0	IN	A	127.0.0.1

;; Query time: 27 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Feb 07 22:18:55 CET 2019
;; MSG SIZE  rcvd: 52

Lo so, questo non lo rende diverso da inserire 127.0.0.1 foo.bar all'interno del file hosts.

Docker run FTW

Per l'installazione di kresd vi invito a visitare il link https://knot-resolver.readthedocs.io/en/stable/build.html oppure, se preferite una soluzione rapida e indolore, docker fa al caso vostro. Infatti è possibile scaricare l'immagine ufficiale di knot-resolver da dockerhub su cznic/knot-resolver. Per una fase di test, vi consiglio di sostituire l'entrypoint con /bin/bash in modo da poter modificare la configurazione con più facilità:

$ docker run \
    --name knot \
    -Pit \
    --entrypoint /bin/bash \
    cznic/knot-resolver

Query policies e DNS over TLS

Knot-Resolver ha diversi moduli, uno tra questi è il modulo "query policy" che permette di bloccare, fare rewrite o alterare le query DNS effettuate da un utente. Una policy è composta da un Filter e una Action. I filtri determinano come identificare la query su cui applicare una determinata azione, essi possono essere: all() che applica sempre l'azione, pattern() applica l'azione se il QNAME (query name) corrisponde a un pattern Lua, suffix() applica l'azione solo se il QNAME fa parte di una lista di domini inclusi in una Lua table e infine rpz() che sta per Response Policy Zone https://dnsrpz.info/.

Le action sono funzioni che modificano la query DNS. I nomi delle action sono abbastanza esplicativi, come ad esempio: PASS, DENY, DROP, REFUSE, ecc.. Per fare un esempio, una semplice policy che risponde con un NXDOMAIN per qualsiasi nome di un determinato dominio, potremmo scrivere una policy come questa:

policy.add(
    policy.suffix(
        policy.DENY, 
        {todname('rev3rse.it')}
    )
)

La funzione todname() converte un nome dal formato string al formato DNS wireformat in cui un nome è identificato da una successione di ottetti che definiscono la lunghezza della successiva label. Ad esempio www.google.com viene rappresentato con: 0x03 www 0x06 google 0x03 com 0x00:

Una tra le Action che hanno contribuito a far conoscere Knot-Resolver è sicuramente TLS_FORWARD. Questa funzione permette di risolvere una query inviata da un utente inoltrandola tramite una connessione TLS a un DNS Server esterno in grado di poter gestire una query via TLS. Questo permette di evitare che il traffico DNS dell'utente possa viaggiare in chiaro verso l'esterno. Non a caso il public DNS 1.1.1.1 di CloudFlare possa accettare connessioni TLS, e a kres basta una semplice Policy.

Come prima cosa importiamo il certificato da 1.1.1.1 usando openssl -showcerts:

$ openssl s_client -showcerts \
    -connect 1.1.1.1:443 </dev/null 2>/dev/null | \
    openssl x509 -outform PEM > /etc/knot-resolver/DigiCertGlobalRootCA.pem

fatto ciò, aggiungiamo la seguente policy:

policy.TLS_FORWARD(
    {{
        '1.1.1.1', 
        hostname='cloudflare-dns.com.', 
        ca_file='/etc/knot-resolver/DigiCertGlobalRootCA.pem' 
    }}
)

In questo modo, le query inviate verso kresd saranno sempre in chiaro ma confinate nell'interfaccia di loopback, a sua volta kresd invierà la nostra query a 1.1.1.1 in TLS evitando così che venga trasmessa in chiaro verso internet.

DNS Application Firewall (DAF)

Un'altro modulo molto interessante è daf ovvero DNS Application Firewall. Questo modulo semplifica la creazione di policy per filtrare una query DNS. Per comprendere meglio le potenzialità di questo modulo, ecco tre esempi:

modules = { 'daf' }

daf.add 'qname ~ %w+.example.com deny'

daf.add 'qname ~ %w+.example.com AND src = 192.0.2.0/24 deny'

daf.add 'src = 127.0.0.0/8 rewrite example.com A 127.0.0.2'

Chi ha familiarità con Lua avrà sicuramente notato che la sintassi dell'unico argomento della funzione daf.add è, in realtà, un Lua pattern. A prima vista può sembrare molto simile a una regular expression, ma i pattern in Lua hanno molti limiti rispetto a PCRE e hanno una sintassi differente.

La prima riga dell'esempio qui sopra include, semplicemente, il modulo DAF.

La seconda riga aggiunge una regola che "blocca" la risoluzione della query se il nome qname corrisponde al formato <qualsiasi carattere alfanumerico>.example.com. Se volessimo tradurlo in PCRE sarebbe qualcosa di molto simile a [a-z0-9]+\.example\.com.

La terza riga impedisce la risoluzione dello stesso pattern della regola precedente, ma solo se la query arriva dalla rete 192.0.2.9/24.

L'ultima riga imposta una risoluzione statica di example.com con un record A 127.0.0.2 solo se la query arriva dalla rete 127.0.0.0/8.

DNS Application Firewall in live

Se trovi interessante questo argomento, allora non perderti la Rev3rse Live di giovedì 28 Febbraio 2019, in cui KNX parlerà di Phishing con Evilginx e 2FA Bypass. Io partirò da questo articolo, e farò vedere come creare un DAF in Python e come creare un modulo Lua per Knot Resolver per proteggere gli utenti della mia rete dall'attività di phishing di KNX. Se non lo hai ancora fatto, iscriviti al canale:
https://www.youtube.com/rev3rsesecurity

Rev3rse Security Live