首页
社区
课程
招聘
[原创]kctf-2023 第九题 突破防线 98k战队wp
2023-9-22 01:33 10423

[原创]kctf-2023 第九题 突破防线 98k战队wp

2023-9-22 01:33
10423

引言

后面两段主要是逆向和内核两位大手子写的

本题叠buf式的考点让几位师傅隔着各种远程投屏互相帮忙调了几个通宵,无数次都在祈求出题人收了神通,不过最后还是做出来了。没打过内核,当时并不觉得把静态c程序exp改成shellcode可以很有可行性,随口提了一句,结果几位大爹说还真可以,属于歪打正着了,最后和IChild大爷被几个socket调用调戏了一通宵到精神恍惚。不知道是否有能在禁止execve/at的情况下更为精妙的可行思路。

大体流程:

1.利用用户态漏洞进行 rop 和布置 shellcode-seg1
2. rop 利用 mprotect 调用成功运行小段 shellcode-seg1
3.在 shellcode-seg1 中用 socket 操作读入大段 shellcode-seg2 (这段直接来自于打内核的 exp binary),并完成场景还原和跳转
4.跳转运行 shellcode-seg2 完成内核漏洞的利用(精神污染),篡改 busybox 的 poweroff 逻辑
5.杀 vm 进程,在防止内核崩掉(需要一点处理)的情况下让 init 高权限顺序执行 poweroff ,利用 8080 端口反弹信息。

程序分析

用户态程序 vm 是 go 语言写的,在 插件 的帮助下顺利恢复函数名,再手动恢复下自定义结构体的信息就能得到程序逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
void __cdecl main_main()
{
  __int64 v0; // x28
  _QWORD *http_server; // x0
  __int64 v2; // x0
  __int64 v3; // x1
  __int64 v4; // x0
  void *server_handle; // [xsp+38h] [xbp-40h]
  __int64 v6[2]; // [xsp+40h] [xbp-38h] BYREF
  _QWORD v7[4]; // [xsp+50h] [xbp-28h] BYREF
  __int64 v8; // [xsp+78h] [xbp+0h] BYREF
 
  if ( (unsigned __int64)&v8 <= *(_QWORD *)(v0 + 16) )
    runtime_morestack_noctxt();
  main_initFs();
  v6[0] = (__int64)&type_string;
  v6[1] = (__int64)&off_2D7E60;
  fmt_Fprintln(&off_2D8F98, qword_45C3E8, v6, 1LL, 1LL);// "Server started ..."
  server_handle = runtime_newobject(&type_http_ServeMux);
  net_http___ServeMux__Handle(server_handle, aApiCreateFile, 16LL, &off_2D9438, off_293378);// 0x1F9A90, main.createFile
  net_http___ServeMux__Handle(server_handle, aApiWriteFile, 15LL, &off_2D9438, &off_293388);// 0x1F9E70, main.writeFile
  net_http___ServeMux__Handle(server_handle, aApiRunFile, 13LL, &off_2D9438, off_293380);// 0x1FA260, main.runFile
  http_server = runtime_newobject(&type_http_Server);
  http_server[1] = 5LL;
  *http_server = a8080;
  http_server[2] = &off_2D8E18;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(http_server + 3, server_handle);
  else
    http_server[3] = server_handle;
  v2 = net_http___Server__ListenAndServe(http_server);
  if ( v2 )
  {
    v7[2] = 0LL;
    v7[3] = 0LL;
    v7[0] = &type_string;
    v7[1] = &off_2D7E80;
    v4 = *(_QWORD *)(v2 + 8);
    v7[2] = v4;
    v7[3] = v3;
    fmt_Fprintln(&off_2D8F98, qword_45C3E8, v7, 2LL, 2LL);// "Error starting the server:"
  }
}
 
void __fastcall main_initFs()
{
  __int64 v0; // x28
  __int64 v1[2]; // [xsp+30h] [xbp-58h] BYREF
  __int64 v2[9]; // [xsp+40h] [xbp-48h] BYREF
  __int64 v3; // [xsp+88h] [xbp+0h] BYREF
 
  if ( (unsigned __int64)&v3 <= *(_QWORD *)(v0 + 16) )
    runtime_morestack_noctxt();
  v2[0] = 0x400000020LL;
  v2[1] = 0xC00000B704000015LL;
  v2[2] = 32LL;
  v2[3] = 0x11900020015LL;
  v2[4] = 0xDD00010015LL;
  v2[5] = 0x7FFF000000000006LL;
  v2[6] = 6LL;
  v1[0] = 7LL;
  v1[1] = (__int64)v2;
  syscall_Syscall(SYS_prctl, PR_SET_NO_NEW_PRIVS, 1LL);
  v2[7] = (__int64)v1;
  syscall_Syscall(SYS_prctl, PR_SET_SECCOMP, 2LL, v1);
}
 
