使用LD_PRELOAD更改程序的openat打开的路径

使用LD_PRELOAD更改程序的openat打开的路径

我想更改程序在文件系统上实际打开的某些路径的路径。原因是我想并行运行一个程序,但该程序用作/tmp/somedir/其临时目录,并且并行实例遇到冲突。

我发现这个很好的答案可以做到这一点:是否可以伪造进程的特定路径?。可悲的是,虽然这适用于cat广告,但它不适用于我的程序。我认为原因是程序使用了C++ API。

为了重现,我首先制作了一个非常简单的程序,在文件中写入一些内容:

#include <fstream>
#include <string_view>
#include <iostream>

int main() {
    std::ofstream myfile;
    myfile.open("test.log");
    std::string_view text{"hello world\n"};
    myfile.write(text.data(), text.size());
    return 0;
}

然后我使用strace并在最后看到了这个:

brk(NULL)                               = 0x558b5d5e3000
brk(0x558b5d604000)                     = 0x558b5d604000
futex(0x7f94e2e7e77c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "hello world\n", 12)           = 12
close(3)                                = 0
exit_group(0)                           = ?

所以看起来是调用了C api,但是使用的函数是openat.

我还看到了 C so 库的这个,稍后会涉及到:

openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

所以我openat除了实现之外open还实现了,这是完整的程序。出于测试目的,我没有更改路径,而是将其记录到文件中。

/*
 * capture calls to a routine and replace with your code
 * g++ -Wall -O2 -fpic -shared -ldl -lstdc++ -o fake_open_file.so fake_open_file.cpp
 * LD_PRELOAD=/home/myname/fake_open_file.so cat
 */
#define _FCNTL_H 1 /* hack for open() prototype */
#undef _GNU_SOURCE
#define _GNU_SOURCE /* needed to get RTLD_NEXT defined in dlfcn.h */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
#include <mutex>
#include <fstream>
#include <string_view>
#include <iostream>


// for the test, I just log anything that was open into a new log file
struct open_reporter
{
    open_reporter() = default;
    void report_filename(std::string_view filename)
    {
        std::lock_guard<std::mutex> l{file_report_mutex};
        if(!is_open) {
            myfile.open("/home/myname/fileopen.log");
        }
        std::string tmp = std::string{filename} + "\n";
        myfile.write(tmp.data(), tmp.size());
    }
    std::ofstream myfile;
    std::mutex file_report_mutex;
    bool is_open = false;
};

static open_reporter reporter_;

extern "C" {
    int open(const char *pathname, int flags, mode_t mode)
    {
        static int (*real_open)(const char *pathname, int flags, mode_t mode) = nullptr;

        if (!real_open) {
            real_open = reinterpret_cast<decltype(real_open)>(dlsym(RTLD_NEXT, "open"));
            char *error = dlerror();
            if (error != nullptr) {
                reporter_.report_filename("ERROR OCCURED!");
                reporter_.report_filename(error);
                exit(1);
            }
        }

        reporter_.report_filename(pathname);
        return real_open(pathname, flags, mode);
    }

    int openat(int dirfd, const char *pathname, int flags, mode_t mode)
    {
        static int (*real_openat)(int dirfd, const char *pathname, int flags, mode_t mode) = nullptr;

        if (!real_openat) {
            real_openat = reinterpret_cast<decltype(real_openat)>(dlsym(RTLD_NEXT, "openat"));
            char *error = dlerror();
            if (error != nullptr) {
                reporter_.report_filename("ERROR OCCURED!");
                reporter_.report_filename(error);
                exit(1);
            }
        }

        reporter_.report_filename(pathname);
        return real_openat(dirfd, pathname, flags, mode);
    }
}

这适用于cat静止,但不适用于我的测试程序。即使我更改openopenat返回 0,虽然这会中断,但cat它不会对我的测试程序产生任何影响。我还检查了这些符号是否在我的二进制文件中:

$ nm -gD fake_open_file.so | grep open
0000000000001470 W _ZN13open_reporterD1Ev
0000000000001470 W _ZN13open_reporterD2Ev
0000000000001450 T open
0000000000001460 T openat

我可以看到这两个功能都存在。查看 C 库,我看到了差异,但我不知道它意味着什么。我编辑了不是open或 的内容openat

$ nm -gD /lib/x86_64-linux-gnu/libc.so.6 |grep open
0000000000114820 W openat@@GLIBC_2.4
0000000000114820 W openat64@@GLIBC_2.4

