Internal Linux: bagaimana / proc / self / mem menulis ke memori yang tidak dapat ditulis



Keunikan aneh dari file palsu /proc/*/mem



terletak pada semantiknya yang kuat. Operasi tulis melalui file ini akan berhasil meskipun memori virtual target ditandai sebagai tidak dapat ditulis. Ini disengaja, dan perilaku ini secara aktif digunakan oleh proyek seperti kompiler Julia JIT atau debugger rr.



Tetapi pertanyaannya adalah: apakah kode istimewa mematuhi izin memori virtual? Sejauh mana perangkat keras dapat mempengaruhi akses memori kernel?



Kami akan mencoba menjawab pertanyaan-pertanyaan ini dan mempertimbangkan nuansa interaksi antara sistem operasi dan perangkat keras yang menjalankannya. Mari kita telusuri batas prosesor yang dapat mempengaruhi kernel dan lihat bagaimana kernel dapat bekerja di sekitarnya.



Patch libc dengan / proc / self / mem



Seperti apa semantik yang menarik ini? Perhatikan kodenya:



#include <fstream>
#include <iostream>
#include <sys/mman.h>

/* Write @len bytes at @ptr to @addr in this address space using
 * /proc/self/mem.
 */
void memwrite(void *addr, char *ptr, size_t len) {
  std::ofstream ff("/proc/self/mem");
  ff.seekp(reinterpret_cast<size_t>(addr));
  ff.write(ptr, len);
  ff.flush();
}

