基于Linux的动态注入技术实现的文件读写追踪器

介绍

一款适用于 Linux 系统的动态追踪文件读写信息的共享库。原理是通过重写与 IO 相关的函数,在程序运行前优先加载本动态链接库,实现记录文件读写信息。

使用 LD_PRELOAD 注入程序

LD_PRELOAD 是 Linux 系统的一个环境变量,它可以影响程序的运行时的链接(Runtime linker),它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。一方面,我们可以以此功能来使用自己的或是更好的函数(无需别人的源码),而另一方面,我们也可以以向别人的程序注入程序,从而达到特定的目的。

一般情况下,其加载顺序为 LD_PRELOAD>LD_LIBRARY_PATH>/etc/ld.so.cache>/lib>/usr/lib。几米夜空转载的文章《LD_PRELOAD 作用》 程序调用流图和代码例子值得一看。

1. 简单举例

我们以 Rafa Cielak 的一篇文章的例子为例说明它的用法,翻译自 Rafa Cielak 的博客,译者 qhwdw

random_num.c:

# include <stdio.h>
# include <stdlib.h>
# include <time.h>
int main() {
    
    
  srand(time(NULL));
  int i = 10;
  while(i--)
      printf("%d\n", rand()%100);
  return 0;
}

我不使用任何参数来编译它,如下所示:

gcc random_num.c -o random_num

我希望它输出的结果是明确的:从 0-99 中选择的十个随机数字,希望每次你运行这个程序时它的输出都不相同。

现在,让我们假装真的不知道这个可执行程序的出处。甚至将它的源文件删除,或者把它移动到别的地方 —— 我们已不再需要它了。我们将对这个程序的行为进行重大的修改,而你并不需要接触到它的源代码,也不需要重新编译它。

因此,让我们来创建另外一个简单的 C 文件:

unrandom.c:

int rand() {
    
    
    // the most random number in the universe
    return 42;
}

我们将编译它进入一个共享库中:

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

因此,现在我们已经有了一个可以输出一些随机数的应用程序,和一个定制的库,它使用一个常数值 42 实现了一个 rand() 函数。现在……就像运行 random_num 一样,然后再观察结果:

LD_PRELOAD=$PWD/unrandom.so ./random_num

如果你想偷懒或者不想自动亲自动手(或者不知什么原因猜不出发生了什么),我来告诉你 —— 它输出了十次常数 42。

如果先这样执行:

export LD_PRELOAD=$PWD/unrandom.so

然后再以正常方式运行这个程序,这个结果也许会更让你吃惊:一个未被改变过的应用程序在一个正常的运行方式中,看上去受到了我们做的一个极小的库的影响……

当我们的程序启动后,为程序提供所需要的函数的某些库被加载。我们可以使用 ldd 去学习它是怎么工作的:

ldd random_num

linux-vdso.so.1 => (0x00007fff4bdfe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f48c03ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f48c07e3000)

它列出了被程序 random_num 所需要的库的列表。这个列表是构建进可执行程序中的,并且它是在编译时决定的。在你的机器上的具体的输出可能与示例有所不同,但是,一个 libc.so 肯定是有的 —— 这个文件提供了核心的 C 函数。它包含了 “真正的” rand()。

我使用下列的命令可以得到一个全部的函数列表,我们看一看 libc 提供了哪些函数:

nm -D /lib/libc.so.6

这个 nm 命令列出了在一个二进制文件中找到的符号。-D 标志告诉它去查找动态符号,因为 libc.so.6 是一个动态库。这个输出是很长的,但它确实在列出的很多标准函数中包括了 rand()。

现在,在我们设置了环境变量 LD_PRELOAD 后发生了什么?这个变量为一个程序强制加载一些库。在我们的案例中,它为 random_num 加载了 unrandom.so,尽管程序本身并没有这样去要求它。下列的命令可以看得出来:

LD_PRELOAD=$PWD/unrandom.so ldd random_nums

linux-vdso.so.1 =>  (0x00007fff369dc000)
/some/path/to/unrandom.so (0x00007f262b439000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f262b044000)
/lib64/ld-linux-x86-64.so.2 (0x00007f262b63d000)

注意,它列出了我们当前的库。实际上这就是代码为什么得以运行的原因:random_num 调用了 rand(),但是,如果 unrandom.so 被加载,它调用的是我们所提供的实现了 rand() 的库。

2. 打开文件显示路径

编写 inspect_open.c:

# define _GNU_SOURCE
# include <dlfcn.h>
# include <stdio.h>

typedef int (*orig_open_f_type) (const char *pathname, int flags);

int open(const char *pathname, int flags, ...)
{
    
    
    // remember to include stdio.h!
    printf("open():%s\n", pathname);
    /* Some evil injected code goes here. */
    orig_open_f_type orig_open;
    orig_open = (orig_open_f_type) dlsym(RTLD_NEXT, "open");
    return orig_open(pathname, flags);
}

FILE *fopen(const char *path, const char *mode) {
    
    
    printf("fopen():%s\n", path);
    FILE* (*original_fopen) (const char*, const char*);
    original_fopen = dlsym(RTLD_NEXT, "fopen");
    return (*original_fopen)(path, mode);
}

编译为动态库:

gcc -shared -fPIC inspect_open.c -o inspect_open.so -ldl

编写读取文件的例子 test.c:

# include<stdlib.h>
# include<stdio.h>

int main()
{
    
    
    FILE*fp;
    fp=fopen("file01.txt","r");
    if(fp==NULL)
    {
    
    
        printf("Can not openthe file!\n");
        exit(0);
    }
    fclose(fp);
    return 0;
}

编译:

gcc test.c -o test

加载库运行:

LD_PRELOAD=$PWD/inspect_open.so ./test

结果如下,成功注入了函数,打印文件路径:

inspect_open.c 中还重写了 open,不过这个实现是不完美的,使用会导致异常。

重写文件列表:

open、openat、write、rename、renameat、execve、read、readlink

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
ssize_t write(int fd, const void *buf, size_t count);
int rename(const char *old, const char *new);
int renameat(int olddirfd, const char *oldpath, int newdirfd, const char *newpath);
int execve(const char *pathname, char *const argv[], char *const envp[]);
ssize_t read(int fd, void *buf, size_t count);
ssize_t readlink(const char *pathname, char *buf, size_t bufsiz);
ssize_t readlinkat(int dirfd, const char *pathname, char *buf, size_t bufsiz);

使用

导入环境变量指定保存文件的位置和过滤文件路径:

export JFZ_SAVE_FILE=/root/JfzTracer.log

export JFZ_TRACE_PATH=/root/_code/aosp-ninja-trace

在运行想追踪的程序前先 LD_PRELOAD 注入本动态链接库:

export LD_PRELOAD=/root/_code/FileDynamicTracer/libJfzTracer.so

可以在 JfzTracer.log 看追踪的文件读写信息了,例如:

unlinkat():/root/_code/aosp-ninja-trace/build
open()0:/root/_code/aosp-ninja-trace/build.ninja
fopen()ab:/root/_code/aosp-ninja-trace/build/.ninja_log
write():/root/_code/aosp-ninja-trace/_ninja_cmd/1589907313732834.log
open64()0:/root/_code/aosp-ninja-trace/src/browse.py

风险

本程序复写的函数代码不完美,很有可能导致系统某些基本指令或程序不能正常使用;

请不要全局载入本动态链接库!

请不要用于生产环境!

猜你喜欢

转载自blog.csdn.net/newlw/article/details/125047615