Selama dua minggu terakhir, kami telah melihat serangkaian artikel yang membicarakan apa yang digambarkan sebagai "master password crack" di pengelola kata sandi sumber terbuka KeePass yang populer.
Bug tersebut dianggap cukup penting untuk mendapatkan pengenal resmi pemerintah AS (dikenal sebagai CVE-2023-32784, jika Anda ingin memburunya), dan mengingat kata sandi utama untuk pengelola kata sandi Anda adalah kunci untuk seluruh kastil digital Anda, Anda dapat memahami mengapa cerita tersebut memicu banyak kegembiraan.
Kabar baiknya adalah bahwa penyerang yang ingin mengeksploitasi bug ini hampir pasti harus sudah menginfeksi komputer Anda dengan malware, dan karena itu akan dapat memata-matai penekanan tombol Anda dan menjalankan program.
Dengan kata lain, bug tersebut dapat dianggap sebagai risiko yang mudah dikelola hingga pembuat KeePass mengeluarkan pembaruan, yang akan segera muncul (tampaknya pada awal Juni 2023).
Sebagai pengungkap bug berhati-hati menunjukkan:
Jika Anda menggunakan enkripsi disk penuh dengan kata sandi yang kuat dan sistem Anda [bebas dari malware], Anda akan baik-baik saja. Tidak ada yang bisa mencuri kata sandi Anda dari jarak jauh melalui internet hanya dengan temuan ini.
Risikonya dijelaskan
Diringkas dengan berat, bug bermuara pada kesulitan memastikan bahwa semua jejak data rahasia dihapus dari memori setelah Anda selesai menggunakannya.
Kami akan mengabaikan di sini masalah bagaimana menghindari data rahasia di memori sama sekali, bahkan secara singkat.
Dalam artikel ini, kami hanya ingin mengingatkan pemrogram di mana pun bahwa kode disetujui oleh peninjau yang sadar akan keamanan dengan komentar seperti "tampaknya dibersihkan dengan benar setelah dirinya sendiri"...
…mungkin sebenarnya tidak dibersihkan sama sekali, dan potensi kebocoran data mungkin tidak terlihat jelas dari studi langsung kode itu sendiri.
Sederhananya, kerentanan CVE-2023-32784 berarti kata sandi master KeePass dapat dipulihkan dari data sistem bahkan setelah program KeyPass keluar, karena informasi yang memadai tentang kata sandi Anda (walaupun sebenarnya bukan kata sandi mentah itu sendiri, yang akan kami fokuskan pada suatu saat) mungkin tertinggal dalam swap sistem atau file tidur, di mana memori sistem yang dialokasikan mungkin akhirnya disimpan untuk nanti.
Pada komputer Windows di mana BitLocker tidak digunakan untuk mengenkripsi hard disk saat sistem dimatikan, ini akan memberikan kesempatan bagi penjahat yang mencuri laptop Anda untuk melakukan booting dari drive USB atau CD, dan bahkan memulihkan kata sandi utama Anda meskipun program KeyPass sendiri berhati-hati untuk tidak menyimpannya secara permanen ke disk.
Kebocoran kata sandi jangka panjang dalam memori juga berarti bahwa kata sandi, secara teori, dapat dipulihkan dari dump memori program KeyPass, bahkan jika dump itu diambil lama setelah Anda mengetikkan kata sandi, dan lama setelah KeePass sendiri tidak perlu lagi menyimpannya.
Jelas, Anda harus berasumsi bahwa malware yang sudah ada di sistem Anda dapat memulihkan hampir semua kata sandi yang diketikkan melalui berbagai teknik pengintaian waktu nyata, selama mereka aktif pada saat Anda mengetik. Tetapi Anda mungkin berharap bahwa waktu Anda terpapar bahaya akan terbatas pada waktu mengetik yang singkat, tidak diperpanjang hingga beberapa menit, jam atau hari setelahnya, atau mungkin lebih lama, termasuk setelah Anda mematikan komputer.
Apa yang tertinggal?
Oleh karena itu, kami berpikir untuk melihat tingkat tinggi bagaimana data rahasia dapat tertinggal di memori dengan cara yang tidak langsung terlihat dari kode.
Jangan khawatir jika Anda bukan seorang programmer – kami akan tetap sederhana, dan menjelaskan sambil jalan.
Kita akan mulai dengan melihat penggunaan dan pembersihan memori dalam program C sederhana yang mensimulasikan memasukkan dan menyimpan kata sandi untuk sementara dengan melakukan hal berikut:
- Mengalokasikan sepotong memori khusus khusus untuk menyimpan password.
- Memasukkan string teks yang dikenal sehingga kita dapat dengan mudah menemukannya di memori jika diperlukan.
- Menambahkan 16 karakter ASCII 8-bit pseudo-random dari jangkauan AP.
- Mencetak penyangga kata sandi yang disimulasikan.
- Membebaskan memori dengan harapan menghapus buffer kata sandi.
- Keluar program.
Sangat disederhanakan, kode C mungkin terlihat seperti ini, tanpa pemeriksaan kesalahan, menggunakan nomor pseudo-random berkualitas buruk dari fungsi runtime C rand()
, dan mengabaikan pemeriksaan buffer overflow (jangan pernah melakukan semua ini dalam kode asli!):
// 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);
Nyatanya, kode yang akhirnya kami gunakan dalam pengujian menyertakan beberapa bagian tambahan yang ditunjukkan di bawah ini, sehingga kami dapat membuang seluruh konten buffer kata sandi sementara saat kami menggunakannya, untuk mencari konten yang tidak diinginkan atau sisa.
Perhatikan bahwa kami sengaja membuang buffer setelah menelepon free()
, yang secara teknis merupakan bug yang dapat digunakan setelah bebas, tetapi kami melakukannya di sini sebagai cara licik untuk melihat apakah ada hal penting yang tertinggal setelah mengembalikan buffer kami, yang dapat menyebabkan lubang kebocoran data yang berbahaya di kehidupan nyata.
Kami juga memasukkan dua Waiting for [Enter]
perintah ke dalam kode untuk memberi diri kita kesempatan untuk membuat dump memori pada poin-poin penting dalam program, memberi kita data mentah untuk dicari nanti, untuk melihat apa yang tertinggal saat program berjalan.
Untuk melakukan dump memori, kami akan menggunakan Microsoft alat Sysinternals procdump
pada pengatur terkenal. Pengatur ini menawarkan bantuan hukum kepada traderapabila trader berselisih dengan broker yang terdaftar dengan mereka. -ma
pilihan (membuang semua memori), yang menghindari kebutuhan untuk menulis kode kita sendiri untuk menggunakan Windows DbgHelp
sistem dan agak rumit MiniDumpXxxx()
fungsi.
Untuk mengompilasi kode C, kami menggunakan build kecil dan sederhana milik Fabrice Bellard yang gratis dan bersumber terbuka Kompiler C Kecil, tersedia untuk Windows 64-bit di sumber dan bentuk biner langsung dari halaman GitHub kami.
Teks salin dan tempel dari semua kode sumber yang digambarkan dalam artikel muncul di bagian bawah halaman.
Inilah yang terjadi ketika kami menyusun dan menjalankan program pengujian:
C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Tiny C Compiler - Hak Cipta (C) 2001-2023 Fabrice Bellard Dilucuti oleh Paul Ducklin untuk digunakan sebagai alat pembelajaran Versi petcc64-0.9.27 [0006] - Menghasilkan 64-bit Hanya 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 -------- ----------------------- bagian ukuran file virt 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 byte) C:UsersduckKEYPASS> unl1.exe Membuang buffer 'baru' di awal 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 batang32cmd. exe.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E data sungai=C:Menang 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 bersih ExplzV.< K. 51390 75 6E 6 69 6 65 EJJCPOMDJHAN.eD 6F79B74 : 65 78 74 4 48 4 4 00 513 0D 45 4A 4C 43 50 4E data sungai=C:Menang 4F44C4: 48 41F 4 00 65C 00 44 00 513 0 72D 69 76 65C 72 44 dowsSystem61 74Dr 61F3D43: 3 5 57 69 6 00C 513 0 64 6 77 73 5 53 79 73 iversDriverData 74F65E6: 33 32 5 44 72F 32 00 513 0 69D 76 65 72 73 5 44F .EFC_72=69.FPS_ 76F65F72: 44 61 74F 61 00 513 0 00F 45 46 43 5F 34 33 37F 32 BROWSER_APP_PROF 3F31: 00 46C 50 53F 5 4372 1 00 513E 0 42D 52 4E 57 53 45 ILE_STRING=Inter 52F5: 41E 50 50 5 50 52 4 46C 00A 51400 F49 4C AC 45B 5 53 net ExplzV.<.K.. Menunggu [ENTER] untuk membebaskan buffer... Membuang buffer setelah free() 54F52: A49 4 F47 3 49 6 74 65 72 00 F51410 6 65 74 20 45 .g......P...... .78F70A6: 7 56A 4A 3 4 00F 00D 00 51390A 0 67 5E 00 00 00 00 00 50 01E riverData=C:Menang 5F00C00: 00 00F 00 00 513C 0 45 4 4 43 50D 4 4 44C 4 48 dowsSystem41Dr 4F00D65: 00 44 00 513 0 72C 69 76 65 72 44 61 74 61 3 43 iversDriverData 3F5E57 : 69 6 00 513 0F 64 6 77 73 5D 53 79 73 74 65 6F .EFC_33=32.FPS_ 5F44F72: 32 00 513F 0 69 76 65 72F 73 5 44 72F 69 76 65F 72 BROWSER_APP_PROF 44F61: 74 61C 00 513F 0 00 45 46 43 E 5 34D 33 37E 32 3 31 ILE_STRING=Antar 00F46: 50E 53 5 4372 1 00 513 0C 42D 52 4 57D AC 53B 45 52 net ExplM..MK. Menunggu [ENTER] keluar dari main()... C:UsersduckKEYPASS>
Dalam proses ini, kami tidak repot mengambil dump memori proses apa pun, karena kami dapat langsung melihat dari output bahwa kode ini membocorkan data.
Tepat setelah memanggil fungsi perpustakaan runtime Windows C malloc()
, kita dapat melihat bahwa buffer yang kita dapatkan kembali menyertakan apa yang tampak seperti data variabel lingkungan yang tersisa dari kode startup program, dengan 16 byte pertama tampaknya diubah agar terlihat seperti semacam header alokasi memori sisa.
(Perhatikan bagaimana 16 byte itu terlihat seperti dua alamat memori 8-byte, 0xF55790
dan 0xF50150
, yang masing-masing tepat setelah dan tepat sebelum buffer memori kita sendiri.)
Ketika kata sandi seharusnya ada di memori, kita dapat melihat seluruh string dengan jelas di buffer, seperti yang kita harapkan.
Tapi setelah menelepon free()
, perhatikan bagaimana 16 byte pertama dari buffer kami telah ditulis ulang dengan apa yang tampak seperti alamat memori terdekat sekali lagi, mungkin agar pengalokasi memori dapat melacak blok dalam memori yang dapat digunakan kembali…
… tetapi sisa teks kata sandi "dihapus" kami (12 karakter acak terakhir EJJCPOMDJHAN
) telah tertinggal.
Kita tidak hanya perlu mengelola alokasi dan de-alokasi memori kita sendiri di C, kita juga perlu memastikan bahwa kita memilih fungsi sistem yang tepat untuk buffer data jika kita ingin mengontrolnya dengan tepat.
Misalnya, dengan beralih ke kode ini, kita mendapatkan sedikit lebih banyak kontrol atas apa yang ada di memori:
Dengan beralih dari malloc()
dan free()
untuk menggunakan fungsi alokasi Windows tingkat rendah VirtualAlloc()
dan VirtualFree()
langsung, kita mendapatkan kontrol yang lebih baik.
Namun, kami membayar harga dalam kecepatan, karena setiap panggilan ke VirtualAlloc()
melakukan lebih banyak pekerjaan daripada panggilan malloc()
, yang bekerja dengan terus membagi dan membagi lagi blok memori tingkat rendah yang telah dialokasikan sebelumnya.
Menggunakan VirtualAlloc()
berulang kali untuk blok kecil juga menggunakan lebih banyak memori secara keseluruhan, karena setiap blok di-discharge oleh VirtualAlloc()
biasanya menggunakan kelipatan memori 4KB (atau 2MB, jika Anda menggunakan apa yang disebut halaman memori besar), sehingga buffer 128-byte kita di atas dibulatkan menjadi 4096 byte, membuang 3968 byte di akhir blok memori 4KB.
Namun, seperti yang Anda lihat, memori yang kami dapatkan kembali secara otomatis dikosongkan (diatur ke nol), jadi kami tidak dapat melihat apa yang ada di sana sebelumnya, dan kali ini program mogok saat kami mencoba menggunakan setelah bebas. trik, karena Windows mendeteksi bahwa kami mencoba mengintip memori yang tidak lagi kami miliki:
C:UsersduckKEYPASS> unl2 Membuang buffer 'baru' di awal 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 ................ String lengkap adalah: tidak mungkinteksIBIPJPPHEOPOIDLL 0000EA75: 6 6E 69C 6 65B 6 79C 74 65 78 74 49 42 49 50 0000000000 tidak mungkinteksIBIP 0010EA4: 50A 50 48 45 4 50F 4 49F 44 4 4C 00C 00 00 00 0000000000 JPPHEOPOIDLL .... 0020AA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000A0030 : 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 ............. ... Menunggu [ENTER] untuk membebaskan buffer... Membuang buffer setelah free() 0000000000EA0000: [Program dihentikan di sini karena Windows menangkap penggunaan setelah bebas kami]
Karena memori yang kita bebaskan perlu dialokasikan kembali VirtualAlloc()
sebelum dapat digunakan lagi, kita dapat berasumsi bahwa itu akan dikosongkan sebelum didaur ulang.
Namun, jika kami ingin memastikannya kosong, kami dapat memanggil fungsi khusus Windows RtlSecureZeroMemory()
sebelum membebaskannya, untuk menjamin bahwa Windows akan menulis nol ke buffer kita terlebih dahulu.
Fungsi terkait RtlZeroMemory()
, jika Anda bertanya-tanya, melakukan hal serupa, tetapi tanpa jaminan untuk benar-benar berfungsi, karena kompiler diizinkan untuk menghapusnya secara teoritis berlebihan jika mereka melihat bahwa buffer tidak digunakan lagi setelahnya.
Seperti yang Anda lihat, kita perlu sangat berhati-hati untuk menggunakan fungsi Windows yang tepat jika kita ingin meminimalkan waktu penyimpanan rahasia di memori untuk nanti.
Pada artikel ini, kita tidak akan melihat bagaimana Anda mencegah rahasia disimpan secara tidak sengaja ke file swap Anda dengan menguncinya ke dalam RAM fisik. (Petunjuk: VirtualLock()
sebenarnya tidak cukup dengan sendirinya.) Jika Anda ingin tahu lebih banyak tentang keamanan memori Windows tingkat rendah, beri tahu kami di komentar dan kami akan melihatnya di artikel mendatang.
Menggunakan manajemen memori otomatis
Salah satu cara yang rapi untuk menghindari keharusan mengalokasikan, mengelola, dan membatalkan alokasi memori sendiri adalah dengan menggunakan bahasa pemrograman yang dapat menangani malloc()
dan free()
, atau VirtualAlloc()
dan VirtualFree()
, secara otomatis.
Bahasa scripting seperti Perl, Ular sanca, Lua, JavaScript dan lainnya menyingkirkan bug keamanan memori paling umum yang mengganggu kode C dan C++, dengan melacak penggunaan memori untuk Anda di latar belakang.
Seperti yang kami sebutkan sebelumnya, contoh kode C kami yang ditulis dengan buruk di atas berfungsi dengan baik sekarang, tetapi hanya karena itu masih merupakan program yang sangat sederhana, dengan struktur data ukuran tetap, di mana kami dapat memverifikasi dengan inspeksi bahwa kami tidak akan menimpa 128- buffer byte, dan hanya ada satu jalur eksekusi yang dimulai dengan malloc()
dan diakhiri dengan yang sesuai free()
.
Tetapi jika kami memperbaruinya untuk memungkinkan pembuatan kata sandi dengan panjang variabel, atau menambahkan fitur tambahan ke dalam proses pembuatan, maka kami (atau siapa pun yang mempertahankan kode selanjutnya) dapat dengan mudah berakhir dengan buffer overflows, bug yang tidak digunakan setelahnya, atau memori yang tidak pernah dibebaskan dan karena itu meninggalkan data rahasia lama setelah tidak lagi diperlukan.
Dalam bahasa seperti Lua, kita dapat membiarkan lingkungan run-time Lua, yang melakukan apa yang dikenal dalam jargon sebagai pengumpulan sampah otomatis, berurusan dengan memperoleh memori dari sistem, dan mengembalikannya ketika mendeteksi kami telah berhenti menggunakannya.
Program C yang kami cantumkan di atas menjadi jauh lebih sederhana ketika alokasi memori dan de-alokasi diurus untuk kami:
Kami mengalokasikan memori untuk menyimpan string s
hanya dengan menetapkan string 'unlikelytext'
untuk itu.
Kita nantinya dapat mengisyaratkan kepada Lua secara eksplisit bahwa kita tidak lagi tertarik s
dengan menetapkan nilainya nil
(semua nils
pada dasarnya adalah objek Lua yang sama), atau berhenti menggunakan s
dan menunggu Lua untuk mendeteksi bahwa itu tidak diperlukan lagi.
Either way, memori yang digunakan oleh s
akhirnya akan pulih secara otomatis.
Dan untuk mencegah buffer overflows atau salah urus ukuran ketika menambahkan string teks (operator Lua ..
, diucapkan pertemuan, dasarnya menambahkan dua string bersama-sama, seperti +
dengan Python), setiap kali kita memperpanjang atau memperpendek string, Lua secara ajaib mengalokasikan ruang untuk string baru, daripada memodifikasi atau mengganti yang asli di lokasi memori yang ada.
Pendekatan ini lebih lambat, dan mengarah ke puncak penggunaan memori yang lebih tinggi daripada yang Anda dapatkan di C karena string perantara yang dialokasikan selama manipulasi teks, tetapi jauh lebih aman sehubungan dengan buffer overflows.
Tetapi manajemen string otomatis semacam ini (dikenal dalam jargon sebagai kekekalan, karena string tidak pernah didapat bermutasi, atau dimodifikasi di tempat, setelah dibuat), memang membawa sakit kepala keamanan siber baru dengan sendirinya.
Kami menjalankan program Lua di atas pada Windows, hingga jeda kedua, tepat sebelum program keluar:
C:UsersduckKEYPASS> lua s1.lua String lengkapnya adalah: tidak mungkinteksHLKONBOJILAGLNLN Menunggu [ENTER] sebelum membebaskan string... Menunggu [ENTER] sebelum keluar...
Kali ini, kami mengambil dump memori proses, seperti ini:
C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - utilitas dump proses Sysinternals Hak Cipta (C) 2009-2022 Mark Russinovich dan Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Dump 1 diinisiasi: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Tulisan Dump 1: Perkiraan ukuran file dump adalah 10 MB. [00:00:00] Dump 1 selesai: 10 MB ditulis dalam 0.1 detik [00:00:01] Jumlah dump tercapai.
Kemudian kami menjalankan skrip sederhana ini, yang membaca kembali file dump, menemukan di mana-mana di memori bahwa string yang diketahui unlikelytext
muncul, dan mencetaknya, bersama dengan lokasinya di dumpfile dan karakter ASCII yang mengikutinya:
Bahkan jika Anda pernah menggunakan bahasa skrip sebelumnya, atau bekerja di ekosistem pemrograman apa pun yang menampilkan apa yang disebut string yang dikelola, di mana sistem melacak alokasi memori dan dealokasi untuk Anda, dan menanganinya sesuai keinginan…
… Anda mungkin terkejut melihat keluaran yang dihasilkan oleh pemindaian memori ini:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: teks tidak mungkinALJBNGOAPLLBDEB 006D8B3C: teks tidak mungkinALJBNGOA 006D8B7C: teks tidak mungkinALJBNGO 006D8BFC: teks tidak mungkinALJBNGOAPLLBDEBJ 006D8CBC: teks tidak mungkinALJBN 006D8D7C: tidak mungkin teksALJBNGOAP 006D903C: teks tidak mungkinALJBNGOAPL 006D90BC: teks tidak mungkinALJBNGOAPLL 006D90FC: teks tidak mungkinALJBNG 006D913C: teks tidak mungkinALJBNGOAPLLB 006D91BC: teks tidak mungkinALJB 006D91FC: teks tidak mungkinALJBNGOAPLLBD 006D923C : teks tidak mungkinALJBNGOAPLLBDE 006DB70C: teks tidak mungkinALJ 006DBB8C: teks tidak mungkinAL 006DBD0C: teks tidak mungkinA
Lihatlah, pada saat kami mengambil memory dump kami, meskipun kami telah selesai dengan string s
(dan memberi tahu Lua bahwa kami tidak membutuhkannya lagi dengan mengatakan s = nil
), semua string yang telah dibuat oleh kode tersebut masih ada di RAM, belum dipulihkan atau dihapus.
Memang, jika kita mengurutkan output di atas dengan string itu sendiri, daripada mengikuti urutan kemunculannya di RAM, Anda akan dapat membayangkan apa yang terjadi selama loop di mana kita menggabungkan satu karakter pada satu waktu ke string kata sandi kita:
C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: teks tidak mungkinA 006DBB8C: teks tidak mungkinAL 006DB70C: teks tidak mungkinALJ 006D91BC: teks tidak mungkinALJB 006D8CBC: teks tidak mungkinALJBN 006D90FC: teks tidak mungkinALJBNG 006D8B7C: teks tidak mungkinALJBNGO 006D8B3C: teks tidak mungkinALJBNGOA 006D8D 7C: teks tidak mungkinALJBNGOAP 006D903C: teks tidak mungkinALJBNGOAPL 006D90BC: teks tidak mungkinALJBNGOAPLL 006D913C: teks tidak mungkinALJBNGOAPLLB 006D91FC: teks tidak mungkinALJBNGOAPLLBD 006D923C: teks tidak mungkinALJBNGOAPLLBDE 006D8AFC: teks tidak mungkinALJBNGOAPLLBDEB 006D 8BFC : teks tidak mungkinALJBNGOAPLLBDEBJ
Semua string perantara sementara itu masih ada, jadi meskipun kita telah berhasil menghapus nilai akhir dari s
, kami masih membocorkan semuanya kecuali karakter terakhirnya.
Kenyataannya, dalam kasus ini, bahkan ketika kita dengan sengaja memaksa program kita membuang semua data yang tidak diperlukan dengan memanggil fungsi khusus Lua collectgarbage()
(sebagian besar bahasa skrip memiliki sesuatu yang serupa), sebagian besar data dalam string sementara yang mengganggu itu tetap ada di dalam RAM, karena kami telah mengkompilasi Lua untuk melakukan manajemen memori otomatisnya menggunakan yang lama yang baik malloc()
dan free()
.
Dengan kata lain, bahkan setelah Lua sendiri mengambil kembali blok memori sementaranya untuk menggunakannya lagi, kami tidak dapat mengontrol bagaimana atau kapan blok memori itu akan digunakan kembali, dan dengan demikian berapa lama mereka akan berada di dalam proses dengan tangan kiri mereka. atas data yang menunggu untuk diendus, dibuang, atau dibocorkan.
Masukkan .NET
Tapi bagaimana dengan KeePass, dari mana artikel ini dimulai?
KeePass ditulis dalam C#, dan menggunakan runtime .NET, sehingga menghindari masalah salah urus memori yang dibawa oleh program C…
…tetapi C# mengelola string teksnya sendiri, seperti yang dilakukan Lua, yang menimbulkan pertanyaan:
Bahkan jika pemrogram menghindari menyimpan seluruh kata sandi utama di satu tempat setelah dia selesai dengannya, dapatkah penyerang dengan akses ke dump memori tetap menemukan cukup data sementara yang tersisa untuk ditebak atau memulihkan kata sandi utama, bahkan jika itu penyerang mendapat akses ke komputer Anda beberapa menit, jam, atau hari setelah Anda mengetikkan kata sandi?
Sederhananya, apakah ada sisa-sisa kata sandi utama Anda yang dapat terdeteksi dan samar-samar yang bertahan di RAM, bahkan setelah Anda mengharapkannya dihapus?
Mengganggu, sebagai pengguna Github Vdohney ditemukan, jawabannya (setidaknya untuk versi KeePass sebelum 2.54) adalah, “Ya.”
Untuk lebih jelasnya, menurut kami kata sandi utama Anda yang sebenarnya tidak dapat dipulihkan sebagai string teks tunggal dari dump memori KeePass, karena penulis membuat fungsi khusus untuk entri kata sandi utama yang keluar dari caranya untuk menghindari penyimpanan penuh kata sandi di tempat yang mudah ditemukan dan diendus.
Kami puas dengan ini dengan menyetel kata sandi utama kami ke SIXTEENPASSCHARS
, mengetiknya, dan kemudian mengambil memori dump segera, segera, dan lama setelahnya.
Kami mencari dump dengan skrip Lua sederhana yang mencari teks kata sandi itu di mana saja, baik dalam format ASCII 8-bit, dan dalam format UTF-16 (Windows widechar) 16-bit, seperti ini:
Hasilnya menggembirakan:
C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Membaca di file dump... SELESAI. Mencari SIXTEENPASSCHARS sebagai ASCII 8-bit... tidak ditemukan. Mencari SIXTEENPASSCHARS sebagai UTF-16... tidak ditemukan.
Namun Vdohney, penemu CVE-2023-32784, memperhatikan bahwa saat Anda mengetikkan kata sandi utama, KeePass memberi Anda umpan balik visual dengan membuat dan menampilkan string placeholder yang terdiri dari karakter “gumpalan” Unicode, hingga dan termasuk panjang karakter Anda. kata sandi:
Dalam string teks widechar pada Windows (yang terdiri dari dua byte per karakter, bukan hanya satu byte masing-masing seperti pada ASCII), karakter "gumpalan" dikodekan dalam RAM sebagai hex byte 0xCF
diikuti oleh 0x25
(yang kebetulan merupakan tanda persen di ASCII).
Jadi, bahkan jika KeePass sangat berhati-hati dengan karakter mentah yang Anda ketikkan saat memasukkan kata sandi itu sendiri, Anda mungkin berakhir dengan string karakter "gumpalan" yang tersisa, mudah dideteksi dalam memori sebagai pengulangan berulang seperti CF25CF25
or CF25CF25CF25
...
… dan, jika demikian, karakter gumpalan terpanjang yang Anda temukan mungkin akan memberikan panjang kata sandi Anda, yang akan menjadi bentuk sederhana dari kebocoran informasi kata sandi, jika tidak ada yang lain.
Kami menggunakan skrip Lua berikut untuk mencari tanda string placeholder kata sandi sisa:
Hasilnya mengejutkan (kami telah menghapus baris berurutan dengan jumlah blob yang sama, atau dengan blob lebih sedikit dari baris sebelumnya, untuk menghemat ruang):
C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ berlanjut dengan cara yang sama untuk 8 blob, 9 blob, dst. ] [hingga dua baris terakhir masing-masing tepat 16 blob] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [semua kecocokan yang tersisa panjangnya satu gumpalan] 0123B058: *
Pada alamat memori yang berdekatan tetapi terus meningkat, kami menemukan daftar sistematis 3 blob, lalu 4 blob, dan seterusnya hingga 16 blob (panjang kata sandi kami), diikuti oleh banyak contoh string blob tunggal yang tersebar secara acak .
Jadi, string "gumpalan" placeholder itu memang tampaknya bocor ke dalam memori dan tertinggal untuk membocorkan panjang kata sandi, lama setelah perangkat lunak KeePass selesai dengan kata sandi utama Anda.
Langkah selanjutnya
Kami memutuskan untuk menggali lebih jauh, seperti yang dilakukan Vdohney.
Kami mengubah kode pencocokan pola kami untuk mendeteksi rangkaian karakter blob yang diikuti oleh karakter ASCII tunggal mana pun dalam format 16-bit (karakter ASCII direpresentasikan dalam UTF-16 sebagai kode ASCII 8-bit biasa, diikuti dengan byte nol).
Kali ini, untuk menghemat ruang, kami telah menekan keluaran untuk kecocokan apa pun yang sama persis dengan yang sebelumnya:
Kejutan kejutan:
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
Lihat apa yang kami dapatkan dari wilayah memori string terkelola .NET!
Sekumpulan "string blob" sementara yang berisi karakter berurutan dalam kata sandi kita, dimulai dengan karakter kedua.
String yang bocor itu diikuti oleh pencocokan karakter tunggal yang didistribusikan secara luas yang kami asumsikan muncul secara kebetulan. (File dump KeePass berukuran sekitar 250MB, jadi ada banyak ruang untuk karakter "gumpalan" muncul seolah-olah karena keberuntungan.)
Bahkan jika kita mempertimbangkan empat kecocokan ekstra tersebut, alih-alih membuangnya sebagai kemungkinan ketidakcocokan, kita dapat menebak bahwa kata sandi utama adalah salah satu dari:
?IXTENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS
Jelas, teknik sederhana ini tidak menemukan karakter pertama dalam kata sandi, karena "string blob" pertama hanya dibuat setelah karakter pertama diketikkan.
Perhatikan bahwa daftar ini bagus dan singkat karena kami memfilter kecocokan yang tidak diakhiri dengan karakter ASCII.
Jika Anda mencari karakter dalam rentang yang berbeda, seperti karakter Cina atau Korea, Anda mungkin mendapatkan lebih banyak hit yang tidak disengaja, karena ada lebih banyak kemungkinan karakter untuk dicocokkan…
... tetapi kami menduga Anda akan cukup dekat dengan kata sandi utama Anda, dan "string blob" yang terkait dengan kata sandi tampaknya dikelompokkan bersama dalam RAM, mungkin karena mereka dialokasikan pada waktu yang hampir bersamaan oleh bagian yang sama dari waktu proses .NET.
Dan di sana, singkatnya diakui panjang dan diskursif, adalah kisah menarik dari CVE-2023-32784.
Apa yang harus dilakukan?
- Jika Anda pengguna KeePass, jangan panik. Meskipun ini adalah bug, dan secara teknis merupakan kerentanan yang dapat dieksploitasi, penyerang jarak jauh yang ingin meretas kata sandi Anda menggunakan bug ini harus menanamkan malware di komputer Anda terlebih dahulu. Itu akan memberi mereka banyak cara lain untuk mencuri kata sandi Anda secara langsung, meskipun bug ini tidak ada, misalnya dengan mencatat penekanan tombol saat Anda mengetik. Pada titik ini, Anda cukup memperhatikan pembaruan yang akan datang, dan mengambilnya saat sudah siap.
- Jika Anda tidak menggunakan enkripsi full-disk, pertimbangkan untuk mengaktifkannya. Untuk mengekstrak kata sandi sisa dari file swap atau file hibernasi Anda (file disk sistem operasi yang digunakan untuk menyimpan konten memori sementara selama beban berat atau saat komputer Anda "tidur"), penyerang memerlukan akses langsung ke hard disk Anda. Jika Anda mengaktifkan BitLocker atau yang setara untuk sistem operasi lain, mereka tidak akan dapat mengakses file swap Anda, file hibernasi Anda, atau data pribadi lainnya seperti dokumen, spreadsheet, email yang disimpan, dan sebagainya.
- Jika Anda seorang programmer, beri tahu diri Anda tentang masalah manajemen memori. Jangan berasumsi bahwa hanya karena setiap
free()
cocok dengan yang sesuaimalloc()
bahwa data Anda aman dan dikelola dengan baik. Kadang-kadang, Anda mungkin perlu melakukan tindakan pencegahan ekstra untuk menghindari meninggalkan data rahasia di mana-mana, dan tindakan pencegahan tersebut sangat penting dari sistem operasi ke sistem operasi. - Jika Anda seorang penguji QA atau peninjau kode, selalu pikirkan "di balik layar". Bahkan jika kode manajemen memori terlihat rapi dan seimbang, waspadai apa yang terjadi di balik layar (karena pemrogram asli mungkin tidak mengetahuinya), dan bersiaplah untuk melakukan beberapa pekerjaan bergaya pentesting seperti pemantauan runtime dan memori dumping untuk memverifikasi bahwa kode aman benar-benar berfungsi sebagaimana mestinya.
KODE DARI PASAL: UNL1.C
#termasuk #termasuk #termasuk void hexdump(unsigned char* buff, int len) { // Cetak buffer dalam potongan 16-byte for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Tampilkan 16 byte sebagai nilai hex untuk (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Ulangi 16 byte tersebut sebagai karakter untuk (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) { // Memperoleh memori untuk menyimpan kata sandi, dan menunjukkan apa // yang ada di buffer saat secara resmi "baru"... char* buff = malloc(128); printf ("Membuang buffer 'baru' saat startn"); hexdump(buff,128); // Gunakan alamat buffer pseudorandom sebagai seed acak srand((unsigned)buff); // Mulai kata sandi dengan beberapa teks tetap yang dapat dicari strcpy(buff,"unlikelytext"); // Tambahkan 16 huruf pseudorandom, satu per satu untuk (int i = 1; i <= 16; i++) { // Pilih huruf dari A (65+0) hingga P (65+15) char ch = 65 + (rand() & 15); // Kemudian ubah string buff di tempat strncat(buff,&ch,1); } // Kata sandi lengkap sekarang ada di memori, jadi cetak // sebagai string, dan tampilkan seluruh buffer... printf("String penuh adalah: %sn",buff); hexdump(buff,128); // Jeda untuk membuang proses RAM sekarang (coba: 'procdump -ma') puts("Menunggu [ENTER] untuk membebaskan buffer..."); getchar(); // Secara formal bebaskan() memori dan tampilkan buffer // sekali lagi untuk melihat apakah ada yang tertinggal... free(buff); printf("Membuang buffer setelah bebas()n"); hexdump(buff,128); // Jeda untuk membuang RAM lagi untuk memeriksa perbedaan put("Menunggu [ENTER] keluar dari main()..."); getchar(); kembali 0; }
KODE DARI PASAL: UNL2.C
#termasuk #termasuk #termasuk #termasuk void hexdump(unsigned char* buff, int len) { // Cetak buffer dalam potongan 16-byte for (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +i); // Tampilkan 16 byte sebagai nilai hex untuk (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Ulangi 16 byte tersebut sebagai karakter untuk (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) { // Memperoleh memori untuk menyimpan kata sandi, dan menunjukkan apa // yang ada di buffer saat secara resmi "baru"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf ("Membuang buffer 'baru' saat startn"); hexdump(buff,128); // Gunakan alamat buffer pseudorandom sebagai seed acak srand((unsigned)buff); // Mulai kata sandi dengan beberapa teks tetap yang dapat dicari strcpy(buff,"unlikelytext"); // Tambahkan 16 huruf pseudorandom, satu per satu untuk (int i = 1; i <= 16; i++) { // Pilih huruf dari A (65+0) hingga P (65+15) char ch = 65 + (rand() & 15); // Kemudian ubah string buff di tempat strncat(buff,&ch,1); } // Kata sandi lengkap sekarang ada di memori, jadi cetak // sebagai string, dan tampilkan seluruh buffer... printf("String penuh adalah: %sn",buff); hexdump(buff,128); // Jeda untuk membuang proses RAM sekarang (coba: 'procdump -ma') puts("Menunggu [ENTER] untuk membebaskan buffer..."); getchar(); // Secara resmi bebaskan() memori dan tampilkan buffer // lagi untuk melihat apakah ada yang tertinggal... VirtualFree(buff,0,MEM_RELEASE); printf("Membuang buffer setelah bebas()n"); hexdump(buff,128); // Jeda untuk membuang RAM lagi untuk memeriksa perbedaan put("Menunggu [ENTER] keluar dari main()..."); getchar(); kembali 0; }
KODE DARI PASAL: S1.LUA
-- Mulailah dengan beberapa teks tetap yang dapat dicari s = 'unlikelytext' -- Tambahkan 16 karakter acak dari 'A' ke 'P' untuk i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('Full string is:',s,'n') -- Jeda untuk membuang proses RAM print('Menunggu [ENTER] sebelum membebaskan string...') io.read() - - Hapus string dan tandai variabel yang tidak digunakan s = nil -- Dump RAM lagi untuk mencari diffs print('Menunggu [ENTER] sebelum keluar...') io.read()
KODE DARI ARTIKEL: FINDIT.LUA
-- baca di file dump local f = io.open(arg[1],'rb'):read('*a') -- cari teks marker diikuti oleh satu -- atau lebih karakter ASCII acak local b,e ,m = 0,0,nil while true do -- cari kecocokan berikutnya dan ingat offset b,e,m = f:find('(unlikelytext[AZ]+)',e+1) -- keluar jika tidak ada lagi cocok jika tidak b maka break end -- laporkan posisi dan string yang ditemukan print(string.format('%08X: %s',b,m)) end
KODE DARI ARTIKEL: SEARCHKNOWN.LUA
io.write('Membaca di file dump... ') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Mencari SIXTEENPASSCHARS sebagai ASCII 8-bit... ') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 and 'FOUND' or 'not found','.n') io.write ('Mencari SIXTEENPASSCHARS sebagai UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 and 'FOUND' or 'not found','.n' )
KODE DARI ARTIKEL: FINDBLOBS.LUA
-- baca dalam file dump yang ditentukan pada baris perintah local f = io.open(arg[1],'rb'):read('*a') -- Cari satu atau lebih blob kata sandi, diikuti oleh non-blob -- Perhatikan bahwa blob chars (●) disandikan ke Windows widechars -- sebagai kode UTF-16 litte-endian, keluar sebagai CF 25 dalam hex. local b,e,m = 0,0,nil while true do -- Kami menginginkan satu atau lebih blob, diikuti oleh non-blob. -- Kita sederhanakan kodenya dengan mencari CF25 yang eksplisit -- diikuti dengan sembarang string yang hanya memiliki CF atau 25 di dalamnya, -- jadi kita akan menemukan CF25CFCF atau CF2525CF serta CF25CF25. -- Kami akan memfilter "false positive" nanti jika ada. -- Kita perlu menulis '%%' daripada x25 karena karakter x25 -- (tanda persen) adalah karakter pencarian khusus di Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- exit jika tidak ada lagi yang cocok jika tidak b maka break end -- CMD.EXE tidak dapat mencetak gumpalan, jadi kami mengubahnya menjadi bintang. print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) end
KODE DARI ARTIKEL: SEARCHKP.LUA
-- baca dalam file dump yang ditentukan pada baris perintah local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Sekarang, kita ingin satu atau lebih gumpalan (CF25) diikuti dengan kode -- untuk A..Z diikuti dengan 0 byte untuk mengubah ACSCII menjadi UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- exit ketika tidak ada lagi kecocokan jika tidak b maka break end -- CMD.EXE tidak dapat mencetak gumpalan, jadi kami mengonversinya menjadi bintang. -- Untuk menghemat ruang, kami menekan kecocokan berurutan jika m ~= p lalu print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m akhir akhir
- Konten Bertenaga SEO & Distribusi PR. Dapatkan Amplifikasi Hari Ini.
- PlatoAiStream. Kecerdasan Data Web3. Pengetahuan Diperkuat. Akses Di Sini.
- Mencetak Masa Depan bersama Adryenn Ashley. Akses Di Sini.
- Beli dan Jual Saham di Perusahaan PRE-IPO dengan PREIPO®. Akses Di Sini.
- Sumber: https://nakedsecurity.sophos.com/2023/05/31/serious-security-that-keepass-master-password-crack-and-what-we-can-learn-from-it/
- :memiliki
- :adalah
- :bukan
- :Di mana
- ][P
- $NAIK
- 1
- 10
- 12
- 15%
- 20
- 200
- 2023
- 24
- 250
- 27
- 31
- 3d
- 49
- 50
- 67
- 70
- 72
- 77
- 8
- 9
- a
- Sanggup
- Tentang Kami
- atas
- Mutlak
- AC
- mengakses
- Akun
- memperoleh
- mengakuisisi
- aktif
- sebenarnya
- sebenarnya
- menambahkan
- Tambahan
- alamat
- alamat
- Menambahkan
- Setelah
- setelah itu
- lagi
- Semua
- dialokasikan
- mengalokasikan
- alokasi
- alokasi
- mengizinkan
- sendirian
- sepanjang
- sudah
- juga
- diubah
- Meskipun
- selalu
- an
- dan
- Andrew
- menjawab
- Apa pun
- apa saja
- sesuatu yang kritis
- muncul
- Muncul
- pendekatan
- disetujui
- ADALAH
- sekitar
- artikel
- artikel
- AS
- At
- penulis
- mobil
- secara otomatis
- secara otomatis
- tersedia
- menghindari
- dihindari
- sadar
- jauh
- kembali
- latar belakang
- background-image
- BE
- karena
- menjadi
- menjadi
- sebelum
- Awal
- di belakang
- dibalik layar
- di bawah
- Lebih baik
- Bit
- Memblokir
- Blok
- batas
- kedua
- Bawah
- merek
- Merek Baru
- Istirahat
- secara singkat
- membawa
- penyangga
- buffer overflow
- Bug
- bug
- membangun
- tapi
- by
- C + +
- panggilan
- panggilan
- CAN
- Bisa Dapatkan
- yang
- kasus
- tertangkap
- CD
- pusat
- Pasti
- rantai
- kesempatan
- berubah
- karakter
- karakter
- memeriksa
- Cek
- Cina
- Pilih
- jelas
- Jelas
- Penyelesaian
- kode
- warna
- COM
- datang
- kedatangan
- komentar
- komentar
- Umum
- lengkap
- kompleks
- komputer
- Mempertimbangkan
- besar
- dianggap
- Terdiri dari
- membangun
- Konten
- isi
- terus-menerus
- terus
- kontrol
- mengubah
- hak cipta
- Sesuai
- bisa
- menutupi
- retak
- membuat
- dibuat
- pencipta
- kritis
- Keamanan cyber
- BAHAYA
- Berbahaya
- data
- kebocoran data
- Hari
- transaksi
- memutuskan
- dedicated
- dijelaskan
- MELAKUKAN
- perbedaan
- berbeda
- Kesulitan
- DIG
- digital
- langsung
- Akses langsung
- langsung
- Display
- menampilkan
- fitur
- do
- dokumen
- tidak
- Tidak
- melakukan
- dilakukan
- Dont
- turun
- mendorong
- dua
- membuang
- selama
- e
- setiap
- Terdahulu
- mudah
- ekosistem
- antara
- lain
- memungkinkan
- mendorong
- enkripsi
- akhir
- berakhir
- cukup
- memastikan
- memastikan
- Enter
- memasuki
- Seluruh
- masuk
- Lingkungan Hidup
- Setara
- kesalahan
- dasarnya
- diperkirakan
- dll
- Eter (ETH)
- Bahkan
- akhirnya
- terus meningkat
- Setiap
- segala sesuatu
- persis
- contoh
- Kecuali
- Kegembiraan
- eksekusi
- ada
- ada
- Exit
- Keluar
- mengharapkan
- Menjelaskan
- Mengeksploitasi
- terkena
- memperpanjang
- tambahan
- ekstrak
- fakta
- palsu
- sangat menarik
- Fitur
- umpan balik
- sedikit
- perkelahian
- File
- File
- menyaring
- terakhir
- Akhirnya
- Menemukan
- temuan
- menemukan
- akhir
- Pertama
- tetap
- Fokus
- diikuti
- berikut
- Untuk
- bentuk
- Secara formal
- format
- yg akan datang
- ditemukan
- empat
- Gratis
- dari
- penuh
- sepenuhnya
- fungsi
- fungsi
- lebih lanjut
- masa depan
- menghasilkan
- generasi
- mendapatkan
- mendapatkan
- GitHub
- Memberikan
- diberikan
- memberikan
- Pemberian
- Go
- Pergi
- akan
- baik
- Pemerintah
- merebut
- besar
- menjamin
- memiliki
- Menangani
- terjadi
- Kejadian
- Terjadi
- Sulit
- Memiliki
- memiliki
- sakit kepala
- berat
- tinggi
- di sini
- HEX
- tingkat tinggi
- lebih tinggi
- Hits
- memegang
- Lubang
- berharap
- JAM
- melayang-layang
- Seterpercayaapakah Olymp Trade? Kesimpulan
- How To
- HTTPS
- berburu
- i
- identifier
- if
- segera
- penting
- in
- termasuk
- Termasuk
- informasi
- informasi
- sebagai gantinya
- tertarik
- Menengah
- Internet
- ke
- masalah
- IT
- NYA
- Diri
- jargon
- Juni
- hanya
- hanya satu
- Menjaga
- kunci
- Tahu
- dikenal
- Korea
- bahasa
- Bahasa
- laptop
- Terakhir
- kemudian
- memimpin
- Memimpin
- bocor
- kebocoran
- BELAJAR
- pengetahuan
- paling sedikit
- meninggalkan
- meninggalkan
- Panjang
- surat
- Perpustakaan
- Hidup
- 'like'
- Mungkin
- Terbatas
- baris
- baris
- Daftar
- Daftar
- ll
- memuat
- lokal
- tempat
- penebangan
- Panjang
- jangka panjang
- lagi
- melihat
- terlihat seperti
- tampak
- mencari
- TERLIHAT
- Lot
- keberuntungan
- mempertahankan
- membuat
- malware
- mengelola
- berhasil
- pengelolaan
- manajer
- mengelola
- manipulasi
- banyak
- Margin
- tanda
- penanda
- menguasai
- Cocok
- sesuai
- max-width
- Mungkin..
- cara
- Memori
- tersebut
- Microsoft
- mungkin
- menit
- sederhana
- dimodifikasi
- memodifikasi
- saat
- pemantauan
- lebih
- paling
- banyak
- beberapa
- Rapi
- Perlu
- dibutuhkan
- bersih
- tak pernah
- Namun
- New
- berita
- berikutnya
- bagus
- tidak
- normal
- tidak ada
- Melihat..
- sekarang
- jumlah
- nomor
- obyek
- Jelas
- of
- lepas
- resmi
- Secara resmi
- mengimbangi
- Tua
- on
- sekali
- ONE
- hanya
- open source
- operasi
- sistem operasi
- sistem operasi
- operator
- pilihan
- or
- urutan
- asli
- Lainnya
- Lainnya
- jika tidak
- kami
- diri
- di luar
- keluaran
- lebih
- secara keseluruhan
- sendiri
- halaman
- Panik
- bagian
- Kata Sandi
- password manager
- password
- path
- pola
- paul
- berhenti sebentar
- Membayar
- persen
- mungkin
- periode
- tetap
- pribadi
- data pribadi
- fisik
- gambar
- potongan-potongan
- Tempat
- placeholder
- Wabah
- plato
- Kecerdasan Data Plato
- Data Plato
- Cukup
- Titik
- poin
- Populer
- posisi
- mungkin
- Posts
- potensi
- tepat
- menyajikan
- cukup
- mencegah
- sebelumnya
- harga pompa cor beton mini
- Mencetak
- cetakan
- mungkin
- masalah
- proses
- program
- Programmer
- Programmer
- Pemrograman
- program
- jelas
- menempatkan
- Ular sanca
- Q & A
- pertanyaan
- meningkatkan
- RAM
- acak
- jarak
- agak
- Mentah
- data mentah
- RE
- tercapai
- Baca
- Bacaan
- siap
- nyata
- kehidupan nyata
- real-time
- benar-benar
- mengakui
- Memulihkan
- pulih
- terkait
- yang tersisa
- ingat
- terpencil
- menghapus
- ulangi
- ulang
- BERKALI-KALI
- melaporkan
- diwakili
- menghormati
- masing-masing
- ISTIRAHAT
- Hasil
- kembali
- kembali
- mengungkapkan
- Membersihkan
- benar
- Risiko
- risiko
- Kamar
- Run
- berjalan
- pemantauan runtime
- s
- aman
- lebih aman
- sama
- puas
- Save
- mengatakan
- pemindaian
- tersebar
- adegan
- Pencarian
- mencari
- Kedua
- detik
- Rahasia
- Bagian
- aman
- keamanan
- melihat
- benih
- melihat
- terlihat
- terlihat
- melihat
- Seri
- serius
- set
- pengaturan
- Pendek
- Segera
- harus
- Menunjukkan
- ditunjukkan
- menandatangani
- Tanda
- mirip
- Demikian pula
- Sederhana
- disederhanakan
- menyederhanakan
- hanya
- tunggal
- Ukuran
- tidur
- kecil
- Sneaky
- mengintip
- So
- Perangkat lunak
- padat
- beberapa
- sesuatu
- Segera
- sumber
- kode sumber
- Space
- khusus
- khususnya
- ditentukan
- kecepatan
- Bintang
- awal
- mulai
- Mulai
- dimulai
- startup
- Masih
- mencuri
- berhenti
- terhenti
- menyimpan
- tersimpan
- Cerita
- Tali
- kuat
- Belajar
- berhasil
- seperti itu
- cukup
- Seharusnya
- mengherankan
- tercengang
- mengherankan
- bertahan
- SVG
- menukar
- sistem
- sistem
- Mengambil
- diambil
- Dibutuhkan
- pengambilan
- pembicaraan
- teknis
- teknik
- sementara
- uji
- tes
- dari
- bahwa
- Grafik
- Sumber
- mereka
- Mereka
- diri
- kemudian
- teori
- Sana.
- karena itu
- mereka
- hal
- berpikir
- ini
- itu
- meskipun?
- pikir
- waktu
- Judul
- untuk
- bersama
- mengambil
- alat
- puncak
- jalur
- Pelacakan
- transisi
- jelas
- benar
- mencoba
- Berbalik
- dua
- mengetik
- khas
- memahami
- unicode
- sampai
- terpakai
- tidak diinginkan
- Memperbarui
- diperbarui
- URL
- us
- pemerintah kita
- penggunaan
- usb
- menggunakan
- gunakan-setelah-bebas
- bekas
- Pengguna
- kegunaan
- menggunakan
- kegunaan
- nilai
- Nilai - Nilai
- variasi
- memeriksa
- versi
- sangat
- melalui
- kerentanan
- W
- menunggu
- Menunggu
- ingin
- ingin
- adalah
- Menonton
- Cara..
- cara
- we
- minggu
- BAIK
- adalah
- Apa
- ketika
- apakah
- yang
- sementara
- SIAPA
- siapapun
- seluruh
- mengapa
- akan
- menang
- Windows
- menghapus
- dengan
- tanpa
- tanya
- kata
- Kerja
- bekerja
- kerja
- bekerja
- kuatir
- akan
- akan memberi
- menulis
- penulisan
- tertulis
- namun
- kamu
- Anda
- diri
- zephyrnet.dll
- nol