Zephyrnet-logo

Serieuze beveiliging: dat KeePass "hoofdwachtwoord kraken", en wat we ervan kunnen leren

Datum:

De afgelopen twee weken hebben we een reeks artikelen gezien die praten over wat wordt beschreven als een "hoofdwachtwoordkraker" in de populaire open-source wachtwoordbeheerder KeePass.

De bug werd belangrijk genoeg geacht om een ​​officiële identificatiecode van de Amerikaanse overheid te krijgen (het staat bekend als CVE-2023-32784, als je het wilt opzoeken), en gezien het feit dat het hoofdwachtwoord van je wachtwoordbeheerder vrijwel de sleutel is tot je hele digitale kasteel, begrijp je waarom het verhaal veel opwinding veroorzaakte.

Het goede nieuws is dat een aanvaller die deze bug wil misbruiken, vrijwel zeker uw computer al met malware moet hebben geïnfecteerd en daarom uw toetsaanslagen en actieve programma's toch zou kunnen bespioneren.

Met andere woorden, de bug kan worden beschouwd als een gemakkelijk te beheersen risico totdat de maker van KeePass met een update komt, die binnenkort zou moeten verschijnen (blijkbaar begin juni 2023).

Zoals de onthuller van de bug ervoor zorgt aanwijzen:

Als u volledige schijfversleuteling gebruikt met een sterk wachtwoord en uw systeem [vrij van malware] is, zou het goed moeten komen. Alleen met deze bevinding kan niemand uw wachtwoorden op afstand via internet stelen.

De risico's uitgelegd

Zwaar samengevat komt de bug neer op de moeilijkheid om ervoor te zorgen dat alle sporen van vertrouwelijke gegevens uit het geheugen worden gewist zodra u ermee klaar bent.

We zullen hier de problemen negeren hoe we kunnen voorkomen dat we geheime gegevens in het geheugen hebben, zelfs niet voor korte tijd.

In dit artikel willen we programmeurs overal ter wereld eraan herinneren dat code is goedgekeurd door een beveiligingsbewuste recensent met een opmerking als "lijkt zichzelf correct op te ruimen"...

… zou in feite helemaal niet volledig kunnen worden opgeschoond, en het potentiële gegevenslek is misschien niet duidelijk uit een directe studie van de code zelf.

Eenvoudig gezegd betekent de CVE-2023-32784-kwetsbaarheid dat een KeePass-hoofdwachtwoord mogelijk kan worden hersteld van systeemgegevens, zelfs nadat het KeyPass-programma is afgesloten, omdat voldoende informatie over uw wachtwoord (zij het niet echt het onbewerkte wachtwoord zelf, waarop we ons zullen concentreren) aan in een moment) kunnen achterblijven in systeemwissel- of slaapbestanden, waar toegewezen systeemgeheugen uiteindelijk voor later kan worden bewaard.

Op een Windows-computer waar BitLocker niet wordt gebruikt om de harde schijf te coderen wanneer het systeem is uitgeschakeld, zou dit een oplichter die uw laptop heeft gestolen een kans geven om op te starten vanaf een USB- of cd-station en zelfs uw hoofdwachtwoord te herstellen. hoewel het KeyPass-programma er zelf voor zorgt dat het nooit permanent op schijf wordt opgeslagen.

Een langdurig wachtwoordlek in het geheugen betekent ook dat het wachtwoord in theorie kan worden hersteld van een geheugendump van het KeyPass-programma, zelfs als die dump lang nadat u het wachtwoord had ingetypt en lang nadat de KeePass was weggekaapt. zelf had het niet meer nodig om het in de buurt te houden.

Het is duidelijk dat u ervan uit moet gaan dat malware die zich al op uw systeem bevindt, bijna elk ingetypt wachtwoord kan herstellen via een verscheidenheid aan real-time snooping-technieken, zolang deze actief waren op het moment dat u het typte. Maar u mag redelijkerwijs verwachten dat uw tijd die aan gevaar wordt blootgesteld, beperkt is tot de korte periode van typen, niet verlengd tot vele minuten, uren of dagen daarna, of misschien langer, ook niet nadat u uw computer hebt afgesloten.

Wat blijft er achter?

We dachten daarom dat we eens op hoog niveau zouden kijken naar hoe geheime gegevens in het geheugen kunnen worden achtergelaten op manieren die niet direct duidelijk zijn uit de code.

Maakt u zich geen zorgen als u geen programmeur bent – ​​we houden het simpel en leggen het gaandeweg uit.

We beginnen met het kijken naar geheugengebruik en opruimen in een eenvoudig C-programma dat het invoeren en tijdelijk opslaan van een wachtwoord simuleert door het volgende te doen:

  • Een toegewezen stuk geheugen toewijzen speciaal om het wachtwoord op te slaan.
  • Een bekende tekenreeks invoegen zodat we het indien nodig gemakkelijk in het geheugen kunnen vinden.
  • 16 pseudo-willekeurige 8-bits ASCII-tekens toevoegen uit de serie AP.
  • Uitprinten de gesimuleerde wachtwoordbuffer.
  • Het geheugen vrijmaken in de hoop de wachtwoordbuffer te verwijderen.
  • Spannend het programma.

