【文章标题】: happytown第17个crackme算法分析
【文章作者】: dewar
【作者邮箱】: huazi0745@sina.com
【下载地址】: 看雪论坛搜索
【加壳方式】: 无
【保护方式】: 无
【编写语言】: VC
【使用工具】: OD
【操作平台】: WINXP
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
本人菜鸟一只,这是我的笔记,内容很菜,高手略过.
1.PEID查无壳,VC编写.
2.OD截入,停在OEP处,按Ctrl+N,在名称一栏中找到USER32.GetDlgItemTextA,选中后右击====>在每个参考上设置断点.打开断点窗口可看到已设置好两个断点.(一个用户名的,一个注册码的,正好^_^)
3.F9运行,输入注册信息
用户名:dewarj
注册码:abcdefghijklmn
为什么这么输往下看就知道了.
4.点Check按钮,程序立即中断在如下的地方:
......
004011B6 |. 68 C9000000 PUSH 0C9 ; /Count = C9 (201.)
004011BB |. 52 PUSH EDX ; |Buffer
004011BC |. 68 E8030000 PUSH 3E8 ; |ControlID = 3E8 (1000.)
004011C1 |. 50 PUSH EAX ; |hWnd
004011C2 |. FF15 AC504000 CALL DWORD PTR DS:[<&USER32.GetDlg>; \<====中断在这里.获取用户名及长度,用户名放入EDX所指的缓存中,长度在EAX中
004011C8 |. 8BF0 MOV ESI,EAX ; 长度=>ESI
004011CA |. 83FE 06 CMP ESI,6
004011CD |. 0F8C 3B020000 JL CrackMe_.0040140E ; 长度不能小于6
004011D3 |. 83FE 0F CMP ESI,0F
004011D6 |. 0F8F 32020000 JG CrackMe_.0040140E ; 长度不能大于15
004011DC |. 8D4C24 4C LEA ECX,DWORD PTR SS:[ESP+4C] ; 取用户名到ECX
004011E0 |. 56 PUSH ESI
004011E1 |. 51 PUSH ECX
004011E2 |. E8 39020000 CALL CrackMe_.00401420 ; 用户名小写转化为大写
004011E7 |. 83C4 08 ADD ESP,8
004011EA |. 85C0 TEST EAX,EAX ; 用户名中有无字母外的字符
004011EC |. 0F84 1C020000 JE CrackMe_.0040140E ; 有就跳向失败
004011F2 |. 33ED XOR EBP,EBP
004011F4 |. 33C9 XOR ECX,ECX
004011F6 |. 3BF5 CMP ESI,EBP
004011F8 |. 7E 2F JLE SHORT CrackMe_.00401229
004011FA |> 33FF /XOR EDI,EDI ; EDI清零
004011FC |. 3BCD |CMP ECX,EBP ; 循环变量ECX与EBP比较
004011FE |. 7E 24 |JLE SHORT CrackMe_.00401224 ; 小于等于就跳
00401200 |> 8A543C 4C |/MOV DL,BYTE PTR SS:[ESP+EDI+4C] ; 取用户名的第EDI个字符
00401204 |. 8A440C 4C ||MOV AL,BYTE PTR SS:[ESP+ECX+4C] ; 取用户名的第ECX个字符
00401208 |. 3AD0 ||CMP DL,AL ; 两位进行比较
0040120A |. 75 13 ||JNZ SHORT CrackMe_.0040121F ; 不等就跳
0040120C |. 3BCE ||CMP ECX,ESI ; 相等就看是不是串尾
0040120E |. 8BC1 ||MOV EAX,ECX ; 循环变量EAX=ECX
00401210 |. 7D 0D ||JGE SHORT CrackMe_.0040121F ; 是就跳
00401212 |> 8A5404 4D ||/MOV DL,BYTE PTR SS:[ESP+EAX+4D] ; \不是就
00401216 |. 885404 4C |||MOV BYTE PTR SS:[ESP+EAX+4C],DL ; |将用户名第EAX(ECX)位的后一字符前移一位
0040121A |. 40 |||INC EAX ; |循环变量EAX+1(下一位)
0040121B |. 3BC6 |||CMP EAX,ESI ; |是否到串尾
0040121D |.^ 7C F3 ||\JL SHORT CrackMe_.00401212 ; /没有就循环
0040121F |> 47 ||INC EDI ; 循环变量EDI+1(下一位)
00401220 |. 3BF9 ||CMP EDI,ECX ; EDI与ECX比较
00401222 |.^ 7C DC |\JL SHORT CrackMe_.00401200 ; 小于就循环
00401224 |> 41 |INC ECX ; 循环变量ECX+1(下一位)
00401225 |. 3BCE |CMP ECX,ESI ; 是否到串尾
00401227 |.^ 7C D1 \JL SHORT CrackMe_.004011FA ; 没有就循环
00401229 |> 8D4424 4C LEA EAX,DWORD PTR SS:[ESP+4C] ; 以上是去重,但不完全:ECX位ECX+1位与前面有重复,则ECX+1位保留.
0040122D |. 50 PUSH EAX ; /String
0040122E |. FF15 04504000 CALL DWORD PTR DS:[<&KERNEL32.lstr>; \取处理后的用户名长度
00401234 |. 8BF0 MOV ESI,EAX
00401236 |. 83FE 06 CMP ESI,6 ; 不能小于6位
00401239 |. 0F8C CF010000 JL CrackMe_.0040140E ; 小于跳向失败
5.到这里我们知道,我们输入的用户名只能是6~15位,且只能是英文字母.程序得到用户名后要对其进行处理,处理完后的用户名长度不能小于6位.处理过程是这样的:
(1)取用户名的第i位字符(i的范围从1到用户名长度);
(2)用户名第i位前的各位依次与第i位进行比较,如果相同就删除第i位并将其后的各字符前移一位,然后继续比较(这时用户名是变化的);
(3)i加1,如果小于用户名长度(不是串尾)就回到(1)循环
去重不完全的原因是在第(2)中,当第i+1位前移到第i位后它就躲过了比较.可见当相邻N(N≥2)位相同时,若其前相同字符数≤(N-1),就会保留下一个来.如EAEEBEEECEEED经过处理的变化经过是:
EAEEBEEECEEED==>EAEBEEECEEED==>EAEBECEEED==>EAEBECD
最后的'EEE'因前面会留下3个'E'所以全部去掉了.
这样,我输的dewarj就变成了DEWARJ
0040123F |. 8D4C24 4C LEA ECX,DWORD PTR SS:[ESP+4C]
00401243 |. 8D5424 20 LEA EDX,DWORD PTR SS:[ESP+20]
00401247 |. 51 PUSH ECX ; /String2
00401248 |. 52 PUSH EDX ; |String1
00401249 |. FF15 00504000 CALL DWORD PTR DS:[<&KERNEL32.lstr>; \lstrcpyA
0040124F |. B3 41 MOV BL,41 ; A
00401251 |. 896C24 10 MOV DWORD PTR SS:[ESP+10],EBP
00401255 |. 8D7434 20 LEA ESI,DWORD PTR SS:[ESP+ESI+20]
00401259 |> 80FB 4A /CMP BL,4A ; J
0040125C |. 75 0A |JNZ SHORT CrackMe_.00401268
0040125E |. B3 49 |MOV BL,49 ; I
00401260 |. C74424 10 010>|MOV DWORD PTR SS:[ESP+10],1
00401268 |> 0FBEC3 |MOVSX EAX,BL
0040126B |. 8D4C24 20 |LEA ECX,DWORD PTR SS:[ESP+20]
0040126F |. 50 |PUSH EAX
00401270 |. 51 |PUSH ECX
00401271 |. E8 2A020000 |CALL CrackMe_.004014A0
00401276 |. 83C4 08 |ADD ESP,8
00401279 |. 85C0 |TEST EAX,EAX
0040127B |. 75 03 |JNZ SHORT CrackMe_.00401280
0040127D |. 881E |MOV BYTE PTR DS:[ESI],BL
0040127F |. 46 |INC ESI
00401280 |> 8A5424 10 |MOV DL,BYTE PTR SS:[ESP+10]
00401284 |. 896C24 10 |MOV DWORD PTR SS:[ESP+10],EBP
00401288 |. FEC2 |INC DL
0040128A |. 02DA |ADD BL,DL
0040128C |. 80FB 5B |CMP BL,5B ; ASCII(Z)=5A
0040128F |.^ 75 C8 \JNZ SHORT CrackMe_.00401259 ; 从A到Z按顺序补上用户名中没有的字符(J除外)
6.接着对用户名进行处理:从A到Z按顺序补上用户名中没有的字符(J除外),这样就得到了真正参与运算的用户名.我输入的dewarj得到的是:DEWARJBCFGHIKLMNOPQSTUVXYZ
00401291 |. 8B8C24 E00100>MOV ECX,DWORD PTR SS:[ESP+1E0]
00401298 |. 8D8424 140100>LEA EAX,DWORD PTR SS:[ESP+114]
0040129F |. 68 C9000000 PUSH 0C9 ; /Count = C9 (201.)
004012A4 |. 50 PUSH EAX ; |Buffer
004012A5 |. 68 E9030000 PUSH 3E9 ; |ControlID = 3E9 (1001.)
004012AA |. 51 PUSH ECX ; |hWnd
004012AB |. FF15 AC504000 CALL DWORD PTR DS:[<&USER32.GetDlg>; \取注册码
004012B1 |. 83F8 0E CMP EAX,0E ; 必须是14位
004012B4 |. 0F85 54010000 JNZ CrackMe_.0040140E
004012BA |. 8D9424 140100>LEA EDX,DWORD PTR SS:[ESP+114]
004012C1 |. 50 PUSH EAX
004012C2 |. 52 PUSH EDX
004012C3 |. E8 58010000 CALL CrackMe_.00401420 ; 转为大写
004012C8 |. 83C4 08 ADD ESP,8
004012CB |. 85C0 TEST EAX,EAX ; 不能是除字母以外的其它字符
004012CD |. 0F84 3B010000 JE CrackMe_.0040140E
004012D3 |. 8B5C24 10 MOV EBX,DWORD PTR SS:[ESP+10]
004012D7 |. 896C24 1C MOV DWORD PTR SS:[ESP+1C],EBP
004012DB |. 8B6C24 1C MOV EBP,DWORD PTR SS:[ESP+1C]
004012DF |. 8B7C24 1C MOV EDI,DWORD PTR SS:[ESP+1C]
004012E3 |> 8B4424 1C /MOV EAX,DWORD PTR SS:[ESP+1C] ; 取注册码的第EAX组(一组2位)
004012E7 |. 8A9404 140100>|MOV DL,BYTE PTR SS:[ESP+EAX+114] ; 第1位=>DL
004012EE |. 8A8C04 150100>|MOV CL,BYTE PTR SS:[ESP+EAX+115] ; 第2位=>CL
004012F5 |. 8DB404 150100>|LEA ESI,DWORD PTR SS:[ESP+EAX+115>; ESI=注册码第EAX组第2位的地址
004012FC |. 80FA 4A |CMP DL,4A ; 第1位与'J'比较
004012FF |. 884C24 17 |MOV BYTE PTR SS:[ESP+17],CL ; CL=>[ESP+17]
00401303 |. 75 02 |JNZ SHORT CrackMe_.00401307 ; 不等就跳(不处理)
00401305 |. B2 49 |MOV DL,49 ; 相等,J变I
00401307 |> 33C9 |XOR ECX,ECX
00401309 |. 8D7424 20 |LEA ESI,DWORD PTR SS:[ESP+20] ; ESI=处理后用户名的首址
0040130D |> 33C0 |/XOR EAX,EAX ; 循环变量清零EAX=0
0040130F |> 381406 ||/CMP BYTE PTR DS:[ESI+EAX],DL ; DL与用户名的第EAX位比较
00401312 |. 75 0A |||JNZ SHORT CrackMe_.0040131E ; 不等就跳(下一位)
00401314 |. 8B5C24 10 |||MOV EBX,DWORD PTR SS:[ESP+10] ; 相等就
00401318 |. 8BE9 |||MOV EBP,ECX ; 记下组数=>EBP
0040131A |. 894424 18 |||MOV DWORD PTR SS:[ESP+18],EAX ; 记下位数=>[ESP+18]
0040131E |> 40 |||INC EAX ; 下一位
0040131F |. 83F8 05 |||CMP EAX,5 ; 循环变量EAX与5比较
00401322 |.^ 7C EB ||\JL SHORT CrackMe_.0040130F ; 少于5就循环
00401324 |. 41 ||INC ECX ; 下一组
00401325 |. 83C6 05 ||ADD ESI,5 ; 指针ESI后移5位
00401328 |. 83F9 05 ||CMP ECX,5 ; 5组都完了没?
0040132B |.^ 7C E0 |\JL SHORT CrackMe_.0040130D ; 没有就循环
0040132D |. 33F6 |XOR ESI,ESI
0040132F |. 8D4C24 20 |LEA ECX,DWORD PTR SS:[ESP+20]
00401333 |> 33C0 |/XOR EAX,EAX ; 循环变量清零EAX=0
00401335 |> 8A5424 17 ||/MOV DL,BYTE PTR SS:[ESP+17] ; [ESP+17]=CL(注册码第EAX组第2位)=>DL
00401339 |. 3811 |||CMP BYTE PTR DS:[ECX],DL ; 与用户名的第EAX位比较
0040133B |. 75 04 |||JNZ SHORT CrackMe_.00401341 ; 不等就跳(不处理)
0040133D |. 8BFE |||MOV EDI,ESI ; 相等就:组数存EDI
0040133F |. 8BD8 |||MOV EBX,EAX ; 位数存EBX
00401341 |> 40 |||INC EAX
00401342 |. 41 |||INC ECX ; 下一位
00401343 |. 83F8 05 |||CMP EAX,5 ; 位数与5比较
00401346 |.^ 7C ED ||\JL SHORT CrackMe_.00401335 ; 小于就循环
00401348 |. 46 ||INC ESI ; 下一组
00401349 |. 83FE 05 ||CMP ESI,5 ; 组数与5比较
0040134C |.^ 7C E5 |\JL SHORT CrackMe_.00401333 ; 小于就循环
0040134E |. 3BEF |CMP EBP,EDI ; 注册码第EAX组中的两位在用户名中是否同组
00401350 |. 895C24 10 |MOV DWORD PTR SS:[ESP+10],EBX ; 注册码第2位在用户名某组中的位数=>[ESP+10]
00401354 |. 75 3A |JNZ SHORT CrackMe_.00401390 ; 不同组就跳
00401356 |. 8B4424 18 |MOV EAX,DWORD PTR SS:[ESP+18] ; 同组就:
0040135A |. 85C0 |TEST EAX,EAX ; 注册码第1位在用户名某组中的位数是不是第0位
0040135C |. 75 0A |JNZ SHORT CrackMe_.00401368 ; 不是就跳(不处理)
0040135E |. C74424 18 040>|MOV DWORD PTR SS:[ESP+18],4 ; 是就0变4
00401366 |. EB 04 |JMP SHORT CrackMe_.0040136C
00401368 |> FF4C24 18 |DEC DWORD PTR SS:[ESP+18] ; 注册码第1位在用户名某组中的位数减1
0040136C |> 85DB |TEST EBX,EBX ; 注册码第2位在用户名某组中的位数是不是第0位
0040136E |. 75 07 |JNZ SHORT CrackMe_.00401377 ; 不是就跳(不处理)
00401370 |. BB 04000000 |MOV EBX,4 ; 是就0变4
00401375 |. EB 01 |JMP SHORT CrackMe_.00401378
00401377 |> 4B |DEC EBX ; 注册码第2位在用户名某组中的位数减1
00401378 |> 8B4424 18 |MOV EAX,DWORD PTR SS:[ESP+18]
0040137C |. 8BD5 |MOV EDX,EBP
0040137E |. 895C24 10 |MOV DWORD PTR SS:[ESP+10],EBX
00401382 |. 8D0CA8 |LEA ECX,DWORD PTR DS:[EAX+EBP*4]
00401385 |. 03D1 |ADD EDX,ECX ; EDX=第1位的组数*5+第1位的位数
00401387 |. 8D0CBB |LEA ECX,DWORD PTR DS:[EBX+EDI*4] ; CL=用户名(注册码第2位所在组数,注册码第2位所在位数)
0040138A |. 8A4414 20 |MOV AL,BYTE PTR SS:[ESP+EDX+20] ; AL=用户名(注册码第1位所在组数,注册码第1位所在位数)
0040138E |. EB 46 |JMP SHORT CrackMe_.004013D6
00401390 |> 8B4C24 18 |MOV ECX,DWORD PTR SS:[ESP+18] ; 不同组就:
00401394 |. 3BCB |CMP ECX,EBX ; 注册码第EAX组中的两位在用户名中是否同位(组不同)
00401396 |. 75 30 |JNZ SHORT CrackMe_.004013C8 ; 不同就跳
00401398 |. 85ED |TEST EBP,EBP ; 相同就:第1位所在的组数是否为0
0040139A |. 75 07 |JNZ SHORT CrackMe_.004013A3
0040139C |. BD 04000000 |MOV EBP,4 ; 为0就变4
004013A1 |. EB 01 |JMP SHORT CrackMe_.004013A4
004013A3 |> 4D |DEC EBP ; 第1位组数减1
004013A4 |> 85FF |TEST EDI,EDI ; 第2位所在组数是否为0
004013A6 |. 75 07 |JNZ SHORT CrackMe_.004013AF
004013A8 |. BF 04000000 |MOV EDI,4 ; 是0就变4
004013AD |. EB 01 |JMP SHORT CrackMe_.004013B0
004013AF |> 4F |DEC EDI ; 第2位组数减1
004013B0 |> 8D04A9 |LEA EAX,DWORD PTR DS:[ECX+EBP*4]
004013B3 |. 8BCD |MOV ECX,EBP
004013B5 |. 03C8 |ADD ECX,EAX
004013B7 |. 8D14BB |LEA EDX,DWORD PTR DS:[EBX+EDI*4]
004013BA |. 8A440C 20 |MOV AL,BYTE PTR SS:[ESP+ECX+20] ; AL=用户名(注册码第1位所在组数,注册码第1位所在位数)
004013BE |. 8BCF |MOV ECX,EDI
004013C0 |. 03CA |ADD ECX,EDX
004013C2 |. 8A4C0C 20 |MOV CL,BYTE PTR SS:[ESP+ECX+20] ; CL=用户名(注册码第2位所在组数,注册码第2位所在位数)
004013C6 |. EB 16 |JMP SHORT CrackMe_.004013DE
004013C8 |> 8D14AB |LEA EDX,DWORD PTR DS:[EBX+EBP*4]
004013CB |. 8BC5 |MOV EAX,EBP
004013CD |. 03C2 |ADD EAX,EDX ; EAX=第1位组数*5+第2位的位数
004013CF |. 8D0CB9 |LEA ECX,DWORD PTR DS:[ECX+EDI*4]
004013D2 |. 8A4404 20 |MOV AL,BYTE PTR SS:[ESP+EAX+20] ; AL=用户名(注册码第1位所在组数,注册码第2位所在位数)
004013D6 |> 8BD7 |MOV EDX,EDI
004013D8 |. 03D1 |ADD EDX,ECX
004013DA |. 8A4C14 20 |MOV CL,BYTE PTR SS:[ESP+EDX+20] ; CL=用户名(注册码第2位所在组数,注册码第1位所在位数)
004013DE |> 8B5424 1C |MOV EDX,DWORD PTR SS:[ESP+1C]
004013E2 |. 3A4414 3C |CMP AL,BYTE PTR SS:[ESP+EDX+3C] ; AL与'CRACKINGFORFUN'中的相应位进行比较
004013E6 |. 75 26 |JNZ SHORT CrackMe_.0040140E ; 相等才行
004013E8 |. 3A4C14 3D |CMP CL,BYTE PTR SS:[ESP+EDX+3D] ; AL与'CRACKINGFORFUN'中的相应位进行比较
004013EC |. 75 20 |JNZ SHORT CrackMe_.0040140E ; 相等才行
004013EE |. 83C2 02 |ADD EDX,2
004013F1 |. 83FA 0E |CMP EDX,0E
004013F4 |. 895424 1C |MOV DWORD PTR SS:[ESP+1C],EDX
004013F8 |.^ 0F8C E5FEFFFF \JL CrackMe_.004012E3
004013FE |. 5F POP EDI
004013FF |. 5E POP ESI
00401400 |. 5D POP EBP
00401401 |. B8 01000000 MOV EAX,1 ; 返回1,就成功
00401406 |. 5B POP EBX
00401407 |. 81C4 CC010000 ADD ESP,1CC
0040140D |. C3 RETN
0040140E |> 5F POP EDI
0040140F |. 5E POP ESI
00401410 |. 5D POP EBP
00401411 |. 33C0 XOR EAX,EAX ; 返回0,就死
00401413 |. 5B POP EBX
00401414 |. 81C4 CC010000 ADD ESP,1CC
0040141A \. C3 RETN
7.将上面的算法整理一下:
首先,将一个14位的固定的字符串'CRACKINGFORFUN'2位一组,共分为7位,将我们输入的用户名处理后的字串5个一组,可分得5组
第0组:DEWAR
第1组:JBCFG
第2组:HIKLM
第3组:NOPQS
第4组:TUVXY
多出一个Z来,多出来的原因是程序本意是去除J的,但我们的用户名中有意加入了J,所以会多一位.加上程序去重不完全,所以如果你用户名输入的是'EAEEBEEECEEEEDJ'将会多出4位来.程序只取前25位来计算注册码,多出来的几位在正确的注册码中不会出现,并无太大的影响.
(1)将输入的注册码(共14位),2位一组,分为7组.
(2)检查每组第0位是不是J,是就用I代替,
(3)得到每组第0位和第1位在用户名中对应的组数和位数.记为第0位=(组数0,位数0),第1位=(组数1,位数1)
(4)如组数0=组数1,就得到两位新的注册码:第0位=(组数0,(位数0-1)),第1位=(组数1,(位数1-1))(如果位数0(或位数1)原来为0就变4)
(5)如位数0=位数1,就得到两位新的注册码:第0位=((组数0-1),位数0),第1位=((组数1-1),位数1)(如果组数0(或组数1)原来为0就变4)
(6)如果组数和位数都不相同,就得到两位新的注册码:第0位=(组数0,位数1),第1位=(组数1,位数0)
(7)将得到的新的注册码与固定字符串'CRACKINGFORFUN'的相应组进行逐位比较,一旦不同就GAME OVER;
(8)7组都算完了没有,没有就重复第(2)直到算完.全部符合就OK.
8.正确的注册码可由字符串'CRACKINGFORFUN'倒推回去得到:
(1)分组情况同7.
(2)得到字符串'CRACKINGFORFUN'每组中第0位和第1位在用户名中对应的组数和位数.记为第0位=(组数0,位数0),第1位=(组数1,位数1);
(3)如组数0=组数1,则两位注册码:第0位=(组数0,(位数0+1)),第1位=(组数1,(位数1+1))(如果位数0(或位数1)原来为4就变0)
(4)如位数0=位数1,则两位注册码:第0位=((组数0+1),位数0),第1位=((组数1+1),位数1)(如果组数0(或组数1)原来为4就变0)
(5)如果组数和位数都不相同,则两位注册码:第0位=(组数0,位数1),第1位=(组数1,位数0)
(6)7组都算完了没有,没有就重复第(2)直到算完.
(7)将计算得出的7组注册码按顺序连接起来就是正确的注册码了.
本例中:
固定字符串 C R A C K I N G F O R F U N
组数 1 0 0 1 2 2 3 1 1 3 0 1 4 3
位数 2 4 3 2 2 1 0 4 3 1 4 3 1 0
注册码组数 1 0 0 1 2 2 3 1 1 3 0 1 4 3
注册码位数 4 2 2 3 3 2 4 0 1 3 3 4 0 1
注 册 码 G W W F L K S J B Q A G T O
--------------------------------------------------------------------------------
【经验总结】
1.由于注册码的计算和输入的用户名有关系,所以当输入的用户名中无J时,计算出的注册码是没有问题的.
2.当输入的注册名中有J出现时,如果所计算出的注册码中的J出现在奇数位,那么它一定处于某组的第0位,会用I来代替,所以
最后比较时一定不会满足要求.此时就没有正确的注册码,必须更换注册名.
3.当输入的注册名中有J出现时,如果所计算出的注册码中的J出现在偶数位,那么它一定处于某组的第1位,计算时当正常的字
符处理,不会有任何的影响,此是所计算出的注册码就是有效的.
--------------------------------------------------------------------------------
【版权声明】: 转载请注明作者并保持文章的完整, 谢谢!
2007年01月20日 20:41:22
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课