您好,本站仅作演示所用,请勿下单付款!
商品分类

21.ChatGPT4o学KernelSU核心原理中的eBPF

21.ChatGPT4o学KernelSU核心原理中的eBPF

一百行代码实现的简易版“KernelSU”

// SPDX-License-Identifier: GPL-2.0
/*
 * Copyright (C) 2015-2018 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
 */

/* 
 * 这里是一些警告信息,提示用户如果他们的内核启用了某个不安全的功能,可能会导致安全问题。 
 * 它让用户注意不要在不安全的内核上运行。
 * 
 * 你可以忽略这些警告,专注于代码的逻辑。
 */

// 定义一个宏,方便输出调试信息
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

// 引入必需的 Linux 内核头文件
#include <linux/uaccess.h>   // 用户空间访问相关函数
#include <linux/module.h>    // 模块相关操作
#include <linux/fs.h>        // 文件系统相关操作
#include <linux/mman.h>      // 内存映射相关操作
#include <linux/ptrace.h>    // 调试和跟踪相关操作
#include <linux/syscalls.h>  // 系统调用相关操作

// 定义一个函数,检查传入的文件路径是否为 /system/bin/su
static bool is_su(const char __user *filename)  
{
    // 定义目标路径 "/system/bin/su"
    static const char su_path[] = "/system/bin/su";  
    
    // 创建一个缓存数组,用于存放从用户空间拷贝过来的路径
    char ufn[sizeof(su_path)];  

    // copy_from_user 函数:从用户空间拷贝数据到内核空间。这个操作必须小心处理,因为用户空间的数据不可信。
    // 这里拷贝用户传入的文件路径,并存入 ufn 数组。
    return likely(!copy_from_user(ufn, filename, sizeof(ufn)))  
    && unlikely(!memcmp(ufn, su_path, sizeof(ufn)));  // memcmp 函数:比较两个内存区域的内容,返回值 0 表示相同
    // likely 和 unlikely 是用来给编译器提供优化提示,告诉编译器哪部分更可能执行,从而优化分支预测。
}

// 定义一个函数,将数据写入用户空间的栈上,避免额外分配内存页
static void __user *userspace_stack_buffer(const void *d, size_t len)
{
    // 获取当前进程的栈指针,然后往下写数据
    char __user *p = (void __user *)current_user_stack_pointer() - len;

    // copy_to_user 函数:将数据从内核空间拷贝到用户空间。返回值 0 表示成功,非 0 表示失败。
    return copy_to_user(p, d, len) ? NULL : p;
}

// 返回 "/system/bin/sh" 的路径
static char __user *sh_user_path(void)
{
    static const char sh_path[] = "/system/bin/sh";

    // 调用 userspace_stack_buffer 函数,将路径写到用户栈空间
    return userspace_stack_buffer(sh_path, sizeof(sh_path));
}

// 钩住 newfstatat 系统调用,如果路径为 su,则改为执行 sh
static long(*old_newfstatat)(int dfd, const char __user *filename,
                             struct stat *statbuf, int flag);
static long new_newfstatat(int dfd, const char __user *filename,
                           struct stat __user *statbuf, int flag)
{
    // 如果传入的文件路径不是 "/system/bin/su",则继续执行原始的 newfstatat 系统调用
    if (!is_su(filename))
        return old_newfstatat(dfd, filename, statbuf, flag);

    // 如果路径是 "/system/bin/su",则执行 sh 文件
    return old_newfstatat(dfd, sh_user_path(), statbuf, flag);
}

// 钩住 faccessat 系统调用,功能与 newfstatat 类似,改为执行 sh
static long(*old_faccessat)(int dfd, const char __user *filename, int mode);
static long new_faccessat(int dfd, const char __user *filename, int mode)
{
    // 如果路径不是 "/system/bin/su",继续执行原始 faccessat 系统调用
    if (!is_su(filename))
        return old_faccessat(dfd, filename, mode);

    // 如果路径是 "/system/bin/su",执行 sh 文件
    return old_faccessat(dfd, sh_user_path(), mode);
}

// 钩住 execve 系统调用,如果路径是 su,则提升进程权限为 root
extern int selinux_enforcing;
static long (*old_execve)(const char __user *filename,
                          const char __user *const __user *argv,
                          const char __user *const __user *envp);
