Klien Steam menghilangkan kerentanan berbahaya yang telah bersembunyi di sana selama sepuluh tahun

gambar

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.

gambar

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:

gambar

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.

gambar

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.

gambar

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.

gambar

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.

gambar

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:

  1. 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.
  2. Seleksi dilakukan secara berurutan, antara fragmen yang digunakan tidak ada metadata yang memisahkannya.
  3. Setiap blok besar menyimpan daftar memori bebasnya sendiri, diimplementasikan sebagai daftar tertaut tunggal.
  4. 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.

gambar

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

Source: https://habr.com/ru/post/id413003/


All Articles