我真的没做过虚拟机题,这题套了4层虚拟机,属实给力,不能再用以前的手打虚拟机方法了,必须程序化。首先拖入IDA分析。
输入一段Hex Text,转为Hex Bytes。
建立缓冲区,复制虚拟机指令到如图位置。随后按以下结构体构造了虚拟机的参数
随后传入第一个虚拟机上下文参数(vm_sub
)开始执行虚拟机,并由结果进行输出。
进入虚拟机函数,废话和弯路我就不多说了,直接分析可知
这是典型的压栈,再看后续指令
基本都是通过堆栈进行计算。经过两天的弯路后,我最终决定通过KeyStone还原各层虚拟机的源码。由于1,2,3层虚拟机的指令仅仅是替换而已,因此这里只分析第1层。(Butterfly为第0层,三个Buffer分别是1、2、3层,最后一层是关键代码)
只需要模仿通常静态分析手段扫过去就行了,然后按照对应OpCode生成汇编,然后再进行编译,贴上源码
需要注意的是,除了第1层的40和42,以及后续层的这两个位置的指令,其他各层都相同,因此分析后面的只需要改一下case就行了。额外,第2、3层的这两个指令加了不少其他代码,但是我发现不对增加的代码进行增补也可以解题,后面细说。除此之外,还需要注意把lea esp的地方改为add/sub esp,不然IDA不认(非标准
分析完4层指令后,贴上关键的反编译函数。
第1层
第2层
第3层
最终分析目标函数(第4层)
首先获取了前面几层的指令指针
这里其实调用了memcpy系列函数,不过被优化了,由于我偷懒,并没有为每层更改memcpy,memset系列函数的实现,因此看到这个指令,就可以认为调用了Host的那个地方的函数,转而看前面层的代码就可以了。在这里,经过提取分析,得到这里memcpy对前4字节的CRC32
经过爆破,可以得出前4字节为AE CC C0 DE
。
随后分析下一个函数
这里修改了上层Host的指令,可以看出是修改了一些立即数(前后对照),因此在还原函数的时候稍加注意即可,对于第一个memset,提炼出关键校验函数有:
sub_E21
完成了某种变换,可以通过爆破还原,并计算了一个值(D540)避免多解,随后由于前4字节已经计算出,带偏移传入sub_109A
前14个字节以及15字节的低2位变成10*10矩阵,随后初始化棋盘,使用sub_1517进行解密。
完成的是根据输入,从左上角依次访问棋盘,并对访问位置及其相邻的元素进行异或,最终使得全1变为全0。这里算法不多说,可以去看文章。完成求解
此时根据这里的防止多解,完成前8个int的求解
最后分析最后6个int,和前面棋盘大同小异,192比特的前190比特以三角形的方式放入矩阵,并将三角形复制8次填满矩阵,然后求解使得棋盘翻转。
*v6 == 2指的是剩余2比特(高2位),因此求解该矩阵
最终完成求解
拿脚本解出来
因此最终Flag为
struct vm_sub
{
int
param1;
/
/
6
,
6
,
6
,
3
int
param2;
/
/
ins1, ins2, ins3, input_hex
char
*
vm_ins;
int
size;
int
idk_0;
int
id
;
int
idk7;
int
idk8;
};
struct vm_fin
{
unsigned char
*
input_hex;
int
*
len_buf;
vm_sub
*
vmsub;
};
struct vm_context
{
vm_sub subs[
4
];
vm_fin fin;
};
struct vm_sub
{
int
param1;
/
/
6
,
6
,
6
,
3
int
param2;
/
/
ins1, ins2, ins3, input_hex
char
*
vm_ins;
int
size;
int
idk_0;
int
id
;
int
idk7;
int
idk8;
};
struct vm_fin
{
unsigned char
*
input_hex;
int
*
len_buf;
vm_sub
*
vmsub;
};
struct vm_context
{
vm_sub subs[
4
];
vm_fin fin;
};
...
void disassembly_vm1(vm_sub
*
ctx)
{
char
*
eip
=
ctx
-
>vm_ins;
char
*
esp
=
eip
+
2
*
ctx
-
>size;
ks_engine
*
ks;
ks_err err;
err
=
ks_open(KS_ARCH_X86, KS_MODE_32, &ks);
if
(err !
=
KS_ERR_OK)
{
cout <<
"Keystone open error."
<< endl;
return
;
}
ostringstream dasm
=
ostringstream();
dasm <<
"push 6;"
<< endl;
dasm <<
"push 0x20000;"
<< endl;
dasm <<
"call vmins_0;"
<< endl;
dasm <<
"jmp vmins_ret;"
<< endl;
while
(eip < ctx
-
>vm_ins
+
0x792
)
{
int
vm_offset
=
eip
-
ctx
-
>vm_ins;
dasm <<
"vmins_"
<< vm_offset <<
":"
<< endl;
int
ins
=
*
eip
+
+
;
switch (ins)
{
case
17
:
{
dasm <<
"push ebx;"
<< endl;
break
;
}
case
1
:
{
uint8_t off
=
(uint8_t)
*
eip
+
+
;
dasm <<
"xor eax, eax;"
<< endl;
dasm <<
"mov al, "
<< (
int
)off <<
";"
<< endl;
dasm <<
"lea ebx, [ebp+eax*4-400h];"
<< endl;
break
;
}
case
13
:
{
dasm <<
"mov ebx, [ebx];"
<< endl;
break
;
}
case
3
:
{
ecx
=
(uint8_t)
*
eip
+
+
;
dasm <<
"mov ebx, "
<< (
int
)ecx <<
";"
<< endl;
break
;
}
case
8
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
dasm <<
"test ebx, ebx;"
<< endl;
dasm <<
"jz vmins_"
<< (
int
)(vm_offset
+
1
+
off) <<
";"
<< endl;
dasm <<
"jmp vmins_"
<< (
int
)(vm_offset
+
1
+
4
) <<
";"
<< endl;
eip
+
=
4
;
break
;
}
case
21
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"cmp ecx, ebx;"
<< endl;
dasm <<
"jnz vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
15
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"mov [edx], ebx;"
<< endl;
break
;
}
case
6
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
/
/
In disassembly mode we do
not
jump, but skip this instruction.
/
/
eip
+
=
off;
if
(off !
=
4
)
dasm <<
"jmp vmins_"
<< (
int
)(vm_offset
+
1
+
off) <<
";"
<< endl;
eip
+
=
4
;
break
;
}
case
29
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"add ebx, ecx;"
<< endl;
break
;
}
case
30
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"sub eax, ebx;"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
break
;
}
case
14
:
{
dasm <<
"xor ecx, ecx;"
<< endl;
dasm <<
"mov cl, [ebx];"
<< endl;
dasm <<
"mov ebx, ecx;"
<< endl;
break
;
}
case
31
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"imul ebx, edx;"
<< endl;
break
;
}
case
16
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"mov [eax], bl;"
<< endl;
dasm <<
"movsx ebx, bl;"
<< endl;
break
;
}
case
33
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"xor edx, edx;"
<< endl;
dasm <<
"div ebx;"
<< endl;
dasm <<
"mov ebx, edx;"
<< endl;
break
;
}
case
23
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"cmp ecx, ebx;"
<< endl;
dasm <<
"jnb vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
32
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"xor edx, edx;"
<< endl;
dasm <<
"div ebx;"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
break
;
}
case
24
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"cmp edx, ebx;"
<< endl;
dasm <<
"jbe vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
18
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"or ebx, ecx;"
<< endl;
break
;
}
case
28
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"mov ecx, ebx;"
<< endl;
dasm <<
"shr eax, cl;"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
break
;
}
case
20
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"and ebx, ecx;"
<< endl;
break
;
}
case
19
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"xor ebx, ecx;"
<< endl;
break
;
}
case
27
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"mov ecx, ebx;"
<< endl;
dasm <<
"shl edx, cl;"
<< endl;
dasm <<
"mov ebx, edx;"
<< endl;
break
;
}
case
22
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"cmp eax, ebx;"
<< endl;
dasm <<
"jz vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
26
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"cmp ecx, ebx;"
<< endl;
dasm <<
"jb vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
0
:
{
uint8_t off
=
(uint8_t)
*
eip
+
+
;
/
/
ecx
=
(uint32_t)&eax[
4
*
off];
dasm <<
"xor edx, edx;"
<< endl;
dasm <<
"mov dl, "
<< (
int
)off <<
";"
<< endl;
dasm <<
"lea ebx, [ebp+edx*4];"
<< endl;
break
;
}
case
11
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
/
/
esp
+
=
4
*
off;
dasm <<
"mov eax, "
<< (
int
)(off
*
4
) <<
";"
<< endl;
dasm <<
"add esp, eax;"
<< endl;
eip
+
=
4
;
break
;
}
case
4
:
{
ecx
=
*
(uint32_t
*
)eip;
eip
+
=
4
;
dasm <<
"mov ebx, "
<< (
int
)ecx <<
";"
<< endl;
break
;
}
case
40
:
{
/
/
We do
not
execute
/
/
char
*
buf
=
(char
*
)
*
((uint32_t
*
)esp
+
2
);
/
/
uint32_t size
=
*
(uint32_t
*
)esp;
/
/
ecx
=
(uint32_t)buf;
/
/
memset(buf, esp[
4
], size
+
(size &
3
));
/
/
eax
=
ebx;
dasm <<
"mov ecx, [esp+0];"
<< endl;
dasm <<
"xor eax, eax;"
<< endl;
dasm <<
"mov al, [esp+4];"
<< endl;
dasm <<
"mov edi, [esp+8];"
<< endl;
dasm <<
"mov ebx, edi;"
<< endl;
dasm <<
"rep stosb;"
<< endl;
break
;
}
case
42
:
{
/
/
We do
not
execute
/
/
ecx
=
(uint32_t)
*
((uint32_t
*
)esp
+
2
);
/
/
memcpy((void
*
)
*
((uint32_t
*
)esp
+
2
), (void
*
)
*
((uint32_t
*
)esp
+
1
),
*
((uint32_t
*
)esp));
/
/
eax
=
ebx;
dasm <<
"mov ecx, [esp+0];"
<< endl;
dasm <<
"mov edi, [esp+8];"
<< endl;
dasm <<
"mov esi, [esp+4];"
<< endl;
dasm <<
"mov ebx, edi;"
<< endl;
dasm <<
"rep movsb;"
<< endl;
break
;
}
case
9
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
dasm <<
"test ebx, ebx;"
<< endl;
dasm <<
"jz vmins_"
<< (
int
)(vm_offset
+
1
+
4
) <<
";"
<< endl;
dasm <<
"jmp vmins_"
<< (
int
)(vm_offset
+
1
+
off) <<
";"
<< endl;
eip
+
=
4
;
break
;
}
case
2
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
/
/
ecx
=
(uint32_t)&eax[
4
*
off];
eip
+
=
4
;
dasm <<
"mov ecx, "
<< (
int
)off <<
";"
<< endl;
dasm <<
"lea ebx, [ebp+ecx*4];"
<< endl;
break
;
}
case
7
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
/
/
push(esp, (uint32_t)eip
+
4
);
/
/
In disassembly mode we do
not
jump, but skip this instruction.
/
/
eip
+
=
off;
dasm <<
"call vmins_"
<< (
int
)(vm_offset
+
1
+
off) <<
";"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
eip
+
=
4
;
break
;
}
case
10
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
dasm <<
"push ebp;"
<< endl;
dasm <<
"mov ebp, esp;"
<< endl;
dasm <<
"sub esp, "
<< off
*
4
<<
";"
<< endl;
eip
+
=
4
;
break
;
}
case
12
:
/
/
return
{
dasm <<
"mov eax, ebx;"
<< endl;
dasm <<
"mov esp, ebp;"
<< endl;
dasm <<
"pop ebp;"
<< endl;
dasm <<
"ret;"
<< endl;
break
;
}
case
43
:
{
dasm <<
"mov eax, [esp];"
<< endl;
dasm <<
"ret;"
<< endl;
goto finished;
}
default:
{
cout <<
"Error"
;
break
;
}
}
}
finished:
dasm <<
"vmins_ret:"
<< endl;
dasm <<
"push ebx;"
<< endl;
dasm <<
"mov eax, [esp];"
<< endl;
dasm <<
"ret;"
<< endl;
unsigned char
*
output;
size_t outlen
=
0
;
size_t outcnt
=
0
;
string disasm
=
dasm.
str
();
ofstream fout
=
ofstream(
"./disasm_vm1.txt"
, ios_base::ate);
fout << disasm;
fout.flush();
fout.close();
const char
*
code
=
disasm.c_str();
if
(ks_asm(ks, code,
0
, &output, &outlen, &outcnt) !
=
KS_ERR_OK)
{
ks_err err
=
ks_errno(ks);
cout << err;
}
fout
=
ofstream(
"./disasm_vm1.bin"
, ios_base::ate | ios_base::binary);
fout.write((const char
*
)output, outlen);
fout.flush();
fout.close();
ks_free(output);
ks_close(ks);
}
...
void disassembly_vm1(vm_sub
*
ctx)
{
char
*
eip
=
ctx
-
>vm_ins;
char
*
esp
=
eip
+
2
*
ctx
-
>size;
ks_engine
*
ks;
ks_err err;
err
=
ks_open(KS_ARCH_X86, KS_MODE_32, &ks);
if
(err !
=
KS_ERR_OK)
{
cout <<
"Keystone open error."
<< endl;
return
;
}
ostringstream dasm
=
ostringstream();
dasm <<
"push 6;"
<< endl;
dasm <<
"push 0x20000;"
<< endl;
dasm <<
"call vmins_0;"
<< endl;
dasm <<
"jmp vmins_ret;"
<< endl;
while
(eip < ctx
-
>vm_ins
+
0x792
)
{
int
vm_offset
=
eip
-
ctx
-
>vm_ins;
dasm <<
"vmins_"
<< vm_offset <<
":"
<< endl;
int
ins
=
*
eip
+
+
;
switch (ins)
{
case
17
:
{
dasm <<
"push ebx;"
<< endl;
break
;
}
case
1
:
{
uint8_t off
=
(uint8_t)
*
eip
+
+
;
dasm <<
"xor eax, eax;"
<< endl;
dasm <<
"mov al, "
<< (
int
)off <<
";"
<< endl;
dasm <<
"lea ebx, [ebp+eax*4-400h];"
<< endl;
break
;
}
case
13
:
{
dasm <<
"mov ebx, [ebx];"
<< endl;
break
;
}
case
3
:
{
ecx
=
(uint8_t)
*
eip
+
+
;
dasm <<
"mov ebx, "
<< (
int
)ecx <<
";"
<< endl;
break
;
}
case
8
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
dasm <<
"test ebx, ebx;"
<< endl;
dasm <<
"jz vmins_"
<< (
int
)(vm_offset
+
1
+
off) <<
";"
<< endl;
dasm <<
"jmp vmins_"
<< (
int
)(vm_offset
+
1
+
4
) <<
";"
<< endl;
eip
+
=
4
;
break
;
}
case
21
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"cmp ecx, ebx;"
<< endl;
dasm <<
"jnz vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
15
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"mov [edx], ebx;"
<< endl;
break
;
}
case
6
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
/
/
In disassembly mode we do
not
jump, but skip this instruction.
/
/
eip
+
=
off;
if
(off !
=
4
)
dasm <<
"jmp vmins_"
<< (
int
)(vm_offset
+
1
+
off) <<
";"
<< endl;
eip
+
=
4
;
break
;
}
case
29
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"add ebx, ecx;"
<< endl;
break
;
}
case
30
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"sub eax, ebx;"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
break
;
}
case
14
:
{
dasm <<
"xor ecx, ecx;"
<< endl;
dasm <<
"mov cl, [ebx];"
<< endl;
dasm <<
"mov ebx, ecx;"
<< endl;
break
;
}
case
31
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"imul ebx, edx;"
<< endl;
break
;
}
case
16
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"mov [eax], bl;"
<< endl;
dasm <<
"movsx ebx, bl;"
<< endl;
break
;
}
case
33
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"xor edx, edx;"
<< endl;
dasm <<
"div ebx;"
<< endl;
dasm <<
"mov ebx, edx;"
<< endl;
break
;
}
case
23
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"cmp ecx, ebx;"
<< endl;
dasm <<
"jnb vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
32
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"xor edx, edx;"
<< endl;
dasm <<
"div ebx;"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
break
;
}
case
24
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"cmp edx, ebx;"
<< endl;
dasm <<
"jbe vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
18
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"or ebx, ecx;"
<< endl;
break
;
}
case
28
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"mov ecx, ebx;"
<< endl;
dasm <<
"shr eax, cl;"
<< endl;
dasm <<
"mov ebx, eax;"
<< endl;
break
;
}
case
20
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"and ebx, ecx;"
<< endl;
break
;
}
case
19
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"xor ebx, ecx;"
<< endl;
break
;
}
case
27
:
{
dasm <<
"pop edx;"
<< endl;
dasm <<
"mov ecx, ebx;"
<< endl;
dasm <<
"shl edx, cl;"
<< endl;
dasm <<
"mov ebx, edx;"
<< endl;
break
;
}
case
22
:
{
dasm <<
"pop eax;"
<< endl;
dasm <<
"cmp eax, ebx;"
<< endl;
dasm <<
"jz vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
26
:
{
dasm <<
"pop ecx;"
<< endl;
dasm <<
"cmp ecx, ebx;"
<< endl;
dasm <<
"jb vmins_"
<< vm_offset <<
"set0;"
<< endl;
dasm <<
"mov ebx, 1;"
<< endl;
dasm <<
"jmp vmins_"
<< vm_offset
+
1
<<
";"
<< endl;
dasm <<
"vmins_"
<< vm_offset <<
"set0:"
<< endl;
dasm <<
"mov ebx, 0;"
<< endl;
break
;
}
case
0
:
{
uint8_t off
=
(uint8_t)
*
eip
+
+
;
/
/
ecx
=
(uint32_t)&eax[
4
*
off];
dasm <<
"xor edx, edx;"
<< endl;
dasm <<
"mov dl, "
<< (
int
)off <<
";"
<< endl;
dasm <<
"lea ebx, [ebp+edx*4];"
<< endl;
break
;
}
case
11
:
{
uint32_t off
=
*
(uint32_t
*
)eip;
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2020-11-28 18:08
被k1ee编辑
,原因: 修正描述错误