static long new_execve(const char __user *filename,
                       const char __user *const __user *argv,
                       const char __user *const __user *envp)
{
    static const char now_root[] = "You are now root.\n";
    struct cred *cred;

    // 如果路径不是 "/system/bin/su",执行原始 execve 系统调用
    if (!is_su(filename))
        return old_execve(filename, argv, envp);

    // 执行 execve 调用时,路径是 "/system/bin/su",我们将权限提升为 root
    if (!old_execve(filename, argv, envp))
        return 0;

    // 禁用 SELinux 强制执行
    selinux_enforcing = 0;

    // 手动设置当前进程的所有用户凭证为 root(uid 和 gid 设置为 0)
    cred = (struct cred *)__task_cred(current);
    memset(&cred->uid, 0, sizeof(cred->uid));
    memset(&cred->gid, 0, sizeof(cred->gid));
    memset(&cred->suid, 0, sizeof(cred->suid));
    memset(&cred->euid, 0, sizeof(cred->euid));
    memset(&cred->egid, 0, sizeof(cred->egid));
    memset(&cred->fsuid, 0, sizeof(cred->fsuid));
    memset(&cred->fsgid, 0, sizeof(cred->fsgid));
    memset(&cred->cap_inheritable, 0xff, sizeof(cred->cap_inheritable));
    memset(&cred->cap_permitted, 0xff, sizeof(cred->cap_permitted));
    memset(&cred->cap_effective, 0xff, sizeof(cred->cap_effective));
    memset(&cred->cap_bset, 0xff, sizeof(cred->cap_bset));
    memset(&cred->cap_ambient, 0xff, sizeof(cred->cap_ambient));

    // 输出提示信息:你现在是 root
    sys_write(2, userspace_stack_buffer(now_root, sizeof(now_root)),
              sizeof(now_root) - 1);

    // 将执行路径从 su 改为 sh,执行新的命令
    return old_execve(sh_user_path(), argv, envp);
}

// 替换系统调用的函数
extern const unsigned long sys_call_table[];
static void read_syscall(void **ptr, unsigned int syscall)
{
    // 读取指定的系统调用地址
    *ptr = READ_ONCE(*((void **)sys_call_table + syscall));
}

static void replace_syscall(unsigned int syscall, void *ptr)
{
    // 替换指定的系统调用地址
    WRITE_ONCE(*((void **)sys_call_table + syscall), ptr);
}

// 替换指定的系统调用
#define read_and_replace_syscall(name) do { \
    read_syscall((void **)&old_ ## name, __NR_ ## name); \
    replace_syscall(__NR_ ## name, &new_ ## name); \
} while (0)

// 模块初始化函数
static int superuser_init(void)
{
    // 模块启动时输出警告信息
    pr_err("WARNING WARNING WARNING WARNING WARNING\n");
    pr_err("This kernel has kernel-assisted superuser and contains a\n");
    pr_err("trivial way to get root. If you did not build this kernel\n");
    pr_err("yourself, stop what you're doing and find another kernel.\n");
    pr_err("This one is not safe to use.\n");
    pr_err("WARNING WARNING WARNING WARNING WARNING\n");

    // 替换系统调用
    read_and_replace_syscall(newfstatat);
    read_and_replace_syscall(faccessat);
    read_and_replace_syscall(execve);

    return 0;
}

// 模块初始化宏
module_init(superuser_init);

// 模块许可证和描述
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("Kernel-assisted superuser for Android");
MODULE_AUTHOR("Jason A. Donenfeld <Jason@zx2c4.com>");

解释:

  1. copy_from_usercopy_to_user
    • copy_from_user:将数据从用户空间拷贝到内核空间。
    • copy_to_user:将数据从内核空间拷贝到用户空间。
    这两个函数的参数:
    • 源地址:用户空间的地址或内核空间的地址。
    • 目标地址:用户空间或内核空间的目标地址。
    • 拷贝的长度:要拷贝的字节数。
  2. memcmp:用于比较内存中的数据,返回值为 0 表示两段数据相同。
  3. READ_ONCEWRITE_ONCE
    • 这两个宏用于读取和写入指针,确保在多核环境中不会出现读取旧值或写入多个值的情况。
  4. sys_write:这是一个系统调用,用来将数据写到标准错误输出(文件描述符 2)。这里输出一条字符串:“You are now root.\n”。
  5. __task_credstruct cred
    • __task_cred(current):获取当前任务(进程)的凭证结构体。
    • struct cred:存储进程的权限、用户 ID、组 ID 等信息。

执行效果:

  • 如果路径为 /system/bin/su,则会修改进程的权限,使其提升为 root 权限。
  • 通过替换系统调用,确保执行 su 命令时,会自动执行 sh,从而执行 root 提升。
  • 禁用 SELinux 强制执行,确保权限变更不受 SELinux 政策的限制。

示例:

  • 假设某个应用程序调用了 execve("/system/bin/su", ...),原本应该执行 su,但是由于模块的钩子,它会转向执行 /system/bin/sh,并且权限会被提升为 root。

通过这段代码,你可以学到如何通过钩住系统调用来修改进程的行为和权限,进而提升安全性或绕过限制。

Arm64 inline Hook 流程图

https://mp.weixin.qq.com/s/-WNNaAyUetiP5UWC5Re0SQ

inlinehook是实现对一个指定函数的hook,其大致流程如下图:

21.ChatGPT4o学KernelSU核心原理中的eBPF

首先,需要替换目标函数的头部的指令,将pc的值修改为hook函数的地址,跳转过去执行。

对于被覆盖的指令,我们将其放入hook函数中执行,对应图中的红色部分,这里会遇到非常多的问题,有兴趣的自行查找资料。

hook函数执行完之后,需要将 pc 的值修改为覆盖指令的下一条指令地址,然后跳转过去执行。

在这些过程中,我们需要保证寄存器在执行hook函数前后的值是一样的,执行的覆盖指令也不能修改寄存器,除非先将寄存器的值储存起来。

inline hook的相似性

这个代码中有inline hook的实现原理么,如果有具体举例,那一段的结构和inlinehook相似。

是的,这段代码中确实实现了 inline hook 的原理。inline hook 主要是通过修改系统调用表中相应系统调用函数的地址,将系统调用指向我们自定义的处理函数,从而实现对原始功能的修改或增强。
inline hook 原理

在内核中,系统调用通过系统调用表(sys_call_table)进行管理。当用户空间应用程序或内核模块需要调用某个系统调用时,它会通过系统调用表找到对应的系统调用函数。我们通过 inline hook,修改系统调用表中的函数指针,将其指向我们自定义的函数。这样,当系统调用被触发时,我们的自定义函数就会被执行。
相关代码

在你的代码中,new_newfstatat、new_faccessat 和 new_execve 等函数就是通过 inline hook 技术实现的。
代码示例分析

    定义旧的系统调用函数指针:

static long(*old_newfstatat)(int dfd, const char __user *filename,
                             struct stat *statbuf, int flag);

这里,我们声明了一个 old_newfstatat 指针,指向原始的 newfstatat 系统调用函数。之后,我们将 newfstatat 系统调用表中的地址替换为自定义的钩子函数(new_newfstatat),并保留原始的地址在 old_newfstatat 中。

自定义钩子函数 new_newfstatat:

static long new_newfstatat(int dfd, const char __user *filename,
                           struct stat __user *statbuf, int flag)
{
    if (!is_su(filename))
        return old_newfstatat(dfd, filename, statbuf, flag);  // 调用原始系统调用

    // 如果文件路径是 "/system/bin/su",替换为 "/system/bin/sh"
    return old_newfstatat(dfd, sh_user_path(), statbuf, flag); 
}

这里的 new_newfstatat 函数就是我们的钩子函数。当用户调用 newfstatat 系统调用时,首先会执行我们的自定义钩子函数。钩子函数会检查文件路径是否为 /system/bin/su,如果是,我们就替换成 /system/bin/sh,然后继续调用原始的系统调用。

    old_newfstatat(dfd, filename, statbuf, flag):我们保留了原始的系统调用函数指针 old_newfstatat,在钩子函数中调用它来执行原本应该做的操作。

替换系统调用:

read_and_replace_syscall(newfstatat);
read_and_replace_syscall(faccessat);
read_and_replace_syscall(execve);

这些宏是关键部分,它们将原始的系统调用地址替换为我们的钩子函数。read_and_replace_syscall 宏的作用就是:

    使用 read_syscall 获取原始的系统调用函数地址。

    使用 replace_syscall 将 sys_call_table 中的系统调用地址替换为我们定义的钩子函数。

具体实现如下:

    #define read_and_replace_syscall(name) do { \
        read_syscall((void **)&old_ ## name, __NR_ ## name); \
        replace_syscall(__NR_ ## name, &new_ ## name); \
    } while (0)

        read_syscall:从系统调用表中读取原始的系统调用地址。

        replace_syscall:将原始的系统调用地址替换为我们定义的钩子函数地址。

    钩子的执行过程:

    在模块初始化时,superuser_init 函数会调用 read_and_replace_syscall 宏,将原始的 newfstatat、faccessat 和 execve 系统调用替换为我们定义的 new_newfstatat、new_faccessat 和 new_execve。

    这就是 inline hook 的实现。通过这种方式,我们可以对内核中的系统调用进行动态修改。

inline hook 的原理举例

举个简单的例子:假设我们有一个系统调用 newfstatat,它的作用是获取文件的状态。如果我们想要在每次调用 newfstatat 时,检查传入的文件路径是否是 /system/bin/su,并将其替换为 /system/bin/sh,我们可以做如下操作:

    定义钩子函数:我们定义一个新的 new_newfstatat 函数,作为原始 newfstatat 系统调用的替代。

    保存原始函数指针:我们将原始的 newfstatat 函数地址保存在 old_newfstatat 中,以便在钩子中继续调用。

    替换系统调用表中的地址:通过 sys_call_table,我们将 newfstatat 系统调用表项替换为我们自定义的 new_newfstatat 函数。

总结

    inline hook 是通过修改系统调用表,替换系统调用函数的实现,从而改变原系统调用的行为。

    在这段代码中,new_newfstatat 和 new_faccessat 等函数实现了 inline hook,通过替换系统调用,改变执行的行为。

    使用 inline hook 时,我们需要保存原始的系统调用地址,以便在钩子中调用原始的实现,确保系统调用能正常工作。

通过理解这些步骤,你可以掌握 inline hook 技术并了解如何利用它来动态修改内核中的系统调用。

就会把新的函数替换到原来的系统调用表中间去。

通过inlinehook 把这个自定义的new 方法,替换到系统调用表中去。

他的这个功能和frida的inlinehook 是非常相向的。总共100多行, 核心已经实现了这些东西。

KernelSU核心原理:内核hook系统调用

su在执行的时候,他把,这些进程全部改掉, 因为是在内核空间中, 想怎么修改,就怎么修改。

关于作者,weishu的一篇ppt

21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF
21.ChatGPT4o学KernelSU核心原理中的eBPF

hook他的目标就是不要出现这个提示。正确执行了这个结果,那么我就有了root权限。而是当前的 这个进程,提权为root ,这样来的。

21.ChatGPT4o学KernelSU核心原理中的eBPF

sucompat.c 分析

https://github.com/tiann/KernelSU/blob/aefb1aaed24242f6afd6ac2b9f36352a522b21e7/kernel/sucompat.c#L257

// 包含内核相关的头文件,提供访问内核函数和结构体的功能
#include <linux/dcache.h>        // 缓存管理相关
#include <linux/security.h>      // 安全性相关
#include <asm/current.h>         // 当前任务相关
#include <linux/cred.h>          // 进程凭证相关
#include <linux/err.h>           // 错误处理相关
#include <linux/fs.h>            // 文件系统相关
#include <linux/kprobes.h>       // kprobe,内核动态追踪工具
#include <linux/types.h>         // 基本类型相关
#include <linux/uaccess.h>       // 用户空间访问相关
#include <linux/version.h>       // 内核版本相关
#include <linux/sched/task_stack.h> // 获取任务栈指针

// 包含其他模块相关的头文件
#include "objsec.h"              
#include "allowlist.h"           
#include "arch.h"                
#include "klog.h" // IWYU pragma: keep
#include "ksud.h"
#include "kernel_compat.h"

// 定义 su 和 sh 的路径
#define SU_PATH "/system/bin/su"
#define SH_PATH "/system/bin/sh"

// 函数声明,用于跳转到 root 权限
extern void escape_to_root();

// 通过用户栈分配缓冲区,避免 mmap 用户空间内存
static void __user *userspace_stack_buffer(const void *d, size_t len)
{
	/* To avoid having to mmap a page in userspace, just write below the stack
   * pointer. */
	char __user *p = (void __user *)current_user_stack_pointer() - len;

	// 将数据从内核空间拷贝到用户空间
	return copy_to_user(p, d, len) ? NULL : p;
}

// 返回一个指向 "/system/bin/sh" 的路径
static char __user *sh_user_path(void)
{
	static const char sh_path[] = "/system/bin/sh";

	// 调用 userspace_stack_buffer 函数将路径写入用户空间栈
	return userspace_stack_buffer(sh_path, sizeof(sh_path));
}

// 返回一个指向 ksud 路径的用户空间路径
static char __user *ksud_user_path(void)
{
	static const char ksud_path[] = KSUD_PATH;

	// 调用 userspace_stack_buffer 将 ksud 路径写入用户空间栈
	return userspace_stack_buffer(ksud_path, sizeof(ksud_path));
}

// 处理 faccessat 系统调用的钩子函数
int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,
			 int *__unused_flags)
{
	const char su[] = SU_PATH;

	// 检查当前 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(current_uid().val)) {
		return 0;
	}

	char path[sizeof(su) + 1];
	memset(path, 0, sizeof(path));

	// 从用户空间拷贝路径到内核空间
	ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));

	// 如果路径是 "/system/bin/su",替换为 "/system/bin/sh"
	if (unlikely(!memcmp(path, su, sizeof(su)))) {
		pr_info("faccessat su->sh!\n");
		*filename_user = sh_user_path(); // 指向 sh 用户路径
	}

	return 0;
}