int main(int argc, char **argv) {
  // Map an unwritable page. (read-only)
  auto mymap =
      (int *)mmap(NULL, 0x9000,
                  PROT_READ, // <<<<<<<<<<<<<<<<<<<<< READ ONLY <<<<<<<<
                  MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

  if (mymap == MAP_FAILED) {
    std::cout << "FAILED\n";
    return 1;
  }

  std::cout << "Allocated PROT_READ only memory: " << mymap << "\n";
  getchar();

  // Try to write to the unwritable page.
  memwrite(mymap, "\x40\x41\x41\x41", 4);
  std::cout << "did mymap[0] = 0x41414140 via proc self mem..";
  getchar();
  std::cout << "mymap[0] = 0x" << std::hex << mymap[0] << "\n";
  getchar();

  // Try to writ to the text segment (executable code) of libc.
  auto getchar_ptr = (char *)getchar;
  memwrite(getchar_ptr, "\xcc", 1);

  // Run the libc function whose code we modified. If the write worked,
  // we will get a SIGTRAP when the 0xcc executes.
  getchar();
}

      
      





Ini /proc/self/mem



digunakan di sini untuk menulis ke dua halaman memori yang tidak dapat ditulis. Yang pertama berisi kode itu sendiri, dan yang kedua milik libc



(fungsi getchar



). Bagian terakhir yang lebih menarik: kode menulis byte 0xcc (breakpoint dalam aplikasi x86-64), yang, jika dijalankan, akan menyebabkan kernel menyediakan proses kami dengan SIGTRAP. Ini benar-benar mengubah libc yang dapat dieksekusi. Dan jika pada panggilan berikutnya getchar



kita mendapatkan SIGTRAP, kita akan tahu bahwa rekaman itu berhasil.



Seperti inilah tampilannya saat Anda menjalankan program:





Bekerja! Di tengah, dicetak ekspresi yang membuktikan bahwa nilai 0x41414140 berhasil ditulis dan dibaca dari memori. Output terakhir menunjukkan bahwa setelah patching, proses kami menerima SIGTRAP sebagai hasil dari panggilan kami getchar



.



Di dalam video:





Kami telah melihat bagaimana fitur ini bekerja dari perspektif ruang pengguna. Mari kita gali lebih dalam. Untuk sepenuhnya memahami cara kerjanya, Anda perlu melihat bagaimana perangkat keras memaksakan batasan memori.



Peralatan



Pada platform x86-64, ada dua pengaturan prosesor yang mengontrol kemampuan kernel untuk mengakses memori. Mereka digunakan oleh unit manajemen memori (MMU).



Setting pertama adalah Write Protect bit (CR0.WP). Dari manual Intel (Volume 3, Bagian 2.5) kita tahu:



Perlindungan tulis (CR0 bit ke-16). Jika diberikan, ini mencegah prosedur tingkat pengawas untuk menulis ke halaman yang dilindungi dari penulisan. Jika bit kosong, maka prosedur tingkat pengawas dapat menulis ke halaman yang dilindungi dari penulisan (terlepas dari pengaturan bit U / S; lihat Bagian 4.1.3 dan 4.6).


Hal ini mencegah kernel untuk menulis ke halaman yang dilindungi dari penulisan, yang secara alami diizinkan secara default .



Pengaturan kedua adalah Supervisor Mode Access Prevention (SMAP) (CR4.SMAP). Deskripsi lengkap di Volume 3, Bagian 4.6, bertele-tele. Singkatnya, SMAP sepenuhnya menghilangkan kernel dari kemampuan untuk menulis atau membaca dari memori ruang pengguna. Ini mencegah eksploitasi yang membanjiri ruang pengguna dengan data berbahaya yang harus dibaca kernel selama eksekusi.



Jika kode kernel Anda hanya menggunakan saluran yang disetujui ( copy_to_user



dll.), maka SMAP dapat diabaikan dengan aman, fungsi ini secara otomatis akan menggunakannya sebelum dan sesudah mengakses memori. Bagaimana dengan proteksi tulis?



Jika CR0.WP tidak ditentukan, maka implementasi /proc/*/mem



kernel memang bisa begitu saja menulis ke memori ruang pengguna yang dilindungi penulisan.



Namun, CR0.WP diatur saat boot dan biasanya berlaku selama seluruh waktu operasi sistem. Dalam hal ini, saat mencoba menulis, kesalahan halaman akan dikeluarkan. Ini lebih merupakan alat Copy-on-Write daripada alat keamanan, jadi ini tidak memaksakan batasan nyata pada kernel. Dengan kata lain, diperlukan penanganan kesalahan yang tidak nyaman, yang tidak diperlukan untuk bit tertentu.



Mari kita cari tahu implementasinya sekarang.



Bagaimana / proc / * / mem bekerja



/proc/*/mem



Hal ini diterapkan di fs / proc / base.c .



Struktur tersebut file_operations



berisi fungsi penangan, dan fungsi mem_rw () mendukung penuh penangan tulis. mem_rw()



menggunakan access_remote_vm () untuk operasi tulis . Dan access_remote_vm()



itu melakukan ini:



  • Panggilan get_user_pages_remote()



    untuk menemukan bingkai fisik yang cocok dengan alamat virtual target.
  • Panggilan kmap()



    untuk menandai frame ini sebagai dapat ditulis di ruang alamat virtual kernel.
  • Panggilan copy_to_user_page()



    untuk eksekusi akhir operasi tulis.


Implementasi ini sepenuhnya mengabaikan masalah kemampuan kernel untuk menulis ke ruang pengguna yang tidak dapat ditulis! Kontrol kernel atas subsistem memori virtual memungkinkan MMU sepenuhnya dilewati, memungkinkan kernel untuk hanya menulis ke ruang alamatnya sendiri yang dapat ditulisi. Jadi pembahasan CR0.WP menjadi tidak relevan.



Mari kita



lihat setiap langkah: get_user_pages_remote ()



Untuk melewati MMU, kernel perlu melakukan secara manual apa yang dilakukan MMU di perangkat keras dalam aplikasi. Pertama, Anda perlu mengonversi alamat virtual target menjadi alamat fisik. Ini dilakukan oleh keluarga fungsi get_user_pages()



... Mereka melintasi tabel halaman dan mencari bingkai memori fisik yang cocok dengan rentang alamat virtual tertentu.



Pemanggil menyediakan konteks dan menggunakan tanda untuk mengubah perilakunya get_user_pages()



. Bendera FOLL_FORCE



yang sedang dikirim sangat menarik mem_rw()



. Bendera memicu check_vma_flags (logika pemeriksaan akses get_user_pages()



) untuk mengabaikan penulisan ke halaman yang tidak dapat ditulis dan melanjutkan pencarian. Semantik "punchy" sepenuhnya mengacu pada FOLL_FORCE



(komentar saya):



static int check_vma_flags(struct vm_area_struct *vma, unsigned long gup_flags)
{
        [...]
        if (write) { // If performing a write..
                if (!(vm_flags & VM_WRITE)) { // And the page is unwritable..
                        if (!(gup_flags & FOLL_FORCE)) // *Unless* FOLL_FORCE..
                                return -EFAULT; // Return an error
        [...]
        return 0; // Otherwise, proceed with lookup
}

      
      





get_user_pages()



Ini juga menganut semantik copy-on-write (CoW). Jika penulisan ke tabel halaman yang tidak dapat ditulis ditentukan, maka kegagalan halaman ditiru dengan memanggil handle_mm_fault



penangan kesalahan halaman utama. Ini memulai rutinitas pemrosesan salin-saat-tulis do_wp_page



yang sesuai, yang menyalin halaman sesuai kebutuhan. Jadi jika entri melalui /proc/*/mem



dieksekusi oleh pemetaan bersama pribadi, misalnya, libc, maka entri tersebut hanya terlihat dalam proses.



kmap ()



Setelah bingkai fisik ditemukan, bingkai tersebut perlu dipetakan ke ruang alamat virtual kernel, yang dapat ditulis. Ini dilakukan dengan bantuan kmap()



.



Pada platform 64-bit x86, semua memori fisik dipetakan melalui area pemetaan sebaris ruang alamat virtual kernel. Dalam hal ini, ini kmap()



bekerja dengan sangat sederhana: hanya perlu menambahkan alamat awal dari pemetaan linier ke alamat fisik frame untuk menghitung alamat virtual tempat frame ini dipetakan.



Pada platform 32-bit x86, pemetaan inline berisi subset memori fisik, sehingga fungsi kmap()



mungkin perlu memetakan bingkai dengan mengalokasikan memori highmem dan memanipulasi tabel halaman.



Dalam kedua kasus tersebut, pemetaan garis dan pemetaan highmem dilakukan dengan perlindungan. PAGE_KERNEL yang memungkinkan penulisan.



copy_to_user_page ()



Langkah terakhir adalah menjalankan penulisan. Ini dilakukan dengan menggunakan copy_to_user_page()



apa yang pada dasarnya memcpy. Ini berfungsi karena target adalah pemetaan yang dapat ditulisi kmap()



.



Diskusi



Jadi, pertama, kernel, menggunakan tabel halaman memori milik program, mengubah alamat virtual target di ruang pengguna ke bingkai fisik yang sesuai. Kernel kemudian memetakan frame ini ke ruang virtualnya sendiri yang dapat ditulisi. Akhirnya, ia menulis dengan memcpy sederhana.



Menariknya, CR0.WP tidak digunakan di sini. Implementasinya secara elegan melewati titik ini dengan memanfaatkan fakta bahwa ia tidak harus mengakses memori melalui penunjuk ruang pengguna . Karena kernel memiliki kendali penuh atas memori virtual, kernel dapat dengan mudah memetakan ulang bingkai fisik ke dalam ruang alamat virtualnya sendiri dengan resolusi sewenang-wenang dan melakukan apa pun yang diinginkan dengannya.



Penting untuk diperhatikan bahwa izin yang melindungi halaman memori terkait dengan alamat virtual yang digunakan untuk mengakses halaman itu, bukan bingkai fisik yang terkait dengan halaman tersebut . Notasi izin memori merujuk secara eksklusif ke memori virtual, bukan memori fisik.



Kesimpulan



Dengan memeriksa detail dari semantik punchy dalam implementasi, /proc/*/mem



kita dapat merefleksikan hubungan antara inti dan prosesor. Sekilas, kemampuan kernel untuk menulis ke memori yang tidak dapat ditulis menimbulkan pertanyaan: Sejauh mana prosesor dapat memengaruhi akses memori kernel? Manual menjelaskan mekanisme kontrol yang dapat membatasi tindakan kernel. Tetapi jika dilihat lebih dekat, batasannya hanya dangkal. Ini adalah rintangan sederhana untuk menyiasati.



All Articles