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

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

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可, 转载请注明出处, 谢谢合作

因本人技术水平和知识面有限, 内容如有纰漏或者需要修正的地方, 欢迎大家指正, 也欢迎大家提供一些其他好的调试工具以供收录, 鄙人在此谢谢啦


1 /proc/kallsyms显示value全部为0


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

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

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

2 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内核将符号地址打印为全0, root和普通用户都没有权限
1root用户有权限读取, 普通用户没有权限
0root和普通用户都可以读取

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

 值为  时

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

 值为  时

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

 值为0时

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

3 kptr_restrict的设计


时间作者特性描述是否合入主线链接
2010/12/23Dan Rosenberg drosenberg@vsecurity.comkptr_restrict for hiding kernel pointers from unprivileged users引入 kptr_restrict 限制都内核指针的读取v7 ☑ 2.6.38-rc1v7 PatchWork
-------
v7 PatchWork
-------
commit 455cd5ab305c
2017/11/19Tobin C. Harding me@tobin.cchash addresses printed with %p内核中 %p 打印的地址会暴露内核态地址信息, 是极其不安全的, 因此限制 %p 的打印信息, 它将打印一个散列值, 并不是实际的地址. 如果想要打印实际地址, 需要显式指定 %px.v11 ☑ 4.15-rc2PatchWork
-------
关键 commit 57e734423add
2017/11/29Linus Torvalds torvalds@linux-foundation.orgvsprintf: don’t use ‘restricted_pointer()’ when not restricting如果发现 kptr_restrict 为 0, 则直接跳过 restricted_pointer() 流程. 这个补丁将影响 kptr_restrict 为 0 时, %pK 等同于 %p 输出散列地址.v1 4.15-rc2commit ef0010a30935 (“vsprintf: don’t use ‘restricted_pointer()’ when not restricting”)
2018/02/16Andy Shevchenko andriy.shevchenko@linux.intel.comlib/vsprintf: Deduplicate pointer_string()简单的重构, 删除了一些重复的判断, 逻辑没有任何修正PatchWork
-------
关键 commit 496a9a5f3806
2019/04/17Petr Mladek pmladek@suse.comhvsprintf: Prevent silent crashes and consolidate error handling修复上面补丁引入的一个小问题, 限制 restricted_pointer 中 kptr_restrict 为 0 时, 输出散列地址.v7 ☑ 5.2-rc1PatchWork
-------
关键 commit 1ac2f9789c4b
2020/02/19Ilya Dryomov idryomov@gmail.comvsprintf: don’t obfuscate NULL and error pointers同样的, 之前 NULL 指针和错误指针的输出也很混乱, 进行了归一化.v2 ☑ 5.7-rc7PatchWork
-------
关键 commit 7bd57fbc4a4d

3.1 v4.14 之前的设计


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

kptr_restrict_for_4.13

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

// https://elixir.bootlin.com/linux/v4.13.9/source/lib/vsprintf.c#L1794
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.

时间作者特性描述是否合入主线链接
2010/12/23Dan Rosenberg drosenberg@vsecurity.comkptr_restrict for hiding kernel pointers from unprivileged users引入 kptr_restrict 限制都内核指针的读取v7 ☑ 2.6.38-rc1v7 PatchWork
-------
v7 PatchWork
-------
commit 455cd5ab305c

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 的打印全部处理掉.

  1. 没必要的从内核中删除掉

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

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

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

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

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

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

3.2.2 对 %pK 输出的影响


那么在 %p 的输出被散列的情况下, 对 %pK 输出的影响是什么呢?
首先我们看下实现

kptr_restrict_for_5.1

// https://elixir.bootlin.com/linux/v5.1/source/lib/vsprintf.c#L1481
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);
}
kptr_restrict实现输出
0不做任何处理, 直接 pointer_string 输出不限制, 所有用户都可以看到实际地址
1如果有权限(root 用户等), 直接 pointer_string 输出, 否则置 NULL部分限制, 普通用户看到全 0, root 等用户看到实际地址
2直接置 NULL全限制, 任何用户都看到全 0

想要详细了解具体实现的, 可以直接查阅该特性合入时的 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, 输出的是散列地址.

kptr_restrict实现输出
0不做任何处理, 直接跳过 restricted_pointer() 流程, 使用 ptr_to_id() 输出散列地址限制, 所有用户都可以看到散列地址, 相当于 %p 输出.
1如果有权限(root 用户等), 直接 pointer_string() 输出, 否则置 NULL部分限制, 普通用户看到全 0, root 等用户看到实际地址
2直接置 NULL全限制, 任何用户都看到全 0

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

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

  2. 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), 则同样打印散列地址.

时间作者特性描述是否合入主线链接
2017/11/19Tobin C. Harding me@tobin.cchash addresses printed with %p内核中 %p 打印的地址会暴露内核态地址信息, 是极其不安全的, 因此限制 %p 的打印信息, 它将打印一个散列值, 并不是实际的地址. 如果想要打印实际地址, 需要显式指定 %px.v11 ☑ 4.15-rc2PatchWork
-------
关键 commit 57e734423add
2017/11/29Linus Torvalds torvalds@linux-foundation.orgvsprintf: don’t use ‘restricted_pointer()’ when not restricting如果发现 kptr_restrict 为 0, 则直接跳过 restricted_pointer() 流程. 这个补丁将影响 kptr_restrict 为 0 时, %pK 等同于 %p 输出散列地址.v1 4.15-rc2commit ef0010a30935 (“vsprintf: don’t use ‘restricted_pointer()’ when not restricting”)
2019/04/17Petr Mladek pmladek@suse.comhvsprintf: Prevent silent crashes and consolidate error handling修复上面补丁引入的一个小问题, 限制 restricted_pointer 中 kptr_restrict 为 0 时, 输出散列地址.v7 ☑ 5.2-rc1PatchWork
-------
关键 commit 1ac2f9789c4b

3.2.3 v5.7 的修正


时间作者特性描述是否合入主线链接
2020/02/19Ilya Dryomov idryomov@gmail.comvsprintf: don’t obfuscate NULL and error pointers同样的, 之前 NULL 指针和错误指针的输出也很混乱, 进行了归一化.v2 ☑ 5.7-rc7PatchWork
-------
关键 commit 7bd57fbc4a4d

进一步处理了 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

测试程序

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

4 参考


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 机制分析


已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页