Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2022-1015-1016 — Spanische Übersetzung der CVE-2022-1015 und 1016, entdeckt und dokumentiert von David. | Kitploit
Tools/GitHubGitHub/zanezhub/cve-2022-1015-1016
Privilege EscalationSchwachstellenanalyseExploitationLernen & BildungBinary-Exploitation
GitHubzanezhub/cve-2022-1015-1016

CVE-2022-1015-1016

Spanische Übersetzung der CVE-2022-1015 und 1016, entdeckt und dokumentiert von David.

Repository anzeigen
1vor 4 JahrenNoch nicht geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
Webseite

CVE-2022-1015 & CVE-2022-1026

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:

  • Twitter
  • Github

Eine Analyse der zwei neuen Linux-Sicherheitslücken in nf_tables

Veröffentlicht am 2. April 2022.

  • CVE-2022-1015 ermöglicht einen Out-of-Bounds-Zugriff (außerhalb der Grenzen), verursacht durch unzureichende Überprüfung der Eingabeargumente, was zu Remote-Code-Ausführung und lokaler Privilegienausweitung führen kann.
  • CVE-2022-1016 steht im Zusammenhang mit einer schlechten Initialisierung der auf dem Stack abgelegten Variablen, was dazu verwendet werden kann, eine große Vielfalt an Kernel-Daten in den Benutzerbereich (Userspace) zu leaken.

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:

  • Wenn du einfach nur über die Schwachstelle lesen möchtest, beginne mit Abschnitt 4
  • Wenn du auch etwas Kontext zum Kernel-Subsystem möchtest, beginne mit Abschnitt 2
  • Wenn du an etwas zusätzlichem Kontext interessiert bist, lies das gesamte Dokument

1. Kontext

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.

1.1 Zielidentifikation und Audit-Strategie

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:

  • Wenn ein Bug root-Privilegien erfordert, gibt es keine signifikante Sicherheitsgrenze (es sei denn, das Kernel-Modul-Signing ist aktiviert)
    • Einige Dinge, die mir in den Sinn kommen, sind viele der (virtuellen) Dateisystemmodule. Nur der anfängliche root-Benutzer kann diese Dateisysteme mounten. Die Ausnahme liegt bei vfe, das FS_USERNS_MOUNT spezifiziert, in diesem Fall kannst du sie im user namespace mounten.
  • Wenn auf einen Bug nicht über Systemaufrufe zugegriffen werden kann, ist er wahrscheinlich nicht ausnutzbar.
    • Dies gilt für viele Hardware-Treiber, da du keinen physischen Zugriff auf die Maschine hast. Netzwerktreiber auf niedriger Ebene könnten dennoch ein gutes Ziel sein, wenn du z. B. Daten über Bluetooth oder 802.11.ac senden kannst.
    • Offensichtlich hängt dies vom Szenario ab, in dem du dich befindest.
  • Viele Bugs erfordern CAP_SYS_ADMIN oder CAP_NET_ADMIN.
    • Die user namespaces sind standardmäßig aktiviert, also ist das kein Problem.
    • Andernfalls musst du zuerst eine Privilegienausweitung zum namespace des root-Benutzers innerhalb eines Containers durchführen.
  • Nicht alle Module werden in deinem Ziel vorhanden sein.
    • Linux ist ein außergewöhnlich hoch konfigurierbares Stück Software, daher können alle Konfigurationen auf vielfältige Weise variieren.
    • Die Kernelkonfiguration kann normalerweise über /proc/config.gz abgerufen werden. Module können als (=m) geladen oder separat kompiliert und zur Laufzeit geladen werden (=y).
    • Du kannst /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.

1.2 nf_tables: warum?

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.

2. Einführung in netfilter

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.


4. CVE-2022-1015

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.

4.1 Root

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) {

root@kitploit:~
/* 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 */

root@kitploit:~
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;

root@kitploit:~
/* 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;

}

root@kitploit:~
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;

root@kitploit:~
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

root@kitploit:~
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.

4.2 Untersuchung der Exploit-Primitives

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

root@kitploit:~
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.

    • Wenn wir einen Chain Hook als input konfiguriert haben, wird der Hook im Softirq-Kontext des entsprechenden Netzwerkgeräts mit dem Softirq-Stack aktiviert.
    • Wenn wir einen Chain Hook als output konfiguriert haben, wird der Hook im Kontext des Syscall (Systemaufruf) send* mit dem Syscall-Stack aktiviert.
  • Das verwendete Protokoll.

    • Das Senden eines rohen IP-Pakets wird einen Call Stack haben, der ziemlich anders ist als z. B. ein UDP-Paket.

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.

Diagrama del stac con output y UDP

Stack-Layout und die Out-of-Bounds-Bereiche in nft_do_chain, wenn ein gesendetes UDP-Paket einen als output konfigurierten Hook erreicht

4.3 Informationsleck über einen Seitenkanal (Side-Channel)

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.

Alcance fuera de límites de nft_bitwise

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

root@kitploit:~
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.

![](https://assets.kitploit.com/production/public/readmes/35022/71fabf101d1c962c5434bff7babccd3f294cd3d278765f1b03121429c5417b12.png)

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)]

4.4 Beliebige Codeausführung (Arbitrary code execution)

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.

🥳 🥳 🥳 🥳 🥳

Tool herunterladen
/proc/kallsyms
request_module
  • Wenn du dir nicht sicher bist, schreibe ein kleines Programm, das versucht, mit dem Modul zu interagieren.