// 处理 stat 系统调用的钩子函数
int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
{
	const char su[] = SU_PATH;

	// 检查当前 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(current_uid().val)) {
		return 0;
	}

	if (unlikely(!filename_user)) {
		return 0;
	}

	char path[sizeof(su) + 1];
	memset(path, 0, sizeof(path));

	// 从用户空间拷贝路径到内核空间
	ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));

	// 如果路径是 "/system/bin/su",替换为 "/system/bin/sh"
	if (unlikely(!memcmp(path, su, sizeof(su)))) {
		pr_info("newfstatat su->sh!\n");
		*filename_user = sh_user_path(); // 指向 sh 用户路径
	}

	return 0;
}

// 处理 execve 系统调用的钩子函数,支持 su 执行时切换到 sh
int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
			       void *__never_use_argv, void *__never_use_envp,
			       int *__never_use_flags)
{
	const char su[] = SU_PATH;
	char path[sizeof(su) + 1];

	// 检查路径是否为 su
	if (unlikely(!filename_user))
		return 0;

	memset(path, 0, sizeof(path));
	ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));

	if (likely(memcmp(path, su, sizeof(su))))
		return 0;

	// 检查当前 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(current_uid().val))
		return 0;

	pr_info("sys_execve su found\n");
	*filename_user = ksud_user_path(); // 替换为 ksud 路径

	escape_to_root(); // 执行 root 权限操作

	return 0;
}

// 处理设备点(devpts)访问的钩子函数
int ksu_handle_devpts(struct inode *inode)
{
	if (!current->mm) {
		return 0;
	}

	uid_t uid = current_uid().val;
	if (uid % 100000 < 10000) {
		// not untrusted_app, ignore it
		return 0;
	}

	// 检查当前 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(uid))
		return 0;

	if (ksu_devpts_sid) {
		struct inode_security_struct *sec = selinux_inode(inode);
		if (sec) {
			sec->sid = ksu_devpts_sid; // 设置 SELinux 安全标识
		}
	}

	return 0;
}

