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>");
解释:
copy_from_user和copy_to_user:copy_from_user:将数据从用户空间拷贝到内核空间。copy_to_user:将数据从内核空间拷贝到用户空间。
- 源地址:用户空间的地址或内核空间的地址。
- 目标地址:用户空间或内核空间的目标地址。
- 拷贝的长度:要拷贝的字节数。
memcmp:用于比较内存中的数据,返回值为 0 表示两段数据相同。READ_ONCE和WRITE_ONCE:- 这两个宏用于读取和写入指针,确保在多核环境中不会出现读取旧值或写入多个值的情况。
sys_write:这是一个系统调用,用来将数据写到标准错误输出(文件描述符 2)。这里输出一条字符串:“You are now root.\n”。__task_cred和struct 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,其大致流程如下图:

首先,需要替换目标函数的头部的指令,将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















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

sucompat.c 分析
// 包含内核相关的头文件,提供访问内核函数和结构体的功能
#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;
}
代码漏了一些补充一下

寻找 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功能, 所以他的颗粒度,变得非常细致,一些处理,
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

