ITPub博客

首页 > > > kptr_restrict指向用户空间内核中的指针(/proc/kallsyms-modules显示value全部为0)

kptr_restrict指向用户空间内核中的指针(/proc/kallsyms-modules显示value全部为0)

作者:dapan 时间:2023-02-20 10:29:11 0 删除 编辑

一、 /proc/kallsyms显示value全部为0

今天一个同事问我 cat /proc/kallsyms 显示 value 全部为 0. 我在手机端试了一下, 果然如此.

切换到 root 用户运行, 依然是 0. 感到十分奇怪, 因为内核发生 crash 或者打开 trace 的时候, 都是调用的 sprint_ symbol 来打印的. 为啥内核可以, 用户态 cat 就不行呢?

后来发现是系统为了保护这些符号地址泄露, 而用的一种保护手段, 从而使除 root 用户外的普通用户不能直接查看符号地址.

二、kptr_restrict 介绍

原因在于内核文件 kallsyms.c 中的显示符号地址命令中做了如下限制.

seq_printf(m, "%pK %c %s\n", (void *)iter->value, iter->type, iter->name);

只需要把其中的 %pK 换成 %p 就可以让普通用户查看符号地址了. 很多提权漏洞一般会用到此处的修改来获取符号地址

内核提供控制变量 /proc/sys/kernel/kptr_restrict 来进行修改. 从内核文档 Documentation/sysctl/kernel.txt 中可以看到 kptr_restrict 用于控制内核的一些输出打印.

Documentation/printk-formats.txt 有更加详细的描述, 除了我们平时遇到的一些打印格式之外, 还有一些比较特殊的格式(我以前没注意到).

==============================================================


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), the default, there are no restrictions.


When kptr_restrict is set to (1), kernel pointers printed using the %pK

format specifier will be replaced with 0's unless the user has CAP_SYSLOG

and effective user and group ids are equal to the real ids. This is

because %pK checks are done at read() time rather than open() time, so

if permissions are elevated between the open() and the read() (e.g via

a setuid binary) then %pK will not leak kernel pointers to unprivileged

users. Note, this is a temporary solution only. The correct long-term

solution is to do the permission checks at open() time. Consider removing

world read permissions from files that use %pK, and using dmesg_restrict

to protect against uses of %pK in dmesg(8) if leaking kernel pointer

values to unprivileged users is a concern.


When kptr_restrict is set to (2), kernel pointers printed using

%pK will be replaced with 0's regardless of privileges.

==============================================================

kptr_restrict 值为 2 时, 所有用户都无法读取内核符号地址.

kptr_restrict值为1时, 普通用户都无法读取内核符号地址,root用户可以查看.

kptr_restrict值为时, 所有用户都可以读取内核地址.

注意kptr_restrict对内核中很多地址和符号表的信息导出都有影响, 比如/proc/modules等.

三、kptr_restrict的设计

3.1 v4.14 之前的设计

在 linux 2.6.38 中 commit 455cd5ab305c ("kptr_restrict for hiding kernel pointers from unprivileged users") 引入了 kptr_restrict 用来限制用户态读取内核指针的显示.

具体处理 kptr_restrict 实现在 pointer 函数中, 该函数用于处理指针格式的 fmt 输出, 源码位于lib/vsprintf.c, v4.13.9, line 1794, 自 2.6.x 到 4.14 之前的版本这段流程没有太大的改动.

// elixir.bootlin.com/linu

static noinline_for_stack

char *pointer(const char *fmt, char *buf, char *end, void *ptr,

struct printf_spec spec)

{

// ......


case 'K':

switch (kptr_restrict) {

case 0:

/* Always print %pK values */

break;

case 1: {

const struct cred *cred;


/*

* kptr_restrict==1 cannot be used in IRQ context

* because its test for CAP_SYSLOG would be meaningless.

*/

if (in_irq() || in_serving_softirq() || in_nmi()) {

if (spec.field_width == -1)

spec.field_width = default_width;

return string(buf, end, "pK-error", spec);

}


/*

* Only print the real pointer value if the current

* process has CAP_SYSLOG and is running with the

* same credentials it started with. This is because

* access to files is checked at open() time, but %pK

* checks permission at read() time. We don't want to

* leak pointer values if a binary opens a file using

* %pK and then elevates privileges before reading it.

*/

cred = current_cred();

if (!has_capability_noaudit(current, CAP_SYSLOG) ||

!uid_eq(cred->euid, cred->uid) ||

!gid_eq(cred->egid, cred->gid))

ptr = NULL;

break;

}

case 2:

default:

/* Always print 0's for %pK */

ptr = NULL;

break;

}

break;

// ......

}

