【问题标题】:How to hook ALL linux system calls during a binary execution如何在二进制执行期间挂钩所有 linux 系统调用
【发布时间】:2015-09-16 19:55:39
【问题描述】:

我正在尝试修改 linux 系统调用的默认行为。目前,我正在尝试在它们实际被调用之前挂钩并添加一个简单的打印语句。我知道 GCC 链接器的标准“包装”选项以及如何使用它来挂钩包装器Link to GCC Linker options。这非常适用于 open()、fstat()、fwrite() 等(我实际上是在挂钩 libc 包装器)。

更新:

限制是并非所有系统调用都与这种方法挂钩。 为了说明这一点,让我们以一个简单的静态编译二进制文件为例。当我们尝试添加包装器时,它们会受到我们在 main() 之后引入的调用的影响(请参阅下面显示的 strace 输出)

> strace ./sample 

execve("./sample", ["./sample"], [/* 72 vars */]) = 0
uname({sys="Linux", node="kumar", ...})   = 0
brk(0)                                  = 0x71f000
brk(0x7201c0)                           = 0x7201c0
arch_prctl(ARCH_SET_FS, 0x71f880)       = 0
readlink("/proc/self/exe", "/home/admin/sample"..., 4096) = 41
brk(0x7411c0)                           = 0x7411c0
brk(0x742000)                           = 0x742000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fbcc54d1000
write(1, "Hello from the wrapped readlink "..., 36Hello from the wrapped readlink :з
) = 36
readlink("/usr/bin/gnome-www-browser", "/etc/alternatives/gnome-www-brow"..., 255) = 35
write(1, "/etc/alternatives/gnome-www-brow"..., 36/etc/alternatives/gnome-www-browser
) = 36
exit_group(36)                          = ?
+++ exited with 36 +++

如果我们仔细注意到二进制文件,第一个“未拦截”调用 readlink()(系统调用 89 即 0x59)来自这些行——一些链接器相关的代码部分(即 _dl_get_origin)确实readlink() 用于其功能。这些隐式系统调用(尽管存在于二进制代码中)永远不会被我们的“包装”方法所吸引。

  000000000051875c <_dl_get_origin>:
  51875c:       b8 59 00 00 00          mov    $0x59,%eax
  518761:       55                      push   %rbp
  518762:       53                      push   %rbx
  518763:       48 81 ec 00 10 00 00    sub    $0x1000,%rsp
  51876a:       48 89 e6                mov    %rsp,%rsi
  51876d:       0f 05                   syscall 

如何将包装思想扩展到像 readlink() 这样的系统调用(包括所有被调用的隐式调用)?

【问题讨论】:

  • 为了简单起见,我正在做静态链接。所以我认为 LD_PRELOAD 对我没有帮助。

标签: linux gcc linker system-calls binutils


【解决方案1】:

ld 有一个换行选项,引用 from manual:

--换行符

对符号使用包装函数。任何未定义的符号引用都将解析为 __wrap_symbol。对 __real_symbol 的任何未定义引用都将被解析为符号。这可用于为系统函数提供包装器。包装函数应称为 __wrap_symbol。如果它想调用系统函数,它应该调用__real_symbol。

它也适用于系统调用。这是readlink 的示例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>

ssize_t __real_readlink(const char *path, char *buf, size_t bufsiz);

ssize_t __wrap_readlink(const char *path, char *buf, size_t bufsiz) {
    puts("Hello from the wrapped readlink :з");
    __real_readlink(path, buf, bufsiz);
}

int main(void) {
    const char testLink[] = "/usr/bin/gnome-www-browser";
    char buf[256];
    memset(buf, 0, sizeof(buf));
    readlink(testLink, buf, sizeof(buf)-1);
    puts(buf);
}

要将选项从编译器传递给链接器,请使用-Wl 选项:

$ gcc test.c -o a -Wl,--wrap=readlink
$ ./a
Hello from the wrapped readlink :з
/etc/alternatives/gnome-www-browser

这个想法是__wrap_func 是你的函数包装器。 __real_func 链接器将链接到真正的函数func。并且代码中对func 的每次调用都将替换为__wrap_func

