67.2 LD_PRELOAD hack in Linux

Linux允许让我们自己的动态链接库加载在其它动态链接库之前,甚至是系统库(如 libc.so.6)。

反过来想,也就是允许我们用自己写的函数去“代替”系统库的函数。举个例子,我们可以很容易地拦截掉time(),read(),write()等等这些函数。

来瞧瞧我们是如何愚弄uptime这个程序的。我们知道,该程序显示计算机已经工作了多长时间。借助strace的帮助可以看到,该程序通过/proc/uptime文件获取到计算机的工作时长。

  1. $ strace uptime
  2. ...
  3. open("/proc/uptime", O_RDONLY) = 3
  4. lseek(3, 0, SEEK_SET) = 0
  5. read(3, "416166.86 414629.38\n", 2047) = 20
  6. ...

/proc/uptime并不是存放在磁盘的真实文件。而是由Linux Kernel产生的一个虚拟的文件。它有两个数值:

  1. $ cat /proc/uptime
  2. 416690.91 415152.03

我们可以用wikipedia来看一下它的含义:

  1. 第一个数值是系统运行总时长,第二个数值是系统空闲的时间。都以秒为单位表示。

我们来写一个含open(),read(),close()函数的动态链接库。

首先,我们的open()函数会比较一下文件名是不是我们所想要打开的,如果是,则将文件描述符记录下来。然后,read()函数会判断如果我们调用的是不是我们所保存的文件描述符,如果是则代替它输出,否则调用libc.so.6里面原来的函数。最后,close()函数会关闭我们所保存的文件描述符。

在这里我们借助了dlopen()和dlsym()函数来确定原先在libc.so.6的函数的地址,因为我们需要控制“真实”的函数。

题外话,如果我们的程序想劫持strcmp()函数来监控每个字符串的比较,则需要我们自己实现一个strcmp()函数而不能用原先的函数。

  1. #include <stdio.h>
  2. #include <stdarg.h>
  3. #include <stdlib.h>
  4. #include <stdbool.h>
  5. #include <unistd.h>
  6. #include <dlfcn.h>
  7. #include <string.h>
  8. void *libc_handle = NULL;
  9. int (*open_ptr)(const char *, int) = NULL;
  10. int (*close_ptr)(int) = NULL;
  11. ssize_t (*read_ptr)(int, void*, size_t) = NULL;
  12. bool inited = false;
  13. _Noreturn void die (const char * fmt, ...)
  14. {
  15. va_list va;
  16. va_start (va, fmt);
  17. vprintf (fmt, va);
  18. exit(0);
  19. };
  20. static void find_original_functions ()
  21. {
  22. if (inited)
  23. return;
  24. libc_handle = dlopen ("libc.so.6", RTLD_LAZY);
  25. if (libc_handle==NULL)
  26. die ("can't open libc.so.6\n");
  27. open_ptr = dlsym (libc_handle, "open");
  28. if (open_ptr==NULL)
  29. die ("can't find open()\n");
  30. close_ptr = dlsym (libc_handle, "close");
  31. if (close_ptr==NULL)
  32. die ("can't find close()\n");
  33. read_ptr = dlsym (libc_handle, "read");
  34. if (read_ptr==NULL)
  35. die ("can't find read()\n");
  36. inited = true;
  37. }
  38. static int opened_fd=0;
  39. int open(const char *pathname, int flags)
  40. {
  41. find_original_functions();
  42. int fd=(*open_ptr)(pathname, flags);
  43. if (strcmp(pathname, "/proc/uptime")==0)
  44. opened_fd=fd; // that's our file! record its file descriptor
  45. else
  46. opened_fd=0;
  47. return fd;
  48. };
  49. int close(int fd)
  50. {
  51. find_original_functions();
  52. if (fd==opened_fd)
  53. opened_fd=0; // the file is not opened anymore
  54. return (*close_ptr)(fd);
  55. };
  56. ssize_t read(int fd, void *buf, size_t count)
  57. {
  58. find_original_functions();
  59. if (opened_fd!=0 && fd==opened_fd)
  60. {
  61. // that's our file!
  62. return snprintf (buf, count, "%d %d", 0x7fffffff, 0x7fffffff)+1;
  63. };
  64. // not our file, go to real read() function
  65. return (*read_ptr)(fd, buf, count);
  66. };

把它编译成动态链接库:

  1. gcc -fpic -shared -Wall -o fool_uptime.so fool_uptime.c -ldl

运行uptime,并让它在加载其它库之前加载我们的库:

  1. LD_PRELOAD=`pwd`/fool_uptime.so uptime

可以看到:

  1. 01:23:02 up 24855 days, 3:14, 3 users, load average: 0.00, 0.01, 0.05

如果LD_PRELOAD环境变量一直指向我们的动态链接库文件名,其它程序在启动的时候也会加载我们的动态链接库。

更多的例子请看: