Het artikel analyseert een kwetsbaarheid in de OpenBSD-kernel die dateert uit de oorspronkelijke BSD-bronnen. De kern van het probleem is een signed 16-bit short teller voor de koppeling van gedeeld geheugen, die bij overschrijding (boven de 32.767) terugspringt naar een negatieve waarde.
De exploitatie Door deze integer overflow te combineren met een race condition en een out-of-bounds write, konden de onderzoekers lokale root-rechten verkrijgen. De exploit kon beveiligingen zoals KARL, SMAP en SMEP omzeilen door gebruik te maken van de deterministische aard van de kernel-allocatoren, waardoor kernel-pointers op voorspelbare locaties in het geheugen terechtkwamen.
Voorgestelde mitigaties Om dit soort aanvallen in de toekomst te voorkomen, worden diverse defensieve maatregelen voorgesteld:
- Safe-unlinking: Het implementeren van controles op invarianten van dubbel gekoppelde lijsten.
- Randomisatie: Het randomiseren van het allocatieadres van de zelfreferentiële PTE-map en het basisadres van de kernel-allocator om voorspelbaarheid weg te nemen.
- Compiler-optimalisaties: Het gebruik van de array bounds sanitizer en Alloc Tokens om geheugenproblemen en overflows te isoleren.
De auteurs concluderen dat het investeren in het neutraliseren van volledige klassen van kwetsbaarheden cruciaal is voor de systeemveerkracht, zeker nu AI de ontdekking van bugs versnelt.
Big Fish: Het exploiteren van een 37 jaar oude kernelbug in OpenBSD
Begin dit jaar hebben we enkele kwetsbaarheden gemeld bij OpenBSD, vaak samen met voorgestelde patches. Eén daarvan was ongeveer 37 jaar oud en stamt uit de oorspronkelijke BSD-bronnen, waar een teller voor de koppeling van gedeeld geheugen (shared memory attachment counter) was gedefinieerd als een signed 16-bit short. Door herhaaldelijk een proces te forken met records voor de koppeling van gedeeld geheugen, kon die teller boven zijn maximumwaarde van 32.767 worden geduwd, waardoor deze terugsprong (wrap) naar een negatieve waarde. Deze integer overflow kon leiden tot een use-after-free via een race condition; in combinatie met een aparte out-of-bounds write in de kernel maakte dit lokale privilege-escalatie naar root mogelijk. Deze problemen zijn respectievelijk opgelost in errata 026 en 018.
De exploit
Het is ons gelukt om lokale root-rechten in OpenBSD te verkrijgen met KARL / SMAP / SMEP ingeschakeld, zonder gebruik te maken van een information disclosure-kwetsbaarheid, via het volgende pad:
- Map 127 SysV shared memory-segmenten, map deze via
munmap() en fork 259 processen om de shared memory attach count te laten wrappen naar -32.643.
- Voer heap grooming en layout uit, waarbij statistieken van
sysctl(KERNMALLOCSTATS) (zoals kbtotal en kb_totalfree) worden gebruikt om allocaties bij te houden en te bepalen wanneer nieuwe pagina's worden gealloceerd.
- Win de race condition door
IPC_RMID in één proces te gebruiken, terwijl in een helperproces een nieuw size-confused shared memory-segment wordt gekoppeld.
- Lijn IP-socket-allocaties uit en zet
IPMULTICASTTTL op een niet-standaard waarde van 2 om een persistente 16-byte struct ipmoptions te alloceren. Het laatste woord hiervan bevat twee 16-bit tellers: imo + 12 = imonummemberships | imomax_memberships << 16.
- Trigger de conditionele -1 out-of-bounds write om zowel het aantal lidmaatschappen (number of memberships) als de velden voor het maximum aantal lidmaatschappen te overschrijven.
- Sluit de socket via
close() om de groepsleden te doorlopen, wat atomaire decrementen triggert die de PTE-zelfreferentiële mapping twee keer wijzigen. Dit schakelt de mapping om van gebruikers- naar supervisor-toegang, waardoor kernel-code-executie wordt bereikt door een voorspelbare kernel-pointer te overschrijven.
- Ruim de shared memory-boekhouding op en herstel de overschreven kernel-pointer om een root-shell te verkrijgen.
Dat is de korte versie; de langere versie is interessanter en bevat de geschiedenis van de kwetsbaarheid, aangezien een korte shm_nattch ongeveer 37 jaar terug te herleiden is naar de oorspronkelijke BSD-bronnen.
Kijkend naar het grotere geheel, leidde deze kwetsbaarheid tot code-executie via een write-what-where-primitief. De write werd veroorzaakt door een TAILQ_REMOVE, de what was onze userland-geheugenmapping die supervisor-modus toegankelijk was gemaakt om SMAP/SMEP te omzeilen, en de where was een voorspelbare locatie van een kernel-pointer.
Mitigatie van de exploit
Hoewel OpenBSD's KARL de kernel-binary opnieuw linkt om zowel functie- als data-adressen te randomiseren, blijven de geheugenallocatoren van de kernel deterministisch. Zowel de boot-time allocator als de best-fit allocator missen base address randomization. Bijgevolg landen vroege boot-allocaties consistent op voorspelbare locaties over verschillende boots op dezelfde hardware. Wanneer deze voorspelbare structuur-allocaties functiepointers bevatten, worden ze ideale doelwitten voor exploitatie. Zo richt de exploit zich betrouwbaar op de lo0- en enc0-ioctl-handlers, waarbij er één wordt overschreven om de controle over de flow te krijgen en de andere wordt gebruikt om de overschreven pointer te herstellen.
Naast de eerder genoemde errata-fixes zijn er verschillende andere benaderingen die kunnen worden genomen om deze wegen in de toekomst te elimineren of de waarschijnlijkheid van gebruik te verminderen.
Ten eerste kunnen we safe-unlinking implementeren, een techniek die controleert op invarianten van dubbel gekoppelde lijsten voordat de unlink-geheugenschrijfacties worden uitgevoerd. Als de invarianten worden geschonden, wordt er een geschikte abort() of panic() aangeroepen (afhankelijk van of het om user-modus of kernel-modus gaat), waardoor geheugen corruptie wordt voorkomen. Een voorbeeldimplementatie is beschikbaar; FreeBSD en NetBSD hebben al een implementatie van safe-unlinking in hun sys/queue.h-implementatie.
Ten tweede kunnen we het allocatieadres van de zelfreferentiële PTE-map randomiseren, zodat de gerichte atomaire decrement-locaties niet mogelijk zouden zijn. Dit gebeurt door een willekeurige byte (.openbsd.randomdata) te gebruiken en tijdens het opstarten een PML4-slot (320 + (randombyte & 127)) te selecteren, en de recursieve entry op die locatie te installeren. De PTEBASE-definitie wordt dan een runtime-expressie, wat resulteert in bases van 0xffffa00000000000 tot 0xffffdf8000000000 in intervallen van 512 GiB. In het geval dat randomisatie niet geschikt is, zouden andere opties volstaan, zoals het markeren van de PTE als read-only en het wijzigen van CR0.WP tijdens de modificatie.
Ten derde kan de kernel-allocator non-deterministisch worden gemaakt via twee benaderingen: door het basisadres waar geheugenallocaties plaatsvinden tijdens het opstarten te randomiseren, en daarnaast de volgorde van allocatie tijdens runtime te randomiseren. De randomisatie van het basisadres is effectief gratis, aangezien de kloof die het creëert later nog steeds kan worden gebruikt; runtime-randomisatie brengt wel enige overhead met zich mee tijdens allocatie. Het is mogelijk dat diepere wijzigingen in de allocator deze impact kunnen verlichten. Beide benaderingen zouden voldoende zijn om de voorspelbare pointerlocatie te doorbreken zonder dat er aanvullende kwetsbaarheden (softwarematig of hardwarematig) nodig zijn.
Verder zijn er bestaande en komende compilerfuncties die kunnen worden gebruikt om volledige kwetsbaarheden te mitigeren, zoals de array bounds sanitizer (een subset van de Undefined Behaviour sanitizer), die out-of-bounds toegangen tot arrays met een vaste grootte kan elimineren. Calif heeft aan FreeBSD gesuggereerd dat de array bounds sanitizer een nuttige exploit-mitigatie zou kunnen zijn die in release-builds kan worden ingeschakeld, samen met het privé rapporteren van 21 problemen die de array bounds sanitizer zou hebben gedetecteerd; sommige met een impact op lokale privilege-escalatie. Een komende compilerfunctie die kan helpen bij het mitigeren van heap-problemen is Alloc Tokens, waarmee allocaties die pointers bevatten kunnen worden geïsoleerd van allocaties zonder pointers. Deze isolatie kan overflows vanuit allocaties zonder pointers veel moeilijker exploiteerbaar maken door pointer-toegang te voorkomen, en ervoor zorgen dat vulnerability-klassen zoals Use-after-Free niet het tegenovergestelde type allocatie als vervangende geheugenallocatie kunnen gebruiken.
Sinds het begin heeft OpenBSD consequent gepleit voor proactieve beveiliging via broncode-auditing, het vervangen van onveilige API's, geavanceerde wachtwoord-hashing, cryptografie, privilege-scheiding, sandboxing, en geheugenveiligheid via ASLR en W^X; hiermee zetten ze vaak de standaard voor systeem- en netwerkverharding.
Door proactief te verharden en nieuwe en bestaande mitigatie-opties aan te passen, kunnen we vaak systematisch volledige klassen van exploitatie-primitieven neutraliseren, of in ieder geval de kosten voor de aanvaller verhogen (bijv. extra tijdsinvestering, meer kwetsbaarheden nodig om een probleem te exploiteren, het vinden van alternatieve wegen, etc.). Investeren in defensieve verbeteringen voordat kwetsbaarheden actief worden getarget, verhoogt de drempel voor aanvallers en helpt een robuuste bescherming en langetermijnveerkracht te garanderen. In het tijdperk van AI-ondersteunde ontdekking van kwetsbaarheden wordt het mitigeren van volledige kwetsbaarheidsklassen, gecombineerd met het verhogen van de aanvalcomplexiteit, alleen maar kritieker.
Big Fish: Het exploiteren van een 37 jaar oude kernelbug in OpenBSD
Begin dit jaar hebben we enkele kwetsbaarheden gemeld bij OpenBSD, vaak samen met voorgestelde patches. Eén daarvan was ongeveer 37 jaar oud en stamt uit de oorspronkelijke BSD-bronnen, waar een teller voor de koppeling van gedeeld geheugen (shared memory attachment counter) was gedefinieerd als een signed 16-bit short. Door herhaaldelijk een proces te forken met records voor de koppeling van gedeeld geheugen, kon die teller boven zijn maximumwaarde van 32.767 worden geduwd, waardoor deze terugsprong (wrap) naar een negatieve waarde. Deze integer overflow kon leiden tot een use-after-free via een race condition; in combinatie met een aparte out-of-bounds write in de kernel maakte dit lokale privilege-escalatie naar root mogelijk. Deze problemen zijn respectievelijk opgelost in errata 026 en 018.
De exploit
Het is ons gelukt om lokale root-rechten in OpenBSD te verkrijgen met KARL / SMAP / SMEP ingeschakeld, zonder gebruik te maken van een information disclosure-kwetsbaarheid, via het volgende pad:
- Map 127 SysV shared memory-segmenten, map deze via
munmap() en fork 259 processen om de shared memory attach count te laten wrappen naar -32.643.
- Voer heap grooming en layout uit, waarbij statistieken van
sysctl(KERNMALLOCSTATS) (zoals kbtotal en kb_totalfree) worden gebruikt om allocaties bij te houden en te bepalen wanneer nieuwe pagina's worden gealloceerd.
- Win de race condition door
IPC_RMID in één proces te gebruiken, terwijl in een helperproces een nieuw size-confused shared memory-segment wordt gekoppeld.
- Lijn IP-socket-allocaties uit en zet
IPMULTICASTTTL op een niet-standaard waarde van 2 om een persistente 16-byte struct ipmoptions te alloceren. Het laatste woord hiervan bevat twee 16-bit tellers: imo + 12 = imonummemberships | imomax_memberships << 16.
- Trigger de conditionele -1 out-of-bounds write om zowel het aantal lidmaatschappen (number of memberships) als de velden voor het maximum aantal lidmaatschappen te overschrijven.
- Sluit de socket via
close() om de groepsleden te doorlopen, wat atomaire decrementen triggert die de PTE-zelfreferentiële mapping twee keer wijzigen. Dit schakelt de mapping om van gebruikers- naar supervisor-toegang, waardoor kernel-code-executie wordt bereikt door een voorspelbare kernel-pointer te overschrijven.
- Ruim de shared memory-boekhouding op en herstel de overschreven kernel-pointer om een root-shell te verkrijgen.
Dat is de korte versie; de langere versie is interessanter en bevat de geschiedenis van de kwetsbaarheid, aangezien een korte shm_nattch ongeveer 37 jaar terug te herleiden is naar de oorspronkelijke BSD-bronnen.
Kijkend naar het grotere geheel, leidde deze kwetsbaarheid tot code-executie via een write-what-where-primitief. De write werd veroorzaakt door een TAILQ_REMOVE, de what was onze userland-geheugenmapping die supervisor-modus toegankelijk was gemaakt om SMAP/SMEP te omzeilen, en de where was een voorspelbare locatie van een kernel-pointer.
Mitigatie van de exploit
Hoewel OpenBSD's KARL de kernel-binary opnieuw linkt om zowel functie- als data-adressen te randomiseren, blijven de geheugenallocatoren van de kernel deterministisch. Zowel de boot-time allocator als de best-fit allocator missen base address randomization. Bijgevolg landen vroege boot-allocaties consistent op voorspelbare locaties over verschillende boots op dezelfde hardware. Wanneer deze voorspelbare structuur-allocaties functiepointers bevatten, worden ze ideale doelwitten voor exploitatie. Zo richt de exploit zich betrouwbaar op de lo0- en enc0-ioctl-handlers, waarbij er één wordt overschreven om de controle over de flow te krijgen en de andere wordt gebruikt om de overschreven pointer te herstellen.
Naast de eerder genoemde errata-fixes zijn er verschillende andere benaderingen die kunnen worden genomen om deze wegen in de toekomst te elimineren of de waarschijnlijkheid van gebruik te verminderen.
Ten eerste kunnen we safe-unlinking implementeren, een techniek die controleert op invarianten van dubbel gekoppelde lijsten voordat de unlink-geheugenschrijfacties worden uitgevoerd. Als de invarianten worden geschonden, wordt er een geschikte abort() of panic() aangeroepen (afhankelijk van of het om user-modus of kernel-modus gaat), waardoor geheugen corruptie wordt voorkomen. Een voorbeeldimplementatie is beschikbaar; FreeBSD en NetBSD hebben al een implementatie van safe-unlinking in hun sys/queue.h-implementatie.
Ten tweede kunnen we het allocatieadres van de zelfreferentiële PTE-map randomiseren, zodat de gerichte atomaire decrement-locaties niet mogelijk zouden zijn. Dit gebeurt door een willekeurige byte (.openbsd.randomdata) te gebruiken en tijdens het opstarten een PML4-slot (320 + (randombyte & 127)) te selecteren, en de recursieve entry op die locatie te installeren. De PTEBASE-definitie wordt dan een runtime-expressie, wat resulteert in bases van 0xffffa00000000000 tot 0xffffdf8000000000 in intervallen van 512 GiB. In het geval dat randomisatie niet geschikt is, zouden andere opties volstaan, zoals het markeren van de PTE als read-only en het wijzigen van CR0.WP tijdens de modificatie.
Ten derde kan de kernel-allocator non-deterministisch worden gemaakt via twee benaderingen: door het basisadres waar geheugenallocaties plaatsvinden tijdens het opstarten te randomiseren, en daarnaast de volgorde van allocatie tijdens runtime te randomiseren. De randomisatie van het basisadres is effectief gratis, aangezien de kloof die het creëert later nog steeds kan worden gebruikt; runtime-randomisatie brengt wel enige overhead met zich mee tijdens allocatie. Het is mogelijk dat diepere wijzigingen in de allocator deze impact kunnen verlichten. Beide benaderingen zouden voldoende zijn om de voorspelbare pointerlocatie te doorbreken zonder dat er aanvullende kwetsbaarheden (softwarematig of hardwarematig) nodig zijn.
Verder zijn er bestaande en komende compilerfuncties die kunnen worden gebruikt om volledige kwetsbaarheden te mitigeren, zoals de array bounds sanitizer (een subset van de Undefined Behaviour sanitizer), die out-of-bounds toegangen tot arrays met een vaste grootte kan elimineren. Calif heeft aan FreeBSD gesuggereerd dat de array bounds sanitizer een nuttige exploit-mitigatie zou kunnen zijn die in release-builds kan worden ingeschakeld, samen met het privé rapporteren van 21 problemen die de array bounds sanitizer zou hebben gedetecteerd; sommige met een impact op lokale privilege-escalatie. Een komende compilerfunctie die kan helpen bij het mitigeren van heap-problemen is Alloc Tokens, waarmee allocaties die pointers bevatten kunnen worden geïsoleerd van allocaties zonder pointers. Deze isolatie kan overflows vanuit allocaties zonder pointers veel moeilijker exploiteerbaar maken door pointer-toegang te voorkomen, en ervoor zorgen dat vulnerability-klassen zoals Use-after-Free niet het tegenovergestelde type allocatie als vervangende geheugenallocatie kunnen gebruiken.
Sinds het begin heeft OpenBSD consequent gepleit voor proactieve beveiliging via broncode-auditing, het vervangen van onveilige API's, geavanceerde wachtwoord-hashing, cryptografie, privilege-scheiding, sandboxing, en geheugenveiligheid via ASLR en W^X; hiermee zetten ze vaak de standaard voor systeem- en netwerkverharding.
Door proactief te verharden en nieuwe en bestaande mitigatie-opties aan te passen, kunnen we vaak systematisch volledige klassen van exploitatie-primitieven neutraliseren, of in ieder geval de kosten voor de aanvaller verhogen (bijv. extra tijdsinvestering, meer kwetsbaarheden nodig om een probleem te exploiteren, het vinden van alternatieve wegen, etc.). Investeren in defensieve verbeteringen voordat kwetsbaarheden actief worden getarget, verhoogt de drempel voor aanvallers en helpt een robuuste bescherming en langetermijnveerkracht te garanderen. In het tijdperk van AI-ondersteunde ontdekking van kwetsbaarheden wordt het mitigeren van volledige kwetsbaarheidsklassen, gecombineerd met het verhogen van de aanvalcomplexiteit, alleen maar kritieker.