كتبت هذه المذكرة في عام 2014 ، لكني تعرضت للتو للقمع في المحور ولم تر النور. أثناء الحظر ، نسيت ذلك ، لكنني وجدته الآن في نسخ مسودة. اعتقد انه كان لحذف ، ولكن ربما شخص ما في متناول اليدين.

بشكل عام ، يقوم مسؤول الجمعة الصغير بقراءة موضوع البحث عن
LD_PRELOAD "المضمّن".
1. استطرادا صغيرا لأولئك الذين ليسوا على دراية باستبدال الوظيفة
يمكن أن يذهب الباقي مباشرة إلى
الخطوة 2 .
لنبدأ بالمثال الكلاسيكي:
#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand (time(NULL)); for(int i=0; i<5; i++){ printf ("%d\n", rand()%100); } }
ترجمة دون أي أعلام:
$ gcc ./ld_rand.c -o ld_rand
وكما هو متوقع ، نحصل على 5 أرقام عشوائية أقل من 100:
$ ./ld_rand 53 93 48 57 20
لكن لنفترض أننا لا نملك الكود المصدري للبرنامج ، ونحن بحاجة إلى تغيير السلوك.
دعونا ننشئ مكتبتنا الخاصة بنموذج وظيفة خاص بنا ، على سبيل المثال:
int rand(){ return 42; }
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
والآن أصبح اختيارنا العشوائي متوقعًا تمامًا:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand 42 42 42 42 42
تبدو هذه الخدعة أكثر إثارة للإعجاب إذا قمنا أولاً بتصدير مكتبتنا من خلال
$ export LD_PRELOAD=$PWD/ld_rand.so
أو قبل التنفيذ
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
ثم قم بتشغيل البرنامج في الوضع العادي. لم نقم بتغيير سطر واحد في رمز البرنامج نفسه ، ولكن سلوكه يعتمد الآن على وظيفة صغيرة في مكتبتنا. علاوة على ذلك ، في وقت كتابة هذا التقرير ، لم تكن
الراند المزيفة موجودة.
ما الذي جعل برنامجنا يستخدم
راند وهمية؟ دعنا نذهب من خلال الخطوات.
عند بدء تشغيل التطبيق ، يتم تحميل مكتبات معينة تحتوي على الوظائف اللازمة للبرنامج. يمكننا رؤيتهم باستخدام
ldd :
# ldd ./ld_rand linux-vdso.so.1 (0x00007ffc8b1f3000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe3da8af000) /lib64/ld-linux-x86-64.so.2 (0x00007fe3daa7e000)
قد تختلف هذه القائمة وفقًا لإصدار نظام التشغيل ، ولكن يجب أن يكون
هناك ملف
libc.so هناك . هذه المكتبة هي التي توفر مكالمات النظام والوظائف الأساسية ، مثل
open ،
malloc ،
printf ، وما إلى ذلك. تأكد من هذا:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$" 000000000003aef0 T rand
دعونا نرى ما إذا كانت مجموعة المكتبات ستتغير عند استخدام
LD_PRELOAD # LD_PRELOAD=$PWD/ld_rand.so ldd ./ld_rand linux-vdso.so.1 (0x00007ffea52ae000) /scripts/c/ldpreload/ld_rand.so (0x00007f690d3f9000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f690d230000) /lib64/ld-linux-x86-64.so.2 (0x00007f690d405000)
اتضح أن المتغير المحدد
LD_PRELOAD يفرض
ld_rand.so على تحميله ، على الرغم من أن البرنامج نفسه لا يتطلب ذلك. ونظرًا لأن وظيفة
rand يتم تحميلها مسبقًا من
rand من
libc.so ، فإنها تحكم الكرة.
حسنًا ، لقد نجحنا في استبدال الوظيفة الأصلية ، ولكن كيفية التأكد من الحفاظ على وظائفها وإضافة بعض الإجراءات. نقوم بتعديل عشوائي لدينا:
#define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> typedef int (*orig_rand_f_type)(void); int rand() { /* */ printf("Evil injected code\n"); orig_rand_f_type orig_rand; orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand"); return orig_rand(); }
هنا ، كإضافة لدينا ، نطبع سطرًا واحدًا فقط من النص ، ثم ننشئ مؤشرًا لوظيفة
rand الأصلية. للحصول على عنوان هذه الوظيفة ، نحتاج إلى
dlsym - هذه وظيفة من مكتبة
libdl التي ستجد
راندنا في مجموعة المكتبات الديناميكية. بعد ذلك سوف نسمي هذه الوظيفة ونعيد قيمتها. وفقًا لذلك ، سنحتاج إلى إضافة
"-ldl" عند البناء:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand Evil injected code 66 Evil injected code 28 Evil injected code 93 Evil injected code 93 Evil injected code 95
ويستخدم برنامجنا
الراند "الأصلي" ، بعد القيام ببعض الإجراءات غير اللائقة.
2. البحث الدقيق
مع العلم بالتهديد المحتمل ، نريد أن نكتشف أنه تم تنفيذ
التحميل المسبق . من الواضح أن أفضل طريقة للاكتشاف هي دفعها إلى النواة ، لكنني كنت مهتمًا بالتحديد في تعريفات مساحة المستخدم.
بعد ذلك ، سوف تتحول حلول الكشف ودحضها إلى أزواج.
2.1. لنبدأ بكل بساطة
كما ذكرنا سابقًا ، يمكنك تحديد المكتبة المراد تحميلها باستخدام متغير
LD_PRELOAD أو عن طريق كتابتها في ملف
/etc/ld.so.preload . دعونا إنشاء اثنين من أبسط أجهزة الكشف.
الأول هو التحقق من متغير البيئة المحدد:
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> int main() { char* pGetenv = getenv("LD_PRELOAD"); pGetenv != NULL ? printf("LD_PRELOAD (getenv) [+]\n"): printf("LD_PRELOAD (getenv) [-]\n"); }
والثاني هو التحقق من فتح الملف:
#include <stdio.h> #include <fcntl.h> int main() { open("/etc/ld.so.preload", O_RDONLY) != -1 ? printf("LD_PRELOAD (open) [+]\n"): printf("LD_PRELOAD (open) [-]\n"); }
تحميل المكتبات:
$ export LD_PRELOAD=$PWD/ld_rand.so $ echo "$PWD/ld_rand.so" > /etc/ld.so.preload $ ./detect_base_getenv LD_PRELOAD (getenv) [+] $ ./detect_base_open LD_PRELOAD (open) [+]
فيما يلي ، يشير [+] إلى اكتشاف ناجح.
وفقا لذلك ، [-] يعني كشف الالتفافية.
ما مدى فعالية هذا الكاشف؟ أولاً ، دعنا نلقي نظرة على متغير البيئة:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <dlfcn.h> char* (*orig_getenv)(const char *) = NULL; char* getenv(const char *name) { if(!orig_getenv) orig_getenv = dlsym(RTLD_NEXT, "getenv"); if(strcmp(name, "LD_PRELOAD") == 0) return NULL; return orig_getenv(name); }
$ gcc -shared -fpic -ldl ./ld_undetect_getenv.c -o ./ld_undetect_getenv.so $ LD_PRELOAD=./ld_undetect_getenv.so ./detect_base_getenv LD_PRELOAD (getenv) [-]
وبالمثل ، نتخلص من الشيكات
المفتوحة :
#define _GNU_SOURCE #include <string.h> #include <stdlib.h> #include <dlfcn.h> #include <errno.h> int (*orig_open)(const char*, int oflag) = NULL; int open(const char *path, int oflag, ...) { char real_path[256]; if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0){ errno = ENOENT; return -1; } return orig_open(path, oflag); }
$ gcc -shared -fpic -ldl ./ld_undetect_open.c -o ./ld_undetect_open.so $ LD_PRELOAD=./ld_undetect_open.so ./detect_base_open LD_PRELOAD (open) [-]
نعم ، يمكن هنا استخدام طرق أخرى للوصول إلى الملف ، مثل
open64 ،
stat ، وما إلى ذلك ، ولكن في الواقع ، هناك حاجة إلى 5-10 سطور من التعليمات البرمجية
لخداعهم .
2.2. المضي قدما
أعلاه ، استخدمنا
getenv () للحصول على قيمة
LD_PRELOAD ، ولكن هناك أيضًا طريقة "منخفضة المستوى" للوصول إلى متغيرات
ENV . لن نستخدم وظائف وسيطة ، ولكن يرجى الرجوع إلى صفيف
** environ ، حيث يتم تخزين نسخة من البيئة:
#include <stdio.h> #include <string.h> extern char **environ; int main(int argc, char **argv) { int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { printf("LD_PRELOAD (**environ) [+]\n"); return 0; } } printf("LD_PRELOAD (**environ) [-]\n"); return 0; }
نظرًا لأننا هنا نقرأ البيانات مباشرة من الذاكرة ، لا يمكن اعتراض مثل هذه المكالمة ، ولم يعد
تدخلنا في اكتشاف أمر التدخل.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ LD_PRELOAD (**environ) [+]
يبدو أن المشكلة قد تم حلها؟ لا يزال مجرد بداية.
بعد بدء تشغيل البرنامج ، لم تعد قيمة المتغير
LD_PRELOAD في الذاكرة ضرورية للمفرقعات ، أي يمكنك قراءتها وحذفها قبل تنفيذ أي تعليمات. بالطبع ، يعد تحرير صفيف في الذاكرة أسلوب برمجة سيئًا على الأقل ، لكن هل يمكن لهذا أن يوقف شخصًا لا يرغب فينا جيدًا حقًا؟
للقيام بذلك ، نحتاج إلى إنشاء دالة وهمية
(init) الخاصة بنا ، والتي
نعترض فيها LD_PRELOAD المثبت
ونمرره إلى رابطنا:
#define _GNU_SOURCE #include <stdio.h> #include <string.h> #include <unistd.h> #include <dlfcn.h> #include <stdlib.h> extern char **environ; char *evil_env; int (*orig_execve)(const char *path, char *const argv[], char *const envp[]) = NULL; // init // // - void evil_init() { // LD_PRELOAD static const char *ldpreload = "LD_PRELOAD"; int len = strlen(getenv(ldpreload)); evil_env = (char*) malloc(len+1); strcpy(evil_env, getenv(ldpreload)); int i; char env[] = "LD_PRELOAD"; if (environ != NULL) for (i = 0; environ[i] != NULL; i++) { char * pch; pch = strstr(environ[i],env); if(pch != NULL) { // LD_PRELOAD unsetenv(env); break; } } } int execve(const char *path, char *const argv[], char *const envp[]) { int i = 0, j = 0, k = -1, ret = 0; char** new_env; if(!orig_execve) orig_execve = dlsym(RTLD_NEXT,"execve"); // LD_PRELOAD for(i = 0; envp[i]; i++){ if(strstr(envp[i], "LD_PRELOAD")) k = i; } // LD_PRELOAD , if(k == -1){ k = i; i++; } // new_env = (char**) malloc((i+1)*sizeof(char*)); // , LD_PRELOAD for(j = 0; j < i; j++) { // LD_PRELOAD if(j == k) { new_env[j] = (char*) malloc(256); strcpy(new_env[j], "LD_PRELOAD="); strcat(new_env[j], evil_env); } else new_env[j] = (char*) envp[j]; } new_env[i] = NULL; ret = orig_execve(path, argv, new_env); free(new_env[k]); free(new_env); return ret; }
نحن أداء ، تحقق:
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ.c -o ./ld_undetect_environ.so $ LD_PRELOAD=./ld_undetect_environ.so ./detect_environ LD_PRELOAD (**environ) [-]
2.3. / بروك / النفس /
ومع ذلك ، فإن الذاكرة ليست هي آخر مكان يمكنك فيه اكتشاف خداع
LD_PRELOAD ، فهناك أيضًا
/ proc / . لنبدأ
بالتوضيح / proc / {PID} / environ .
في الواقع هناك حل عالمي
للكشف عن
** environ و
/ proc / self / environ . المشكلة هي السلوك "الخاطئ" ل
unsetenv (env) .
الخيار الصحيح void evil_init() {
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ_2.c -o ./ld_undetect_environ_2.so $ (LD_PRELOAD=./ld_undetect_environ_2.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
لكن لنفترض أننا لم نعثر عليها و
/ proc / self / environ يحتوي على بيانات "إشكالية".
أولاً ، جرّب استخدام "تمويه" السابق:
$ (LD_PRELOAD=./ld_undetect_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD LD_PRELOAD=./ld_undetect_environ.so
يستخدم
cat نفس
open () لفتح الملف ، وبالتالي فإن الحل مشابه لما تم فعله بالفعل في القسم 2.1 ، لكننا الآن نقوم بإنشاء ملف مؤقت حيث نقوم بنسخ قيم الذاكرة الحقيقية دون خطوط تحتوي على
LD_PRELOAD .
#define _GNU_SOURCE #include <dlfcn.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #include <sys/stat.h> #include <unistd.h> #include <limits.h> #include <errno.h> #define BUFFER_SIZE 256 int (*orig_open)(const char*, int oflag) = NULL; char *soname = "fakememory_preload.so"; char *sstrstr(char *str, const char *sub) { int i, found; char *ptr; found = 0; for(ptr = str; *ptr != '\0'; ptr++) { found = 1; for(i = 0; found == 1 && sub[i] != '\0'; i++){ if(sub[i] != ptr[i]) found = 0; } if(found == 1) break; } if(found == 0) return NULL; return ptr + i; } void fakeMaps(char *original_path, char *fake_path, char *pattern) { int fd; char buffer[BUFFER_SIZE]; int bytes = -1; int wbytes = -1; int k = 0; pid_t pid = getpid(); int fh; if ((fh=orig_open(fake_path,O_CREAT|O_WRONLY))==-1) { printf("LD: Cannot open write-file [%s] (%d) (%s)\n", fake_path, errno, strerror(errno)); exit (42); } if((fd=orig_open(original_path, O_RDONLY))==-1) { printf("LD: Cannot open read-file.\n"); exit(42); } do { char t = 0; bytes = read(fd, &t, 1); buffer[k++] = t; //printf("%c", t); if(t == '\0') { //printf("\n"); if(!sstrstr(buffer, "LD_PRELOAD")) { if((wbytes = write(fh,buffer,k))==-1) { //printf("write error\n"); } else { //printf("writed %d\n", wbytes); } } k = 0; } } while(bytes != 0); close(fd); close(fh); } int open(const char *path, int oflag, ...) { char real_path[PATH_MAX], proc_path[PATH_MAX], proc_path_0[PATH_MAX]; pid_t pid = getpid(); if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open"); realpath(path, real_path); snprintf(proc_path, PATH_MAX, "/proc/%d/environ", pid); if(strcmp(real_path, proc_path) == 0) { snprintf(proc_path, PATH_MAX, "/tmp/%d.fakemaps", pid); realpath(proc_path_0, proc_path); fakeMaps(real_path, proc_path, soname); return orig_open(proc_path, oflag); } return orig_open(path, oflag); }
وقد تم الانتهاء من هذه المرحلة:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $
المكان الواضح التالي هو
/ proc / self / maps . ليس من المنطقي الاستمرار في ذلك. الحل مطابق تمامًا للحل السابق: نسخ البيانات من الملف مطروحًا منها السطور بين
libc.so و
ld.so.2.4. خيار من Chokepoint
أعجبني هذا الحل بشكل خاص بسبب بساطته. قارن بين عناوين الوظائف التي تم تحميلها مباشرةً من
libc وعنوان "NEXT".
#define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #define LIBC "/lib/x86_64-linux-gnu/libc.so.6" int main(int argc, char *argv[]) { void *libc = dlopen(LIBC, RTLD_LAZY); // Open up libc directly char *syscall_open = "open"; int i; void *(*libc_func)(); void *(*next_func)(); libc_func = dlsym(libc, syscall_open); next_func = dlsym(RTLD_NEXT, syscall_open); if (libc_func != next_func) { printf("LD_PRELOAD (syscall - %s) [+]\n", syscall_open); printf("Libc address: %p\n", libc_func); printf("Next address: %p\n", next_func); } else { printf("LD_PRELOAD (syscall - %s) [-]\n", syscall_open); } return 0; }
نحمّل المكتبة بالاعتراض
"open ()" ونفحص:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so $ ./detect_chokepoint LD_PRELOAD (syscall - open) [+] Libc address: 0x7fa86893b160 Next address: 0x7fa868a26135
تبين أن الدحض أبسط من ذلك:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> extern void * _dl_sym (void *, const char *, void *); void * dlsym (void * handle, const char * symbol) { return _dl_sym (handle, symbol, dlsym); }
# LD_PRELOAD=./ld_undetect_chokepoint.so ./detect_chokepoint LD_PRELOAD (syscall - open) [-]
2.5. Syscalls
يبدو أن هذا كل شيء ، لكنه لا يزال يتعثر. إذا قمنا بتوجيه مكالمة نظام مباشرة إلى النواة ، فسوف يتحايل ذلك على عملية الاعتراض بأكملها. الحل أدناه ، بطبيعة الحال ،
يعتمد على الهندسة المعمارية (
x86_64 ). دعونا نحاول تطبيق
ld.so.preload للكشف عن الفتح.
#include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #define BUFFER_SIZE 256 int syscall_open(char *path, long oflag) { int fd = -1; __asm__ ( "mov $2, %%rax;" // Open syscall number "mov %1, %%rdi;" // Address of our string "mov %2, %%rsi;" // Open mode "mov $0, %%rdx;" // No create mode "syscall;" // Straight to ring0 "mov %%eax, %0;" // Returned file descriptor :"=r" (fd) :"m" (path), "m" (oflag) :"rax", "rdi", "rsi", "rdx" ); return fd; } int main() { syscall_open("/etc/ld.so.preload", O_RDONLY) > 0 ? printf("LD_PRELOAD (open syscall) [+]\n"): printf("LD_PRELOAD (open syscall) [-]\n"); }
$ ./detect_syscall LD_PRELOAD (open syscall) [+]
وهذه المشكلة لها حل. مقتطف من
الرجل :
ptrace هي أداة تسمح لعملية الأصل بمراقبة تدفق عملية أخرى والتحكم فيها ، وعرض وتغيير بياناتها وسجلاتها. عادةً ما يتم استخدام هذه الوظيفة لإنشاء نقاط توقف في برنامج تصحيح الأخطاء وتعقب مكالمات النظام.
يمكن أن تبدأ العملية الأصل في التتبع عن طريق استدعاء دالة fork (2) أولاً ، وبعد ذلك يمكن تنفيذ العملية التابعة الناتجة PTRACE_TRACEME ، متبوعة بـ (عادةً) exec (3). من ناحية أخرى ، يمكن أن تبدأ العملية الأصل في تصحيح العملية الحالية باستخدام PTRACE_ATTACH.
عند التتبع ، تتوقف العملية الفرعية في كل مرة يتم فيها تلقي إشارة ، حتى إذا تم تجاهل هذه الإشارة. (الاستثناء هو SIGKILL ، والذي يعمل بالطريقة المعتادة.) سيتم إخطار العملية الأصل بهذا عند استدعاء الانتظار (2) ، وبعد ذلك يمكنه عرض وتعديل محتويات العملية الفرعية قبل أن تبدأ. بعد ذلك ، تسمح العملية الرئيسية للطفل بمواصلة العمل ، وفي بعض الحالات تتجاهل الإشارة المرسلة إليه أو ترسل إشارة أخرى بدلاً من ذلك).
وبالتالي ، فإن الحل هو تتبع العملية وإيقافها قبل استدعاء كل نظام ، وإذا لزم الأمر ، قم بإعادة توجيه مؤشر الترابط إلى وظيفة الملاءمة.
#define _GNU_SOURCE #include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <errno.h> #include <limits.h> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/reg.h> #include <sys/user.h> #include <asm/unistd.h> #if defined(__x86_64__) #define REG_SYSCALL ORIG_RAX #define REG_SP rsp #define REG_IP rip #endif long NOHOOK = 0; long evil_open(const char *path, long oflag, long cflag) { char real_path[PATH_MAX], maps_path[PATH_MAX]; long ret; pid_t pid; pid = getpid(); realpath(path, real_path); if(strcmp(real_path, "/etc/ld.so.preload") == 0) { errno = ENOENT; ret = -1; } else { NOHOOK = 1; // Entering NOHOOK section ret = open(path, oflag, cflag); } // Exiting NOHOOK section NOHOOK = 0; return ret; } void init() { pid_t program; // program = fork(); if(program != 0) { int status; long syscall_nr; struct user_regs_struct regs; // if(ptrace(PTRACE_ATTACH, program) != 0) { printf("Failed to attach to the program.\n"); exit(1); } waitpid(program, &status, 0); // SYSCALLs ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD); while(1) { ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); if(WIFEXITED(status) || WIFSIGNALED(status)) break; else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80) { // syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL); if(syscall_nr == __NR_open) { // NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK); // if(!NOHOOK) { // // regs ptrace(PTRACE_GETREGS, program, 0, ®s); // Push return address on the stack regs.REG_SP -= sizeof(long); // ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP); // RIP evil_open regs.REG_IP = (unsigned long) evil_open; // ptrace(PTRACE_SETREGS, program, 0, ®s); } } ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); } } exit(0); } else { sleep(0); } }
نتحقق من:
$ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-]
+ 0-0 = 5شكرا جزيلا
تشارلز هوبينممراValdikSSفيليب تيوينderhassالذين مقالاتهم ، أكواد المصدر والتعليقات فعلت أكثر مني بكثير لجعل هذه المادة تظهر هنا.