UPD:人们可能会注意到正在编译的二进制文件会静态调用另一个readlink,而不会被拦截。要了解原因,只需做一个小实验——将代码编译为目标文件,并列出符号,例如:

$ gcc test.c -c -o a.o -Wl,--wrap=readlink
$ nm a.o
0000000000000037 T main
                 U memset
                 U puts
                 U readlink
                 U __real_readlink
                 U __stack_chk_fail
0000000000000000 T __wrap_readlink

这里有趣的是,在进入主函数之前,您不会看到对 strace 看到的一堆函数的引用——例如uname()brk()access() 等等。这是因为 main 函数不是在二进制文件中调用的第一个代码。 bit of research with objdump 会告诉你,第一个函数叫做 _start

现在,让我们再做一个例子——覆盖_start函数:

$ cat test2.c
#include <stdio.h>
#include <unistd.h>

void _start() {
        puts("Hello");
        _exit(0);
}
$ gcc test2.c -o a -nostartfiles
$ strace ./a
execve("./a", ["./a"], [/* 69 vars */]) = 0
brk(0)                                  = 0x150c000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=177964, ...}) = 0
mmap(NULL, 177964, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3ece531000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320\37\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0
mmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f3ecdf78000
mprotect(0x7f3ece133000, 2093056, PROT_NONE) = 0
mmap(0x7f3ece332000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7f3ece332000
mmap(0x7f3ece338000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f3ece338000
close(3)                                = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece530000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece52e000
arch_prctl(ARCH_SET_FS, 0x7f3ece52e740) = 0
mprotect(0x7f3ece332000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7f3ece55f000, 4096, PROT_READ) = 0
munmap(0x7f3ece531000, 177964)          = 0
fstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 10), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55c000
write(1, "Hello\n", 6Hello
)                  = 6
exit_group(0)                           = ?
+++ exited with 0 +++
$

那是什么?!我们刚刚覆盖了二进制文件中的第一个函数,仍然可以看到系统调用——为什么?

实际上这是因为调用不是由您的应用程序执行的,而是在您的应用程序加载到内存并允许运行之前由内核执行的。

UPD: 正如我们之前看到的,您的应用程序不会调用这些函数。老实说,在 shell 为您的应用程序调用 execve 之后,我找不到 静态二进制文件正在做什么,但从列表中看起来,您看到的每个调用都是由内核本身完成的 - 没有任何辅助应用程序,例如静态二进制文件不需要的动态链接器(因为有像 brk 这样的函数可以处理数据段)

无论如何,你肯定不能那么容易地修改这种行为,你需要一些黑客攻击。因为如果您可以轻松地覆盖在二进制文件运行之前执行的代码的函数(即从另一个二进制文件),那将是安全性的一个大黑洞,想象一下:一旦您需要 root 权限,您就覆盖了一个函数用一个来执行您的代码,并稍等片刻,而某些具有 root 权限的守护程序恰好执行脚本,从而触发您的代码发挥作用。

【讨论】:

  • 感谢您的帖子。但解决方案不正确。您正在挂钩您添加的 readlink() 而不是默认的 readlink() 可执行文件调用。如果您执行 strace,您会看到两个 readlink,并且您的 printf 输出来自您介绍的 readlink()
  • readlink("/proc/self/exe", "/home/admin/sample"..., 4096) = 41. 这从来没有被迷住,这是我的实际问题
  • @SandhyaKumar 什么?如果我不挂钩系统调用,我怎么能得到/etc/alternatives/gnome-www-browser 输出?
  • @SandhyaKumar 我刚刚做了 strace,我看到了 readlink("/usr/bin/gnome-www-browser", "/etc/alternatives/gnome-www-brow"..., 255) = 35。这是你想要的吗?
  • 如果你执行 strace,你应该注意到两个 readlinks() 正在执行。即第一个是我上面提到的“/proc/self/exe”行。第二行是您额外添加的 readlink() 调用。第二个电话是正确的。
猜你喜欢
  • 2011-03-31
  • 2016-06-23
  • 1970-01-01
  • 2011-01-07
  • 1970-01-01
  • 1970-01-01
  • 2021-01-08
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多