
Spanische Übersetzung der CVE-2022-1015 und 1016, entdeckt und dokumentiert von David.
Diese README.md ist eine Übersetzung von Davids Blog. David fand die CVE-1015 und CVE-1016 im Linux-Kernel. Du kannst seine Webseite besuchen, um das Originaldokument zu lesen.
Hier sind seine Social-Media-Profile:
Veröffentlicht am 2. April 2022.
Diese Probleme sollten in den Standardkonfigurationen der neuesten Version von Ubuntu und von RHEL ausnutzbar sein. Ich schrieb meinen Proof-of-Concept (PoC) für CVE-2022-1015 mit Zielkernelversion 5.16-rc3 von Arch Linux.
Dieses Dokument richtet sich an Personen mit grundlegenden Kenntnissen des Linux-Kernels in Bezug auf Funktionalität und Sicherheit. Ich habe versucht, dieses Dokument für Personen ohne Kenntnisse des Netzwerk-Stacks zugänglich zu machen, um es für ein breites Publikum verständlich zu halten.
Hier ist eine Leseanleitung:
Mitte Februar kündigte das Google-Sicherheitsprogramm an, dass sie ihr kCTF-Belohnungsprogramm fortsetzen würden, mit Belohnungen von $31,337 bis $91,337 für einen Exploit im Linux-Kernel, der Privilegien auf den Root-Benutzer von nicht privilegierten Prozessen in einer nsjail-Sandbox eskaliert.
Als armer Student hat das natürlich meine Aufmerksamkeit erregt. Es war mein erstes Mal, dass ich nach einer „echten" Schwachstelle suchte, aber durch meine Abenteuer beim Spielen von CTF mit meinem Team bin ich mit dem Linux-Kernel in Bezug auf Sicherheit vertraut geworden.
Nach Stunden und Stunden mit kaum Fortschritt (aber mit mehr Wissen über Linux) gelang es mir, einige Schwachstellen im Modul nf_tables zu finden.
Leider stellte ich am Ende fest, dass dieses Modul in den Regeln des Google kCTF nicht vorhanden war (daher erhielt ich keine Belohnung für diese beiden Schwachstellen). Aber natürlich meldete ich sie trotzdem und schrieb einen LPE-Exploit (Local Privilege Escalation) für CVE-2022-1015.
Nun gut, du hast also beschlossen, einige Schwachstellen in Linux zu finden. Was nun? Linux ist ein riesiges Projekt, und es ist leicht, den Wald vor lauter Bäumen nicht zu sehen (du konzentrierst dich so sehr auf die Details, dass du den Überblick über das verlierst, was wirklich wichtig ist, du hast keinen Gesamtüberblick). Um die Sache noch schlimmer zu machen, sind viele Teile nicht dokumentiert und du musst eine Menge Code lesen, um zu verstehen, was vor sich geht.
Ich begann damit, eine detaillierte Perspektive auf das Linux-Sicherheitsmodell zu bekommen. Einen Bug zu finden ist eine Sache; aber einen guten Bug zu finden ist eine ganz andere. Schließlich sind nicht alle Bugs gleich geschaffen:
FS_USERNS_MOUNT spezifiziert, in diesem Fall kannst du sie im user namespace mounten.CAP_SYS_ADMIN oder CAP_NET_ADMIN.
/proc/config.gz abgerufen werden. Module können als (=m) geladen oder separat kompiliert und zur Laufzeit geladen werden (=y)./proc/modules und verwenden, aber sie sind nicht immer zuverlässig, da Module dynamisch in den Kernel geladen werden können (z. B. ).Diese Einschränkungen helfen uns, die Grenzen der Dateisysteme zu verstehen, in denen wir nach Schwachstellen suchen können. Ich denke, es ist eine gute Idee, sich Zeit zu nehmen, um deinen Angriff auf das gewünschte Ziel zu planen.
Ich habe meine Lektion aus dem obigen Punkt gelernt. Wie erwähnt, war das Modul nf_tables in der von kCTF präsentierten Instanz nicht geladen. Ich hätte das von Anfang an bemerken und mir die Enttäuschung ersparen können :p. Andererseits würdest du diesen Blog jetzt wahrscheinlich nicht lesen, wenn ich es bemerkt hätte, ich schätze, die Dinge sind am Ende doch gut gelaufen.
Eine Erklärung, warum COS, Googles container-optimierter Linux-Fork, kein nf_tables hatte, findet sich hier und hier.
Nachdem ich die oben genannten Punkte bewertet hatte, entschied ich, dass mein bester Weg zum Start wahrscheinlich wäre, mir den Quellcode des Netzwerks anzusehen. Viele der interessanten Funktionen dort erfordern CAP_NET_ADMIN, aber wie erwähnt, ist das eigentlich kein Problem. Im Gegenteil, ich vermute, dass Komponenten, die spezielle Fähigkeiten erfordern, im Allgemeinen weniger sicher sind, da die Kernel-Entwickler möglicherweise ein falsches Sicherheitsgefühl haben.
Ich habe mich auch bemüht, das Dateisystem auszuwählen, über das ich mehr erfahren wollte; auf diese Weise, selbst wenn du keinen Fehler findest, wirst du trotzdem eine Menge interessanter Dinge lernen.
Ich untersuchte viele Netzwerk-Dateisysteme, fand aber nichts Wichtiges. Nachdem ich das Unterverzeichnis net/ durchstöbert hatte, stieß ich auf das Modul nf_tables. Es schien etwas komplex, also entschloss ich mich, mir etwas Zeit zu nehmen, um mich darüber zu informieren.
Netfilter (net/netfilter) ist ein ziemlich großes Netzwerk-Dateisystem-Subsystem im Kernel. Kurz gesagt, Netfilter platziert Hooks durch die Netzwerkmodule, für die andere Module Handler registrieren können. Wenn ein Hook erreicht wird, wird die Kontrolle an diese Handler delegiert, und sie können mit der entsprechenden Netzwerkpaketstruktur operieren. Die Handler können Pakete akzeptieren, verwerfen und modifizieren.
Nach einigen Stunden, in denen ich die API von nf_tables (net/netfilter/nf_tables_api.c) durchforstete, um genau zu verstehen, wie sie funktioniert, beschloss ich, einen Blick auf die logische Validierung der vom Benutzer gesendeten Register zu werfen und fand einige verdächtige Verhaltensweisen. Nachdem ich überlegte, ob ich verrückt werde oder nicht, schrieb ich einen kleinen PoC (Proof-of-Concept), um zu versuchen, die von mir gefundene Schwachstelle auszulösen: eine als OOB oder außerhalb der Grenzen bekannte Schwachstelle, die das Lesen und Schreiben in den Stack-Speicher ermöglicht.
Nachdem ich einen Weg gefunden hatte, Kernel-Adressen zu leaken, war es ziemlich einfach, die Kontrolle über den Speicherzeiger zu übernehmen. Nach ein wenig ROP (Return-Oriented Programming) wurde die Shell mit root-Privilegien zur Realität.
Jedes Mal, wenn die init-Routine eines Ausdrucks ein Register einer Netlink-Benutzernachricht parsen muss, wird die Routine nft_parse_register_load oder nft_parse_register_store aufgerufen, je nachdem, ob es sich um ein Quell- oder Zielregister handelt. Ich habe einige Kommentare hinzugefügt:```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;
}
Die Varianten `*_store` sind praktisch identisch, außer dass sie das Schreiben in das *verbdict* unter bestimmten Bedingungen erlauben.
Nach der Überprüfung der letzten Validierung ist hier wirklich etwas nicht in Ordnung:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
Dies scheint ein integer overflow zu sein, nicht wahr? Wenn wir erreichen können, dass reg einen Wert enthält, der mit 4 multipliziert einen overflow erzeugt, wenn len addiert wird, können wir die Bedingungen erfüllen. In nft_parse_register_load wird das letzte wertvolle Byte von reg immer noch auf den Zeiger u8 *sreg geschrieben, was in unser nft_expr fällt, das später als Index verwendet wird.```c
*sreg = reg;
Können wir das wirklich? `reg` ist ein `enum nft_registers` in der Validierung der Routine. Wir können Werte übergeben, die im Bereich von `0x00000001` bis einschließlich `0xfffffffb` liegen, dem Bereich von `nft_parse_register`; aber ist `reg` ein 32-Bit-Wert in `nft_validate_register_load`? Es ist bekannt, dass Compiler *enum types* verkleinern können, wenn ein kleinerer Typ alle Werte darstellen kann. Holen wir uns eine zweite Meinung.
Aus dem GCC-Handbuch:```
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? Hängt vom ABI und dem möglichen Optimierungsgrad ab. Ich konnte keine konkreten Beweise dafür finden, ob diese Option in den Linux-Builds standardmäßig aktiviert ist.
Aber der Assembler lügt nie. Werfen wir einen Blick darauf:```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
Die Funktionsaufrufe sind recht gut ausgerichtet. Die wichtigen Operationen befinden sich 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 ist das Ergebnis von ntf_parse_register, rdx ist die angegebene len und rsi ist der Zeiger sreg. Wir haben uns bereits Klarheit verschafft.
nft_parse_register_store zeigt dasselbe Verhalten. Solange die Register auf dem Stack leben, wird unsere OOB-Schwachstelle offensichtlich relativ zum Stack sein. Das ist gut, denn mit etwas Glück können wir Speicher direkt überschreiben und zurückkehren.
Um ein Beispiel für eine verwundbare Eingabe zu geben: Ein Register von 0xfffffffb und eine Länge von 0x20 ergibt 0xfffffffb * 4 + 0x20 = 0x0c < 0x50. Nach der Validierung wird (u8)0xfffffffb = 0xfb in *sreg geschrieben.
Allerdings gibt es ein Problem: Gibt es Ausdrücke, die es uns erlauben, eine Länge zu verwenden, die einen Overflow verursachen kann, wenn die Addition durchgeführt wird? Nach einiger Recherche fand ich, dass nft_bitwise und nft_payload es Ihnen erlauben, Ihre eigene Länge von 0x00 bis 0xff anzugeben. Viele andere Ausdrücke scheinen statische Längen zu haben, die sehr klein sind.
Im Moment sieht das vielversprechend aus. Der nächste Schritt ist, diese Exploit-Primitives (allgemeine Fähigkeiten, die während eines Exploits erlangt werden) zu nehmen und zu nutzen.
Wenn wir die Art der Macht definieren können, die uns unser Exploit geben kann, sollte die Ausnutzung dieser Schwachstelle einfacher sein. Also, geben Sie mir etwas Geduld, denn wir werden uns ein wenig Arithmetik ansehen.
Es gibt drei Punkte, die wir für unseren Overflow bei der Multiplikation des Registers nutzen können, da dieses mit 4 = 2^2 multipliziert wird: 2^32 - 1, 2^31 - 1 und 2^30 - 1 (jeweils 0xffffffff, 0x7fffffff und 0x3fffffff). Diese Werte können verkleinert werden, bis wir unsere maximale erlaubte Länge addieren, was nach der Multiplikation mit vier nicht zu einem Overflow führt. Ein weiterer zu beachtender Punkt ist, dass wir keine Werte größer als 0xfffffffb verwenden können, wie bereits erwähnt.
Bei einer gegebenen Länge bilden die niederwertigsten Bytes, die einen Overflow mit dieser Länge ermöglichen, unser Intervall der OOB-Indizes, die wir nutzen können.
Letztendlich spielt es keine Rolle, welche Overflow-Punkte verwendet werden. Nehmen Sie zum Beispiel die folgenden Werte mit einem LSB (niederwertigstes Bit) von 0xf0:```
0xfffffff0 * 4 = 0xffffffc0
0x7ffffff0 * 4 = 0xffffffc0
0x3ffffff0 * 4 = 0xffffffc0
Von nun an werden wir Registerwerte nahe `0x7fffffff` verwenden.
Zuvor haben wir über `nft_payload` und `nft_bitwise` gesprochen. Einige Eigenschaften dieser Ausdrücke sind:
* `nft_payload` kann nur *OOB*-Schreibvorgänge durchführen, während `nft_bitwise` *OOB*-Schreib- und Lesevorgänge durchführen kann.
* `nft_payload` kann *OOB*-Schreibvorgänge mit bis zu 0xff Bytes beliebiger Daten durchführen.
* `nft_bitwise` kann tatsächlich nur bis zu `0x40` Bytes beliebiger Daten schreiben und nur `0x40` Bytes aus dem *Stack* des Registerraums lesen.
* `nft_bitwise` erfordert ein `sreg` und ein `dreg`, welche die Validierung mit demselben Längenwert bestehen müssen.
* Wir haben nur `0x40` Bytes Registerraum, also möchten wir entweder aus dem Registerraum lesen oder schreiben, können aber die Validierung mit einer Länge größer als `0x40` nicht bestehen.
Wir könnten einen größeren Längenwert für `nft_bitwise` verwenden, aber das würde bedeuten, dass `sreg` und `dreg` außerhalb der Grenzen liegen müssten, was für unsere Zwecke nicht sehr nützlich wäre. Daher arbeiten wir vorerst mit der Länge `0x40`.
Unter Berücksichtigung all dessen: Welche Arten von *Exploits* können wir verwenden?
`nft_bitwise` hat eine maximale Länge von `0x40`. Das bedeutet, dass der mit vier multiplizierte Registerwert mindestens `0xffffffc0` sein sollte. Der größte Wert, den wir durch Multiplikation mit vier erhalten können, ist `0xfffffffb`, und da `0xfffffffb + 0x40 = 0x3b <= 0x50` ist, wird dies die Validierung bestehen.
`0x7ffffff0 * 4 = 0xffffffc0`: die untere Grenze ist `0xf0`.
`0x7fffffff * 4 = 0xfffffffb`: die obere Grenze ist `0xff`.
Übersetzt in [*Byte-Offsets*](https://es.wikipedia.org/wiki/Offset_(inform%C3%A1tica)):```
0xc1 * 4 = 0x304
0xeb * 4 + 0xff = 0x4ab
nft_payload kann außerhalb der Grenzen durch die Offsets [0x304, 0x4ab] von struct nft_regs schreiben.
Nachdem dies nun geklärt ist, was befindet sich tatsächlich auf dem Stack an diesen Offsets?
Die Routine nft_do_chain kann über viele Codepfade aufgerufen werden. Es gibt viele Faktoren, die die Form des Stacks vor dem Stack Frame von nft_do_chain ändern:
Ob der Chain Hook ein input oder output ist.
input konfiguriert haben, wird der Hook im Softirq-Kontext des entsprechenden Netzwerkgeräts mit dem Softirq-Stack aktiviert.output konfiguriert haben, wird der Hook im Kontext des Syscall (Systemaufruf) send* mit dem Syscall-Stack aktiviert.Das verwendete Protokoll.
Ich glaube, man kann viele Variationen von Call Stacks erhalten, indem man verschiedene Kombinationen von Protokollen, Schnittstellen und Hook-Positionen verwendet. Vorläufig werden wir einen Chain Hook verwenden, der als output mit einem UDP-Paket konfiguriert ist.

