Pada bagian sebelumnya, kami hanya mencelupkan jari kaki kami ke perairan namespace dan pada saat yang sama melihat betapa mudahnya untuk memulai proses dalam namespace UTS yang terisolasi. Dalam posting ini kita akan membahas ruang nama Pengguna.
Di antara sumber daya terkait keamanan lainnya, ruang nama pengguna mengisolasi pengidentifikasi pengguna dan grup dalam sistem. Dalam posting ini, kami akan fokus hanya pada sumber daya ID pengguna dan grup (masing-masing UID dan GID), karena mereka memainkan peran mendasar dalam melakukan pemeriksaan izin dan kegiatan terkait keamanan lainnya di seluruh sistem.
Di Linux, ID ini hanya bilangan bulat yang mengidentifikasi pengguna dan grup dalam sistem. Dan beberapa dari mereka ditugaskan untuk setiap proses untuk mengatur operasi / sumber daya yang mana proses ini dapat dan tidak bisa mendapatkan akses. Kemampuan suatu proses untuk membahayakan tergantung pada izin yang terkait dengan ID yang ditugaskan.
Ruang nama pengguna
Kami akan menggambarkan kemampuan ruang nama pengguna hanya menggunakan ID pengguna. Tindakan yang persis sama berlaku untuk ID grup, yang akan kami bahas nanti dalam posting ini.
Ruang nama pengguna memiliki salinan pengidentifikasi pengguna dan grup sendiri. Kemudian isolasi memungkinkan Anda untuk mengaitkan proses dengan set ID lain, tergantung pada namespace pengguna yang saat ini dimiliki. Sebagai contoh, proses $pid
dapat dijalankan dari root
(UID 0) di namespace pengguna P dan tiba-tiba terus dijalankan dari proxy
(UID 13) setelah beralih ke namespace pengguna lain Q.
Ruang pengguna bisa disarangkan! Ini berarti bahwa instance namespace kustom (induk) dapat memiliki nol atau lebih namespace anak, dan setiap namespace anak dapat, pada gilirannya, memiliki namespace anak sendiri dan seterusnya ... (hingga mencapai batas 32 level bersarang). Ketika namespace C baru dibuat, Linux menetapkan namespace Pengguna saat ini dari proses pembuatan C sebagai induk untuk C dan ini tidak dapat diubah nanti. Akibatnya, semua ruang nama pengguna memiliki tepat satu induk, membentuk struktur ruang nama seperti pohon. Dan, seperti dalam kasus tree, pengecualian aturan ini ada di bagian atas, di mana kita memiliki namespace root (atau awal, default). Ini, jika Anda belum melakukan semacam sihir kontainer, kemungkinan besar namespace pengguna tempat semua proses Anda berada, karena ini adalah satu-satunya namespace pengguna sejak sistem dimulai.
Dalam posting ini, kita akan menggunakan perintah meminta P $ dan C $ untuk mengindikasikan shell yang saat ini berjalan di namespace pengguna P dan child C.
Pemetaan ID Pengguna
Namespace pengguna, pada kenyataannya, berisi satu set pengidentifikasi dan beberapa informasi yang menghubungkan ID ini dengan satu set ID dari namespace pengguna lain - duet ini mendefinisikan ide lengkap ID proses yang tersedia dalam sistem. Mari kita lihat bagaimana tampilannya:
P$ whoami iffy P$ id uid=1000(iffy) gid=1000(iffy)
Di jendela terminal lain, mari kita mulai shell menggunakan unshare
(flag -U
menciptakan proses di namespace pengguna baru):
P$ whoami iffy P$ unshare -U bash
Tunggu sebentar, siapa? Sekarang kita berada di shell bersarang di C , pengguna saat ini menjadi bukan siapa-siapa? Kami mungkin menduga bahwa karena C adalah namespace pengguna baru, prosesnya mungkin memiliki jenis ID yang berbeda. Karena itu, kami mungkin tidak berharap dia tetap iffy
, tetapi nobody
ada yang tidak lucu. Di sisi lain, itu hebat karena kami mendapat isolasi yang kami inginkan. Proses kami sekarang memiliki substitusi ID yang berbeda (walaupun rusak) dalam sistem - saat ini ia melihat semua orang sebagai nobody
- nobody
dan setiap kelompok sebagai nogroup
.
Informasi yang menghubungkan UID dari satu ruang nama pengguna ke yang lain disebut pemetaan ID pengguna . Ini adalah tabel pencarian untuk mencocokkan ID di namespace pengguna saat ini untuk ID di namespace lain dan setiap namespace pengguna dikaitkan dengan tepat satu pemetaan UID (selain pemetaan GID lain untuk ID grup).
Pemetaan ini adalah apa yang rusak di shell unshare
kami. Ternyata ruang nama pengguna baru dimulai dengan pemetaan kosong, dan sebagai hasilnya, Linux menggunakan pengguna yang mengerikan itu secara default. Kami perlu memperbaiki ini sebelum kami dapat melakukan pekerjaan yang bermanfaat di namespace baru kami. Misalnya, saat ini, panggilan sistem (seperti setuid
) yang mencoba bekerja dengan UID akan gagal. Tapi jangan takut! Sesuai dengan tradisi all-is-file , Linux menyajikan pemetaan ini menggunakan sistem file /proc
di /proc/$pid/uid_map
(di /proc/$pid/gid_map
untuk GID), di mana $pid
adalah ID proses. Kami akan memanggil dua file ini memetakan file.
File peta
File peta adalah file khusus dalam sistem. Apa yang spesial? Ya, dengan mengembalikan konten yang berbeda setiap kali Anda membacanya, tergantung pada proses yang Anda baca. Sebagai contoh, file peta /proc/$pid/uid_maps
mengembalikan pemetaan dari UID dari namespace pengguna yang dimiliki oleh proses $pid
, UID di namespace pengguna dari proses membaca. Dan, sebagai hasilnya, konten yang dikembalikan ke proses X mungkin berbeda dari apa yang dikembalikan ke proses Y , bahkan jika mereka membaca file peta yang sama pada waktu yang sama.
Secara khusus, proses X , yang membaca file peta UID /proc/$pid/uid_map
, menerima serangkaian string. Setiap baris memetakan rentang berkelanjutan UID ke ruang nama pengguna C dari proses $pid
, yang sesuai dengan rentang UID di ruang nama lain.
Setiap baris memiliki format $fromID $toID $length
, di mana:
$fromID
adalah UID awal rentang untuk namespace pengguna dari proses $pid
$lenght
adalah panjang rentang.- Terjemahan
$toID
tergantung pada proses membaca X. Jika X milik pengguna lain namespace U , maka $toID
adalah UID awal rentang di U yang memetakan dari $fromID
. Jika tidak, $toID
adalah UID awal rentang dalam P , namespace pengguna induk dari proses C.
Misalnya, jika suatu proses membaca file /proc/1409/uid_map
dan di antara baris yang diterima Anda dapat melihat 15 22 5
, maka UID 15-19 di namespace pengguna proses 1409
dipetakan ke UID 22-26 dari namespace pengguna terpisah dari proses membaca.
Di sisi lain, jika suatu proses membaca dari file /proc/$$/uid_map
(atau file peta dari setiap proses milik ruang nama pengguna yang sama dengan proses membaca) dan menerima 15 22 5
, maka UID dari 15 hingga 19 di pengguna namespace C memetakan ke dalam UID dari 22 hingga 26 induk untuk namespace pengguna C.
Mari kita coba:
P$ echo $$ 1442
Ya, itu tidak terlalu menarik, karena ini adalah dua kasus ekstrem, tetapi ada beberapa hal di sana:
- Ruang nama pengguna yang baru dibuat sebenarnya akan memiliki file peta kosong.
- UID 4294967295 tidak dapat dipetakan dan tidak cocok untuk digunakan bahkan di namespace pengguna
root
. Linux menggunakan UID ini secara khusus untuk menunjukkan tidak adanya ID pengguna .
Menulis File Peta UID
Untuk memperbaiki namespace pengguna kami yang baru dibuat C , kami hanya perlu menyediakan pemetaan yang diperlukan dengan menulis kontennya untuk memetakan file untuk setiap proses milik C (kami tidak dapat memperbarui file ini setelah menulis ke sana). Menulis ke file ini memberi tahu Linux dua hal:
- UID apa yang tersedia untuk proses yang terkait dengan namespace pengguna target C.
- UID mana dalam ruang nama pengguna saat ini yang sesuai dengan UID di C.
Sebagai contoh, jika kita menulis berikut ini dari P namespace pengguna induk ke dalam file peta untuk namespace C anak:
0 1000 1 3 0 1
kami pada dasarnya memberi tahu Linux bahwa:
- Untuk proses dalam C , satu-satunya UID yang ada dalam sistem adalah UID
0
dan 3
. Misalnya, panggilan sistem setuid(9)
akan selalu diakhiri dengan sesuatu seperti id pengguna yang tidak valid . - UID
1000
dan 0
dalam P sesuai dengan UID 0
dan 3
dalam C. Sebagai contoh, jika sebuah proses yang berjalan dengan UID 1000
di P beralih ke C , ia akan menemukan bahwa setelah beralih, UID-nya telah menjadi root
0
.
Namespace dan pemilik hak istimewa
Dalam posting sebelumnya, kami menyebutkan bahwa saat membuat ruang nama baru, akses dengan tingkat pengguna super diperlukan. Ruang nama pengguna tidak memaksakan persyaratan ini. Bahkan, fitur lain adalah mereka dapat memiliki ruang nama lain.
Setiap kali N namespace non-pengguna dibuat , Linux menetapkan P namespace pengguna saat ini dari proses yang menciptakan N untuk menjadi pemilik namespace N. Jika P dibuat bersama dengan ruang nama lain dalam panggilan sistem clone
sama, Linux memastikan bahwa P akan dibuat terlebih dahulu dan dijadikan pemilik ruang nama lain.
Pemilik ruang nama adalah penting karena proses yang meminta untuk mengambil tindakan istimewa pada sumber daya yang bukan ruang nama pengguna akan diperiksa hak-hak UID-nya terhadap pemilik ruang nama pengguna ini, dan bukan ruang nama pengguna root. Misalnya, katakanlah P adalah namespace pengguna induk dari anak C , dan P dan C memiliki masing-masing namespace jaringan M dan N. Suatu proses mungkin tidak memiliki hak istimewa untuk membuat perangkat jaringan yang termasuk dalam M , tetapi mungkin dapat melakukan ini untuk N.
Konsekuensi dari memiliki pemilik namespace bagi kita adalah bahwa kita dapat menghapus persyaratan sudo
ketika menjalankan perintah menggunakan unshare
atau isolate
jika kita juga meminta pembuatan namespace pengguna. Misalnya, unshare -u bash
akan membutuhkan sudo
, tetapi unshare -Uu bash
tidak akan lagi:
Sayangnya, kami akan menerapkan kembali persyaratan superuser di posting berikutnya, karena isolate
kebutuhan root
privilege di root namespace pengguna untuk mengkonfigurasi dengan benar Mount dan Network namespace. Tetapi kami pasti akan menghapus hak istimewa proses tim untuk memastikan bahwa tim tidak memiliki izin yang tidak perlu.
Bagaimana ID dipecahkan
Kami baru saja melihat proses berjalan ketika pengguna biasa 1000
tiba-tiba beralih ke root
. Jangan khawatir, tidak ada peningkatan hak istimewa. Ingat bahwa ini hanya ID pemetaan : selama proses kami berpikir bahwa itu adalah root
pada sistem, Linux tahu bahwa root
- dalam kasusnya - berarti UID 1000
normal (berkat pemetaan kami). Jadi pada saat namespaces milik namespace pengguna barunya (seperti network namespace di C ) mengenali haknya sebagai root
, yang lain (seperti namespace jaringan di P ) tidak. Oleh karena itu, proses tidak dapat melakukan apa pun yang tidak dapat dilakukan oleh pengguna 1000
.
Setiap kali proses dalam namespace pengguna bersarang melakukan operasi yang memerlukan izin memeriksa - misalnya, membuat file - UID-nya di namespace pengguna ini dibandingkan dengan ID pengguna yang setara di namespace pengguna root dengan melintasi pemetaan di pohon namespace ke root. Ada gerakan ke arah yang berlawanan, misalnya, ketika dia membaca ID pengguna, seperti yang kita lakukan dengan ls -l my_file
. UID dari pemilik my_file
dipetakan dari namespace pengguna root ke yang sekarang dan ID terkait terakhir (atau tidak ada orang jika pemetaan tidak ada di suatu tempat di sepanjang seluruh pohon) diberikan untuk proses membaca.
ID grup
Bahkan jika kita root di C , kita masih dikaitkan dengan nogroup
mengerikan sebagai ID grup kita. Kita hanya perlu melakukan hal yang sama untuk /proc/$pid/gid_map
. Sebelum kita dapat melakukan ini, kita perlu menonaktifkan panggilan sistem setgroups
(ini tidak perlu jika pengguna kita sudah memiliki kemampuan CAP_SETGID
di P , tetapi kami tidak akan menganggap ini, karena ini biasanya datang dengan hak akses superuser) dengan menulis "tolak "ke file proc/$pid/setgroups
:
Implementasi
Kode sumber untuk posting ini dapat ditemukan di sini .
Seperti yang Anda lihat, ada banyak kesulitan yang terkait dengan mengelola ruang nama pengguna, tetapi implementasinya cukup sederhana. Yang perlu kita lakukan adalah menulis banyak baris ke file - itu suram untuk mengetahui apa dan di mana untuk menulis. Tanpa basa-basi lagi, inilah tujuan kami:
- Mengkloning proses tim di namespace penggunanya sendiri.
- Tulis dalam file peta UID dan GID dari proses tim.
- Setel ulang semua hak pengguna super sebelum menjalankan perintah.
1
dicapai dengan hanya menambahkan bendera CLONE_NEWUSER
ke panggilan sistem clone
kami.
int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER;
Untuk 2
kami menambahkan fungsi prepare_user_ns
, yang dengan hati-hati mewakili satu pengguna biasa 1000
sebagai root
.
static void prepare_userns(int pid) { char path[100]; char line[100]; int uid = 1000; sprintf(path, "/proc/%d/uid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); sprintf(path, "/proc/%d/setgroups", pid); sprintf(line, "deny"); write_file(path, line); sprintf(path, "/proc/%d/gid_map", pid); sprintf(line, "0 %d 1\n", uid); write_file(path, line); }
Dan kita akan menyebutnya dari proses utama di namespace pengguna induk tepat sebelum kita memberi sinyal proses perintah.
...
Untuk langkah 3
kami memperbarui fungsi cmd_exec
untuk memastikan bahwa perintah dijalankan dari 1000
pengguna yang tidak terjangkau yang kami sediakan dalam pemetaan (ingat bahwa pengguna root 0
dalam ruang nama pengguna dari proses tim adalah pengguna 1000
):
...
Dan itu saja! isolate
sekarang memulai proses dalam namespace pengguna yang terisolasi.
$ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root)
Ada beberapa detail dalam posting ini tentang cara kerja ruang nama Pengguna, tetapi pada akhirnya, pengaturan instance relatif tidak menyakitkan. Pada posting berikutnya, kita akan melihat kemungkinan menjalankan perintah di Mount namespace kita sendiri menggunakan isolate
(mengungkapkan rahasia di balik pernyataan FROM
dari Dockerfile
). Di sana kita perlu membantu Linux sedikit lebih banyak untuk mengonfigurasi instance dengan benar.