0000000000114690 W open@@GLIBC_2.2.5
0000000000114690 W open64@@GLIBC_2.2.5

0000000000114690 W __open@@GLIBC_2.2.5
0000000000114690 W __open64@@GLIBC_2.2.5
00000000001147c0 T __open64_2@@GLIBC_2.7
0000000000119b80 T __open64_nocancel@@GLIBC_PRIVATE
0000000000114660 T __open_2@@GLIBC_2.7
0000000000040800 T __open_catalog@@GLIBC_PRIVATE
0000000000119b80 T __open_nocancel@@GLIBC_PRIVATE
0000000000114950 T __openat64_2@@GLIBC_2.7
00000000001147f0 T __openat_2@@GLIBC_2.7

除了内容之外@@GLIBC,这些都是相同的。我以前从未这样做过,所以这就是我的调试能力。我在这里问而不是这样,因为这是我得到原始答案的地方,而且这看起来更像是Linux知识而不是编程问题,程序本身非常简单。

答案1

背景:C

你的strace输出显示...

brk(NULL)                               = 0x558b5d5e3000
brk(0x558b5d604000)                     = 0x558b5d604000
futex(0x7f94e2e7e77c, FUTEX_WAKE_PRIVATE, 2147483647) = 0
openat(AT_FDCWD, "test.log", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
write(3, "hello world\n", 12)           = 12
close(3)                                = 0
exit_group(0)                           = ?

...但strace跟踪系统调用,而不是函数调用...以及通过LD_PRELOAD使用的函数插入功能来电。对于 C 程序,该openat调用可能是通过open或调用的open64。例如,如果我开始这样的事情:

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

int main() {
    int fd;
    int nb;

    if (-1 == (fd = open("test.log", O_RDWR|O_CREAT, 0666))) {
        perror("open");
        exit(1);
    }

    if (-1 == (nb = write(fd, "hello world\n", 12))) {
        perror("write");
        exit(1);
    }

    printf("write %d bytes\n", nb);

    return 0;
}

我在输出中看到strace这称为:

openat(AT_FDCWD, "test.log", O_RDWR|O_CREAT, 0666) = 3

但是,如果我尝试像您一样使用 覆盖openatLD_PRELOAD我会看到相同的行为:它不起作用。但是,如果我拦截该open调用:

int open(const char *pathname, int flags, mode_t mode) {
    static int (*real_open)(const char *, int, mode_t);

    fprintf(stderr, "OPEN PATH: %s\n", pathname);

    if (!real_open) {
        real_open = (dlsym(RTLD_NEXT, "open"));
        char *error = dlerror();
        if (error != NULL) {
            fprintf(stderr, "ERROR OCCURED! %s\n", error);
            exit(1);
        }
    }

    return real_open(pathname, flags, mode);
}

然后效果很好:

$ LD_PRELOAD=./fakeopen.so ./c_example
OPEN PATH: test.log
write 12 bytes

更复杂:C++

C++ 代码的情况有点复杂,因为当你编写......

myfile.open("test.log");

...到底什么是被调用?如果我们查看 的输出LD_DEBUG=symbols ./your_program,我们会看到如下内容:

    420239:     symbol=_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode;  lookup in file=./cc_main [0]
    420239:     symbol=_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode;  lookup in file=/lib64/libstdc++.so.6 [0]

所以实际的函数调用是损坏的名字像_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode.我们可以像任何其他函数一样覆盖它。如果我们创建wrapper.cc

#include <iostream>

extern "C" {
    int _ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode() {
        {
            std::cerr << "This is the wrapped open method\n";
            return 0;
        }
    }
}

并将其编译为wrapper.so

g++ -shared -fPIC -o wrapper.so wrapper.cc

然后我们可以将它与您的简单程序一起使用:

LD_PRELOAD=./wrapper.so ./your_program

并得到输出:

$ LD_PRELOAD=./wrapper.so ./your_program
This is the wrapped open method

这样,我们就成功地包装了该open方法!为了让它成功调用真正的方法,您需要弄清楚函数签名实际上_ZNSt14basic_ofstreamIcSt11char_traitsIcEE4openEPKcSt13_Ios_Openmode是什么样的。我不是 C++ 人员,我无法回答这个问题,但希望这可以帮助您取得进步。

杂项注释

可以使用 C++ 执行函数重载,这将允许您使用常规函数名称而不是损坏的名称(并且您不需要猜测 C++ 函数的 C 原型是什么样子)。

这在中进行了一些讨论stackoverflow 上的这个问题

相关内容