
Traduzione in spagnolo dei CVE-2022-1015 e 1016 scoperti e documentati da David.
Questo README.md è una traduzione del blog di David. David ha trovato i CVE 1015 e 1016 nel kernel Linux. Puoi visitare la sua pagina web per leggere il documento originale.
Ecco i suoi profili social:
Pubblicato il 2 aprile 2022.
Questi problemi dovrebbero essere sfruttabili nelle configurazioni predefinite della versione più recente di Ubuntu e di RHEL. Ho scritto la mia proof of concept (PoC) per il CVE-2022-1015 prendendo di mira la versione del kernel 5.16-rc3 di Arch Linux.
Questo documento è rivolto alle persone che abbiano una conoscenza di base del kernel Linux in termini di funzionalità e sicurezza. Ho cercato di rendere questo documento accessibile anche a chi non ha familiarità con lo stack di rete, così da renderlo fruibile a tutti.
Ecco una guida alla lettura:
A metà febbraio, il programma di sicurezza di Google ha annunciato che avrebbe continuato il suo programma di ricompense kCTF, offrendo ricompense che vanno da $31.337 fino a 91.337 dollari per un exploit nel kernel Linux in grado di elevare i privilegi all'utente root da processi senza privilegi in una sandbox nsjail.
Essendo uno studente squattrinato, ovviamente questo ha catturato la mia attenzione. Era la mia prima volta alla ricerca di una vulnerabilità del "mondo reale", ma nelle mie avventure giocando a CTF con la mia squadra, ho acquisito familiarità con il kernel Linux in termini di sicurezza. Dopo ore e ore con pochissimo o nessun progresso (ma con una maggiore conoscenza di Linux), sono riuscito a trovare alcune vulnerabilità nel modulo nf_tables.
Purtroppo, alla fine dei conti, mi sono reso conto che questo modulo non era presente nelle regole del kCTF di Google (quindi non ho ricevuto alcuna ricompensa per queste due vulnerabilità). Ma ovviamente le ho comunque segnalate e ho scritto un exploit LPE (Escalation dei Privilegi Locali) per il CVE-2022-1015.
Bene, quindi hai deciso che troverai alcune vulnerabilità in Linux. E adesso? Linux è un progetto gigantesco, ed è molto facile non vedere il bosco per gli alberi (ti concentri così tanto sui dettagli da perdere di vista ciò che è davvero importante, non hai una visione generale della situazione). Per peggiorare le cose, molte parti non sono documentate e devi leggere un sacco di codice per capire cosa sta succedendo.
Ho iniziato cercando di avere una prospettiva dettagliata del modello di sicurezza di Linux. Trovare un bug è una cosa; ma trovare un buon bug è un'altra cosa molto diversa. Dopotutto, non tutti i bug sono creati uguali:
FS_USERNS_MOUNT, nel qual caso puoi montarli nello user namespace.CAP_SYS_ADMIN o CAP_NET_ADMIN.
/proc/config.gz. I moduli possono essere caricati (=m) oppure compilati separatamente e caricati in fase di esecuzione (=y).Queste restrizioni ci aiutano a capire i limiti dei sottosistemi in cui possiamo cercare vulnerabilità. Penso che sia una buona idea prenderti il tuo tempo per cercare di pianificare il tuo attacco all'obiettivo che desideri.
Ho già imparato la lezione sul punto precedente. Come ho detto, il modulo nf_tables non era caricato nell'istanza che ci ha presentato kCTF. Avrei potuto accorgermene fin dall'inizio e risparmiarmi la delusione :p. D'altra parte, probabilmente non staresti leggendo questo blog adesso se me ne fossi accorto prima; suppongo che le cose siano andate bene dopo tutto.
Una spiegazione del perché il COS, il fork Linux container-optimized di Google, non avesse nf_tables può essere trovata qui e qui.
Dopo aver valutato i punti sopra menzionati, ho deciso che la mia migliore strada per iniziare sarebbe probabilmente stata guardare il codice sorgente della rete. Molte delle funzionalità interessanti lì richiedono CAP_NET_ADMIN, ma come ho detto, in realtà questo non è un problema. Al contrario, sospetto che i componenti che richiedono capacità speciali siano generalmente meno sicuri, poiché gli sviluppatori del kernel possono avere un falso senso di sicurezza.
Ho anche fatto lo sforzo di scegliere il sottosistema di cui volevo saperne di più; in questo modo, anche se non trovi alcun bug, potrai comunque imparare un sacco di cose interessanti.
Ho studiato molti sottosistemi di rete, ma non ho trovato nulla di importante. Dopo aver navigato nella sottodirectory net/, mi sono imbattuto nel modulo nf_tables. Sembrava un po' complesso, quindi ho deciso di prendermi del tempo per conoscerlo.
Netfilter (net/netfilter) è un sottosistema di rete piuttosto grande nel kernel. In breve, netfilter posiziona hook nei moduli di rete a cui altri moduli possono registrare dei gestori (handler). Quando viene raggiunto un hook, il controllo viene delegato a questi gestori, che possono operare sulla rispettiva struttura dei pacchetti di rete. I gestori possono accettare, scartare e modificare i pacchetti.
Dopo alcune ore a navigare l'API di nf_tables (net/netfilter/nf_tables_api.c) per iniziare a capire come funziona esattamente, ho deciso di dare un'occhiata alla validazione logica dei registri che l'utente invia, e ho trovato alcuni comportamenti sospetti. Dopo aver riflettuto se stavo impazzendo o no, ho scritto una piccola PoC (proof of concept) per cercare di attivare la vulnerabilità che avevo trovato: una vulnerabilità nota come OOB (out-of-bounds, fuori dai limiti), che consente di leggere e scrivere nella memoria stack.
Dopo aver trovato un modo per far trapelare gli indirizzi del kernel, prendere il controllo del puntatore di memoria è stato abbastanza facile. Dopo un po' di ROP (programmazione orientata al ritorno), la shell con privilegi root è diventata una realtà.
Ogni volta che la routine init di un'espressione deve fare il parsing di un registro da un messaggio utente di netlink, viene chiamata la routine nft_parse_register_load o nft_parse_register_store a seconda che si tratti di un registro sorgente o di un registro di destinazione. Ho aggiunto alcuni commenti:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
Le varianti `*_store` sono virtualmente identiche, tranne che consentono di scrivere nel *verbdict* in alcune condizioni.
Dopo aver rivisto l'ultima validazione, qualcosa è davvero fuori posto qui:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
Questo sembra essere un integer overflow, non credete? Se possiamo fare in modo che reg contenga un valore moltiplicato per 4 che generi un overflow quando gli viene sommato len, possiamo soddisfare le condizioni. In nft_parse_register_load, l'ultimo byte significativo di reg viene ancora scritto nel puntatore u8 *sreg, cadendo nel nostro nft_expr che viene successivamente usato come indice.```c
*sreg = reg;
¿De verdad podemos? `reg` es un `enum nft_registers` en la validación de la rutina, de todas formas. Podemos pasar valores que tengan un rango entre `0x00000001` hasta `0xfffffffb` inclusive, el rango de `nft_parse_register`; pero ¿será `reg` un valor de 32 bits en `nft_validate_register_load`? Se sabe que los compiladores podrían encoger los *enum types* si un tipo más pequeño puede representar todos los valores. Vamos a obtener una segunda opinión.
Obtenido del manual de GCC:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
TL;DR? Dipende dall'ABI e dal possibile grado di ottimizzazione. Non sono riuscito a trovare alcuna prova concreta che questa opzione sia attivata per impostazione predefinita nelle build di Linux.
Ma l'assembler non mente mai. Diamo un'occhiata:```objdump.x86asm
0000000000001b60 <nft_parse_register_load>:
1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5>
1b65: 55 push rbp
1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4]
1b69: 0f c8 bswap eax
1b6b: 89 c7 mov edi,eax
1b6d: 8d 48 fc lea ecx,[rax-0x4]
1b70: c1 e7 04 shl edi,0x4
1b73: 48 89 e5 mov rbp,rsp
1b76: c1 ef 02 shr edi,0x2
1b79: 83 f8 04 cmp eax,0x4
1b7c: 89 f8 mov eax,edi
1b7e: 0f 47 c1 cmova eax,ecx
1b81: 85 d2 test edx,edx
1b83: 74 13 je 1b98 <nft_parse_register_load+0x38>
1b85: 83 f8 03 cmp eax,0x3
1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38>
1b8a: 8d 14 82 lea edx,[rdx+rax*4]
1b8d: 83 fa 50 cmp edx,0x50
1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f>
1b92: 88 06 mov BYTE PTR [rsi],al
1b94: 5d pop rbp
1b95: 31 c0 xor eax,eax
1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea
1b9d: 5d pop rbp
1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde
1ba4: 5d pop rbp
1ba5: c3 ret
Le chiamate di funzione sono allineate abbastanza bene. Le operazioni importanti sono in `1b8a`:```objdump.x86asm
lea edx, [rdx+rax*4]
cmp edx, 0x50
ja 1b9f <nft_parse_register_load+0x3f>
mov BYTE PTR [rsi], al
rax è il risultato di ntf_parse_register, rdx è len fornita, e rsi è il puntatore sreg. Ci siamo già tolti il dubbio.
nft_parse_register_store mostra lo stesso comportamento. Finché i registri vivono nello stack, la nostra vulnerabilità OOB sarà ovviamente relativa allo stack. Questo è un bene, perché con un po' di fortuna potremo sovrascrivere e ritornare memoria direttamente.
Per fare un esempio di input vulnerabile, un registro 0xfffffffb e una lunghezza di 0x20 porteranno a valutare 0xfffffffb * 4 + 0x20 = 0x0c < 0x50. Dopo la validazione, (u8)0xfffffffb = 0xfb verrà scritto in *sreg.
C'è però un problema: esistono espressioni che ci permettano di usare una lunghezza in grado di causare un overflow quando viene eseguita la somma? Dopo qualche ricerca, ho scoperto che nft_bitwise e nft_payload ti permettono di fornire come input la tua lunghezza, da 0x00 a 0xff. Molte altre espressioni sembrano avere lunghezze statiche molto piccole.
Per ora la cosa sembra promettente. Il passo successivo è prendere questi exploit primitives (capacità generica acquisita durante un exploit) e usarli.
Se riusciamo a definire il tipo di potere che il nostro exploit può darci, sfruttare questa vulnerabilità dovrebbe essere più facile. Quindi, abbiate un po' di pazienza, perché vedremo un po' di aritmetica.
Ci sono tre punti che possiamo usare per il nostro overflow per la moltiplicazione del registro, dato che questo viene moltiplicato per 4 = 2^2: 2^32 - 1, 2^31 - 1 e 2^30 - 1 (rispettivamente 0xffffffff, 0x7fffffff e 0x3fffffff). Questi valori possono continuare a diminuire finché, sommando la nostra lunghezza massima consentita dopo che questa è stata moltiplicata per quattro, non si otterrà più un overflow. Un altro punto da tenere in considerazione è che non possiamo usare valori maggiori di 0xfffffffb, come accennato in precedenza.
Data una lunghezza specifica, i byte meno significativi che possono permettere un overflow usando questa lunghezza formeranno il nostro intervallo di indici OOB utilizzabile.
Dopotutto, non importa quali punti di overflow vengano usati. Prendi ad esempio i seguenti valori con un LSB (bit meno significativo) di 0xf0:```
0xfffffff0 * 4 = 0xffffffc0
0x7ffffff0 * 4 = 0xffffffc0
0x3ffffff0 * 4 = 0xffffffc0
D'ora in poi, useremo valori di registro vicini a `0x7fffffff`.
In precedenza abbiamo parlato di `nft_payload` e `nft_bitwise`. Alcune proprietà di queste espressioni sono:
* `nft_payload` può eseguire solo scritture *OOB*, mentre `nft_bitwise` può eseguire scritture e letture *OOB*.
* `nft_payload` può eseguire scritture *OOB* fino a 0xff byte di dati arbitrari.
* `nft_bitwise` in realtà può scrivere solo fino a `0x40` byte di dati arbitrari e può leggere solo `0x40` byte di dati che si trovano nello *stack* dello spazio dei registri.
* `nft_bitwise` richiede un `sreg` e un `dreg`, i quali devono superare la validazione con lo stesso valore di lunghezza.
* Abbiamo solo `0x40` byte di spazio dei registri, quindi vogliamo leggere o scrivere dallo spazio dei registri, ma non possiamo superare la validazione con una lunghezza maggiore di `0x40`.
Possiamo usare un valore di lunghezza più grande per `nft_bitwise`, ma significa che `sreg` e `dreg` devono essere fuori dai limiti, il che non sarebbe molto utile per i nostri scopi. Quindi, per ora lavoreremo con la lunghezza di `0x40`.
Tenendo conto di tutto ciò, che tipi di *exploit* potremo usare?
`nft_bitwise` ha una lunghezza massima di `0x40`. Ciò significa che il valore del registro moltiplicato per quattro dovrebbe essere almeno `0xffffffc0`. Il valore più grande che possiamo ottenere moltiplicando per quattro è `0xfffffffb`, e poiché `0xfffffffb + 0x40 = 0x3b <= 0x50`, supererà la validazione.
`0x7ffffff0 * 4 = 0xffffffc0`: il limite inferiore è `0xf0`.
`0x7fffffff * 4 = 0xfffffffb`: il limite superiore è `0xff`.
Traducendo in [*byte offsets*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)):```
0xc1 * 4 = 0x304
0xeb * 4 + 0xff = 0x4ab
nft_payload può scrivere fuori dai limiti attraverso gli offset [0x304, 0x4ab] da struct nft_regs.
Ora che tutto questo è stato chiarito, cosa c'è effettivamente nello stack a questi offset?
La routine nft_do_chain può essere chiamata attraverso molti percorsi di codice. Esistono molti fattori che cambieranno la forma dello stack prima del stack frame (frame di stack) di nft_do_chain:
Che il chain hook sia un input o output.
input, l'hook si attiverà nel contesto softirq del rispettivo dispositivo di rete con lo stack softirq.output, l'hook si attiverà nel contesto di syscall (chiamata di sistema) send* con lo stack di syscall.Il protocollo che stiamo usando.
Penso che tu possa ottenere molte varianti di call stack usando diverse combinazioni di protocolli, interfacce e localizzazioni degli hook. Per il momento useremo un chain hook configurato come output con un pacchetto UDP.

