في الجزء السابق ، غمرنا أصابع قدمنا في مياه مساحة الاسم ورأينا في نفس الوقت مدى سهولة بدء العملية في مساحة اسم UTS معزولة. في هذا المنشور ، سنغطي مساحة اسم المستخدم.
من بين الموارد الأخرى المتعلقة بالأمان ، تقوم مساحات أسماء المستخدمين بعزل معرفات المستخدمين والمجموعات في النظام. في هذا المنشور ، سنركز فقط على موارد معرّف المستخدم والمجموعة (UID و GID ، على التوالي) ، نظرًا لأنهم يلعبون دورًا أساسيًا في إجراء عمليات فحص الأذونات وغيرها من الأنشطة المتعلقة بالأمان عبر النظام.
على نظام Linux ، تعد هذه المعرفات مجرد أعداد صحيحة تحدد المستخدمين والمجموعات في النظام. ويتم تخصيص بعضها لكل عملية من أجل تحديد العمليات / الموارد التي يمكن لهذه العملية ولا يمكن الوصول إليها. تعتمد قدرة العملية على الضرر على الأذونات المرتبطة بالمعرفات المعينة.
مساحات أسماء المستخدمين
سنوضح إمكانيات مساحات أسماء المستخدمين باستخدام معرفات المستخدم فقط. تنطبق نفس الإجراءات تمامًا على معرفات المجموعات ، والتي سنتناولها لاحقًا في هذا المنشور.
مساحة اسم المستخدم لها نسختها الخاصة من معرفات المستخدم والمجموعة. ثم يسمح لك العزل بربط العملية بمجموعة أخرى من المعرفات ، اعتمادًا على مساحة اسم المستخدم التي تنتمي إليها حاليًا. على سبيل المثال ، يمكن تشغيل عملية $pid
من root
(UID 0) في مساحة اسم المستخدم P وتستمر فجأة في التشغيل من proxy
(UID 13) بعد التبديل إلى مساحة اسم مستخدم أخرى Q.
يمكن أن تكون مسافات المستخدم متداخلة! هذا يعني أنه يمكن أن يكون لمثل مساحة الاسم المخصصة (الأصل) مساحات أسماء فرعية تابعة أو أكثر ، ويمكن أن يكون لكل مساحة اسم تابعة ، بدورها ، مساحات أسماء تابعة خاصة بها وما إلى ذلك ... (حتى تصل إلى حد مستويات التداخل 32). عند إنشاء مساحة اسم جديدة C ، يقوم Linux بتعيين مساحة اسم المستخدم الحالية للعملية P التي تنشئ C باعتبارها الأصل لـ C ولا يمكن تغيير ذلك لاحقًا. نتيجةً لذلك ، تحتوي جميع مساحات أسماء المستخدمين على أصل واحد تمامًا ، مما يشكل بنية مساحات شبيهة بالشجرة من مساحات الأسماء. وكما في حالة الأشجار ، يوجد استثناء في هذه القاعدة في الأعلى ، حيث لدينا مساحة اسم الجذر (أو الأولي ، الافتراضية). هذا ، إذا لم تكن تقوم بالفعل بنوع من سحر الحاوية ، فغالبًا ما يكون ذلك هو مساحة اسم المستخدم التي تنتمي إليها جميع عملياتك ، حيث إنها مساحة اسم المستخدم الوحيدة منذ بدء تشغيل النظام.
في هذا المنشور ، سوف نستخدم موجهي الأوامر P $ و C $ للإشارة إلى الصدفة التي يتم تشغيلها حاليًا في مساحة اسم المستخدم الأصلية P و التابعة C على التوالي.
تعيينات معرف المستخدم
في الواقع ، تحتوي مساحة اسم المستخدم على مجموعة من المعرفات وبعض المعلومات التي تربط هذه المعرفات بمجموعة من معرفات مساحة اسم المستخدم الأخرى - يحدد هذا الثنائي فكرة كاملة عن معرفات العمليات المتوفرة في النظام. دعونا نرى كيف يمكن أن تبدو:
P$ whoami iffy P$ id uid=1000(iffy) gid=1000(iffy)
في نافذة طرفية أخرى ، لنبدأ تشغيل shell باستخدام unshare
(علامة -U
تنشئ عملية في مساحة اسم المستخدم الجديدة):
P$ whoami iffy P$ unshare -U bash
انتظر لحظة ، من؟ الآن بعد أن أصبحنا في قشرة متداخلة في C ، يصبح المستخدم الحالي أحدًا؟ ربما خمننا أن C هي مساحة اسم مستخدم جديدة ، فقد يكون لهذه العملية نوع مختلف من المعرف. لذلك ، ربما لم نتوقع منه أن يبقى غير iffy
، لكن nobody
ليس مضحكا. من ناحية أخرى ، إنه أمر رائع لأننا حصلنا على العزلة التي أردناها. تحتوي عمليتنا الآن على استبدال معرف هوية مختلف (وإن كان nogroup
) في النظام - في الوقت الحالي ، يرى الجميع أنه nobody
وكل مجموعة على شكل nogroup
.
تسمى المعلومات التي تربط UID من مساحة اسم المستخدم بأخرى تعيين معرف المستخدم . إنه جدول بحث لمطابقة المعرفات في مساحة اسم المستخدم الحالية للمعرفات في مساحة الاسم الأخرى ويرتبط كل مساحة اسم المستخدم مع تعيين UID واحد بالضبط (بالإضافة إلى تعيين GID آخر لمعرف المجموعة).
هذا التعيين هو ما تم كسره في غلاف unshare
. اتضح أن مساحات أسماء المستخدمين الجديدة تبدأ بالتعيين الفارغ ، ونتيجة لذلك ، يستخدم Linux المستخدم الرهيب الذي nobody
افتراضيًا. نحتاج إلى إصلاح هذا قبل أن نتمكن من القيام بأي عمل مفيد في مساحة الاسم الجديدة الخاصة بنا. على سبيل المثال ، في الوقت الحالي ، ستفشل مكالمات النظام (مثل setuid
) التي تحاول العمل مع UID. لكن لا تخف! طبقًا لتقليد all-is-file ، يقدم Linux هذا التعيين باستخدام نظام ملفات /proc
في /proc/$pid/uid_map
(في /proc/$pid/gid_map
لـ GID) ، حيث $pid
هو معرف العملية. سوف نسمي هذين الملفين خريطة الملفات.
خريطة الملفات
ملفات الخرائط هي ملفات خاصة في النظام. ما هي خاصة؟ حسنًا ، من خلال إعادة محتويات مختلفة في كل مرة تقرأ منها ، بناءً على ما تقرأه عمليتك. على سبيل المثال ، تقوم map-file /proc/$pid/uid_maps
بإرجاع التعيين من UIDs من مساحة اسم المستخدم التي تنتمي إليها عملية $pid
، UIDs في مساحة اسم المستخدم الخاصة بعملية القراءة. وكنتيجة لذلك ، قد يختلف المحتوى الذي تم إرجاعه إلى العملية X عن المحتوى الذي تم إرجاعه إلى العملية Y ، حتى لو كان قد قرأ ملف الخريطة نفسه في نفس الوقت.
على وجه الخصوص ، العملية X ، التي تقرأ ملف مخطط UID /proc/$pid/uid_map
، تستقبل مجموعة من السلاسل. يعيّن كل سطر نطاقًا مستمرًا من UIDs إلى مساحة اسم المستخدم C لعملية $pid
، المقابلة لمجموعة من UIDs في مساحة اسم أخرى.
يحتوي كل سطر بالتنسيق $fromID $toID $length
، حيث:
$fromID
هو UID $fromID
للنطاق لمساحة اسم المستخدم لعملية $pid
$lenght
هو طول النطاق.- تعتمد ترجمة
$toID
على عملية القراءة X. إذا كان X ينتمي إلى مساحة اسم مستخدم أخرى U ، فإن $toID
هو UID $toID
للنطاق في U الذي يعين $fromID
. خلاف ذلك ، فإن $toID
هي UID لبدء النطاق في P ، مساحة اسم المستخدم الأصلي للعملية C.
على سبيل المثال ، إذا قرأت العملية الملف /proc/1409/uid_map
ومن بين الأسطر المستلمة ، يمكنك رؤية 15 22 5
، ثم يتم تعيين UIDs من 15 إلى 19 في مساحة اسم المستخدم الخاصة بالعملية 1409
إلى UIDs 22-26 لمساحة مستخدم منفصلة لعملية القراءة.
من ناحية أخرى ، إذا كانت العملية تقرأ من الملف /proc/$$/uid_map
(أو ملف خريطة لأي عملية تنتمي إلى نفس مساحة اسم المستخدم مثل عملية القراءة) وتتلقى 15 22 5
، ثم UIDs من 15 إلى 19 في تعيين مساحة اسم المستخدم C في UIDs من 22 إلى 26 من الأصل لمساحة اسم المستخدم C.
لنجربها:
P$ echo $$ 1442
حسنًا ، لم يكن هذا أمرًا مثيرًا للغاية ، لأن هاتين الحالتين كانتا متطرفتين ، لكن هذا يوضح بعض الأشياء:
- سيكون لمساحة المستخدم التي تم إنشاؤها حديثًا ملفات خرائط فارغة بالفعل.
- UID 4294967295 غير قابل للتعيين وغير مناسب للاستخدام حتى في مساحة اسم المستخدم
root
. يستخدم Linux UID هذا بشكل محدد للإشارة إلى عدم وجود معرف مستخدم .
كتابة ملفات خريطة UID
لإصلاح مساحة اسم المستخدم التي تم إنشاؤها حديثًا C ، نحتاج فقط إلى توفير مناظراتنا الضرورية عن طريق كتابة محتوياتها لملفات الخريطة لأي عملية تخص C (لا يمكننا تحديث هذا الملف بعد الكتابة إليه). الكتابة إلى هذا الملف تخبر Linux شيئين:
- ما هي معرفات UID المتاحة للعمليات المرتبطة بمساحة اسم المستخدم المستهدف C.
- أي UID في مساحة اسم المستخدم الحالية تتوافق مع UIDs في C.
على سبيل المثال ، إذا كتبنا ما يلي من مساحة اسم المستخدم الأصلي P في ملف الخريطة لمساحة الاسم C الفرعية:
0 1000 1 3 0 1
نحن نخبرك Linux بشكل أساسي:
- بالنسبة للعمليات في C ، فإن UIDs الوحيدة الموجودة في النظام هي UIDs
0
و 3
. على سبيل المثال ، ستنتهي مكالمة نظام setuid(9)
دائمًا بشيء مثل معرف مستخدم غير صالح . - UIDs
1000
و 0
في P تتوافق مع UIDs 0
و 3
في C. على سبيل المثال ، إذا تحولت العملية التي تعمل باستخدام UID 1000
في P إلى C ، فستجد أنه بعد التبديل ، أصبح UID الخاص به هو root
0
.
مساحة الاسم وامتياز المالك
في منشور سابق ، ذكرنا أنه عند إنشاء مساحات أسماء جديدة ، يكون الوصول بمستوى المستخدم الخارق مطلوبًا. مساحات أسماء المستخدمين لا تفرض هذا المطلب. في الواقع ، هناك ميزة أخرى وهي أنه يمكنهم امتلاك مساحات أسماء أخرى.
عندما يتم إنشاء مساحة اسم غير مستخدم N ، يعين Linux مساحة اسم المستخدم الحالية P للعملية التي تنشئ N لتكون مالك مساحة الاسم N. إذا تم إنشاء P جنبًا إلى جنب مع مساحات الأسماء الأخرى في نفس استدعاء نظام clone
، يضمن Linux إنشاء P أولاً وجعله مالكًا لمساحات الأسماء الأخرى.
يعد مالك مساحات الأسماء مهمًا لأن العملية التي تتطلب اتخاذ إجراء متميز على مورد ليس مساحة اسم المستخدم سيتم فحص امتيازات UID الخاصة به مقابل مالك مساحة اسم المستخدم هذه ، وليس مساحة اسم المستخدم الجذر. على سبيل المثال ، دعنا نقول أن P هي مساحة اسم المستخدم الأصلية للطفل C ، و P و C يمتلكان مساحة اسم الشبكة الخاصة بهما M و N ، على التوالي. قد لا تتمتع العملية بامتيازات لإنشاء أجهزة الشبكة المضمنة في M ، ولكنها قد تكون قادرة على القيام بذلك من أجل N.
نتيجة وجود مالك مساحة اسم لنا هو أنه يمكننا إسقاط متطلبات sudo
عند تنفيذ الأوامر باستخدام unshare
أو isolate
إذا طلبنا أيضًا إنشاء مساحة اسم مستخدم. على سبيل المثال ، سيتطلب unshare -u bash
sudo
، لكن unshare -Uu bash
لن يكون:
لسوء الحظ ، سوف نعيد تطبيق متطلبات المستخدم الخارق في المنشور التالي ، حيث إن isolate
يحتاج إلى امتيازات root
في مساحة اسم المستخدم الجذر من أجل تكوين مساحة اسم الشبكة والشبكة بشكل صحيح. لكننا بالتأكيد سنتخلى عن امتيازات عملية الفريق للتأكد من أن الفريق لا يملك أذونات غير ضرورية.
كيف يتم حل المعرفات
لقد رأينا للتو عملية تعمل كمستخدم عادي 1000
تحولت فجأة إلى root
. لا تقلق ، لم يكن هناك تصعيد للامتيازات. تذكر أن هذا مجرد معرّف تعيين : طالما أن عمليتنا تعتقد أنه root
على النظام ، يعلم Linux أن root
- في حالته - يعني UID 1000
عادي (بفضل التعيين لدينا). لذلك في وقت تتعرف فيه مساحات الأسماء التي تنتمي إلى مساحة اسم المستخدم الجديدة الخاصة به (مثل مساحة اسم الشبكة في C ) على حقوقه root
، لا يتعرف الآخرون (مثل مساحة اسم الشبكة في P ). لذلك ، لا تستطيع العملية القيام بأي شيء لن يتمكن المستخدم 1000
من القيام به.
عندما تقوم إحدى العمليات في مساحة اسم المستخدم المتداخلة بإجراء عملية تتطلب التحقق من إذن - على سبيل المثال ، إنشاء ملف - تتم مقارنة معرف المستخدم الخاص بها في مساحة اسم المستخدم هذه بمعرف المستخدم المكافئ في مساحة اسم المستخدم الجذر من خلال اجتياز التعيينات في شجرة مساحة الاسم إلى الجذر. هناك حركة في الاتجاه المعاكس ، على سبيل المثال ، عندما يقرأ معرفات المستخدمين ، كما نفعل مع ls -l my_file
. يتم تعيين UID الخاص بالمالك my_file
من مساحة اسم مستخدم الجذر إلى المعرف الحالي ويتم تقديم المعرف النهائي المطابق (أو لا أحد في حالة غياب التعيين في مكان ما على طول الشجرة بأكملها) إلى عملية القراءة.
معرف المجموعة
حتى لو كنا الجذر في C ، فإننا لا نزال nogroup
الرهيبة معرف معرف مجموعتنا. نحتاج فقط إلى القيام بنفس الشيء بالنسبة لـ /proc/$pid/gid_map
. قبل أن نتمكن من القيام بذلك ، نحتاج إلى تعطيل استدعاء نظام setgroups
(هذا ليس ضروريًا إذا كان لدى CAP_SETGID
بالفعل قدرة CAP_SETGID
في P ، لكننا لن نفترض ذلك ، لأن هذا يأتي عادةً مع امتيازات المستخدم الخارق) عن طريق الكتابة "رفض" "إلى ملف proc/$pid/setgroups
:
تطبيق
يمكن العثور على الكود المصدري لهذا المنشور هنا .
كما ترى ، هناك العديد من الصعوبات المرتبطة بإدارة مساحات أسماء المستخدمين ، لكن التنفيذ بسيط للغاية. كل ما نحتاج إلى فعله هو كتابة مجموعة من الخطوط إلى ملف - لقد كان كئيبًا أن نعرف ماذا وأين نكتب. دون مزيد من اللغط ، وهنا أهدافنا:
- استنساخ عملية الفريق في مساحة اسم المستخدم الخاصة به.
- اكتب في ملفات خرائط UID و GID لعملية الفريق.
- إعادة تعيين جميع الامتيازات الخارق قبل تشغيل الأمر.
1
تحقيق 1
خلال إضافة علامة CLONE_NEWUSER
ببساطة إلى استدعاء نظام clone
.
int clone_flags = SIGCHLD | CLONE_NEWUTS | CLONE_NEWUSER;
بالنسبة إلى 2
نضيف الدالة prepare_user_ns
، والتي تمثل بعناية مستخدمًا عاديًا 1000
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); }
وسوف نسميها من العملية الرئيسية في مساحة اسم المستخدم الأصل قبل الإشارة إلى عملية الأمر.
...
بالنسبة للخطوة 3
نقوم بتحديث الدالة cmd_exec
للتأكد من أن الأمر قد تم تنفيذه من المستخدم غير المعتاد 1000
المعتاد الذي قدمناه في التعيين (تذكر أن المستخدم الجذر 0
في مساحة اسم المستخدم لعملية الفريق هو المستخدم 1000
):
...
وهذا كل شيء! isolate
الآن يبدأ العملية في مساحة اسم مستخدم معزولة.
$ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root)
كانت هناك بعض التفاصيل القليلة في هذا المنشور حول كيفية عمل مساحات أسماء المستخدمين ، ولكن في النهاية ، كان إعداد المثيل غير مؤلم نسبيًا. في Dockerfile
التالي ، سننظر في إمكانية تشغيل أمر في مساحة الاسم الخاصة بـ Mount باستخدام isolate
(الكشف عن السر وراء بيان FROM
من Dockerfile
). هناك سنحتاج إلى مساعدة Linux أكثر قليلاً من أجل تكوين المثيل بشكل صحيح.