void __fastcall main_createFile(ResponseWriter_table *Response_vtable, void *response, void *request)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  if ( (unsigned __int64)&v31 <= *(_QWORD *)(v3 + 16) )
    runtime_morestack_noctxt();
  v30 = Response_vtable->net_http_(_response)_Header(response);
  *(_OWORD *)v4 = net_textproto_CanonicalMIMEHeaderKey(aContentType, 12LL);
  v35 = v4[0];
  value = runtime_newobject(&type_string_array_1_ptr);
  *((_QWORD *)value + 1) = 16LL;
  *(_QWORD *)value = aApplicationJso;
  v6 = (void **)runtime_mapassign_faststr(&type_textproto_MIMEHeader, v30, v35, v4[1]);
  v6[1] = (void *)1;
  v6[2] = (void *)1;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v6, value);
  else
    *v6 = value;
  v29 = Response_vtable->net_http_(_response)_Header(response);
  *(_OWORD *)v7 = net_textproto_CanonicalMIMEHeaderKey(aAccessControlA_3, 27LL);
  v35 = v7[0];
  v8 = runtime_newobject(&type_string_array_1_ptr);
  v33 = v8;
  v8[1] = 1LL;
  *v8 = "*";
  v9 = (void **)runtime_mapassign_faststr(&type_textproto_MIMEHeader, v29, v35, v7[1]);
  v9[1] = (void *)1;
  v9[2] = (void *)1;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v9, v33);
  else
    *v9 = v33;
  v37 = (main::Req *)runtime_newobject(&type_main_Req);
  v28 = *((_QWORD *)request + 9);
  v32 = runtime_convI2I(&type_io_Reader, *((_QWORD *)request + 8));
  v10 = runtime_newobject(&type_json_Decoder);
  *v10 = v32;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v10 + 1, v28);
  else
    v10[1] = v28;
  v11 = encoding_json___Decoder__Decode(v10, &type_main_Req_ptr, v37);
  if ( v11 )
  {
    v24 = (*(__int64 (__fastcall **)(__int64))(v11 + 24))(v12);
    net_http_Error(Response_vtable, response, v24, v25, 400LL);
  }
  else if ( v37->Size > 0x8000 || (*(_OWORD *)v14 = os_OpenFile(aDevVmfs, 9LL, 0LL, 0x180LL), v13 = v14[0], v14[1]) )
  {
    net_http_Error(Response_vtable, response, aInternalServer, 21LL, 500LL);
  }
  else
  {
    v31 = (_QWORD *)v14[0];
    Id = v37->Id;
    Size = v37->Size;
    v27[1] = 0LL;
    v27[3] = 0LL;
    v27[4] = 0LL;
    v27[0] = Id;
    v27[2] = Size;
    v36 = v27;
    if ( v14[0] )
    {
      if ( (*(_BYTE *)(*(_QWORD *)v14[0] + 80LL) & 1) != 0 )
      {
        internal_poll___FD__SetBlocking(*(_QWORD *)v14[0]);
        v13 = (__int64)v31;
      }
      v17 = *(_QWORD *)(*(_QWORD *)v13 + 16LL);
    }
    else
    {
      v17 = -1LL;
    }
    syscall_Syscall(SYS_ioctl, v17, 0xFF00LL, v36);
    if ( v31 )
    {
      v26 = v18;
      os___file__close(*v31);
      v18 = v26;
    }
    if ( v18
      || (v38 = aSuccess,
          v39 = 7LL,
          v19 = runtime_convTstring(aSuccess),
          v20 = encoding_json_Marshal(&type_main_Message, v19),
          v23) )
    {
      net_http_Error(Response_vtable, response, aInternalServer, 21LL, 500LL);
    }
    else
    {
      Response_vtable->net_http_(_response)_Write(response, v20, v21, v22);
    }
  }
}
 
