【问题标题】:How to hook sys_clone in newer Linux Kernel如何在较新的 Linux 内核中挂接 sys_clone
【发布时间】:2018-08-01 10:22:18
【问题描述】:

我正在尝试挂钩sys_clone,以绕过工具unhidehttp://www.unhide-forensics.info/ 使用的方法。这个想法是将 PID 从真实转换为虚假,将虚假转换为真实,使工具能够将隐藏的 PID 和未隐藏的 PID 视为连续标识符。 我的意思是,这个工具不断地分叉并检查在所有循环中是否有一些 PID 很忙并且不可见。 通过提供连续的 PID 来欺骗这个工具的想法。

问题在于,当程序执行我的sys_clone 的钩子处理程序时,返回后,wait(&status) 给出了 SEGVFAULT 的信号。

我使用的方法与我用来挂钩所有其他系统调用的方法相同,包括 sys_forksys_vfork 和其他系统调用。

sys_vforksys_fork 使用相同的函数_do_fork() 创建新进程: https://elixir.bootlin.com/linux/v4.15.1/source/kernel/fork.c#L2148

https://elixir.bootlin.com/linux/v4.15.1/source/kernel/fork.c#L2111

https://elixir.bootlin.com/linux/v4.15.1/source/kernel/fork.c#L2123

已编辑: 我创建了一个最小的工作示例,重现了这个错误,所以你可以更好地测试它,很抱歉,因为我以前没有:

#include <linux/module.h>
#include <linux/uaccess.h>

void **sys_call_table = NULL;
asmlinkage long (*sys_read)(long a1, long a2, long a3, long a4, long a5, long a6) = NULL;
asmlinkage long (*sys_clone)(long a1, long a2, long a3, long a4, long a5, long a6) = NULL;
asmlinkage long my_clone64(long a1, long a2, long a3, long a4, long a5, long a6);

char *sct_str;

module_param(sct_str, charp, 0);

inline void disable_wp(void) {
        asm("cli\n\tmov\t%cr0, %rax\n\tand\t$0xfffffffffffeffff, %rax\n\tmov\t%rax, %cr0\n\tsti");
}

inline void enable_wp(void) {
        asm("cli\n\tmov\t%cr0, %rax\n\tor\t$0x10000, %rax\n\tmov\t%rax, %cr0\n\tsti");
}

int init_module(void) {
        int ret = 0;
        mm_segment_t old_fs;

        kstrtoul(sct_str, 16, (unsigned long *) &sys_call_table);
        printk("%lx\n", sys_call_table);

        if (!sys_call_table) {
                return -1;
        }

        sys_read = sys_call_table[__NR_read];
        sys_clone = sys_call_table[__NR_clone];

        // hook sys_clone
        printk("be\n");
        disable_wp();
        sys_call_table[__NR_clone] = my_clone64;
        enable_wp();
        printk("af\n");

        // wait user's ENTER
        old_fs = get_fs();
        set_fs(KERNEL_DS);
        sys_read(0, (long)&ret, 1, 0, 0, 0);
        set_fs(old_fs);

        // restore sys_clone
        disable_wp();
        sys_call_table[__NR_clone] = sys_clone;
        enable_wp();

        return -1;
}

void cleanup_module(void) {
}


asmlinkage long my_clone64(long a1, long a2, long a3, long a4, long a5, long a6) {
        long ret = 0;
        printk("pid = %d\n", ret);
        ret = sys_clone(a1, a2, a3, a4, a5, a6);
        return ret;
}

MODULE_LICENSE("GPL");

使用这个Makefile编译:

obj-m += so.o

KERNEL_HEADERS = /lib/modules/$(shell uname -r)/build

all:
        make V=1 -C $(KERNEL_HEADERS) M=$(PWD) modules

clean:
        make V=1 -C $(KERNEL_HEADERS) M=$(PWD) clean

使用make 编译并加载:

diwou@diwou-VirtualBox:~/arpso$ sudo grep sys_call_table /proc/kallsyms
ffffffff9de00180 R sys_call_table
ffffffff9de01540 R ia32_sys_call_table
diwou@diwou-VirtualBox:~/arpso$ sudo insmod so.ko sct_str="ffffffff9de00180"
[NOW PRESS ENTER TO UNHOOK]
insmod: ERROR: could not insert module so.ko: Operation not permitted
diwou@diwou-VirtualBox:~/arpso$

Operattion not permitted 是由于我使用返回码 (-1) 来自动卸载模块。

这是另一个会话的输出,当sys_clone 被钩住时。我运行/bin/ls

