I løbet af de sidste to uger har vi set en række artikler, der fortæller om, hvad der er blevet beskrevet som en "master password crack" i den populære open source password manager KeePass.
Fejlen blev anset for at være vigtig nok til at få en officiel identifikation af den amerikanske regering (den er kendt som CVE-2023-32784, hvis du vil jagte det), og i betragtning af at hovedadgangskoden til din adgangskodemanager stort set er nøglen til hele dit digitale slot, kan du forstå, hvorfor historien fremkaldte masser af spænding.
Den gode nyhed er, at en angriber, der ønskede at udnytte denne fejl, næsten helt sikkert skulle have inficeret din computer med malware allerede, og ville derfor være i stand til at spionere på dine tastetryk og kørende programmer alligevel.
Med andre ord kan fejlen betragtes som en let-administreret risiko, indtil skaberen af KeePass kommer ud med en opdatering, som skulle vises snart (tilsyneladende i begyndelsen af juni 2023).
Som afslører af fejlen sørger for påpege:
Hvis du bruger fuld diskkryptering med en stærk adgangskode, og dit system er [fri for malware], burde du have det fint. Ingen kan stjæle dine adgangskoder via internettet alene med dette fund.
Risiciene forklaret
Tungt opsummeret kan fejlen koges ned til vanskeligheden ved at sikre, at alle spor af fortrolige data bliver slettet fra hukommelsen, når du er færdig med dem.
Vi vil her ignorere problemerne med, hvordan man overhovedet undgår at have hemmelige data i hukommelsen, selv kortvarigt.
I denne artikel vil vi blot minde programmører overalt om, at kode godkendt af en sikkerhedsbevidst anmelder med en kommentar såsom "synes at rydde op korrekt efter sig selv"...
… måske faktisk slet ikke rydde helt op, og det potentielle datalæk er måske ikke indlysende fra en direkte undersøgelse af selve koden.
Kort sagt betyder CVE-2023-32784-sårbarheden, at en KeePass-hovedadgangskode muligvis kan gendannes fra systemdata, selv efter KeyPass-programmet er afsluttet, fordi tilstrækkelig information om din adgangskode (omend ikke faktisk selve den rå adgangskode, som vi vil fokusere på) tændt om et øjeblik) kan blive efterladt i systembytte- eller dvalefiler, hvor allokeret systemhukommelse kan ende med at blive gemt til senere.
På en Windows-computer, hvor BitLocker ikke bruges til at kryptere harddisken, når systemet er slukket, ville dette give en skurk, der stjal din bærbare computer, en kæmpe chance for at starte op fra et USB- eller cd-drev, og selv gendanne dit hovedadgangskode. selvom KeyPass-programmet selv sørger for aldrig at gemme det permanent på disken.
En langsigtet adgangskodelæk i hukommelsen betyder også, at adgangskoden i teorien kunne gendannes fra et hukommelsesdump af KeyPass-programmet, selvom det dump blev grebet længe efter, du havde indtastet adgangskoden, og længe efter KeePass. selv havde ikke længere behov for at beholde det.
Det er klart, at du bør antage, at malware, der allerede er på dit system, kunne gendanne næsten enhver indtastet adgangskode via en række realtids snooping-teknikker, så længe de var aktive på det tidspunkt, du skrev. Men du kan med rimelighed forvente, at din tid udsat for fare ville være begrænset til den korte periode med at skrive, ikke forlænget til mange minutter, timer eller dage bagefter, eller måske længere, også efter du har lukket din computer ned.
Hvad bliver efterladt?
Vi tænkte derfor, at vi ville tage et kig på højt niveau på, hvordan hemmelige data kan blive efterladt i hukommelsen på måder, der ikke er direkte indlysende fra koden.
Bare rolig, hvis du ikke er programmør – vi holder det enkelt og forklarer, mens vi går.
Vi starter med at se på hukommelsesbrug og oprydning i et simpelt C-program, der simulerer indtastning og midlertidig lagring af en adgangskode ved at gøre følgende:
- Tildeling af en dedikeret del af hukommelsen specielt til at gemme adgangskoden.
- Indsættelse af en kendt tekststreng så vi nemt kan finde det i hukommelsen, hvis det er nødvendigt.
- Tilføjelse af 16 pseudo-tilfældige 8-bit ASCII-tegn fra området AP.
- Udskriver den simulerede adgangskodebuffer.
- Frigør hukommelsen i håbet om at fjerne adgangskodebufferen.
- Afslutning programmet.
Meget forenklet kan C-koden se nogenlunde sådan ud uden fejlkontrol ved at bruge pseudo-tilfældige tal af dårlig kvalitet fra C runtime-funktionen rand()
, og ignorer eventuelle bufferoverløbstjek (gør aldrig noget af dette i ægte kode!):
// 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);
Faktisk indeholder den kode, vi endelig brugte i vores test, nogle ekstra bits og stykker vist nedenfor, så vi kunne dumpe hele indholdet af vores midlertidige adgangskodebuffer, som vi brugte den, for at lede efter uønsket eller efterladt indhold.
Bemærk, at vi bevidst dumper bufferen efter opkald free()
, som teknisk set er en brug-efter-fri fejl, men vi gør det her som en snigende måde at se, om noget kritisk bliver efterladt efter at have afleveret vores buffer tilbage, hvilket kan føre til et farligt datalækagehul i det virkelige liv.
Vi har også indsat to Waiting for [Enter]
prompter ind i koden for at give os selv en chance for at oprette hukommelsesdumps på nøglepunkter i programmet, hvilket giver os rådata til at søge senere for at se, hvad der blev efterladt, mens programmet kørte.
For at lave hukommelsesdumps bruger vi Microsoft Sysinternals værktøj procdump
med -ma
mulighed (dump al hukommelse), hvilket undgår behovet for at skrive vores egen kode for at bruge Windows DbgHelp
systemet og det temmelig komplekse MiniDumpXxxx()
funktioner.
For at kompilere C-koden brugte vi vores egen lille og enkle opbygning af Fabrice Bellards gratis og open source Tiny C Compiler, tilgængelig til 64-bit Windows i kilde og binær form direkte fra vores GitHub-side.
Kopi-og-pasbar tekst af al kildekoden afbildet i artiklen vises nederst på siden.
Dette er, hvad der skete, da vi kompilerede og kørte testprogrammet:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Copyright (C) 2001-2023 Fabrice Bellard strippet ned af Paul Ducklin til brug som et læringsværktøj Version petcc64-0.9.27 [0006] - Genererer 64-bit Kun PE'er -> 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 filstørrelse sektion 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata ---------- ----------------------- <- unl1.exe (3584 bytes) C:UsersduckKEYPASS> unl1.exe Dumper 'ny' buffer ved 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 D 5 53 79 73 74 C 65 6 dowsSystem33Dr 32F5D44: 72 32 00 513 0 69C 76 65 72 73 5 44 72 69 76 65 iversDriverData 72F44E61: 74 61 00 513 0F 00 45 46 43 5D 34 33 37 32. 3F31: 00 46 50F 53 5 4372 1 00F 513 0 42 52F 4 57 53F 45 BROWSER_APP_PROF 52F5: 41 50C 50 5F 50 52 4 46 00E 51400 49D 4 45E 5 53 54 ILE_STRING=Inter 52F49 4 47C 3A 49 F6 74C AC 65B 72 00 netto ExplzV.< .K.. Fuld streng var: usandsynligtekstJHKNEJJCPOMDJHAN 51410F6: 65 74E 20C 45 78B 70 6C 7 56 4 3 4 00A 00 00B 51390E usandsynligtekstJHKN 75F6 6A 69A 6 65A 6 79A 74A 65 78 74A 4 48 4E 4 00 513 0 EJJCPOMDJHAN.eD 45F4B4 d 43Dr 50F4D4: 44 4 48 41 4 00C 65 00 44 00 513 0 72 69 76 65 iversDriverData 72F44E61: 74 61 3 43 3F 5 57 69 6 00D 513 0 64 6 77 73F .EFC5_53. 79F 73 74 65 6 33F 32 5 44 72F 32 00 513F 0 BROWSER_APP_PROF 69F76: 65 72C 73 5F 44 72 69 76 65E 72 44D 61 74E 61 00 513 ILE_STRING=Inter 0F00: 45E 46 43 5A 34C C AC 33B 37 32 netto ExplzV.<.K.. Venter på, at [ENTER] frigør buffer... Dumper buffer efter free() 3F31: A00 46 F50 53 5 4372 1 00 513 0 F42 52 4 57 53 45 .g......P...... 52F5A41: 50 50A 5A 50 52 4F 46D 00 51400A 49 4 45E 5 53 54 52 EJJCPOMDJHAN.eD 49F4B47: 3 49 6 74 65 72 00 51410 6 65 74 20E riverData=C:Win 45F78C70: 6 7F 56 4 3C 4 00 00 00 51390 0D 67 5 00C 00 00 dowsSystem00Dr 00F50D01: 5 00 00 00 00 00C 00 513 0 45 4 4 43 50 Driver:E Dato 4 4 44 4 48F 41 4 00 65 00D 44 00 513 0 72 69F .EFC_76=65.FPS_ 72F44F61: 74 61 3F 43 3 5 57 69F 6 00 513 0F 64 6 77F 73 BROWSER_APP_PROF 5F53: 79 E 73 74 65 6D 33 32E 5 44 72 ILE_STRING=Inter 32F00: 513E 0 69 76 65 72 73 5C 44D 72 69 76D AC 65B 72 44 netto ExplM..MK. Venter på, at [ENTER] afslutter main()... C:UsersduckKEYPASS>
I denne kørsel gad vi ikke få fat i nogen proceshukommelsesdumps, fordi vi kunne se med det samme fra outputtet, at denne kode lækker data.
Lige efter at have kaldt Windows C runtime-biblioteksfunktionen malloc()
, kan vi se, at den buffer, vi får tilbage, inkluderer, hvad der ligner miljøvariable data, der er tilbage fra programmets startkode, med de første 16 bytes tilsyneladende ændret til at ligne en slags tilbageværende hukommelsesallokeringsheader.
(Bemærk, hvordan de 16 bytes ligner to 8-byte hukommelsesadresser, 0xF55790
, 0xF50150
, som er henholdsvis lige efter og lige før vores egen hukommelsesbuffer.)
Når kodeordet formodes at være i hukommelsen, kan vi se hele strengen tydeligt i bufferen, som vi ville forvente.
Men efter at have ringet free()
, bemærk, hvordan de første 16 bytes af vores buffer er blevet omskrevet med noget, der ligner nærliggende hukommelsesadresser igen, formentlig så hukommelsesallokatoren kan holde styr på blokke i hukommelsen, som den kan genbruge...
… men resten af vores "fjernede" adgangskodetekst (de sidste 12 tilfældige tegn EJJCPOMDJHAN
) er blevet efterladt.
Ikke alene skal vi styre vores egne hukommelsesallokeringer og deallokeringer i C, vi skal også sikre, at vi vælger de rigtige systemfunktioner til databuffere, hvis vi vil kontrollere dem præcist.
For eksempel, ved at skifte til denne kode i stedet, får vi lidt mere kontrol over, hvad der er i hukommelsen:
Ved at skifte fra malloc()
, free()
for at bruge Windows-tildelingsfunktionerne på lavere niveau VirtualAlloc()
, VirtualFree()
direkte, får vi bedre kontrol.
Vi betaler dog en pris i hastighed, fordi hvert opkald til VirtualAlloc()
udfører mere arbejde end en opfordring til malloc()
, som virker ved løbende at opdele og underopdele en blok af præ-allokeret lav-niveau hukommelse.
Ved brug af VirtualAlloc()
gentagne gange for små blokke bruger også mere hukommelse generelt, fordi hver blok disket ud af VirtualAlloc()
bruger typisk et multiplum af 4KB hukommelse (eller 2MB, hvis du bruger såkaldte store hukommelsessider), så vores 128-byte buffer ovenfor rundes op til 4096 bytes, hvilket spilder de 3968 bytes i slutningen af 4KB hukommelsesblokken.
Men som du kan se, bliver den hukommelse, vi får tilbage, automatisk lukket ud (sat til nul), så vi kan ikke se, hvad der var der før, og denne gang går programmet ned, når vi forsøger at gøre vores brug-efter-fri trick, fordi Windows registrerer, at vi forsøger at kigge på hukommelse, vi ikke længere ejer:
C:UsersduckKEYPASS> unl2 Dumper 'ny' buffer ved start 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0010 00 00 00 EA 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ 0020EA00: 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 ................ 00EA00: 00 00 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 0000000000 0060 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 0000000000 0070 00 00 00 00 00 00 00 00 00 00 00 ................ Fuld streng var: unlikelytextIBIPJPPHEOPOIDLL 00EA00: 00 00E 00C 0000000000 0080B 00 00C 00 00 00 00 00 00 00 00 00 usandsynligt 00IP 00IP 00IP 00 00 0000000000 0000 75F 6 6F 69 6 65C 6C 79 74 65 78 JPPHEOPOIDLL.... 74EA49: 42 49 50 0000000000 0010 4 50 50 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 ................ 00EA00: 00 0000000000 0030 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0040 00 00 00 00 00 00 00 00 00 00 00 00 ............... 00EA00: 00 00 0000000000 0050 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00EA00: 00 00 0000000000 0060 00 00 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 0000000000 0070 00 00 00 00 ............. ... Venter på, at [ENTER] skal frigøre buffer... Dumper buffer efter free() 00EA00: [Program afsluttet her, fordi Windows fangede vores use-efter-free]
Fordi den hukommelse, vi frigjorde, skal omfordeles med VirtualAlloc()
før det kan bruges igen, kan vi antage, at det vil blive nulstillet, før det genbruges.
Men hvis vi ville sikre os, at det var lukket ud, kunne vi kalde den særlige Windows-funktion RtlSecureZeroMemory()
lige før den frigives, for at garantere, at Windows skriver nuller i vores buffer først.
Den relaterede funktion RtlZeroMemory()
, hvis du undrede dig, gør en lignende ting, men uden garanti for rent faktisk at fungere, fordi compilere har lov til at fjerne det som teoretisk overflødigt, hvis de bemærker, at bufferen ikke bruges igen bagefter.
Som du kan se, skal vi være meget omhyggelige med at bruge de rigtige Windows-funktioner, hvis vi vil minimere den tid, hemmeligheder gemt i hukommelsen kan ligge rundt om til senere.
I denne artikel skal vi ikke se på, hvordan du forhindrer, at hemmeligheder gemmes ved et uheld til din swap-fil ved at låse dem i fysisk RAM. (Antydning: VirtualLock()
er faktisk ikke nok i sig selv.) Hvis du gerne vil vide mere om Windows-hukommelsessikkerhed på lavt niveau, så lad os det vide i kommentarerne, og vi vil se på det i en fremtidig artikel.
Brug af automatisk hukommelsesstyring
En smart måde at undgå selv at skulle allokere, administrere og deallokere hukommelse er at bruge et programmeringssprog, der tager sig af malloc()
, free()
eller VirtualAlloc()
, VirtualFree()
, automatisk.
Manuskriptsprog som f.eks Perl, Python, Lua, JavaScript og andre slipper af med de mest almindelige hukommelsessikkerhedsfejl, der plager C- og C++-koden, ved at spore hukommelsesbrug for dig i baggrunden.
Som vi nævnte tidligere, fungerer vores dårligt skrevne eksempel C-kode ovenfor fint nu, men kun fordi det stadig er et supersimpelt program med datastrukturer i fast størrelse, hvor vi ved inspektion kan bekræfte, at vi ikke vil overskrive vores 128- byte buffer, og at der kun er én eksekveringssti, der starter med malloc()
og slutter med et tilsvarende free()
.
Men hvis vi opdaterede den for at tillade generering af adgangskoder med variabel længde eller tilføjede yderligere funktioner til genereringsprocessen, så kunne vi (eller hvem der nu vedligeholder koden) nemt ende med bufferoverløb, brug efter-fri fejl eller hukommelse, der bliver aldrig frigivet og lader derfor hemmelige data hænge længe efter, at det ikke længere er nødvendigt.
På et sprog som Lua kan vi lade Lua køre-time-miljøet, som gør det, der er kendt i jargonen som automatisk affaldsindsamling, beskæftige sig med at hente hukommelse fra systemet og returnere den, når den registrerer, at vi er holdt op med at bruge den.
C-programmet, vi nævnte ovenfor, bliver meget enklere, når hukommelsesallokering og -deallokering er taget hånd om for os:
Vi tildeler hukommelse til at holde strengen s
blot ved at tildele strengen 'unlikelytext'
til det.
Vi kan senere enten hentyde til Lua eksplicit, at vi ikke længere er interesserede i s
ved at tildele den værdien nil
(alle nils
er i det væsentlige det samme Lua-objekt), eller stop med at bruge s
og vent på, at Lua opdager, at det ikke længere er nødvendigt.
Uanset hvad, hukommelsen brugt af s
vil i sidste ende blive gendannet automatisk.
Og for at forhindre bufferoverløb eller størrelsesfejl, når du tilføjer tekststrenge (Lua-operatøren ..
, udtalt konkat, i det væsentlige tilføjer to strenge sammen, f.eks +
i Python), hver gang vi forlænger eller forkorter en streng, tildeler Lua på magisk vis plads til en helt ny streng, i stedet for at ændre eller erstatte den originale på dens eksisterende hukommelsesplacering.
Denne tilgang er langsommere og fører til hukommelsesforbrugstoppe, der er højere, end du ville få i C på grund af de mellemliggende strenge, der tildeles under tekstmanipulation, men det er meget sikrere med hensyn til bufferoverløb.
Men denne form for automatisk strengstyring (kendt i jargonen som uforanderlighed, fordi strenge aldrig får muteret, eller modificeret på plads, når de først er blevet oprettet), bringer nye cybersikkerhedshoveder i sig selv.
Vi kørte Lua-programmet ovenfor på Windows, op til den anden pause, lige før programmet afsluttede:
C:UsersduckKEYPASS> lua s1.lua Fuld streng er: unlikelytextHLKONBOJILAGLNLN Venter på [ENTER] før du frigør streng... Venter på [ENTER] før du afslutter...
Denne gang tog vi et proceshukommelsesdump, som dette:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Sysinternals proces dump hjælpeprogram Copyright (C) 2009-2022 Mark Russinovich og Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump initieret: C:UsersduckKEYPASSlua-s1.dmp [1:00:00] Dump 00-skrivning: Estimeret dumpfilstørrelse er 1 MB. [10:00:00] Dump 00 fuldført: 1 MB skrevet på 10 sekunder [0.1:00:00] Dump-antal nået.
Så kørte vi dette simple script, som læser dump-filen ind igen, finder overalt i hukommelsen, at den kendte streng unlikelytext
dukkede op, og udskriver den sammen med dens placering i dumpfilen og ASCII-tegnene, der umiddelbart fulgte:
Selv hvis du har brugt scriptsprog før, eller arbejdet i et hvilket som helst programmeringsøkosystem, der har såkaldte klarede strenge, hvor systemet holder styr på hukommelsestildelinger og -deallokeringer for dig og håndterer dem, som det passer...
… du kan blive overrasket over at se det output, som denne hukommelsesscanning producerer:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: unlikelytextALJBNGOAPLLBDEB 006D8B3C: unlikelytextALJBNGOA 006D8B7C: unlikelytextALJBNGO 006D8BNGO 006D8BNGOAPLLBDEB: unlikelytextALJBNGOA 006D8B7C: unlikelytextALJBNGO 006D903BNGOAPLLBDEB 006D90BNGOAPLLBDEB textALJBN 006D90D006C: usandsynligtekstALJBNGOAP 913D006C: usandsynligtekstALJBNGOAPL 91D006BC: usandsynligtekstALJBNGOAPLL 91D006FC: usandsynligtekstALJBNG 923D006C: usandsynligt70BNGOAPLJBBC006 8D006FC: usandsynlig tekstALJBNGOAPLLBD 0DXNUMXC : usandsynligtekstALJBNGOAPLLBDE XNUMXDBXNUMXC: usandsynligtekstALJ XNUMXDBBXNUMXC: usandsynligtekstAL XNUMXDBDXNUMXC: usandsynligtekstA
Se, på det tidspunkt greb vi vores hukommelsesdump, selvom vi var færdige med snoren s
(og fortalte Lua, at vi ikke havde brug for det mere ved at sige s = nil
), alle de strenge, som koden havde oprettet undervejs, var stadig til stede i RAM, endnu ikke gendannet eller slettet.
Faktisk, hvis vi sorterer ovenstående output efter strengene selv, i stedet for at følge den rækkefølge, som de optrådte i RAM, vil du være i stand til at forestille dig, hvad der skete under loopet, hvor vi sammenkædede et tegn ad gangen til vores adgangskodestreng:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: usandsynligttekstA 006DBB8C: usandsynligttekstAL 006DB70C: usandsynligttekstALJ 006D91BC: usandsynligttekstALJB 006D8CBC: usandsynligttekstALJBN 006D90FC: usandsynligt 006BD8BC7D006BC: usandsynligt tekstALJBN 8D3FC: usandsynligt 006BNGB8N 7B006C: usandsynligtekstALJBNGOA 903D006D90C: usandsynligtekstALJBNGOAP 006D913C: usandsynligtekstALJBNGOAPL 006D91BC: usandsynligtekstALJBNGOAPLL 006D923C: usandsynligtekstALJBNGOAPLLB006BNGOAPLL8BNGOAPLL006BNGOAPLL8BNGOAPLLXNUMXBNGOAPLLXNUMXBNDAL XNUMXC: usandsynlig tekstALJBNGOAPLLBDE XNUMXDXNUMXAFC: usandsynlig tekstALJBNGOAPLLBDEB XNUMXDXNUMXBFC : usandsynlig tekstALJBNGOAPLLBDEBJ
Alle de midlertidige, mellemliggende strenge er der stadig, så selvom vi med succes havde udslettet den endelige værdi af s
, ville vi stadig lække alt undtagen dens sidste karakter.
Faktisk, i dette tilfælde, selv når vi bevidst tvang vores program til at bortskaffe alle unødvendige data ved at kalde den specielle Lua-funktion collectgarbage()
(de fleste scriptsprog har noget lignende), de fleste af dataene i de irriterende midlertidige strenge sad alligevel fast i RAM, fordi vi havde kompileret Lua til at udføre dens automatiske hukommelseshåndtering ved hjælp af gode gamle malloc()
, free()
.
Med andre ord, selv efter at Lua selv havde genvundet sine midlertidige hukommelsesblokke for at bruge dem igen, kunne vi ikke kontrollere, hvordan eller hvornår disse hukommelsesblokke ville blive genbrugt, og dermed hvor længe de ville ligge rundt inde i processen med deres venstre- over data, der venter på at blive opsnuset, dumpet eller på anden måde lækket.
Indtast .NET
Men hvad med KeePass, hvor denne artikel startede?
KeePass er skrevet i C# og bruger .NET runtime, så det undgår problemerne med hukommelsesfejlstyring, som C-programmer bringer med sig...
…men C# administrerer sine egne tekststrenge, ligesom Lua gør, hvilket rejser spørgsmålet:
Selvom programmøren undgik at gemme hele hovedadgangskoden ét sted, efter at han var færdig med den, kunne angribere med adgang til et hukommelsesdump alligevel finde nok tilbageværende midlertidige data til at gætte på eller gendanne masteradgangskoden alligevel, selvom de angribere fik adgang til din computer minutter, timer eller dage efter du havde indtastet adgangskoden ?
Kort sagt, er der sporbare, spøgelsesagtige rester af dit hovedkodeord, der overlever i RAM, selv efter du havde forventet, at de var blevet slettet?
Irriterende, som Github-bruger Vdohney opdagede, svaret (i det mindste for KeePass-versioner tidligere end 2.54) er "Ja."
For at være klar, tror vi ikke, at din egentlige hovedadgangskode kan gendannes som en enkelt tekststreng fra et KeePass-hukommelsesdump, fordi forfatteren har oprettet en speciel funktion til indtastning af hovedadgangskode, der går ud af sin måde for at undgå at gemme den fulde adgangskode, hvor det nemt kunne spottes og opsnuses.
Vi tilfredsstillede os selv med dette ved at indstille vores hovedadgangskode til SIXTEENPASSCHARS
, indtaste det og derefter tage hukommelsesdumps med det samme, kort tid og længe efter.
Vi søgte på dumps med et simpelt Lua-script, der kiggede overalt efter den adgangskodetekst, både i 8-bit ASCII-format og i 16-bit UTF-16 (Windows widechar) format, som dette:
Resultaterne var opmuntrende:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Læser i dump-fil... UDFØRT. Søger efter SIXTEENPASSCHARS som 8-bit ASCII... ikke fundet. Søger efter SIXTEENPASSCHARS som UTF-16... ikke fundet.
Men Vdohney, opdageren af CVE-2023-32784, bemærkede, at mens du indtaster dit hovedadgangskode, giver KeePass dig visuel feedback ved at konstruere og vise en pladsholderstreng bestående af Unicode "blob"-tegn, op til og inklusive længden af din adgangskode:
I widechar tekststrenge på Windows (som består af to bytes pr. tegn, ikke kun én byte hver som i ASCII), er "blob"-tegnet kodet i RAM som hexbyte 0xCF
efterfulgt af 0x25
(som tilfældigvis er et procenttegn i ASCII).
Så selvom KeePass er meget omhyggelig med de rå tegn, du indtaster, når du indtaster selve adgangskoden, kan du ende op med rester af "blob"-tegn, der let kan registreres i hukommelsen som gentagne kørsler som f.eks. CF25CF25
or CF25CF25CF25
...
…og hvis det er tilfældet, ville den længste serie af blob-tegn, du fandt, sandsynligvis give længden af dit kodeord væk, hvilket ville være en beskeden form for adgangskodeinformationslækage, om ikke andet.
Vi brugte følgende Lua-script til at lede efter tegn på resterende adgangskodepladsholderstrenge:
Outputtet var overraskende (vi har slettet på hinanden følgende linjer med det samme antal klatter eller med færre klatter end den forrige linje for at spare plads):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ fortsætter på samme måde for 8 klatter, 9 klatter osv. ] [ indtil to sidste linjer med nøjagtig 16 klatter hver ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [alle resterende kampe er en klat lange] 0123B058: *
Ved tætte, men stadigt stigende hukommelsesadresser fandt vi en systematisk liste med 3 blobs, derefter 4 blobs, og så videre op til 16 blobs (længden af vores adgangskode), efterfulgt af mange tilfældigt spredte forekomster af enkelt-blob strenge .
Så disse pladsholder "blob"-strenge ser faktisk ud til at lække ind i hukommelsen og blive tilbage for at lække adgangskodens længde, længe efter at KeePass-softwaren er færdig med din hovedadgangskode.
Det næste trin
Vi besluttede at grave videre, ligesom Vdohney gjorde.
Vi ændrede vores mønstertilpasningskode for at detektere kæder af blob-tegn efterfulgt af ethvert enkelt ASCII-tegn i 16-bit-format (ASCII-tegn er repræsenteret i UTF-16 som deres sædvanlige 8-bit ASCII-kode, efterfulgt af en nul byte).
Denne gang, for at spare plads, har vi undertrykt output for enhver match, der matcher nøjagtigt den forrige:
Overraskelse, overraskelse:
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
Se, hvad vi får ud af .NET's administrerede strenghukommelsesregion!
Et tæt samlet sæt af midlertidige "blob-strenge", der afslører de på hinanden følgende tegn i vores adgangskode, startende med det andet tegn.
Disse utætte strenge efterfølges af udbredte enkelttegns-matches, som vi antager, er opstået tilfældigt. (En KeePass-dumpfil er omkring 250 MB i størrelse, så der er masser af plads til, at "blob"-tegn vises som ved et held.)
Selvom vi tager disse ekstra fire match i betragtning, i stedet for at kassere dem som sandsynlige uoverensstemmelser, kan vi gætte på, at hovedadgangskoden er en af:
?IXTENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Det er klart, at denne simple teknik ikke finder det første tegn i adgangskoden, fordi den første "blob-streng" kun er konstrueret efter det første tegn er blevet indtastet
Bemærk, at denne liste er pæn og kort, fordi vi frafiltrerede kampe, der ikke endte med ASCII-tegn.
Hvis du ledte efter tegn i et andet område, såsom kinesiske eller koreanske tegn, kan du ende med flere tilfældige hits, fordi der er mange flere mulige tegn at matche på...
…men vi formoder, at du alligevel kommer ret tæt på din hovedadgangskode, og "blob-strengene", der relaterer til adgangskoden, ser ud til at være grupperet sammen i RAM, formentlig fordi de blev tildelt på omtrent samme tidspunkt af den samme del af .NET runtime.
Og dér, i en ganske vist lang og diskursiv nøddeskal, er den fascinerende historie om CVE-2023-32784.
Hvad skal jeg gøre?
- Hvis du er KeePass-bruger, skal du ikke gå i panik. Selvom dette er en fejl og teknisk set er en sårbarhed, der kan udnyttes, skal fjernangribere, der ønsker at knække din adgangskode ved hjælp af denne fejl, først implantere malware på din computer. Det ville give dem mange andre måder at stjæle dine adgangskoder direkte, selvom denne fejl ikke eksisterede, for eksempel ved at logge dine tastetryk, mens du skriver. På dette tidspunkt kan du blot holde øje med den kommende opdatering og få fat i den, når den er klar.
- Hvis du ikke bruger fuld-disk-kryptering, kan du overveje at aktivere det. For at udtrække resterende adgangskoder fra din swap-fil eller dvalefil (operativsystemdiskfiler, der bruges til at gemme hukommelsesindhold midlertidigt under hård belastning, eller når din computer "sover"), skal angribere have direkte adgang til din harddisk. Hvis du har BitLocker eller dets tilsvarende for andre operativsystemer aktiveret, vil de ikke være i stand til at få adgang til din swap-fil, din dvalefil eller andre personlige data såsom dokumenter, regneark, gemte e-mails og så videre.
- Hvis du er programmør, skal du holde dig orienteret om problemer med hukommelsesstyring. Antag ikke, at bare fordi hver
free()
matcher dens tilsvarendemalloc()
at dine data er sikre og velforvaltede. Nogle gange skal du muligvis tage ekstra forholdsregler for at undgå at efterlade hemmelige data liggende, og disse forholdsregler meget fra operativsystem til operativsystem. - Hvis du er en QA-tester eller en kodeanmelder, så tænk altid "bag kulisserne". Selvom hukommelsesstyringskoden ser ryddelig og velafbalanceret ud, skal du være opmærksom på, hvad der sker bag kulisserne (fordi den oprindelige programmør måske ikke vidste at gøre det), og gør dig klar til at udføre noget gennemgribende arbejde, såsom runtime-overvågning og hukommelse dumping for at bekræfte, at sikker kode virkelig opfører sig, som den skal.
KODE FRA ARTIKLEN: UNL1.C
#omfatte #omfatte #omfatte void hexdump(usigneret char* buff, int len) { // Udskriv buffer i 16-byte bidder for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Vis 16 bytes som hex-værdier for (int j = 0; j < 16; j = j+1) { printf("%02X", buff[i+j]); } // Gentag disse 16 bytes som tegn for (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) { // Hent hukommelse til at gemme adgangskode, og vis hvad // er i bufferen, når den officielt er "ny"... char* buff = malloc(128); printf("Dumper 'ny' buffer ved start"); hexdump(buff,128); // Brug pseudotilfældig bufferadresse som tilfældig frø srand((usigneret)buff); // Start adgangskoden med en fast, søgbar tekst strcpy(buff,"unlikelytext"); // Tilføj 16 pseudotilfældige bogstaver, et ad gangen for (int i = 1; i <= 16; i++) { // Vælg et bogstav fra A (65+0) til P (65+15) char ch = 65 + (rand() & 15); // Rediger derefter buff-strengen på plads strncat(buff,&ch,1); } // Den fulde adgangskode er nu i hukommelsen, så udskriv // den som en streng, og vis hele bufferen... printf("Fuld streng var: %sn",buff); hexdump(buff,128); // Pause for at dumpe proces-RAM nu (prøv: 'procdump -ma') puts("Venter på, at [ENTER] frigør buffer..."); getchar(); // Formelt frigør() hukommelsen og vis bufferen // igen for at se om der var noget tilbage... free(buff); printf("Dumping buffer efter free()n"); hexdump(buff,128); // Pause for at dumpe RAM igen for at inspicere forskelle puts("Venter på [ENTER] for at afslutte main()"); getchar(); retur 0; }
KODE FRA ARTIKLEN: UNL2.C
#omfatte #omfatte #omfatte #omfatte void hexdump(usigneret char* buff, int len) { // Udskriv buffer i 16-byte bidder for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Vis 16 bytes som hex-værdier for (int j = 0; j < 16; j = j+1) { printf("%02X", buff[i+j]); } // Gentag disse 16 bytes som tegn for (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) { // Hent hukommelse for at gemme adgangskode, og vis hvad // er i bufferen, når den officielt er "ny"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Dumper 'ny' buffer ved start"); hexdump(buff,128); // Brug pseudotilfældig bufferadresse som tilfældig frø srand((usigneret)buff); // Start adgangskoden med en fast, søgbar tekst strcpy(buff,"unlikelytext"); // Tilføj 16 pseudotilfældige bogstaver, et ad gangen for (int i = 1; i <= 16; i++) { // Vælg et bogstav fra A (65+0) til P (65+15) char ch = 65 + (rand() & 15); // Rediger derefter buff-strengen på plads strncat(buff,&ch,1); } // Den fulde adgangskode er nu i hukommelsen, så udskriv // den som en streng, og vis hele bufferen... printf("Fuld streng var: %sn",buff); hexdump(buff,128); // Pause for at dumpe proces-RAM nu (prøv: 'procdump -ma') puts("Venter på, at [ENTER] frigør buffer..."); getchar(); // Formelt frigør() hukommelsen og vis bufferen // igen for at se om der var noget tilbage... VirtualFree(buff,0,MEM_RELEASE); printf("Dumping buffer efter free()n"); hexdump(buff,128); // Pause for at dumpe RAM igen for at inspicere forskelle puts("Waiting for [ENTER] to exit main()"); getchar(); returnere 0; }
KODE FRA ARTIKLEN: S1.LUA
-- Start med en fast, søgbar tekst s = 'unlikelytext' -- Tilføj 16 tilfældige tegn fra 'A' til 'P' for i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Fuld streng er:',s,'n') -- Pause for at dumpe proces RAM print('Venter på [ENTER] før frigør streng...') io.read() - - Tør streng og markér variabel ubrugt s = nul -- Dump RAM igen for at se efter diffs print('Venter på [ENTER] før du afslutter...') io.read()
KODE FRA ARTIKLEN: FINDIT.LUA
-- læs i dumpfil lokal f = io.open(arg[1],'rb'):read('*a') -- kig efter markørtekst efterfulgt af et -- eller flere tilfældige ASCII-tegn lokale b,e ,m = 0,0,nul mens sand gør -- kig efter næste match og husk offset b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- afslut når der ikke er mere matcher hvis ikke b, så bryd ende -- rapport position og streng fundet print(string.format('%08X: %s',b,m)) end
KODE FRA ARTIKLEN: SEARCHKNOWN.LUA
io.write('Læser i dumpfil... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Søger efter SIXTEENPASSCHARS som 8-bit ASCII... ') local p08 = f:find('SEXTEENPASSCHARS') io.write(p08 og 'FOUND' eller 'ikke fundet','.n') io.write ('Søger efter SIXTEENPASSCHARS som UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx) 00.FOUND,'FOUND,'16Sx,'XNUMX. '.n')
KODE FRA ARTIKLEN: FINDBLOBS.LUA
-- læs i dump-fil angivet på kommandolinjen lokal f = io.open(arg[1],'rb'):read('*a') -- Se efter en eller flere adgangskode-blobs, efterfulgt af enhver ikke-blob -- Bemærk, at blob-tegn (●) koder til Windows widechars -- som litte-endian UTF-16-koder, der kommer ud som CF 25 i hex. local b,e,m = 0,0,nil mens sand do -- Vi vil have en eller flere klatter efterfulgt af enhver ikke-blob. -- Vi forenkler koden ved at lede efter en eksplicit CF25 -- efterfulgt af enhver streng, der kun har CF eller 25 i den, -- så vi finder CF25CFCF eller CF2525CF såvel som CF25CF25. -- Vi filtrerer "falske positive" fra senere, hvis der er nogen. -- Vi skal skrive '%%' i stedet for x25, fordi x25 ---tegnet (procenttegn) er et særligt søgetegn i Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- afslut når der ikke er flere matcher hvis ikke b så break end -- CMD.EXE kan ikke udskrive klatter, så vi konverterer dem til stjerner. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) end
KODE FRA ARTIKLEN: SEARCHKP.LUA
-- læs i dump-fil angivet på kommandolinjen lokal f = io.open(arg[1],'rb'):read('*a') lokal b,e,m,p = 0,0,nul,nil mens sand gør -- Nu vil vi have en eller flere klatter (CF25) efterfulgt af koden -- for A..Z efterfulgt af en 0 byte for at konvertere ACSCII til UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- afslut, når der ikke er flere matcher, hvis ikke b, så break end -- CMD.EXE kan ikke udskrive blobs, så vi konverterer dem til stjerner. -- For at spare plads undertrykker vi successive matches hvis m ~= p derefter print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m ende ende
- SEO Powered Content & PR Distribution. Bliv forstærket i dag.
- PlatoAiStream. Web3 Data Intelligence. Viden forstærket. Adgang her.
- Udmøntning af fremtiden med Adryenn Ashley. Adgang her.
- Køb og sælg aktier i PRE-IPO-virksomheder med PREIPO®. Adgang her.
- Kilde: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :har
- :er
- :ikke
- :hvor
- ][s
- $OP
- 1
- 10
- 12
- 15 %
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- I stand
- Om
- over
- absolutte
- AC
- adgang
- Konto
- erhverve
- erhverve
- aktiv
- faktiske
- faktisk
- tilføjet
- Yderligere
- adresse
- adresser
- Tilføjer
- Efter
- bagefter
- igen
- Alle
- allokeret
- allokerer
- allokering
- tildelinger
- tillade
- alene
- sammen
- allerede
- også
- ændret
- Skønt
- altid
- an
- ,
- Andrew
- besvare
- enhver
- noget
- noget kritisk
- vises
- dukkede
- tilgang
- godkendt
- ER
- omkring
- artikel
- artikler
- AS
- At
- forfatter
- auto
- Automatisk Ur
- automatisk
- til rådighed
- undgå
- undgås
- opmærksom på
- væk
- tilbage
- baggrund
- background-billede
- BE
- fordi
- bliver
- været
- før
- Begyndelse
- bag
- bag scenen
- jf. nedenstående
- Bedre
- Bit
- Bloker
- Blocks
- grænse
- både
- Bund
- brand
- Brand New
- Pause
- kortvarigt
- bringe
- buffer
- bufferoverløb
- Bug
- bugs
- bygge
- men
- by
- C + +
- ringe
- ringer
- CAN
- Kan få
- hvilken
- tilfælde
- fanget
- CD
- center
- sikkert
- kæder
- chance
- ændret
- karakter
- tegn
- kontrol
- Kontrol
- kinesisk
- Vælg
- klar
- tydeligt
- Luk
- kode
- farve
- KOM
- kommer
- kommer
- KOMMENTAR
- kommentarer
- Fælles
- fuldføre
- komplekse
- computer
- Overvej
- betydelig
- betragtes
- Bestående
- konstruere
- indhold
- indhold
- løbende
- fortsætter
- kontrol
- konvertere
- ophavsret
- Tilsvarende
- kunne
- dæksel
- sprække
- skabe
- oprettet
- skaberen
- kritisk
- Cybersecurity
- FARE
- Dangerous
- data
- datalækage
- Dage
- deal
- besluttede
- dedikeret
- beskrevet
- DID
- forskelle
- forskellige
- Vanskelighed
- DIG
- digital
- direkte
- Direkte adgang
- direkte
- Skærm
- visning
- har
- do
- dokumenter
- gør
- Er ikke
- gør
- færdig
- Dont
- ned
- køre
- grund
- dumpe
- i løbet af
- e
- hver
- tidligere
- nemt
- økosystem
- enten
- andet
- emails
- muliggør
- tilskynde
- kryptering
- ende
- ender
- nok
- sikre
- sikring
- Indtast
- indtastning
- Hele
- indrejse
- Miljø
- Ækvivalent
- fejl
- væsentlige
- anslået
- etc.
- Ether (ETH)
- Endog
- til sidst
- stadigt stigende
- Hver
- at alt
- præcist nok
- eksempel
- Undtagen
- Spænding
- udførelse
- eksisterer
- eksisterende
- Udgang
- Afslutning
- forvente
- Forklar
- Exploit
- udsat
- udvide
- ekstra
- ekstrakt
- Faktisk
- falsk
- fascinerende
- Funktionalitet
- tilbagemeldinger
- færre
- kampene
- File (Felt)
- Filer
- filtrere
- endelige
- Endelig
- Finde
- finde
- fund
- ende
- Fornavn
- fast
- Fokus
- efterfulgt
- efter
- Til
- formular
- Formelt
- format
- kommende
- fundet
- fire
- Gratis
- fra
- fuld
- fuldt ud
- funktion
- funktioner
- yderligere
- fremtiden
- genererer
- generation
- få
- få
- GitHub
- Giv
- given
- giver
- Give
- Go
- Goes
- gå
- godt
- Regering
- grab
- stor
- garanti
- havde
- Håndterer
- skete
- Happening
- sker
- Hård Ost
- Have
- have
- hovedpine
- tunge
- højde
- link.
- HEX
- højt niveau
- højere
- Hits
- hold
- Hole
- håber
- HOURS
- hover
- Hvordan
- How To
- HTTPS
- jagt
- i
- identifikator
- if
- straks
- vigtigt
- in
- omfatter
- Herunder
- oplysninger
- informeret
- i stedet
- interesseret
- Mellem
- Internet
- ind
- spørgsmål
- IT
- ITS
- selv
- jargon
- juni
- lige
- bare en
- Holde
- Nøgle
- Kend
- kendt
- koreansk
- Sprog
- Sprog
- laptop
- Efternavn
- senere
- føre
- Leads
- lække
- Lækager
- LÆR
- læring
- mindst
- forlader
- til venstre
- Længde
- brev
- Bibliotek
- Livet
- ligesom
- Sandsynlig
- Limited
- Line (linje)
- linjer
- Liste
- Børsnoterede
- ll
- belastning
- lokale
- placering
- logning
- Lang
- langsigtet
- længere
- Se
- ligner
- kiggede
- leder
- UDSEENDE
- Lot
- held
- fastholder
- lave
- malware
- administrere
- lykkedes
- ledelse
- leder
- administrerer
- Håndtering
- mange
- Margin
- markere
- markør
- Master
- Match
- matchende
- max-bredde
- Kan..
- midler
- Hukommelse
- nævnte
- microsoft
- måske
- minutter
- beskedne
- modificeret
- ændre
- øjeblik
- overvågning
- mere
- mest
- meget
- flere
- neat
- Behov
- behov
- netto
- aldrig
- Ikke desto mindre
- Ny
- nyheder
- næste
- rart
- ingen
- normal
- intet
- Varsel..
- nu
- nummer
- numre
- objekt
- Obvious
- of
- off
- officiel
- Officielt
- offset
- Gammel
- on
- engang
- ONE
- kun
- open source
- drift
- operativsystem
- operativsystemer
- operatør
- Option
- or
- ordrer
- original
- Andet
- Andre
- Ellers
- vores
- os selv
- ud
- output
- i løbet af
- samlet
- egen
- side
- Panic
- del
- Adgangskode
- Password Manager
- Nulstilling/ændring af adgangskoder
- sti
- Mønster
- paul
- pause
- Betal
- procent
- måske
- periode
- permanent
- personale
- Personlig data
- fysisk
- billede
- stykker
- Place
- pladsholder
- Plague
- plato
- Platon Data Intelligence
- PlatoData
- Masser
- Punkt
- punkter
- Populær
- position
- mulig
- Indlæg
- potentiale
- præcist
- præsentere
- smuk
- forhindre
- tidligere
- pris
- udskrifter
- sandsynligvis
- problemer
- behandle
- Program
- programmør
- Programmører
- Programmering
- Programmer
- udtalt
- sætte
- Python
- Spørgsmål og svar
- spørgsmål
- rejser
- RAM
- tilfældig
- rækkevidde
- hellere
- Raw
- rådata
- RE
- nået
- Læs
- Læsning
- klar
- ægte
- I virkeligheden
- realtid
- virkelig
- genkende
- Recover
- komme sig
- relaterede
- resterende
- huske
- fjern
- Fjern
- gentag
- gentaget
- GENTAGNE GANGE
- indberette
- repræsenteret
- respekt
- henholdsvis
- REST
- Resultater
- afkast
- vender tilbage
- afsløre
- Rid
- højre
- Risiko
- risici
- Værelse
- Kør
- kører
- runtime overvågning
- s
- sikker
- sikrere
- samme
- tilfreds
- Gem
- siger
- scanne
- spredt
- scener
- Søg
- søgning
- Anden
- sekunder
- Secret
- Sektion
- sikker
- sikkerhed
- se
- frø
- se
- synes
- set
- Sees
- Series
- alvorlig
- sæt
- indstilling
- Kort
- Inden længe
- bør
- Vis
- vist
- underskrive
- Skilte
- lignende
- Tilsvarende
- Simpelt
- forenklet
- forenkle
- ganske enkelt
- enkelt
- Størrelse
- søvn
- lille
- Luskede
- snooping
- So
- Software
- solid
- nogle
- noget
- Snart
- Kilde
- kildekode
- Space
- særligt
- specielt
- specificeret
- hastighed
- Stjerner
- starte
- påbegyndt
- Starter
- starter
- opstart
- Stadig
- Stole
- Stands
- stoppet
- butik
- opbevaret
- Story
- String
- stærk
- Studere
- Succesfuld
- sådan
- tilstrækkeligt
- formodes
- overraskelse
- overrasket
- overraskende
- overlever
- SVG
- bytte
- systemet
- Systemer
- Tag
- taget
- tager
- tager
- taler
- teknisk set
- teknikker
- midlertidig
- prøve
- tests
- end
- at
- The Source
- deres
- Them
- selv
- derefter
- teori
- Der.
- derfor
- de
- ting
- tror
- denne
- dem
- selvom?
- tænkte
- tid
- Titel
- til
- sammen
- tog
- værktøj
- top
- spor
- Sporing
- overgang
- gennemsigtig
- sand
- prøv
- Drejede
- to
- typen
- typisk
- forstå
- unicode
- indtil
- ubrugt
- uønsket
- Opdatering
- opdateret
- URL
- us
- os regering
- Brug
- usb
- brug
- bruge efter frigivelse
- anvendte
- Bruger
- bruger
- ved brug af
- nytte
- værdi
- Værdier
- række
- verificere
- udgave
- meget
- via
- sårbarhed
- W
- vente
- Venter
- ønsker
- ønskede
- var
- Ur
- Vej..
- måder
- we
- uger
- GODT
- var
- Hvad
- hvornår
- hvorvidt
- som
- mens
- WHO
- hvem
- Hele
- hvorfor
- vilje
- vinde
- vinduer
- tørre
- med
- uden
- undrende
- ord
- Arbejde
- arbejdede
- arbejder
- virker
- bekymre sig
- ville
- ville give
- skriver
- skrivning
- skriftlig
- endnu
- dig
- Din
- dig selv
- zephyrnet
- nul