وصف Linux لاستدعاءات نظام kernel. Man syscalls (2): مكالمات نظام Linux
في أغلب الأحيان، يتم تعريف رمز استدعاء النظام بالرقم __NR_xxx، في /usr/include/asm/unistd.h، يمكن العثور عليها في مصدر الرمزنواة لينكس في الوظائف sys_xxx(). (يمكن العثور على جدول الاتصال لـ i386 في /usr/src/linux/arch/i386/kernel/entry.S.) هناك العديد من الاستثناءات لهذه القاعدة، ويرجع ذلك أساسًا إلى حقيقة أن معظم استدعاءات النظام القديمة يتم استبدالها باستدعاءات جديدة، دون أي نظام. على منصات محاكاة أنظمة التشغيل الخاصة مثل parisc، وsparc، وsparc64، وalpha، هناك العديد من استدعاءات النظام الإضافية؛ يحتوي mips64 أيضًا على مجموعة كاملة من مكالمات النظام 32 بت.
مع مرور الوقت، حدثت تغييرات في واجهة بعض مكالمات النظام، إذا لزم الأمر. أحد أسباب هذه التغييرات هو الحاجة إلى زيادة حجم الهياكل أو القيم العددية التي تم تمريرها إلى استدعاء النظام. وبسبب هذه التغييرات، ظهرت في بعض البنيات (أي i386 الأقدم 32 بت) مجموعات مختلفة من استدعاءات النظام المماثلة (على سبيل المثال، اقتطاع(2 و اقتطاع64(2))، والتي تؤدي نفس المهام، ولكنها تختلف في حجم وسيطاتها. (كما ذكرنا، هذا لا يؤثر على التطبيقات: تقوم وظائف غلاف glibc ببعض الأعمال لتشغيل استدعاء النظام الصحيح، وهذا يضمن توافق ABI مع الثنائيات الأقدم.) أمثلة على استدعاءات النظام التي لها إصدارات متعددة:
*يوجد حاليًا ثلاثة إصدارات مختلفة القانون الأساسي(2): sys_stat() (مكان __NR_oldstat), sys_newstat() (مكان __NR_stat) و sys_stat64() (مكان __NR_stat64)، ويستخدم هذا الأخير في هذه اللحظة. حالة مماثلة مع lstat(2 و com.fstat(2). * تعريف بالمثل __NR_oldolduname, __NR_oldunameو __NR_unameللمكالمات sys_olduname(), sys_uname() و sys_newuname(). * ظهرت في لينكس 2.0 نسخة جديدة vm86(٢) جديد و نسخة قديمةتسمى الإجراءات النووية sys_vm86old() و sys_vm86(). * لينكس 2.4 لديه نسخة جديدة com.getrlimit(2) تسمى الإصدارات الجديدة والقديمة من الإجراءات النووية sys_old_getrlimit() (مكان __NR_getrlimit) و sys_getrlimit() (مكان __NR_ugetrlimit). * في Linux 2.4، تمت زيادة حجم حقول معرف المستخدم والمجموعة من 16 إلى 32 بت. تمت إضافة العديد من استدعاءات النظام لدعم هذا التغيير (على سبيل المثال: chown32(2), getuid32(2), getgroups32(2), setresuid32(2))، حذف المكالمات السابقة بنفس الأسماء، ولكن بدون اللاحقة "32". * أضاف Linux 2.4 دعمًا للوصول إلى الملفات الكبيرة (التي لا تتناسب أحجامها وإزاحاتها مع 32 بت) في التطبيقات ذات بنيات 32 بت. يتطلب هذا إجراء تغييرات على استدعاءات النظام التي تعمل مع أحجام الملفات والإزاحات. تمت إضافة مكالمات النظام التالية: fcntl64(2), getdents64(2), stat64(2), statfs64(2), اقتطاع64(2) ونظائرها، التي تتعامل مع واصفات الملفات أو الروابط الرمزية. تقوم استدعاءات النظام هذه بإزالة استدعاءات النظام القديمة، والتي تم تسميتها أيضًا، باستثناء استدعاءات "stat"، ولكنها لا تحتوي على اللاحقة "64".
في الأنظمة الأساسية الأحدث التي تتمتع فقط بإمكانية الوصول إلى الملفات 64 بت وUID/GID 32 بت (مثل alpha وia64 وs390x وx86-64)، يوجد إصدار واحد فقط من استدعاءات النظام للوصول إلى UID/GID والوصول إلى الملفات. على الأنظمة الأساسية (عادةً الأنظمة الأساسية 32 بت) حيث تتوفر مكالمات *64 و*32، تكون الإصدارات الأخرى قديمة.
* التحديات رت_سيج*تمت إضافته إلى kernel 2.2 لدعم الإشارات الإضافية في الوقت الفعلي (انظر kernel 2.2). الإشارة(7)). تحل استدعاءات النظام هذه محل استدعاءات النظام القديمة التي تحمل نفس الأسماء، ولكن بدون البادئة "rt_". * في مكالمات النظام يختار(2 و mmap(2) تم استخدام خمس وسيطات أو أكثر، مما تسبب في حدوث مشكلات في تحديد كيفية تمرير الوسائط على i386. ونتيجة لذلك، بينما في البنى الأخرى يدعو sys_select() و sys_mmap() مباراة __NR_selectو __NR_mmap، على i386 أنها تتوافق old_select() و old_mmap() (الإجراءات التي تستخدم مؤشرًا لكتلة من الوسائط). حاليا لم تعد هناك مشكلة في تمرير أكثر من خمس وسيطات، وهناك __NR__newselect، وهو ما يتوافق تمامًا sys_select()، ونفس الوضع مع __NR_mmap2.
فلاديمير ميشكوف
اعتراض مكالمات النظام في نظام التشغيل Linux
في السنوات الأخيرة، احتل نظام التشغيل Linux مكانة رائدة كمنصة خادم، متفوقًا على العديد من التطورات التجارية. ومع ذلك، قضايا الحماية نظم المعلومات، المبني على أساس نظام التشغيل هذا، لا يتوقف عن كونه ذا صلة. موجود عدد كبير من الوسائل التقنية، سواء البرامج أو الأجهزة، مما يساعد على ضمان أمان النظام. هذه هي أدوات تشفير البيانات و ازدحام انترنت، التفريق بين حقوق الوصول إلى مصادر المعلومات، حماية بريد إلكترونيخوادم الويب، الحماية من الفيروسات، إلخ. القائمة، كما تفهم، طويلة جدًا. ندعوك في هذه المقالة إلى النظر في آلية الحماية التي تعتمد على اعتراض مكالمات نظام التشغيل. أنظمة لينكس. تسمح لك هذه الآلية بالتحكم في تشغيل أي تطبيق وبالتالي منع الإجراءات التدميرية المحتملة التي قد يقوم بها.
مكالمات النظام
لنبدأ بالتعريف. استدعاءات النظام هي مجموعة من الوظائف التي يتم تنفيذها في نواة نظام التشغيل. تتم ترجمة أي طلب تطبيق مستخدم في النهاية إلى استدعاء نظام ينفذ الإجراء المطلوب. توجد قائمة كاملة باستدعاءات نظام Linux في الملف /usr/include/asm/unistd.h. دعونا نلقي نظرة على الآلية العامة لتنفيذ مكالمات النظام مع مثال. اسمح لمصدر التطبيق الخاص بك باستدعاء الدالة create() لإنشاء ملف جديد. يقوم المترجم، بعد أن واجه استدعاء لهذه الوظيفة، بتحويلها إلى رمز التجميع، وتحميل رقم استدعاء النظام المطابق لهذه الوظيفة ومعلماتها في سجلات المعالج ثم استدعاء المقاطعة 0x80. يتم تحميل القيم التالية في سجلات المعالج:
- لتسجيل EAX– رقم استدعاء النظام. لذلك، في حالتنا، سيكون رقم استدعاء النظام هو 8 (انظر __NR_creat)؛
- إلى سجل EBX- المعلمة الأولى للوظيفة (لإنشاء هذا هو مؤشر إلى سطر يحتوي على اسم الملف الذي يتم إنشاؤه)؛
- إلى سجل ECX- المعلمة الثانية (حقوق الوصول إلى الملف).
يتم تحميل المعلمة الثالثة في سجل EDX؛ لإجراء مكالمة نظام في نظام التشغيل Linux، استخدم وظيفة system_call، المحددة في الملف /usr/src/liux/arch/i386/kernel/entry.S. هذه الوظيفة هي نقطة الدخول لجميع مكالمات النظام. تستجيب النواة للمقاطعة 0x80 عن طريق استدعاء الدالة system_call، والتي تعد في الأساس معالجًا للمقاطعة 0x80.
للتأكد من أننا على المسار الصحيح، دعونا نكتب جزءًا صغيرًا من الاختبار بلغة التجميع. سنرى فيه ما تتحول إليه وظيفة create() بعد التجميع. دعنا نسمي الملف test.S. وهنا محتواه:
Globl_start
نص
يبدأ:
نقوم بتحميل رقم استدعاء النظام في سجل EAX:
موفل $8،٪ eax
في سجل EBX – المعلمة الأولى، مؤشر إلى السطر الذي يحمل اسم الملف:
movl $filename، %ebx
في سجل ECX - المعلمة الثانية، حقوق الوصول:
نقل $0، %ecx
استدعاء المقاطعة:
كثافة العمليات $0x80
نخرج من البرنامج. للقيام بذلك، استدعاء وظيفة الخروج (0):
movl $1، %eax movl $0، %ebx int $0x80
في مقطع البيانات، نشير إلى اسم الملف الذي سيتم إنشاؤه:
بيانات
اسم الملف: سلسلة "file.txt"
تجميع:
اختبار دول مجلس التعاون الخليجي -c
اختبار ld -s -o test.o
سيظهر اختبار الملف القابل للتنفيذ في الدليل الحالي. من خلال تشغيله سوف نقوم بإنشاء ملف جديداسمه file.txt.
الآن دعنا نعود إلى النظر في آلية استدعاء النظام. لذا، فإن النواة تستدعي معالج المقاطعة 0x80 - وظيفة system_call. يضع System_call نسخًا من السجلات التي تحتوي على معلمات الاستدعاء على المكدس باستخدام الماكرو SAVE_ALL ويستدعي وظيفة النظام المطلوبة باستخدام أمر الاستدعاء. يوجد جدول المؤشرات لوظائف kernel التي تنفذ استدعاءات النظام في مصفوفة sys_call_table (راجع الملف Arch/i386/kernel/entry.S). يعد رقم استدعاء النظام الموجود في سجل EAX بمثابة فهرس في هذه المصفوفة. وبالتالي، إذا تم العثور على القيمة 8 في EAX، فسيتم استدعاء وظيفة kernel sys_creat(). لماذا هناك حاجة إلى الماكرو SAVE_ALL؟ الشرح هنا بسيط جداً. نظرًا لأن جميع وظائف نظام kernel تقريبًا مكتوبة بلغة C، فإنها تبحث عن معلماتها على المكدس. ويتم دفع المعلمات إلى المكدس باستخدام الماكرو SAVE_ALL! يتم تخزين القيمة المرجعة لاستدعاء النظام في سجل EAX.
الآن دعونا نتعرف على كيفية اعتراض مكالمة النظام. ستساعدنا آلية وحدات kernel القابلة للتحميل في ذلك. على الرغم من أننا قد نظرنا سابقًا في تطوير واستخدام وحدات النواة، فمن أجل الاتساق في تقديم المادة، سننظر بإيجاز في ماهية وحدة النواة، ومما تتكون، وكيف تتفاعل مع النظام.
وحدة النواة القابلة للتحميل
وحدة kernel القابلة للتحميل (دعنا نشير إليها LKM - وحدة Kernel القابلة للتحميل) هي كود برنامج يتم تنفيذه في مساحة kernel. الميزة الرئيسية لـ LKM هي القدرة على التحميل والتفريغ ديناميكيًا دون الحاجة إلى إعادة تشغيل النظام بأكمله أو إعادة ترجمة النواة.
يتكون كل LKM من وظيفتين رئيسيتين (الحد الأدنى):
- وظيفة تهيئة الوحدة. يتم استدعاؤه عند تحميل LKM في الذاكرة:
int_module(باطل) (...)
- وظيفة تفريغ الوحدة:
وحدة تنظيف باطلة (باطلة) (...)
فيما يلي مثال لوحدة بسيطة:
#تعريف الوحدة النمطية
#يشمل
int_module(باطل)
printk("مرحبا بالعالم");
العودة 0؛
وحدة تنظيف باطلة (باطلة)
برينتك("وداعا");
ترجمة وتحميل الوحدة. يتم تحميل الوحدة في الذاكرة باستخدام الأمر insmod:
دول مجلس التعاون الخليجي -c -O3 helloworld.c
insmod helloworld.o
توجد معلومات حول جميع الوحدات المحملة حاليًا في النظام في الملف /proc/modules. للتحقق من تحميل الوحدة، أدخل الأمر cat /proc/modules أو lsmod. يقوم الأمر rmmod بتفريغ الوحدة النمطية:
rmmod helloworld
خوارزمية اعتراض مكالمات النظام
لتنفيذ وحدة تعترض استدعاء النظام، من الضروري تحديد خوارزمية الاعتراض. الخوارزمية هي على النحو التالي:
- حفظ مؤشر الاستدعاء الأصلي (الأصلي) بحيث يمكن استعادته؛
- إنشاء وظيفة تنفذ استدعاء نظام جديد؛
- في جدول مكالمات النظام sys_call_table، استبدل المكالمات، على سبيل المثال. إعداد مؤشر مناسب لاستدعاء النظام الجديد؛
- عند الانتهاء من العمل (عند تفريغ الوحدة)، قم باستعادة استدعاء النظام الأصلي باستخدام المؤشر المحفوظ مسبقًا.
يتيح لك التتبع معرفة مكالمات النظام التي يتم استخدامها عند تشغيل تطبيق المستخدم. ومن خلال التتبع، يمكنك تحديد استدعاء النظام الذي يجب اعتراضه للتحكم في التطبيق. سيتم مناقشة مثال على استخدام برنامج التتبع أدناه.
لدينا الآن معلومات كافية لبدء دراسة أمثلة لتطبيقات الوحدات النمطية التي تعترض مكالمات النظام.
أمثلة على اعتراض مكالمات النظام
حظر إنشاء الدليل
عند إنشاء دليل، يتم استدعاء وظيفة kernel sys_mkdir. المعلمة عبارة عن سلسلة تحتوي على الاسم الدليل الذي يتم إنشاؤه. دعونا نلقي نظرة على الكود الذي يعترض استدعاء النظام المقابل.
#يشمل
#يشمل
#يشمل
تصدير جدول مكالمات النظام:
الفراغ الخارجي *sys_call_table;
لنحدد مؤشرًا لحفظ استدعاء النظام الأصلي:
int (*orig_mkdir)(const char *path);
لنقم بإنشاء مكالمة نظام خاصة بنا. مكالمتنا لا تفعل شيئًا، فقط تُرجع قيمة فارغة:
int own_mkdir(const char *path)
العودة 0؛
أثناء تهيئة الوحدة، نحفظ مؤشرًا للاستدعاء الأصلي ونستبدل استدعاء النظام:
int_module()
orig_mkdir=sys_call_table;
sys_call_table=own_mkdir; العودة 0؛
عند التفريغ، نقوم باستعادة المكالمة الأصلية:
باطلة cleanup_module ()
Sys_call_table=orig_mkdir;
سنقوم بحفظ الكود في الملف sys_mkdir_call.c. للحصول على وحدة نمطية للكائن، قم بإنشاء ملف Makefile بالمحتوى التالي:
CC = دول مجلس التعاون الخليجي
CFLAGS = -O3 -Wall -fomit-frame-pointer
sys_mkdir_call.o: sys_mkdir_call.c
$(CC) -c $(CFLAGS) $(MODFLAGS) sys_mkdir_call.c
استخدم الأمر make لإنشاء وحدة kernel. بعد تنزيله، سنحاول إنشاء دليل باستخدام الأمر mkdir. كما ترون، لا يحدث شيء. الأمر لا يعمل. لاستعادة وظائفه، ما عليك سوى إلغاء تحميل الوحدة.
منع قراءة ملف
لقراءة ملف، يجب أولاً فتحه باستخدام وظيفة الفتح. من السهل تخمين أن هذه الوظيفة تتوافق مع استدعاء النظام sys_open. ومن خلال اعتراضه، يمكننا حماية الملف من القراءة. دعونا نلقي نظرة على تنفيذ وحدة اعتراضية.
#يشمل
#يشمل
#يشمل
#يشمل
#يشمل
#يشمل
#يشمل
الفراغ الخارجي *sys_call_table;
مؤشر لحفظ استدعاء النظام الأصلي:
int (*orig_open)(const char *pathname, int flag, int mode);
المعلمة الأولى للوظيفة المفتوحة هي اسم الملف المراد فتحه. يجب أن يقوم استدعاء النظام الجديد بمقارنة هذه المعلمة باسم الملف الذي نريد حمايته. إذا تطابقت الأسماء، سيتم محاكاة خطأ في فتح الملف. تبدو مكالمة النظام الجديدة لدينا كما يلي:
int own_open(const char *pathname, int flag, int mode)
لنضع اسم الملف الذي سيتم فتحه هنا:
شار *kernel_path;
اسم الملف الذي نريد حمايته:
إخفاء الحرف = "test.txt"
لنخصص الذاكرة وننسخ اسم الملف الذي سيتم فتحه هناك:
kernel_path=(char *)kmalloc(255,GFP_KERNEL);
Copy_from_user(kernel_path, pathname, 255);
فلنقارن:
إذا (strstr(kernel_path,(char *)&hide) != NULL) (
نقوم بتحرير الذاكرة وإرجاع رمز خطأ إذا كانت الأسماء متطابقة:
kfree(kernel_path);
عودة -ENOENT؛
آخر(
إذا كانت الأسماء غير متطابقة، فإننا نستدعي استدعاء النظام الأصلي لتنفيذ الإجراء القياسي لفتح ملف:
kfree(kernel_path);
إرجاع orig_open(pathname, flag, mode);
int_module()
orig_open=sys_call_table;
sys_call_table=own_open;
العودة 0؛
باطلة cleanup_module ()
sys_call_table=orig_open;
لنحفظ الكود في الملف sys_open_call.c وننشئ ملف Makefile للحصول على وحدة الكائن:
CC = دول مجلس التعاون الخليجي
CFLAGS = -O2 -Wall -fomit-frame-pointer
MODFLAGS = -D__KERNEL__ -DMODULE -I/usr/src/linux/include
sys_open_call.o: sys_open_call.c
$(CC) -c $(CFLAGS) $(MODFLAGS) sys_open_call.c
في الدليل الحالي، قم بإنشاء ملف يسمى test.txt، ثم قم بتحميل الوحدة وأدخل الأمر cat test.txt. سيقوم النظام بالإبلاغ عن عدم وجود ملف بهذا الاسم.
بصراحة، من السهل تجاوز هذه الحماية. يكفي استخدام الأمر mv لإعادة تسمية الملف ثم قراءة محتوياته.
إخفاء إدخال ملف في الدليل
دعونا نحدد استدعاء النظام المسؤول عن قراءة محتويات الدليل. للقيام بذلك، دعونا نكتب جزءًا اختباريًا آخر يقرأ الدليل الحالي:
/* الملف dir.c*/
#يشمل
#يشمل
انت مين()
دير * د؛
البنية المباشرة *dp;
د = opendir(".");
موانئ دبي = قراءة(د);
العودة 0؛
دعنا نحصل على الوحدة القابلة للتنفيذ:
دول مجلس التعاون الخليجي -o dir dir.c
وتتبعه:
ستراس ./دير
دعونا ننتبه إلى السطر قبل الأخير:
getdents(6, /* 4 إدخالات*/, 3933) = 72;
تتم قراءة محتويات الدليل بواسطة وظيفة getdents. يتم تخزين النتيجة كقائمة بنيات من النوع struct dirent. المعلمة الثانية لهذه الوظيفة هي مؤشر لهذه القائمة. ترجع الدالة طول كافة الإدخالات في الدليل. في مثالنا، حددت وظيفة getdents وجود أربعة إدخالات في الدليل الحالي - "."، ".." والملفين لدينا، الوحدة القابلة للتنفيذ والنص المصدر. طول كافة الإدخالات في الدليل هو 72 بايت. يتم تخزين المعلومات حول كل سجل، كما قلنا من قبل، في البنية المباشرة. يهمنا مجالان من هذا الهيكل:
- d_reclen- حجم السجل؛
- d_name- اسم الملف.
لإخفاء إدخال حول ملف (بمعنى آخر، جعله غير مرئي)، تحتاج إلى اعتراض استدعاء النظام sys_getdents، والعثور على الإدخال المقابل في قائمة الهياكل المستلمة، وحذفه. دعونا نلقي نظرة على الكود الذي ينفذ هذه العملية (مؤلف الكود الأصلي هو Michal Zalewski):
الفراغ الخارجي *sys_call_table;
int (*orig_getdents)(u_int, struct dirent *, u_int);
دعونا نحدد استدعاء نظامنا.
int own_getdents(u_int fd, struct dirent *dirp, u_int count)
int غير الموقعة tmp، n؛
كثافة العمليات؛
سيتم عرض الغرض من المتغيرات أدناه. بالإضافة إلى ذلك، نحن بحاجة إلى الهياكل:
البنية المباشرة *dirp2, *dirp3;
اسم الملف الذي نريد إخفاءه:
شار إخفاء = "our.file"؛
لنحدد طول الإدخالات في الدليل:
tmp=(*orig_getdents)(fd,dirp,count);
إذا (تمب> 0)(
لنخصص ذاكرة للبنية في مساحة kernel وننسخ محتويات الدليل إليها:
dirp2=(struct dirent *)kmalloc(tmp,GFP_KERNEL);
сopy_from_user(dirp2,dirp,tmp);
لنستخدم البنية الثانية ونحفظ طول الإدخالات في الدليل:
dirp3=dirp2;
t=tmp;
لنبدأ بالبحث عن ملفنا:
بينما(ر>0) (
نقرأ طول الإدخال الأول ونحدد طول الإدخالات المتبقية في الدليل:
n=dirp3->d_reclen;
t-=n;
نتحقق مما إذا كان اسم الملف من الإدخال الحالي يطابق الاسم الذي نبحث عنه:
إذا (strstr((char *)&(dirp3->d_name),(char *)&hide) != NULL) (
إذا كان الأمر كذلك، فاستبدل الإدخال واحسب الطول الجديد للإدخالات في الدليل:
memcpy(dirp3,(char *)dirp3+dirp3->d_reclen,t);
tmp-=n;
نضع المؤشر على الإدخال التالي ونواصل البحث:
dirp3=(struct dirent *)((char *)dirp3+dirp3->d_reclen);
نعيد النتيجة ونحرر الذاكرة:
Copy_to_user(dirp,dirp2,tmp);
kfree(dirp2);
إرجاع طول الإدخالات في الدليل:
عودة تمب؛
وظائف تهيئة الوحدة النمطية وتفريغها لها نموذج قياسي:
int_module(باطل)
orig_getdents=sys_call_table;
sys_call_table=own_getdents;
العودة 0؛
باطلة cleanup_module ()
sys_call_table=orig_getdents;
لنحفظ النص المصدر في الملف sys_call_getd.c وننشئ ملف Makefile بالمحتوى التالي:
CC = دول مجلس التعاون الخليجي
الوحدة النمطية = sys_call_getd.o
CFLAGS = -O3 -الجدار
لينكس = /usr/src/linux
MODFLAGS = -D__KERNEL__ -DMODULE -I$(LINUX)/تشمل
sys_call_getd.o: sys_call_getd.c $(CC) -c
$(CFLAGS) $(MODFLAGS) sys_call_getd.c
في الدليل الحالي، قم بإنشاء ملف Our.file وقم بتحميل الوحدة. يختفي الملف، وهو ما يجب إثباته.
كما تفهم، ليس من الممكن النظر في مثال لاعتراض كل استدعاء للنظام في مقال واحد. لذلك أنصح المهتمين بهذا الموضوع بزيارة المواقع:
هناك يمكنك العثور على أمثلة أكثر تعقيدًا وإثارة للاهتمام لاعتراض مكالمات النظام. يرجى الكتابة عن جميع التعليقات والاقتراحات في منتدى المجلة.
عند إعداد المقال تم استخدام مواد من الموقع
هذه المادة عبارة عن تعديل للمقال الذي يحمل نفس الاسم بقلم فلاديمير ميشكوف المنشور في مجلة "System Administrator"
هذه المادة عبارة عن نسخة من مقالات فلاديمير ميشكوف من مجلة "مسؤول النظام". يمكن العثور على هذه المقالات باستخدام الروابط أدناه. تم أيضًا تغيير بعض الأمثلة على الأكواد المصدرية للبرامج - وتحسينها ووضعها في صيغتها النهائية. (تم تعديل المثال 4.2 بشكل كبير، حيث كان علينا اعتراض مكالمة نظام مختلفة قليلاً) عناوين URL: http://www.samag.ru/img/uploaded/p.pdf http://www.samag.ru/img/uploaded /a3.pdf
هل لديك أسئلة؟ ثم هنا تذهب: [البريد الإلكتروني محمي]
- 2. وحدة النواة القابلة للتحميل
- 4. أمثلة على اعتراض مكالمات النظام بناءً على LKM
- 4.1 حظر إنشاء الدليل
1. منظر عام لبنية Linux
تتيح لنا الرؤية الأكثر عمومية رؤية نموذج من مستويين للنظام. نواة<=>بروغ في الوسط (يسار) هو جوهر النظام. تتفاعل النواة مباشرة مع أجهزة الكمبيوتر، مما يؤدي إلى عزل البرامج التطبيقية عن الميزات المعمارية. تحتوي النواة على مجموعة من الخدمات المقدمة للبرامج التطبيقية. تتضمن خدمات النواة عمليات الإدخال/الإخراج (فتح الملفات وقراءتها وكتابتها وإدارتها)، وإنشاء العمليات وإدارتها، ومزامنتها، والتواصل بين العمليات. تطلب جميع التطبيقات خدمات kernel من خلال مكالمات النظام.يتكون المستوى الثاني من التطبيقات أو المهام، سواء تلك الخاصة بالنظام، والتي تحدد وظائف النظام، أو التطبيقات، التي توفر واجهة مستخدم Linux. ومع ذلك، على الرغم من عدم التجانس الخارجي للتطبيقات، فإن مخططات التفاعل مع النواة هي نفسها.
يحدث التفاعل مع النواة من خلال واجهة استدعاء النظام القياسية. تمثل واجهة استدعاء النظام مجموعة من خدمات kernel وتحدد تنسيق طلبات الخدمة. تطلب العملية خدمة من خلال استدعاء النظام لإجراء محدد في kernel، يشبه في المظهر استدعاء دالة المكتبة العادية. تقوم النواة، نيابة عن العملية، بتنفيذ الطلب وإرجاع البيانات الضرورية إلى العملية.
في المثال أعلاه، يقوم البرنامج بفتح ملف، وقراءة البيانات منه، ثم إغلاق الملف. في هذه الحالة، يتم إجراء عملية فتح (فتح) وقراءة (قراءة) وإغلاق (إغلاق) ملف بواسطة النواة بناءً على طلب المهمة، ويتم فتح (2) وقراءة (2) وإغلاق (2) ) الوظائف هي مكالمات النظام.
/* المصدر 1.0 */ #include
- في سجل EAX - رقم مكالمة النظام. لذلك، في حالتنا، رقم استدعاء النظام هو 5 (انظر __NR_open).
- في سجل EBX - المعلمة الأولى للوظيفة (لـ open() - هذا مؤشر إلى سلسلة تحتوي على اسم الملف الذي سيتم فتحه.
- إلى سجل ECX - المعلمة الثانية (حقوق الوصول إلى الملف)
للتأكد من أننا نسير على الطريق الصحيح، دعونا نلقي نظرة على كود الدالة open() في مكتبة نظام libc:
# gdb -q /lib/libc.so.6 (gdb) disas open تفريغ كود المجمّع للوظيفة المفتوحة: 0x000c8080
الآن دعنا نعود إلى النظر في آلية استدعاء النظام. لذلك، تستدعي النواة معالج المقاطعة 0x80 - وظيفة system_call. يضع System_call نسخًا من السجلات التي تحتوي على معلمات الاستدعاء على المكدس باستخدام الماكرو SAVE_ALL ويستدعي وظيفة النظام المطلوبة باستخدام أمر الاستدعاء. يوجد جدول المؤشرات لوظائف kernel التي تنفذ استدعاءات النظام في مصفوفة sys_call_table (راجع الملف Arch/i386/kernel/entry.S). يعد رقم استدعاء النظام الموجود في سجل EAX بمثابة فهرس في هذه المصفوفة. وبالتالي، إذا كانت EAX تحتوي على القيمة 5، فسيتم استدعاء دالة kernel sys_open(). لماذا هناك حاجة إلى الماكرو SAVE_ALL؟ الشرح هنا بسيط جداً. نظرًا لأن جميع وظائف نظام kernel تقريبًا مكتوبة بلغة C، فإنها تبحث عن معلماتها على المكدس. ويتم دفع المعلمات إلى المكدس باستخدام SAVE_ALL! يتم تخزين القيمة المرجعة لاستدعاء النظام في سجل EAX.
الآن دعونا نتعرف على كيفية اعتراض مكالمة النظام. ستساعدنا آلية وحدات kernel القابلة للتحميل في ذلك.
2. وحدة النواة القابلة للتحميل
وحدة Kernel القابلة للتحميل (الاختصار الشائع LKM - وحدة Kernel القابلة للتحميل) هي رمز برنامج يتم تنفيذه في مساحة kernel. الميزة الرئيسية لـ LKM هي القدرة على التحميل والتفريغ ديناميكيًا دون الحاجة إلى إعادة تشغيل النظام بأكمله أو إعادة ترجمة النواة.يتكون كل LKM من وظيفتين رئيسيتين (الحد الأدنى):
- وظيفة تهيئة الوحدة. يتم استدعاؤه عند تحميل LKM في الذاكرة: int init_module(void) ( ... )
- وظيفة تفريغ الوحدة النمطية: void cleanup_module(void) (...)
3. خوارزمية اعتراض مكالمات النظام على أساس LKM
لتنفيذ وحدة تعترض استدعاء النظام، من الضروري تحديد خوارزمية الاعتراض. الخوارزمية هي على النحو التالي:- احفظ مؤشرًا على الاستدعاء الأصلي (الأصلي) حتى يمكن استعادته
- إنشاء وظيفة تنفذ استدعاء نظام جديد
- في جدول مكالمات النظام sys_call_table، استبدل المكالمات، أي قم بإعداد مؤشر مطابق لاستدعاء نظام جديد
- عند الانتهاء من العمل (عند تفريغ الوحدة)، قم باستعادة استدعاء النظام الأصلي باستخدام المؤشر المحفوظ مسبقًا
4. أمثلة على اعتراض مكالمات النظام بناءً على LKM
4.1 حظر إنشاء الدليل
عند إنشاء دليل، يتم استدعاء وظيفة kernel sys_mkdir. المعلمة عبارة عن سلسلة تحتوي على اسم الدليل الذي سيتم إنشاؤه. دعونا نلقي نظرة على الكود الذي يعترض استدعاء النظام المقابل. /* المصدر 4.1 */ #include4.2 إخفاء إدخال ملف في الدليل
دعونا نحدد استدعاء النظام المسؤول عن قراءة محتويات الدليل. للقيام بذلك، دعونا نكتب جزءًا اختباريًا آخر يقرأ الدليل الحالي: /* المصدر 4.2.1 */ #include- d_reclen - حجم السجل
- d_name - اسم الملف
5. طريقة الوصول المباشر إلى مساحة عنوان kernel /dev/kmem
دعونا نفكر أولاً من الناحية النظرية في كيفية تنفيذ الاعتراض باستخدام الوصول المباشر إلى مساحة عنوان kernel، ثم سننتقل إلى التنفيذ العملي.يتم توفير الوصول المباشر إلى مساحة عنوان kernel بواسطة ملف الجهاز /dev/kmem. يعرض هذا الملف كافة مساحة العنوان الظاهرية المتوفرة، بما في ذلك قسم المبادلة. للعمل مع ملف kmem، يتم استخدام وظائف النظام القياسية - open()، read()، write(). من خلال فتح /dev/kmem بالطريقة القياسية، يمكننا الوصول إلى أي عنوان في النظام، وتعيينه كإزاحة في هذا الملف. هذه الطريقةتم تطويره بواسطة سيلفيو سيزار.
يتم الوصول إلى وظائف النظام عن طريق تحميل معلمات الوظيفة في سجلات المعالج ثم استدعاء مقاطعة البرنامج 0x80. معالج هذه المقاطعة، وظيفة system_call، يدفع معلمات الاستدعاء إلى المكدس، ويسترد عنوان وظيفة النظام المستدعى من جدول sys_call_table وينقل التحكم إلى هذا العنوان.
نأخذ الوصول الكاملإلى مساحة عنوان kernel، يمكننا الحصول على محتويات جدول استدعاء النظام بالكامل، أي: عناوين كافة وظائف النظام. ومن خلال تغيير عنوان أي استدعاء للنظام، فإننا بذلك نعترضه. ولكن لهذا تحتاج إلى معرفة عنوان الجدول، أو بمعنى آخر الإزاحة في الملف /dev/kmem الذي يوجد به هذا الجدول.
لتحديد عنوان جدول sys_call_table، عليك أولاً حساب عنوان وظيفة system_call. بسبب ال هذه الوظيفةهو معالج المقاطعة، دعونا نلقي نظرة على كيفية التعامل مع المقاطعات في الوضع المحمي.
في الوضع الحقيقي، عند تسجيل المقاطعة، يصل المعالج إلى جدول متجه المقاطعة، والذي يقع دائمًا في بداية الذاكرة ويحتوي على عناوين مكونة من كلمتين لبرامج معالجة المقاطعة. في الوضع المحمي، يكون التناظري لجدول متجه المقاطعة هو جدول واصف المقاطعة (IDT، جدول واصف المقاطعة)، الموجود في نظام تشغيل الوضع المحمي. لكي يتمكن المعالج من الوصول إلى هذا الجدول، يجب تحميل عنوانه في سجل IDTR (سجل جدول واصف المقاطعة). يحتوي جدول IDT على واصفات معالجات المقاطعة، والتي تتضمن، على وجه الخصوص، عناوينها. وتسمى هذه الواصفات البوابات. يقوم المعالج، بعد تسجيل المقاطعة، باسترداد البوابة من IDT باستخدام رقمها، ويحدد عنوان المعالج وينقل التحكم إليه.
لحساب عنوان وظيفة system_call من جدول IDT، من الضروري استخراج بوابة المقاطعة int $0x80، ومنه عنوان المعالج المقابل، أي. عنوان الدالة system_call. في وظيفة system_call، يتم الوصول إلى جدول system_call_table باستخدام أمر الاتصال<адрес_таблицы>(،٪ إياكس، 4). بعد العثور على رمز التشغيل (التوقيع) لهذا الأمر في الملف /dev/kmem، سنجد أيضًا عنوان جدول استدعاء النظام.
لتحديد كود التشغيل، سنستخدم مصحح الأخطاء ونقوم بتفكيك وظيفة system_call:
# gdb -q /usr/src/linux/vmlinux (gdb) disas system_call تفريغ كود المجمع للوظيفة system_call: 0xc0194cbc
دعونا نلقي نظرة على الكود الكاذب الذي ينفذ عملية الاعتراض:
Readaddr(old_syscall, scr + SYS_CALL*4, 4); writeaddr(new_syscall, scr + SYS_CALL*4, 4); تقوم وظيفة readaddr بقراءة عنوان استدعاء النظام من جدول استدعاء النظام وتخزينه في المتغير old_syscall. يستغرق كل إدخال في جدول sys_call_table 4 بايت. يقع العنوان المطلوب في الإزاحة sct + SYS_CALL*4 في الملف /dev/kmem (هنا sct هو عنوان جدول sys_call_table، وSYS_CALL هو الرقم التسلسلي لاستدعاء النظام). تقوم وظيفة writeaddr بالكتابة فوق عنوان استدعاء النظام SYS_CALL بعنوان وظيفة new_syscall، وستتم خدمة جميع المكالمات إلى استدعاء النظام SYS_CALL بواسطة هذه الوظيفة.
يبدو أن كل شيء بسيط وتم تحقيق الهدف. ومع ذلك، دعونا نتذكر أننا نعمل في مساحة عنوان المستخدم. إذا وضعنا وظيفة نظام جديدة في مساحة العنوان هذه، فعندما نستدعي هذه الوظيفة، سنتلقى رسالة خطأ لطيفة. ومن هنا الاستنتاج - يجب وضع استدعاء النظام الجديد في مساحة عنوان kernel. للقيام بذلك، تحتاج إلى: الحصول على كتلة ذاكرة في مساحة kernel، وإجراء مكالمة نظام جديدة في هذه الكتلة.
يمكنك تخصيص الذاكرة في مساحة kernel باستخدام وظيفة kmalloc. ولكن من المستحيل استدعاء وظيفة النواة مباشرة من مساحة عنوان المستخدم، لذلك سوف نستخدم الخوارزمية التالية:
- بمعرفة عنوان الجدول sys_call_table، نحصل على عنوان بعض مكالمات النظام (على سبيل المثال، sys_mkdir)
- نحدد وظيفة تستدعي وظيفة kmalloc. تقوم هذه الوظيفة بإرجاع مؤشر إلى كتلة من الذاكرة في مساحة عنوان kernel. دعنا نسمي هذه الوظيفة get_kmalloc
- حفظ البايتات N الأولى من استدعاء النظام sys_mkdir، حيث N هو حجم الدالة get_kmalloc
- قم بالكتابة فوق أول N بايت من استدعاء sys_mkdir باستخدام الدالة get_kmalloc
- نقوم باستدعاء استدعاء النظام sys_mkdir، وبالتالي تشغيل وظيفة get_kmalloc
- استعادة البايتات N الأولى لاستدعاء النظام sys_mkdir
ولكن للتنفيذ من هذه الخوارزميةنحن بحاجة إلى عنوان الدالة kmalloc. هناك عدة طرق للعثور عليه. أبسطها هو قراءة هذا العنوان من ملف System.map أو تحديده باستخدام مصحح أخطاء gdb (طباعة &kmalloc). إذا تم تمكين دعم الوحدة النمطية في النواة، فيمكن تحديد عنوان kmalloc باستخدام وظيفة get_kernel_syms(). سيتم مناقشة هذا الخيار بشكل أكبر. إذا لم يكن هناك دعم لوحدات kernel، فسيتعين البحث عن عنوان وظيفة kmalloc بواسطة كود تشغيل أمر استدعاء kmalloc - على غرار ما تم إجراؤه لجدول sys_call_table.
تأخذ وظيفة kmalloc معلمتين: حجم الذاكرة المطلوبة ومحدد GFP. للبحث عن كود التشغيل، سنستخدم مصحح الأخطاء ونقوم بتفكيك أي وظيفة kernel تحتوي على استدعاء للدالة kmalloc.
# gdb -q /usr/src/linux/vmlinux (gdb) disas inter_module_register تفريغ كود المجمع للوظيفة inter_module_register: 0xc01a57b4
وبهذا تنتهي الحسابات النظرية، وباستخدام الطريقة المذكورة أعلاه، سنعترض استدعاء النظام sys_mkdir.
6. مثال على الاعتراض باستخدام /dev/kmem
/* المصدر 6.0 */ #includeنهاية الورقة/EOP
تم اختبار وظائف الكود من جميع الأقسام على النواة 2.4.22. عند إعداد التقرير تم استخدام مواد من الموقعمكالمات النظام
حتى الآن، كان على جميع البرامج التي قمنا بإنشائها أن تستخدم آليات kernel محددة جيدًا لتسجيل ملفات /proc وبرامج تشغيل الأجهزة. يعد هذا أمرًا رائعًا إذا كنت تريد القيام بشيء تم توفيره بالفعل بواسطة مبرمجي kernel، مثل كتابة برنامج تشغيل الجهاز. ولكن ماذا لو كنت تريد أن تفعل شيئًا غير عادي، أو تغير سلوك النظام بطريقة ما؟
هذا هو المكان الذي تصبح فيه برمجة kernel خطيرة. عند كتابة المثال أدناه، قمت بتدمير استدعاء النظام المفتوح. وهذا يعني أنني لم أتمكن من فتح أي ملفات، ولم أتمكن من تشغيل أي برامج، ولم أتمكن من إيقاف تشغيل النظام باستخدام أمر إيقاف التشغيل. لا بد لي من إيقاف الطاقة لإيقافه. ولحسن الحظ، لم يتم إتلاف أي ملفات. ولضمان عدم فقدان الملفات أيضًا، يرجى إجراء المزامنة قبل إصدار أوامر insmod وrmmod.
نسيان ملفات /proc وملفات الجهاز. إنها مجرد تفاصيل صغيرة. إن عملية الاتصال الحقيقية بالنواة، والتي تستخدمها جميع العمليات، هي استدعاءات النظام. عندما تطلب عملية ما خدمة من النواة (مثل فتح ملف، أو بدء عملية جديدة، أو طلب المزيد من الذاكرة)، يتم استخدام هذه الآلية. إذا كنت تريد تغيير سلوك النواة بطرق مثيرة للاهتمام، فهذا هو المكان المناسب لك. بالمناسبة، إذا كنت تريد معرفة استدعاءات النظام التي يستخدمها البرنامج، قم بتشغيل: strace
بشكل عام، العملية غير قادرة على الوصول إلى النواة. لا يمكنه الوصول إلى ذاكرة kernel ولا يمكنه استدعاء وظائف kernel. تحدد أجهزة وحدة المعالجة المركزية هذه الحالة (هناك سبب يطلق عليها "الوضع المحمي"). تعد مكالمات النظام استثناءً لهذه القاعدة العامة موقع محدد مسبقًا في النواة (بالطبع، تتم قراءته بواسطة عمليات المستخدم ولكن لا تتم الكتابة فوقه بواسطتها). ضمن وحدات المعالجة المركزية Intel، يتم تحقيق ذلك عن طريق المقاطعة 0x80. يعرف الجهاز أنه بمجرد وصولك إلى هذا الموقع، فلن تعد قيد التشغيل بدلاً من ذلك، فإنك تعمل كنواة نظام التشغيل، وبالتالي يُسمح لك بفعل ما تريد.
الموقع في النواة الذي يمكن أن تتصل به العملية يسمى system_call. يتحقق الإجراء الموجود من رقم استدعاء النظام، الذي يخبر النواة بما تريده العملية بالضبط. ثم يبحث بعد ذلك عن جدول استدعاء النظام (sys_call_table) للعثور على عنوان وظيفة kernel المراد الاتصال بها. يتم بعد ذلك استدعاء الوظيفة المطلوبة، وبعد أن تقوم بإرجاع قيمة، يتم إجراء العديد من عمليات فحص النظام. يتم بعد ذلك إرجاع النتيجة مرة أخرى إلى العملية (أو إلى عملية أخرى إذا انتهت العملية). إذا كنت تريد رؤية الكود الذي يفعل كل هذا، فهو موجود مصدر الملفقوس/< architecture >/kernel/entry.S، بعد السطر ENTRY(system_call) .
لذا، إذا أردنا تغيير كيفية عمل بعض استدعاءات النظام، فإن أول شيء يتعين علينا القيام به هو كتابة وظيفتنا الخاصة للقيام بالشيء المناسب (عادةً عن طريق إضافة القليل من التعليمات البرمجية الخاصة بنا، ثم استدعاء الوظيفة الأصلية)، ثم قم بتغيير المؤشر إلى sys_call_table للإشارة إلى وظيفتنا. نظرًا لأنه قد يتم حذفنا لاحقًا ولا نريد ترك النظام في حالة غير متناسقة، فمن المهم لـ cleanup_module استعادة الجدول إلى حالته الأصلية.
رمز المصدر الموضح هنا هو مثال على هذه الوحدة. نريد "التجسس" على بعض المستخدمين وإرسال رسالة عبر printk في أي وقت هذا المستخدميفتح الملف. نستبدل مكالمة النظام التي تفتح ملفًا بمكالمتنا وظيفة خاصةدعا لدينا_sys_open. تتحقق هذه الوظيفة من uid (معرف المستخدم) للعملية الحالية، وإذا كان مساويا لـ uid الذي نتجسس عليه، فستستدعي printk لعرض اسم الملف الذي سيتم فتحه. ثم يستدعي وظيفة الفتح الأصلية بنفس المعلمات، ويفتح الملف بالفعل.
تقوم الدالة init_module بتغيير الموقع المقابل في sys_call_table وتخزن المؤشر الأصلي في متغير. تستخدم وظيفة cleanup_module هذا المتغير لاستعادة كل شيء إلى وضعه الطبيعي. يعد هذا الأسلوب خطيرًا بسبب إمكانية قيام وحدتين بتعديل نفس استدعاء النظام. تخيل أن لدينا وحدتين، A وB. لنستدعي استدعاء النظام المفتوح للوحدة A A_open ونستدعي نفس الاستدعاء للوحدة B B_open. الآن بعد أن تم استبدال syscall المدرج في kernel بـ A_open، والذي سوف يستدعي sys_open الأصلي عندما يقوم بما يجب عليه القيام به. بعد ذلك، سيتم إدراج B في النواة، وسيستبدل استدعاء النظام بـ B_open، والذي سوف يستدعي ما يعتقد أنه استدعاء النظام الأصلي، ولكنه في الواقع A_open.
الآن، إذا تمت إزالة B أولاً، فسيكون كل شيء على ما يرام: سيستعيد ببساطة استدعاء النظام على A_open الذي يستدعي النسخة الأصلية. ومع ذلك، إذا تمت إزالة A ثم تمت إزالة B، فسوف ينهار النظام. ستؤدي إزالة A إلى استعادة استدعاء النظام إلى sys_open الأصلي، مما يؤدي إلى قطع B خارج الحلقة. وبعد ذلك، عند إزالة B، فإنه سيعيد استدعاء النظام إلى ما يعتقد أنه الأصلي وسيتم توجيه الاستدعاء فعليًا إلى A_open، الذي لم يعد موجودًا في الذاكرة. للوهلة الأولى، يبدو أننا نستطيع حل هذه المشكلة تحديدًا عن طريق التحقق مما إذا كان استدعاء النظام يساوي دالتنا المفتوحة، وإذا كان الأمر كذلك، فلا نغير قيمة هذا الاستدعاء (بحيث لا يغير B استدعاء النظام عند حذفه )، ولكن هذا من شأنه أن يسبب مشكلة أخرى أسوأ. عند إزالة A، فإنه يرى أن استدعاء النظام قد تم تغييره إلى B_open بحيث لم يعد يشير إلى A_open، لذلك لن يستعيد المؤشر إلى sys_open قبل إزالته من الذاكرة. لسوء الحظ، سيظل B_open يحاول استدعاء A_open، الذي لم يعد موجودًا في الذاكرة، لذلك حتى بدون إزالة B، سيظل النظام يتعطل.
أرى طريقتين لمنع هذه المشكلة. أولاً: استعادة الوصول إلى القيمة الأصلية لـ sys_open. لسوء الحظ، sys_open ليس جزءًا من جدول kernel في /proc/ksyms، لذلك لا يمكننا الوصول إليه. الحل الآخر هو استخدام عداد مرجعي لمنع إلغاء تحميل الوحدة. يعد هذا أمرًا جيدًا للوحدات النمطية العادية، ولكنه سيئ للوحدات "التعليمية".
/* syscall.c * * نموذج "سرقة" استدعاء النظام */ /* حقوق الطبع والنشر (C) 1998-99 بواسطة Ori Pomerantz */ /* ملفات الرأس الضرورية */ /* المعيار في وحدات kernel */ #include