void __fastcall main_writeFile(ResponseWriter_table *Response_vtable, void *response, void *request)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  if ( (unsigned __int64)&v33 <= *(_QWORD *)(v3 + 16) )
    runtime_morestack_noctxt();
  v32 = Response_vtable->net_http_(_response)_Header(response);
  *(_OWORD *)v4 = net_textproto_CanonicalMIMEHeaderKey(aContentType, 12LL);
  v37 = v4[0];
  v5 = runtime_newobject(&type_string_array_1_ptr);
  value = (__int64)v5;
  v5[1] = 16LL;
  *v5 = aApplicationJso;
  v6 = (__int64 *)runtime_mapassign_faststr(&type_textproto_MIMEHeader, v32, v37, v4[1]);
  v6[1] = 1LL;
  v6[2] = 1LL;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v6, value);
  else
    *v6 = value;
  v31 = Response_vtable->net_http_(_response)_Header(response);
  *(_OWORD *)v7 = net_textproto_CanonicalMIMEHeaderKey(aAccessControlA_3, 27LL);
  v37 = v7[0];
  v8 = runtime_newobject(&type_string_array_1_ptr);
  v35 = (__int64)v8;
  v8[1] = 1LL;
  *v8 = "*";
  v9 = (__int64 *)runtime_mapassign_faststr(&type_textproto_MIMEHeader, v31, v37, v7[1]);
  v9[1] = 1LL;
  v9[2] = 1LL;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v9, v35);
  else
    *v9 = v35;
  v39 = (main::Req *)runtime_newobject(&type_main_Req);
  v30 = *((_QWORD *)request + 9);
  v34 = runtime_convI2I(&type_io_Reader, *((_QWORD *)request + 8));
  v10 = runtime_newobject(&type_json_Decoder);
  *v10 = v34;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v10 + 1, v30);
  else
    v10[1] = v30;
  v11 = encoding_json___Decoder__Decode(v10, &type_main_Req_ptr, v39);
  if ( v11 )
  {
    v26 = (*(__int64 (__fastcall **)(__int64))(v11 + 24))(v12);
    net_http_Error(Response_vtable, response, v26, v27, 400LL);
  }
  else if ( v39->Size > 8 * v39->Data.len
         || (*(_OWORD *)v14 = os_OpenFile(aDevVmfs, 9LL, 0LL, 0600LL), v13 = v14[0], v14[1]) )
  {
    net_http_Error(Response_vtable, response, aInternalServer, 21LL, 500LL);
  }
  else
  {
    v33 = (_QWORD *)v14[0];
    v15 = v39->Id;
    v16 = v39->Data.data;
    v17 = v39->Size;
    v18 = v39->ChooseIndex;
    v29[4] = 0LL;
    v29[0] = v15;
    v29[1] = (__int64)v16;
    v29[2] = v17;
    v29[3] = v18;
    v38 = v29;
    if ( v14[0] )
    {
      if ( (*(_BYTE *)(*(_QWORD *)v14[0] + 80LL) & 1) != 0 )
      {
        internal_poll___FD__SetBlocking(*(_QWORD *)v14[0]);
        v13 = (__int64)v33;
      }
      v19 = *(_QWORD *)(*(_QWORD *)v13 + 16LL);
    }
    else
    {
      v19 = -1LL;
    }
    syscall_Syscall(SYS_ioctl, v19, 0xFF03LL, v38);
    if ( v33 )
    {
      v28 = v20;
      os___file__close(*v33);
      v20 = v28;
    }
    if ( v20
      || (v40 = aSuccess,
          v41 = 7LL,
          v21 = runtime_convTstring(aSuccess),
          v22 = encoding_json_Marshal(&type_main_Message, v21),
          v25) )
    {
      net_http_Error(Response_vtable, response, aInternalServer, 21LL, 500LL);
    }
    else
    {
      Response_vtable->net_http_(_response)_Write(response, v22, v23, v24);
    }
  }
}
 
