如何在二进制执行期间挂钩所有linux系统调用

San*_*mar 6 linux linker gcc system-calls binutils

我试图修改linux系统调用的默认行为.目前我正试图在实际调用它们之前挂钩并添加一个简单的print语句.我知道GCC链接器的标准'wrap'选项以及它如何用于挂钩包装器链接到GCC链接器选项.这完全适用于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 +++
Run Code Online (Sandbox Code Playgroud)

如果我们仔细注意到二进制文件,第一个"未拦截"调用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 
Run Code Online (Sandbox Code Playgroud)

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

Hi-*_*gel 1

ld有一个包装选项,引用自手册

\n\n
\n

--换行符号

\n\n

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

\n
\n\n

它也适用于系统调用。这是一个例子readlink

\n\n
#include <stdio.h>\n#include <string.h>\n#include <unistd.h>\n\nssize_t __real_readlink(const char *path, char *buf, size_t bufsiz);\n\nssize_t __wrap_readlink(const char *path, char *buf, size_t bufsiz) {\n    puts("Hello from the wrapped readlink :\xd0\xb7");\n    __real_readlink(path, buf, bufsiz);\n}\n\nint main(void) {\n    const char testLink[] = "/usr/bin/gnome-www-browser";\n    char buf[256];\n    memset(buf, 0, sizeof(buf));\n    readlink(testLink, buf, sizeof(buf)-1);\n    puts(buf);\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n
$ gcc test.c -o a -Wl,--wrap=readlink\n$ ./a\nHello from the wrapped readlink :\xd0\xb7\n/etc/alternatives/gnome-www-browser\n
Run Code Online (Sandbox Code Playgroud)\n\n

这个想法是__wrap_func你的函数包装器。链接__real_func器将与实际函数链接funcfunc代码中对 a 的每次调用都将替换为__wrap_func将替换为.

\n\n

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

\n\n
$ gcc test.c -c -o a.o -Wl,--wrap=readlink\n$ nm a.o\n0000000000000037 T main\n                 U memset\n                 U puts\n                 U readlink\n                 U __real_readlink\n                 U __stack_chk_fail\n0000000000000000 T __wrap_readlink\n
Run Code Online (Sandbox Code Playgroud)\n\n

这里有趣的是,在进入主函数 \xe2\x80\x94 之前,你不会看到对 strace 看到的一堆函数的引用,例如uname()brk()access()等。这是因为主函数不是\' t 在二进制文件中调用的第一个代码。一些研究objdump将告诉您,第一个函数称为_start.

\n\n

现在,让我们做另一个示例 \xe2\x80\x94 覆盖该_start函数:

\n\n
$ cat test2.c\n#include <stdio.h>\n#include <unistd.h>\n\nvoid _start() {\n        puts("Hello");\n        _exit(0);\n}\n$ gcc test2.c -o a -nostartfiles\n$ strace ./a\nexecve("./a", ["./a"], [/* 69 vars */]) = 0\nbrk(0)                                  = 0x150c000\naccess("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)\nmmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55d000\naccess("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)\nopen("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3\nfstat(3, {st_mode=S_IFREG|0644, st_size=177964, ...}) = 0\nmmap(NULL, 177964, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f3ece531000\nclose(3)                                = 0\naccess("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)\nopen("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3\nread(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\nfstat(3, {st_mode=S_IFREG|0755, st_size=1840928, ...}) = 0\nmmap(NULL, 3949248, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f3ecdf78000\nmprotect(0x7f3ece133000, 2093056, PROT_NONE) = 0\nmmap(0x7f3ece332000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1ba000) = 0x7f3ece332000\nmmap(0x7f3ece338000, 17088, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f3ece338000\nclose(3)                                = 0\nmmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece530000\nmmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece52e000\narch_prctl(ARCH_SET_FS, 0x7f3ece52e740) = 0\nmprotect(0x7f3ece332000, 16384, PROT_READ) = 0\nmprotect(0x600000, 4096, PROT_READ)     = 0\nmprotect(0x7f3ece55f000, 4096, PROT_READ) = 0\nmunmap(0x7f3ece531000, 177964)          = 0\nfstat(1, {st_mode=S_IFCHR|0600, st_rdev=makedev(136, 10), ...}) = 0\nmmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3ece55c000\nwrite(1, "Hello\\n", 6Hello\n)                  = 6\nexit_group(0)                           = ?\n+++ exited with 0 +++\n$\n
Run Code Online (Sandbox Code Playgroud)\n\n

它以前如何?!我们刚刚覆盖了二进制文件中的第一个函数,但仍然看到系统调用 \xe2\x80\x94 为什么?

\n\n

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

\n\n

UPD:正如我们之前所看到的,您的应用程序不会调用这些函数。老实说,在 shell 调用您的应用程序后,我找不到对静态二进制文件execve执行的操作,但从列表中看来,您看到的每个调用都是由内核本身 \xe2\x80\x94 完成的,没有任何侧面应用程序,例如静态二进制文件不需要的动态链接器(并且因为有类似的函数brk可用于数据段)

\n\n

无论如何,您肯定无法那么容易地修改此行为,您将需要一些黑客攻击。因为如果您可以轻松地覆盖在二进制文件运行 \xe2\x80\x94 之前执行的代码的函数,即从另一个二进制文件 \xe2\x80\x94 中执行的代码,那么这将是安全性中的一个大黑洞,想象一下:一旦你需要 root 权限,你用 root 权限覆盖一个函数来执行你的代码,然后稍等一下,某个具有 root 权限的守护进程恰好执行一个脚本,从而触发你的代码运行。

\n