记一个linux内核内存提权问题

本文详细解析了Linux内核中的一个内存提权漏洞。通过巧妙利用suid程序和/proc/$pid/mem文件,攻击者可以修改suid程序的可执行代码,进而为普通用户开启带root权限的shell。文章深入探讨了suid机制、/proc/$pid/mem文件的特性及其权限检查机制,并提供了一种绕过检查的方法。

前些天,linux内核曝出了一个内存提权漏洞。通过骇客的精心构造,suid程序将print的输出信息写到了自己的/proc/$pid/mem文件里面,从而修改了自己的可执行代码,为普通用户开启了一个带root权限的shell。这个过程还是挺有意思的,不得不佩服骇客们的聪明才智,故在此分享一下,以表崇敬之情。

 

首先,破解过程使用到了suid程序。suid并不是一个程序,而是可执行文件的一种属性。当你执行一个带有suid属性的程序时,在执行suid程序期间,你启动的进程的user将被临时改为suid程序的owner,进程将拥有程序owner所拥有的权限。这一特性经常用于让普通用户临时获得root权限。
在linux系统中,很多功能是需要root权限才能使用的。用户如果想要用到这些功能,可以有两个办法:一是使用root用户登录。但是很可能你没有root密码。就算有,这样做也不安全(误操作是致命的);二是执行一个owner是root的suid程序。这样就可以在不使用root用户登录的情况下,被允许使用一些需要root权限的功能,既方便又安全。
比如我们经常用到的ping命令,它就是这样的一个suid程序。
$ ll /bin/ping
-rwsr-xr-x 1 root root 37312 Aug  6  2008 /bin/ping
(注意s属性,这就是suid标志。)
ping使用了ICMP协议,通过发送ICMP报文来探测网络的连通性。但是因为ICMP是IP层协议,其行为代表的是整个机器(而不是像传输层协议那样,代表机器上的某个应用),故只有root用户才能建立ICMP报文。而我们之所以不使用root用户登录也可以执行ping命令,其原因正是suid。

那么,怎么保证suid程序提供的root权限不被滥用呢?换句话说,通过suid得到root权限跟使用root用户登录有什么不同呢?一般来说,suid程序都是封闭的,只干某件事情(并且干的事情都确定不会有危害),干完就退出(不会节外生枝)。再以ping为例,普通用户可以执行ping命令来发送ICMP报文。但是ping命令只会发送网络探测相关的ICMP报文,普通用户无法利用它来建立任意的ICMP报文,更不可能利用它来完成其他需要root权限才能做的事情,比如删除用户。

能不能通过修改suid程序,使其去做一些越权的事情呢?比如修改ping程序,使其能够删除用户?这也是不可以的。suid程序跟其他文件一样,受访问权限的保护,一般只有其owner才能有权限修改它,其他用户只能读或者执行。
不过,在这次的破解中,骇客却真的修改了suid程序。怎么办到的呢?利用/proc/$pid/mem。

 

proc文件/proc/$pid/mem是$pid进程的一份内存镜像,能够通过它来读写到进程的所有内存,包括可执行代码(它们已经映射到内存中)。在2.6.39版本以前,这份内存镜像是不可写的,不过后来这个限制被取消了。当然,对/proc/$pid/mem文件的操作也并不是任意的,如果是$pid进程自己写自己的/proc/$pid/mem文件,那么可以允许;如果是调试进程写被调试的进程,也允许。其他情况就不行了。
而骇客的想法是:当我们执行suid程序的时候,它不是会有些输出么?对于有些程序,它输出的内容正好会包含你传递给它的参数。于是,如果我们将suid程序的stderr(或stdout)重定向到/proc/$pid/mem,它在输出信息的时候不就会将你输入的信息改写到自己的内存里去了么!

比如骇客利用的su命令:
$ ll /bin/su
-rwsr-xr-x 1 root root 28336 Oct 31  2008 /bin/su
$ su hahahaha
su: user hahahaha does not exist

输入参数"hahahaha"是一个不存在的用户,su命令会通过stderr输出错误信息,并且信息里面就包含我们的输入参数"hahahaha"。如果输入参数是一段二进制代码,那么它同样也会出现在输出信息中!
然后,跟其他可执行程序一样,su的输出是可以重定向的,比如:
$ su hahahaha 2> ttt
$ cat ttt
su: user hahahaha does not exist