#ifdef CONFIG_KPROBES
// 钩子处理函数,用于监听 faccessat 系统调用
static int sys_faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
	struct pt_regs *real_regs = PT_REAL_REGS(regs);
	int *dfd = (int *)&PT_REGS_PARM1(real_regs);
	const char __user **filename_user =
		(const char **)&PT_REGS_PARM2(real_regs);
	int *mode = (int *)&PT_REGS_PARM3(real_regs);

	return ksu_handle_faccessat(dfd, filename_user, mode, NULL);
}

// 钩子处理函数,用于监听 newfstatat 系统调用
static int sys_newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
	struct pt_regs *real_regs = PT_REAL_REGS(regs);
	int *dfd = (int *)&PT_REGS_PARM1(real_regs);
	const char __user **filename_user = (const char **)&PT_REGS_PARM2(real_regs);
	int *flags = (int *)&PT_REGS_SYSCALL_PARM4(real_regs);

	return ksu_handle_stat(dfd, filename_user, flags);
}

// 钩子处理函数,用于监听 execve 系统调用
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
	struct pt_regs *real_regs = PT_REAL_REGS(regs);
	const char __user **filename_user =
		(const char **)&PT_REGS_PARM1(real_regs);

	return ksu_handle_execve_sucompat(AT_FDCWD, filename_user, NULL, NULL,
					  NULL);
}

// 钩子注册
static struct kprobe faccessat_kp = {
	.symbol_name = SYS_FACCESSAT_SYMBOL,
	.pre_handler = sys_faccessat_handler_pre,
};

static struct kprobe newfstatat_kp = {
	.symbol_name = SYS_NEWFSTATAT_SYMBOL,
	.pre_handler = sys_newfstatat_handler_pre,
};

static struct kprobe execve_kp = {
	.symbol_name = SYS_EXECVE_SYMBOL,
	.pre_handler = sys_execve_handler_pre,
};

// 钩子处理函数,用于监听 devpts 访问
static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs)
{
	struct inode *inode;
	struct file *file = (struct file *)PT_REGS_PARM2(regs);
	inode = file->f_path.dentry->d_inode;

	return ksu_handle_devpts(inode);
}

static struct kprobe pts_unix98_lookup_kp = { .symbol_name =
						      "pts_unix98_lookup",
					      .pre_handler =
						      pts_unix98_lookup_pre };

#endif

// 初始化 sucompat,注册 kprobe 钩子
void ksu_sucompat_init()
{
#ifdef CONFIG_KPROBES
	int ret;
	ret = register_kprobe(&execve_kp);
	pr_info("sucompat: execve_kp: %d\n", ret);
	ret = register_kprobe(&newfstatat_kp);
	pr_info("sucompat: newfstatat_kp: %d\n", ret);
	ret = register_kprobe(&faccessat_kp);
	pr_info("sucompat: faccessat_kp: %d\n", ret);
	ret = register_kprobe(&pts_unix98_lookup_kp);
	pr_info("sucompat: devpts_kp: %d\n", ret);
#endif
}

// 清理 sucompat,注销 kprobe 钩子
void ksu_sucompat_exit()
{
#ifdef CONFIG_KPROBES
	unregister_kprobe(&execve_kp);
	unregister_kprobe(&newfstatat_kp);
	unregister_kprobe(&faccessat_kp);
	unregister_kprobe(&pts_unix98_lookup_kp);
#endif
}

register_kprobe在内核中注册eBPF函数

register_kprobe 是 Linux 内核中的一个函数,它用于 注册一个 kprobe,即在内核的特定位置插入一个钩子(hook),从而允许我们在该位置执行自定义的代码。kprobe 是一种动态调试技术,它允许开发人员和管理员在不修改内核源代码的情况下,在内核函数的入口插入自定义的处理程序。
register_kprobe 的作用

插入钩子:register_kprobe 允许你在指定的内核函数入口处插入一个钩子函数。当内核执行到该函数时,会先执行钩子函数,然后再继续执行目标函数。这种机制非常有用,尤其是在调试和性能分析时。

动态分析:通过使用 kprobe,可以监控、分析或者记录内核函数的行为,跟踪函数的调用,查看函数参数等,而不需要修改内核源代码。这对于内核调试、故障排除和性能分析非常有用。

捕获函数调用:kprobe 可以在任意的内核函数入口处插入钩子,捕获函数的执行,并在函数执行前后或过程中插入自定义的代码。例如,在 sys_execve、sys_open 或其他重要系统调用中,钩子可以执行额外的操作,如记录日志、修改参数或增强安全性等。


register_kprobe 函数原型

int register_kprobe(struct kprobe *kp);

    参数:

        kp:指向一个 kprobe 结构体的指针。该结构体定义了钩子的相关信息,如目标函数的符号名称、钩子处理函数等。

    返回值:

        成功时返回 0。

        失败时返回负值,表示注册失败。失败的原因可能包括目标函数不存在、无法插入钩子等。

