Sederhananya, saya bosan suatu hari dan meminta teman saya, Maximus Hackerman, untuk memberi saya sesuatu untuk dilakukan. Segera, ia mengirim melalui executable sederhana yang disebut "Reverseme.exe" (tautan Virustotal) dengan komentar sederhana "Cetak pesan tersembunyi di aplikasi Anda sendiri". Secara alami, file header CPP tidak disediakan. Setelah menjalankan yang dapat dieksekusi, itu hanya dibuka sebagai aplikasi berbasis konsol, mencetak "mengirim semua paket ajaib, segera keluar, bip boop!" dan kemudian segera keluar; Saya kira "segera" subyektif.
Untungnya, tampaknya tidak ada anti-debugging, jadi saya kira Hackerman merasa baik pada hari itu.
Setelah memasang windbg dan disassembler, kita dapat melihat bahwa aplikasi melempar, apa yang awalnya tampak, divide by zero pengecualian.
Namun, pada pemeriksaan lebih dekat, kami menemukan bahwa pengecualian ini dilemparkan sesaat sebelum aplikasi mencetak satu -satunya pesan yang terlihat. Penting untuk dicatat bahwa dua Kendaraan (penangan pengecualian vektor) terdaftar sedikit sebelum titik ini, jadi kemungkinan pengecualian ini disengaja, dan digunakan untuk mengayuh aliran kontrol. Trik murah benar -benar mencoba dan membuat kami benar -benar pergi!
Melupakan Kendaraan untuk saat ini, aman untuk mengasumsikan bahwa jika aplikasi tersebut "mengirim" paket, maka itu menggunakan fungsi Kirim Winsock untuk melakukannya (walaupun, aplikasi tersebut jelas mengatakan mereka paket "ajaib", jadi kita lihat).
Seperti yang diharapkan, ternyata Anda tidak dapat mengirim paket dengan sihir, jadi fungsi send Winsock memang diimpor.
Dengan menempatkan breakpoint pada fungsi Kirim, kita dapat melihat dan melihat apakah ada data yang relevan yang dapat kita abstrak pada saat pengiriman. Sayangnya, pada saat buffer dimuat ke dalam fungsi, data sudah dienkripsi. Namun, kita dapat menentukan bahwa buffer selalu panjangnya 3 byte, soket selalu terikat pada nilai hardcoded 0x69 ( bagus ), dan bahwa breakpoint fungsi kirim ditekan 14 kali total.
Ada beberapa cara untuk mencari -cari enkripsi tersebut. Salah satunya adalah untuk sepenuhnya membalikkannya, yang bisa berubah menjadi banyak upaya, yang lain adalah menemukan data yang diinginkan sebelum enkripsi dan abstrak sebelum dienkripsi. Yang terakhir secara signifikan lebih mudah daripada yang pertama, jadi kita akan melakukannya; Jangan ragu untuk membalikkan enkripsi, saya memiliki pandangan singkat dan itu tidak menghebohkan. Atau, jika Anda merasa mewah, Anda selalu dapat menimpa nilai pegangan soket dan memiliki aplikasi penerima dengan itu.
Sayangnya, tidak ada string yang berguna di segmen data hanya baca, terlepas dari pesan awal, jadi tidak ada petunjuk bermanfaat dari aspek itu!
Kembali ke Vehs, kita dapat melihat bahwa kedua penangan terdaftar digunakan untuk menyalin beberapa objek yang tidak diketahui ke dalam buffer memori yang dialokasikan. Ini tampaknya dilakukan dengan menggunakan memcpy, menggunakan fungsi sebagai parameter kedua, yang pada gilirannya menggunakan integer hardcoded sebagai parameter kedua. Perlu dicatat bahwa nilai -nilai hardcoded ini tidak melebihi 14 pada titik mana pun. Saya hanya menyertakan salah satu Kendaraan di tangkapan layar, karena pada dasarnya mereka identik dengan kode kecuali untuk nilai-nilai hardcoded.
Dengan breakpointing di salah satu fungsi memcpy dan memeriksa subfungsi pada parameter kedua, kita dapat melihat bahwa bilangan bulat hardcoded (13 / 0dh dalam contoh di bawah ini) diatur ke byte pertama parameter A1, dan A1+1 berisi karakter, tepat setelah pointer diasingkan.
Jika kita memeriksa beberapa dari 14 panggilan lainnya ke fungsi ini, kita dapat menemukan perilaku yang sama berulang; Mengatur karakter dari 1 hingga 14, menggunakan angka yang sesuai sebagai indikator ketertiban, kita dapat melihat bahwa mereka mulai mengeja beberapa kata yang dapat dibaca. Sekarang, kita bisa sangat malas dan hanya membuat aplikasi konsol untuk mencetak pesan, begitu kita tahu apa itu, tapi itu benar -benar terasa seperti curang. Plus, pesan tersebut dapat berubah di beberapa titik. Jadi, mari kita tulis gua kode untuk mencegat nilai sebelum dienkripsi.
Maaf, tapi kami menggunakan C ++ untuk ini! Jika Anda lebih suka menggunakan C#, jangan ragu untuk melalui seluruh platform Invoke Process untuk 9,7 juta fungsi dan kembali ke sini setelah selesai. Ngomong -ngomong, pertama -tama kita perlu memutuskan di mana kode gua. Untungnya, kita sudah tahu! Dengan menganalisis fungsi di atas, namun secara singkat, kita tahu bahwa dengan titik yang disorot RDX berisi indeks, dan RDX+1 berisi karakter yang sesuai. Di bawah ini adalah kode perakitan untuk fungsi yang dibahas.
Sekarang, secara logis, tempat terbaik untuk mengambil lompatan adalah di mov [rsp+arg_8], rdx , karena kami tidak terlalu peduli dengan parameter ketiga, tetapi ingin mencegat register RDX dan RDX+1 . Untuk melakukan anak -anak ini, kita akan membutuhkan beberapa byte: 10 byte untuk instruksi MOV , untuk memindahkan alamat gua kode kita ke register (kita akan menggunakan RAX , lebih lanjut tentang ini nanti selama jam 10), dan 2 byte untuk instruksi JMP , untuk melompat ke register. Bagi Anda yang memiliki PTSD dari matematika lebih lanjut, total pada 12 byte. Sekarang, sebelum kita pergi semua bibi Bessie dan spagetifikasi kode itu dengan writeProcessMemory, kita perlu mempertimbangkan bahwa tidak ada tempat yang ideal untuk mengganti 12 byte dalam perakitan di atas. Jika kita ingin melompat dari Offset 0x2905 ( mov [rsp+arg_8], rdx ), dan kita membutuhkan 12 byte untuk melakukannya, maka itu membutuhkan kita untuk mengimbangi 0x2917 , yang menampar di antara dua instruksi MOV . Sayangnya, jika kita hanya menulis byte kita ke sana, itu akan benar -benar mengayuh majelis, dan kemungkinan menyebabkan beberapa, eh, efek samping "menarik". Akibatnya, ini akan lebih mudah (mungkin sedikit lebih hacky, maaf tidak menyesal) untuk menambahkan beberapa instruksi satu-byte untuk padding dan membulatkan ke akhir instruksi. Selamat datang, 0x90.
Ngomong -ngomong, jadi sekarang kita tahu apa rencana kita, jadi mari kita tulis beberapa kode: isyarat musik peretas yang intens !
Di bawah ini adalah bagaimana lompatan awal ke gua kode kami akan terlihat begitu byte ditulis ke dalam perakitan aplikasi, lengkap dengan slide NOP sendiri.
Namun, sebelum kita benar -benar menulis dan mengganti perakitan apa pun, kita perlu memulai aplikasi dalam keadaan yang ditangguhkan; Ini akan menghentikan runtime aplikasi pada tahap awal, sehingga perubahan memori dapat dilakukan sebelum aplikasi sampai pada instruksi yang kami minati. Ekstrak kode di bawah ini menunjukkan proses ini, dan saya tidak akan membahasnya terlalu banyak karena Anda dapat melihatnya dalam file sumber repo ini, dan sebagian besar berbicara sendiri.
Sekarang prosesnya bertelur, kita perlu memperoleh alamat dasar proses. Biasanya Anda dapat menggunakan EnumProcessModules untuk ini, tetapi karena kami segera menangguhkan utas proses utama, PEB tidak mengandung struktur PEB_LDR_DATA yang berpenduduk sepenuhnya, khususnya InMemoryOrderModuleList , sehingga saat ini kami tidak dapat memperoleh alamat dasar. Ngomong -ngomong, ini tampaknya tidak didokumentasikan di mana pun di MSDN. Untungnya, ini relatif mudah dihindari; Dengan sangat cepat melanjutkan proses, menanyakan modul, dan kemudian menyadarkan kembali proses, kita bisa mendapatkan informasi yang kita butuhkan tanpa proses berkembang terlalu banyak. Seperti kebanyakan hal di Windows, Microsoft suka mengulangi bahwa sistem operasi itu adalah yang superior dengan tidak mendokumentasikan fungsi yang kita butuhkan: NtSuspendProcess dan NtResumeProcess . Dengan mudah, ayah saya berteman dengan Bill Gates, dan dia memberi tahu saya bahwa fungsi -fungsi ini hidup digabungkan dalam ntdll.dll , sehingga kami dapat mengambilnya dengan menggunakan kelas di bawah ini yang saya buat sebelumnya:
Sekarang kita memiliki dua fungsi yang kita butuhkan, kita dapat melanjutkan prosesnya, menanyakan modul, dan menyadarkan kembali prosesnya:
Anda mungkin bertanya -tanya, mengapa loop sementara menunggu dua penemuan modul sebagai lawan satu? Nah, Microsoft ingin mencetak hattrick dengan bakso di belakang jaring spaghetti yang tidak berdokumen dengan juga tidak menyebutkan bahwa modul pertama yang ditemukan oleh EnumProcessModules akan menjadi ntdll.dll, dan yang kedua akan menjadi eksekusi. Meskipun ini terdengar masuk akal, setelah dieksekusi ditemukan, ia akan bertukar indeks dengan ntdll.dll. Inilah contohnya:
Hasilnya setelah menanyakan hanya modul pertama:
Hasilnya setelah menanyakan dua modul:
Sebelum kita melangkah lebih jauh, kita perlu menulis kode perakitan yang melakukan lompatan awal ke gua kode, dan gua kode itu sendiri. Pada dasarnya, kita akan menimpa beberapa memori mulai dari offset 0x2905 , ambil lompatan, lakukan memata -matai kode kami, dan kemudian melompat kembali ke 0x2911 untuk melanjutkan aliran program normal. Awalnya, kami mendeklarasikan pemindahan alamat ke register RAX sebagai MOV RAX, 0x0 , sebagai alamat yang akan kami lompati adalah dinamis dan kami belum tahu apa itu. RAX adalah register yang aman untuk digunakan, karena register yang mudah berubah dan ditimpa segera setelah itu. Fakta menyenangkan, ini adalah bagaimana Nintendo memprogram platformer Mario Bros asli, dengan banyak lompatan (katakan padaku aku lucu)! Di bawah ini adalah bagaimana kode terlihat di kompiler; Ini dapat dibuat dengan cara lain, tetapi saya telah memilih untuk menyembunyikan instruksi perakitan yang diperlukan ke dalam bytecode. Jika Anda ingin melakukan ini di rumah, gunakan situs web ini.
Kode untuk gua kode yang sebenarnya sedikit lebih kompleks, dan logika untuk itu juga dikomentari dalam file ini, tetapi inilah proses kasar:
R10RDX , yang berisi indeks paket, ke R11BRDX , yang berisi karakter, ke R11B+12 ) ke R10 (yaitu 0x2911 )R10 kita juga perlu menulis ulang kode perakitan yang kita timpa (dengan slide NOP) ke gua kode kita untuk melestarikan tumpukan dll.; Kode ini dirujuk di wilayah “ Predetermined Assembly ”, tidak termasuk NOP. Di bawah ini adalah gua kode dalam kemuliaan yang jelek:Sebagian besar, kebanyakan.
Pada titik ini, kami pada dasarnya memiliki semua yang kami butuhkan untuk menyedot pesan tersembunyi di luar memori, kami hanya perlu mengimplementasikannya. Untuk merekap, kami memiliki pegangan ke proses yang ditangguhkan yang kami lewatkan, array 2 byte yang mewakili logika perakitan, alamat dasar dari proses yang ditangguhkan, disimpan dalam modules[0] , dan offset di mana kami perlu menulis logika lompatan. Cuplikan kode di bawah ini membuat alamat untuk melompat dari, alamat gua kode (untuk melompat ke), alamat penyimpanan memori 3-byte kami, menulis alamat ke dalam kode perakitan, dan kemudian menulis kode perakitan proses yang ditangguhkan, sebelum melanjutkannya:
Pengecoran gaya Harry Potter ajaib untuk codeCaveStorageAddr adalah untuk mengubah alamat menjadi byte, dan nilai -nilai hardcoded di loop adalah untuk penempatan alamat dinamis dalam array perakitan. Tentu saja, ini dapat dilakukan dengan cara yang jauh lebih bersih (jangan menulis anak -anak Magic Numbers), saya hanya malas setelah harus secara manual menulis semua byte itu. Beberapa bit terakhir yang akan ditulis adalah loop untuk dibaca, menggunakan ReadProcessMemory, memori, menyimpan karakter dan indeks ke dalam array byte yang dipesan, memberi sinyal byte, menggunakan WriteProcessMemory , ketika pembacaan memori saat ini selesai, dan mencetak pesan tersembunyi. Kita tahu bahwa fungsi Kirim hanya terjadi 14 kali, jadi kami keluar saat loop setelah array byte diisi; Ini dapat diubah dengan lebih banyak pengeditan memori untuk memberi sinyal pada aplikasi kami bahwa prosesnya "keluar" dan loop kami dapat dihentikan, daripada menggunakan nilai hardcoded 14, tetapi ini berfungsi untuk contoh ini.
Hasilnya? Pesan tersembunyi kami dicetak dalam aplikasi konsol kami sendiri! Jika ada yang penasaran, NPT adalah referensi ke bagian lain dari perangkat lunak maksimum peretas yang ditulis oleh orang-orang yang disebut-dia.