那么,如果将输出重定向到执行su的进程自己的/proc/$pid/mem呢,不就可以达到修改可执行代码的目的了么!
比如这样:
$ su hahahaha 2> /proc/self/mem


不过现在还有两个问题要解决……
第一个还是权限问题。现在已经让执行su的进程自己修改自己的/proc/$pid/mem,不过还不够。再来看看具体还有哪些权限检查。

1、open操作:
static int mem_open(struct inode* inode, struct file* file)
{
 file->private_data = (void*)((long)current->self_exec_id);
 ......
}
没有权限检查,但是会将current->self_exec_id记录下来,后面会对其做校验。

2、write操作:
static ssize_t mem_write(struct file * file, const char __user *buf,size_t count, loff_t *ppos)
{
 ......
 struct task_struct *task = get_proc_task(file->f_path.dentry->d_inode);
 ......
 mm = check_mem_permission(task);
 copied = PTR_ERR(mm);
 if (IS_ERR(mm))
  goto out_free;
 ......
 if (file->private_data != (void *)((long)current->self_exec_id))
  goto out_mm;
 ......
}
有两处检查,一是通过check_mem_permission()检查当前进程是否可以操作该文件,这就是前面所提到的,只会允许本进程或者调试进程的操作。现在这一关已经过了。
另一处检查是对self_exec_id的检查,要求进程在对/proc/$pid/mem进行open()和write()的时候拥有相同的self_exec_id(注意,前面在open的时候已经把当时的self_exec_id记录在了file->private_data中)。另一方面,每当一个进程调用exec()来执行程序时,进程的self_exec_id会自增:
void setup_new_exec(struct linux_binprm * bprm)
{
 ......
 current->self_exec_id++;
 ......
}

而我们之前的那句shell命令(su hahahaha 2> /proc/self/mem)大致是这样实现的:
int fd = open("/proc/self/mem", O_WRONLY);
dup2(fd, 2);
close(fd);
execve("/bin/su", {"su", "hahahaha"}, {...});

注意,虽然suid程序的错误输出被重定向到了/proc/self/mem,但是由于open()和write()分别发生于execve()的之前和之后,两次的self_exec_id是不同的,所以write()操作无法通过权限检查……内核正是利用self_exec_id来确保/proc/$pid/mem是程序自己打开的,而不是在程序执行之前就被打开的。
不过这里的检查不够健壮,还是有办法突破的。一个办法是一个劲地exec(),直到self_exec_id溢出。不过这样搞就是耗时太长。
另外一个办法是:
1、fork()一下,生成的子进程会拥有跟父进程相同的self_exec_id;
2、在子进程中exec()一下,执行一个自己写的程序,并在程序中open()父进程的/proc/$pid/mem(注意open的时候没有权限检查,所以能够open成功);
3、通过诸如unix socket的方法,将子进程中打开的fd传回给父进程(没想到unix socket还有这么一招吧~ man一下cmsg,看看关于SCM_RIGHTS的内容);
4、由于子进程是exec()之后再open()的,记录在file中的self_exec_id会自增一次。所以父进程exec()执行suid程序之后,write()时的self_exec_id刚好就跟open()时一样了;

 

OK!之前提到的两个问题,第一个权限问题已经解决了,现在我们已经能让suid程序在自己的内存空间中写一些我们想要的可执行代码。第二个问题,这些可执行代码该写到什么地方去?随便乱写显然是没有意义的。

首先,我们能控制suid程序写文件的位置吗?
可以!比如这样:
int fd = open("/proc/self/mem", O_WRONLY);
dup2(fd, 2);
lseek64(fd, pos_what_we_want, SEEK_SET);
close(fd);
execve("/bin/su", {"su", "hahahaha"}, {...});
然后su就会顺着我们lseek64()设置的位置开始写。

其次,应该选择哪个位置呢?有两个条件:
1、在我们期望的write()之后的必经之路上;
2、程序流程是跳转到这个位置来的,而不是顺序执行下来的。因为像su的输出那样("su: user hahahaha does not exist"),在我们输入的内容前面会有一些其他的信息("su: user "),这些信息肯定会把可执行代码写坏的,唯一的办法就是让程序流程不要执行到它们,而是直接跳转到我们的输入上;
比如骇客选择了exit()函数的入口,就能满足以上两个条件:程序最后都会调用libc库函数exit()来退出、而作为函数的入口点,程序流程是通过call指令跳转过来的。而lseek64()所需要指定的位置,就是exit()入口点减去strlen("su: user ")的位置。

