-
-
[原创]KCTF 2021秋季赛 第九题 万事俱备
-
2021-12-9 12:41 19737
-
初步分析
易语言?
拿到程序,发现是易语言写的,还有点奇怪,直接x64dbg调试先看,跑起来发现易语言只是套了个外壳:Py脚本
从temp目录找到check.py和对应的CPython2.7解释器,不用想解释器肯定是改过的,先定位到程序版本:
程序版本是2.7.18,从官网下载源码编译,https://www.python.org/ftp/python/2.7.18/Python-2.7.18.tgz- Windows:解压源码,进入PCBuild目录,打开pcbuild.sln,选择Debug模式编译python和pythoncore
- 其他的模块,如socket等按需编译即可(如果import失败则编译)
- 编译报错:
Python执行原理
温顾一遍解释器原理也是有必要的,首先定位到几个关键点:- PyObject基类
- PyEval_EvalFrameEx函数 解释器dispatcher
- run_pyc_file函数 执行pyc
- opcode.h里面定义各种opcode宏,大于90是带参数的
- 直接各种调试下断点,栈回溯即可摸清执行流程
详细分析
处理Opcode
目前还没找到很简单的方法,目前用了两种方式:
1、编写测试代码生成,用两个解释器生成pyc,再用dis模块反汇编对比,这里可写个脚本提高下效率,这种方式能找到到绝大部分。
2、对于方式1不好生成的指令,通过IDA对比PyEval_EvalFrameEx函数,手动识别特征代码(字符串、关系调用等)重新编译python
替换了源码中的Include/opcode.h和Lib/opcode.py,最终生成python可以执行check.py,开始以为作者在解释器上做了大量工作,事实上并没有。
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 | / / 恢复的opcode如下 #define NOP 65 #define STOP_CODE 61 #define POP_TOP 30 #define ROT_TWO 52 #define ROT_THREE 56 #define DUP_TOP 13 #define ROT_FOUR 16 #define UNARY_POSITIVE 32 #define UNARY_NEGATIVE 89 #define UNARY_NOT 57 #define UNARY_CONVERT 87 #define UNARY_INVERT 25 #define BINARY_POWER 77 #define BINARY_MULTIPLY 69 #define BINARY_DIVIDE 71 #define BINARY_MODULO 14 #define BINARY_ADD 81 #define BINARY_SUBTRACT 53 #define BINARY_SUBSCR 40 #define BINARY_FLOOR_DIVIDE 76 #define BINARY_TRUE_DIVIDE 48 #define INPLACE_FLOOR_DIVIDE 26 #define INPLACE_TRUE_DIVIDE 63 #define SLICE 0 #define SLICE_1 1 #define SLICE_2 2 #define SLICE_3 3 #define STORE_SLICE 4 #define STORE_SLICE_1 5 #define STORE_SLICE_2 6 #define STORE_SLICE_3 7 #define DELETE_SLICE 8 #define DELETE_SLICE_1 9 #define DELETE_SLICE_2 10 #define DELETE_SLICE_3 11 #define STORE_MAP 64 #define INPLACE_ADD 17 #define INPLACE_SUBTRACT 86 #define INPLACE_MULTIPLY 20 #define INPLACE_DIVIDE 74 #define INPLACE_MODULO 67 #define STORE_SUBSCR 73 #define DELETE_SUBSCR 23 #define BINARY_LSHIFT 66 #define BINARY_RSHIFT 19 #define BINARY_AND 18 #define BINARY_XOR 88 #define BINARY_OR 85 #define INPLACE_POWER 33 #define GET_ITER 70 #define PRINT_EXPR 39 #define PRINT_ITEM 59 #define PRINT_NEWLINE 15 #define PRINT_ITEM_TO 62 #define PRINT_NEWLINE_TO 24 #define INPLACE_LSHIFT 41 #define INPLACE_RSHIFT 72 #define INPLACE_AND 45 #define INPLACE_XOR 37 #define INPLACE_OR 29 #define BREAK_LOOP 50 #define WITH_CLEANUP 42 #define LOAD_LOCALS 83 #define RETURN_VALUE 46 #define IMPORT_STAR 28 #define EXEC_STMT 51 #define YIELD_VALUE 60 #define POP_BLOCK 22 #define END_FINALLY 31 #define BUILD_CLASS 54 #define HAVE_ARGUMENT 90 /* Opcodes from here have an argument: */ #define STORE_NAME 112 #define DELETE_NAME 127 #define UNPACK_SEQUENCE 107 #define FOR_ITER 108 #define LIST_APPEND 141 #define STORE_ATTR 102 #define DELETE_ATTR 137 #define STORE_GLOBAL 98 #define DELETE_GLOBAL 114 #define DUP_TOPX 110 #define LOAD_CONST 131 #define LOAD_NAME 94 #define BUILD_TUPLE 106 #define BUILD_LIST 133 #define BUILD_SET 116 #define BUILD_MAP 139 #define LOAD_ATTR 140 #define COMPARE_OP 95 #define IMPORT_NAME 124 #define IMPORT_FROM 135 #define JUMP_FORWARD 115 #define JUMP_IF_FALSE_OR_POP 123 #define JUMP_IF_TRUE_OR_POP 128 #define JUMP_ABSOLUTE 118 #define POP_JUMP_IF_FALSE 92 #define POP_JUMP_IF_TRUE 120 #define LOAD_GLOBAL 138 #define CONTINUE_LOOP 113 #define SETUP_LOOP 93 #define SETUP_EXCEPT 111 #define SETUP_FINALLY 130 #define LOAD_FAST 109 #define STORE_FAST 119 #define DELETE_FAST 142 #define RAISE_VARARGS 134 #define CALL_FUNCTION 90 #define MAKE_FUNCTION 126 #define BUILD_SLICE 105 #define MAKE_CLOSURE 136 #define LOAD_CLOSURE 132 #define LOAD_DEREF 125 #define STORE_DEREF 122 #define CALL_FUNCTION_VAR 99 #define CALL_FUNCTION_KW 100 #define CALL_FUNCTION_VAR_KW 101 #define SETUP_WITH 143 #define EXTENDED_ARG 145 #define SET_ADD 146 #define MAP_ADD 147 |
- 反汇编pyc
使用uncompyle6,发现反编译都失败了,最后选择了Decompyle++
Decompyle++仓库:https://github.com/zrax/pycdc
替换opcode后,WSL里cmake编译,执行pycdas,反编译虽然失败,至少能够反汇编了,最后py代码做了些保护如下:
名称混淆:
指令switch平坦 + 花指令:
switch:每一条指令通过XOR跳往switch判断下一条指令
花指令:这里的opcode当NOP处理
字符串加密(这里是后面Trace发现的),通过base64、zlib、xor等解密
1 | eNpVlq + LImEYx / Xu9g6GxbDBIGLYsCwiGwz + AQO3YRCDwXAHEwwHLmIweN1gEDEYDCIGg0HEYNg / wGAYlglymA + DYVgmHGI + n1nm8 / KUL + / M + 7zPj + / zfZ + ZnzeJROLx0xUav5PXVVJWt7y7 + XxdfZPVq0BPoCLgC6R5V + LE9ys + fpGVG280xgLPmOzF6df43YeDyN9CoClQwPhOjP8m4902GTwIvAh4AlMBSyCMS2h0BXYE2hHIFvghcBLY4OWAyUWgzmMeiFzNKatHoKjefwIjCNvyLvKcEThismJjRn6OTrKPscfjEA76cBDwuKWiJRtDYtzH7H7kXKCOPjHudJJbTCLCcpz1qWihV1mBMv2IWraHtS05 / 8JkxtkWG + 90dUqmAxrfxPMU7s9U3kQHDr2M + pEitXt6OSGrOYqw2XV1e0LOjiBiTi6vqMRChANChtpzl7hF7eqNaGeOFWnABNVddFY2mUa9XLMRciwi9g92BWCCvxPdr + ir0aPyPMdGNL6N5z4hPYpZQ8mFUgdcjYB2b5FADTpPxB2wWsJGR6DKCRu1L7BbINY62jhiZ + E5RPzmuhjGl2yMGX0W9 / cJpweq3CABF65KUOLQPI9ALfLbUH4bmjxGhqNvtwNNAYkbjZvrZ0b4kHQXekp1YdLXQtpQWxoh5aBuSZVFcikTsstQCCh6j0kRDdXhdEWgHYS1Ma6itR3EHvHiIgtbT8IenktsbLhlVYKfaWiHcZPHX4r7G + rm1eGqS1ZZiC1rO4vJ9cyqxjUYE2iNF5tdn9aaqdKmHwd9IqWH1pGmmA9Lh5AuSQ4he4zCamykqXyNBExDz5x9oSlmmhlKmoj1nSQrWu1rzYFHkj7 + WggkpL97 / T2q6q + puY0jGpCjSgsTcw1M0Q / MiCqdDnjMkOkFDZ2p0kyGgv6gTTQRTzA5o / wVArZgKMsJc89LqCmjf6gc7swEwm4JJKn9B4X61ks = ', 866: ' 0 ', 715: ' marshal ', 341: ' zlib ', 203: ' base64'}, 866 ) |
动态Trace
因为指令switch那块比较固定,静态还原代码也不是很难,考虑效率问题,最后还是找到了不错的动态跟踪的工具:
x-python仓库:https://github.com/rocky/x-python
做了如下改动:
- 仓库依赖xdis,修改其opcode_2x.py等
- 简单修改PyVM的eval_frame和format_instruction过滤掉干扰的指令12345678910
#eval_frame函数
if
(bytecode_name
not
in
[
'COMPARE_OP'
,
'POP_JUMP_IF_FALSE'
,
'JUMP_ABSOLUTE'
,
'NOP'
,
'LOAD_FAST'
,
'STORE_FAST'
]):
if
len
(arguments) >
0
:
if
isinstance
(arguments[
0
],
long
):
if
arguments[
0
] <
111111183538799472
:
self
.log(bytecode_name, int_arg, arguments, offset, line_number)
else
:
self
.log(bytecode_name, int_arg, arguments, offset, line_number)
else
:
self
.log(bytecode_name, int_arg, arguments, offset, line_number)
12345678#format_instruction函数
if
vm
and
bytecode_name
in
vm.byteop.stack_fmt:
stack_args
=
vm.byteop.stack_fmt[bytecode_name](vm, int_arg,
repr
)
if
(bytecode_name
=
=
'INPLACE_XOR'
):
pos
=
stack_args.find(
'L'
)
if
pos !
=
-
1
:
if
long
(stack_args[stack_args.find(
'('
)
+
1
:pos])>
111111111111111111111
:
return
""
最后执行python_d.exe xpy.py -v check.pyc,几分钟后到了raw_input输入,看了下之前代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | INFO:xpython.vm: @ 7266 : LOAD_NAME dict INFO:xpython.vm: @ 7637 : BUILD_LIST 0 INFO:xpython.vm: @ 5069 : LOAD_NAME __import__ INFO:xpython.vm: @ 6232 : LOAD_CONST marshal INFO:xpython.vm: @ 4648 : CALL_FUNCTION ( __import__ ) 1 positional, 0 named INFO:xpython.vm: @ 8028 : LOAD_ATTR loads INFO:xpython.vm: @ 6832 : LOAD_CONST eNpdmD2LKkkUhmf37geIGPSCgQwGBjKIdGDgDzAwkMFggg5c6MBAuCIGBv4AAwNxO5igA5EODAwGMTAwWDYyMJCLLCL9Aww6EOlAlonX8uJz9mxyqLKqznnPez6q2t9 / fnp6evnxJr7 + + eU2 + sGMkvz2202 + / GpGayOGRtSN2BuR5reynDCjX8yoyup989yIjhFFNv9xM / fykxm5Dy1f / cfZ71sKZvT3l4eCnhEpI9qc8Iw4GlEy4h24MyMmRmSN + MeIjBFLI6ZGRKA6se8OPGdEBX0n1E / waGNEDOYTUxdAAyNGRlyN + IaCGgpyHMtwwgJ9jLBAsAZV / KR4GePRDM3CQQ6T4lH0CPd / TE7QF8FazIkBW8psuY8CoA1wP8fmNpvXWDuB5b65jwt99j2D4GDEBxwcWVgxvcB9wghH77svtIxosNDAmguTb4TnirUsNGWBcc / srhELTGZwtY2qsxE2 + AJGR07cleY1vgRaPIxXWPgkliWI3ZDKNVC1dWnMyPuALD5iN0VQ2oxeiYK4H6EvQaCukNgEhnhU1Mk / JzcCcm2KC + JbmeqJsZbGmsNvz8AooioJ + tOjjXzX8k7cBugrMi2TUneyd4DcsLBDc1pHJoODM73Fwm4T4xGrWxRIfyni5RloAal3RoGL5yuI9THkwu6BjLUB9MmCw9mWpsnB8x7AA5B2UbDF6QJu + WTYmlpwyM4 + WXLlWB19Cc5uQeVgY0AJdTR / 0kqXqDqDRQrb1r3JhisLzTIKCe0BNq66yViYzGPojZukoGt1A + YUdsfUucBNU4hjSKwQ6S4Z9s6xOkG2cGtHii7ohM / gC6lpm + mZmjnqYsqTf55u9VmMt5gmQPoBk3v0fdAPQkrcIih1fJPeniL6PnAzjCSRdqCv89sEzDGaM5rdOV6WdH / xod0nlj4mu2he6Us9DTk9 / ZutA1 + GnDRVewGQh4Ihbs3B1yRafaxVONEmZHk2T3B / ra + xKZSUtDNb / JV3jrybpuirgGWD6OpkyBOUI6rq0D5kX6DTJ4 / nPfB1mC5gsgiJCVxNcZuO9DXRxP0IG1tiPsGjV4p4wbEBeVrARgJX5VgMJXvq7QSxHbRIV6nRRsa4ekFplWLvAbKDNUf3XXnizEBfA6ncGkuOOeArozQgCicSJIlbUxrKFgdT + lXqUecbnaLySFjox528uUbAfcfagNUWoypnLfKgioMOkTnhqqcvNBvMPYS8ckP0ORDbJXcbUFLFeAiWEazNWUgR3wmpV2LfSPeIEWRnubHTsDaEziWtr0HtN3X9hmTsHmFjY6O7VAd / k5ytkbF54KaZzvS7xMLBHf6uwNzRfbxGlsw422XkkwIRJ + RefWZ1TicM2bdncwm4ZRiakHotoMn9a8GfvMctPC + SAhd9X9ZJPblYTmSdsNFA + HA / 1vdWESLeqIAtdemRFg4V8L / Hp08 + N0kam5D1UTpEeKwe9LdazMIUxuXBYoFeviHW + pEw1VFYsCC5O9J9fM7UhaEhSgMi7REU + ZqoQESWtHWxmwXQJ8bLGrOkihDxCdnylSrA67Cx1LF0iUwTrkJUyaegVE + ku0UJuBdaboEge1SUp19LS + wmdYNPQaeDCxHlUiDNYl26SVyV727JXcmcHOQ47AtJ6hAiclTAK6ik3QT6P4ouXkaALACoxtQlrxaETP7BaLMq3TsiyCt9WW / pL5K7Hv7K7dLS0Lb6vRvrv3LkK1W + Eg66GitsyaB5DC93pX / dxL / / a4kn INFO:xpython.vm: @ 4908 : LOAD_ATTR decode INFO:xpython.vm: @ 4959 : LOAD_CONST base64 INFO:xpython.vm: @ 6483 : CALL_FUNCTION (decode) 1 positional, 0 named INFO:xpython.vm: @ 7383 : LOAD_ATTR decode INFO:xpython.vm: @ 8232 : LOAD_CONST zlib INFO:xpython.vm: @ 4318 : CALL_FUNCTION (decode) 1 positional, 0 named INFO:xpython.vm: @ 5568 : CALL_FUNCTION (loads) 1 positional, 0 named INFO:xpython.vm: @ 4207 : FOR_ITER 7304 INFO:xpython.vm: @ 5658 : STORE_NAME (( 954 , ( 12 ,))) ooo00o INFO:xpython.vm: @ 5122 : LOAD_NAME ooo00o INFO:xpython.vm: @ 8417 : LOAD_CONST 0 INFO:xpython.vm: @ 5897 : BINARY_SUBSCR (( 954 , ( 12 ,)), 0 ) INFO:xpython.vm: @ 8584 : LOAD_CONST 216 INFO:xpython.vm: @ 8639 : BINARY_XOR ( 954 , 216 ) |
执行效果还不错,看到了具体代码,以后有空再改造下把栈的信息拿出来,方便分析。
验证过程
- 1、KCTF补位16字节,KCTF@021GoodLuck
- 2、helloctf_pediy_Archaia进行md5计算,然后初始化表
- 3、迭代两次,flag和表里元素xor,拿到中间结果
- 4、接着进行解密运算,拿到结果和用户名比对,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 | #两次迭代过程,直接把表提取出来 t1 = [( 145 , 161 ),( 125 , 161 ),( 11 , 161 ),( 202 , 161 ),( 7 , 161 ),( 12 , 161 ),( 126 , 161 ),( 130 , 161 ),( 114 , 161 ),( 69 , 161 ),( 43 , 161 ),( 110 , 161 ),( 48 , 161 ),( 225 , 161 ),( 43 , 161 ),( 6 , 161 )] t2 = [( 22 , 61 ),( 174 , 61 ),( 99 , 61 ),( 135 , 61 ),( 98 , 61 ),( 99 , 61 ),( 186 , 61 ),( 139 , 61 ),( 90 , 61 ),( 211 , 61 ),( 91 , 61 ),( 87 , 61 ),( 255 , 61 ),( 225 , 61 ),( 13 , 61 ),( 144 , 61 )] import binascii def build_flag(x): flag = '' for i,v in enumerate (x): c = ord (v)^t1[i][ 0 ]^t1[i][ 1 ]^t2[i][ 0 ]^t2[i][ 1 ] flag + = chr (c) flag = binascii.b2a_hex(flag.encode( 'latin-1' )).decode( 'latin-1' ).upper() return 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 | #最后解密运算 def WORD(v): return v& 0xffff def BYTE_LO(v): return v& 0xff def BYTE_HI(v): return v>> 8 def MAKE_BYTE(lo,hi): return lo|(hi<< 8 ) def ROR(v, num): return (v>>( 16 - num)) | (v<<num) def ROL(v, num): return (v<<( 16 - num)) | (v>>num) def check_sn(a, b, x): flag = [] for i in range ( 8 ): pos1 = (b ^ (a + i * 2 )) % 16 pos2 = (b ^ (a + i * 2 + 1 )) % 16 flag.append((x[pos1]<< 8 )|x[pos2]) m = (flag[ 0 ]^flag[ 1 ]) n = (flag[ 2 ] + flag[ 3 ]) j = (flag[ 4 ] - flag[ 5 ]) k = (flag[ 6 ]^flag[ 7 ]) k = (k& 0x55555555 ) + (k>> 1 & 0x55555555 ) k = (k& 0x33333333 ) + ((k>> 2 )& 0x33333333 ) k = (k& 0x0F0F0F0F ) + ((k>> 4 )& 0x0F0F0F0F ) k = (k& 0xff00ff ) + ((k>> 8 )& 0xff00ff ) k = WORD(k) + ((k>> 16 )& 0xff00ff ) xx = (m&n)|((~m)&j) yy = WORD(WORD((m * xx)>>k) + 24 ) zz = yy^j qq = (yy&xx)|(zz&xx)|(zz&yy) flag[ 0 ] = WORD(flag[ 0 ]^qq) flag[ 1 ] = WORD(flag[ 1 ]^qq) flag[ 2 ] = WORD(flag[ 2 ] + zz) flag[ 3 ] = WORD(flag[ 3 ] - zz) flag[ 4 ] = WORD(flag[ 4 ] + yy) flag[ 5 ] = WORD(flag[ 5 ] + yy) flag[ 6 ] = WORD(ROR(flag[ 6 ]^xx, k)) flag[ 7 ] = WORD(ROR(flag[ 7 ]^xx, k)) user = [] for v in flag: lo = BYTE_LO(v) hi = BYTE_HI(v) #print('%c%c'%(lo,hi),end='') user.append(lo) user.append(hi) return user |
最后求解
直接逆推不出来,注意到m、n、j、k的初始值和最后运算方式一致,因此可以把mnjk推导出来,至于k的值(最多是0-15),因此跑个16次计算,再把结果校验,最后得到KCTF的flag:
1 2 | KCTF@ 021GoodLuck AD0A1F8179ABE48ED3B073F840DA52A7 |
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 | #求解过程片段 #计算差值 for i in range ( int ( len (x) / 2 )): v = x[i * 2 ]|(x[i * 2 + 1 ]<< 8 ) brutes.append(v) print (brutes) m0 = brutes[ 0 ]^brutes[ 1 ] m1 = brutes[ 2 ] + brutes[ 3 ] m2 = brutes[ 4 ] - brutes[ 5 ] m3 = brutes[ 6 ]^brutes[ 7 ] def reverse(flag): cc = flag.copy() for i in range ( 16 ): m,n,j,k = 1311 , 24946 , 2776 ,i #m0,m1,m2 xx = (m&n)|((~m)&j) yy = WORD(WORD((m * xx)>>k) + 24 ) zz = yy^j qq = (yy&xx)|(zz&xx)|(zz&yy) #print(xx,yy,zz,qq) flag = cc.copy() flag[ 0 ] = WORD(flag[ 0 ]^qq) flag[ 1 ] = WORD(flag[ 1 ]^qq) flag[ 2 ] = WORD(flag[ 2 ] - zz) flag[ 3 ] = WORD(flag[ 3 ] + zz) flag[ 4 ] = WORD(flag[ 4 ] - yy) flag[ 5 ] = WORD(flag[ 5 ] - yy) flag[ 6 ] = WORD(ROL(flag[ 6 ], k)^xx) flag[ 7 ] = WORD(ROL(flag[ 7 ], k)^xx) x = [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] a,b = 14 , 5 for i in range ( 8 ): pos1 = (b ^ (a + i * 2 )) % 16 pos2 = (b ^ (a + i * 2 + 1 )) % 16 x[pos1] = flag[i]>> 8 x[pos2] = flag[i]& 0xff import binascii t1 = [( 145 , 161 ),( 125 , 161 ),( 11 , 161 ),( 202 , 161 ),( 7 , 161 ),( 12 , 161 ),( 126 , 161 ),( 130 , 161 ),( 114 , 161 ),( 69 , 161 ),( 43 , 161 ),( 110 , 161 ),( 48 , 161 ),( 225 , 161 ),( 43 , 161 ),( 6 , 161 )] t2 = [( 22 , 61 ),( 174 , 61 ),( 99 , 61 ),( 135 , 61 ),( 98 , 61 ),( 99 , 61 ),( 186 , 61 ),( 139 , 61 ),( 90 , 61 ),( 211 , 61 ),( 91 , 61 ),( 87 , 61 ),( 255 , 61 ),( 225 , 61 ),( 13 , 61 ),( 144 , 61 )] gogo = '' for i,v in enumerate (x): c = v^t1[i][ 0 ]^t1[i][ 1 ]^t2[i][ 0 ]^t2[i][ 1 ] gogo + = chr (c) xxx = x user = user_encrypt( 14 , 5 ,xxx) if ( 'KCTF' not in user): continue print (user) gogo = binascii.b2a_hex(gogo.encode( 'latin-1' )).decode( 'latin-1' ).upper() print (gogo) |
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课