Linux Kernel代码艺术(3)

漏洞 CVE-2009-0029 解析

如果我们查看2.6.28 之前的代码,系统调用确实没有这样写,但在2009年64位 Linux 内核在某些64位平台下被发现系统调用有漏洞,为了修复该漏洞系统调用才改写成现在这样的。该漏洞被命名为 CVE-2009-0029 ,对该漏洞的简单描述如下:

The ABI in the Linux kernel 2.6.28 and earlier on s390, powerpc, sparc64, and mips 64-bit platforms requires that a 32-bit argument in a 64-bit register was properly sign extended when sent from a user-mode application, but cannot verify this, which allows local users to cause a denial of service (crash) or possibly gain privileges via a crafted system call.

意思是说,在Linux 2.6.28及以前版本内核中,IBM/S390、PowerPC、Sparc64以及MIPS 64位平台的ABI要求在系统调用时,用户空间程序将系统调用中32位的参数存放在64位的寄存器中要做到正确的符号扩展,但是用户空间程序却不能保证做到这点,这样就会可以通过向有漏洞的系统调用传送特制参数便可以导致系统崩溃或获得权限提升。

举例来说,假如下面是个系统调用的内核代码,参数是32位的无符号整型,但使用的64位寄存器传参,上面提及到平台的ABI要求32为参数存放在64位寄存器中要符号扩展,由程序的调用者来完成,在系统调用的函数中则由用户程序来保证进行了正确的寄存器符号扩展,但用户空间程序却无法保证。

asmlinkage long sys_example(unsigned int index) { if (index > 5) return -EINVAL; return example_array[index]; }

在上面程序中,调用程序必须将索引符号扩展为64位,如传入参数index=3,那么将寄存器的低32位赋值为3,并未修改高32位,此时该寄存器的高32位假设为0xFFFFFFFF,在进入该系统调用函数时,由于编译器认为你已经进行符号扩展了,所以直接引用64位寄存器的值代表index,此时index=-4294967293,判断不大于5,返回example_array[-4294967293],很可能访问到一块没有权限访问的地址空间或者其他地址异常的错误而导致程序崩溃。

怎么去解决这个问题呢,也许你会想既然用户空间没有进行寄存器的符号扩展,那么我在系统调用函数之前加入一些汇编代码将寄存器进行符号扩展,但有个问题是,系统调用前代码都是公共的,因此并不能将某个寄存器一定符号扩展。

在Linux内核中,解决这个问题的办法很巧妙,它先将所有参数都当成long类型(64位),然后再强制转化到相应的类型,这样就能解决问题了。如果去每个系统调用中一一这么做,这是一般程序员选择的做法,但写内核的大牛们不仅要完成功能,而且完成得有艺术!这就出现了现在的做法,定义了下面的宏:

#define __SYSCALL_DEFINEx(x, name, ...) \ asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__)); \ static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__)); \ asmlinkage long SyS##name(__SC_LONG##x(__VA_ARGS__)) \ { \ __SC_TEST##x(__VA_ARGS__); \ return (long) SYSC##name(__SC_CAST##x(__VA_ARGS__)); \ } \ SYSCALL_ALIAS(sys##name, SyS##name); \ static inline long SYSC##name(__SC_DECL##x(__VA_ARGS__))

那么仍然是以open系统调用为例,

1 SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) 2 { 3 4 } 5 6 展开之后如下: 7 asmlinkage long sys_open(const char __user * filename, int flags, umode_t mode); 8 static inline long SYSC_open(const char __user * filename, int flags, umode_t mode); 9 asmlinkage long SyS_open((long)filename, (long)flags, (long)mode) 10 { 11 __SC_TEST3(const, char __user * filename, int, flags, umode_t, mode); 12 return (long)SYSC_open(const char __user * filename, int flags, umode_t mode); 13 } 14 SYSCALL_ALIAS(sys_open, SyS_open); 15 static inline long SYSC_open(const char __user * filename, int flags, umode_t mode) 16 { 17 18 }

由于某些体系结构是不存在此类问题的,如x86_64等,Linux内核定义了一个配置选项CONFIG_HAVE_SYSCALL_WRAPPERS,一开始介绍的扩展的宏定义是在没有配置该选项扩展的结果,如果是S390、PowerPC、Sparc 64等平台就需要配置该选项。

说明

本文前面部分主要介绍一些表面的东西,比较简单,对于后面部分是我思考的部分,在去年读内核时我就有这个疑问——为什么内核要这样把简单的系统调用定义成这么复杂的宏?这两天通过查一下资料终于找到能解释这个问题的理由,原因在于漏洞 CVE-2009-0029 导致系统调用函数定义不能直接用那个原型,内核大牛们就写出了现在这样的代码。但需要说明的是,对于 CVE-2009-0029 产生的原因、解决方法,由于涉及到我并不熟悉的体系结构平台,所以上面只是我根据网上仅有很少的资料进行推断出来的,肯定不是很准确,希望大家能指正!

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/096dd6b3a6b419aee085612bdeb6b877.html