最近网上查了查还原平坦化相关的资料。看了无名侠大佬的文章。由于本人太菜了。好多地方看的有点懵懵懂懂的。最终还是自己摸索着尝试还原平坦化。写的哪里不对。还请大佬指正。
先贴上几位大佬的文章
[ARM64 OLLVM反混淆][https://bbs.pediy.com/thread-252321.htm]
[利用符号执行去除控制流平坦化][https://security.tencent.com/index.php/blog/msg/112]
翻阅过大佬的文章后。本菜菜得到两个简单的结论。
一、fla主要功能是将if else这类的逻辑给转换成while+switch组合来处理。达到将简单的逻辑给复杂化,增加静态分析难度的目的。
二、模拟执行找出真实块之间的关联,patch修改相应的代码。直接将真实块关联起来,从而实现反混淆
接下来。我将采用先射箭后画靶的方式。来逐步的还原一个标准的ollvm fla的案例。
首先是准备我们的案例,如下是未混淆前的源码
然后是混淆的参数配置是add_definitions("-mllvm -fla -mllvm -split -mllvm -split_num=3")
贴上混淆后的样本:链接: https://pan.baidu.com/s/1Dd0T49xhsjgWrJw7PSTQFA 密码: rrem
然后贴上ida解析出来的代码
我们对比看下混淆前和混淆后的代码。原本我们只需要看if条件就知道意义的。但是现在就需要不停的顺着条件。进入case一步步的分析每一步。如果是这么简单的例子。我们还是可以静态分析看的出来真正的条件。但是代码量过于庞大时就很难跟踪分析了。
先简单说下如何先射箭后画靶,上面这个例子的逻辑比较简单。初学的情况,我们可以直接静态分析下,找出所有的真实块。然后用ida的keypatch插件来手动将真实块关联起来。再用ida解析看看结果是否和我们的混淆前的一致。结果一致后。我们就可以开始写unidbg代码来根据特征匹配真实块。特征匹配的结果可以和我们静态分析的结果对比看是否一致。最后只要unidbg实现出我们手动关联的效果,就完成了自动化反混淆了。下面我就先手动的还原。
1、找出所有真实块以及对应的汇编地址,标准的ollvm虚假块中一般只有简单的修改v6的值,其他的基本都是真实块
找出所有真实块的地址后。接着就是顺着逻辑将他们全部串联起来。这里我举两个比较典型的例子。先从函数开始的地方开始。下面简单整理下第一个真实块的关联
根据v6=910266824第一次赋值。我们锁定到下面的case分支
根据上面的代码。我们第一个真实块。应该是直接跳转到0xF5F8这个位置。而第一个跳转的位置。应该是在while循环前。先贴上第一个block的汇编代码
我们可以直接第一句就跳转到真实指令。因为这里使用的while是fla混淆的所以这些数据我们并不需要去执行。修改代码如下
这样第一个块就关联好了。然后继续关联第二个真实块0xF624。下面先列出第一个真实块的汇编
汇编的最后是跳转到了0xf778。这里就是又进入控制分发器了。我们直接让他跳转到第二个真实块。修改最后一行汇编如下
第二个真实块这里就有点特殊了。这是一个分支让我们的第三个真实块可能是0xF648位置也可能是0xF678位置。我们先看看c++的部分
那么这里我们就不能简单的b跳转哪个真实块了。这里我的预想是把这块的代码修改成如下
但是我们肯定不能修改c++的代码。所以看看这个真实块的汇编部分
这里可以看到那个v7的值就是w8。所以我们修改成判断w8=1。就跳转到0xf648。否则跳转到0xf678。下面贴上修改后的结果
再往后基本就都是这两种方式关联了。下面我就直接列一下手动修改后的跳转关联
上面的跳转修改完后。最后把让我们while循环的跳转给改成nop。
最后我们f5解析一下。基本结果就差不多了。
第一步先用unidbg跑通流程
先找找真实块和混淆块的特征,然后区分它们。可以看出来这个案例中的控制流程只要靠v6这个变量来驱动。真实块中有条件判断。有函数调用。而混淆块中只有对v6的修改。接下来看看混淆块的汇编代码是什么样的。当然不同的混淆参数也会导致特征不一样。要根据实际情况来进行调整。下面是一个混淆块
这个例子的混淆特征就是一个4行指令的block。mov、movk、str、b。由于我们看到例子中用于驱动控制流程的变量是同一个。所以str的写入地址也可以当做特征。如果无法通过混淆block的特征区分精准。就通过真实块的特征区分。比如block中有bl指令跳转到函数的是真实块。有运算符操作的是真实块。有条件判断的是真实块
能够区分出混淆块和真实块之后。最后的工作就是完成block之间的关系连接。找出所有的真实块。然后直接跳转过去
还原流程分析完后。接着列一下实现的步骤
1、将所有执行的block保存下来
2、遍历所有block。将真实的block筛选出来
3、遍历所有真实block。用b指令将他们串联起来。
下面是保存所有执行块的代码部分
这样就将所有执行的block给保存了出来。其中保存的address就是块的第一个地址。后面我们连线到真实块。就是要直接跳到第一个地址。接下来筛选出所有的真实block。这里我的筛选比较简单。能满足我这个例子。其他比较复杂需要调整下。
获取到真实块之后。我们就需要进行跳转。首先处理我们的第一种关联方式。分支的关联方式比较复杂我们放的下面再说
然后看看日志。因为我们之前已经静态分析解混淆过一次了。所以结果我们是清楚的。那么下面的结果对比我们手动的答案要不同
问题其实就是因为我们没有处理分支。只是单纯根据执行结果在跳转。那么这里我们需要判断一下csel来特殊处理一下。
需要处理的地方有两处。
1、分支的当前关联。也就是仿照我们手动关联时的cmp + b.eq + b来实现跳转两个地方
2、未执行分支的后续关联。手动关联是我们虽然不知道执行哪个分支。但是看v6的变化可以找到下一个真实块。但是自动化时。我们前面获取到的真实块。都是基于执行流程的。未执行部分的block块我们并未保存下来。所以我们要不就是一步步解析未执行分支的后续关联。要不就是直接修改分支判断的寄存器。把另一条分支的执行块也给记录下来。
我们先调整下hook部分的代码。将分支不同的执行块结果保存出来。
然后就可以在跳转的地方获取到两个跳转的真实块地址了。然后打上patch替换掉原来的三行汇编
贴上执行完成后解析的结果
对比之前我们手动还原的结果看了下。还是差了点。但是当前的执行流程的反混淆结果可以看了。未执行部分的分支如何去关联我想了好久越想越复杂了。就没有继续倒腾了。有知道的大佬麻烦提点下。
中间还有一些细节处理的地方没有贴代码,我就直接丢上地址了。代码我都尽量写上注释了。
https://github.com/dqzg12300/unidbg_tools.git
std::string calcKey(std::string data){
if
(data.length()>
10
){
data
=
"ceshi"
;
}
else
if
(data.length()>
30
){
data
=
"ollvm"
;
}
else
{
data
=
"fla"
;
}
return
data;
}
extern
"C"
JNIEXPORT jstring JNICALL
Java_com_example_ollvmdemo2_MainActivity_stringFromJNI(
JNIEnv
*
env,
jobject
/
*
this
*
/
) {
std::string hello
=
"Hello from C++"
;
hello
=
calcKey(hello);
return
env
-
>NewStringUTF(hello.c_str());
}
std::string calcKey(std::string data){
if
(data.length()>
10
){
data
=
"ceshi"
;
}
else
if
(data.length()>
30
){
data
=
"ollvm"
;
}
else
{
data
=
"fla"
;
}
return
data;
}
extern
"C"
JNIEXPORT jstring JNICALL
Java_com_example_ollvmdemo2_MainActivity_stringFromJNI(
JNIEnv
*
env,
jobject
/
*
this
*
/
) {
std::string hello
=
"Hello from C++"
;
hello
=
calcKey(hello);
return
env
-
>NewStringUTF(hello.c_str());
}
unsigned __int64 __usercall calcKey@<X0>(unsigned __int64 result@<X0>, __int64 a2@<X8>)
{
signed
int
v2;
/
/
w8
signed
int
v3;
/
/
w8
unsigned __int64 v4;
/
/
[xsp
+
10h
] [xbp
-
40h
]
__int64 v5;
/
/
[xsp
+
18h
] [xbp
-
38h
]
signed
int
v6;
/
/
[xsp
+
24h
] [xbp
-
2Ch
]
bool
v7;
/
/
[xsp
+
3Fh
] [xbp
-
11h
]
unsigned __int64 v8;
/
/
[xsp
+
40h
] [xbp
-
10h
]
bool
v9;
/
/
[xsp
+
4Fh
] [xbp
-
1h
]
v6
=
910266824
;
v5
=
a2;
v4
=
result;
while
( v6 !
=
-
1929161090
)
{
switch ( v6 )
{
case
-
1578842400
:
v6
=
56578246
;
break
;
case
-
1521928633
:
v9
=
v8 >
0x1E
;
v6
=
-
124633339
;
break
;
case
-
124633339
:
if
( v9 )
v3
=
1872828810
;
else
v3
=
1744272424
;
v6
=
v3;
break
;
case
56578246
:
v6
=
1089662144
;
break
;
case
256858465
:
if
( v7 )
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
break
;
case
910266824
:
v6
=
2056492010
;
break
;
case
1089662144
:
result
=
sub_F8E4(v5, v4);
v6
=
-
1929161090
;
break
;
case
1363893773
:
result
=
sub_F77C(v4);
v8
=
result;
v6
=
-
1521928633
;
break
;
case
1493239651
:
v6
=
1089662144
;
break
;
case
1697837166
:
v6
=
56578246
;
break
;
case
1744272424
:
result
=
sub_F830(v4,
"fla"
);
v6
=
1697837166
;
break
;
case
1872828810
:
result
=
sub_F830(v4,
"ollvm"
);
v6
=
-
1578842400
;
break
;
case
1938015978
:
result
=
sub_F830(v4,
"ceshi"
);
v6
=
1493239651
;
break
;
case
2056492010
:
result
=
sub_F77C(v4);
v7
=
result >
0xA
;
v6
=
256858465
;
break
;
}
}
return
result;
}
unsigned __int64 __usercall calcKey@<X0>(unsigned __int64 result@<X0>, __int64 a2@<X8>)
{
signed
int
v2;
/
/
w8
signed
int
v3;
/
/
w8
unsigned __int64 v4;
/
/
[xsp
+
10h
] [xbp
-
40h
]
__int64 v5;
/
/
[xsp
+
18h
] [xbp
-
38h
]
signed
int
v6;
/
/
[xsp
+
24h
] [xbp
-
2Ch
]
bool
v7;
/
/
[xsp
+
3Fh
] [xbp
-
11h
]
unsigned __int64 v8;
/
/
[xsp
+
40h
] [xbp
-
10h
]
bool
v9;
/
/
[xsp
+
4Fh
] [xbp
-
1h
]
v6
=
910266824
;
v5
=
a2;
v4
=
result;
while
( v6 !
=
-
1929161090
)
{
switch ( v6 )
{
case
-
1578842400
:
v6
=
56578246
;
break
;
case
-
1521928633
:
v9
=
v8 >
0x1E
;
v6
=
-
124633339
;
break
;
case
-
124633339
:
if
( v9 )
v3
=
1872828810
;
else
v3
=
1744272424
;
v6
=
v3;
break
;
case
56578246
:
v6
=
1089662144
;
break
;
case
256858465
:
if
( v7 )
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
break
;
case
910266824
:
v6
=
2056492010
;
break
;
case
1089662144
:
result
=
sub_F8E4(v5, v4);
v6
=
-
1929161090
;
break
;
case
1363893773
:
result
=
sub_F77C(v4);
v8
=
result;
v6
=
-
1521928633
;
break
;
case
1493239651
:
v6
=
1089662144
;
break
;
case
1697837166
:
v6
=
56578246
;
break
;
case
1744272424
:
result
=
sub_F830(v4,
"fla"
);
v6
=
1697837166
;
break
;
case
1872828810
:
result
=
sub_F830(v4,
"ollvm"
);
v6
=
-
1578842400
;
break
;
case
1938015978
:
result
=
sub_F830(v4,
"ceshi"
);
v6
=
1493239651
;
break
;
case
2056492010
:
result
=
sub_F77C(v4);
v7
=
result >
0xA
;
v6
=
256858465
;
break
;
}
}
return
result;
}
case
-
1521928633
:
/
/
0xF694
v9
=
v8 >
0x1E
;
v6
=
-
124633339
;
break
;
case
-
124633339
:
/
/
0xF6BC
if
( v9 )
v3
=
1872828810
;
else
v3
=
1744272424
;
v6
=
v3;
break
;
case
256858465
:
/
/
0xF624
if
( v7 )
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
break
;
case
1089662144
:
/
/
0xF750
result
=
sub_F8E4(v5, v4);
v6
=
-
1929161090
;
break
;
case
1363893773
:
/
/
0xF678
result
=
sub_F77C(v4);
v8
=
result;
v6
=
-
1521928633
;
break
;
case
1744272424
:
/
/
0xF710
result
=
sub_F830(v4,
"fla"
);
v6
=
1697837166
;
break
;
case
1872828810
:
/
/
0xF6E0
result
=
sub_F830(v4,
"ollvm"
);
v6
=
-
1578842400
;
break
;
case
1938015978
:
/
/
0xF648
result
=
sub_F830(v4,
"ceshi"
);
v6
=
1493239651
;
break
;
case
2056492010
:
/
/
0xF5F8
result
=
sub_F77C(v4);
v7
=
result >
0xA
;
v6
=
256858465
;
break
;
case
-
1521928633
:
/
/
0xF694
v9
=
v8 >
0x1E
;
v6
=
-
124633339
;
break
;
case
-
124633339
:
/
/
0xF6BC
if
( v9 )
v3
=
1872828810
;
else
v3
=
1744272424
;
v6
=
v3;
break
;
case
256858465
:
/
/
0xF624
if
( v7 )
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
break
;
case
1089662144
:
/
/
0xF750
result
=
sub_F8E4(v5, v4);
v6
=
-
1929161090
;
break
;
case
1363893773
:
/
/
0xF678
result
=
sub_F77C(v4);
v8
=
result;
v6
=
-
1521928633
;
break
;
case
1744272424
:
/
/
0xF710
result
=
sub_F830(v4,
"fla"
);
v6
=
1697837166
;
break
;
case
1872828810
:
/
/
0xF6E0
result
=
sub_F830(v4,
"ollvm"
);
v6
=
-
1578842400
;
break
;
case
1938015978
:
/
/
0xF648
result
=
sub_F830(v4,
"ceshi"
);
v6
=
1493239651
;
break
;
case
2056492010
:
/
/
0xF5F8
result
=
sub_F77C(v4);
v7
=
result >
0xA
;
v6
=
256858465
;
break
;
unsigned __int64 __usercall calcKey@<X0>(unsigned __int64 result@<X0>, __int64 a2@<X8>)
{
v6
=
910266824
;
v5
=
a2;
v4
=
result;
while
( v6 !
=
-
1929161090
)
{
switch ( v6 )
{
....
case
910266824
:
v6
=
2056492010
;
break
;
....
case
2056492010
:
/
/
0xF5F8
第一个真实块
result
=
sub_F77C(v4);
v7
=
result >
0xA
;
v6
=
256858465
;
break
;
....
case
256858465
:
/
/
0xF624
第二个真实块
if
( v7 )
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
}
}
return
result;
}
unsigned __int64 __usercall calcKey@<X0>(unsigned __int64 result@<X0>, __int64 a2@<X8>)
{
v6
=
910266824
;
v5
=
a2;
v4
=
result;
while
( v6 !
=
-
1929161090
)
{
switch ( v6 )
{
....
case
910266824
:
v6
=
2056492010
;
break
;
....
case
2056492010
:
/
/
0xF5F8
第一个真实块
result
=
sub_F77C(v4);
v7
=
result >
0xA
;
v6
=
256858465
;
break
;
....
case
256858465
:
/
/
0xF624
第二个真实块
if
( v7 )
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
}
}
return
result;
}
.text:
000000000000F470
loc_F470
.text:
000000000000F470
LDR W8, [SP,
.text:
000000000000F474
MOV W9,
.text:
000000000000F478
MOVK W9,
.text:
000000000000F47C
CMP
W8, W9
.text:
000000000000F480
STR
W8, [SP,
.text:
000000000000F484
B.EQ loc_F76C
.text:
000000000000F488
B loc_F48C
.text:
000000000000F470
loc_F470
.text:
000000000000F470
LDR W8, [SP,
.text:
000000000000F474
MOV W9,
.text:
000000000000F478
MOVK W9,
.text:
000000000000F47C
CMP
W8, W9
.text:
000000000000F480
STR
W8, [SP,
.text:
000000000000F484
B.EQ loc_F76C
.text:
000000000000F488
B loc_F48C
.text:
000000000000F470
loc_F470
.text:
000000000000F470
LDR W8, [SP,
.text:
000000000000F474
B loc_F5F8 ; Keypatch modified this
from
:
.text:
000000000000F474
; MOV W9,
.text:
000000000000F478
;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
.text:
000000000000F478
MOVK W9,
.text:
000000000000F47C
CMP
W8, W9
.text:
000000000000F480
STR
W8, [SP,
.text:
000000000000F484
B.EQ loc_F76C
.text:
000000000000F488
B loc_F48C
.text:
000000000000F470
loc_F470
.text:
000000000000F470
LDR W8, [SP,
.text:
000000000000F474
B loc_F5F8 ; Keypatch modified this
from
:
.text:
000000000000F474
; MOV W9,
.text:
000000000000F478
;
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
.text:
000000000000F478
MOVK W9,
.text:
000000000000F47C
CMP
W8, W9
.text:
000000000000F480
STR
W8, [SP,
.text:
000000000000F484
B.EQ loc_F76C
.text:
000000000000F488
B loc_F48C
.text:
000000000000F5F8
loc_F5F8
.text:
000000000000F5F8
.text:
000000000000F5F8
LDR X0, [SP,
.text:
000000000000F5FC
BL sub_F77C
.text:
000000000000F600
CMP
X0,
.text:
000000000000F604
CSET W8, HI
.text:
000000000000F608
MOV W9,
.text:
000000000000F60C
AND W8, W8, W9
.text:
000000000000F610
STURB W8, [X29,
.text:
000000000000F614
MOV W8,
.text:
000000000000F618
MOVK W8,
.text:
000000000000F61C
STR
W8, [SP,
.text:
000000000000F620
B loc_F778
.text:
000000000000F5F8
loc_F5F8
.text:
000000000000F5F8
.text:
000000000000F5F8
LDR X0, [SP,
.text:
000000000000F5FC
BL sub_F77C
.text:
000000000000F600
CMP
X0,
.text:
000000000000F604
CSET W8, HI
.text:
000000000000F608
MOV W9,
.text:
000000000000F60C
AND W8, W8, W9
.text:
000000000000F610
STURB W8, [X29,
.text:
000000000000F614
MOV W8,
.text:
000000000000F618
MOVK W8,
.text:
000000000000F61C
STR
W8, [SP,
.text:
000000000000F620
B loc_F778
.text:
000000000000F618
MOVK W8,
.text:
000000000000F61C
STR
W8, [SP,
.text:
000000000000F620
B loc_F624 ; Keypatch modified this
from
:
.text:
000000000000F620
; B loc_F778
.text:
000000000000F618
MOVK W8,
.text:
000000000000F61C
STR
W8, [SP,
.text:
000000000000F620
B loc_F624 ; Keypatch modified this
from
:
.text:
000000000000F620
; B loc_F778
case
256858465
:
if
( v7 )
/
/
根据条件让第三个真实块不固定了
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
case
256858465
:
if
( v7 )
/
/
根据条件让第三个真实块不固定了
v2
=
1938015978
;
else
v2
=
1363893773
;
v6
=
v2;
case
256858465
:
if
( v7 )
/
/
根据条件让第三个真实块不固定了
/
/
直接跳转到
0xF648
else
/
/
直接跳转到
0xF678
v6
=
v2;
case
256858465
:
if
( v7 )
/
/
根据条件让第三个真实块不固定了
/
/
直接跳转到
0xF648
else
/
/
直接跳转到
0xF678
v6
=
v2;
.text:
000000000000F624
loc_F624
.text:
000000000000F624
.text:
000000000000F624
LDURB W8, [X29,
.text:
000000000000F628
MOV W9,
.text:
000000000000F62C
MOVK W9,
.text:
000000000000F630
MOV W10,
.text:
000000000000F634
MOVK W10,
.text:
000000000000F638
TST W8,
.text:
000000000000F63C
CSEL W8, W9, W10, NE
.text:
000000000000F640
STR
W8, [SP,
.text:
000000000000F644
B loc_F778
.text:
000000000000F624
loc_F624
.text:
000000000000F624
.text:
000000000000F624
LDURB W8, [X29,
.text:
000000000000F628
MOV W9,
.text:
000000000000F62C
MOVK W9,
.text:
000000000000F630
MOV W10,
.text:
000000000000F634
MOVK W10,
.text:
000000000000F638
TST W8,
.text:
000000000000F63C
CSEL W8, W9, W10, NE
.text:
000000000000F640
STR
W8, [SP,
.text:
000000000000F644
B loc_F778
.text:
000000000000F624
loc_F624
.text:
000000000000F624
.text:
000000000000F624
LDURB W8, [X29,
.text:
000000000000F628
MOV W9,
.text:
000000000000F62C
MOVK W9,
.text:
000000000000F630
MOV W10,
.text:
000000000000F634
MOVK W10,
.text:
000000000000F638
CMP
W8,
.text:
000000000000F638
; TST W8,
.text:
000000000000F63C
B.EQ loc_F648 ; Keypatch modified this
from
:
.text:
000000000000F63C
; CSEL W8, W9, W10, NE
.text:
000000000000F640
B loc_F678 ; Keypatch modified this
from
:
.text:
000000000000F640
;
STR
W8, [SP,
.text:
000000000000F624
loc_F624
.text:
000000000000F624
.text:
000000000000F624
LDURB W8, [X29,
.text:
000000000000F628
MOV W9,
.text:
000000000000F62C
MOVK W9,
.text:
000000000000F630
MOV W10,
.text:
000000000000F634
MOVK W10,
.text:
000000000000F638
CMP
W8,
.text:
000000000000F638
; TST W8,
.text:
000000000000F63C
B.EQ loc_F648 ; Keypatch modified this
from
:
.text:
000000000000F63C
; CSEL W8, W9, W10, NE
.text:
000000000000F640
B loc_F678 ; Keypatch modified this
from
:
.text:
000000000000F640
;
STR
W8, [SP,
0xF474
-
-
-
>
0xF5F8
0xF620
-
-
-
>
0xF624
0xF63C
-
-
-
>
0xF648
0xF640
-
-
-
>
0xF678
0xF664
-
-
-
>
0xF750
0xF690
-
-
-
>
0xF694
0xF6B8
-
-
-
>
0xF6BC
0xF6D4
-
-
-
>
0xF6E0
0xF6D8
-
-
-
>
0xF710
0xF6FC
-
-
-
>
0xF750
0xF474
-
-
-
>
0xF5F8
0xF620
-
-
-
>
0xF624
0xF63C
-
-
-
>
0xF648
0xF640
-
-
-
>
0xF678
0xF664
-
-
-
>
0xF750
0xF690
-
-
-
>
0xF694
0xF6B8
-
-
-
>
0xF6BC
0xF6D4
-
-
-
>
0xF6E0
0xF6D8
-
-
-
>
0xF710
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课