Seperti semua programmer, Anda suka kode. Anda dan dia adalah teman baik. Tapi cepat atau lambat dalam hidup akan datang saat seperti itu ketika tidak ada kode dengan Anda. Ya, sulit dipercaya, tetapi akan ada celah besar di antara Anda: Anda di luar, dan dia jauh di dalam. Dari keputusasaan, Anda, seperti semua orang, harus pergi ke sisi lain. Ke sisi rekayasa terbalik.
Menggunakan contoh tugas No. 2 dari fase online
NeoQUEST-2019, kami akan menganalisis prinsip umum driver Windows terbalik. Tentu saja, contohnya cukup disederhanakan, tetapi esensi dari proses tidak berubah dari ini - satu-satunya pertanyaan adalah jumlah kode yang perlu dilihat. Berbekal pengalaman dan keberuntungan, mari kita mulai!
Diberikan
Menurut legenda, kami diberi dua file: dump lalu lintas dan file biner yang menghasilkan lalu lintas yang sama. Pertama, lihat tempat pembuangan menggunakan Wireshark:
Tumpukan berisi aliran paket UDP, yang masing-masing berisi 6 byte data. Sekilas, data ini adalah beberapa set byte acak - tidak mungkin untuk mendapatkan apa pun dari traffic. Karena itu, kami mengalihkan perhatian kami ke binar, yang seharusnya memberi tahu Anda cara mendekripsi semuanya.
Buka di IDA:
Tampaknya kita menghadapi semacam pengemudi. Fungsi dengan awalan WSK merujuk ke Winsock Kernel, antarmuka pemrograman jaringan mode-kernel Windows. Pada MSDN, Anda dapat
melihat deskripsi struktur dan fungsi yang digunakan dalam WSK.
Untuk kenyamanan, Anda dapat memuat Windows Driver Kit 8 (mode kernel) - pustaka wdk8_km (atau yang lebih baru) ke dalam IDA untuk menggunakan jenis yang ditentukan di sana:
Perhatian, mundur!
Seperti biasa, mulailah dari titik masuk:
Mari kita mulai. Pertama, Wsk diinisialisasi, soket dibuat dan mengikat - kami tidak akan menjelaskan fungsi-fungsi ini secara rinci, mereka tidak membawa informasi apa pun yang berguna bagi kami.
Fungsi sub_140001608 menetapkan 4 variabel global. Sebut saja InitVars. Di salah satu dari mereka, nilai ditulis di alamat 0xFFFFF78000000320. Googling alamat ini sedikit, kita dapat membuat asumsi bahwa itu mencatat jumlah kutu dari timer sistem dari saat sistem boot. Untuk sekarang, beri nama variabel TickCount.
EntryPoint kemudian mengatur fungsi untuk memproses paket IRP (Paket Permintaan I / O). Anda dapat
membaca lebih lanjut tentang mereka di MSDN. Untuk semua jenis permintaan, fungsi didefinisikan yang hanya meneruskan paket ke driver berikutnya di stack.
Tetapi untuk tipe IRP_MJ_READ (3) fungsi terpisah didefinisikan; sebut saja IrpRead.
Di dalamnya, pada gilirannya, CompletionRoutine diinstal.
CompletionRoutine mengisi struktur yang tidak diketahui dengan data yang diterima dari IRP dan memasukkannya ke dalam daftar. Sejauh ini, kami tidak tahu apa yang ada di dalam paket - kami akan kembali ke fungsi ini nanti.
Kami melihat lebih jauh di EntryPoint. Setelah mendefinisikan penangan IRP, fungsi sub_1400012F8 dipanggil. Mari kita lihat ke dalam dan segera perhatikan bahwa perangkat (IoCreateDevice) dibuat di dalamnya.
Panggil fungsi AddDevice. Jika jenisnya benar, maka kita akan melihat bahwa nama perangkat adalah "\\ Device \\ KeyboardClass0". Jadi driver kami berinteraksi dengan keyboard. Googling tentang IRP_MJ_READ dalam konteks keyboard, Anda dapat
menemukan bahwa struktur KEYBOARD_INPUT_DATA ditransmisikan dalam paket. Mari kita kembali ke CompletionRoutine dan melihat data seperti apa yang dilewatinya.
IDA di sini tidak menguraikan struktur dengan baik, tetapi dengan offset dan panggilan lebih lanjut Anda dapat memahami bahwa itu terdiri dari ListEntry, KeyData (kode pindai kunci disimpan di sini) dan KeyFlags.
Setelah AddDevice, fungsi sub_140001274 dipanggil di EntryPoint. Dia menciptakan aliran baru.
Mari kita lihat apa yang terjadi di ThreadFunc.
Dia mendapatkan nilai dari daftar dan memprosesnya. Segera perhatikan fungsi sub_140001A18.
Ini melewati data yang diproses ke input fungsi sub_140001A68, bersama dengan pointer ke WskSocket dan angka 0x89E0FEA928230002. Setelah menganalisis jumlah parameter dengan byte (0x89 = 137, 0xE0 = 224, 0xFE = 243, 0xA9 = 169, 0x2328 = 9000), kami mendapatkan alamat dan port yang persis sama dari tempat pembuangan lalu lintas: 169.243.224.137:9000. Adalah logis untuk berasumsi bahwa fungsi ini mengirim paket jaringan ke alamat dan port yang ditentukan - kami tidak akan mempertimbangkannya secara rinci.
Mari kita lihat bagaimana data diproses sebelum dikirim.
Untuk dua elemen pertama, yang setara dilakukan dengan nilai yang dihasilkan. Karena jumlah kutu digunakan untuk menghitung, dapat diasumsikan bahwa kita dihadapkan pada generasi nomor pseudo-acak.
Setelah menghasilkan angka, itu menimpa nilai variabel yang sebelumnya kita sebut TickCount. Variabel untuk rumus diatur dalam InitVars. Jika kita kembali ke panggilan ke fungsi ini, kita akan menemukan nilai-nilai untuk variabel-variabel ini, dan sebagai hasilnya kita akan mendapatkan rumus berikut:
(54773 + 7141 * prev_value)% 259200Ini adalah
generator nomor acak semu kongruen
acak . Ini diinisialisasi dalam InitVars menggunakan TickCount. Untuk setiap nomor berikutnya, yang sebelumnya bertindak sebagai nilai awal (generator mengembalikan nilai byte ganda, dan yang sama digunakan untuk generasi berikutnya).
Setelah setara dengan jumlah acak dari dua nilai yang dikirimkan dari keyboard, sebuah fungsi disebut yang membentuk sisa dua byte pesan. Ini hanya menghasilkan
xor dari dua parameter yang sudah dienkripsi dan beberapa nilai konstan. Ini tidak mungkin untuk mendekripsi data entah bagaimana, jadi dua byte terakhir dari pesan untuk kami tidak membawa informasi yang berguna, dan mereka tidak dapat dipertimbangkan. Tetapi apa yang harus dilakukan dengan data terenkripsi?
Mari kita lihat lebih dekat apa yang sebenarnya dienkripsi. KeyData adalah kode pemindaian yang dapat mengambil rentang nilai yang cukup luas, menebak itu tidak mudah. Tapi
KeyFlags adalah bidang bit:
Jika Anda melihat
tabel kode pemindaian, Anda akan melihat bahwa paling sering flag tersebut adalah 0 (kuncinya turun) atau 1 (kuncinya dinaikkan). KEY_E0 akan diekspos sangat jarang, tetapi mungkin akan muncul, tetapi untuk memenuhi KEY_E1 kemungkinannya sangat kecil. Oleh karena itu, Anda dapat mencoba untuk melakukan hal berikut: kita pergi melalui data dari dump, pilih nilai KeyFlags yang dienkripsi, buat setara dengan 0, hasilkan dua PSC berturut-turut. Pertama, KeyData adalah satu byte, dan kita dapat memeriksa kebenaran dari MSS yang dihasilkan oleh byte tinggi. Dan kedua, KeyFlags terenkripsi berikutnya, ketika melakukan yang setara dengan PSC yang benar, akan mengambil nilai bit yang sama. Jika ini ternyata salah, maka kami menganggap bahwa KeyFlag yang awalnya kami lihat adalah 1, dll.
Mari kita coba implementasikan algoritma kami. Kami akan menggunakan python untuk ini:
Jalankan skrip kami pada data yang diterima dari dump:
Dan dalam lalu lintas yang didekripsi kita menemukan jalur yang paling diinginkan!
NQ2019DABE17518674F97DBA393415E9727982FC52C202549E6C1740BC0933C694B3DESegera akan ada artikel dengan analisis tugas yang tersisa, jangan lewatkan!
PS Dan kami mengingatkan Anda bahwa setiap orang yang telah menyelesaikan setidaknya satu tugas di NeoQUEST-2019 sepenuhnya berhak atas hadiah! Periksa surat Anda untuk surat, dan jika surat itu tidak sampai kepada Anda, tulislah ke
support@neoquest.ru !