kprobe 结构体

kprobe 结构体包含了钩子函数的相关信息。它的主要字段包括:

struct kprobe {
    const char *symbol_name;   // 目标函数的符号名称(例如 sys_execve)
    kprobe_handler_t pre_handler; // 钩子函数,处理目标函数的前置逻辑
    kprobe_handler_t post_handler; // 钩子函数,处理目标函数的后置逻辑
    unsigned long addr;        // 目标函数的地址
    void *data;                // 用户数据,钩子函数可以通过该字段传递信息
};

    symbol_name:指定钩子函数应该插入到哪个内核函数的入口。

    pre_handler:指定在目标函数执行之前调用的函数。通常用来记录日志、修改输入参数或执行安全检查等。

    post_handler:指定在目标函数执行之后调用的函数。可以用来检查函数的返回值或执行清理工作。

    addr:目标函数的地址。如果使用符号名称(如 symbol_name)注册钩子,则此字段可为空。

    data:钩子函数可能需要一些用户自定义的数据,可以通过此字段传递。

使用示例:

#include <linux/kprobes.h>

static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
    pr_info("Entering the function %s\n", p->symbol_name);
    return 0;
}

static struct kprobe kp = {
    .symbol_name = "sys_execve",  // 目标函数
    .pre_handler = handler_pre,   // 前置钩子函数
};

static int __init kprobe_example_init(void)
{
    int ret = register_kprobe(&kp); // 注册 kprobe
    if (ret < 0) {
        pr_err("Registering kprobe failed\n");
        return ret;
    }
    pr_info("Kprobe registered for sys_execve\n");
    return 0;
}

static void __exit kprobe_example_exit(void)
{
    unregister_kprobe(&kp); // 注销 kprobe
    pr_info("Kprobe unregistered\n");
}

module_init(kprobe_example_init);
module_exit(kprobe_example_exit);
MODULE_LICENSE("GPL");

解释:

    handler_pre:这是一个前置钩子函数,它会在 sys_execve 函数执行之前调用。此函数打印一条日志,指示 sys_execve 被调用。

    kp:这是一个 kprobe 结构体,定义了目标函数 sys_execve 和对应的钩子函数 handler_pre。

    register_kprobe:此函数将 kprobe 注册到内核中,在目标函数 sys_execve 被调用时,handler_pre 会首先执行。

    unregister_kprobe:在模块卸载时注销钩子。

总结

register_kprobe 是 Linux 内核中的一种强大调试工具,它允许你在内核函数执行时插入自定义代码,进行动态分析、监控和调试。通过 kprobe,开发人员可以轻松实现对内核函数的跟踪和修改,而无需直接修改内核源代码或重编译内核。

流程2:命中函数的时候执行的是这个


// 钩子处理函数,用于监听 newfstatat 系统调用
static int sys_newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
	struct pt_regs *real_regs = PT_REAL_REGS(regs); // 这里是读参数,
	int *dfd = (int *)&PT_REGS_PARM1(real_regs); // 读取第一参数
	const char __user **filename_user = (const char **)&PT_REGS_PARM2(real_regs);  // 读取第二参数
	int *flags = (int *)&PT_REGS_SYSCALL_PARM4(real_regs); // 读取第三参数

	return ksu_handle_stat(dfd, filename_user, flags); //返回ksu_handle_stat
}

// 处理 stat 系统调用的钩子函数
int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
{
	const char su[] = SU_PATH;

	// 检查当前 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(current_uid().val)) {
		return 0;
	}

	if (unlikely(!filename_user)) {
		return 0;
	}

	char path[sizeof(su) + 1];
	memset(path, 0, sizeof(path));

	// 从用户空间拷贝路径到内核空间
	ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));

	// 如果路径是 "/system/bin/su",替换为 "/system/bin/sh"
	if (unlikely(!memcmp(path, su, sizeof(su)))) {
		pr_info("newfstatat su->sh!\n");
		*filename_user = sh_user_path(); // 指向 sh 用户路径
	}

	return 0;
}

代码漏了一些补充一下

21.ChatGPT4o学KernelSU核心原理中的eBPF

寻找 sh_user_path(void)

以下代码的作用

int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
{
	// const char sh[] = SH_PATH;
	const char su[] = SU_PATH;

	if (!ksu_is_allow_uid(current_uid().val)) {
		return 0;
	}

	if (unlikely(!filename_user)) {
		return 0;
	}

	char path[sizeof(su) + 1];
	memset(path, 0, sizeof(path));
// Remove this later!! we use syscall hook, so this will never happen!!!!!
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0) && 0
	// it becomes a `struct filename *` after 5.18
	// https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216
	const char sh[] = SH_PATH;
	struct filename *filename = *((struct filename **)filename_user);
	if (IS_ERR(filename)) {
		return 0;
	}
	if (likely(memcmp(filename->name, su, sizeof(su))))
		return 0;
	pr_info("vfs_statx su->sh!\n");
	memcpy((void *)filename->name, sh, sizeof(sh));
#else
	ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));

	if (unlikely(!memcmp(path, su, sizeof(su)))) {
		pr_info("newfstatat su->sh!\n");
		*filename_user = sh_user_path();
	}
