/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
部分信息可能已经过时