void __fastcall main_runFile(ResponseWriter_table *Response_vtable, void *response, void *request)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  if ( (unsigned __int64)&v52 < 0x18170 || (unsigned __int64)&v34[15] <= *(_QWORD *)(v3 + 16) )
  {
    v53 = Response_vtable;
    v54 = response;
    v55 = request;
    runtime_morestack_noctxt();
  }
  v55 = request;
  v53 = Response_vtable;
  v54 = response;
  v41 = Response_vtable->net_http_(_response)_Header(response);
  *(_OWORD *)v4 = net_textproto_CanonicalMIMEHeaderKey(aContentType, 12LL);
  v36 = v4[1];
  v46 = v4[0];
  v5 = runtime_newobject(&type_string_array_1_ptr);
  v45 = v5;
  v5[1] = 16LL;
  *v5 = aApplicationJso;
  v6 = (_QWORD *)runtime_mapassign_faststr(&type_textproto_MIMEHeader, v41, v46, v36);
  v6[1] = 1LL;
  v6[2] = 1LL;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v6, v45);
  else
    *v6 = v45;
  v40 = v53->net_http_(_response)_Header(v54);
  *(_OWORD *)v7 = net_textproto_CanonicalMIMEHeaderKey(aAccessControlA_3, 27LL);
  v36 = v7[1];
  v46 = v7[0];
  v8 = runtime_newobject(&type_string_array_1_ptr);
  v44 = v8;
  v8[1] = 1LL;
  *v8 = "*";
  v9 = (_QWORD *)runtime_mapassign_faststr(&type_textproto_MIMEHeader, v40, v46, v36);
  v9[1] = 1LL;
  v9[2] = 1LL;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v9, v44);
  else
    *v9 = v44;
  v48 = (main::Req *)runtime_newobject(&type_main_Req);
  v39 = v55[9];
  v43 = runtime_convI2I(&type_io_Reader, v55[8]);
  v10 = runtime_newobject(&type_json_Decoder);
  *v10 = v43;
  if ( dword_4927C0 )
    runtime_gcWriteBarrier(v10 + 1, v39);
  else
    v10[1] = v39;
  v11 = encoding_json___Decoder__Decode(v10, &type_main_Req_ptr, v48);
  if ( v11 )
  {
    v32 = (*(__int64 (__fastcall **)(__int64))(v11 + 24))(v12);
    net_http_Error(v53, v54, v32, v33, 400LL);
  }
  else
  {
    *(_OWORD *)v14 = os_OpenFile(aDevVmfs, 9LL, 0LL, 0600LL);
    v13 = v14[0];
    if ( v14[1] )
      goto LABEL_31;
    v42 = (_QWORD *)v14[0];
    v15 = regs_and_code;
    do
    {
      *v15 = 0LL;
      v15[1] = 0LL;
      v15 += 2;
    }
    while ( (__int64)v15 <= (__int64)&v51.len );
    v16 = memory;
    do
    {
      *v16 = 0LL;
      v16[1] = 0LL;
      v16 += 2;
    }
    while ( (__int64)v16 <= (__int64)&memory[4094] );
    v51.data = memory;
    v51.len = 4096LL;
    v51.cap = 4096LL;
    v17 = v48->Id;
    v18 = v48->Size;
    v19 = v48->ChooseIndex;
    v37[4] = 0LL;
    v37[0] = v17;
    v37[1] = &regs_and_code[17];
    v37[2] = v18;
    v37[3] = v19;
    v47 = v37;
    if ( v14[0] )
    {
      if ( (*(_BYTE *)(*(_QWORD *)v14[0] + 80LL) & 1) != 0 )
      {
        internal_poll___FD__SetBlocking(*(_QWORD *)v14[0]);
        v13 = (__int64)v42;
      }
      v20 = *(_QWORD *)(*(_QWORD *)v13 + 16LL);
    }
    else
    {
      v20 = -1LL;
    }
    syscall_Syscall(SYS_ioctl, v20, 0xFF04LL, v47);
    if ( v42 )
    {
      v35 = v21;
      os___file__close(*v42);
      v21 = v35;
    }
    if ( v21 )
    {
LABEL_31:
      net_http_Error(v53, v54, aInternalServer, 21LL, 500LL);
    }
    else
    {
      v22 = v34;
      v23 = regs_and_code;
      do
      {
        v24 = *v23;
        v25 = v23[1];
        v23 += 2;
        *v22 = v24;
        v22[1] = v25;
        v22 += 2;
      }
      while ( (__int64)v23 <= (__int64)&v51.len );
      v26 = main_runVM();
      v49[0] = aSuccess;
      v49[1] = 7LL;
      v49[2] = v26;
      v27 = runtime_convT(&type_main_VmMessage, v49);
      v28 = encoding_json_Marshal(&type_main_VmMessage, v27);
      if ( v31 )
        net_http_Error(v53, v54, aInternalServer, 21LL, 500LL);
      else
        v53->net_http_(_response)_Write(v54, v28, v29, v30);
    }
  }
}

