这份笔记写于2014年,但我只是在枢纽受到压制,她没有看到光明。 在禁令期间,我忘记了它,但是现在我在草稿中找到了它。 以为是删除,但也许有人派上了用场。

通常,星期五的小型管理员阅读有关搜索“ included”
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等。我们的
rand也是其中之一。 确保这一点:
# 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函数要比
libc.so的 rand早加载,因此它可以控制球。
好的,我们设法替换了本机功能,但是如何确保保留其功能并添加了一些操作。 我们修改随机数:
#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库中的一个函数,它将在动态库堆栈中找到我们的
rand 。 之后,我们将调用此函数并返回其值。 因此,在构建时,我们将需要添加
“ -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变量。 我们将不使用中间函数,而是引用
**环境数组,在其中存储环境的副本:
#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; }
由于在这里我们直接从内存读取数据,因此无法拦截此类调用,并且我们的
undetect_getenv不再干扰确定入侵。
$ 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。 / proc /自我/
但是,内存并不是您可以检测到
LD_PRELOAD欺骗的最后一个地方,还有
/ proc / 。 让我们从显而易见的
/ proc / {PID} / 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。 系统调用
看起来就这么多,但仍然比比皆是。 如果我们直接将系统调用定向到内核,则将绕过整个拦截过程。 当然,以下解决方案
取决于体系结构(
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是一个例外,它以通常的方式工作。)在调用wait(2)时,将向父进程通知此情况,之后它可以在启动之前查看和修改子进程的内容。 之后,父进程允许孩子继续工作,在某些情况下,忽略了发送给他的信号或发送另一个信号)。
因此,解决方案是跟踪进程,在每次系统调用之前将其停止,并在必要时将线程重定向到trap函数。
#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非常感谢你
查尔斯·胡本扼流点瓦尔迪克斯菲利普·特文德哈斯他们的文章,源代码和注释使我的文章比我做的要多得多。