KernelSU对内核导出符号限制的绕过
/proc/kallsyms
/proc/kallsyms是一个符号表,它包含了内核中所有的符号信息,包括函数、变量、常量等等。这些符号信息可以被内核模块和其他程序使用,这些符号类型可以帮助开发人员更好地理解内核中的符号信息,从而更好地进行内核开发和调试。
与 System.map 相比,/proc/kallsyms 是动态的,其包含了内核模块的符号地址

格式为 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

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,并且写入函数的绝对地址
这样内核模块在加载时就不会爆找不到符号的错误
另一种绕过方法
利用 kallsyms_lookup_name这个函数获取内核函数的绝对地址,然后通过这个地址来调用对应的函数
这里要注意,调用时的参数必须严格与这个函数的参数签名相同,否则可能无法通过cfi check