再次,怎么找到exit()的入口点呢?
最简单的办法就是objdump,如:
$ objdump -d /bin/su | awk '$2 == "<exit@plt>:"{print}'
0000000000001c90 <exit@plt>:
还有一点就是要求suid程序被载入内存的时候位置是不能随机的,否则objdump看到的exit()入口点就不是运行时真正的入口点(并且每次运行的入口点都还可能不一样)。
通过"readelf -h $bin"查看输出的"Type"字段可以知道可执行文件$bin是否是按位置无关来编译的(DYN表示位置无关、EXEC则相反)。如果不是位置无关,那么objdump看到的地址就是运行时的地址。骇客使用了su程序来进行破解,正是因为在他的系统上,/bin/su的Type是EXEC。

在别的系统上这个未必成立,比如我的系统:
$ readelf -h /bin/su | grep Type          
  Type:                              DYN (Shared object file)

在我的系统中,可以选用umount来进行破解,也是同样的道理。
$ ll /bin/umount
-rwsr-xr-x 1 root root 40208 Nov 26  2008 /bin/umount
$ umount hahahaha
umount: hahahaha is not mounted (according to mtab)
$ readelf -h /bin/umount | grep Type    
  Type:                              EXEC (Executable file)

最后,就是要在exit()的入口点写什么内容的问题了。很简单,写一段代码,使用execve()系统调用运行一个shell就行了。suid程序已经带来了root权限,以后想干什么都交给这个shell吧~
不过注意,这里要写的不是C代码、不是汇编代码、而是二进制的机器代码。

 

骇客的原文见:http://blog.zx2c4.com/749,里面包含了破解代码的链接。还有,linus的补丁也已经出来了,在骇客的那篇文章中也能找到链接。如果本文所讨论的问题你都已经理解了,骇客的blog原文对于你来说也就不会有难度。

