Nelle ultime due settimane, abbiamo visto una serie di articoli parlare di quello che è stato descritto come un "crack della password principale" nel popolare gestore di password open source KeePass.
Il bug è stato considerato abbastanza importante da ottenere un identificatore ufficiale del governo degli Stati Uniti (è noto come CVE-2023-32784, se vuoi scovarlo), e dato che la password principale del tuo gestore di password è praticamente la chiave di tutto il tuo castello digitale, puoi capire perché la storia ha suscitato molta eccitazione.
La buona notizia è che un utente malintenzionato che volesse sfruttare questo bug avrebbe quasi sicuramente bisogno di aver già infettato il tuo computer con malware, e sarebbe quindi in grado di spiare le tue sequenze di tasti e i programmi in esecuzione comunque.
In altre parole, il bug può essere considerato un rischio facilmente gestibile fino a quando il creatore di KeePass non uscirà con un aggiornamento, che dovrebbe apparire presto (all'inizio di giugno 2023, a quanto pare).
Come si prende cura del rivelatore del bug indicare:
Se utilizzi la crittografia completa del disco con una password complessa e il tuo sistema è [privo di malware], dovresti andare bene. Nessuno può rubare le tue password da remoto su Internet solo con questa scoperta.
I rischi spiegati
Riassumendo pesantemente, il bug si riduce alla difficoltà di garantire che tutte le tracce di dati riservati vengano eliminate dalla memoria una volta che hai finito con loro.
Ignoreremo qui i problemi di come evitare di avere dati segreti in memoria, anche per breve tempo.
In questo articolo, vogliamo solo ricordare ai programmatori di tutto il mondo che il codice approvato da un revisore attento alla sicurezza con un commento del tipo "sembra ripulirsi correttamente dopo se stesso"...
… potrebbe infatti non ripulirsi completamente e la potenziale perdita di dati potrebbe non essere evidente da uno studio diretto del codice stesso.
In poche parole, la vulnerabilità CVE-2023-32784 significa che una password principale di KeePass potrebbe essere recuperabile dai dati di sistema anche dopo che il programma KeyPass è terminato, perché informazioni sufficienti sulla tua password (anche se non in realtà la password grezza stessa, su cui ci concentreremo in un momento) potrebbe rimanere indietro nei file di scambio o di sospensione del sistema, dove la memoria di sistema allocata potrebbe finire salvata per dopo.
Su un computer Windows in cui BitLocker non viene utilizzato per crittografare il disco rigido quando il sistema è spento, ciò darebbe a un truffatore che ha rubato il tuo laptop una possibilità di eseguire l'avvio da un'unità USB o CD e recuperare la tua password principale anche sebbene il programma KeyPass stesso si preoccupi di non salvarlo mai in modo permanente su disco.
Una perdita di password a lungo termine nella memoria significa anche che la password potrebbe, in teoria, essere recuperata da un dump della memoria del programma KeyPass, anche se quel dump è stato acquisito molto tempo dopo aver digitato la password e molto tempo dopo il KeePass stesso non aveva più bisogno di tenerlo in giro.
Chiaramente, dovresti presumere che il malware già presente sul tuo sistema possa recuperare quasi tutte le password digitate tramite una varietà di tecniche di snooping in tempo reale, purché fossero attive al momento della digitazione. Ma potresti ragionevolmente aspettarti che il tuo tempo esposto al pericolo sia limitato al breve periodo di digitazione, non esteso a molti minuti, ore o giorni dopo, o forse più a lungo, anche dopo aver spento il computer.
Cosa viene lasciato indietro?
Abbiamo quindi pensato di dare uno sguardo di alto livello a come i dati segreti possono essere lasciati nella memoria in modi che non sono direttamente ovvi dal codice.
Non preoccuparti se non sei un programmatore: lo manterremo semplice e spiegheremo mentre procediamo.
Inizieremo esaminando l'uso e la pulizia della memoria in un semplice programma C che simula l'inserimento e la memorizzazione temporanea di una password procedendo come segue:
- Allocazione di un blocco di memoria dedicato appositamente per memorizzare la password.
- Inserimento di una stringa di testo nota quindi possiamo trovarlo facilmente in memoria se necessario.
- Aggiunta di 16 caratteri ASCII a 8 bit pseudo-casuali dalla gamma AP.
- Stampare il buffer di password simulato.
- Liberare la memoria nella speranza di eliminare il buffer delle password.
- Uscita il programma.
Notevolmente semplificato, il codice C potrebbe assomigliare a questo, senza controllo degli errori, utilizzando numeri pseudo-casuali di scarsa qualità dalla funzione di runtime C rand()
e ignorando qualsiasi controllo di overflow del buffer (non eseguire mai nulla di tutto ciò nel codice reale!):
// 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 effetti, il codice che abbiamo finalmente utilizzato nei nostri test include alcuni bit e pezzi aggiuntivi mostrati di seguito, in modo da poter scaricare l'intero contenuto del nostro buffer di password temporaneo mentre lo usavamo, per cercare contenuti indesiderati o avanzati.
Nota che scarichiamo deliberatamente il buffer dopo la chiamata free()
, che tecnicamente è un bug use-after-free, ma lo stiamo facendo qui come un modo subdolo per vedere se qualcosa di critico viene lasciato indietro dopo aver restituito il nostro buffer, il che potrebbe portare a un pericoloso buco di perdita di dati nella vita reale.
Ne abbiamo inseriti anche due Waiting for [Enter]
richiede nel codice di darci la possibilità di creare dump della memoria nei punti chiave del programma, fornendoci dati grezzi da cercare in seguito, per vedere cosa è stato lasciato indietro durante l'esecuzione del programma.
Per eseguire i dump della memoria, utilizzeremo Microsoft Strumento Sysinternals procdump
con la -ma
opzione (scaricare tutta la memoria), che evita la necessità di scrivere il nostro codice per utilizzare Windows DbgHelp
sistema ed è piuttosto complesso MiniDumpXxxx()
funzioni.
Per compilare il codice C, abbiamo utilizzato la nostra build piccola e semplice del software gratuito e open-source di Fabrice Bellard Piccolo compilatore C, disponibile per Windows a 64 bit in sorgente e forma binaria direttamente dalla nostra pagina GitHub.
Il testo copiabile e incollabile di tutto il codice sorgente raffigurato nell'articolo appare in fondo alla pagina.
Questo è quello che è successo quando abbiamo compilato ed eseguito il programma di test:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Compilatore Tiny C - Copyright (C) 2001-2023 Fabrice Bellard Ridotto da Paul Ducklin per l'uso come strumento di apprendimento Versione petcc64-0.9.27 [0006] - Genera 64 bit Solo PE -> 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 -------- -------------- Dimensione file virt sezione 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 byte) C:UsersduckKEYPASS> unl1.exe Dumping 'nuovo' buffer all'avvio 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 stem32cmd. 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_ 00F513 0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 net ExplzV.< .K.. La stringa completa era: improbabile testoJHKNEJJCPOMDJHAN 51390F75: 6 6E 69C 6 65B 6 79C 74 65 78 74 4 48A 4 4B 00E improbabile testoJHKN 513F0A45: 4 4A 43A 50 4 4F 44D 4 48 A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72 : 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00E riverData=C:Win 513F0C64: 6 77F 73 5 53C 79 73 74 65 6 33D 32 5 44C 72 32 dowsSystem00Dr 513F0D69: 76 65 72 73 5 44C 72 69 76 65 72 44 61 74 61 00 iversDriverData 513F0E00: 45 46 43 5 34F 33 37 32 3 31D 00 46 50 53 5 4372F .EFC_1=00.FPS_ 513F0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 net ExplzV.<.K.. In attesa che [ENTER] liberi il buffer... Scarica il buffer dopo free() 51390F0: A67 5 F00 00 00 00 00 50 01 5 F00 00 00 00 00 00 .g......P...... 513F0A45: 4 4A 43A 50 4 4F 44D 4 48A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72: 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00 513E riverData=C:Win 0F64C6: 77 73F 5 53 79C 73 74 65 6 33 32D 5 44 72C 32 00 dowsSystem513Dr 0F69D76: 65 72 73 5 44 72C 69 76 65 72 44 61 74 61 00 513 iversDriverData 0F00E45: 46 43 5 34 33F 37 32 3 31 00D 46 50 53 5 4372 1F .EFC_00=513.FPS_ 0F42F52: 4 57 53F 45 52 5 41 50F 50 5 50 52F 4 46 00F 51400 BROWSER_APP_PROF 49F4: 45 5C 53 54F 52 49 4 47 3E 49 6D 74 65E 72 00 51410 ILE_STRING=Inter 6F65: 74E 20 45 78 70 6 4 00C 00D 4 4 00D AC 00B XNUMX XNUMX net ExplM..MK. In attesa che [ENTER] esca da main()... C:UsersduckKEYPASS>
In questa esecuzione, non ci siamo preoccupati di acquisire alcun dump della memoria del processo, perché abbiamo potuto vedere subito dall'output che questo codice perde dati.
Subito dopo aver chiamato la funzione della libreria di runtime di Windows C malloc()
, possiamo vedere che il buffer che otteniamo include quelli che sembrano dati di variabili d'ambiente lasciati dal codice di avvio del programma, con i primi 16 byte apparentemente alterati per sembrare una sorta di intestazione di allocazione della memoria rimanente.
(Nota come quei 16 byte sembrano due indirizzi di memoria da 8 byte, 0xF55790
ed 0xF50150
, che sono rispettivamente subito dopo e subito prima del nostro buffer di memoria.)
Quando si suppone che la password sia in memoria, possiamo vedere chiaramente l'intera stringa nel buffer, come ci aspetteremmo.
Ma dopo aver chiamato free()
, si noti come i primi 16 byte del nostro buffer siano stati riscritti con quelli che sembrano indirizzi di memoria vicini ancora una volta, presumibilmente in modo che l'allocatore di memoria possa tenere traccia dei blocchi in memoria che può riutilizzare...
… ma il resto del testo della nostra password “cancellata” (gli ultimi 12 caratteri casuali EJJCPOMDJHAN
) è stato lasciato indietro.
Non solo dobbiamo gestire le nostre allocazioni e deallocazioni di memoria in C, ma dobbiamo anche assicurarci di scegliere le giuste funzioni di sistema per i buffer di dati se vogliamo controllarli con precisione.
Ad esempio, passando invece a questo codice, otteniamo un po' più di controllo su cosa c'è in memoria:
Passando da malloc()
ed free()
per utilizzare le funzioni di allocazione di Windows di livello inferiore VirtualAlloc()
ed VirtualFree()
direttamente, otteniamo un migliore controllo.
Tuttavia, paghiamo un prezzo in velocità, perché ogni chiamata a VirtualAlloc()
fa più lavoro che una chiamata a malloc()
, che funziona dividendo e suddividendo continuamente un blocco di memoria di basso livello preallocata.
utilizzando VirtualAlloc()
ripetutamente per piccoli blocchi utilizza anche più memoria in generale, perché ogni blocco viene distribuito da VirtualAlloc()
in genere consuma un multiplo di 4 KB di memoria (o 2 MB, se si utilizzano i cosiddetti grandi pagine di memoria), in modo che il nostro buffer di 128 byte sopra sia arrotondato a 4096 byte, sprecando i 3968 byte alla fine del blocco di memoria da 4 KB.
Ma, come puoi vedere, la memoria che otteniamo viene automaticamente cancellata (impostata a zero), quindi non possiamo vedere cosa c'era prima, e questa volta il programma va in crash quando proviamo a fare il nostro use-after-free trucco, perché Windows rileva che stiamo cercando di sbirciare la memoria che non possediamo più:
C:UsersduckKEYPASS> unl2 Scarica il "nuovo" buffer all'avvio 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 ................ La stringa completa era: improbabiletestoIBIPJPPHEOPOIDLL 0000EA75: 6 6E 69C 6 65B 6 79C 74 65 78 74 49 42 49 50 0000000000 improbabiletestoIBIP 0010EA4: 50A 50 48 45 4 50F 4 49f 44 4 4C 00C 00 00 00 0000000000 JPPHEOPOIDLLLLL .... 0020EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0030 ................ 00EA00 : 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 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0060 ............... 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0070 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0080 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0000 ............. ... In attesa che [INVIO] liberi il buffer... Scarica il buffer dopo free() XNUMXEAXNUMX: [Programma terminato qui perché Windows ha rilevato il nostro use-after-free]
Perché la memoria che abbiamo liberato dovrà essere riallocata VirtualAlloc()
prima che possa essere riutilizzato, possiamo presumere che verrà azzerato prima di essere riciclato.
Tuttavia, se volessimo assicurarci che fosse oscurato, potremmo chiamare la funzione speciale di Windows RtlSecureZeroMemory()
appena prima di liberarlo, per garantire che Windows scriva prima gli zeri nel nostro buffer.
La relativa funzione RtlZeroMemory()
, se te lo stavi chiedendo, fa una cosa simile, ma senza la garanzia di funzionare effettivamente, perché i compilatori possono rimuoverlo come teoricamente ridondante se notano che il buffer non viene riutilizzato in seguito.
Come puoi vedere, dobbiamo prestare molta attenzione nell'usare le giuste funzioni di Windows se vogliamo ridurre al minimo il tempo in cui i segreti archiviati nella memoria potrebbero rimanere per dopo.
In questo articolo, non esamineremo come impedire che i segreti vengano salvati accidentalmente nel file di scambio bloccandoli nella RAM fisica. (Suggerimento: VirtualLock()
in realtà non è sufficiente da solo.) Se desideri saperne di più sulla sicurezza della memoria di basso livello di Windows, faccelo sapere nei commenti e lo esamineremo in un futuro articolo.
Utilizzo della gestione automatica della memoria
Un modo accurato per evitare di dover allocare, gestire e deallocare la memoria da soli è utilizzare un linguaggio di programmazione che si occupi di malloc()
ed free()
, o VirtualAlloc()
ed VirtualFree()
, automaticamente.
Linguaggio di scripting come Perl, Python, prendere, JavaScript e altri eliminano i più comuni bug di sicurezza della memoria che affliggono il codice C e C++, monitorando l'utilizzo della memoria per te in background.
Come accennato in precedenza, il nostro codice C di esempio scritto male sopra funziona bene ora, ma solo perché è ancora un programma semplicissimo, con strutture dati di dimensioni fisse, in cui possiamo verificare mediante ispezione che non sovrascriveremo il nostro 128- buffer di byte e che esiste un solo percorso di esecuzione che inizia con malloc()
e termina con un corrispondente free()
.
Ma se lo aggiornassimo per consentire la generazione di password di lunghezza variabile o aggiungessimo funzionalità aggiuntive nel processo di generazione, allora noi (o chiunque mantenga il codice successivo) potremmo facilmente finire con overflow del buffer, bug use-after-free o memoria che non viene mai liberato e quindi lascia i dati segreti in giro molto tempo dopo che non sono più necessari.
In un linguaggio come Lua, possiamo lasciare che l'ambiente di runtime Lua, che esegue ciò che in gergo è noto come raccolta automatica dei rifiuti, occuparsi dell'acquisizione della memoria dal sistema e restituirla quando rileva che abbiamo smesso di usarla.
Il programma C che abbiamo elencato sopra diventa molto più semplice quando l'allocazione e la deallocazione della memoria vengono curate per noi:
Assegniamo memoria per contenere la stringa s
semplicemente assegnando la stringa 'unlikelytext'
ad esso.
In seguito possiamo accennare esplicitamente a Lua che non ci interessa più s
assegnandogli il valore nil
(tutti nils
sono essenzialmente lo stesso oggetto Lua), o smetti di usarlo s
e attendi che Lua rilevi che non è più necessario.
Ad ogni modo, la memoria utilizzata da s
verrà infine recuperato automaticamente.
E per evitare overflow del buffer o cattiva gestione delle dimensioni durante l'aggiunta di stringhe di testo (l'operatore Lua ..
, pronunciato concatenato, essenzialmente aggiunge due stringhe insieme, come +
in Python), ogni volta che estendiamo o accorciamo una stringa, Lua alloca magicamente lo spazio per una stringa nuova di zecca, invece di modificare o sostituire quella originale nella sua posizione di memoria esistente.
Questo approccio è più lento e porta a picchi di utilizzo della memoria superiori a quelli che si otterrebbero in C a causa delle stringhe intermedie allocate durante la manipolazione del testo, ma è molto più sicuro rispetto agli overflow del buffer.
Ma questa sorta di gestione automatica delle stringhe (conosciuta in gergo come immutabilità, perché le stringhe non ottengono mai mutato, o modificati sul posto, una volta che sono stati creati), comporta nuovi grattacapi per la sicurezza informatica.
Abbiamo eseguito il programma Lua sopra su Windows, fino alla seconda pausa, appena prima che il programma uscisse:
C:UsersduckKEYPASS> lua s1.lua La stringa completa è: improbabiletestoHLKONBOJILAGLNLN In attesa di [INVIO] prima di liberare la stringa... In attesa di [INVIO] prima di uscire...
Questa volta, abbiamo eseguito un dump della memoria del processo, in questo modo:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Utilità di dump del processo di Sysinternals Copyright (C) 2009-2022 Mark Russinovich e Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump 1 iniziato: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Scrittura dump 1: la dimensione stimata del file dump è di 10 MB. [00:00:00] Dump 1 completato: 10 MB scritti in 0.1 secondi [00:00:01] Numero di dump raggiunto.
Quindi abbiamo eseguito questo semplice script, che rilegge il file di dump, trova ovunque in memoria quella stringa nota unlikelytext
è apparso e lo stampa, insieme alla sua posizione nel dumpfile e ai caratteri ASCII immediatamente successivi:
Anche se hai già utilizzato linguaggi di scripting o hai lavorato in qualsiasi ecosistema di programmazione che presenta i cosiddetti stringhe gestite, dove il sistema tiene traccia delle allocazioni e deallocazioni di memoria per te e le gestisce come meglio crede...
… potresti essere sorpreso di vedere l'output prodotto da questa scansione della memoria:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: improbabiletestoALJBNGOAPLLBDEB 006D8B3C: improbabiletestoALJBNGOA 006D8B7C: improbabiletestoALJBNGO 006D8BFC: improbabiletestoALJBNGOAPLLBDEBJ 006D8CBC: improbabiletestoALJBN 006D8D 7C: improbabiletestoALJBNGOAP 006D903C: improbabiletestoALJBNGOAPL 006D90BC: improbabiletestoALJBNGOAPLL 006D90FC: improbabiletestoALJBNG 006D913C: improbabiletestoALJBNGOAPLLB 006D91BC: improbabiletestoALJB 006D91FC: improbabiletestoALJBNGOAPLLBD 006D923C : improbabiletestoALJBNGOAPLLBDE 006DB70C: improbabiletestoALJ 006DBB8C: improbabiletestoAL 006DBD0C: improbabiletestoA
Ed ecco, nel momento in cui abbiamo afferrato il nostro deposito di memoria, anche se avevamo finito con la stringa s
(e ha detto a Lua che non ne avevamo più bisogno dicendo s = nil
), tutte le stringhe che il codice aveva creato strada facendo erano ancora presenti in RAM, non ancora recuperate o cancellate.
In effetti, se ordiniamo l'output di cui sopra in base alle stringhe stesse, anziché seguire l'ordine in cui sono apparse nella RAM, sarai in grado di immaginare cosa è successo durante il ciclo in cui abbiamo concatenato un carattere alla volta alla stringa della nostra password:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: improbabiletestoA 006DBB8C: improbabiletestoAL 006DB70C: improbabiletestoALJ 006D91BC: improbabiletestoALJB 006D8CBC: improbabiletestoALJBN 006D90FC: improbabiletestoALJBNG 006D8B7C: improbabiletestoALJBNGO 006D8B3C: improbabiletestoALJBNGOA 006 8D7D006C: improbabiletestoALJBNGOAP 903D006C: improbabiletestoALJBNGOAPL 90D006BC: improbabiletestoALJBNGOAPLL 913D006C: improbabiletestoALJBNGOAPLLB 91D006FC: improbabiletestoALJBNGOAPLLBD 923D006C: improbabiletestoALJBNGOAPLLBDE 8D006AFC: improbabiletestoALJBNGOAP LLBDEB8DXNUMXBFC : testoimprobabileALJBNGOAPLLBDEBJ
Tutte quelle stringhe intermedie temporanee sono ancora lì, quindi anche se avessimo cancellato con successo il valore finale di s
, continueremmo a far trapelare tutto tranne il suo ultimo carattere.
Infatti, in questo caso, anche quando abbiamo deliberatamente forzato il nostro programma a smaltire tutti i dati non necessari chiamando la speciale funzione Lua collectgarbage()
(la maggior parte dei linguaggi di scripting ha qualcosa di simile), la maggior parte dei dati in quelle fastidiose stringhe temporanee è comunque bloccata nella RAM, perché avevamo compilato Lua per eseguire la sua gestione automatica della memoria usando il buon vecchio malloc()
ed free()
.
In altre parole, anche dopo che la stessa Lua ha recuperato i suoi blocchi di memoria temporanei per usarli di nuovo, non siamo riusciti a controllare come o quando quei blocchi di memoria sarebbero stati riutilizzati, e quindi per quanto tempo sarebbero rimasti all'interno del processo con il loro sinistro- sui dati in attesa di essere individuati, scaricati o divulgati in altro modo.
Inserisci .NET
Ma per quanto riguarda KeePass, da dove è iniziato questo articolo?
KeePass è scritto in C# e utilizza il runtime .NET, quindi evita i problemi di cattiva gestione della memoria che i programmi C portano con sé...
… ma C# gestisce le proprie stringhe di testo, un po' come fa Lua, il che solleva la domanda:
Anche se il programmatore avesse evitato di memorizzare l'intera password principale in un unico posto dopo averla terminata, gli aggressori con accesso a un dump della memoria potrebbero comunque trovare abbastanza dati temporanei rimanenti per indovinare o recuperare comunque la password principale, anche se quelli gli aggressori hanno avuto accesso al tuo computer minuti, ore o giorni dopo che avevi digitato la password ?
In poche parole, ci sono resti rilevabili e spettrali della tua password principale che sopravvivono nella RAM, anche dopo che ti aspetteresti che siano stati cancellati?
Fastidiosamente, come utente Github Scoprì Vdohney, la risposta (almeno per le versioni di KeePass precedenti alla 2.54) è "Sì".
Per essere chiari, non pensiamo che la tua vera password principale possa essere recuperata come una singola stringa di testo da un dump della memoria di KeePass, perché l'autore ha creato una funzione speciale per l'immissione della password principale che fa di tutto per evitare di memorizzare l'intero password dove potrebbe essere facilmente individuata e fiutata.
Ci siamo accontentati di questo impostando la nostra password principale su SIXTEENPASSCHARS
, digitandolo e quindi eseguendo i dump della memoria immediatamente, poco e molto tempo dopo.
Abbiamo cercato i dump con un semplice script Lua che cercava ovunque il testo della password, sia in formato ASCII a 8 bit, sia in formato UTF-16 a 16 bit (Windows widechar), in questo modo:
I risultati sono stati incoraggianti:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Lettura nel file dump... FATTO. Ricerca di SIXTEENPASSCHARS come ASCII a 8 bit... non trovato. Ricerca di SIXTEENPASSCHARS come UTF-16... non trovato.
Ma Vdohney, lo scopritore di CVE-2023-32784, ha notato che mentre digiti la tua password principale, KeePass ti dà un feedback visivo costruendo e visualizzando una stringa segnaposto composta da caratteri "blob" Unicode, fino alla lunghezza del tuo parola d'ordine:
Nelle stringhe di testo widechar su Windows (che consistono di due byte per carattere, non solo un byte ciascuna come in ASCII), il carattere "blob" è codificato nella RAM come byte esadecimale 0xCF
seguito da 0x25
(che sembra essere un segno di percentuale in ASCII).
Quindi, anche se KeePass presta molta attenzione ai caratteri grezzi che digiti quando inserisci la password stessa, potresti ritrovarti con stringhe rimanenti di caratteri "blob", facilmente rilevabili in memoria come esecuzioni ripetute come CF25CF25
or CF25CF25CF25
...
…e, in tal caso, la sequenza più lunga di caratteri blob che hai trovato probabilmente rivelerebbe la lunghezza della tua password, che sarebbe una forma modesta di perdita di informazioni sulla password, se non altro.
Abbiamo utilizzato il seguente script Lua per cercare i segni delle stringhe segnaposto della password rimanenti:
L'output è stato sorprendente (abbiamo cancellato righe successive con lo stesso numero di blob, o con meno blob rispetto alla riga precedente, per risparmiare spazio):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ continua allo stesso modo per 8 blob, 9 blob, ecc. ] [ fino a due righe finali di esattamente 16 blob ciascuna ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [tutte le corrispondenze rimanenti sono lunghe un blob] 0123B058: *
A indirizzi di memoria ravvicinati ma sempre crescenti, abbiamo trovato un elenco sistematico di 3 BLOB, poi 4 BLOB e così via fino a 16 BLOB (la lunghezza della nostra password), seguito da molte istanze sparse casualmente di stringhe a BLOB singolo .
Quindi, quelle stringhe "blob" segnaposto sembrano effettivamente fuoriuscire dalla memoria e rimanere indietro per far trapelare la lunghezza della password, molto tempo dopo che il software KeePass ha terminato con la tua password principale.
Il prossimo passo
Abbiamo deciso di scavare ulteriormente, proprio come ha fatto Vdohney.
Abbiamo modificato il nostro codice di corrispondenza del modello per rilevare catene di caratteri blob seguiti da qualsiasi singolo carattere ASCII in formato a 16 bit (i caratteri ASCII sono rappresentati in UTF-16 come il loro normale codice ASCII a 8 bit, seguito da un byte zero).
Questa volta, per risparmiare spazio, abbiamo soppresso l'output per qualsiasi corrispondenza che corrisponde esattamente a quella precedente:
Sorpresa sorpresa:
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
Guarda cosa otteniamo dalla regione di memoria delle stringhe gestite di .NET!
Un insieme fitto di "stringhe blob" temporanee che rivelano i caratteri successivi nella nostra password, a partire dal secondo carattere.
Quelle stringhe che perdono sono seguite da corrispondenze a carattere singolo ampiamente distribuite che presumiamo siano sorte per caso. (Un file di dump di KeePass ha una dimensione di circa 250 MB, quindi c'è molto spazio per far apparire i caratteri "blob" come per fortuna.)
Anche se prendiamo in considerazione quelle quattro corrispondenze extra, invece di scartarle come probabili mancate corrispondenze, possiamo supporre che la password principale sia una delle seguenti:
?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Ovviamente questa semplice tecnica non trova il primo carattere della password, perché la prima “stringa di blob” viene costruita solo dopo che quel primo carattere è stato digitato
Nota che questo elenco è carino e breve perché abbiamo filtrato le corrispondenze che non terminano con caratteri ASCII.
Se stavi cercando caratteri in un intervallo diverso, come i caratteri cinesi o coreani, potresti ritrovarti con più risultati accidentali, perché ci sono molti più caratteri possibili su cui abbinare...
... ma sospettiamo che ti avvicinerai comunque abbastanza alla tua password principale, e le "stringhe blob" che si riferiscono alla password sembrano essere raggruppate insieme nella RAM, presumibilmente perché sono state allocate all'incirca nello stesso momento dalla stessa parte di il runtime di .NET.
E lì, in un guscio di noce dichiaratamente lungo e discorsivo, c'è l'affascinante storia di CVE-2023-32784.
Cosa fare?
- Se sei un utente KeePass, niente panico. Sebbene si tratti di un bug e tecnicamente di una vulnerabilità sfruttabile, gli aggressori remoti che desiderano decifrare la tua password utilizzando questo bug dovrebbero prima impiantare malware sul tuo computer. Ciò darebbe loro molti altri modi per rubare direttamente le tue password, anche se questo bug non esistesse, ad esempio registrando le tue sequenze di tasti mentre digiti. A questo punto, puoi semplicemente fare attenzione al prossimo aggiornamento e prenderlo quando è pronto.
- Se non utilizzi la crittografia dell'intero disco, considera di abilitarla. Per estrarre le password rimanenti dal file di scambio o dal file di ibernazione (file del disco del sistema operativo utilizzati per salvare temporaneamente il contenuto della memoria durante un carico pesante o quando il computer è "inattivo"), gli aggressori avrebbero bisogno dell'accesso diretto al disco rigido. Se hai attivato BitLocker o il suo equivalente per altri sistemi operativi, non saranno in grado di accedere al tuo file di scambio, al tuo file di ibernazione o ad altri dati personali come documenti, fogli di calcolo, e-mail salvate e così via.
- Se sei un programmatore, tieniti informato sui problemi di gestione della memoria. Non dare per scontato che solo perché ogni
free()
corrisponde al suo corrispondentemalloc()
che i tuoi dati siano al sicuro e ben gestiti. A volte, potrebbe essere necessario prendere ulteriori precauzioni per evitare di lasciare dati segreti in giro, e quelle precauzioni molto da sistema operativo a sistema operativo. - Se sei un tester QA o un revisore del codice, pensa sempre "dietro le quinte". Anche se il codice di gestione della memoria sembra ordinato e ben bilanciato, sii consapevole di ciò che sta accadendo dietro le quinte (perché il programmatore originale potrebbe non saperlo) e preparati a svolgere un lavoro in stile pentesting come il monitoraggio del runtime e la memoria dumping per verificare che il codice sicuro si stia davvero comportando come dovrebbe.
CODICE DELL'ARTICOLO: UNL1.C
#includere #includere #includere void hexdump(unsigned char* buff, int len) { // Stampa il buffer in blocchi di 16 byte for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Mostra 16 byte come valori esadecimali per (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Ripeti quei 16 byte come caratteri 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) { // Acquisisce memoria per memorizzare la password e mostra cosa // c'è nel buffer quando è ufficialmente "nuovo"... char* buff = malloc(128); printf("Dumping 'nuovo' buffer all'avvio"); hexdump(buff,128); // Usa l'indirizzo del buffer pseudocasuale come seme casuale srand((unsigned)buff); // Inizia la password con del testo fisso e ricercabile strcpy(buff,"unlikelytext"); // Aggiungi 16 lettere pseudocasuali, una alla volta for (int i = 1; i <= 16; i++) { // Scegli una lettera da A (65+0) a P (65+15) char ch = 65 + (rand() & 15); // Quindi modifica la stringa buff in place strncat(buff,&ch,1); } // La password completa è ora in memoria, quindi stampala // come una stringa e mostra l'intero buffer... printf("La stringa completa era: %sn",buff); hexdump(buff,128); // Metti in pausa per eseguire il dump della RAM del processo ora (prova: 'procdump -ma') puts("In attesa di [INVIO] per liberare il buffer..."); getchar(); // Formalmente libera() la memoria e mostra di nuovo il buffer // per vedere se è stato lasciato qualcosa... free(buff); printf("Scarica buffer dopo free()n"); hexdump(buff,128); // Pausa per eseguire nuovamente il dump della RAM per ispezionare le differenze puts("In attesa che [INVIO] esca da main()..."); getchar(); ritorno 0; }
CODICE DELL'ARTICOLO: UNL2.C
#includere #includere #includere #includere void hexdump(unsigned char* buff, int len) { // Stampa il buffer in blocchi di 16 byte for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Mostra 16 byte come valori esadecimali per (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Ripeti quei 16 byte come caratteri 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) { // Acquisisce memoria per memorizzare la password e mostra cosa // c'è nel buffer quando è ufficialmente "nuovo"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Dumping 'nuovo' buffer all'avvio"); hexdump(buff,128); // Usa l'indirizzo del buffer pseudocasuale come seme casuale srand((unsigned)buff); // Inizia la password con del testo fisso e ricercabile strcpy(buff,"unlikelytext"); // Aggiungi 16 lettere pseudocasuali, una alla volta for (int i = 1; i <= 16; i++) { // Scegli una lettera da A (65+0) a P (65+15) char ch = 65 + (rand() & 15); // Quindi modifica la stringa buff in place strncat(buff,&ch,1); } // La password completa è ora in memoria, quindi stampala // come una stringa e mostra l'intero buffer... printf("La stringa completa era: %sn",buff); hexdump(buff,128); // Metti in pausa per eseguire il dump della RAM del processo ora (prova: 'procdump -ma') puts("In attesa di [INVIO] per liberare il buffer..."); getchar(); // Formalmente libera() la memoria e mostra di nuovo il buffer // per vedere se è stato lasciato qualcosa... VirtualFree(buff,0,MEM_RELEASE); printf("Scarica buffer dopo free()n"); hexdump(buff,128); // Pausa per eseguire nuovamente il dump della RAM per ispezionare le differenze puts("In attesa che [INVIO] esca da main()..."); getchar(); ritorno 0; }
CODICE DALL'ARTICOLO: S1.LUA
-- Inizia con del testo fisso e ricercabile s = 'unlikelytext' -- Aggiungi 16 caratteri casuali da 'A' a 'P' per i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('La stringa completa è:',s,'n') -- Pausa per eseguire il dump della RAM del processo print('In attesa di [INVIO] prima di liberare la stringa...') io.read() - - Cancella la stringa e contrassegna la variabile inutilizzata s = nil -- Scarica di nuovo la RAM per cercare le differenze print('In attesa di [INVIO] prima di uscire...') io.read()
CODICE DALL'ARTICOLO: FINDIT.LUA
-- legge nel file dump local f = io.open(arg[1],'rb'):read('*a') -- cerca il testo del marcatore seguito da uno -- o più caratteri ASCII casuali local b,e ,m = 0,0,nil while true do -- cerca la corrispondenza successiva e ricorda l'offset b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- esci quando non c'è più corrisponde se non b allora break end -- segnala la posizione e la stringa trovata print(string.format('%08X: %s',b,m)) end
CODICE DALL'ARTICOLO: SEARCHKNOWN.LUA
io.write('Lettura nel file dump... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Ricerca di SIXTEENPASSCHARS come ASCII a 8 bit... ') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 e 'TROVATO' o 'non trovato','.n') io.write ('Cercando SIXTEENPASSCHARS come UTF-16...') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 e 'TROVATO' o 'non trovato' ,'.n')
CODICE DELL'ARTICOLO: FINDBLOBS.LUA
-- legge nel file di dump specificato sulla riga di comando local f = io.open(arg[1],'rb'):read('*a') -- Cerca uno o più blob di password, seguiti da qualsiasi non-blob -- Si noti che i caratteri blob (●) codificano in Windows widechars -- come codici litte-endian UTF-16, risultando come CF 25 in esadecimale. local b,e,m = 0,0,nil while true do -- Vogliamo uno o più blob, seguiti da qualsiasi non-blob. -- Semplifichiamo il codice cercando un CF25 esplicito -- seguito da qualsiasi stringa che contenga solo CF o 25 -- quindi troveremo CF25CFCF o CF2525CF così come CF25CF25. -- Filtreremo i "falsi positivi" in seguito, se ce ne sono. -- Dobbiamo scrivere '%%' invece di x25 perché il carattere x25 -- (segno di percentuale) è un carattere di ricerca speciale in Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- esci quando non ci sono più corrispondenze se no b then break end -- CMD.EXE non può stampare blob, quindi li convertiamo in stelle. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) end
CODICE DALL'ARTICOLO: SEARCHKP.LUA
-- legge nel file dump specificato sulla riga di comando local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Ora, vogliamo uno o più blob (CF25) seguiti dal codice -- for A..Z seguito da un byte 0 per convertire ACSCII in UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- esci quando non ci sono più corrispondenze if not b then break end -- CMD.EXE non può stampare i blob, quindi li convertiamo in stelle. -- Per risparmiare spazio eliminiamo le corrispondenze successive if m ~= p then print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m fine fine
- Distribuzione di contenuti basati su SEO e PR. Ricevi amplificazione oggi.
- PlatoAiStream. Intelligenza dei dati Web3. Conoscenza amplificata. Accedi qui.
- Coniare il futuro con Adryenn Ashley. Accedi qui.
- Acquista e vendi azioni in società PRE-IPO con PREIPO®. Accedi qui.
- Fonte: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :ha
- :È
- :non
- :Dove
- ][P
- $ SU
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- capace
- Chi siamo
- sopra
- Assoluta
- AC
- accesso
- Il mio account
- acquisire
- l'acquisizione di
- attivo
- presenti
- effettivamente
- aggiunto
- aggiuntivo
- indirizzo
- indirizzi
- Aggiunge
- Dopo shavasana, sedersi in silenzio; saluti;
- dopo
- ancora
- Tutti
- allocato
- alloca
- assegnazione
- allocazioni
- consentire
- da solo
- lungo
- già
- anche
- alterato
- Sebbene il
- sempre
- an
- ed
- Andrea
- rispondere
- in qualsiasi
- nulla
- qualcosa di critico
- apparire
- apparso
- approccio
- approvato
- SONO
- in giro
- articolo
- news
- AS
- At
- autore
- auto
- Automatico
- automaticamente
- disponibile
- evitare
- evitato
- consapevole
- lontano
- precedente
- sfondo
- background-image
- BE
- perché
- diventa
- stato
- prima
- Inizio
- dietro
- dietro le quinte
- sotto
- Meglio
- Po
- Bloccare
- Blocchi
- sistema
- entrambi
- Parte inferiore
- marca
- Nuovo di zecca
- Rompere
- brevemente
- portare
- bufferizzare
- buffer overflow
- Insetto
- bug
- costruire
- ma
- by
- C++
- chiamata
- chiamata
- Materiale
- Può ottenere
- che
- Custodie
- catturati
- CD
- centro
- certamente
- Catene
- possibilità
- cambiato
- carattere
- caratteri
- verifica
- Controlli
- Cinese
- Scegli
- pulire campo
- chiaramente
- Chiudi
- codice
- colore
- COM
- viene
- arrivo
- commento
- Commenti
- Uncommon
- completamento di una
- complesso
- computer
- Prendere in considerazione
- notevole
- considerato
- Consistente
- costruire
- contenuto
- testuali
- continuamente
- continua
- di controllo
- convertire
- copyright
- Corrispondente
- potuto
- coprire
- crepa
- creare
- creato
- Creatore
- critico
- Cybersecurity
- PERICOLO
- Pericoloso
- dati
- perdita di dati
- Giorni
- affare
- deciso
- dedicato
- descritta
- DID
- differenze
- diverso
- Livello di difficoltà
- DIG
- digitale
- dirette
- Accesso diretto
- direttamente
- Dsiplay
- visualizzazione
- ha
- do
- documenti
- effettua
- non
- fare
- fatto
- Dont
- giù
- guidare
- dovuto
- cumulo di rifiuti
- durante
- e
- ogni
- In precedenza
- facilmente
- ecosistema
- o
- altro
- consentendo
- incoraggiando
- crittografia
- fine
- finisce
- abbastanza
- garantire
- assicurando
- entrare
- entrare
- Intero
- iscrizione
- Ambiente
- Equivalente
- errore
- essenzialmente
- stimato
- eccetera
- Etere (ETH)
- Anche
- alla fine
- sempre crescente
- Ogni
- qualunque cosa
- di preciso
- esempio
- Tranne
- Eccitazione
- esecuzione
- esistere
- esistente
- uscita
- Uscita
- attenderti
- Spiegare
- Sfruttare
- esposto
- estendere
- extra
- estratto
- fatto
- falso
- affascinante
- Caratteristiche
- feedback
- meno
- lotta
- Compila il
- File
- filtro
- finale
- Infine
- Trovare
- ricerca
- trova
- sottile
- Nome
- fisso
- Focus
- seguito
- i seguenti
- Nel
- modulo
- formalmente
- formato
- imminente
- essere trovato
- quattro
- Gratis
- da
- pieno
- completamente
- function
- funzioni
- ulteriormente
- futuro
- genera
- ELETTRICA
- ottenere
- ottenere
- GitHub
- Dare
- dato
- dà
- Dare
- Go
- va
- andando
- buono
- Enti Pubblici
- afferrare
- grande
- di garanzia
- ha avuto
- Maniglie
- successo
- Happening
- accade
- Hard
- Avere
- avendo
- mal di testa
- pesante
- altezza
- qui
- HEX
- alto livello
- superiore
- Visualizzazioni
- tenere
- Foro
- speranza
- ORE
- librarsi
- Come
- Tutorial
- HTTPS
- caccia
- i
- identificatore
- if
- subito
- importante
- in
- inclusi
- Compreso
- informazioni
- informati
- invece
- interessato
- Intermedio
- Internet
- ai miglioramenti
- sicurezza
- IT
- SUO
- stessa
- gergo
- giugno
- ad appena
- solo uno
- mantenere
- Le
- Sapere
- conosciuto
- Coreano
- Lingua
- Le Lingue
- laptop
- Cognome
- dopo
- portare
- Leads
- perdita
- Perdite
- IMPARARE
- apprendimento
- meno
- partenza
- a sinistra
- Lunghezza
- lettera
- Biblioteca
- Vita
- piace
- probabile
- Limitato
- linea
- Linee
- Lista
- elencati
- ll
- caricare
- locale
- località
- registrazione
- Lunghi
- a lungo termine
- più a lungo
- Guarda
- una
- guardò
- cerca
- SEMBRA
- lotto
- fortuna
- mantiene
- make
- il malware
- gestire
- gestito
- gestione
- direttore
- gestisce
- Manipolazione
- molti
- Margine
- marchio
- marcatore
- Mastercard
- partita
- corrispondenza
- max-width
- Maggio..
- si intende
- Memorie
- menzionato
- Microsoft
- forza
- verbale
- modesto
- modificato
- modificare
- momento
- monitoraggio
- Scopri di più
- maggior parte
- molti
- multiplo
- pulito
- Bisogno
- di applicazione
- rete
- mai
- tuttavia
- New
- notizie
- GENERAZIONE
- bello
- no
- normale
- Niente
- Avviso..
- adesso
- numero
- numeri
- oggetto
- ovvio
- of
- MENO
- ufficiale
- Ufficialmente
- offset
- Vecchio
- on
- una volta
- ONE
- esclusivamente
- open source
- operativo
- sistema operativo
- sistemi operativi
- operatore
- Opzione
- or
- minimo
- i
- Altro
- Altri
- altrimenti
- nostro
- noi stessi
- su
- produzione
- ancora
- complessivo
- proprio
- pagina
- Panico
- parte
- Password
- gestore di password
- Le password
- sentiero
- Cartamodello
- Paul
- pausa
- Paga le
- per cento
- Forse
- periodo
- permanentemente
- cronologia
- dati personali
- Fisico
- immagine
- pezzi
- posto
- segnaposto
- Peste
- Platone
- Platone Data Intelligence
- PlatoneDati
- Abbondanza
- punto
- punti
- Popolare
- posizione
- possibile
- Post
- potenziale
- precisamente
- presenti
- piuttosto
- prevenire
- precedente
- prezzo
- Stampa
- stampe
- probabilmente
- problemi
- processi
- Programma
- Programmatore
- I programmatori
- Programmazione
- Programmi
- pronunciato
- metti
- Python
- Domande e risposte
- domanda
- solleva
- RAM
- casuale
- gamma
- piuttosto
- Crudo
- dati grezzi
- RE
- a raggiunto
- Leggi
- Lettura
- pronto
- di rose
- vita reale
- tempo reale
- veramente
- riconoscere
- Recuperare
- recupero
- relazionato
- rimanente
- ricorda
- a distanza
- rimuovere
- ripetere
- ripetuto
- RIPETUTAMENTE
- rapporto
- rappresentato
- rispetto
- rispettivamente
- REST
- Risultati
- ritorno
- di ritorno
- rivelare
- Rid
- destra
- Rischio
- rischi
- Prenotazione sale
- Correre
- running
- monitoraggio del tempo di esecuzione
- s
- sicura
- più sicuro
- stesso
- soddisfatte
- Risparmi
- detto
- scansione
- sparpagliato
- Scene
- Cerca
- ricerca
- Secondo
- secondo
- Segreto
- Sezione
- sicuro
- problemi di
- vedere
- seme
- vedendo
- sembrare
- visto
- vede
- Serie
- grave
- set
- regolazione
- Corti
- In breve
- dovrebbero
- mostrare attraverso le sue creazioni
- mostrato
- segno
- Segni
- simile
- Allo stesso modo
- Un'espansione
- semplificata
- semplificare
- semplicemente
- singolo
- Taglia
- sonno
- piccole
- Subdolo
- snooping
- So
- Software
- solido
- alcuni
- qualcosa
- Arrivo
- Fonte
- codice sorgente
- lo spazio
- la nostra speciale
- appositamente
- specificato
- velocità
- Stelle
- inizia a
- iniziato
- Di partenza
- inizio
- startup
- Ancora
- stola
- Fermare
- fermato
- Tornare al suo account
- memorizzati
- Storia
- Corda
- forte
- Studio
- Con successo
- tale
- sufficiente
- suppone
- sorpresa
- sorpreso
- sorprendente
- sopravvivere
- SVG
- swap
- sistema
- SISTEMI DI TRATTAMENTO
- Fai
- preso
- prende
- presa
- parlando
- tecnicamente
- tecniche
- temporaneo
- test
- test
- di
- che
- I
- L’ORIGINE
- loro
- Li
- si
- poi
- teoria
- Là.
- perciò
- di
- cosa
- think
- questo
- quelli
- anche se?
- pensiero
- tempo
- Titolo
- a
- insieme
- ha preso
- top
- pista
- Tracking
- transizione
- trasparente
- vero
- prova
- Turned
- seconda
- Digitare
- tipicamente
- capire
- unicode
- fino a quando
- non usato
- non desiderato
- Aggiornanento
- aggiornato
- URL
- us
- noi governo
- Impiego
- usb
- uso
- uso-dopo-libero
- utilizzato
- Utente
- usa
- utilizzando
- utilità
- APPREZZIAMO
- Valori
- varietà
- verificare
- versione
- molto
- via
- vulnerabilità
- W
- aspettare
- In attesa
- volere
- ricercato
- Prima
- Orologio
- Modo..
- modi
- we
- Settimane
- WELL
- sono stati
- Che
- quando
- se
- quale
- while
- OMS
- chiunque
- tutto
- perché
- volere
- vincere
- finestre
- Pulire
- con
- senza
- chiedendosi
- parole
- Lavora
- lavorato
- lavoro
- lavori
- preoccuparsi
- sarebbe
- darebbe
- scrivere
- scrittura
- scritto
- ancora
- Tu
- Trasferimento da aeroporto a Sharm
- te stesso
- zefiro
- zero