Layout dello stack e portata fuori dai limiti in nft_do_chain quando un pacchetto UDP inviato raggiunge un hook configurato come output
Per poter creare un exploit stabile, dovremo prima filtrare l'indirizzo dell'immagine del kernel.
L'indirizzo dell'immagine del kernel ha 9 bit di entropia (misura dell'incertezza esistente rispetto a un insieme di messaggi, dal quale ne verrà ricevuto uno solo), il che significa che ci sono 512 posizioni diverse in cui il kernel può essere caricato. A seconda dello scenario del tuo attacco, c'è una probabilità di 1 su 512 che l'attacco funzioni correttamente; ma sarebbe meglio se riuscissimo a ottenere un exploit più stabile di così.
Il passo più semplice è provare a usare la nostra capacità di lettura fuori dai limiti che nft_bitwise ci ha procurato per copiare alcuni dei dati dello stack nei nostri registri. Poiché l'intervallo totale che possiamo leggere ha una lunghezza di0x7c byte, c'è una probabilità abbastanza alta che l'indirizzo del kernel sia lì.

Portata fuori dai limiti di nft_bitwise
Oggi è il nostro giorno! Ce ne sono due:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
Scrivere questo nei registri è una cosa, ma estrarli è un'altra. Dopo aver investigato, sembra che non esista un modo semplice per leggere direttamente i registri quando `nft_do_chain` è in esecuzione.
Nel mio report originale a [email protected], mi informarono dell'espressione `nft_dynset` da un maintainer di netfilter, che ha supporto per [*dynamic sets*](https://en.wikipedia.org/wiki/Dynamic_set) che possono agire come una sorta di database in grado di scrivere e leggere attraverso diverse esecuzioni di `nft_do_chain`. A quanto pare, `nft_payload` ha anche la capacità di scrivere al pacchetto da sé, non me ne ero accorto.
Invece, ho deciso di continuare con il mio [*side-channel attack*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral). A causa della natura di `nf_tables`, puoi causare effetti collaterali. In effetti, potresti dire che non sono nemmeno effetti collaterali, ma effetti primari.
Creando regole che scartano o accettano il pacchetto basandosi sul valore dell'indirizzo di memoria del kernel che stiamo copiando, poco a poco possiamo dedurre quale sia il valore esaminando se i pacchetti che inviamo sono stati anch'essi ricevuti.
1. Creare un *socket* UDP che riceve pacchetti su `127.0.0.1:9999`:
* Dovrebbe ricevere pacchetti in un thread diverso.
* Un messaggio dovrebbe essere rimandato indietro per ogni pacchetto ricevuto.
2. Aggiungi una regola che:
1. Copia l'indirizzo del kernel nei registri con `nft_bitwise`.
2. Usa `nft_cmp_expr` per confrontare l'indirizzo con una costante.
3. Scarta un pacchetto se la comparazione valutata è vera.
3. Invia un pacchetto UDP a `127.0.0.1:9999`
1. Possiamo determinare un po' di informazioni sull'indirizzo del kernel in base al fatto se riceviamo un messaggio di ritorno.
4. Ripeti 2 e 3 con i valori appropriati finché non hai abbastanza informazioni per determinare l'informazione da sola.

Esistono ancora alcune avvertenze. Per esempio, il pacchetto che riceviamo potrebbe essere scartato senza alcun preavviso. Per mitigare questo, possiamo aggiungere una riduzione del rumore, per la quale avremo bisogno di una *base chain* e di una *auxiliary regular chain*.
*Regola nella base chain:*
| # | Espressione | Argomenti | Commento |
| --- | -------------------- | -------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------- |
| 0 | `nft_payload` | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | Scrivere la porta di destinazione del pacchetto nel registro 8. |
| 1 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999 | Confronta la porta di destinazione con `9999` e restituisci `NFT_BREAK` se il risultato non è uguale. |
| 2 | `nft_payload` | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8 | Scrivere i primi otto byte del pacchetto nel registro 8. |
| 3 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de | Confronta i primi otto byte con il valore magico e restituisci `NFT_BREAK` se non è uguale. |
| 4 | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain | Poiché la regola sta ancora valutando, le condizioni devono corrispondere, e viene chiamata la nostra *auxiliary chain*. |
*Regola nella auxiliary chain:*
| # | Espressione | Argomenti | Commento |
| --- | --------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | `nft_bitwise` | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | Scrive l'indirizzo del kernel nei registri usando la lettura fuori dai limiti, shiftato di `SHIFT_AMT` bit per ottenere il byte dell'indirizzo desiderato nel registro corretto. |
| 1 | `nft_cmp` | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND | Confronta i byte dell'indirizzo del kernel con `COMPARAND`, restituisci `NFT_BREAK` se questo risultato non è uguale. |
| 2 | `nft_immediate` | verdict=NFT_DROP | Scarta il pacchetto se il byte dell'indirizzo è più grande di `COMPARAND`. |
Controllando la porta di destinazione e confrontando i primi otto byte interni dell'*header* con un valore magico, possiamo attivare gli effetti collaterali per i pacchetti che vogliamo.
Cambiando dinamicamente `COMPARAND` possiamo fare una ricerca binaria per trovare il byte dell'indirizzo del kernel in tempo `0(log(n))`. Cambiando dinamicamente `SHIFT_AMT` ai prossimi multipli di otto possiamo spostarci al prossimo byte di memoria e ricominciare.
#### 4.3.1 Pseudo-codice del filtro
Un po' di codice in python per filtrare l'indirizzo di memoria. La cosa divertente è che avrei potuto facilmente implementare questo in python. Ricordate che non sempre dovete fare i vostri exploit per un kernel in C :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''
def leak_byte(pos):
s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
s.settimeout(200) # 200ms debería ser más que suficiente
s.bind(("127.0.0.1", 1234))
# buscar los límites
low = 0, high = 255
while True:
mid = (low + high) // 2
# si encontramos el valor, lo regresamos
if low == high:
s.close()
return mid
set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)
# Enviar el paquete y activar la auxiliary chain
s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))
# El hilo secundario regresa a 127.0.0.1:1234
res = s.recvfrom(0x2000)
if not res:
'''
nuestro paquete fue soltado
ya que no se regresó nada en los 200ms
lo que significa que
byte to leak >= mid
el byte a filtrar es mayor o igual a mid (127)
'''
low = mid
else:
'''
[sanity check o prueba de cordura]
se usa para evaluar rápidamente si
el valor a calcular es siquiera posible
https://es.wikipedia.org/wiki/Prueba_de_cordura
'''
if res != b"MSG_OK":
print("Something went wrong")
return None
'''
Nuestro paquete fue aceptado, lo que
significa que
byte to leak < mid
byte a filtrar es menor a mid (127)
'''
high = mid - 1
leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]
Ora che abbiamo ottenuto il leak, l'esecuzione arbitraria di codice dovrebbe essere molto facile. La scrittura fuori dai limiti di nft_payload dovrebbe poter scrivere un attacco RoP in catena per lo stack, giusto?
Nop. Non abbiamo avuto molta fortuna, almeno su questo kernel in particolare. La scrittura fuori dai limiti di nft_payload quasi interamente si allinea con lo stack frame della routine udp_sendmsg. L'indirizzo di udp_sendmsg si trova all'offset +0x2f8 relativo ai registri; questa posizione è troppo bassa per essere raggiunta con nft_payload o nft_bitwise (possiamo iniziare a scrivere a partire dall'offset +0x304, così vicino...). L'indirizzo inet_sendmsg si trova all'offset +0x4a8. Tecnicamente possiamo raggiungerlo (e sovrascrivere i tre byte inferiori), ma c'è uno stack canary (tecnica utilizzata per la rilevazione di uno stack buffer overflow prima che l'esecuzione di codice malintenzionato possa avvenire) all'indirizzo +0x0458 che dobbiamo anche sovrascrivere per riuscirci. Questo ovviamente farebbe crashare il kernel, quindi fare ciò non è un'opzione.
Sono riuscito a usare questo metodo su un'altra build del kernel, ma sembra che provare a fare lo stesso per il kernel che sto usando per questo blog sarà un po' più difficile.
Ora, forse possiamo fare un po' di contrived stack frame hacking per sovrascrivere le variabili locali in udp_sendmsg. Potremmo anche provare a sovrascrivere il verdict chain pointer, usando un valore dei registri p. es. 0x7fffff00 (penso che potrebbe essere una tecnica geniale; considerando la sfida).
Proviamo a cambiare la base chain hook che usiamo. Stavamo usando una chain output, cosa succederebbe se la cambiassimo in una input?

Il diagramma della scrittura fuori dai limiti in nft_do_chain se un pacchetto UDP inviato raggiunge l'input hook
Questo sembra già un po' meglio! Possiamo sovrascrivere l'indirizzo di ritorno del frame di __netif_receive_skb_one_core (offset +0x328), che ritorna a __netif_receive_skb. Dato che si trova relativamente vicino all'altezza della portata fuori dai limiti del nostro nft_payload, possiamo far puntare il nostro indice OOB (fuori dai limiti) direttamente a questo indirizzo di ritorno, eludendo lo stack canary all'offset +0x310. L'offset +0x328 si traduce nell'indice 0xca.
Per attivare la sovrascrittura dell'indirizzo di ritorno, creiamo una nuova chain input nella tabella e aggiungiamo una regola con un nft_payload che scrive 0xff byte dall'header interno del pacchetto verso l'indice 0xca. Poi inviamo un pacchetto con il payload, e boom.

🥳 🥳 🥳 🥳 🥳
/proc/modules/proc/kallsymsrequest_module