Stack-Layout und die Out-of-Bounds-Bereiche in nft_do_chain, wenn ein gesendetes UDP-Paket einen als output konfigurierten Hook erreicht
Um einen stabilen Exploit erstellen zu können, müssen wir zuerst die Adresse des Kernel-Images leaken.
Die Adresse des Kernel-Images hat 9 Bits Entropie (Maß für die Unsicherheit bei einer Menge von Nachrichten, aus der nur eine empfangen wird), was bedeutet, dass es 512 verschiedene Positionen gibt, an denen der Kernel geladen werden kann. Abhängig von deinem Angriffsszenario besteht eine Wahrscheinlichkeit von 1 zu 512, dass der Angriff korrekt funktioniert; aber es wäre besser, wenn wir daraus einen stabileren Exploit machen könnten.
Der einfachste Schritt ist zu versuchen, unsere Out-of-Bounds-Lesefähigkeit, die wir durch nft_bitwise erhalten haben, zu nutzen, um einige Daten vom Stack in unsere Register zu kopieren. Da das gesamte Intervall, das wir lesen können, eine Länge von 0x7c Bytes hat, besteht eine ziemlich gute Wahrscheinlichkeit, dass die Kernel-Adresse dort ist.

Out-of-Bounds-Bereich von nft_bitwise
Heute ist unser Tag! Es gibt zwei:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
Das Schreiben in die Register ist eine Sache, aber das Auslesen eine andere. Nach Recherche scheint es keine einfache Möglichkeit zu geben, die Register direkt auszulesen, während `nft_do_chain` ausgeführt wird.
In meinem ursprünglichen Bericht an [email protected] wurde ich von einem Netfilter-Maintainer auf den Ausdruck `nft_dynset` hingewiesen, der [*dynamic sets*](https://en.wikipedia.org/wiki/Dynamic_set) unterstützt, die als eine Art Datenbank fungieren können, die über verschiedene `nft_do_chain`-Ausführungen hinweg schreiben und lesen kann. Anscheinend hat `nft_payload` auch die Fähigkeit, selbst in das Paket zu schreiben – das ist mir entgangen.
Stattdessen entschied ich mich, mit meinem [*Side-Channel-Angriff*](https://es.wikipedia.org/wiki/Ataque_de_canal_lateral) fortzufahren. Aufgrund der Natur von `nf_tables` kann man Seiteneffekte verursachen. Man könnte sogar sagen, dass es sich nicht einmal um Nebeneffekte handelt, sondern um primäre Effekte.
Indem wir Regeln erstellen, die das Paket basierend auf dem Wert der Kernel-Speicheradresse, die wir kopieren, verwerfen oder akzeptieren, können wir nach und nach den Wert ableiten, indem wir prüfen, ob die von uns gesendeten Pakete auch empfangen wurden.
1. Einen UDP-Socket erstellen, der Pakete auf `127.0.0.1:9999` empfängt:
* Er sollte Pakete in einem separaten Thread empfangen.
* Für jedes empfangene Paket sollte eine Nachricht zurückgesendet werden.
2. Füge eine Regel hinzu, die:
1. Die Kernel-Adresse mit `nft_bitwise` in die Register kopiert.
2. `nft_cmp_expr` verwendet, um die Adresse mit einer Konstante zu vergleichen.
3. Ein Paket verwirft, wenn der Vergleich wahr ist.
3. Sende ein UDP-Paket an `127.0.0.1:9999`
1. Wir können ein wenig Information über die Kernel-Adresse ableiten, basierend darauf, ob wir eine Antwortnachricht erhalten.
4. Wiederhole 2 und 3 mit den richtigen Werten, bis du genügend Informationen hast, um die Information allein zu bestimmen.

Es gibt jedoch noch einige Einschränkungen. Zum Beispiel könnte das Paket, das wir empfangen, auch ohne Vorwarnung verworfen werden. Um dies zu mildern, können wir eine Rauschunterdrückung hinzufügen, für die wir eine *base chain* und eine *auxiliary regular chain* benötigen.
*Rule in base chain:*
| # | Ausdruck | Argumente | Kommentar |
| --- | ------------------- | ------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------------- |
| 0 | `nft_payload` | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | Den Zielport des Pakets in Register 8 schreiben. |
| 1 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999 | Den Zielport mit `9999` vergleichen und `NFT_BREAK` zurückgeben, wenn das Ergebnis nicht gleich ist. |
| 2 | `nft_payload` | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8 | Die ersten acht Bytes des Pakets in Register 8 schreiben. |
| 3 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de | Die ersten acht Bytes mit dem magischen Wert vergleichen und `NFT_BREAK` zurückgeben, wenn nicht gleich. |
| 4 | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain | Da die Regel noch ausgewertet wird, müssen die Bedingungen übereinstimmen, und unsere *auxiliary chain* wird aufgerufen. |
*Rule in auxiliary chain:*
| # | Ausdruck | Argumente | Kommentar |
| --- | --------------- | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | `nft_bitwise` | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | Schreibt die Kernel-Adresse unter Verwendung des Out-of-Bounds-Lesevorgangs in die Register, verschoben um die Bits von `SHIFT_AMT`, um das gewünschte Byte der Adresse in das richtige Register zu bekommen. |
| 1 | `nft_cmp` | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND | Vergleicht das Byte der Kernel-Adresse mit `COMPARAND`, gibt `NFT_BREAK` zurück, wenn das Ergebnis nicht gleich ist. |
| 2 | `nft_immediate` | verdict=NFT_DROP | Verwirft das Paket, wenn das Adress-Byte größer als `COMPARAND` ist. |
Durch Überprüfen des Zielports und Vergleichen der ersten acht Bytes des inneren Headers mit einem magischen Wert können wir die Seiteneffekte für die gewünschten Pakete aktivieren.
Durch dynamisches Ändern von `COMPARAND` können wir eine binäre Suche durchführen, um das Byte der Kernel-Adresse in `O(log(n))` Zeit zu finden. Indem wir `SHIFT_AMT` dynamisch auf die nächsten Vielfachen von acht ändern, können wir zum nächsten Speicherbyte wechseln und von vorne beginnen.
#### 4.3.1 Pseudo-Code zum Filtern
Ein bisschen Python-Code, um die Speicheradresse zu filtern. Das Lustige ist, dass ich das leicht in Python hätte implementieren können. Denkt daran, dass man seine Exploits nicht immer für einen Kernel in C schreiben muss :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)]
Jetzt, da wir den Leck haben, sollte die beliebige Codeausführung sehr einfach sein. Der Schreibzugriff außerhalb der Grenzen von nft_payload sollte in der Lage sein, einen RoP-Angriff auf den Stack zu schreiben, oder?
Nein. Wir hatten nicht viel Glück, zumindest bei diesem speziellen Kernel. Der Schreibzugriff außerhalb der Grenzen von nft_payload ist fast vollständig am Stack Frame der Routine udp_sendmsg ausgerichtet. Die Adresse von udp_sendmsg befindet sich im Offset +0x2f8 relativ zu den Registern; dieser Ort ist zu niedrig, um mit nft_payload oder nft_bitwise erreicht zu werden (wir können ab dem Offset +0x304 zu schreiben beginnen, so nah...). Die Adresse inet_sendmsg befindet sich im Offset +0x4a8. Technisch können wir sie erreichen (und die unteren drei Bytes überschreiben), aber es gibt einen Stack Canary (eine Technik zur Erkennung eines Stack Buffer Overflows, bevor bösartiger Code ausgeführt werden kann) an der Adresse +0x0458, den wir ebenfalls überschreiben müssten, um dies zu erreichen. Das würde offensichtlich den Kernel zum Absturz bringen, also ist das keine Option.
Ich habe diese Methode bei einem anderen Kernel-Build erfolgreich eingesetzt, aber es scheint, dass der Versuch, dasselbe für den Kernel zu tun, den ich für diesen Blog verwende, etwas schwieriger sein wird.
Nun, vielleicht können wir ein wenig contrived stack frame hacking betreiben, um die lokalen Variablen in udp_sendmsg zu überschreiben. Wir könnten auch versuchen, den Verdict Chain Pointer zu überschreiben, indem wir einen Wert aus den Registern verwenden, z. B. 0x7fffff00 (ich denke, das könnte eine coole Technik sein; angesichts der Herausforderung).
Versuchen wir, den verwendeten Base Chain Hook zu ändern. Wir haben einen output-Chain verwendet; was würde passieren, wenn wir ihn in einen input-Chain ändern?

Diagramm des Out-of-Bounds-Bereichs in nft_do_chain, wenn ein gesendetes UDP-Paket den Input-Hook erreicht
Das sieht schon etwas besser aus! Wir können die Rücksprungadresse des Frames von __netif_receive_skb_one_core überschreiben (Offset +0x328), die zu __netif_receive_skb zurückführt. Da sie relativ nahe an der Höhe des Out-of-Bounds-Bereichs unseres nft_payload liegt, können wir unseren OOB-Index (Out-of-Bounds-Bereich) direkt auf diese Rücksprungadresse zeigen lassen und dabei den Stack Canary am Offset +0x310 umgehen. Der Offset +0x328 entspricht dem Index 0xca.
Um die Überschreibung der Rücksprungadresse auszulösen, erstellen wir einen neuen input-Chain in der Tabelle und fügen eine Regel mit einem nft_payload hinzu, das 0xff Bytes vom inneren Header des Pakets zum Index 0xca schreibt. Dann senden wir ein Paket mit dem Payload, und zack.

🥳 🥳 🥳 🥳 🥳
/proc/kallsymsrequest_module