الغوص العميق في مساحات أسماء Linux ، الجزء الثاني

في الجزء السابق ، غمرنا أصابع قدمنا ​​في مياه مساحة الاسم ورأينا في نفس الوقت مدى سهولة بدء العملية في مساحة اسم 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 #    ,     user namespace C$ whoami nobody C$ id uid=65534(nobody) gid=65534(nogroup) C$ ls -l my_file -rw-r--r-- 1 nobody nogroup 0 May 18 16:00 my_file 

انتظر لحظة ، من؟ الآن بعد أن أصبحنا في قشرة متداخلة في 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 #   user namespace... C$ echo $$ 1409 # C      ,     C$ cat /proc/1409/uid_map #  #   namespace P      # UIDs    UID    P$ cat /proc/1442/uid_map 0 0 4294967295 # UIDs  0  4294967294  P  #  4294967295 -  ID no user -  C. C$ cat /proc/1409/uid_map 0 4294967295 4294967295 

حسنًا ، لم يكن هذا أمرًا مثيرًا للغاية ، لأن هاتين الحالتين كانتا متطرفتين ، لكن هذا يوضح بعض الأشياء:


  1. سيكون لمساحة المستخدم التي تم إنشاؤها حديثًا ملفات خرائط فارغة بالفعل.
  2. UID 4294967295 غير قابل للتعيين وغير مناسب للاستخدام حتى في مساحة اسم المستخدم root . يستخدم Linux UID هذا بشكل محدد للإشارة إلى عدم وجود معرف مستخدم .

كتابة ملفات خريطة UID


لإصلاح مساحة اسم المستخدم التي تم إنشاؤها حديثًا C ، نحتاج فقط إلى توفير مناظراتنا الضرورية عن طريق كتابة محتوياتها لملفات الخريطة لأي عملية تخص C (لا يمكننا تحديث هذا الملف بعد الكتابة إليه). الكتابة إلى هذا الملف تخبر Linux شيئين:


  1. ما هي معرفات UID المتاحة للعمليات المرتبطة بمساحة اسم المستخدم المستهدف C.
  2. أي UID في مساحة اسم المستخدم الحالية تتوافق مع UIDs في C.

على سبيل المثال ، إذا كتبنا ما يلي من مساحة اسم المستخدم الأصلي P في ملف الخريطة لمساحة الاسم C الفرعية:


 0 1000 1 3 0 1 

نحن نخبرك Linux بشكل أساسي:


  1. بالنسبة للعمليات في C ، فإن UIDs الوحيدة الموجودة في النظام هي UIDs 0 و 3 . على سبيل المثال ، ستنتهي مكالمة نظام setuid(9) دائمًا بشيء مثل معرف مستخدم غير صالح .
  2. 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 لن يكون:


 # UID 1000 --      user namespace P. P$ id uid=1000(iffy) gid=1000(iffy) #           # network namespace. P$ ip link add type veth RTNETLINK answers: Operation not permitted #     ,     #  user  network namespace P$ unshare -nU bash # :  sudo C$ ip link add type veth RTNETLINK answers: Operation not permitted # ,  . ,  # UID 0 (root)    ,  #     nobody.   . C$ echo $$ 13294 #   P,   UID 1000  P  UID 0  C P$ echo "0 1000 1" > /proc/13294/uid_map #   ? C$ id uid=0(root) gid=65534(nogroup) C$ ip link add type veth # ! 

لسوء الحظ ، سوف نعيد تطبيق متطلبات المستخدم الخارق في المنشور التالي ، حيث إن 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 :


 #  13294 -- pid  unshared  C$ id uid=0(root) gid=65534(nogroup) P$ echo deny > /proc/13294/setgroups P$ echo "0 1000 1" > /proc/13294/gid_map #  group ID   C$ id uid=0(root) gid=0(root) 

تطبيق


يمكن العثور على الكود المصدري لهذا المنشور هنا .

كما ترى ، هناك العديد من الصعوبات المرتبطة بإدارة مساحات أسماء المستخدمين ، لكن التنفيذ بسيط للغاية. كل ما نحتاج إلى فعله هو كتابة مجموعة من الخطوط إلى ملف - لقد كان كئيبًا أن نعرف ماذا وأين نكتب. دون مزيد من اللغط ، وهنا أهدافنا:


  1. استنساخ عملية الفريق في مساحة اسم المستخدم الخاصة به.
  2. اكتب في ملفات خرائط UID و GID لعملية الفريق.
  3. إعادة تعيين جميع الامتيازات الخارق قبل تشغيل الأمر.

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); } 

وسوف نسميها من العملية الرئيسية في مساحة اسم المستخدم الأصل قبل الإشارة إلى عملية الأمر.


  ... //      . int pipe = params.fd[1]; //      namespace ... prepare_userns(cmd_pid); //   ,     . ... 

بالنسبة للخطوة 3 نقوم بتحديث الدالة cmd_exec للتأكد من أن الأمر قد تم تنفيذه من المستخدم غير المعتاد 1000 المعتاد الذي قدمناه في التعيين (تذكر أن المستخدم الجذر 0 في مساحة اسم المستخدم لعملية الفريق هو المستخدم 1000 ):


  ... //   ' '   . await_setup(params->fd[0]); if (setgid(0) == -1) die("Failed to setgid: %m\n"); if (setuid(0) == -1) die("Failed to setuid: %m\n"); ... 

وهذا كل شيء! isolate الآن يبدأ العملية في مساحة اسم مستخدم معزولة.


 $ ./isolate sh ===========sh============ $ id uid=0(root) gid=0(root) 

كانت هناك بعض التفاصيل القليلة في هذا المنشور حول كيفية عمل مساحات أسماء المستخدمين ، ولكن في النهاية ، كان إعداد المثيل غير مؤلم نسبيًا. في Dockerfile التالي ، سننظر في إمكانية تشغيل أمر في مساحة الاسم الخاصة بـ Mount باستخدام isolate (الكشف عن السر وراء بيان FROM من Dockerfile ). هناك سنحتاج إلى مساعدة Linux أكثر قليلاً من أجل تكوين المثيل بشكل صحيح.

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


All Articles