Sterk vereenvoudigd, zou de C-code er ongeveer zo uit kunnen zien, zonder foutcontrole, met behulp van pseudo-willekeurige getallen van slechte kwaliteit van de C-runtime-functie rand(), en eventuele bufferoverloopcontroles negeren (doe dit nooit in echte code!):

 // Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);

In feite bevat de code die we uiteindelijk in onze tests hebben gebruikt enkele extra stukjes en beetjes die hieronder worden weergegeven, zodat we de volledige inhoud van onze tijdelijke wachtwoordbuffer zoals we die gebruikten, konden dumpen om te zoeken naar ongewenste of overgebleven inhoud.

Merk op dat we de buffer opzettelijk dumpen na het aanroepen free(), wat technisch gezien een gebruik-na-vrije bug is, maar we doen het hier als een stiekeme manier om te zien of er iets kritisch achterblijft nadat we onze buffer hebben teruggegeven, wat in het echte leven zou kunnen leiden tot een gevaarlijk datalek.

We hebben er ook twee ingevoegd Waiting for [Enter] prompts in de code om onszelf de kans te geven geheugendumps te maken op belangrijke punten in het programma, waardoor we onbewerkte gegevens krijgen om later te doorzoeken, om te zien wat er achterbleef terwijl het programma liep.

Om geheugendumps te doen, gebruiken we de Microsoft Sysinternals-tool procdump met de -ma keuze (al het geheugen dumpen), waardoor het niet nodig is om onze eigen code te schrijven om Windows te gebruiken DbgHelp systeem en het is nogal complex MiniDumpXxxx() functies.

Om de C-code te compileren, gebruikten we onze eigen kleine en eenvoudige build van Fabrice Bellard's gratis en open-source Tiny C-compiler, beschikbaar voor 64-bits Windows in bron en binaire vorm rechtstreeks vanaf onze GitHub-pagina.

Kopieer-en-plakbare tekst van alle broncode die in het artikel wordt afgebeeld, verschijnt onderaan de pagina.

Dit is wat er gebeurde toen we het testprogramma compileerden en uitvoerden:

C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard Gestript door Paul Ducklin voor gebruik als leermiddel Versie petcc64-0.9.27 [0006] - Genereert 64-bit Alleen PE's -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/users/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- ----------------------- virt bestandsgrootte sectie 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 bytes) C:UsersduckKEYPASS> unl1.exe Dumping 'nieuwe' buffer bij start 00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 stuurpen32cmd. exe.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F 513F0: 42 52 4F 57 53 45 52 5F 41 50 50 5F 50 52 4F 46 BROWSER_APP_PROF 00F51400: 49 4C 45 5F 53 54 52 49 4E 47 3D 49 6E 74 65 72 ILE_STRING=Inter 00F51410: 6E 65 74 20 45 78 70 6C 7A 56 F4 3C AC 4B 00 00 netto ExplzV.< .K.. Volledige reeks was: onwaarschijnlijktextJHKNEJJCPOMDJHAN 00F51390: 75 6E 6C 69 6B 65 6C 79 74 65 78 74 4A 48 4B 4E onwaarschijnlijktextJHKN 00F513A0: 45 4A 4A 43 50 4F 4D 44 4 A 48 41 4E 00 65 00 44 EJJCPOMDJHAN.eD 00F513B0 : 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E r 00F513D0: 64 6 77 73 5 53C 79 73 74 65 6 33 32 5 44 72 iversDriverData 32F00E513: 0 69 76 65 72F 73 5 44 72 69D 76 65 72 44 61 74F .EFC_61=00.FPS_ 513F0F00: 45 46 43V 5 34 33 37 32V 3 31 00 46V 50 53 5F 4372 BROWSER_APP_PROF 1F00: 513 0C 42 52F 4 57 53 45 52E 5 41D 50 50E 5 50 52 ILE_STRING=Inter 4F46: 00E 51400 49 4 45 5 53 54C 52A 49 F4 47C AC 3B 49 6 netto ExplzV.<.K.. Wachten op [ENTER] om buffer vrij te maken... Dumping buffer na free() 74F65: A72 00 F51410 6 65 74 20 45 78 70 F6 7 56 4 3 4 .g......P...... 00D 00 00A 51390C 0 67 5 00E riverData=C:Win 00F00C00: 00 50F 01 5 00C 00 00 00 00 00 513D 0 45 4C 4 43 dowsSystem50Dr 4F4D44: 4 48 41 4 00 65C 00 44 00 513 0 72 69 76 65 72 iversDriverData 44F61E74: 61 3 43 3 5F 57 69 6 00 513D 0 64 6 77 73 5F .EFC_53=79.FPS_ 73F74F65: 6 33 32F 5 44 72 32 00F 513 0 69 76F 65 72 73F 5 BROWSER_APP_PROF 44F72: 69 76C 65 72F 44 61 74 61 00E 513 0D 00 45E 46 43 5 ILE_STRING=Inter 34F33: 37E 32 3 31 00 46 50 53C 5D 4372 1 00D AC 513B 0 42 net ExplM..MK. Wachten op [ENTER] om main() af te sluiten... C:UsersduckKEYPASS>

