Under de senaste två veckorna har vi sett en serie artiklar som talar om vad som har beskrivits som en "master password crack" i den populära öppen källkodshanteraren KeePass.
Felet ansågs tillräckligt viktigt för att få en officiell identifierare för amerikanska myndigheter (den kallas CVE-2023-32784, om du vill jaga det), och med tanke på att huvudlösenordet till din lösenordshanterare i stort sett är nyckeln till hela ditt digitala slott, kan du förstå varför historien väckte mycket spänning.
Den goda nyheten är att en angripare som ville utnyttja denna bugg nästan säkert skulle behöva ha infekterat din dator med skadlig programvara redan, och skulle därför kunna spionera på dina tangenttryckningar och program som körs ändå.
Med andra ord kan buggen betraktas som en lätthanterlig risk tills skaparen av KeePass kommer ut med en uppdatering, som bör dyka upp snart (i början av juni 2023, tydligen).
Som den som avslöjar felet tar hand om påpeka:
Om du använder fullständig diskkryptering med ett starkt lösenord och ditt system är [fritt från skadlig programvara] borde det gå bra. Ingen kan stjäla dina lösenord på distans över internet bara med detta fynd.
Riskerna förklaras
Tungt sammanfattat handlar buggen om svårigheten att se till att alla spår av konfidentiell data rensas från minnet när du är klar med dem.
Vi kommer här att bortse från problemen med hur man kan undvika att ha hemlig data i minnet överhuvudtaget, även kort.
I den här artikeln vill vi bara påminna programmerare överallt om att kod som godkänts av en säkerhetsmedveten granskare med en kommentar som "tycks städa upp korrekt efter sig"...
...kan faktiskt inte städa upp helt alls, och det potentiella dataläckaget kanske inte är uppenbart från en direkt studie av själva koden.
Enkelt uttryckt innebär CVE-2023-32784-sårbarheten att ett KeePass-huvudlösenord kan återställas från systemdata även efter att KeyPass-programmet har avslutats, eftersom tillräcklig information om ditt lösenord (även om det inte faktiskt är själva rålösenordet, vilket vi kommer att fokusera på) på om ett ögonblick) kan bli kvar i systembytes- eller vilofiler, där tilldelat systemminne kan sluta sparas för senare.
På en Windows-dator där BitLocker inte används för att kryptera hårddisken när systemet är avstängt, skulle detta ge en skurk som stal din bärbara dator en strid chans att starta upp från en USB- eller CD-enhet och återställa ditt huvudlösenord till och med även om KeyPass-programmet självt ser till att aldrig spara det permanent på disken.
En långvarig lösenordsläcka i minnet innebär också att lösenordet i teorin kan återställas från en minnesdump av KeyPass-programmet, även om den dumpningen togs långt efter att du skrivit in lösenordet och långt efter KeePass. själv behövde inte längre ha den kvar.
Det är klart att du bör anta att skadlig programvara som redan finns på ditt system kan återställa nästan alla inskrivna lösenord via en mängd olika tekniker för snoopning i realtid, så länge de var aktiva när du skrev. Men du kan rimligen förvänta dig att din tid som utsätts för fara skulle vara begränsad till den korta tiden du skriver, inte förlängd till många minuter, timmar eller dagar efteråt, eller kanske längre, inklusive efter att du stängt av din dator.
Vad blir kvar?
Vi tänkte därför ta en titt på hög nivå på hur hemlig data kan bli kvar i minnet på sätt som inte är direkt uppenbara från koden.
Oroa dig inte om du inte är en programmerare – vi håller det enkelt och förklarar allt eftersom.
Vi börjar med att titta på minnesanvändning och rensning i ett enkelt C-program som simulerar inmatning och tillfällig lagring av ett lösenord genom att göra följande:
- Tilldela en dedikerad bit minne speciellt för att lagra lösenordet.
- Infoga en känd textsträng så att vi lätt kan hitta den i minnet om det behövs.
- Lägger till 16 pseudo-slumpmässiga 8-bitars ASCII-tecken från intervallet AP.
- Utskrift den simulerade lösenordsbufferten.
- Frigör minnet i hopp om att radera lösenordsbufferten.
- avsluta programmet.
Mycket förenklat kan C-koden se ut ungefär så här, utan felkontroll, med pseudoslumptal av dålig kvalitet från C runtime-funktionen rand()
, och ignorera eventuella buffertspillkontroller (gör aldrig något av detta i riktig kod!):
// 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);
Faktum är att koden som vi slutligen använde i våra tester innehåller några ytterligare bitar och delar som visas nedan, så att vi kan dumpa hela innehållet i vår tillfälliga lösenordsbuffert när vi använde den, för att leta efter oönskat eller överblivet innehåll.
Observera att vi medvetet dumpar bufferten efter att ha anropat free()
, som tekniskt sett är en bugg utan användning efter fri, men vi gör det här som ett lömskt sätt att se om något kritiskt blir kvar efter att ha lämnat tillbaka vår buffert, vilket kan leda till ett farligt dataläckage i verkligheten.
Vi har också lagt in två Waiting for [Enter]
uppmaningar i koden för att ge oss själva en chans att skapa minnesdumpar vid viktiga punkter i programmet, vilket ger oss rådata att söka efter senare, för att se vad som blev kvar när programmet kördes.
För att göra minnesdumpar kommer vi att använda Microsoft Sysinternals verktyg procdump
med -ma
alternativ (dumpa allt minne), vilket undviker behovet av att skriva vår egen kod för att använda Windows DbgHelp
systemet och dess ganska komplicerade MiniDumpXxxx()
funktioner.
För att kompilera C-koden använde vi vår egen lilla och enkla konstruktion av Fabrice Bellards gratis och öppen källkod Tiny C-kompilator, tillgänglig för 64-bitars Windows i källa och binär form direkt från vår GitHub-sida.
Kopiera och klistra in text av all källkod som visas i artikeln visas längst ner på sidan.
Detta är vad som hände när vi kompilerade och körde testprogrammet:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard avskalad av Paul Ducklin för användning som ett läromedel Version petcc64-0.9.27 [0006] - Genererar 64-bitars Endast PEs -> 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 filstorlek avsnitt 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 byte) C:UsersduckKEYPASS> unl1.exe Dumpar "ny" buffert vid 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 65d. exe.D 00F44B32: 00 513 0 72 69 76 65 72 44 61D 74 61A 3C 43 3 5E riverData=C:Win 57F69C6: 00 513F 0 64 6 77 73 5 53 79 73 74 65 C 6 33 dowsSystem32Dr 5F44D72: 32 00 513 0 69 76C 65 72 73 5 44 72 69 76 65 72 iversDriverData 44F61E74: 61 00 513 0 00F 45 46 43 5 34D 33 37 32 3 31F 00 46 50 53F 5F4372: 1 00 513F 0 42 52 4 57F 53 45 52 5F 41 50 50F 5 BROWSER_APP_PROF 50F52: 4 46C 00 51400F 49 4 45 5 53E 54 52D 49 4E 47 3 49 ILE_STRING=Inter 6F74E 65 72F00 51410 6C 65A 74 F20 45C AC 78B 70 6 netto ExplzV.< .K.. Hela strängen var: osannoliktextJHKNEJJCPOMDJHAN 7F56: 4 3E 4C 00 00B 00 51390C 75 6 6 69 6 65A 6 79B 74E osannoliktextJHKN 65F78A 74F4 48A 4A 4 00A 513 0 45 4A 4 43 50E 4 4 44 4 EJJCPOMDJHAN.eD 48F41B4 d 00Dr 65F00D44: 00 513 0 72 69 76C 65 72 44 61 74 61 3 43 3 5 iversDriverData 57F69E6: 00 513 0 64 6F 77 73 5 53 79D 73 74 65 6 33 32F .EFC5_44: 72F _32 = 00F _513:0 69F 76 65 72 73 5F 44 72 69 76F 65 72 44F 61 BROWSER_APP_PROF 74F61: 00 513C 0 00F 45 46 43 5 34E 33 37D 32 3E 31 00 46 ILE_STRING=Inter 50F53: 5E 4372 1 00A 513C C AC 0B 42 52 netto ExplzV.<.K.. Väntar på att [ENTER] ska frigöra buffert... Dumpa buffert efter free() 4F57: A53 45 F52 5 41 50 50 5 50 52 F4 46 00 51400 49 4 .g......P...... 45F5A53: 54 52A 49A 4 47 3F 49D 6 74A 65 72 00E 51410 6 65 74 EJJCPOMDJHAN.eD 20F45B78: 70 6 7 56 4 3 4 00 00 00 51390 0E riverData=C:Win 67F5C00: 00 00F 00 00 50C 01 5 00 00 00 00D 00 00 513C 0 45 dowsSystem4Dr 4F43D50: 4 4 44 4 48 41C 4 00 65 00 44 00 513 0 Driver:E Date 72 69 76 65 72 44 61 74F 61 3 43 3 5D 57 69 6 00 513 0F .EFC_64=6.FPS_ 77F73F5: 53 79 73F 74 65 6 33 32F 5 44 72 32F 00 513 0F 69 BROWSER_APP_PROF 76F65: 72 E 73 5 44 72D 69 76E 65 72 44 ILE_STRING=Inter 61F74: 61E 00 513 0 00 45 46 43C 5D 34 33 37D AC 32B 3 31 netto ExplM..MK. Väntar på att [ENTER] ska avsluta main()... C:UsersduckKEYPASS>
I den här körningen brydde vi oss inte om att ta tag i några processminnesdumpar, eftersom vi kunde se direkt från utgången att den här koden läcker data.
Direkt efter att ha anropat Windows C runtime-biblioteksfunktionen malloc()
, kan vi se att bufferten vi får tillbaka innehåller vad som ser ut som miljövariabeldata som blivit över från programmets startkod, med de första 16 byten som tydligen har ändrats för att se ut som någon form av överbliven minnesallokeringshuvud.
(Observera hur dessa 16 byte ser ut som två 8-byte minnesadresser, 0xF55790
och 0xF50150
, som är precis efter respektive precis före vår egen minnesbuffert.)
När lösenordet ska finnas i minnet kan vi se hela strängen tydligt i bufferten, som vi förväntar oss.
Men efter att ha ringt free()
, notera hur de första 16 byten i vår buffert har skrivits om med vad som ser ut som närliggande minnesadresser igen, förmodligen så att minnesallokatorn kan hålla reda på block i minnet som den kan återanvända ...
… men resten av vårt "raderade" lösenordstext (de sista 12 slumpmässiga tecknen EJJCPOMDJHAN
) har lämnats kvar.
Vi behöver inte bara hantera våra egna minnesallokeringar och avallokeringar i C, vi måste också se till att vi väljer rätt systemfunktioner för databuffertar om vi vill kontrollera dem exakt.
Till exempel, genom att byta till den här koden istället får vi lite mer kontroll över vad som finns i minnet:
Genom att byta från malloc()
och free()
för att använda Windows-tilldelningsfunktionerna på lägre nivå VirtualAlloc()
och VirtualFree()
direkt får vi bättre kontroll.
Däremot betalar vi ett pris i hastighet, eftersom varje samtal till VirtualAlloc()
gör mer arbete än en uppmaning till malloc()
, som fungerar genom att kontinuerligt dela upp och dela upp ett block av förallokerat lågnivåminne.
Använda VirtualAlloc()
upprepade gånger för små block använder också mer minne totalt sett, eftersom varje block delas ut av VirtualAlloc()
förbrukar vanligtvis en multipel av 4KB minne (eller 2MB, om du använder s.k. stora minnessidor), så att vår 128-byte buffert ovan avrundas uppåt till 4096 byte, vilket slösar bort de 3968 byte i slutet av 4KB minnesblocket.
Men, som du kan se, töms minnet vi får tillbaka automatiskt (inställt på noll), så vi kan inte se vad som fanns där innan, och den här gången kraschar programmet när vi försöker göra vår användning efter-fri trick, eftersom Windows upptäcker att vi försöker kika på minne som vi inte längre äger:
C:UsersduckKEYPASS> unl2 Dumpning av 'ny' buffert vid start 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0020 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0030 00 00. ............ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0040 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0050 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0060 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0070 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0080 00 ................ Hela strängen var: unlikelytextIBIPJPPHEOPOIDLL 00EA00: 00 00E 00C 00 00B 00 00C 00 00 00 00 00 00 0000000000 0000 75 osannolikt 6IP 6 69IP 6 65 6 79 74F 65 78F 74 49 42C 49C 50 0000000000 0010 4 JPPHEOPOIDLL.... 50EA50: 48 45 4 50 4 49 44 4 4 00 00 00 00 0000000000 0020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0030 00 00 00 00 : 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0040 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0050 00 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 0000000000 0060 00 00 00 ............... ... 00 00 00 00 ................ 00EA00: 00 00 00 00 00 00 00 0000000000 0070 00 00 00 00 00 00 00 ............. ... Väntar på att [ENTER] ska frigöra buffert... Dumpar buffert efter free() 00EA00: [Programmet avslutades här eftersom Windows fångade vår use-efter-free]
Eftersom minnet vi frigjorde kommer att behöva omfördelas med VirtualAlloc()
innan den kan användas igen kan vi anta att den nollställs innan den återvinns.
Men om vi ville försäkra oss om att den var tom, kunde vi anropa den speciella Windows-funktionen RtlSecureZeroMemory()
precis innan den frigörs, för att garantera att Windows kommer att skriva nollor i vår buffert först.
Den relaterade funktionen RtlZeroMemory()
, om du undrade, gör en liknande sak, men utan garanti för att faktiskt fungera, eftersom kompilatorer får ta bort det som teoretiskt överflödigt om de märker att bufferten inte används igen efteråt.
Som du kan se måste vi vara mycket noga med att använda rätt Windows-funktioner om vi vill minimera den tid som hemligheter lagrade i minnet kan ligga runt för senare.
I den här artikeln kommer vi inte att titta på hur du förhindrar att hemligheter sparas av misstag till din växlingsfil genom att låsa dem i fysiskt RAM. (Antydan: VirtualLock()
räcker faktiskt inte i sig.) Om du vill veta mer om lågnivå Windows-minnessäkerhet, låt oss veta i kommentarerna så kommer vi att titta på det i en framtida artikel.
Använder automatisk minneshantering
Ett snyggt sätt att undvika att behöva allokera, hantera och deallokera minne själva är att använda ett programmeringsspråk som tar hand om malloc()
och free()
, eller VirtualAlloc()
och VirtualFree()
, automatiskt.
Skriptspråk som t.ex Perl, Python, lua, JavaScript och andra blir av med de vanligaste minnessäkerhetsbuggarna som plågar C- och C++-kod genom att spåra minnesanvändning åt dig i bakgrunden.
Som vi nämnde tidigare fungerar vår dåligt skrivna exempel C-kod ovan bra nu, men bara för att det fortfarande är ett superenkelt program, med datastrukturer med fast storlek, där vi genom inspektion kan verifiera att vi inte kommer att skriva över vår 128- byte buffert, och att det bara finns en exekveringsväg som börjar med malloc()
och slutar med en motsvarande free()
.
Men om vi uppdaterade den för att tillåta generering av lösenord med variabel längd, eller la till ytterligare funktioner i genereringsprocessen, så skulle vi (eller vem som än underhåller koden härnäst) lätt kunna sluta med buffertspill, buggar utan användning efter fri, eller minne som frigörs aldrig och lämnar därför hemlig data hängande långt efter att den inte längre behövs.
På ett språk som Lua kan vi låta Lua köra miljön, som gör det som är känt på jargongen som automatisk sophämtning, ta itu med att skaffa minne från systemet och returnera det när det upptäcker att vi har slutat använda det.
C-programmet vi listade ovan blir mycket enklare när minnesallokering och deallokering tas om hand för oss:
Vi tilldelar minne för att hålla strängen s
helt enkelt genom att tilldela strängen 'unlikelytext'
till den.
Vi kan senare antingen antyda till Lua uttryckligen att vi inte längre är intresserade av s
genom att tilldela den värdet nil
(Allt nils
är i huvudsak samma Lua-objekt), eller sluta använda s
och vänta på att Lua ska upptäcka att det inte längre behövs.
Hursomhelst, minnet som används av s
kommer så småningom att återställas automatiskt.
Och för att förhindra buffertspill eller storleksfelhantering vid tillägg till textsträngar (Lua-operatorn ..
, uttalad konkat, lägger i huvudsak två strängar tillsammans, som +
i Python), varje gång vi förlänger eller förkortar en sträng, allokerar Lua magiskt utrymme för en helt ny sträng, snarare än att modifiera eller ersätta den ursprungliga i dess befintliga minnesplats.
Detta tillvägagångssätt är långsammare och leder till minnesanvändningstoppar som är högre än vad du skulle få i C på grund av de mellanliggande strängarna som tilldelas under textmanipulering, men det är mycket säkrare med avseende på buffertspill.
Men den här typen av automatisk stränghantering (känd på jargongen som oföränderlighet, eftersom strängar aldrig får muterad, eller modifierade på plats, när de väl har skapats), ger sin egen cybersäkerhetshuvudvärk.
Vi körde Lua-programmet ovan på Windows, upp till den andra pausen, precis innan programmet avslutades:
C:UsersduckKEYPASS> lua s1.lua Hela strängen är: unlikelytextHLKONBOJILAGLNLN Väntar på [ENTER] innan strängen frigörs... Väntar på [ENTER] innan du avslutar...
Den här gången tog vi en processminnesdump, så här:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Sysinternals process dump-verktyg Copyright (C) 2009-2022 Mark Russinovich och Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump initierad: C:UsersduckKEYPASSlua-s1.dmp [1:00:00] Dump 00-skrivning: Uppskattad dumpfilstorlek är 1 MB. [10:00:00] Dump 00 klar: 1 MB skriven på 10 sekunder [0.1:00:00] Antal dumpningar har uppnåtts.
Sedan körde vi det här enkla skriptet, som läser in dumpfilen igen, hittar överallt i minnet att den kända strängen unlikelytext
dök upp och skriver ut den tillsammans med dess plats i dumpfilen och ASCII-tecken som omedelbart följde:
Även om du har använt skriptspråk tidigare, eller arbetat i något programmeringsekosystem som har sk hanterade strängar, där systemet håller reda på minnesallokeringar och avallokeringar åt dig och hanterar dem som det vill...
…du kanske blir förvånad över att se resultatet som denna minnesskanning producerar:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: unlikelytextALJBNGOAPLLBDEB 006D8B3C: unlikelytextALJBNGOA 006D8B7C: unlikelytextALJBNGO 006D8BNGOAPLLBDEB 006D8B006C: unlikelytextALJBNGOA 8D7B006C: unlikelytextALJBNGO 903D006BNGOAPLLBDEB: unlikelytextALJBNGOA 90D006B90C: unlikelytextALJBNGO 006D913BNGOAPLLBDEB textALJBN 006D91D006C: unlikelytextALJBNGOAP 91D006C: unlikelytextALJBNGOAPL 923D006BC: unlikelytextALJBNGOAPLL 70D006FC: osannoliktextALJBNG 8D006C: unlikelytextALJBNGOAPL 0DXNUMXBC: unlikelytextALJBNGOAPLL XNUMXDXNUMXFC: osannoliktextALJBNG XNUMXDXNUMXC: unlikely XNUMXDXNUMXFC: osannoliktextALJBNGOAPLLBD XNUMXDXNUMXC : unlikelytextALJBNGOAPLLBDE XNUMXDBXNUMXC: unlikelytextALJ XNUMXDBBXNUMXC: unlikelytextAL XNUMXDBDXNUMXC: unlikelytextA
Se och se, när vi tog tag i vår minnesdump, även om vi var klara med snöret s
(och sa till Lua att vi inte behövde det längre genom att säga s = nil
), alla strängar som koden hade skapat längs vägen fanns fortfarande kvar i RAM, ännu inte återställda eller raderade.
Faktum är att om vi sorterar ovanstående utdata efter själva strängarna, istället för att följa den ordning som de dök upp i RAM, kommer du att kunna föreställa dig vad som hände under loopen där vi sammanfogade ett tecken i taget till vår lösenordssträng:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sortera /+10 006DBD0C: osannoliktextA 006DBB8C: osannoliktextAL 006DB70C: osannoliktextALJ 006D91BC: osannoliktextALJB 006D8CBC: osannoliktextALJBN 006D90FC: osannolikt 006BNGB8BNGB7N 006B8C: osannoliktextALJBNGOA 3D006D8C: osannoliktextALJBNGOAP 7D006C: osannoliktextALJBNGOAPL 903D006BC: osannoliktextALJBNGOAPLL 90D006C: osannoliktextALJBNGOAPLLB913BNGOAPLL006BNGOAPLL91BNGOAPLL006BNGOAPLL923BNGOAPLL006BNGOAPLL8D006C: osannolik 8C: unlikelytextALJBNGOAPLLBDE XNUMXDXNUMXAFC: unlikelytextALJBNGOAPLLBDEB XNUMXDXNUMXBFC : osannoliktextALJBNGOAPLLBDEBJ
Alla dessa tillfälliga, mellanliggande strängar finns fortfarande kvar, så även om vi framgångsrikt hade utplånat det slutliga värdet av s
, vi skulle fortfarande läcka allt utom dess sista karaktär.
Faktum är att i det här fallet, även när vi medvetet tvingade vårt program att göra sig av med all onödig data genom att anropa den speciella Lua-funktionen collectgarbage()
(de flesta skriptspråk har något liknande), det mesta av data i de där irriterande tillfälliga strängarna fastnade i RAM-minnet ändå, eftersom vi hade kompilerat Lua för att göra sin automatiska minneshantering med gammalt gott malloc()
och free()
.
Med andra ord, även efter att Lua själv återtog sina temporära minnesblock för att använda dem igen, kunde vi inte kontrollera hur eller när dessa minnesblock skulle återanvändas, och därmed hur länge de skulle ligga inne i processen med sina vänster- över data som väntar på att sniffas upp, dumpas eller på annat sätt läckas ut.
Ange .NET
Men hur är det med KeePass, det var där den här artikeln började?
KeePass är skrivet i C# och använder .NET runtime, så det undviker problemen med minnesfelhantering som C-program för med sig...
…men C# hanterar sina egna textsträngar, ungefär som Lua gör, vilket väcker frågan:
Även om programmeraren undvek att lagra hela huvudlösenordet på ett ställe efter att han var klar med det, kunde angripare med tillgång till en minnesdump ändå hitta tillräckligt med överbliven temporär data för att gissa eller återställa huvudlösenordet ändå, även om de angripare fick tillgång till din dator minuter, timmar eller dagar efter att du skrivit in lösenordet?
Enkelt uttryckt, finns det spårbara, spöklika rester av ditt huvudlösenord som finns kvar i RAM-minnet, även efter att du hade förväntat dig att de skulle ha raderats?
Irriterande, som Github-användare Vdohney upptäckte, svaret (för KeePass-versioner tidigare än 2.54, åtminstone) är "Ja."
För att vara tydlig, tror vi inte att ditt faktiska huvudlösenord kan återställas som en enda textsträng från en KeePass-minnesdump, eftersom författaren skapade en speciell funktion för inmatning av huvudlösenord som gör allt för att undvika att lagra hela lösenord där det lätt kunde upptäckas och nosas upp.
Vi nöjde oss med detta genom att ställa in vårt huvudlösenord till SIXTEENPASSCHARS
, skriva in det och sedan ta minnesdumpar direkt, kort och långt efteråt.
Vi sökte i dumparna med ett enkelt Lua-skript som letade överallt efter den lösenordstexten, både i 8-bitars ASCII-format och i 16-bitars UTF-16-format (Windows widechar), så här:
Resultaten var uppmuntrande:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Läser i dumpfil... KLAR. Söker efter SEXTONPASSCHARS som 8-bitars ASCII... hittades inte. Söker efter SIXTEENPASSCHARS som UTF-16... hittades inte.
Men Vdohney, upptäckaren av CVE-2023-32784, märkte att när du skriver in ditt huvudlösenord ger KeePass dig visuell feedback genom att konstruera och visa en platshållarsträng som består av Unicode "blob"-tecken, upp till och inklusive längden på din Lösenord:
I widechar textsträngar på Windows (som består av två byte per tecken, inte bara en byte vardera som i ASCII), kodas "blob"-tecknet i RAM som hexbyte 0xCF
följd av 0x25
(vilket bara råkar vara ett procenttecken i ASCII).
Så även om KeePass är mycket noga med de råa tecknen du skriver in när du anger själva lösenordet, kan du sluta med överblivna strängar av "blob"-tecken, lätt att upptäcka i minnet som upprepade körningar som t.ex. CF25CF25
or CF25CF25CF25
.
… och i så fall skulle den längsta uppsättningen av blob-tecken du hittade förmodligen ge bort längden på ditt lösenord, vilket skulle vara en blygsam form av lösenordsinformationsläckage, om inte annat.
Vi använde följande Lua-skript för att leta efter tecken på överblivna lösenordsplatshållarsträngar:
Resultatet var överraskande (vi har tagit bort rader i rad med samma antal blobbar, eller med färre blobbar än föregående rad, för att spara utrymme):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ fortsätter på samma sätt för 8 blobbar, 9 blobbar, etc. ] [ tills två sista rader med exakt 16 blobbar vardera ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [ alla återstående matcher är en blob långa] 0123B058: *
Vid täta men ständigt ökande minnesadresser hittade vi en systematisk lista med 3 blobbar, sedan 4 blobbar, och så vidare upp till 16 blobbar (längden på vårt lösenord), följt av många slumpmässigt utspridda instanser av enkla blobsträngar .
Så, dessa platshållare "blob"-strängar verkar verkligen läcka in i minnet och stanna kvar för att läcka lösenordslängden, långt efter att KeePass-mjukvaran har slutat med ditt huvudlösenord.
Nästa steg
Vi bestämde oss för att gräva vidare, precis som Vdohney gjorde.
Vi ändrade vår mönstermatchningskod för att upptäcka kedjor av blob-tecken följt av ett enda ASCII-tecken i 16-bitarsformat (ASCII-tecken representeras i UTF-16 som deras vanliga 8-bitars ASCII-kod, följt av en noll byte).
Den här gången, för att spara utrymme, har vi undertryckt utdata för alla matchningar som exakt matchar den föregående:
Överraskning, överraskning:
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
Titta vad vi får ut av .NET:s hanterade strängminnesområde!
En tätt sammansatt uppsättning tillfälliga "blob-strängar" som avslöjar de på varandra följande tecknen i vårt lösenord, med början med det andra tecknet.
Dessa läckande strängar följs av brett spridda enkaraktärsmatchningar som vi antar att de uppstod av en slump. (En KeePass-dumpfil är cirka 250 MB stor, så det finns gott om utrymme för "blob"-tecken att visas som av tur.)
Även om vi tar hänsyn till de fyra extra matchningarna, snarare än att kassera dem som sannolika felmatchningar, kan vi gissa att huvudlösenordet är ett av:
?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Uppenbarligen hittar inte denna enkla teknik det första tecknet i lösenordet, eftersom den första "blob-strängen" bara konstrueras efter att det första tecknet har skrivits in
Observera att den här listan är trevlig och kort eftersom vi filtrerade bort matchningar som inte slutade med ASCII-tecken.
Om du letade efter tecken i ett annat intervall, som kinesiska eller koreanska tecken, kan du få fler oavsiktliga träffar, eftersom det finns många fler möjliga tecken att matcha på...
…men vi misstänker att du kommer ganska nära ditt huvudlösenord ändå, och "blob-strängarna" som hänför sig till lösenordet verkar vara grupperade i RAM-minne, förmodligen för att de tilldelades ungefär samtidigt av samma del av .NET-körtiden.
Och där, i ett visserligen långt och diskursivt nötskal, finns den fascinerande historien om CVE-2023-32784.
Vad göra?
- Om du är en KeePass-användare, få inte panik. Även om detta är en bugg och tekniskt sett är en exploateringsbar sårbarhet, måste fjärrangripare som vill knäcka ditt lösenord med hjälp av denna bugg först implantera skadlig programvara på din dator. Det skulle ge dem många andra sätt att stjäla dina lösenord direkt, även om denna bugg inte fanns, till exempel genom att logga dina tangenttryckningar medan du skriver. Vid det här laget kan du helt enkelt se upp för den kommande uppdateringen och ta tag i den när den är klar.
- Om du inte använder heldiskkryptering kan du överväga att aktivera det. För att extrahera överblivna lösenord från din växlingsfil eller vilolägesfil (operativsystemdiskfiler som används för att spara minnesinnehåll tillfälligt under tung belastning eller när din dator "slar"), skulle angripare behöva direkt åtkomst till din hårddisk. Om du har BitLocker eller motsvarande för andra operativsystem aktiverat, kommer de inte att kunna komma åt din växlingsfil, din vilolägesfil eller andra personliga data som dokument, kalkylblad, sparade e-postmeddelanden och så vidare.
- Om du är en programmerare, håll dig informerad om minneshanteringsproblem. Anta inte att bara för att varje
free()
matchar dess motsvarandemalloc()
att din data är säker och välskött. Ibland kan du behöva vidta extra försiktighetsåtgärder för att undvika att lämna hemliga data liggande, och dessa försiktighetsåtgärder mycket från operativsystem till operativsystem. - Om du är en QA-testare eller en kodgranskare, tänk alltid "bakom kulisserna". Även om minneshanteringskoden ser snygg och välbalanserad ut, var medveten om vad som händer bakom kulisserna (eftersom den ursprungliga programmeraren kanske inte visste att det gjorde det) och gör dig redo att göra lite inträngande arbete som körtidsövervakning och minne dumpning för att verifiera att säkerhetskoden verkligen beter sig som den ska.
KOD FRÅN ARTIKELN: UNL1.C
#omfatta #omfatta #omfatta void hexdump(osignerad char* buff, int len) { // Skriv ut buffert i 16-byte bitar för (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Visa 16 byte som hex-värden för (int j = 0; j < 16; j = j+1) { printf("%02X", buff[i+j]); } // Upprepa dessa 16 byte som tecken för (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) { // Skaffa minne för att lagra lösenord, och visa vad // finns i bufferten när den är officiellt "ny"... char* buff = malloc(128); printf("Dumpar 'ny' buffert vid start"); hexdump(buff,128); // Använd pseudoslumpmässig buffertadress som slumpmässigt frö srand((osignerad)buff); // Starta lösenordet med någon fast, sökbar text strcpy(buff,"unlikelytext"); // Lägg till 16 pseudoslumpmässiga bokstäver, en i taget för (int i = 1; i <= 16; i++) { // Välj en bokstav från A (65+0) till P (65+15) char ch = 65 + (rand() & 15); // Ändra sedan buff-strängen på plats strncat(buff,&ch,1); } // Det fullständiga lösenordet finns nu i minnet, så skriv ut // det som en sträng och visa hela bufferten... printf("Full sträng var: %sn",buff); hexdump(buff,128); // Pausa för att dumpa process-RAM nu (försök: 'procdump -ma') puts("Väntar på att [ENTER] ska frigöra buffert..."); getchar(); // Formellt frigöra() minnet och visa bufferten // igen för att se om något lämnades kvar... free(buff); printf("Dumpning av buffert efter free()n"); hexdump(buff,128); // Pausa för att dumpa RAM igen för att inspektera skillnader puts("Väntar på att [ENTER] ska avsluta main()"); getchar(); returnera 0; }
KOD FRÅN ARTIKELN: UNL2.C
#omfatta #omfatta #omfatta #omfatta void hexdump(osignerad char* buff, int len) { // Skriv ut buffert i 16-byte bitar för (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Visa 16 byte som hex-värden för (int j = 0; j < 16; j = j+1) { printf("%02X", buff[i+j]); } // Upprepa dessa 16 byte som tecken för (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) { // Skaffa minne för att lagra lösenord och visa vad // finns i bufferten när den är officiellt "ny"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Dumpar 'ny' buffert vid start"); hexdump(buff,128); // Använd pseudoslumpmässig buffertadress som slumpmässigt frö srand((osignerad)buff); // Starta lösenordet med någon fast, sökbar text strcpy(buff,"unlikelytext"); // Lägg till 16 pseudoslumpmässiga bokstäver, en i taget för (int i = 1; i <= 16; i++) { // Välj en bokstav från A (65+0) till P (65+15) char ch = 65 + (rand() & 15); // Ändra sedan buff-strängen på plats strncat(buff,&ch,1); } // Det fullständiga lösenordet finns nu i minnet, så skriv ut // det som en sträng och visa hela bufferten... printf("Full sträng var: %sn",buff); hexdump(buff,128); // Pausa för att dumpa process-RAM nu (försök: 'procdump -ma') puts("Väntar på att [ENTER] ska frigöra buffert..."); getchar(); // Frigör formellt() minnet och visa bufferten // igen för att se om något lämnats kvar... VirtualFree(buff,0,MEM_RELEASE); printf("Dumpning av buffert efter free()n"); hexdump(buff,128); // Pausa för att dumpa RAM igen för att inspektera skillnader puts("Väntar på att [ENTER] ska avsluta main()"); getchar(); returnera 0; }
KOD FRÅN ARTIKELN: S1.LUA
-- Börja med någon fast, sökbar text s = 'unlikelytext' -- Lägg till 16 slumpmässiga tecken från 'A' till 'P' för i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Full sträng är:',s,'n') -- Pausa för att dumpa process RAM-utskrift('Väntar på [ENTER] innan sträng frigörs...') io.read() - - Torka strängen och markera variabel unused s = noll -- Dumpa RAM igen för att leta efter diffs print('Waiting for [ENTER] before exiting...') io.read()
KOD FRÅN ARTIKELN: FINDIT.LUA
-- läs i dumpfil lokal f = io.open(arg[1],'rb'):read('*a') -- leta efter markörtext följt av ett -- eller flera slumpmässiga ASCII-tecken lokala b,e ,m = 0,0,noll medan sant gör -- leta efter nästa matchning och kom ihåg offset b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- avsluta när inget mer matchar om inte b, bryt slut -- rapportera position och sträng hittad print(string.format('%08X: %s',b,m)) end
KOD FRÅN ARTIKELN: SEARCHKNOWN.LUA
io.write('Läser i dumpfil... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Söker efter SEXTONPASSCHARS som 8-bitars ASCII... ') local p08 = f:find('SEXTONPASSCHARS') io.write(p08 och 'FOUND' eller 'not found','.n') io.write ('Söker efter SIXTEENPASSCHARS som UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx 00.' '.n')
KOD FRÅN ARTIKELN: FINDBLOBS.LUA
-- läs in dumpfil specificerad på kommandoraden lokal f = io.open(arg[1],'rb'):read('*a') -- Leta efter en eller flera lösenordsblobbar, följt av valfri icke-blobb -- Observera att blob-tecken (●) kodar in i Windows widechars -- som litte-endian UTF-16-koder, som kommer ut som CF 25 i hex. lokal b,e,m = 0,0,noll medan sant gör -- Vi vill ha en eller flera blobbar, följt av valfri icke-blobb. -- Vi förenklar koden genom att leta efter en explicit CF25 -- följt av valfri sträng som bara har CF eller 25 i den, -- så vi hittar CF25CFCF eller CF2525CF såväl som CF25CF25. -- Vi kommer att filtrera bort "falska positiva" senare om det finns några. -- Vi måste skriva '%%' istället för x25 eftersom tecknet x25 -- (procenttecken) är ett speciellt söktecken i Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- avsluta när inga fler matcher om inte b sedan break end -- CMD.EXE kan inte skriva ut blobbar, så vi omvandlar dem till stjärnor. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) end
KOD FRÅN ARTIKELN: SEARCHKP.LUA
-- läs in dumpfil specificerad på kommandoraden lokal f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil medan sant gör -- Nu vill vi ha en eller flera blobbar (CF25) följt av koden -- för A..Z följt av en 0 byte för att konvertera ACSCII till UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- avsluta när inga fler matchningar om inte b sedan break end -- CMD.EXE kan inte skriva ut blobbar, så vi konverterar dem till stjärnor. -- För att spara utrymme undertrycker vi successiva matchningar om m ~= p sedan print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m slutändan
- SEO-drivet innehåll och PR-distribution. Bli förstärkt idag.
- PlatoAiStream. Web3 Data Intelligence. Kunskap förstärkt. Tillgång här.
- Minting the Future med Adryenn Ashley. Tillgång här.
- Köp och sälj aktier i PRE-IPO-företag med PREIPO®. Tillgång här.
- Källa: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- : har
- :är
- :inte
- :var
- ][s
- $UPP
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- Able
- Om oss
- ovan
- Absolut
- AC
- tillgång
- Konto
- förvärva
- förvärvande
- aktiv
- faktiska
- faktiskt
- lagt till
- Annat
- adress
- adresser
- Lägger
- Efter
- efteråt
- igen
- Alla
- allokeras
- allokerar
- fördelning
- tilldelningar
- tillåter
- ensam
- längs
- redan
- också
- förändrad
- Även
- alltid
- an
- och
- Andrew
- svara
- vilken som helst
- något
- något kritiskt
- visas
- syntes
- tillvägagångssätt
- godkänd
- ÄR
- runt
- Artikeln
- artiklar
- AS
- At
- Författaren
- bil
- Automat
- automatiskt
- tillgänglig
- undvika
- undvek
- medveten
- bort
- tillbaka
- bakgrund
- bakgrund-bild
- BE
- därför att
- blir
- varit
- innan
- Börjar
- bakom
- bakom kulisserna
- nedan
- Bättre
- Bit
- Blockera
- Block
- gränsen
- båda
- Botten
- varumärke
- Brand New
- Ha sönder
- i korthet
- föra
- buffert
- buffertöverskridning
- Bug
- fel
- SLUTRESULTAT
- men
- by
- C + +
- Ring
- anropande
- KAN
- Kan få
- vilken
- Vid
- fångas
- CD
- Centrum
- säkerligen
- kedjor
- chans
- ändrats
- karaktär
- tecken
- kontroll
- Kontroller
- kinesisk
- Välja
- klar
- klart
- Stänga
- koda
- färg
- COM
- kommer
- kommande
- kommentar
- kommentarer
- Gemensam
- fullborda
- komplex
- dator
- Tänk
- betydande
- anses
- Bestående
- konstruera
- innehåll
- innehåll
- kontinuerligt
- fortsätter
- kontroll
- konvertera
- upphovsrätt
- Motsvarande
- kunde
- täcka
- spricka
- skapa
- skapas
- skaparen
- kritisk
- Cybersäkerhet
- FARA
- Dangerous
- datum
- dataläcka
- Dagar
- behandla
- beslutade
- dedicerad
- beskriven
- DID
- skillnader
- olika
- Svårighet
- GRÄV
- digital
- rikta
- Direkt tillgång
- direkt
- Visa
- visning
- har
- do
- dokument
- gör
- inte
- gör
- gjort
- inte
- ner
- driv
- grund
- dumpa
- under
- e
- varje
- Tidigare
- lätt
- ekosystemet
- antingen
- annars
- e
- möjliggör
- uppmuntra
- kryptering
- änden
- slutar
- tillräckligt
- säkerställa
- säkerställa
- ange
- in
- Hela
- inträde
- Miljö
- Motsvarande
- fel
- väsentligen
- beräknad
- etc
- Eter (ETH)
- Även
- så småningom
- ständigt ökande
- Varje
- allt
- exakt
- exempel
- Utom
- Spänning
- utförande
- existerar
- befintliga
- Utgång
- avsluta
- förvänta
- Förklara
- Exploit
- utsatta
- förlänga
- extra
- extrahera
- Faktum
- falsk
- fascinerande
- Funktioner
- återkoppling
- färre
- bekämpa
- Fil
- Filer
- filtrera
- slutlig
- Slutligen
- hitta
- finna
- fynd
- änden
- Förnamn
- fixerad
- Fokus
- följt
- efter
- För
- formen
- Formellt
- format
- kommande
- hittade
- fyra
- Fri
- från
- full
- fullständigt
- fungera
- funktioner
- ytterligare
- framtida
- genererar
- generering
- skaffa sig
- få
- GitHub
- Ge
- ges
- ger
- Ge
- Go
- Går
- kommer
- god
- Regeringen
- ta
- stor
- garanti
- hade
- Handtag
- hänt
- Happening
- händer
- Hård
- Har
- har
- huvudvärk
- tung
- höjd
- här.
- HEX
- högnivå
- högre
- träffar
- hålla
- Hål
- hoppas
- ÖPPETTIDER
- hovring
- Hur ser din drömresa ut
- How To
- HTTPS
- jakt
- i
- identifierare
- if
- blir omedelbart
- med Esport
- in
- innefattar
- Inklusive
- informationen
- informeras
- istället
- intresserad
- Mellanliggande
- Internet
- in
- problem
- IT
- DESS
- sig
- jargong
- juni
- bara
- bara en
- Ha kvar
- Nyckel
- Vet
- känd
- koreanska
- språk
- Språk
- laptop
- Efternamn
- senare
- leda
- Leads
- läckage
- Läckor
- LÄRA SIG
- inlärning
- t minst
- lämnar
- vänster
- Längd
- brev
- Bibliotek
- livet
- tycka om
- sannolikt
- Begränsad
- linje
- rader
- Lista
- Noterade
- ll
- läsa in
- lokal
- läge
- skogsavverkning
- Lång
- lång sikt
- längre
- se
- ser ut som
- såg
- du letar
- UTSEENDE
- Lot
- tur
- upprätthåller
- göra
- malware
- hantera
- förvaltade
- ledning
- chef
- förvaltar
- Manipulation
- många
- Marginal
- markera
- markör
- Master
- Match
- matchande
- max-bredd
- Maj..
- betyder
- Minne
- nämnts
- Microsoft
- kanske
- minuter
- blygsam
- modifierad
- modifiera
- ögonblick
- övervakning
- mer
- mest
- mycket
- multipel
- Propert
- Behöver
- behövs
- netto
- aldrig
- Icke desto mindre
- Nya
- nyheter
- Nästa
- trevligt
- Nej
- normala
- inget
- Lägga märke till..
- nu
- antal
- nummer
- objektet
- Uppenbara
- of
- sänkt
- tjänsteman
- Officiellt
- offset
- Gamla
- on
- gång
- ONE
- endast
- öppen källkod
- drift
- operativsystem
- operativsystem
- Operatören
- Alternativet
- or
- beställa
- ursprungliga
- Övriga
- Övrigt
- annat
- vår
- själva
- ut
- produktion
- över
- övergripande
- egen
- sida
- Panic
- del
- Lösenord
- Password Manager
- lösenord
- bana
- Mönster
- paul
- paus
- Betala
- procent
- kanske
- perioden
- permanent
- personlig
- personlig information
- fysisk
- Bild
- bitar
- Plats
- platshållare
- Plague
- plato
- Platon Data Intelligence
- PlatonData
- Massor
- Punkt
- poäng
- Populära
- placera
- möjlig
- inlägg
- potentiell
- exakt
- presentera
- pretty
- förhindra
- föregående
- pris
- Skriva ut
- utskrifter
- förmodligen
- problem
- process
- Program
- Programmerare
- programmerare
- Programmering
- Program
- uttalad
- sätta
- Python
- Frågor och svar
- fråga
- höjer
- RAM
- slumpmässig
- område
- snarare
- Raw
- rådata
- RE
- kommit fram till
- Läsa
- Läsning
- redo
- verklig
- verkliga livet
- realtid
- verkligen
- erkänna
- Recover
- återhämta
- relaterad
- Återstående
- ihåg
- avlägsen
- ta bort
- upprepa
- upprepade
- UPPREPAT
- rapport
- representerade
- avseende
- respektive
- REST
- Resultat
- avkastning
- tillbaka
- avslöjar
- Befria
- höger
- Risk
- risker
- Rum
- Körning
- rinnande
- körtidsövervakning
- s
- säker
- säkrare
- Samma
- nöjd
- Save
- säger
- scanna
- spridda
- scener
- Sök
- söka
- Andra
- sekunder
- Secret
- §
- säkra
- säkerhet
- se
- frö
- se
- verka
- sett
- ser
- Serier
- allvarlig
- in
- inställning
- Kort
- Inom kort
- skall
- show
- visas
- signera
- Tecken
- liknande
- Liknande
- Enkelt
- förenklade
- förenkla
- helt enkelt
- enda
- Storlek
- sova
- Small
- Lömsk
- snokande
- So
- Mjukvara
- fast
- några
- något
- Alldeles strax
- Källa
- källkod
- Utrymme
- speciell
- speciellt
- specificerade
- fart
- Stjärnor
- starta
- igång
- Starta
- startar
- start
- Fortfarande
- stola
- Sluta
- slutade
- lagra
- lagras
- Historia
- Sträng
- stark
- Läsa på
- Framgångsrikt
- sådana
- tillräcklig
- förment
- överraskning
- överraskad
- förvånande
- överleva
- SVG
- byta
- system
- System
- Ta
- tagen
- tar
- tar
- tala
- tekniskt
- tekniker
- temporär
- testa
- tester
- än
- den där
- Smakämnen
- källan
- deras
- Dem
- sig själva
- sedan
- Teorin
- Där.
- därför
- de
- sak
- tror
- detta
- de
- fastän?
- trodde
- tid
- Titel
- till
- tillsammans
- tog
- verktyg
- topp
- spår
- Spårning
- övergång
- transparent
- sann
- prova
- vände
- två
- Typ
- typiskt
- förstå
- unicode
- tills
- oanvänd
- oönskade
- Uppdatering
- uppdaterad
- URL
- us
- USAs regering
- Användning
- usb
- användning
- använd-efter-fri
- Begagnade
- Användare
- användningar
- med hjälp av
- verktyg
- värde
- Värden
- mängd
- verifiera
- version
- mycket
- via
- sårbarhet
- W
- vänta
- väntar
- vill
- ville
- var
- Kolla på
- Sätt..
- sätt
- we
- veckor
- VÄL
- były
- Vad
- när
- om
- som
- medan
- VEM
- den som
- Hela
- varför
- kommer
- vinna
- fönster
- torka
- med
- utan
- undrar
- ord
- Arbete
- arbetade
- arbetssätt
- fungerar
- oro
- skulle
- skulle ge
- skriva
- skrivning
- skriven
- ännu
- dig
- Din
- själv
- zephyrnet
- noll-