3
1
0

KernelSU对内核导出符号限制的绕过

2026-03-02

/proc/kallsyms

/proc/kallsyms是一个符号表,它包含了内核中所有的符号信息,包括函数、变量、常量等等。这些符号信息可以被内核模块和其他程序使用,这些符号类型可以帮助开发人员更好地理解内核中的符号信息,从而更好地进行内核开发和调试。

System.map 相比,/proc/kallsyms 是动态的,其包含了内核模块的符号地址

image

格式为 addr | type | symbol | [module name]

type的定义

T:表示该符号是一个函数,可以被其他代码调用。

t:表示该符号是一个局部函数,只能在当前文件中使用。

D:表示该符号是一个全局变量,可以被其他代码访问和修改。

d:表示该符号是一个局部变量,只能在当前文件中使用。

R:表示该符号是一个只读变量,不能被修改。

r:表示该符号是一个只读局部变量,只能在当前文件中使用。

A:表示该符号是一个可读写的变量,可以被其他代码访问和修改。

a:表示该符号是一个可读写的局部变量,只能在当前文件中使用。

B:表示该符号是一个未初始化的全局变量,它的值在程序启动时被初始化为0。

b:表示该符号是一个未初始化的局部变量,它的值在程序启动时被初始化为0。

G:表示该符号是一个全局变量,但是它的值在程序运行时可能会被修改。

C:表示该符号是一个常量,它的值在程序运行时不能被修改。

W:表示该符号是一个弱符号,

?: 表示该符号的类型未知。

/proc/sys/kernel/kptr_restrict

linux-kernel/Documentation/sysctl/kernel.txt at master · tinganho/linux-kernel

kptr_restrict:

This toggle indicates whether restrictions are placed on
exposing kernel addresses via /proc and other interfaces. When
kptr_restrict is set to (0), there are no restrictions. When
kptr_restrict is set to (1), the default, kernel pointers
printed using the %pK format specifier will be replaced with 0's
unless the user has CAP_SYSLOG. When kptr_restrict is set to
(2), kernel pointers printed using %pK will be replaced with 0's
regardless of privileges.

kptr_restrict 输出(在能访问/proc/kallsyms的前提下)
0 任何用户读取kallsyms都输出addr
1 拥有CAP_SYSLOG 权能的用户可以输出addr
2 任何用户读取kallsyms输出的addr均为0

上文中的截图 addr 输出为0就是因为 kptr_restrict 的值被设置为2

设置为1或0即可读取addr

image

ksuinit

KernelSU/userspace/ksuinit/src/loader.rs at main · tiann/KernelSU

use anyhow::{Context, Result};
use goblin::elf::{Elf, section_header, sym::Sym};
use rustix::{cstr, system::init_module};
use scroll::{Pwrite, ctx::SizeWith};
use std::collections::HashMap;
use std::fs;

struct Kptr {
    value: String,
}

impl Kptr {
    pub fn new() -> Result<Self> {
        let value = fs::read_to_string("/proc/sys/kernel/kptr_restrict")?;
        fs::write("/proc/sys/kernel/kptr_restrict", "1")?;
        Ok(Kptr { value })
    }
}

impl Drop for Kptr {
    fn drop(&mut self) {
        let _ = fs::write("/proc/sys/kernel/kptr_restrict", self.value.as_bytes());
    }
}

fn parse_kallsyms() -> Result<HashMap<String, u64>> {
    let _dontdrop = Kptr::new()?;

    let allsyms = fs::read_to_string("/proc/kallsyms")?
        .lines()
        .map(|line| line.split_whitespace())
        .filter_map(|mut splits| {
            splits
                .next()
                .and_then(|addr| u64::from_str_radix(addr, 16).ok())
                .and_then(|addr| splits.nth(1).map(|symbol| (symbol, addr)))
        })
        .map(|(symbol, addr)| {
            (
                symbol
                    .find("$")
                    .or_else(|| symbol.find(".llvm."))
                    .map_or(symbol, |pos| &symbol[0..pos])
                    .to_owned(),
                addr,
            )
        })
        .collect::<HashMap<_, _>>();

    Ok(allsyms)
}

pub fn load_module(path: &str) -> Result<()> {
    // check if self is init process(pid == 1)
    if !rustix::process::getpid().is_init() {
        anyhow::bail!("{}", "Invalid process");
    }

    let mut buffer = fs::read(path).with_context(|| format!("Cannot read file {}", path))?;
    let elf = Elf::parse(&buffer)?;

    let kernel_symbols = parse_kallsyms().context("Cannot parse kallsyms")?;

    let mut modifications = Vec::new();
    for (index, mut sym) in elf.syms.iter().enumerate() {
        if index == 0 {
            continue;
        }

        if sym.st_shndx != section_header::SHN_UNDEF as usize {
            continue;
        }

        let Some(name) = elf.strtab.get_at(sym.st_name) else {
            continue;
        };

        let offset = elf.syms.offset() + index * Sym::size_with(elf.syms.ctx());
        let Some(real_addr) = kernel_symbols.get(name) else {
            log::warn!("Cannot find symbol: {}", &name);
            continue;
        };
        sym.st_shndx = section_header::SHN_ABS as usize;
        sym.st_value = *real_addr;
        modifications.push((sym, offset));
    }

    let ctx = *elf.syms.ctx();
    for ele in modifications {
        buffer.pwrite_with(ele.0, ele.1, ctx)?;
    }
    init_module(&buffer, cstr!("")).context("init_module failed.")?;
    Ok(())
}

为了绕过内核中导出函数的限制

ksuinit在启动时将kptr_restrict设置为1,并且读取各个函数的地址

并且遍历 kernelsu.ko 的符号表,修复SHN_UNDEF的符号为SHN_ABS,并且写入函数的绝对地址

这样内核模块在加载时就不会爆找不到符号的错误

另一种绕过方法

rwProcMem33/hwBreakpointProcModule/hwBreakpointProc_module/kallsyms_lookup_api.h at master · abcz316/rwProcMem33

利用 kallsyms_lookup_name这个函数获取内核函数的绝对地址,然后通过这个地址来调用对应的函数

这里要注意,调用时的参数必须严格与这个函数的参数签名相同,否则可能无法通过cfi check

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

KernelSU对内核导出符号限制的绕过
/archives/kernelsu-export-symbol-bypass
作者
Britney
发布于
2026-03-02
许可协议
CC BY-NC-SA 4.0

评论