In deze run hebben we niet de moeite genomen om procesgeheugendumps te pakken, omdat we direct aan de uitvoer konden zien dat deze code gegevens lekt.

Direct na het aanroepen van de Windows C runtime-bibliotheekfunctie malloc(), kunnen we zien dat de buffer die we terugkrijgen bevat wat eruitziet als gegevens van omgevingsvariabelen die zijn overgebleven van de opstartcode van het programma, waarbij de eerste 16 bytes blijkbaar zijn gewijzigd om eruit te zien als een soort overgebleven geheugentoewijzingskop.

(Merk op hoe die 16 bytes eruitzien als twee geheugenadressen van 8 bytes, 0xF55790 en 0xF50150, die respectievelijk net na en net voor onze eigen geheugenbuffer zijn.)

Als het wachtwoord in het geheugen hoort te staan, kunnen we de hele string duidelijk in de buffer zien, zoals we zouden verwachten.

Maar na bellen free(), merk op hoe de eerste 16 bytes van onze buffer opnieuw zijn herschreven met wat lijkt op geheugenadressen in de buurt, vermoedelijk zodat de geheugentoewijzer blokken in het geheugen kan bijhouden die hij opnieuw kan gebruiken...

... maar de rest van onze "verwijderde" wachtwoordtekst (de laatste 12 willekeurige tekens EJJCPOMDJHAN) is achtergelaten.

We moeten niet alleen onze eigen geheugentoewijzingen en de-toewijzingen in C beheren, we moeten er ook voor zorgen dat we de juiste systeemfuncties voor gegevensbuffers kiezen als we ze nauwkeurig willen beheren.

Door bijvoorbeeld naar deze code over te schakelen, krijgen we wat meer controle over wat er in het geheugen staat:

Door over te schakelen van malloc() en free() om de toewijzingsfuncties van Windows op een lager niveau te gebruiken VirtualAlloc() en VirtualFree() direct krijgen we betere controle.

We betalen echter een prijs in snelheid, omdat elke oproep aan VirtualAlloc() doet meer werk dan bellen malloc(), dat werkt door continu een blok van vooraf toegewezen low-level geheugen te verdelen en onder te verdelen.