可见, kptr_restrict

  • 为 0 时, 未作任何处理, 直接输出, 这样对所有用户都没有限制.
  • 为 1 时, 中断上下文则不允许输出, 否则则校验了用户的权限.
  • 为 2 时, 将指针直接置 NULL, 这样所有用户都只能看到全 0.

整体思路还是比较清晰的. 想要详细了解实现全貌的, 可以直接查阅该特性合入时的 patchwork 或者 commit.

3.2 v4.15 开始的设计

3.2.1 printk 地址散列化

在 linux 4.15 中 commit 57e734423add (“vsprintf: refactor %pK code out of pointer()”) 将 point 中对 %pK(kptr_restrict) 的处理都封装到了 restricted_pointer 函数中. 这个修改比较简单, 就不详细描述了. 想要详细了解的同学可以查看提交的 commit

这次修改所属的 patchset 最主要是为了限制 %p 直接暴露内核的地址. 在早期内核中, 我们可以通过 System.map 或者 vmlinux 获取内核符号的地址信息, 内核中也较多的使用 %p 输出一些调试信息, 帮助用户显示一些符号的地址. 但是随着内核不断的演进, 开发者们发现, 随意暴露内核的地址是十分不安全的.

内核实现了越老越多的安全特性, 比如内核地址随机化等等. 正常情况下, kernel image 会按照 vmlinux 链接脚本中的链接地址去映射虚拟地址, 如果开启kaslr, 则会重新再映射一次, 映射到 链接地址 + offset的新地址上去. 如果 offset 每次开机都由 bootloader 随机生成, 那么每次开机后, kernel image最后映射的虚拟地址都不一样, 这就是内核地址随机化.

通过反汇编 vmlinux 以及读取 System.map 得不到内核中函数或者汇编语句的真正地址, 因为攻击者并不知道 kaslr 的 offset. 但是 %p 暴露内核地址将打破了这个限定. 攻击者通过 %p 打印中获取到的地址和 vmlinux 中该符号的地址进行比较, 就可以直接获取到 offset 偏移, 进而整个地址随机化机制将形同虚设.

最直接的办法就是将 %p 的打印全部处理掉.

没必要的从内核中删除掉

有必要但是可以限制的限制下(比如替换成 %pK).

特殊流程下, 要绕过限制直接打印实际地址的, 不做修改(比如 crash 流程输出 PANIC 日志的时候, 有必要输出实际地址.).

但是这些打印太多了, 改起来太麻烦了. 因此社区讨论后, 提出了一种新的方案.

限制 %p 的输出, 不再输出实际的地址, 而是输出一个散列化的地址.

想要在 crash 等流程输出实际地址时, 则使用 %px 输出内核符号的实际地址.

这样不用繁多的删动 %p 打印, 只需要限制 %p 的输出即可. 对于那些有必要输出实际地址的流程, 将 %p 替换为 %px, 这种路径通常是极少的, 改动起来也很少.

3.2.2 对 %pK 输出的影响

那么在 %p 的输出被散列的情况下, 对 %pK 输出的影响是什么呢?

首先我们看下实现

// elixir.bootlin.com/linu

static noinline_for_stack

char *restricted_pointer(char *buf, char *end, const void *ptr,

struct printf_spec spec)

{

switch (kptr_restrict) {

case 0:

/* Always print %pK values */

break;

case 1: {

const struct cred *cred;


/*

* kptr_restrict==1 cannot be used in IRQ context

* because its test for CAP_SYSLOG would be meaningless.

*/

if (in_irq() || in_serving_softirq() || in_nmi()) {

if (spec.field_width == -1)

spec.field_width = 2 * sizeof(ptr);

return string(buf, end, "pK-error", spec);

}


/*

* Only print the real pointer value if the current

* process has CAP_SYSLOG and is running with the

* same credentials it started with. This is because

* access to files is checked at open() time, but %pK

* checks permission at read() time. We don't want to

* leak pointer values if a binary opens a file using

* %pK and then elevates privileges before reading it.

*/

cred = current_cred();

if (!has_capability_noaudit(current, CAP_SYSLOG) ||

!uid_eq(cred->euid, cred->uid) ||

!gid_eq(cred->egid, cred->gid))

ptr = NULL;

break;

}

case 2:

default:

/* Always print 0's for %pK */

ptr = NULL;

break;

}


return pointer_string(buf, end, ptr, spec);

}

想要详细了解具体实现的, 可以直接查阅该特性合入时的 patchwork (“hash addresses printed with %p”) 或者 commit 57e734423add (“vsprintf: refactor %pK code out of pointer()”).

