【问题标题】:How to skip a system call with ptrace?如何使用 ptrace 跳过系统调用?
【发布时间】:2017-01-03 03:53:28
【问题描述】:

我正在尝试使用 ptrace 编写一个程序来跟踪孩子所做的所有系统调用。

现在我有一个禁止孩子使用的系统调用列表。我可以使用 ptrace 跟踪所有系统调用,但我只是不知道如何跳过特定的系统调用。

目前,每次子进程进入或退出系统调用 (PTRACE_SYSCALL) 时,我的跟踪(父)进程都会收到一个信号。但是,如果孩子试图进入一个被禁止的系统调用,那么我不想让孩子跳过那个调用并进入下一步。另外,当我这样做时,我希望孩子知道存在权限被拒绝错误,所以我将设置 errno = 13,这样就足够了吗?

更新: gdb提供了这种跳行的特性。。gdb使用什么机制?

如何实现?

更新: 使用 ptrace 实现此目的的最佳方法是将原始系统调用重定向到其他一些系统调用,例如 nanosleep() 调用。此调用将失败,因为它将收到非法参数。然后您只需将 EAX 中的返回码更改为 -EACCES 以假装由于 Permission denied 错误而导致调用失败。

【问题讨论】:

  • 失败的系统调用返回负错误值。例如在 x86-64 上,rax = -EPERM。 glibc 系统调用包装器负责检测负值并设置errno 全局变量。

标签: linux x86-64 system-calls ptrace


【解决方案1】:

如果你想禁用系统调用,使用符号插入而不是 ptrace 可能是最简单的。 (假设您的目标不是针对恶意二进制文件的安全性。如果这是出于安全原因,PSKocik 的回答显示了如何使用 seccomp)。


创建一个提供gettimeofday 函数的共享库,该函数只设置errno 并返回而不进行系统调用。

使用LD_PRELOAD=./my_library.so ./a.out 在 libc 之前加载它。

这不适用于静态链接 libc 或使用内联系统调用而不是 libc 包装器的二进制文件(例如 mov eax, SYS_gettimeofday / syscall)。您可以反汇编二进制文件并查找 syscall (x86-64) 或 int 0x80 (i386 ABI) 来检查。

请注意,glibc 的 gettimeofday 和 clock_gettime 实现实际上从不进行真正的系统调用;相反,他们使用RDTSC 和内核导出的 VDSO 页面来了解如何将时间戳计数器实时缩放。 (所以拦截库函数是你唯一的希望;strace 风格的方法无论如何都无法捕获它们。)


顺便说一句,失败的系统调用返回负错误值。例如在 x86-64 上,rax = -EPERM。 glibc 系统调用包装器负责检测负值并设置errno 全局变量。所以如果你用 ptrace 拦截syscall 指令,那就是你需要做的。


re:edit: gdb 跳过行

gdb 可以通过使用 ptrace 跳过一行以在不同的位置恢复执行。不过,这仅在您已经停在那里时才有效。因此,要使用它来“跳过”系统调用,您必须在每个要在整个过程中阻塞的系统调用站点设置断点。

这听起来不是一个有用的方法。如果有人积极地试图打败它,他们可以 JIT 编译一些直接进行系统调用的代码。您可以阻止进程映射可写和可执行的内存,并在每次检测到进程跳入请求可执行但您的机制只是将其设置为可写的内存中的故障时扫描它以查找系统调用。 (所以在幕后,您捕获硬件生成的异常并将页面从可写翻转为可执行并扫描它,或者返回可写但不可执行。)

这听起来像是要正确实施很多内核黑客攻击,如果您需要能够抵抗变通方法和静态二进制文件的东西,您可以只使用 seccomp(请参阅其他答案)。

【讨论】:

  • 感谢这种方法。唯一的问题是我不知道将向我的程序提供哪个二进制文件,因此这种方法不适用于所有测试用例。也请检查更新的问题详情!
  • @harrythomas:您认为某些二进制文件会积极尝试破坏这种机制吗?还是静态链接的二进制文件?这真的很不寻常。如果您需要一种能够抵抗解决/击败它的机制,您可能应该按照其他答案的建议查看 seccomp。
  • 感谢更新。不,二进制文件不会试图破坏这种机制。无论如何,我可以通过修改寄存器值并将调用重定向到 nanosleep() 内核调用来实现这一点。感谢您的帮助!
【解决方案2】:

我发现两个大学讲座提到无法中止已启动的系统调用是 ptrace 的一个缺点(手册页提到 PTRACE_SYSEMU 宏看起来可以做到这一点,但较新的标题没有它)。从理论上讲,您可以利用 ptrace 入口和出口停止来抵消您不想要的调用——通过交换会导致系统调用失败或不执行任何操作的虚假参数,或者通过注入可以抵消的代码以前的系统调用,但这似乎非常hacky。

在 Linux 上,您应该能够通过 seccomp 实现您的目标:

#include <fcntl.h>
#include <seccomp.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>

static int set_security(){
    int rc = -1;
    scmp_filter_ctx ctx;
    struct scmp_arg_cmp arg_cmp[] = { SCMP_A0(SCMP_CMP_EQ, 2) };

    ctx = seccomp_init(SCMP_ACT_ERRNO(ENOSYS));
    /*ctx = seccomp_init(SCMP_ACT_ALLOW);*/
    if (ctx == NULL)
        goto out;

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
    if (rc < 0)
        goto out;
    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(close), 0);
    if (rc < 0)
        goto out;

    rc = seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
            SCMP_CMP(0, SCMP_CMP_EQ, 1));
    if (rc < 0)
        goto out;

    rc = seccomp_rule_add_array(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 1,
            arg_cmp);
    if (rc < 0)
        goto out;

    rc = seccomp_load(ctx);
    if (rc < 0)
        goto out;

    /* ... */

out:
    seccomp_release(ctx);
    return -rc;
}
int main(int argc, char *argv[])
{
    int fd;
    const char out_msg[] = "stdout test\n";
    const char err_msg[] = "stderr test\n";
    if(0>set_security())
        return 1;
    if(0>write(1, out_msg, sizeof(out_msg)))
        perror("Write stdout");
    if(0>write(2, err_msg, sizeof(err_msg)))
        perror("Write stderr");

    //This should fail with ENOSYS
    if(0>(fd=open("/dev/zero", O_RDONLY)))
        perror("open");

    exit(0);

}

【讨论】:

  • 感谢您提及 seccomp,我将尝试使用它,...请检查有问题添加的更新
猜你喜欢
  • 2012-08-14
  • 2011-07-26
  • 2012-10-23
  • 2013-12-24
  • 1970-01-01
  • 2012-04-03
  • 2012-11-05
  • 2011-06-09
  • 1970-01-01
相关资源
最近更新 更多