<think>嗯,用户想了解Linux如何防止攻击者通过内核漏洞。这个问题挺重要的,毕竟内核漏洞一旦被利用,攻击者可能获得更高的限,危害系统安全。首先,我需要回忆一下Linux的安全机制和相关防护措施。 首先想到的是限分离,比如使用最小限原则,确保应用程序以普通用户限运行,而不是root。这样可以限制漏洞的影响范围。然后,内核的漏洞修复,及时更新系统补丁,这应该是基础但关键的步骤。 接下来,内核的保护机制。比如地址空间布局随机化(ASLR),这个技术可以随机化内存地址,增加攻击者利用漏洞的难度。不过ASLR主要是用户空间的保护,内核自身可能有其他机制,比如KASLR(Kernel ASLR),用来随机化内核代码的位置。 还有内核模块的签名机制,确保只有经过验证的模块才能加载,这样可以防止恶意模块的插入。SELinux或AppArmor这样的强制访问控制(MAC)系统,可以限制进程的限,即使有漏洞,攻击者也可能被MAC策略阻止。 另外,用户命名空间(User Namespaces)允许非特用户创建命名空间,但这也可能被滥用,所以需要限制其使用。比如,sysctl中的user.max_user_namespaces可以控制用户命名空间的数量,防止滥用。 grsecurity和PaX这些第三方补丁供了额外的安全增强,比如加强的内存保护和漏洞利用防御,但可能不在主流内核中,需要自行编译或使用特定发行版。 cgroups(控制组)可以用来限制资源,虽然主要功能是资源管理,但结合其他机制也能增强安全性。Linux内核的漏洞利用缓解措施,比如STACKPROTECTOR(栈保护)、PTE标禁止执行(NX位),这些都能防止某些类型的攻击,如缓冲区溢出。 seccomp是一个强大的工具,可以限制进程能够执行的系统调用,减少攻击面。比如,容器运行时如Docker就使用seccomp来限制容器内的系统调用。 审计和监控工具,如auditd,可以录系统调用和异常行为,帮助及时发现入侵尝试。定期检查日志也很重要。 硬件层面的安全功能,如Intel的SMAP/SMEP,防止内核执行用户空间的内存,或者访问用户空间的数据,这些都需要内核支持并启用。 另外,容器化和沙箱技术,比如Docker、Kubernetes,通过隔离应用运行环境,限制漏洞的影响范围。虽然容器不是完全隔离,但结合其他安全措施可以高防护能力。 可能还需要到社区和发行版的安全响应,及时修补已知漏洞。比如,使用长期支持(LTS)版本,或者关注CVE公告,应用补丁。 不过,用户可能更想知道具体的配置方法和实践步骤,而不仅仅是概念。比如,如何启用SELinux,如何配置seccomp,如何更新内核,如何设置sysctl参数限制用户命名空间等。需要确保回答中涵盖这些实际操作,同时说明原理。 还要注意,有些措施可能需要衡性能和易用性,比如启用某些安全模块可能会增加系统开销,或者导致兼容性问题。因此,根据实际场景选择合适的防护措施很重要。 总结下来,防护措施可以分为几个层面:内核自身的安全机制(如KASLR、模块签名)、访问控制(SELinux、AppArmor)、漏洞利用缓解技术(NX、栈保护)、限限制(非root运行、seccomp)、隔离技术(命名空间、cgroups)、监控审计,以及及时的补丁管理。需要综合这些层面来构建纵深防御,减少攻击面,即使某个层被突破,其他层仍能供保护。</think>Linux系统通过多层防御机制来防止攻击者利用内核漏洞进行。以下是关键防护策略及实施方法: --- ### **1. 最小限原则** - **非Root运行** 应用程序应始终以**普通用户**身份运行,避免使用`root`限。可通过以下方式实现: ```bash chmod u-s /path/to/app # 移除SUID限 sudo -u nobody /path/to/app # 指定低限用户运行 ``` - **Capabilities分割** 使用`capabilities`机制赋予进程最小特(而非直接授予root): ```bash setcap cap_net_bind_service=+ep /path/to/app # 仅允许绑定低端口 ``` --- ### **2. 内核漏洞缓解技术** - **地址空间随机化** - 用户空间ASLR:通过`sysctl -w kernel.randomize_va_space=2`启用 - 内核KASLR:在内核启动参数添加`kaslr`(需硬件支持) - **内存保护** - **NX/DEP**:阻止数据段代码执行(默认启用) - **SMAP/SMEP**:防止内核访问用户空间内存(需CPU支持,启用参数`smep smap`) - **堆栈保护** 编译时启用`-fstack-protector-strong`,防范缓冲区溢出攻击。 --- ### **3. 强制访问控制(MAC)** - **SELinux** 1. 检查状态:`sestatus` 2. 配置策略:为应用程序创建最小限策略模板: ```bash audit2allow -a -M myapp_policy semodule -i myapp_policy.pp ``` - **AppArmor** 1. 创建配置文件:`aa-genprof /path/to/app` 2. 限制文件访问与系统调用 --- ### **4. 内核模块安全** - **模块签名验证** 启用内核配置选项`CONFIG_MODULE_SIG`,仅加载已签名模块: ```bash echo 1 > /proc/sys/kernel/modules_disabled # 禁止动态加载模块(极端场景) ``` --- ### **5. 系统调用过滤(seccomp)** - **限制非必要系统调用** 通过`seccomp`配置文件(如Docker默认配置文件)限制应用程序行为: ```json { "defaultAction": "SCMP_ACT_ERRNO", "syscalls": [{"names": ["read", "write"], "action": "SCMP_ACT_ALLOW"}] } ``` --- ### **6. 命名空间隔离** - **User Namespaces限制** 防止非特用户滥用命名空间: ```bash sysctl -w kernel.unprivileged_userns_clone=0 # 禁止非特用户创建命名空间 sysctl -w user.max_user_namespaces=0 # 完全禁用用户命名空间 ``` --- ### **7. 漏洞修复与监控** - **及时更新内核** 使用`uname -r`查看版本,通过包管理器更新: ```bash apt update && apt upgrade linux-image-$(uname -r) # Debian/Ubuntu yum update kernel # RHEL/CentOS ``` - **审计日志分析** 启用`auditd`监控敏感操作: ```bash auditctl -a always,exit -S all -F pid=1234 # 跟踪特定进程的系统调用 ``` --- ### **8. 硬件辅助安全** - **启用Intel TXT/AMD SVM** 在BIOS中开启硬件虚拟化支持,结合`dm-verity`实现启动完整性校验。 --- ### **总结:纵深防御架构** 通过 **限最小化 + 漏洞利用缓解 + 强制访问控制 + 运行时隔离 + 持续监控** 的多层防护,可显著降低内核漏洞被利用的风险。实际部署需结合业务场景平衡安全性与性能。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值