, 可以直接查看 restricted_pointer 函数, v5.1, Line 1481.

上面的提交是最早一版本实现, 后来 Linus 提交了 commit ef0010a30935 (“vsprintf: don’t use ‘restricted_pointer()’ when not restricting”).

static noinline_for_stack

char *pointer(const char *fmt, char *buf, char *end, void *ptr,

struct printf_spec spec)

{

// ......

case 'K':

if (!kptr_restrict)

break;

return restricted_pointer(buf, end, ptr, spec);

// ......


/* default is to _not_ leak addresses, hash before printing */

return ptr_to_id(buf, end, ptr, spec);

}

这个提交在发现如果 kptr_restrict 为 0 的时候, 就不再走 restricted_pointer() 流程, 而是走标准的 %p 格式处理流程, 这个会打印散列的地址. 这本身是合理的, 但是 kptr_restrict 的影响发生了变化. 当然只是 kptr_restrict == 0 时, 等同于 %p, 输出的是散列地址.

但是大家发现没发现, 上面的补丁其实有个问题, 存在一个空隙. 如果在处理 %pK 输出

在走到 pointer 的时候, 判断 kptr_restrict 不为 0, 走到 restricted_pointer() 流程.

restricted_pointer() 流程中, 如果会继续检查 kptr_restrict 的值, 去做不同的处理.

这里就有间隙了. 前后两次判断 kptr_restrict 的过程中, 并没有加锁, 因此如果前后读取的过程中 kptr_restrict 的值被修改了, 那么行为会变的很奇怪. 因为在 restricted_pointer 流程中, 如果发现 kptr_restrict 为 0, 则是直接用 pointer_string() 输出了实际地址的. 而如果在 restricted_pointer 函数外面, 则等同于 %p, 输出的散列地址. 可见不同时机 %pK 的输出在 kptr_restrict == 0 的时候表现竟然不一样.

这有两种处理办法, 一种是加锁, 让这段流程成为临界区, 互斥执行. 但是这显然没必要. 另外一种方法最简单, 不保证互斥, 只是在 restricted_pointer 流程中如果发现用户期望不限制(kptr_restrict == 0), 则同样打印散列地址.

3.2.3 v5.7 的修正

进一步处理了 errr-ptr 和 NULL-ptr 的输出, 让在所有情况下, 显示都保持清晰, 一致.

ptr error-ptr NULL

%p: 0000000001f8cc5b fffffffffffffff2 0000000000000000

%pK, kptr = 0: 0000000001f8cc5b fffffffffffffff2 0000000000000000

%px: ffff888048c04020 fffffffffffffff2 0000000000000000

%pK, kptr = 1: ffff888048c04020 fffffffffffffff2 0000000000000000

%pK, kptr = 2: 0000000000000000 0000000000000000 0000000000000000

3.2.4 示例

我们制作一个非常简单的用例, 分别用 %p, %pK, %px 打印一下子驱动中指针指向的数组的地址(指针的值)和指针本身的地址.

#include <linux/init.h>

#include <linux/module.h>

#include <linux/proc_fs.h>

#include <linux/seq_file.h>

#include <linux/percpu.h>

#include <linux/sched.h>

#include <linux/version.h>


#define SIZE 10

int array[SIZE];


int *p_arr = array;

static int __init exam_seq_init(void)

{

pr_info("%%p = %p, %%pK = %pK, %%px = %px\n", p_arr, p_arr, p_arr);

pr_info("%%p = %p, %%pK = %pK, %%px = %px\n", &p_arr, &p_arr, &p_arr);

return 0;

}


static void __exit exam_seq_exit(void)

{

}

module_init(exam_seq_init);

module_exit(exam_seq_exit);

MODULE_LICENSE("GPL");

我们写个脚本, 分别在 kptr_restrict 为 0, 1, 2 时插入模块, 显示下 %p* 的输出.

#cat test.sh

#!/bin/bash

dmesg -c

for kptr in `seq 0 2`

do

echo $kptr > /proc/sys/kernel/kptr_restrict

KPTR=`cat /proc/sys/kernel/kptr_restrict`

echo "/proc/sys/kernel/kptr_restrict: $KPTR"

insmod ./kptr_restrict_test.ko

dmesg -c

done

可以看到输出的内容与我的分析一致.

四、参考

Introducing Linux Kernel Symbols

Is there a way to set kptr_restrict to 0?

kptr_restrict for hiding kernel pointers from unprivileged users

Linux kallsyms 机制分析

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/758/viewspace-3153988/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2020-08-07

  • 博文量
    4504
  • 访问量
    87085