diwou@diwou-VirtualBox:~$ ls
Violación de segmento (`core' generado)

解开sys_clone(按回车键)后,ls 命令再次起作用。

编辑: 如果您将my_clone64 替换为以下代码,则会发生相同的情况。但是,如果您将 call 更改为 jmp 它会起作用:

asm(
".globl my_clone64\n\t"
".type my_clone64, @function\n"
"my_clone64:\n\t"
"call sys_clone(%rip)\n\t"
// do something with %rax
"ret\n\t"
".size my_clone64, .-my_clone64\n\t"
);

这指向 Linux 内核中的某种实现......我说的对吗?

编辑: 有趣的是,当我在一个 ssh 会话中运行 strace -f bash 并且 read(0, 等待我的输入时,然后我在另一个会话中加载 LKM,并在被跟踪的 bash 中写入 ls,并且它可以工作。我可以看到ls 命令的输出。 但是如果我打开一个ssh会话,我加载LKM,并在bash上写ls而不被跟踪,只是一个普通的ssh会话,输出是segmentation fault:

clone(strace: Process 3233 attached
child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fa5f68be9d0) = 3233
[...]
[pid  3233] stat("arpso", {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
[pid  3233] open("arpso", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
[pid  3233] fstat(3, {st_mode=S_IFDIR|0775, st_size=4096, ...}) = 0
[pid  3233] getdents(3, /* 15 entries */, 32768) = 480
[pid  3233] lstat("arpso/so.c", {st_mode=S_IFREG|0664, st_size=1575, ...}) = 0
[pid  3233] lstat("arpso/so.ko", {st_mode=S_IFREG|0664, st_size=5760, ...}) = 0
[pid  3233] lstat("arpso/modules.order", {st_mode=S_IFREG|0664, st_size=31, ...}) = 0
[pid  3233] lstat("arpso/Module.symvers", {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
[pid  3233] lstat("arpso/so.o", {st_mode=S_IFREG|0664, st_size=5440, ...}) = 0
[pid  3233] lstat("arpso/so.mod.o", {st_mode=S_IFREG|0664, st_size=2528, ...}) = 0
[pid  3233] lstat("arpso/so.mod.c", {st_mode=S_IFREG|0664, st_size=542, ...}) = 0
[pid  3233] lstat("arpso/Makefile", {st_mode=S_IFREG|0664, st_size=177, ...}) = 0
[pid  3233] getdents(3, /* 0 entries */, 32768) = 0
[pid  3233] close(3)                    = 0
[...]
+++ exited with 0 +++
diwou@diwou-VirtualBox:~$ ls
Violación de segmento (`core' generado)
diwou@diwou-VirtualBox:~$

任何帮助将不胜感激。

谢谢。

【问题讨论】:

  • 我试图通过向您解释您已经将 Linux 内核完全超出其设计规范来提供帮助,因此它崩溃并不奇怪,并且可能有更好的方法来做你要。您处于“XY problem”认知陷阱中。
  • 你可能是对的。但我认为clone 更有可能对您在未向我们展示的代码中做错了什么特别敏感。无论如何,我不希望你再下降任何级别,我希望你上升几个级别,并首先解释你通过挂钩 clone 来实现的目标。
  • 这个问题很难回答(尽管它很重要或存在本体论问题)。您对 Linux 内核进行了一些任意且记录在问题中的更改。您发现了一个通常被描述为“分段错误”的问题。呈现的代码似乎有问题:C 类型是否正确?当现代内核使用gs 时,为什么要交换fs?你考虑过KPTI吗?你看过entry_64.Scommon.c吗?
  • 我认为人们不赞成这个问题,因为它并不完全清楚你做了什么(或者更重要的是为什么)。您的问题太大了,无法总结为一个好的 SO 问题。这似乎不是一个很好的minimal reproducible example。没有人说过任何关于否决票的事情,因为他们反对白帽黑客。
  • @AbelRomeroPérez:在那次讨论中,我站在 PeterCordes 一边。你想挂钩系统调用。你说你知道你正在做一些不寻常的事情。好的。然后你应该准备好调试困难。我明白了,你已经完成了一些调试。好的。但是要询问 Stack Overflow,您需要更多 - 您需要 minimal reproducible example。删除所有不必要的代码并尽可能简化其余部分。在my_clone64 实现中将SYSCALL64() 替换为sys_clone() 是否有效?如果不是,则完全删除SYSCALL64。如果是,请检查您是否最终使用正确的参数调用sys_clone。以此类推。

标签: c assembly linux-kernel hook clone


【解决方案1】:

我找到了答案。

常见的挂钩问题(也就是替换系统调用表中的系统调用处理程序)实际上是(至少在内核 4.15.1 中)定义了一个名为ptregs_sys_clone 的额外符号,这使得最终@987654322 有了一些技巧@到sys_clone。并且嵌套调用不是一种选择,因为检查了 retaddr 是否从一条路径或另一条路径...所以我找到的解决方案是替换 ptregs_sys_clone 中对 sys_clone 的引用。

代码如下。

core.c

#include <linux/uaccess.h>

void **sys_call_table = NULL;
asmlinkage long (*sys_read)(long a1, long a2, long a3, long a4, long a5, long a6) = NULL;
asmlinkage long (*ptregs_sys_clone)(struct pt_regs *regs) = NULL;
asmlinkage long (*sys_clone)(long a1, long a2, long a3, long a4, long a5, long a6) = NULL;
asmlinkage long my_clone64(long a1, long a2, long a3, long a4, long a5, long a6);

extern char *sct_str;

void disable_wp(void) {
        asm("cli\n\tmov\t%cr0, %rax\n\tand\t$0xfffffffffffeffff, %rax\n\tmov\t%rax, %cr0\n\tsti");
}

void enable_wp(void) {
        asm("cli\n\tmov\t%cr0, %rax\n\tor\t$0x10000, %rax\n\tmov\t%rax, %cr0\n\tsti");
}

int patch_ptregs_syscall(void *addr, long newaddr, long *oldaddr) {
        int i = 0, ret = 0, *p = NULL;
        long vaddr = 0; // variable address

        p = addr + 3; // point to offset in: lea offset(%rip), %register
        //printk("value before patch = %lx\n", *p);
        vaddr = (long)*p + addr + 7; // offset + %rip + lea' size
        if (oldaddr) {
                *oldaddr = vaddr;
        }
        printk("address %lx, ofsset %lx\n", vaddr, *p);
        vaddr = newaddr - (long)addr - 7;
        printk("new address %lx, new offset %lx\n", newaddr, vaddr);
        disable_wp();
        ret = probe_kernel_write(p, &vaddr, sizeof(int));
        enable_wp();
        if (ret != 0) {
                return -2;
        }

        return 0;
}

int install_hooks(void) {
        int ret = 0;
        mm_segment_t old_fs;

        if (!sct_str) {
                return -2;
        }

        kstrtoul(sct_str, 16, (unsigned long *) &sys_call_table);
        printk("sct: %lx\n", sys_call_table);

        if (!sys_call_table) {
                return -2;
        }

        sys_read = sys_call_table[__NR_read];
        ptregs_sys_clone = sys_call_table[__NR_clone];

        // hook sys_clone
        patch_ptregs_syscall(ptregs_sys_clone, (long)my_clone64, (long *)&sys_clone);

        // wait user's ENTER
        old_fs = get_fs();
        set_fs(KERNEL_DS);
        sys_read(0, (long)&ret, 1, 0, 0, 0);
        set_fs(old_fs);

        // restore sys_clone
        patch_ptregs_syscall(ptregs_sys_clone, (long)sys_clone, NULL);

        return -1;
}

asmlinkage long my_clone64(long a1, long a2, long a3, long a4, long a5, long a6) {
        pid_t pid = 0;

        pid = sys_clone(a1, a2, a3, a4, a5, a6);
        printk("pid %d\n", pid);
        return pid;
}

main.c:

#include <linux/module.h>

extern int install_hooks(void);

char *sct_str;

module_param(sct_str, charp, 0);

int init_module(void) {
        return install_hooks();
}

void cleanup_module(void) {
}

MODULE_LICENSE("GPL");

生成文件

obj-m += so.o

so-objs := main.o core-asm.o

EXTRA_CFLAGS := -O0

KERNEL_HEADERS = /lib/modules/$(shell uname -r)/build

all:
        make V=1 -C $(KERNEL_HEADERS) M=$(PWD) core.s
        gcc -c core.s -o core-asm.o
        make V=1 -C $(KERNEL_HEADERS) M=$(PWD) modules

clean:
        make V=1 -C $(KERNEL_HEADERS) M=$(PWD) clean

祝你好运,感谢您的所有 cmets 和帮助。

【讨论】:

  • 另一种选择可能是使用 jmp sys_clone 而不是 call sys_clone / ret 尾调用函数。因此堆栈将处于相同状态,您的调用者与sys_clone 看到的调用者一样。
  • 是的,如果您不需要获取新进程 pid,这很有用。但我需要抓住它。我正在尝试的另一个选择是修复堆栈,以便能够在较新的内核中从 ptregs_sys_clone 调用 sys_clone,在较旧的内核中调用 stub_sys_clone。但仍在研究 =)
  • 您问题中的示例并未显示您对返回值执行任何操作。我猜你的真实代码在callret 之间有更多指令。
  • 我要解决这个问题。你说得对。但在我使用的答案中......无论如何我都会修复它。是的,正如我所说的项目比这里给出的要大,我不得不做一个最小的可验证的例子,因为我得到了太多的 -1。在实际项目中,我与 sys_clone 挂钩没有任何关系,因为我是在最小的可验证示例上开发它的。但是我只是直接从钩子处理程序返回,以更清楚地解决问题:那种常见的钩子方式不起作用,甚至更简单的钩子处理程序。
  • 是的,只需在 MCVE 中添加评论,例如 "# do something with the PID in eax before returning\n\t"
猜你喜欢
  • 1970-01-01
  • 2022-08-24
  • 1970-01-01
  • 1970-01-01
  • 2014-11-05
  • 1970-01-01
  • 2020-11-27
  • 1970-01-01
  • 2015-05-29
相关资源
最近更新 更多