Files
SukiSU-Ultra/kernel/ksud.c
backslashxx d288b8f24f ksud: migrate ksud execution to security_bprm_check (tiann#2653)
This migrates ksud execution decision-making to bprm_check_security.
This requires passing proper argv and envp to a modified _ksud handler
aptly named 'ksu_handle_bprm_ksud'.

Introduces:
int ksu_handle_bprm_ksud(const char *filename, const char *argv1,
const char *envp, size_t envp_len)

which is adapted from:
int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,
struct user_arg_ptr *argv,
struct user_arg_ptr *envp,
int *flags)

ksu_handle_bprm_ksud handles all the decision making, it decides when it is
time to apply_kernelsu_rules depending if it sees "second_stage".

For LSM hook, turns out we can pull out argv and envp from mm_struct.
The code in here explains itself on how to do it.

whole blob exists on arg_start to arg_end, so we just pull it out and grab next
array after the first null terminator.

as for envp, we pass the pointer then hunt for it when needed

My reasoning on adding a fallback on usercopy is that on some devices a fault
happens, and it copies garbled data. On my creation of this, I actually had to lock
that _nofault copy on a spinlock as a way to mimic preempt_disable/enable without
actually doing it. As per user reports, no failed _nofault copies anyway but we
have-to-have a fallback for resilience.

References:
- old version1 6efcd8193e
- old version2 37d5938e66
- bad usercopy #21

This now provides a small helper function, ksu_copy_from_user_retry, which explains
itself. First we attempt a _nofault copy, if that fails, we try plain.

With that, It also provides an inlined copy_from_user_nofault for < 5.8.

While using strncpy_from_user_nofault was considered, this wont do, this will
only copy up to the first \0.

devlog:
16e5dce9e7...16c1f5f521
28642e60d7...728de0c571

References:
https://elixir.bootlin.com/linux/v4.14.1/source/include/linux/mm_types.h#L429
https://elixir.bootlin.com/linux/v4.14.1/source/include/linux/lsm_hooks.h

Stale: https://github.com/tiann/KernelSU/pull/2653

Signed-off-by: backslashxx <118538522+backslashxx@users.noreply.github.com>
2025-09-22 23:21:47 +08:00

652 lines
16 KiB
C