开了 sandbox , dump 出来发现是禁用了 execve 和 execveat 。开了个 http server ,有三个接口,每个接口的输入格式都是 main.Req 的 json 形式。提取出来逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
api post interface: {"Id": uint64, "Size": uint64, "ChooseIndex": uint64, "Data": uint64[]}
// {"Id": 0, "Size": 1, "ChooseIndex": 0, "Data": [1, 2]}
 
 
url: ?:8080/api/create-file
    assert(Size <= 0x8000);
    gofile_obj = os.OpenFile("/dev/vmfs", 0, 0o600);
    sys_ioctl(gofile_obj->fd, 0xFF00, &(struct_to_kernel) {Id, 0, Size, 0, 0});
    os.(_file).close(*gofile_obj);
 
url: ?:8080/api/write-file:
    assert(Size <= 8 * Data.length);
    gofile_obj = os.OpenFile("/dev/vmfs", 0, 0o600);
    sys_ioctl(gofile_obj->fd, 0xFF03, &(struct_to_kernel) {Id, Data.ptr, Size, ChooseIndex, 0});
    os.(_file).close(*gofile_obj);
 
url: ?:8080/api/run-file:
    gofile_obj = os.OpenFile("/dev/vmfs", 0, 0o600);
    sys_ioctl(gofile_obj->fd, 0xFF04, &(struct_to_kernel) {Id, output_data, Size, ChooseIndex, 0});
    os.(_file).close(*gofile_obj);
    retval = main.runVM(output_data); // uint64[]
    Write({"Text": "success", "Return": retval});

ioctl 调用说明这里要跟内核交互了。逆内核模块 vmfs ,得到几项功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct struct_to_kernel {
    uint64 Id;
    uint64* Data;
    uint64 Size;
    uint64 ChooseIndex;
    uint64 method;
};
 
ff00: create(Id, Size)
ff01: mov_to_trash(Id, ChooseIndex)
ff02: delete_from_trash(Id, ChooseIndex)
ff03: set_data(Id, Size, ChooseIndex, Data)
ff04: get_data(Id, Size, ChooseIndex, Data)
ff05: sort(method) // 3: select sort

create 会对 Size 做检测,不能大于 0x10000

mov_to_trash 会将 list 中已有的项添加到 trash 中,并释放分配的数组内存,但是没有置为空

delete_from_trash 将 trash 中的一项彻底删除,并将 list 中对应项释放掉并且置为空

set_data 和 get_data 就是向分配的数组中写入数据和拿出数据

sort 会对 list 中的项目排序,不同 method 值采用不同的排序算法。其中当 method = 3 时采用选择排序,而选择排序不是稳定的排序算法,即不能保证排序之前值相同的项在排序后保持相同的顺序,再结合 mov_to_trash 就可以 double free 。不过这是内核的利用部分了,用户态程序是没有 ff01 的接口的,需要先找用户态程序的漏洞。

在内核模块初始化时会自动添加一条 list 中的记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
v4 = (object *)kmalloc_trace(MEMORY[0x33E0], 0xCC0LL, 0x18LL);
v5 = v4;
if ( v4 )
{
  v4->addr = 0LL;
  v4->Id = 0x636DB8C2LL;
  v4->Size = 0x10000LL;
  v6 = (uint64 *)kmalloc_large(0x10000LL, 0xCC0LL);
  v5->addr = v6;
  if ( v6 )
  {
    memset(v6, 0, 0x10000uLL);
    list[0] = v5;
  }
}