gebruik VirtualAlloc() herhaaldelijk voor kleine blokken verbruikt ook meer geheugen in het algemeen, omdat elk blok voorbij komt VirtualAlloc() verbruikt doorgaans een veelvoud van 4KB geheugen (of 2MB, als u zogenaamde grote geheugenpagina's), zodat onze buffer van 128 bytes hierboven wordt afgerond naar 4096 bytes, waardoor de 3968 bytes aan het einde van het geheugenblok van 4 KB worden verspild.

Maar zoals je kunt zien, wordt het geheugen dat we terugkrijgen automatisch leeggemaakt (op nul gezet), dus we kunnen niet zien wat er eerder was, en deze keer crasht het programma wanneer we onze use-after-free proberen te doen truc, omdat Windows detecteert dat we proberen te gluren naar geheugen dat we niet langer bezitten:

C:UsersduckKEYPASS> unl2 Dumping 'nieuwe' buffer bij start 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 .. .............. 0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ Volledige tekenreeks was: onwaarschijnlijktextIBIPJPPHEOPOIDLL 0000EA75: 6 6E 69C 6 65B 6 79C 74 65 78 74 49 42 49 50 0000000000 onwaarschijnlijktextIBIP 0010EA4: 50A 50 48 45 4 50 4F 49 44F 4 4 00C 00C 00 00 0000000000 0020 00 JPPHEOPOIDLL ... : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0030EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0040EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ............... 0050EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0060EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................. 0070EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ............. ... Wachten op [ENTER] om buffer vrij te maken... Buffer dumpen na free() 0080EA00: [Programma is hier beëindigd omdat Windows onze use-after-free betrapte]

Omdat het geheugen dat we hebben vrijgemaakt, opnieuw moet worden toegewezen VirtualAlloc() voordat het opnieuw kan worden gebruikt, kunnen we ervan uitgaan dat het op nul wordt gezet voordat het wordt gerecycled.

Als we er echter zeker van willen zijn dat het leeg is, kunnen we de speciale Windows-functie aanroepen RtlSecureZeroMemory() net voordat het wordt vrijgegeven, om te garanderen dat Windows eerst nullen in onze buffer schrijft.

De bijbehorende functie RtlZeroMemory(), als je het je afvroeg, doet iets soortgelijks, maar zonder de garantie dat het daadwerkelijk werkt, omdat compilers het als theoretisch overbodig mogen verwijderen als ze merken dat de buffer daarna niet meer wordt gebruikt.

Zoals u kunt zien, moeten we er goed op letten de juiste Windows-functies te gebruiken als we de tijd willen minimaliseren dat geheimen die in het geheugen zijn opgeslagen, voor later kunnen blijven liggen.

In dit artikel gaan we niet kijken hoe u kunt voorkomen dat geheimen per ongeluk in uw wisselbestand worden opgeslagen door ze in fysiek RAM-geheugen te vergrendelen. (Tip: VirtualLock() is eigenlijk niet genoeg op zichzelf.) Als u meer wilt weten over Windows-geheugenbeveiliging op laag niveau, laat het ons dan weten in de opmerkingen en we zullen er in een toekomstig artikel naar kijken.

Automatisch geheugenbeheer gebruiken

Een handige manier om te voorkomen dat we zelf geheugen moeten toewijzen, beheren en vrijgeven, is door een programmeertaal te gebruiken die ervoor zorgt malloc() en free()of VirtualAlloc() en VirtualFree()automatisch.

Scripttaal zoals Perl, Python, Lua, JavaScript en anderen verwijderen de meest voorkomende geheugenveiligheidsbugs die C- en C++-code teisteren, door het geheugengebruik voor u op de achtergrond bij te houden.

Zoals we eerder vermeldden, werkt onze slecht geschreven voorbeeld C-code hierboven nu prima, maar alleen omdat het nog steeds een supereenvoudig programma is, met datastructuren van vaste grootte, waar we door inspectie kunnen verifiëren dat we onze 128- niet zullen overschrijven. byte buffer, en dat er slechts één uitvoeringspad is dat begint met malloc() en eindigt met een corresponderende free().

Maar als we het zouden updaten om het genereren van wachtwoorden met een variabele lengte mogelijk te maken, of als we extra functies aan het generatieproces zouden toevoegen, dan zouden wij (of degene die de volgende code onderhoudt) gemakkelijk eindigen met bufferoverflows, bugs die na gebruik niet meer gebruikt kunnen worden of geheugen dat komt nooit vrij en laat daarom geheime gegevens lang achter nadat ze niet langer nodig zijn.

In een taal als Lua kunnen we de Lua runtime-omgeving laten draaien, die doet wat in het jargon bekend staat als automatische afvalinzameling, omgaan met het verkrijgen van geheugen van het systeem en het teruggeven wanneer het detecteert dat we het niet meer gebruiken.

Het C-programma dat we hierboven hebben genoemd, wordt veel eenvoudiger wanneer geheugentoewijzing en de-toewijzing voor ons worden geregeld:

We wijzen geheugen toe om de string vast te houden s gewoon door de string toe te wijzen 'unlikelytext' aan.

We kunnen Lua later expliciet laten doorschemeren dat we er niet langer in geïnteresseerd zijn s door er de waarde aan toe te kennen nil (allemaal nils zijn in wezen hetzelfde Lua-object), of stoppen met gebruiken s en wacht tot Lua detecteert dat het niet langer nodig is.

Hoe dan ook, het geheugen dat wordt gebruikt door s wordt uiteindelijk automatisch hersteld.

En om bufferoverflows of verkeerd beheer van de grootte te voorkomen bij het toevoegen aan tekenreeksen (de Lua-operator .., uitgesproken concat, voegt in wezen twee strings samen, zoals + in Python), elke keer dat we een string verlengen of verkorten, wijst Lua op magische wijze ruimte toe voor een geheel nieuwe string, in plaats van de originele string te wijzigen of te vervangen in de bestaande geheugenlocatie.

Deze aanpak is langzamer en leidt tot pieken in het geheugengebruik die hoger zijn dan in C vanwege de tussenliggende reeksen die worden toegewezen tijdens tekstmanipulatie, maar het is veel veiliger met betrekking tot bufferoverflows.

Maar dit soort automatisch stringbeheer (in het jargon bekend als onveranderlijkheid, omdat strings nooit krijgen gemuteerd, of op hun plaats aangepast, zodra ze zijn gemaakt), brengt wel nieuwe cyberbeveiligingsproblemen met zich mee.

We hebben het Lua-programma hierboven uitgevoerd op Windows, tot de tweede pauze, net voordat het programma werd afgesloten:

C:UsersduckKEYPASS> lua s1.lua Volledige tekenreeks is: onwaarschijnlijktextHLKONBOJILAGLNLN Wachten op [ENTER] voordat tekenreeks wordt vrijgegeven... Wachten op [ENTER] voordat wordt afgesloten...

Deze keer hebben we een procesgeheugendump gemaakt, zoals deze:

C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Sysinternals-hulpprogramma voor procesdump Copyright (C) 2009-2022 Mark Russinovich en Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump 1 gestart: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Dump 1 schrijven: geschatte grootte van het dumpbestand is 10 MB. [00:00:00] Dump 1 voltooid: 10 MB geschreven in 0.1 seconden [00:00:01] Aantal dumps bereikt.

Vervolgens hebben we dit eenvoudige script uitgevoerd, dat het dumpbestand weer inleest en overal in het geheugen de bekende string vindt unlikelytext verscheen en drukt het af, samen met de locatie in het dumpbestand en de ASCII-tekens die er onmiddellijk op volgden:

Zelfs als je eerder scripttalen hebt gebruikt of in een programmeer-ecosysteem hebt gewerkt met zogenaamde beheerde snaren, waar het systeem geheugentoewijzingen en deallocaties voor u bijhoudt en deze naar eigen inzicht afhandelt...

…u zult misschien verrast zijn om de uitvoer te zien die deze geheugenscan produceert:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: onwaarschijnlijktextALJBNGOAPLLBDEB 006D8B3C: onwaarschijnlijktextALJBNGOA 006D8B7C: onwaarschijnlijktextALJBNGO 006D8BFC: onwaarschijnlijktextALJBNGOAPLLBDEBJ 006D8CBC: onwaarschijnlijktextALJBN 006D8D7C : onwaarschijnlijktekstALJBNGOAP 006D903C: onwaarschijnlijktekstALJBNGOAPL 006D90BC: onwaarschijnlijktekstALJBNGOAPLL 006D90FC: onwaarschijnlijktekstALJBNG 006D913C: onwaarschijnlijktekstALJBNGOAPLLB 006D91BC: onwaarschijnlijktekstALJB 006D91FC: onwaarschijnlijktekstALJBNGOAPLLBD 006D923C : onwaarschijnlijktekstALJBNGOAPLLBDE 006DB70C: onwaarschijnlijktekstALJ 006DBB8C: onwaarschijnlijktekstAL 006DBD0C: onwaarschijnlijktekstA

Kijk eens, op dat moment pakten we onze geheugendump, ook al waren we klaar met de string s (en vertelde Lua dat we het niet meer nodig hadden door te zeggen s = nil), waren alle strings die de code onderweg had gemaakt nog steeds aanwezig in het RAM, nog niet hersteld of verwijderd.

Als we de bovenstaande uitvoer sorteren op de tekenreeksen zelf, in plaats van de volgorde te volgen waarin ze in RAM verschenen, kun je je voorstellen wat er gebeurde tijdens de lus waarin we één teken tegelijk aan onze wachtwoordreeks hebben toegevoegd:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: onwaarschijnlijktextA 006DBB8C: onwaarschijnlijktextAL 006DB70C: onwaarschijnlijktextALJ 006D91BC: onwaarschijnlijktextALJB 006D8CBC: onwaarschijnlijktextALJBN 006D90FC: onwaarschijnlijktextALJBNG 006D8B7C: onwaarschijnlijktextALJBNGO 006D8B3C: onwaarschijnlijktextALJBNGOA 006 D8D7C: onwaarschijnlijktekstALJBNGOAP 006D903C: onwaarschijnlijktekstALJBNGOAPL 006D90BC: onwaarschijnlijktekstALJBNGOAPLL 006D913C: onwaarschijnlijktekstALJBNGOAPLLB 006D91FC: onwaarschijnlijktekstALJBNGOAPLLBD 006D923C: onwaarschijnlijktekstALJBNGOAPLLBDE 006D8AFC: onwaarschijnlijktekstALJBNGOAPLL BDEB 006D8BFC : onwaarschijnlijktekstALJBNGOAPLLBDEBJ

Al die tijdelijke, tussenliggende strings zijn er nog steeds, dus zelfs als we met succes de uiteindelijke waarde van hadden weggevaagd s, zouden we nog steeds alles lekken behalve het laatste karakter.

In dit geval zelfs toen we ons programma opzettelijk dwongen om alle onnodige gegevens te verwijderen door de speciale Lua-functie aan te roepen collectgarbage() (de meeste scripttalen hebben iets soortgelijks), de meeste gegevens in die vervelende tijdelijke tekenreeksen bleven toch in het RAM hangen, omdat we Lua hadden gecompileerd om het automatische geheugenbeheer te doen met de goede oude malloc() en free().

Met andere woorden, zelfs nadat Lua zelf zijn tijdelijke geheugenblokken had teruggevorderd om ze opnieuw te gebruiken, hadden we geen controle over hoe of wanneer die geheugenblokken zouden worden hergebruikt, en dus hoe lang ze met hun linkerhand in het proces zouden rondslingeren. over gegevens die wachten om te worden opgesnoven, gedumpt of anderszins gelekt.

Voer .NET in

Maar hoe zit het met KeePass, waar dit artikel mee begon?

KeePass is geschreven in C# en gebruikt de .NET-runtime, dus het vermijdt de problemen van slecht geheugenbeheer die C-programma's met zich meebrengen...

...maar C# beheert zijn eigen tekstreeksen, ongeveer zoals Lua dat doet, wat de vraag oproept:

Zelfs als de programmeur vermeed om het volledige hoofdwachtwoord op één plek op te slaan nadat hij ermee klaar was, zouden aanvallers met toegang tot een geheugendump toch genoeg overgebleven tijdelijke gegevens kunnen vinden om het hoofdwachtwoord te raden of te herstellen, zelfs als die aanvallers toegang tot uw computer kregen, minuten, uren of dagen nadat u het wachtwoord had ingevoerd?

Simpel gezegd, zijn er detecteerbare, spookachtige overblijfselen van uw hoofdwachtwoord die overleven in het RAM-geheugen, zelfs nadat u zou verwachten dat ze zijn verwijderd?

Vervelend, als Github-gebruiker ontdekte Vdohney, is het antwoord (tenminste voor KeePass-versies ouder dan 2.54) "Ja".

Voor alle duidelijkheid: we denken niet dat uw daadwerkelijke hoofdwachtwoord kan worden hersteld als een enkele tekenreeks uit een KeePass-geheugendump, omdat de auteur een speciale functie heeft gemaakt voor het invoeren van het hoofdwachtwoord die er alles aan doet om te voorkomen dat het volledige wachtwoord wordt opgeslagen. wachtwoord waar het gemakkelijk kan worden opgemerkt en opgesnoven.

We stelden ons hiervan tevreden door ons hoofdwachtwoord in te stellen op SIXTEENPASSCHARS, typ het in en neem onmiddellijk, kort en lang daarna geheugendumps.

We doorzochten de dumps met een eenvoudig Lua-script dat overal naar die wachtwoordtekst zocht, zowel in 8-bits ASCII-indeling als in 16-bits UTF-16 (Windows widechar)-indeling, zoals deze:

De resultaten waren bemoedigend:

C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Lezen van dumpbestand... KLAAR. Zoeken naar SIXTEENPASSCHARS als 8-bit ASCII... niet gevonden. Zoeken naar SIXTEENPASSCHARS als UTF-16... niet gevonden.

Maar Vdohney, de ontdekker van CVE-2023-32784, merkte op dat terwijl je je hoofdwachtwoord typt, KeePass je visuele feedback geeft door een tijdelijke aanduiding te maken en weer te geven die bestaat uit Unicode "blob"-tekens, tot en met de lengte van je wachtwoord:

In widechar-tekstreeksen op Windows (die uit twee bytes per teken bestaan, niet slechts één byte zoals in ASCII), wordt het "blob"-teken in RAM gecodeerd als de hex-byte 0xCF gevolgd door 0x25 (wat toevallig een procentteken is in ASCII).

Dus zelfs als KeePass grote zorg besteedt aan de onbewerkte tekens die u invoert wanneer u het wachtwoord zelf invoert, zou u kunnen eindigen met overgebleven reeksen "blob" -tekens, gemakkelijk te detecteren in het geheugen als herhaalde runs zoals CF25CF25 or CF25CF25CF25...

... en als dat zo is, zou de langste reeks blob-tekens die u hebt gevonden waarschijnlijk de lengte van uw wachtwoord verraden, wat een bescheiden vorm van lekken van wachtwoordinformatie zou zijn, als er niets anders was.

We hebben het volgende Lua-script gebruikt om te zoeken naar tekenen van overgebleven tekenreeksen voor tijdelijke aanduidingen voor wachtwoorden:

De uitvoer was verrassend (we hebben opeenvolgende regels verwijderd met hetzelfde aantal blobs, of met minder blobs dan de vorige regel, om ruimte te besparen):

C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ gaat op dezelfde manier door voor 8 blobs, 9 blobs, enz. ] [ tot twee laatste regels van precies 16 blobs elk ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [ alle resterende overeenkomsten zijn één blob lang] 0123B058: *

Op dicht bij elkaar gelegen maar steeds groter wordende geheugenadressen vonden we een systematische lijst van 3 blobs, vervolgens 4 blobs, enzovoort tot 16 blobs (de lengte van ons wachtwoord), gevolgd door vele willekeurig verspreide instanties van single-blob strings .

Dus die placeholder "blob" -reeksen lijken inderdaad in het geheugen te lekken en achter te blijven om de wachtwoordlengte te lekken, lang nadat de KeePass-software klaar is met uw hoofdwachtwoord.

De volgende stap

We besloten om verder te graven, net als Vdohney deed.

We hebben onze patroonvergelijkingscode gewijzigd om ketens van blob-tekens te detecteren, gevolgd door een enkel ASCII-teken in 16-bits indeling (ASCII-tekens worden in UTF-16 weergegeven als hun gebruikelijke 8-bits ASCII-code, gevolgd door een nulbyte).

Deze keer hebben we, om ruimte te besparen, de uitvoer onderdrukt voor elke overeenkomst die exact overeenkomt met de vorige:

Verrassing, verrassing:

C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp
00BE581B: *I
00BE621B: **X
00BE6BD3: ***T
00BE769B: ****E
00BE822B: *****E
00BE8C6B: ******N
00BE974B: *******P
00BEA25B: ********A
00BEAD33: *********S
00BEB81B: **********S
00BEC383: ***********C
00BECEEB: ************H
00BEDA5B: *************A
00BEE623: **************R
00BEF1A3: ***************S
03E97CF2: *N
0AA6F0AF: *W
0D8AF7C8: *X
0F27BAF8: *S

Kijk eens wat we halen uit de beheerde stringgeheugenregio van .NET!

Een dicht opeengepakte set tijdelijke "blobstrings" die de opeenvolgende tekens in ons wachtwoord onthullen, te beginnen met het tweede teken.

Die lekkende reeksen worden gevolgd door wijdverspreide overeenkomsten van één teken waarvan we aannemen dat ze door toeval zijn ontstaan. (Een KeePass-dumpbestand is ongeveer 250 MB groot, dus er is genoeg ruimte voor "blob"-tekens om als bij toeval te verschijnen.)

Zelfs als we rekening houden met die extra vier overeenkomsten, in plaats van ze weg te gooien als waarschijnlijke mismatches, kunnen we raden dat het hoofdwachtwoord een van de volgende is:

?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS

Het is duidelijk dat deze eenvoudige techniek het eerste teken in het wachtwoord niet vindt, omdat de eerste "blobstring" pas wordt geconstrueerd nadat dat eerste teken is ingetypt

Merk op dat deze lijst lekker kort is omdat we overeenkomsten hebben uitgefilterd die niet eindigen op ASCII-tekens.

Als je op zoek was naar karakters in een ander bereik, zoals Chinese of Koreaanse karakters, zou je meer toevallige treffers kunnen krijgen, omdat er veel meer mogelijke karakters zijn om op te matchen...

…maar we vermoeden dat je toch vrij dicht bij je hoofdwachtwoord komt, en de "blobstrings" die betrekking hebben op het wachtwoord lijken bij elkaar gegroepeerd in RAM, vermoedelijk omdat ze ongeveer tegelijkertijd door hetzelfde deel van het wachtwoord werden toegewezen de .NET-runtime.

En daar, in een weliswaar lange en discursieve notendop, is het fascinerende verhaal van CVE-2023-32784.

Wat te doen?

  • Als u een KeePass-gebruiker bent, raak dan niet in paniek. Hoewel dit een bug is en technisch gezien een kwetsbaarheid die kan worden misbruikt, moeten aanvallers op afstand die uw wachtwoord willen kraken met behulp van deze bug eerst malware op uw computer installeren. Dat zou hen veel andere manieren geven om uw wachtwoorden rechtstreeks te stelen, zelfs als deze bug niet bestond, bijvoorbeeld door uw toetsaanslagen te loggen terwijl u typt. Op dit moment kunt u gewoon uitkijken naar de aanstaande update en deze pakken wanneer deze klaar is.
  • Als u geen volledige schijfversleuteling gebruikt, kunt u overwegen deze in te schakelen. Om achtergebleven wachtwoorden uit uw wisselbestand of slaapstandbestand (schijfbestanden van het besturingssysteem die worden gebruikt om geheugeninhoud tijdelijk op te slaan tijdens zware belasting of wanneer uw computer "slaapt"), hebben aanvallers directe toegang tot uw harde schijf nodig. Als u BitLocker of het equivalent daarvan voor andere besturingssystemen hebt geactiveerd, hebben ze geen toegang tot uw wisselbestand, uw slaapstandbestand of andere persoonlijke gegevens zoals documenten, spreadsheets, opgeslagen e-mails, enzovoort.
  • Als je een programmeur bent, blijf dan op de hoogte van problemen met geheugenbeheer. Ga er niet zomaar vanuit dat elke free() komt overeen met de corresponderende malloc() dat uw gegevens veilig zijn en goed worden beheerd. Soms moet u extra voorzorgsmaatregelen nemen om te voorkomen dat er geheime gegevens rondslingeren, en die voorzorgsmaatregelen variëren van besturingssysteem tot besturingssysteem.
  • Als u een QA-tester of een codebeoordelaar bent, denk dan altijd "achter de schermen". Zelfs als de geheugenbeheercode er netjes en uitgebalanceerd uitziet, moet u zich bewust zijn van wat er achter de schermen gebeurt (omdat de oorspronkelijke programmeur dit misschien niet wist), en bereid u voor op wat pentest-achtig werk, zoals runtime-monitoring en geheugen dumping om te controleren of de beveiligde code zich echt gedraagt ​​zoals het hoort.

CODE UIT HET ARTIKEL: UNL1.C

#erbij betrekken #erbij betrekken #erbij betrekken void hexdump(unsigned char* buff, int len) {// Print buffer in 16-byte brokken voor (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +ik); // Toon 16 bytes als hexadecimale waarden voor (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Herhaal die 16 bytes als karakters voor (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Verzamel geheugen om het wachtwoord op te slaan, en laat zien wat // in de buffer staat als het officieel "nieuw" is... char* buff = malloc(128); printf("Dumpen 'nieuwe' buffer bij startn"); hexdump(buff,128); // Gebruik pseudowillekeurig bufferadres als random seed srand((unsigned)buff); // Start het wachtwoord met een vaste, doorzoekbare tekst strcpy(buff,"unlikelytext"); // Voeg 16 pseudowillekeurige letters toe, één voor één voor (int i = 1; i <= 16; i++) { // Kies een letter van A (65+0) tot P (65+15) char ch = 65 + (rand() & 15); // Wijzig vervolgens de buff-string in plaats strncat(buff,&ch,1); } // Het volledige wachtwoord is nu in het geheugen, dus print // het als een string, en toon de hele buffer... printf("Volledige string was: %sn",buff); hexdump(buff,128); // Pauzeer nu om proces-RAM te dumpen (probeer: 'procdump -ma') puts("Waiting for [ENTER] to free buffer..."); getchar(); // Maak formeel het geheugen vrij() en toon de buffer // nogmaals om te zien of er iets is achtergebleven... free(buff); printf("Dumping buffer na free()n"); hexdump(buff,128); // Pauzeer om RAM opnieuw te dumpen om verschillen te inspecteren puts ("Waiting for [ENTER] to exit main()..."); getchar(); geef 0 terug; }

CODE UIT HET ARTIKEL: UNL2.C

#erbij betrekken #erbij betrekken #erbij betrekken #erbij betrekken void hexdump(unsigned char* buff, int len) {// Print buffer in 16-byte brokken voor (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +ik); // Toon 16 bytes als hexadecimale waarden voor (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Herhaal die 16 bytes als karakters voor (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Verzamel geheugen om het wachtwoord op te slaan, en laat zien wat // in de buffer staat als het officieel "nieuw" is... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Dumpen 'nieuwe' buffer bij startn"); hexdump(buff,128); // Gebruik pseudowillekeurig bufferadres als random seed srand((unsigned)buff); // Start het wachtwoord met een vaste, doorzoekbare tekst strcpy(buff,"unlikelytext"); // Voeg 16 pseudowillekeurige letters toe, één voor één voor (int i = 1; i <= 16; i++) { // Kies een letter van A (65+0) tot P (65+15) char ch = 65 + (rand() & 15); // Wijzig vervolgens de buff-string in plaats strncat(buff,&ch,1); } // Het volledige wachtwoord is nu in het geheugen, dus print // het als een string, en toon de hele buffer... printf("Volledige string was: %sn",buff); hexdump(buff,128); // Pauzeer nu om proces-RAM te dumpen (probeer: 'procdump -ma') puts("Waiting for [ENTER] to free buffer..."); getchar(); // Maak formeel het geheugen vrij() en toon de buffer // nogmaals om te zien of er iets is achtergebleven... VirtualFree(buff,0,MEM_RELEASE); printf("Dumping buffer na free()n"); hexdump(buff,128); // Pauzeer om RAM opnieuw te dumpen om verschillen te inspecteren puts ("Waiting for [ENTER] to exit main()..."); getchar(); geef 0 terug; }

CODE UIT HET ARTIKEL: S1.LUA

-- Begin met een vaste, doorzoekbare tekst s = 'unlikelytext' -- Voeg 16 willekeurige tekens toe van 'A' tot 'P' voor i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Volledige string is:',s,'n') -- Pauzeren om proces te dumpen RAM print('Wachten op [ENTER] voordat string wordt vrijgegeven...') io.read() - - Wis tekenreeks en markeer variabele ongebruikt s = nil -- Dump RAM opnieuw om te zoeken naar diffs print('Waiting for [ENTER] before exiting...') io.read()

CODE UIT HET ARTIKEL: FINDIT.LUA

-- lees in dumpbestand lokaal f = io.open(arg[1],'rb'):read('*a') -- zoek naar markeringstekst gevolgd door één -- of meer willekeurige ASCII-tekens lokaal b,e ,m = 0,0,nil terwijl waar do -- zoek naar volgende overeenkomst en onthoud offset b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- verlaat wanneer niet meer komt overeen zo niet b then break end -- rapporteer positie en string gevonden print(string.format('%08X: %s',b,m)) end

CODE UIT HET ARTIKEL: SEARCHKNOWN.LUA

io.write('Lezen in dumpbestand... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Zoeken naar SIXTEENPASSCHARS als 8-bit ASCII... ') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 en 'FOUND' of 'not found','.n') io.write ('Zoeken naar SIXTEENPASSCHARS als UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 en 'GEVONDEN' of 'niet gevonden','.n ')

CODE UIT HET ARTIKEL: FINDBLOBS.LUA

-- lees het dumpbestand in dat is opgegeven op de opdrachtregel local f = io.open(arg[1],'rb'):read('*a') -- Zoek naar een of meer wachtwoord-blobs, gevolgd door een niet-blob -- Merk op dat blob-tekens (●) coderen in Windows-widechars -- als litte-endian UTF-16-codes, die uitkomen als CF 25 in hex. local b,e,m = 0,0,nil while true do -- We willen een of meer blobs, gevolgd door een niet-blob. -- We vereenvoudigen de code door te zoeken naar een expliciete CF25 -- gevolgd door een tekenreeks die alleen CF of 25 bevat, dus we vinden zowel CF25CFCF of CF2525CF als CF25CF25. -- We filteren "false positives" later uit als die er zijn. -- We moeten '%%' schrijven in plaats van x25 omdat het x25 ---teken (procentteken) een speciaal zoekteken is in Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- afsluiten als er geen overeenkomsten meer zijn, zo niet b dan einde beëindigen -- CMD.EXE kan niet afdrukken blobs, dus we converteren ze naar sterren. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) einde

CODE UIT HET ARTIKEL: ZOEKENKP.LUA

-- dumpbestand inlezen gespecificeerd op opdrachtregel local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Nu willen we een of meer blobs (CF25) gevolgd door de code -- voor A..Z gevolgd door een 0 byte om ACSCII naar UTF-16 te converteren b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- exit when no more matches if not b then break end -- CMD.EXE kan geen blobs printen, dus converteren we ze naar sterren. -- Om ruimte te besparen onderdrukken we opeenvolgende overeenkomsten als m ~= p then print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m einde einde

spot_img

Laatste intelligentie

spot_img

Chat met ons

Hallo daar! Hoe kan ik u helpen?