#include <asm/current.h>
#include <linux/compat.h>
#include <linux/cred.h>
#include <linux/dcache.h>
#include <linux/err.h>
#include <linux/file.h>
#include <linux/fs.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 4, 0)
#include <linux/input-event-codes.h>
#else
#include <uapi/linux/input.h>
#endif
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 1, 0)
#include <linux/aio.h>
#endif
#include <linux/kprobes.h>
#include <linux/printk.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/workqueue.h>
#include "allowlist.h"
#include "arch.h"
#include "klog.h" // IWYU pragma: keep
#include "ksud.h"
#include "kernel_compat.h"
#include "selinux/selinux.h"
bool ksu_is_compat __read_mostly = false; // let it here
static const char KERNEL_SU_RC[] =
"\n"
"on post-fs-data\n"
" start logd\n"
// We should wait for the post-fs-data finish
" exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n"
"\n"
"on nonencrypted\n"
" exec u:r:su:s0 root -- " KSUD_PATH " services\n"
"\n"
"on property:vold.decrypt=trigger_restart_framework\n"
" exec u:r:su:s0 root -- " KSUD_PATH " services\n"
"\n"
"on property:sys.boot_completed=1\n"
" exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n"
"\n"
"\n";
static void stop_vfs_read_hook(void);
static void stop_execve_hook(void);
static void stop_input_hook(void);
#ifdef CONFIG_KSU_KPROBES_HOOK
static struct work_struct stop_vfs_read_work;
static struct work_struct stop_bprm_check_work;
static struct work_struct stop_input_hook_work;
#else
bool ksu_vfs_read_hook __read_mostly = true;
bool ksu_input_hook __read_mostly = true;
#endif
bool ksu_execveat_hook __read_mostly = true;
u32 ksu_devpts_sid;
// Detect whether it is on or not
static bool is_boot_phase = true;
void on_post_fs_data(void)
{
static bool done = false;
if (done) {
pr_info("%s already done\n", __func__);
return;
}
done = true;
pr_info("%s!\n", __func__);
ksu_load_allow_list();
// sanity check, this may influence the performance
stop_input_hook();
ksu_devpts_sid = ksu_get_devpts_sid();
pr_info("devpts sid: %d\n", ksu_devpts_sid);
// End of boot state
is_boot_phase = false;
}
struct user_arg_ptr {
#ifdef CONFIG_COMPAT
bool is_compat;
#endif
union {
const char __user *const __user *native;
#ifdef CONFIG_COMPAT
const compat_uptr_t __user *compat;
#endif
} ptr;
};
// since _ksud handler only uses argv and envp for comparisons
// this can probably work
// adapted from ksu_handle_execveat_ksud
static int ksu_handle_bprm_ksud(const char *filename, const char *argv1, const char *envp, size_t envp_len)
{
static const char app_process[] = "/system/bin/app_process";
static bool first_app_process = true;
/* This applies to versions Android 10+ */
static const char system_bin_init[] = "/system/bin/init";
/* This applies to versions between Android 6 ~ 9 */
static const char old_system_init[] = "/init";
static bool init_second_stage_executed = false;
// return early when disabled
if (!ksu_execveat_hook)
return 0;
if (!filename)
return 0;
// debug! remove me!
pr_info("%s: filename: %s argv1: %s envp_len: %zu\n", __func__, filename, argv1, envp_len);
if (init_second_stage_executed)
goto first_app_process;
// /system/bin/init with argv1
if (!init_second_stage_executed
&& (!memcmp(filename, system_bin_init, sizeof(system_bin_init) - 1))) {
if (argv1 && !strcmp(argv1, "second_stage")) {
pr_info("%s: /system/bin/init second_stage executed\n", __func__);
apply_kernelsu_rules();
init_second_stage_executed = true;
ksu_android_ns_fs_check();
}
}
// /init with argv1
if (!init_second_stage_executed
&& (!memcmp(filename, old_system_init, sizeof(old_system_init) - 1))) {
if (argv1 && !strcmp(argv1, "--second-stage")) {
pr_info("%s: /init --second-stage executed\n", __func__);
apply_kernelsu_rules();
init_second_stage_executed = true;
ksu_android_ns_fs_check();
}
}
// /init without argv1/useless-argv1 but usable envp
// untested! TODO: test and debug me!
if (!init_second_stage_executed && (!memcmp(filename, old_system_init, sizeof(old_system_init) - 1))) {
// we hunt for "INIT_SECOND_STAGE"
const char *envp_n = envp;
unsigned int envc = 1;
do {
if (strstarts(envp_n, "INIT_SECOND_STAGE"))
break;
envp_n += strlen(envp_n) + 1;
envc++;
} while (envp_n < envp + envp_len);
pr_info("%s: envp[%d]: %s\n", __func__, envc, envp_n);
if (!strcmp(envp_n, "INIT_SECOND_STAGE=1")
|| !strcmp(envp_n, "INIT_SECOND_STAGE=true") ) {
pr_info("%s: /init +envp: INIT_SECOND_STAGE executed\n", __func__);
apply_kernelsu_rules();
init_second_stage_executed = true;
ksu_android_ns_fs_check();
}
}
first_app_process:
if (first_app_process && !memcmp(filename, app_process, sizeof(app_process) - 1)) {
first_app_process = false;
pr_info("%s: exec app_process, /data prepared, second_stage: %d\n", __func__, init_second_stage_executed);
on_post_fs_data();
stop_execve_hook();
}
return 0;
}
int ksu_handle_pre_ksud(const char *filename)
{
if (likely(!ksu_execveat_hook))
return 0;
// not /system/bin/init, not /init, not /system/bin/app_process (64/32 thingy)
// return 0;
if (likely(strcmp(filename, "/system/bin/init") && strcmp(filename, "/init")
&& !strstarts(filename, "/system/bin/app_process") ))
return 0;
if (!current || !current->mm)
return 0;
// https://elixir.bootlin.com/linux/v4.14.1/source/include/linux/mm_types.h#L429
// unsigned long arg_start, arg_end, env_start, env_end;
unsigned long arg_start = current->mm->arg_start;
unsigned long arg_end = current->mm->arg_end;
unsigned long env_start = current->mm->env_start;
unsigned long env_end = current->mm->env_end;
size_t arg_len = arg_end - arg_start;
size_t envp_len = env_end - env_start;
if (arg_len <= 0 || envp_len <= 0) // this wont make sense, filter it
return 0;
#define ARGV_MAX 32 // this is enough for argv1
#define ENVP_MAX 256 // this is enough for INIT_SECOND_STAGE
char args[ARGV_MAX];
size_t argv_copy_len = (arg_len > ARGV_MAX) ? ARGV_MAX : arg_len;
char envp[ENVP_MAX];
size_t envp_copy_len = (envp_len > ENVP_MAX) ? ENVP_MAX : envp_len;
// we cant use strncpy on here, else it will truncate once it sees \0
if (ksu_copy_from_user_retry(args, (void __user *)arg_start, argv_copy_len))
return 0;
if (ksu_copy_from_user_retry(envp, (void __user *)env_start, envp_copy_len))
return 0;
args[ARGV_MAX - 1] = '\0';
envp[ENVP_MAX - 1] = '\0';
#ifdef CONFIG_KSU_DEBUG
char *envp_n = envp;
unsigned int envc = 1;
do {
pr_info("%s: envp[%d]: %s\n", __func__, envc, envp_n);
envp_n += strlen(envp_n) + 1;
envc++;
} while (envp_n < envp + envp_copy_len);
#endif
// we only need argv1 !
// abuse strlen here since it only gets length up to \0
char *argv1 = args + strlen(args) + 1;
if (argv1 >= args + argv_copy_len) // out of bounds!
argv1 = "";
return ksu_handle_bprm_ksud(filename, argv1, envp, envp_copy_len);
}
int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,
struct user_arg_ptr *argv, struct user_arg_ptr *envp,
int *flags)
{
// this is now handled via security_bprm_check
// we only keep this for the sake of old hooks.
return 0;
}
static ssize_t (*orig_read)(struct file *, char __user *, size_t, loff_t *);
static ssize_t (*orig_read_iter)(struct kiocb *, struct iov_iter *);
static struct file_operations fops_proxy;
static ssize_t read_count_append = 0;
static ssize_t read_proxy(struct file *file, char __user *buf, size_t count,
loff_t *pos)
{
bool first_read = file->f_pos == 0;
ssize_t ret = orig_read(file, buf, count, pos);
if (first_read) {
pr_info("read_proxy append %ld + %ld\n", ret,
read_count_append);
ret += read_count_append;
}
return ret;
}
static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to)
{
bool first_read = iocb->ki_pos == 0;
ssize_t ret = orig_read_iter(iocb, to);
if (first_read) {
pr_info("read_iter_proxy append %ld + %ld\n", ret,
read_count_append);
ret += read_count_append;
}
return ret;
}
int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,
size_t *count_ptr, loff_t **pos)
{
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_vfs_read_hook) {
return 0;
}
#endif
struct file *file;
char __user *buf;
size_t count;
if (strcmp(current->comm, "init")) {
// we are only interest in `init` process
return 0;
}
file = *file_ptr;
if (IS_ERR(file)) {
return 0;
}
if (!d_is_reg(file->f_path.dentry)) {
return 0;
}
const char *short_name = file->f_path.dentry->d_name.name;
if (strcmp(short_name, "atrace.rc")) {
// we are only interest `atrace.rc` file name file
return 0;
}
char path[256];
char *dpath = d_path(&file->f_path, path, sizeof(path));
if (IS_ERR(dpath)) {
return 0;
}
if (strcmp(dpath, "/system/etc/init/atrace.rc")) {
return 0;
}
// we only process the first read
static bool rc_inserted = false;
if (rc_inserted) {
// we don't need this kprobe, unregister it!
stop_vfs_read_hook();
return 0;
}
rc_inserted = true;
// now we can sure that the init process is reading
// `/system/etc/init/atrace.rc`
buf = *buf_ptr;
count = *count_ptr;
size_t rc_count = strlen(KERNEL_SU_RC);
pr_info("vfs_read: %s, comm: %s, count: %zu, rc_count: %zu\n", dpath,
current->comm, count, rc_count);
if (count < rc_count) {
pr_err("count: %zu < rc_count: %zu\n", count, rc_count);
return 0;
}
size_t ret = copy_to_user(buf, KERNEL_SU_RC, rc_count);
if (ret) {
pr_err("copy ksud.rc failed: %zu\n", ret);
return 0;
}
// we've succeed to insert ksud.rc, now we need to proxy the read and modify the result!
// But, we can not modify the file_operations directly, because it's in read-only memory.
// We just replace the whole file_operations with a proxy one.
memcpy(&fops_proxy, file->f_op, sizeof(struct file_operations));
orig_read = file->f_op->read;
if (orig_read) {
fops_proxy.read = read_proxy;
}
orig_read_iter = file->f_op->read_iter;
if (orig_read_iter) {
fops_proxy.read_iter = read_iter_proxy;
}
// replace the file_operations
file->f_op = &fops_proxy;
read_count_append = rc_count;
*buf_ptr = buf + rc_count;
*count_ptr = count - rc_count;
return 0;
}
int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr,
size_t *count_ptr)
{
struct file *file = fget(fd);
if (!file) {
return 0;
}
int result = ksu_handle_vfs_read(&file, buf_ptr, count_ptr, NULL);
fput(file);
return result;
}
static unsigned int volumedown_pressed_count = 0;
static bool is_volumedown_enough(unsigned int count)
{
return count >= 3;
}
int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,
int *value)
{
#ifndef CONFIG_KSU_KPROBES_HOOK
if (!ksu_input_hook) {
return 0;
}
#endif
if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) {
int val = *value;
pr_info("KEY_VOLUMEDOWN val: %d\n", val);
if (val && is_boot_phase) {
// key pressed, count it
volumedown_pressed_count += 1;
if (is_volumedown_enough(volumedown_pressed_count)) {
stop_input_hook();
}
}
}
return 0;
}
bool ksu_is_safe_mode()
{
static bool safe_mode = false;
if (safe_mode) {
// don't need to check again, userspace may call multiple times
return true;
}
// stop hook first!
stop_input_hook();
pr_info("volumedown_pressed_count: %d\n", volumedown_pressed_count);
if (is_volumedown_enough(volumedown_pressed_count)) {
// pressed over 3 times
pr_info("KEY_VOLUMEDOWN pressed max times, safe mode detected!\n");
safe_mode = true;
return true;
}
return false;
}
#ifdef CONFIG_KSU_KPROBES_HOOK
// https://elixir.bootlin.com/linux/v5.10.158/source/fs/exec.c#L1864
static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
int *fd = (int *)&PT_REGS_PARM1(regs);
struct filename **filename_ptr =
(struct filename **)&PT_REGS_PARM2(regs);
struct user_arg_ptr argv;
#ifdef CONFIG_COMPAT
argv.is_compat = PT_REGS_PARM3(regs);
if (unlikely(argv.is_compat)) {
argv.ptr.compat = PT_REGS_CCALL_PARM4(regs);
} else {
argv.ptr.native = PT_REGS_CCALL_PARM4(regs);
}
#else
argv.ptr.native = PT_REGS_PARM3(regs);
#endif
return ksu_handle_execveat_ksud(fd, filename_ptr, &argv, NULL, NULL);
}
extern int ksu_bprm_check(struct linux_binprm *bprm);
static int bprm_check_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
struct linux_binprm *bprm_local = (struct linux_binprm *)PT_REGS_PARM1(regs);
return ksu_bprm_check(bprm_local);
};
static int sys_read_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
struct pt_regs *real_regs = PT_REAL_REGS(regs);
unsigned int fd = PT_REGS_PARM1(real_regs);
char __user **buf_ptr = (char __user **)&PT_REGS_PARM2(real_regs);
size_t count_ptr = (size_t *)&PT_REGS_PARM3(real_regs);
return ksu_handle_sys_read(fd, buf_ptr, count_ptr);
}
static int input_handle_event_handler_pre(struct kprobe *p,
struct pt_regs *regs)
{
unsigned int *type = (unsigned int *)&PT_REGS_PARM2(regs);
unsigned int *code = (unsigned int *)&PT_REGS_PARM3(regs);
int *value = (int *)&PT_REGS_CCALL_PARM4(regs);
return ksu_handle_input_handle_event(type, code, value);
}
static struct kprobe bprm_check_kp = {
.symbol_name = "security_bprm_check",
.pre_handler = bprm_check_handler_pre,
};
static struct kprobe vfs_read_kp = {
.symbol_name = SYS_READ_SYMBOL,
.pre_handler = sys_read_handler_pre,
};
static struct kprobe input_event_kp = {
.symbol_name = "input_event",
.pre_handler = input_handle_event_handler_pre,
};
static void do_stop_vfs_read_hook(struct work_struct *work)
{
unregister_kprobe(&vfs_read_kp);
}
static void do_stop_bprm_check_hook(struct work_struct *work)
{
unregister_kprobe(&bprm_check_kp);
}
static void do_stop_input_hook(struct work_struct *work)
{
unregister_kprobe(&input_event_kp);
}
#else
static int ksu_execve_ksud_common(const char __user *filename_user,
struct user_arg_ptr *argv)
{
struct filename filename_in, *filename_p;
char path[32];
long len;
// return early if disabled.
if (!ksu_execveat_hook) {
return 0;
}
if (!filename_user)
return 0;
len = ksu_strncpy_from_user_nofault(path, filename_user, 32);
if (len <= 0)
return 0;
path[sizeof(path) - 1] = '\0';
// this is because ksu_handle_execveat_ksud calls it filename->name
filename_in.name = path;
filename_p = &filename_in;
return ksu_handle_execveat_ksud(AT_FDCWD, &filename_p, argv, NULL,
NULL);
}
int __maybe_unused
ksu_handle_execve_ksud(const char __user *filename_user,
const char __user *const __user *__argv)
{
struct user_arg_ptr argv = { .ptr.native = __argv };
return ksu_execve_ksud_common(filename_user, &argv);
}
#if defined(CONFIG_COMPAT) && defined(CONFIG_64BIT)
int __maybe_unused ksu_handle_compat_execve_ksud(
const char __user *filename_user, const compat_uptr_t __user *__argv)
{
struct user_arg_ptr argv = { .ptr.compat = __argv };
return ksu_execve_ksud_common(filename_user, &argv);
}
#endif /* COMPAT & 64BIT */
#endif
static void stop_vfs_read_hook(void)
{
#ifdef CONFIG_KSU_KPROBES_HOOK
bool ret = schedule_work(&stop_vfs_read_work);
pr_info("unregister vfs_read kprobe: %d!\n", ret);
#else
ksu_vfs_read_hook = false;
pr_info("stop vfs_read_hook\n");
#endif
}
static void stop_execve_hook(void)
{
#ifdef CONFIG_KSU_KPROBES_HOOK
bool ret = schedule_work(&stop_bprm_check_work);
pr_info("unregister execve kprobe: %d!\n", ret);
#else
pr_info("stop execve_hook\n");
ksu_execveat_hook = false;
#endif
}
static void stop_input_hook(void)
{
#ifdef CONFIG_KSU_KPROBES_HOOK
static bool input_hook_stopped = false;
if (input_hook_stopped) {
return;
}
input_hook_stopped = true;
bool ret = schedule_work(&stop_input_hook_work);
pr_info("unregister input kprobe: %d!\n", ret);
#else
if (!ksu_input_hook) {
return;
}
ksu_input_hook = false;
pr_info("stop input_hook\n");
#endif
}
// ksud: module support
void ksu_ksud_init(void)
{
#ifdef CONFIG_KSU_KPROBES_HOOK
int ret;
ret = register_kprobe(&bprm_check_kp);
pr_info("ksud: bprm_check_kp: %d\n", ret);
ret = register_kprobe(&vfs_read_kp);
pr_info("ksud: vfs_read_kp: %d\n", ret);
ret = register_kprobe(&input_event_kp);
pr_info("ksud: input_event_kp: %d\n", ret);
INIT_WORK(&stop_vfs_read_work, do_stop_vfs_read_hook);
INIT_WORK(&stop_bprm_check_work, do_stop_bprm_check_hook);
INIT_WORK(&stop_input_hook_work, do_stop_input_hook);
#endif
}
void ksu_ksud_exit(void)
{
#ifdef CONFIG_KSU_KPROBES_HOOK
unregister_kprobe(&bprm_check_kp);
// this should be done before unregister vfs_read_kp
// unregister_kprobe(&vfs_read_kp);
unregister_kprobe(&input_event_kp);
is_boot_phase = false;
#endif
}