Size 为 0x10000 ,而用户态程序可创建的最大大小为 0x8000 ,如果只是看用户态程序是没问题的,而添加的这一条记录就给用户态程序带来了漏洞,因为 run-file 会将数据从内核复制到用户态程序中,用户态程序并没有检查 Size 的大小,用来保存数据的栈内存只有 0x8000 字节(即 4096 个 uint64 ),如果从内核里复制出来的长度大于 0x8000 字节就可以覆盖栈上数据,先覆盖的是 vm 执行时的 memory 结构体,覆盖指针、长度就可以任意读写;再之后是上层函数的 lr 指针,再后面是当前函数的参数,因为函数返回之前要调用第一个参数的一个虚表函数,所以需要设置为一个程序里的值。再后面就是随便栈溢出构造 rop 了。 arm 的 rop 不太好做,不过幸好能找到这样一条 gadget ,大概是类似于x86的 setcontext 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:000000000007347C                 LDP             X25, X26, [SP,#0xC8]
.text:0000000000073480                 LDP             X23, X24, [SP,#0xB8]
.text:0000000000073484                 LDP             X21, X22, [SP,#0xA8]
.text:0000000000073488                 LDP             X19, X20, [SP,#0x98]
.text:000000000007348C                 LDP             X16, X17, [SP,#0x88]
.text:0000000000073490                 LDP             X14, X15, [SP,#0x78]
.text:0000000000073494                 LDP             X12, X13, [SP,#0x68]
.text:0000000000073498                 LDP             X10, X11, [SP,#0x58]
.text:000000000007349C                 LDP             X8, X9, [SP,#0x48]
.text:00000000000734A0                 LDP             X6, X7, [SP,#0x38]
.text:00000000000734A4                 LDP             X4, X5, [SP,#0x28]
.text:00000000000734A8                 LDP             X2, X3, [SP,#0x18]
.text:00000000000734AC                 LDP             X0, X1, [SP,#8]
.text:00000000000734B0                 LDR             X30, [SP,#0x1F0]
.text:00000000000734B4                 LDUR            X29, [SP,#-8]
.text:00000000000734B8                 LDR             X27, [SP]
.text:00000000000734BC                 ADD             SP

再结合 go 程序里自带的 syscall 函数,加上前面的任意地址读写,加上程序没开 pie ,可以直接在 bss 段写入 shellcode 之后调用 mprotect 改 bss 段权限,再跳转到 shellcode 执行,在 shellcode 中调用 accept 得到与用户交互的 fd ,这样就可以读取更多的 shellcode 用于执行,这样就可以进入到内核利用了。

接下来就是这题比较恶心的点了。调试时首先循环 read 4 号 fd ,发现不阻塞且无法得到大于 0 的结果。最后关掉 4 ,然后用对 socket fd 3 调用 sys_accept ,调了半天还是 accept 不进来,最后发现原来是 accept 不阻塞。最后循环 accept 和 recvfrom ,才开始稳定读取 shellcode 并利用。

由于禁用掉了 execve 和 execveat ,于是只能把用来打内核的 exp 的 binary 给读出来,作为 shellcode 来执行。 mmap 出代码段,即 exp 中的 0x23300000 和 stack 段,全部内存 dump 进去还原现场,设置完 sp 和 pc ,最后从 start 开始执行

Kernel Module Exploitation

周二事情比较多,加上俺也确实不太会逆向,所以专业的事情让专业的逆向爷爷来做。所以当我真正上手这个题的时候逻辑好像跟我无关了。

在我的视角中,只知道有内核菜单堆的几个功能,其中 0xff01 的 free 之后还留着 Dangling Pointer。并且,这个 Dangling Pointer 还会在选择排序之后被换位... 然后就是... Double Free~

补充一点:选择排序是一种不稳定的排序算法。在这个题中,选择排序会导致相同 id 不同 idx 的两个 object 位置发生交换。当其中一个已经是 Dangling Pointer 的时候,进行选择排序,再尝试 free 另一个 object。因为发生了交换,所以第二次 free 的还是原来的那个 object。这就构成了 Double Free。

Dangling Pointer

POC 如下:

1
2
3
4
5
6
7
PRINT_AND_EXECUTE(dev_alloc(2, 0xc0););
PRINT_AND_EXECUTE(dev_alloc(2, 0xc0););
PRINT_AND_EXECUTE(dev_alloc(1, 0xc0););
PRINT_AND_EXECUTE(dev_alloc(1, 0xc0););
PRINT_AND_EXECUTE(dev_free(2, 1););
PRINT_AND_EXECUTE(dev_select_sort(););
PRINT_AND_EXECUTE(dev_free(2, 0););

于是接下来就进入到常规 Linux kernel exploitation 的环节。Double Free 在内核堆利用中算是比较好利用的一种,而且这个题还可以控制 double free object 的大小,所以思路应该有很多。比如用直接用 tty_struct 泄露+ROP。

不过俺之前被 aarch64 的 ROP 整怕了,而且也不知道新内核里面 gadget 够不够用,思来想去还是别赌了,还是打稳定一点的 cross-cache 吧...

思路呢就和去年 N1CTF 的 Praymoon 差不多。指路牌 -> N1CTF 2022 Praymoon Write Up | V1me's Blog (imv1.me)

这里简单概括一下:

  1. 通过 sock 构造 order 0 的页风水。可以参考 Will's Root: Reviving Exploits Against Cred Structs - Six Byte Cross Cache Overflow to Leakless Data-Oriented Kernel Pwnage (willsroot.io)。当然,嫌麻烦 pipe_buffer 也能构造出来,但是后面的利用会用到 pipe_buffer,所以不想搞混了。
  2. 释放掉 sock 获取的奇数页面,分配 pipe_buffer 以占据。
  3. 释放掉 sock 获取的偶数页面,先用 vm_area_struct 占用一点,再用题目的漏洞结构体占用一点。所以页面布局大概就是下面这样。
  4. 第一次 free vuln_object,用 user_key_payload 再占回来;第二次 free vuln_object,再用 vuln_object 占用回来(题目的 vuln_object 还挺好用,居然可以往里面写东西,感动)。这样就可以修改 user_key_payload 的 datalen 字段以实现溢出读。又因为 vm_area_struct 里面有 list_head 结构,所以很容易泄露出堆地址。同样的也泄露出了 pipe_buffer 们的地址。
  5. 将 double_free 从 vuln_object 转移到 pipe_buffer。这里用的是 poll_list (感谢 CorCTF 两位大爹)。结合我们已经泄露出的堆地址,我们用 poll_list 占据步骤 4 中用完释放的 user_key_payload。此时步骤 4 中分配的 vuln_object 和 poll_list 占用同一块空间,所以利用 edit 功能把 poll_list->next 改到 pipe_buffer 上,就可以构造 pipe_buffer 的 UAF 了。
  6. 利用和 pipe_buffer 同在 kmalloc-cg 的 msg_msg 结构体(两年不见,你还好吗)就可以修改 pipe_buffer 的 flag 了。
  7. 最后就是喜闻乐见的 pipe_primitive 提权了。修改 busybox 的 halt_main 函数,就可以在 init.sh 执行到 poweroff 的时候以 root 身份执行 shellcode。

image-20230922002307088

一切都看起来很简单,俺也在 6 个小时的时间内调通了内核 exp。此时正是凌晨 4 点,用户态那边也基本弄完了,一切都在向好的方向发展...

所以,为什么俺们又从早上搞到了晚上 9 点呢?

联合调试

前情提要,用户态 vm 开了 seccomp,ban 掉了 execve 和 execveat。如果只是一个用户态题目,rop 执行 orw 也就结束了,但是... 我们还有一个内核 exp 的二进制需要上传执行。没有 shell,怎么传?传不了一点。

和用户态爷爷(还是 IChild)商量了一下,可以在用户态执行 shellcode 之后,就可以给我 mmap 一段 rwx 内存,我就可以手动把二进制文件"加载"到内存中,然后执行。(WHO NEEDS EXECVE? UH?)

然后,问题就来了。SegmentFault... SegmentFault... SegmentFault....

单步跟了一下,发现问题出现在栈上。 libc_start_main 会从栈上拿 env。所以,我们这次把一个合法的栈也拷贝进内存了。

然后。。。继续 SegmentFault。。。 WTF???

而且这次死的地方十分玄学,死在内核 exp 刚刚进去的 mmap syscall 中,也就是程序进了 mmap syscall 中再也没出来。。。

于是果断地拉来了巨神,在巨神的指导下把 aarch64 的 strace 编译好放到了 rootfs 里面,运行...

“这个 SIGURG 是你预期的吗?”

“【黑人问号】哪来的 SIGURG?”

“但它就是被 SIGURG 杀了...”

于是在 shellcode 里面 handle 了一下 SIGURG(此处省略 4 个小时。。。)

...

终于在马拉松式的调试一天之后,成功改完了 busybox,复用 8080 端口 nc -e 拿到了反弹的 root shell!


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

收藏
点赞8
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回