Peneliti utama Tom Court of Context, sebuah perusahaan keamanan informasi, berbicara tentang bagaimana ia berhasil mendeteksi bug yang berpotensi berbahaya dalam kode klien Steam.Pemain PC yang sadar akan keamanan telah memperhatikan bahwa Valve baru-baru ini merilis pembaruan klien Steam baru.
Dalam posting ini, saya ingin membuat
alasan untuk bermain game di tempat kerja untuk menceritakan kisah bug terkait yang ada di klien Steam setidaknya selama sepuluh tahun, yang hingga Juli tahun lalu dapat menyebabkan eksekusi kode jarak jauh (eksekusi kode jarak jauh, RCE) di semua 15 juta pelanggan aktif.
Sejak Juli, ketika Valve (akhirnya) menyusun kodenya dengan perlindungan eksploitasi modern diaktifkan, itu hanya dapat menyebabkan kegagalan klien, dan RCE hanya mungkin dalam kombinasi dengan kerentanan kebocoran informasi yang terpisah.
Kami mendeklarasikan Valve sebagai kerentanan pada 20 Februari 2018, dan, sesuai kredit perusahaan, itu diperbaiki di cabang beta kurang dari 12 jam kemudian. Perbaikan dipindahkan ke cabang stabil pada 22 Maret 2018.
Ulasan singkat
Dasar dari kerentanan adalah kerusakan pada tumpukan di dalam perpustakaan klien Steam, yang bisa disebut jarak jauh, di bagian kode yang terlibat dalam memulihkan datagram terfragmentasi dari beberapa paket UDP yang diterima.
Klien Steam bertukar data melalui protokolnya sendiri (Steam protocol), yang diimplementasikan di atas UDP. Ada dua area dalam protokol ini yang sangat menarik karena kerentanannya:
- Panjang paket
- Total panjang datagram yang direkonstruksi
Kesalahan itu disebabkan oleh kurangnya pemeriksaan sederhana. Kode tidak memverifikasi bahwa panjang datagram terfragmentasi pertama kurang dari atau sama dengan total panjang datagram. Ini tampak seperti pengawasan umum yang diberikan bahwa untuk semua paket berikutnya yang mengirimkan fragmen datagram, pemeriksaan dilakukan.
Tanpa bug data kebocoran tambahan, tumpukan kerusakan pada sistem operasi modern sangat sulit untuk dikendalikan, sehingga eksekusi kode jauh sulit untuk diimplementasikan. Namun, dalam kasus ini, berkat pengalokasi memori Steam sendiri dan ASLR yang hilang dari file biner steamclient.dll (hingga Juli lalu), bug ini dapat digunakan sebagai dasar untuk eksploitasi yang sangat andal.
Di bawah ini adalah deskripsi teknis tentang kerentanan dan eksploitasi terkait hingga
implementasi eksekusi kode.
Detail Kerentanan
Informasi yang diperlukan untuk pemahaman
Protokol
Pihak ketiga (misalnya,
https://imfreedom.org/wiki/Steam_Friends ), berdasarkan analisis lalu lintas yang dihasilkan oleh klien Steam, melakukan rekayasa balik dan membuat dokumentasi terperinci dari protokol Steam. Awalnya, protokol ini didokumentasikan pada tahun 2008 dan tidak banyak berubah sejak itu.
Protokol diimplementasikan sebagai protokol transmisi dengan pembentukan koneksi melalui aliran datagram UDP. Paket, sesuai dengan dokumentasi pada tautan di atas, memiliki struktur sebagai berikut:
Aspek penting:
- Semua paket dimulai dengan 4 byte " VS01 "
- package_len menjelaskan panjang informasi yang berguna (untuk datagram yang tidak dibagi, nilainya sama dengan panjang data)
- tipe menggambarkan tipe paket, yang dapat memiliki nilai-nilai berikut:
- 0x2 Otentikasi Panggilan
- 0x4 Terima Koneksi
- 0x5 Reset Koneksi
- 0x6 Paket adalah sebuah fragmen dari datagram
- Paket 0x7 adalah datagram terpisah
- Bidang sumber dan tujuan adalah pengidentifikasi yang ditetapkan untuk merutekan paket dengan benar pada beberapa koneksi dalam klien Steam
- Dalam kasus paket adalah sebuah fragmen dari datagram:
- split_count menunjukkan jumlah fragmen datagram dibagi
- data_len menunjukkan panjang total datagram yang dipulihkan
- Pemrosesan awal paket UDP ini terjadi pada fungsi CUDPConnection :: UDPRecvPkt di dalam steamclient.dll
Enkripsi
Informasi yang berguna dari paket datagram dienkripsi oleh AES-256 menggunakan kunci, yang dinegosiasikan antara klien dan server di setiap sesi. Negosiasi kunci dilakukan sebagai berikut:
- Klien menghasilkan kunci acak AES 32-byte, dan RSA mengenkripsi dengan kunci publik Valve sebelum mengirimnya ke server.
- Server, memiliki kunci pribadi, dapat mendekripsi nilai ini dan menerimanya sebagai kunci AES-256, yang akan digunakan dalam sesi
- Setelah kunci disepakati, semua informasi yang berguna dalam sesi saat ini dienkripsi dengan kunci ini.
Kerentanan
Kerentanan hadir di dalam metode
RecvFragment dari kelas
CUDPConnection . Tidak ada simbol dalam versi rilis dari perpustakaan steamclient, namun, ketika mencari melalui garis biner dalam fungsi yang menarik bagi kami, tautan ke "
CUDPConnection :: RecvFragment " ditemukan. Memasukkan fungsi ini dilakukan ketika klien menerima paket UDP yang berisi datagram Steam tipe 0x6 ("fragmen datagram").
1. Fungsi dimulai dengan memeriksa status koneksi untuk memastikan bahwa itu dalam keadaan "
Connected ".
2. Kemudian, bidang
data_len di datagram Steam diperiksa untuk memastikan bahwa itu berisi kurang dari
0x20000060 byte (tampaknya nilai ini dipilih secara sewenang-wenang).
3. Jika pemeriksaan dilewatkan, fungsi memeriksa apakah koneksi mengumpulkan fragmen beberapa datagram, atau apakah itu paket pertama dari aliran.
4. Jika ini adalah paket pertama dalam aliran, maka bidang
split_count diperiksa untuk melihat berapa banyak paket yang aliran ini akan diperluas
5. Jika aliran dibagi menjadi beberapa paket, maka bidang
seq_no_of_first_pkt diperiksa untuk memastikan bahwa itu cocok dengan nomor seri paket saat ini. Ini memastikan bahwa paket tersebut adalah yang pertama dalam aliran.
6. Bidang
data_len diperiksa lagi terhadap batas
0x20000060 byte. Selain itu, diverifikasi bahwa
split_count kurang dari
0x709b paket.
7. Jika kondisi ini terpenuhi, maka nilai Boolean diatur untuk menunjukkan bahwa kami sekarang mengumpulkan fragmen. Itu juga memeriksa bahwa kita belum memiliki buffer yang dialokasikan untuk menyimpan fragmen.
8. Jika pointer ke buffer kumpulan fragmen tidak nol, maka buffer koleksi fragmen saat ini dibebaskan dan buffer baru dialokasikan (lihat kotak kuning pada gambar di bawah). Di sinilah kesalahan muncul. Buffer kumpulan fragmen diharapkan akan dialokasikan dalam ukuran
data_len byte. Jika semuanya berhasil (dan kode tidak memeriksa - kesalahan kecil), maka informasi yang berguna dari datagram disalin ke buffer ini menggunakan
memmove , percaya bahwa jumlah byte untuk disalin ditunjukkan dalam
package_len .
Pengawasan paling penting dari pengembang adalah bahwa pemeriksaan " packet_len kurang dari atau sama dengan data_len " tidak dilakukan. Ini berarti bahwa dimungkinkan untuk mentransfer data_len kurang dari packet_len dan memiliki hingga 64 KB data (karena bidang packet_len menjadi 2 byte lebar) disalin ke buffer yang sangat kecil, yang memungkinkan untuk mengeksploitasi tumpukan korupsi.Eksploitasi kerentanan
Bagian ini mengasumsikan bahwa ada solusi untuk ASLR. Ini mengarah pada fakta bahwa sebelum memulai operasi, alamat awal steamclient.dll diketahui.
Paket spoofing
Agar paket UDP yang menyerang diterima oleh klien, ia harus memeriksa datagram keluar (klien -> server), yang dikirim untuk mengetahui pengidentifikasi koneksi klien / server, serta nomor seri. Kemudian, penyerang harus mem-spoof alamat IP dan port sumber / tujuan bersama dengan pengidentifikasi klien / server dan menambah nomor seri yang dipelajari dengan satu.
Manajemen memori
Untuk mengalokasikan memori lebih dari 1024 (0x400) byte, digunakan pengalokasi sistem standar. Untuk mengalokasikan memori kurang dari atau sama dengan 1024 byte, Steam menggunakan pengalokasi sendiri yang bekerja sama pada semua platform yang didukung. Artikel ini tidak akan membahas secara rinci distributor ini, dengan pengecualian aspek-aspek utama berikut:
- Blok memori besar diminta dari pengalokasi sistem, yang kemudian dibagi menjadi beberapa bagian dengan ukuran tetap untuk digunakan di bawah permintaan alokasi memori klien Steam.
- Seleksi dilakukan secara berurutan, antara fragmen yang digunakan tidak ada metadata yang memisahkannya.
- Setiap blok besar menyimpan daftar memori bebasnya sendiri, diimplementasikan sebagai daftar tertaut tunggal.
- Bagian atas daftar memori bebas menunjukkan fragmen bebas pertama dalam memori, dan 4 byte pertama dari fragmen ini menunjukkan fragmen bebas berikutnya (jika ada).
Alokasi memori
Saat mengalokasikan memori, blok bebas pertama terputus dari bagian atas daftar memori bebas, dan 4 byte pertama dari blok ini, sesuai dengan
next_free_block , disalin ke
variabel anggota
freelist_head di dalam kelas
pengalokasi .
Memori bebas
Ketika sebuah blok dibebaskan, bidang
freelist_head disalin ke 4 byte pertama dari blok yang dibebaskan (
next_free_block ), dan alamat blok yang dibebaskan disalin ke
variabel anggota
freelist_head dari kelas distributor.
Cara mendapatkan rekaman primitif
Buffer buffer terjadi pada heap, dan tergantung pada ukuran paket yang menyebabkan korupsi, alokasi memori dapat dikontrol baik oleh pengalokasi Windows standar (ketika mengalokasikan memori lebih dari 0x400 byte) atau oleh pengalokasi Steam sendiri (ketika mengalokasikan memori kurang dari 0x400 byte). Karena kurangnya langkah-langkah keamanan di distributor Steam saya sendiri, saya memutuskan lebih mudah menggunakannya untuk eksploitasi.
Mari kita kembali ke bagian manajemen memori: diketahui bahwa bagian atas daftar memori bebas dari blok dengan ukuran tertentu disimpan sebagai variabel anggota kelas distributor, dan penunjuk ke blok gratis berikutnya dalam daftar disimpan sebagai 4 byte pertama dari setiap blok bebas dari daftar.
Jika ada blok gratis di sebelah blok tempat terjadi overflow, kerusakan pada heap memungkinkan kita untuk menimpa pointer
next_free_block . Jika Anda menganggap bahwa banyak yang dapat disiapkan untuk ini, maka pointer
next_free_block ditulis ulang dapat diatur ke alamat untuk ditulis, setelah itu alokasi memori selanjutnya akan ditulis ke tempat ini.
Apa yang akan digunakan: datagram atau fragmen
Kesalahan dengan kerusakan memori terjadi dalam kode yang bertanggung jawab untuk memproses fragmen datagrams (paket tipe 6). Setelah terjadinya kerusakan, fungsi
RecvFragment () dalam keadaan di mana ia mengharapkan untuk menerima fragmen lebih lanjut. Namun, jika mereka tiba, maka pemeriksaan dilakukan:
fragment_size + num_bytes_already_received < sizeof(collection_buffer)
Tapi jelas, ini bukan kasus seperti itu, karena paket pertama kami telah melanggar aturan ini (keberadaan kesalahan dimungkinkan untuk melewati pemeriksaan ini) dan kesalahan akan terjadi. Untuk menghindari ini, Anda harus menghindari metode
CUDPConnection :: RecvFragment () setelah
kerusakan memori.
Untungnya,
CUDPConnection :: RecvDatagram () masih dapat menerima dan memproses paket tipe 7 (datagram) yang dikirim hingga
RecvFragment () valid, dan ini dapat digunakan untuk memulai perekaman primitif.
Masalah Enkripsi
Paket yang diterima oleh
RecvDatagram () dan
RecvFragment () diharapkan akan dienkripsi. Dalam kasus
RecvDatagram (), dekripsi dilakukan segera setelah diterimanya. Dalam kasus
RecvFragment (), ini terjadi setelah menerima fragmen terakhir di sesi.
Masalah mengeksploitasi kerentanan muncul karena kita tidak tahu kunci enkripsi yang dibuat di setiap sesi. Ini berarti bahwa setiap kode OP / kode shell yang kami kirim akan "didekripsi" menggunakan AES256, yang akan mengubah data kami menjadi sampah. Oleh karena itu, perlu untuk menemukan metode operasi, yang mungkin segera setelah menerima paket, sebelum prosedur dekripsi akan dapat memproses informasi berguna yang terkandung dalam buffer paket.
Cara mencapai eksekusi kode
Mengingat pembatasan dekripsi yang dijelaskan di atas, operasi harus dilakukan sebelum dekripsi data yang masuk. Ini memberlakukan batasan tambahan, tetapi tugasnya masih layak: Anda dapat menulis ulang pointer sehingga menunjuk ke objek
CWorkThreadPool yang disimpan di tempat yang dapat diprediksi di dalam bagian data dari file biner. Meskipun detail dan fungsionalitas internal kelas ini tidak diketahui, dapat diasumsikan namanya mendukung kumpulan thread yang dapat Anda gunakan saat Anda perlu melakukan "pekerjaan." Setelah mempelajari beberapa garis debug dalam file biner, Anda dapat memahami bahwa di antara karya-karya tersebut ada enkripsi dan dekripsi (
CWorkItemNetFilterEncrypt ,
CWorkItemNetFilterDecrypt ), jadi ketika tugas-tugas ini di-antri, kelas
CWorkThreadPool digunakan . Dengan menimpa pointer ini dan menulis tempat yang diinginkan di dalamnya, kita dapat mensimulasikan pointer vtable dan vtable yang terkait dengannya, yang memungkinkan kita untuk mengeksekusi kode, misalnya ketika
CWorkThreadPool :: AddWorkItem () dipanggil, yang harus terjadi sebelum proses dekripsi.
Gambar di bawah ini menunjukkan keberhasilan eksploitasi kerentanan hingga tahap mendapatkan kendali atas register EIP.
Mulai sekarang, Anda dapat membuat rantai ROP yang mengarah ke pelaksanaan kode arbitrer. Video di bawah ini menunjukkan bagaimana seorang penyerang dari jarak jauh memulai kalkulator Windows dalam versi Windows 10 yang sepenuhnya ditambal.
Untuk meringkas
Jika Anda sampai pada bagian artikel ini, terima kasih atas kegigihan Anda! Saya harap Anda mengerti bahwa ini adalah bug yang sangat sederhana, yang cukup mudah untuk dieksploitasi karena kurangnya perlindungan modern terhadap eksploitasi. Kode rentan mungkin sangat tua, tetapi jika tidak berfungsi dengan baik, sehingga pengembang tidak melihat perlunya memeriksanya atau memperbarui skrip pembuatannya. Pelajaran di sini adalah bahwa penting bagi pengembang untuk secara berkala meninjau kode lama dan membangun sistem untuk memastikan bahwa mereka mematuhi standar keamanan modern, bahkan jika fungsi kode itu sendiri tetap tidak berubah. Sungguh menakjubkan menemukan pada tahun 2018 bug yang begitu sederhana dengan konsekuensi serius pada platform perangkat lunak yang sangat populer. Ini harus menjadi insentif untuk mencari kerentanan seperti itu untuk semua peneliti!
Akhirnya, ada baiknya berbicara tentang proses pengungkapan informasi yang bertanggung jawab. Kami melaporkan bug ini kepada Valve dalam sepucuk surat kepada
tim keamanannya (
security@valvesoftware.com ) sekitar pukul 16:00 GMT dan hanya 8 jam kemudian, perbaikan dibuat dan diluncurkan ke klien beta Steam. Berkat ini, Valve sekarang berada di tempat pertama dalam tabel (imajiner) kontes kami "Siapa yang akan memperbaiki kerentanan lebih cepat" - pengecualian yang menyenangkan dibandingkan dengan mengungkapkan kesalahan kepada perusahaan lain, yang sering sering menghasilkan proses persetujuan yang panjang.
Halaman yang menjelaskan detail semua pembaruan klien