#endif

	return 0;
}

// the call from execve_handler_pre won't provided correct value for __never_use_argument, use them after fix execve_handler_pre, keeping them for consistence for manually patched code
int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
				 void *__never_use_argv, void *__never_use_envp,
				 int *__never_use_flags)
{
	struct filename *filename;
	const char sh[] = KSUD_PATH;
	const char su[] = SU_PATH;

	if (unlikely(!filename_ptr))
		return 0;

	filename = *filename_ptr;
	if (IS_ERR(filename)) {
		return 0;
	}

	if (likely(memcmp(filename->name, su, sizeof(su))))
		return 0;

	if (!ksu_is_allow_uid(current_uid().val))
		return 0;

	pr_info("do_execveat_common su found\n");
	memcpy((void *)filename->name, sh, sizeof(sh));

	escape_to_root();

	return 0;
}

 

 这段代码包含了两个函数:ksu_handle_stat 和 ksu_handle_execveat_sucompat,它们都涉及对内核函数的钩子处理,特别是与 su 命令相关的路径处理。
这两个函数的目的是在某些条件下,通过修改文件路径,替换 su 路径为 sh 路径,从而使得进程可以在执行 su 命令时,跳转到 sh(Shell),并可能获得 root 权限。

ksu_handle_stat 函数解析




int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
{
	const char su[] = SU_PATH;

	// 检查当前进程的 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(current_uid().val)) {
		return 0;
	}

	// 如果 filename_user 为 NULL,直接返回
	if (unlikely(!filename_user)) {
		return 0;
	}

	// 定义一个临时缓冲区,用来存储从用户空间传来的路径
	char path[sizeof(su) + 1];
	memset(path, 0, sizeof(path));

	// 这段代码用于 Linux 内核版本 5.18 及以上版本(暂时未启用)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0) && 0
	// 这里会转换为 `struct filename *` 类型
	const char sh[] = SH_PATH;
	struct filename *filename = *((struct filename **)filename_user);
	if (IS_ERR(filename)) {
		return 0;
	}

	// 检查文件路径是否为 su,如果是,则替换为 sh
	if (likely(memcmp(filename->name, su, sizeof(su))))
		return 0;
	pr_info("vfs_statx su->sh!\n");
	memcpy((void *)filename->name, sh, sizeof(sh));
#else
	// 从用户空间将文件路径拷贝到内核空间
	ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));

	// 检查路径是否为 "/system/bin/su",如果是,则修改为 "/system/bin/sh"
	if (unlikely(!memcmp(path, su, sizeof(su)))) {
		pr_info("newfstatat su->sh!\n");
		*filename_user = sh_user_path(); // 用 sh 的路径替换 su 的路径
	}
#endif

	return 0;
}



作用:

ksu_handle_stat 是一个系统调用钩子函数,针对 stat 系统调用(获取文件状态)进行处理。

它检查当前进程的 UID 是否允许执行 su 命令,如果不允许,函数直接返回。

如果路径是 /system/bin/su,则会将路径修改为 /system/bin/sh,从而将 su 命令重定向到 sh,避免实际执行 su 而是执行 sh。

内核版本小于 5.18 的情况下,使用 ksu_strncpy_from_user_nofault 从用户空间拷贝文件路径,内核版本大于等于 5.18 则会使用 struct filename * 类型来处理。

重点逻辑:

ksu_is_allow_uid(current_uid().val):检查当前进程的 UID 是否允许执行 su。

memcmp(path, su, sizeof(su)):比较文件路径是否为 su,如果是,则替换为 sh。

*filename_user = sh_user_path():将 filename_user 指向 sh 的路径。

ksu_handle_execveat_sucompat 函数解析



int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
				 void *__never_use_argv, void *__never_use_envp,
				 int *__never_use_flags)
{
	struct filename *filename;
	const char sh[] = KSUD_PATH;
	const char su[] = SU_PATH;

	// 如果 filename_ptr 为空,则返回
	if (unlikely(!filename_ptr))
		return 0;

	// 获取文件路径
	filename = *filename_ptr;
	if (IS_ERR(filename)) {
		return 0;
	}

	// 如果路径不是 su,则直接返回
	if (likely(memcmp(filename->name, su, sizeof(su))))
		return 0;

	// 检查当前 UID 是否允许执行 su 操作
	if (!ksu_is_allow_uid(current_uid().val))
		return 0;

	pr_info("do_execveat_common su found\n");

	// 将文件路径从 su 替换为 sh
	memcpy((void *)filename->name, sh, sizeof(sh));

	// 调用 escape_to_root 提升权限
	escape_to_root();

	return 0;
}

两个函数的作用:

ksu_handle_execveat_sucompat 主要是处理 execveat 系统调用,钩住该系统调用实现对 su 命令的特殊处理。

当执行 execveat 时,如果文件路径是 /system/bin/su,并且当前进程的 UID 允许执行 su,则会将文件路径替换为 sh,并调用 escape_to_root() 提升权限。

两个函数的重点逻辑:

memcmp(filename->name, su, sizeof(su)):检查执行的命令路径是否为 su,如果是,则替换为 sh。

ksu_is_allow_uid(current_uid().val):检查当前 UID 是否允许执行 su,如果不允许,直接返回。

escape_to_root():执行权限提升的操作,可能是通过修改当前进程的用户凭证(cred)来提升为 root 权限。

两个函数的总结:

ksu_handle_stat:它钩住了 stat 系统调用,当某个进程查询 /system/bin/su 的状态时,会将其路径修改为 /system/bin/sh,避免执行 su,并且只有在 UID 被允许的情况下才进行路径替换。

ksu_handle_execveat_sucompat:钩住了 execveat 系统调用。当执行 su 时,钩子会把 su 路径修改为 sh,并且调用 escape_to_root 提升当前进程的权限,达到类似 root 权限的效果。

这两个函数的核心目的是通过修改系统调用的文件路径,将 su 命令转换为 sh 命令,并在适当的条件下提升进程权限,从而绕过一些限制,可能使某些进程能够获得 root 权限。

escape_to_root提权完整流程解析

提权操作,还是要将这些函数修改成为 0.就像下面

    // 手动设置当前进程的所有用户凭证为 root(uid 和 gid 设置为 0)
    cred = (struct cred *)__task_cred(current);
    memset(&cred->uid, 0, sizeof(cred->uid));
    memset(&cred->gid, 0, sizeof(cred->gid));
    memset(&cred->suid, 0, sizeof(cred->suid));
    memset(&cred->euid, 0, sizeof(cred->euid));
    memset(&cred->egid, 0, sizeof(cred->egid));
    memset(&cred->fsuid, 0, sizeof(cred->fsuid));
    memset(&cred->fsgid, 0, sizeof(cred->fsgid));
    memset(&cred->cap_inheritable, 0xff, sizeof(cred->cap_inheritable));
    memset(&cred->cap_permitted, 0xff, sizeof(cred->cap_permitted));
    memset(&cred->cap_effective, 0xff, sizeof(cred->cap_effective));
    memset(&cred->cap_bset, 0xff, sizeof(cred->cap_bset));
    memset(&cred->cap_ambient, 0xff, sizeof(cred->cap_ambient));

所以这个流程,和上面的几乎一样, 核心原理基本是大同小异的。 因为后面增加的app 的profile功能, 所以他的颗粒度,变得非常细致,一些处理,

https://github.com/tiann/KernelSU/blob/aefb1aaed24242f6afd6ac2b9f36352a522b21e7/kernel/core_hook.c#L109

void escape_to_root(void)
{
	struct cred *cred;

	cred = (struct cred *)__task_cred(current);

	if (cred->euid.val == 0) {
		pr_warn("Already root, don't escape!\n");
		return;
	}
	struct root_profile *profile = ksu_get_root_profile(cred->uid.val);

	cred->uid.val = profile->uid;
	cred->suid.val = profile->uid;
	cred->euid.val = profile->uid;
	cred->fsuid.val = profile->uid;

	cred->gid.val = profile->gid;
	cred->fsgid.val = profile->gid;
	cred->sgid.val = profile->gid;
	cred->egid.val = profile->gid;

	BUILD_BUG_ON(sizeof(profile->capabilities.effective) !=
		     sizeof(kernel_cap_t));

	// setup capabilities
	// we need CAP_DAC_READ_SEARCH becuase `/data/adb/ksud` is not accessible for non root process
	// we add it here but don't add it to cap_inhertiable, it would be dropped automaticly after exec!
	u64 cap_for_ksud =
		profile->capabilities.effective | CAP_DAC_READ_SEARCH;
	memcpy(&cred->cap_effective, &cap_for_ksud,
	       sizeof(cred->cap_effective));
	memcpy(&cred->cap_inheritable, &profile->capabilities.effective,
	       sizeof(cred->cap_inheritable));
	memcpy(&cred->cap_permitted, &profile->capabilities.effective,
	       sizeof(cred->cap_permitted));
	memcpy(&cred->cap_bset, &profile->capabilities.effective,
	       sizeof(cred->cap_bset));
	memcpy(&cred->cap_ambient, &profile->capabilities.effective,
	       sizeof(cred->cap_ambient));

	// disable seccomp
#if defined(CONFIG_GENERIC_ENTRY) &&                                           \
	LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)
	current_thread_info()->syscall_work &= ~SYSCALL_WORK_SECCOMP;
#else
	current_thread_info()->flags &= ~(TIF_SECCOMP | _TIF_SECCOMP);
#endif

#ifdef CONFIG_SECCOMP
	current->seccomp.mode = 0;
	current->seccomp.filter = NULL;
#else
#endif

	setup_groups(profile, cred);   // 这里进行了用户组的处理。

	setup_selinux(profile->selinux_domain); // 这里进行了 selinux的处理。
}

所以对 app profile 处理更加细致。

LSM_hook预定义插桩hook处理SELinux

21.ChatGPT4o学KernelSU核心原理中的eBPF

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

联系我们

联系我们

888-888-8888

在线咨询:点击这里给我发消息

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信
关注微信
分享本页
返回顶部