20199121《网络攻防实践》综合实践
1、论文介绍
- Title
- Angora: Efficient Fuzzing by Principled Search
-
Source
- IEEE Symposium on Security and Privacy,2018
-
Authors
-
Peng Chen ;ShanghaiTech University ; chenpeng@shanghaitech.edu.cn
-
Hao Chen ; University of California, Davis ; chen@ucdavis.edu
-
Fuzzing是一种流行的发现软件错误的技术。
然而,最先进的fuzzer的性能还有很多需要改进的地方。基于符号执行的fuzzer产生高质量的输入,但运行速度慢;而基于随机变异的fuzzer运行速度快,但难以产生高质量的输入。该论文提出了一种新的基于变异的Angora,它的性能远远优于最新的fuzzer。
Angora的主要目标是通过解决路径约束而不需要符号执行来增加分支覆盖率。为了有效地解决路径约束问题,论文引入了几个关键技术:可扩展的字节级污点跟踪、上下文相关的分支计数、基于梯度下降的搜索和输入长度探索。
2、基础知识
-
软件脆弱性研究中主要有三种形式:模糊测试、污点分析、符号执行。
-
模糊测试(Fuzzing)
一般来说,模糊测试通过生成大量的随机测试用例,并以这些测试用例为输入执行被测程序,希望能到导致程序异常或崩溃,从而捕捉到导致程序异常或崩溃的错误或安全漏洞。模糊测试之所以能够受到软件测试业界的青睐,是因为它具有以下的优点:1)模糊测试可以针对任意输入的程序,可在程序源代码或可执行字节码上进行;2)模糊测试针对实际可执行的被测程序,不会出现静态测试技术中的误报问题;3)模糊测试不需要进行大量的准备工作,只需要提供被测程序及其初始文件或符合规范的输入,便可进行模糊测试用例生成,对软件进行安全漏洞检测;4)模糊测试易于自动化实现。在模糊测试技术的众多优点中,易于自动化实现是其能够被人们广泛关注的主要优点之一。
-
符号执行
符号执行是一种信息流分析技术,它在程序执行过程中以符号输入代替实际输入,将程序变量符号化,并在分析中通过插桩(Instrumenation)不断收集路径约束条件(Path Condition),通过约束求解器(Solver)生成测试用例以发现软件存在的脆弱性。
- 插桩是指通过注入插桩代码,来分析二进制应用程序在运行时的行为的方法。
符号执行的最大问题是,由于软件分支数目和循环次数巨大,存在着天文数字的执行路径,导致符号执行在实际应用中具有潜在的路径爆炸问题,这已经成为符号执行应用的最大瓶颈。
-
污点分析
污点分析是检测蠕虫攻击和自动提取行为特征的有效方法。该方法将一切来自于非信任源的数据标记为“污染”,对“污染”数据进行追踪,所有对“污染”数据的运算操作结果均会标记为“污染操作”,然后根据各种安全策略对“污染操作”进行分析,凡违反安全策略的“污染操作”都会发出警报,以此达到发现软件脆弱性的目的。
-
-
Fuzzing技术分类(按测试用例来分)
-
基于随机变异(Mutation-based)
是将一个(或一组)正常的符合规范(或协议)的初始输入文件作为初始种子(seed),通过对种子进行随机变异,生成大量的测试用例,对软件进行安全漏洞检测,是目前广泛使用的一种模糊测试技术。但基于变异的模糊测试用例生成对种子的依赖性较强,不同的初始种子,其安全漏洞检测效果也大不一样。因此,如何选取合适的种子,进行基于变异的模糊测试用例生成,是提高模糊测试技术安全漏洞检测能力的一个关键问题。
-
基于模板(Generation-based)
不需要种子文件,依赖于安全人员结合自己的知识,给出输入数据的模板,构造丰富的输入测试数据。
-
-
覆盖率
If( a > 2) a=2; if (b > 2) b=2; else a=3;b=4;
- 路径覆盖( 覆盖程序中所有可能的路径 ): 4个数据集(a=3,b=3 ; a=1, b=3 ; a=3,b=2 ; a=1,b=2)
- 分支覆盖( 使得程序中每个判断的取真分支和取假分支至少经历一次 ): 4个数据集(a=3,b=3 ; a=1, b=3 ; a=3,b=2 ; a=1,b=2)
- 代码行覆盖: 2个数据集(a=3,b=3 ; a=3,b=2)
-
AFL介绍
灰盒模糊测试介于白盒模糊测试和黑盒模糊测试之间,是一种针对程序可执行代码进行的模糊测试方法。灰盒模糊测试是基于二进制插桩而不是源代码分析上。基于覆盖的灰盒模糊测试(Coverage-based grey-box fuzzing)试图在生成大量随机测试用例的生成过程中,更有效地进行路径探索,增加代码覆盖率,因此,基于覆盖的灰盒模糊测试已成为目前检测软件安全漏洞的一种有效测试方法。
AInerican Fuzzy Lop (AFL) 是目前使用广泛的一种基于覆盖的灰盒模糊测试工具。它通过对被测程序的可执行代码进行插桩,跟踪记录测试用例的覆盖情况;以输入(种子)的二进制字节、双字节、四字节为单位,进行随机测试用例的生成,以覆盖新基本块为指导,进行种子队列的更新,生成大量的随机测试用例,对被测程序进行测试,从而捕捉到导致程序异常或崩溃的错误或安全漏洞。但AFL难以发现隐藏在被测程序循环或嵌套条件语句深处的错误和安全漏洞。
3、论文创新点及结果
论文创新点
AFL和其他类似的fuzzer使用分支覆盖作为度量。不同的是,Angora通过解决路径约束而不使用符号执行来探索程序的状态。Angora跟踪未探明的分支,并试图解决这些分支的路径限制。 有效地解决路径约束的技术如下:
-
1)用上下文敏感的方式进行分支覆盖
AFL所使用的上下文不敏感的分支处理,不能识别相同分支潜在的不同内部状态。相较于AFL只将分支的始块和终块作为分支特征,作者在此基础上又加入了上下文特征。
如下图所示,f中的x参数是输入的一部分,而trigger所控制的分支,受 "是否第一次调用f" 这个上下文环境影响。该分支是一种内部状态。在第一次运行期间,程序接受输入
10
。当它在第19行调用 \(f()\) 时,它在第4行执行\(true\)分支。稍后,当它在第21行调用 \(f()\) 时,它在第10行执行\(false\)分支。由于AFL对分支的定义是上下文不敏感的,它认为两个分支都已执行。后来,当程序接受一个新的输入01
时,AFL认为这个输入不会触发新的内部状态,因为第4行和第10行的分支都是在前一次运行中执行的。但事实上,这个新输入触发了一个新的内部状态,因为当输入input[2]=1
时,它将导致第6行崩溃。Angora用一个三元组\((l_{prev},l_{cur},context)\)定义一个分支,其中 \(context\) 是\(h(stack)\),\(stack\)包含了调用栈的状态。利用对栈进行 \(hash\) 的方法,来获取上下文。为了避免产生过多的独立分支,作者使用的hash函数会异或调用栈。即 \(h(stack)=⊕cs∈stackID(cs)\)
-
2)字节级的污点追踪
污点跟踪的代价是昂贵的,特别是跟踪的字节不是相互独立的情况下,所以AFL没有使用这个。
在污点追踪中,作者使用某一字节在输入中的偏移作为taint lable,记为 \(t_x\) ,将 \(t_x\) 与变量 \(x\) 关联。taint lable一个简单的实现是用一个 bit vector,为了减小taint lable的存储空间,该方案维护一个表,这个表的索引是taint lable,这种情况下的数据结构在面对UNION操作时代价很大。
于是作者又提出了新的数据结构,数据结构包含两个组件:
- 一个二叉,保存 bit vector 到 label 的映射关系。每个 bit vector 表示为树上的一个结点 \(v_b\),层次为 \(∣b∣\),\(∣b∣\)为 \(b\) 的长度。\(v_b\) 保存 \(b\) 的 label。
- 查询表,记录从 label 到 bit vector 的映射
然后作者又分析了空间复杂度,确实减少了内存占用。
通过这种污点分析,可以得到输入的使用情况,继而判断各个字节作为变量的长度,以便进一步的进行数据突变等操作。
-
3)使用梯度下降来求解条件语句
字节级别的污点跟踪输入中的哪些字节传入条件表达式中进行计算。但是如何变异这些输入来探索未探索到的区域? 目前很多fuzzer都是随机地变异输入或使用粗糙的启发式方法。 如果使用符号执行的方法,成本太高。
作者把探索区域的问题视为搜索问题,选择使用梯度下降的求解的方法来获得满足条件语句的值。假设有一个黑盒函数\(f(x)\),其中\(x\)是一个值的向量。对于\(f(x)\)有三种约束:
- \(f(x)<0\)
- \(f(x)≤0\)
- \(f(x)==0\)
然后一些比较就可以转换为上述三种约束了,如下表,作者将条件判断语句转换成误差函数,而后利用梯度下降的方法求解误差函数,进一步的可实现对条件语句的满足。
正好本学期金鑫老师的机器学习课程讲解了梯度下降算法,所以这部分理解起来不太困难。在机器学习中,梯度下降常常会陷入局部最优, 但是在fuzzing中不存在这个问题。 若一个约束是\(f(x)< 0\),我们只需要找到满足这个约束的 \(x\) 就行了,而不需要找到 \(f(x)\) 的全局最小值。
在神经网络中,求偏导可以得到\(f(x)\)的解析形式,但是在fuzzing时\(f(x)\)是黑盒的。对于这个问题采取使用数值近似的方法:
对于这个问题采取使用数值近似的方法:
\(\frac{\partial f(x)}{\partial x_i}=\frac{f(x+\delta v_i)-f(x)}{\delta}\) 其中\(v_i\) 是第 \(i\) 维的单元向量理论上梯度下降可以解决任何约束,在实际中,梯度下降的速度依赖于数学函数的复杂度:
- 如果\(f(x)\)是单调或者凸函数,梯度下降可以很快的找到解,即使\(f(x)\)是一个复杂的解析形式
- 若局部最小值满足约束,那么找到解也是很快的
- 若局部最优找不到解,
Angora
会随机采样到其他的\(x′\),然后重新进行梯度下降来找到另一个满足约束的局部最优
下图算法5即搜索算法,每次迭代从输入 \(x\) 开始,然后计算\(f(x)\)在\(x\)处的梯度\(\nabla_x f(x)\) ,然后进行梯度下降操作,\(x\leftarrow x - \epsilon\nabla_x f(x)\),其中\(\epsilon\)是学习率。
-
4)变量大小与类型判断
\(x\) 是输入中值的向量。
我们可以把\(x\)中的每一个字节作为一个元素。但是在梯度下降时,由于类型不匹配会产生问题。
假设四个连续的字节序列 \(b_3b_2b_1b_0\) 是一个整数,\(x_i\)表示一个整数值。当计算\(f(x+δvi)\)时,我们应该\(\delta\) 到整数中。但是这样简单地把 \(x\) 中的每个字节 \(b_i\) 看做一个值,把其中某个字节增加后再拼接起来,在求偏导数就会出现问题。
为了抑制这个问题,需要确定:
- 输入中的哪些字节常作为一个值来使用
- 值的类型是什么
作者把第一个问题称为shape inference,第二个问题称为type inference,并在动态污点分析时解决他们。
对于shape inference,初始化输入中的所有字节都是独立的。在污点分析期间,当插桩读到字节序列符合某些原始的大小(1,2,4,8字节等),
Angora
就会把这些字节标记为同一个值。当产生冲突时,Angora
选择匹配到的最小的size。对于type inference,Angora
依赖于在这些值上的操作的插桩。如果在一个有符号整数上进行插桩操作,
Angora
就会把两个操作数视为有符号整数。若一个值同时作为有符号和无符号整数,则把他视为无符号类型。如果Angora
没有成功推导值的精确的大小和类型,梯度下降并不会阻碍它找到一个结果,只是搜索时间会更长。变量在污点分析时已经判断过大小了,而类型(是否带符号)可以通过语义判断。当明确了变量的大小和类型后,基于条件语句构造的误差函数中变量的定义会更准确。
-
5)探测输入的长度
输入太短没有效果,太大会爆内存。因此需要得出合适的输入长度。作者认为只有当增加长度能得到新分支时,才增加长度,也就是使得读取长度尽可能满足程序的需要。
具体做法是,在污点跟踪时,
Angora
把read相关函数调用的目标内存地址和相关的字节偏移相关联,同时也记录read调用的返回值,如果返回值在条件语句中使用且约束不满足,则Angora
增加输入长度。
结果
在和其他fuzzer的比较中,fuzzer运行使用CPU核心数这一变量被统一限定为单核(注:作者提到Angora
支持多核,但是实验中未作改变核心数的纵向比较)。每种实验做5次,取平均结果。
-
1)通过LAVA语料与其他fuzzer比较 (能力与效率)
LAVA技术可以用来在源码中生成现实bug,LAVA-M语料集在每个程序里添加了许多bug。LAVA-M包含了四种 GNU coretutils程序:
uniq
,base64
,md5sum
,who
。作者使用这些语料来做和其他程序做对比测试。
结果如Table 1,Angora
对于发现bug的能力明显优于现有fuzzer,甚至发现了一些在LAVA作者预期内,但是没做处理的BUG。对于
Angora
明显强于较好的其他fuzzer(VUzzer
,Steelix
)的原因,作者认为有两点:- 追踪了输入字节的偏移
- 对条件表达式做了有力的计算
通过Figure 4我们可以看出
Angora
的时间效率,本图测试样本为who,在前5分钟就可以发现近1000个bug,虽然之后发现bug的速率下降,但是45分钟的运行已经能让他发现超过1500个bug。 -
2)通过未修改的现实软件测试Angora
作者选用
AFL
和Angora
做对比,由于测试的是现实软件,bug较少,所以作者除了测试发现新bug,还对运行覆盖情况做了测试。其中对新bug发现能力的检测,以触发unique crash的数量为标准。
分别经过5小时的运行,Angora
发现的unique crash数多倍于AFL。同时,通过表5我们可以看出,Angora
在代码覆盖率上对AFL有所改进,在jhead做样本时的实验中Angora
在行覆盖和分支覆盖上,分别有127.4%和144.0%的增长。
-
3)上下文敏感的分支计数
- 效果
在前文中提到,作者认为,如果同一分支能以来源于不同的调用者区分开来(改分支计数部分),那么fuzzer可以找到更多bug。为了验证这一假设,作者分别使用是否带有上下文敏感机制的Angora
,来测试file程序。
结果是带有上下文敏感的Angora
在代码覆盖上不明显强于非上下文敏感,但是带有上下文敏感的Angora
可以发现6个unique crash,而不带有的不能。 - hash碰撞
Angora
和AFL
都使用了hash表来存储分支,增加上下文敏感机制使得unique分支的数量至多是不带有上下文敏感机制时的8倍左右。因此,作者使用了16倍于AFL
的hash表大小的hash表。尽管Angora
的hash表大小与cache大小不适配,他的查找机制使得这种不适配的性能影响得到减弱。
- 效果
-
4)基于梯度下降的搜索算法
Angora
对限制条件进行梯度下降求解比随机突变和magic bytes突变的方法能求解更多的条件表达式,如Table 8所示。 此外,没使用符号执行,使得在fuzz大程序上更有效率 。 -
5)输入长度探测
Angora
对比AFL
的随机长度机制,以更少的增长次数等到更多的有用路径,同时,更短的平均执行长度,使得Angora
运行得更快。 -
6)执行速度
相较于
AFL
,如果没有taint机制,Angora
与AFL
的插桩机制拥有相同的运行速度。从Table 10 可以看出,在taint机制的影响下,Angora
的运行效率比AFL
稍低,相同时间处理的输入较少。不过结合之前的实验,在fuzzer的执行结果方面,Angora
效率更高。
4、复现
1)Angora的安装
-
下载 Angora ,其路径为
/home/zhangtuoning/ztn/Angora
,其百度云链接如下。git clone https://github.com/AngoraFuzzer/Angora
链接:https://pan.baidu.com/s/1y9D7aS9YW4Y4_z2m811uMw
提取码:951g -
安装cmake、cargo,命令
apt-get install cmake
、apt-get install cargo
。 -
安装LLVM,在Angora目录下新建文件夹llvm,运行:
PREFIX=/home/zhangtuoning/ztn/Angora/llvm ./build/install_llvm.sh
由于网络原因可能会一直卡在 wget下载llvm的压缩包的地方,可以点击该链接手动下载,将下载后的压缩包放在llvm文件夹下,再将install_llvm.sh中的wget那一行语句删除。llvm的压缩包百度云链接如下。
链接:https://pan.baidu.com/s/1FPyo8dzKsvG2BjBYL77F9A
提取码:cjw1按照他的提示添加环境变量,命令如下:
#export PATH=/home/zhangtuoning/ztn/Angora/llvm/clang+llvm/bin:$PATH #export LD_LIBRARY_PATH=/home/zhangtuoning/ztn/Angora/llvm/clang+llvm/lib:$LD_LIBRARY_PATH
附tar.xz解压方法
# xz -d XXX.tar.xz # tar -xvf XXX.tar
-
安装Rust
参考 https://rustup.rs/ 给出的命令
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
,不能一步到位的话参考 https://blog.csdn.net/sinat_37954989/article/details/82913413 中的方法手动安装。 -
安装Angora,在Angora目录下运行
./build/build.sh
即可。 -
CPU设置,
echo core | sudo tee /proc/sys/kernel/core_pattern
-
测试Angora,在Angora/tests/目录下运行,
./test.sh mini
。
2)LAVA-M测试集的安装
-
LAVA是由Brendan Dolan-Gavitt等人提出的用于在程序中插入bug的技术方法,其相关论文《LAVA: Large-scale Automated Vulnerability Addition》发表在了2016年的S&P上。
通过LAVA在uniq、who、md5sum、base64四个程序上进行bug插入而形成的测试集即为LAVA-M。
LAVA-M被广泛应用于fuzz领域的工具效果评估。
-
LAVA-M下载地址 http://panda.moyix.net/~moyix/lava_corpus.tar.xz ,百度云链接如下。
链接:https://pan.baidu.com/s/12QsrCUYY7R43Nrb-2qx2iA
提取码:636z -
以base64为例介绍安装方法,进入base64文件夹,
./validate.sh
使用脚本进行安装。如果显示
Validated 0/44bugs
,则可能的解决办法为:-
缺少了libacl,通过命令
sudo apt-get install libacl1-dev
安装即可。 -
validate.sh中的 --prefix要改为绝对路径,
/你的路径/lava_corpus/LAVA-M/base64
,原脚本中提供的命令为./configure --prefix=`pwd`/lava-install LIBS="-lacl" &> /dev/null
-
3)使用AFL对LAVA-M及进行测试
-
LAVA-M安装时为自动化脚本安装方法,安装后的base64默认为gcc编译,为了使用AFL系列工具对LAVA-M进行测试,我们需要将编译选项进行调整,以生成被afl-gcc插桩后的base64。 设置环境如下。
export CC=afl-gcc export CXX=afl-g++
再运行脚本
./validate.sh
,插入bug数量仍为44/44,即安装成功。 -
生成被afl-gcc插桩后的base64,之后即可通过AFL系列工具进行测试。
4)使用Angora对LAVA-M及进行测试
Angora的GitHub主页对LAVA-M的安装及测试方法进行介绍 ,其中主要介绍了如何使用gllvm对LAVA-M进行编译。 接下来使用angora-clang进行编译安装:
-
在coreutils-8.24-lava-safe目录下,使用angora-clang进行编译:
CC=/home/zhangtuoning/ztn/Angora/bin/angora-clang CXX=/home/zhangtuoning/ztn/Angora/bin/angora-clang++ LD=/home/zhangtuoning/ztn/Angora/bin/angora-clang ./configure --prefix=pwd/lava-install LIBS="-lacl"
此处若报错:
根据提示修改环境变量
export FORCE_UNSAFE_CONFIGURE=1
, 一堆checking通过后生成Makefile:
-
依次使用TRACK模式和FAST进行编译安装,先生成TRACK模式下插桩编译的base64,复制为
base64.taint
。 :USE_TRACK=1 make -j make install
再使用FAST模式进行编译,将FAST模式下生成的base64复制为base64.fast。注意,base64.fast是直接可以通过输入input进行crash的插入校验。 :
make clean USE_FAST=1 make -j make install
-
最后即可进行测试:
angora-fuzzer -i input -o output -t base64.taint -- base64.fast -d @@
Table8 即是进行上述操作得到的结果,只不过这里只以base64为例。
5)使用Angora对GNU Binutils 2.32进行测试
与LAVA-M的操作类似,GNU Binutils 2.32是常用的评估漏洞挖掘工具效率的Benchmark软件。
-
通过./configure配置Make所需环境变量 , 在Binutils 2.32所在目录下运行:
CC=/home/zhangtuoning/ztn/Angora/bin/angora-clang CXX=/home/zhangtuoning/ztn/Angora/bin/angora-clang++ LD=/home/zhangtuoning/ztn/Angora/bin/angora-clang PREFIX=/home/zhangtuoning/ztn/binutils-2.32 ./configure --disable-shared
成功配置并生成Makefile文件:
-
对Binutils进行污点跟踪编译, 在Binutils 2.32所在目录下运行:
USE_TRACK=1 make -j make install
编译安装完成后,在binutils目录下生成了被污点跟踪技术插桩编译产生的可执行文件objdump等。创建target目录,在/home/XXX/目录下建立文件夹/target,在/home/XXX/target/目录下分别建立文件夹/fast及/taint,将binutils目录下的可执行文件objdump、readelf等复制到/root/target/taint/目录下,并重命名为objdump.taint、readelf.taint等。
-
对Binutils进行快速编译, 在Binutils 2.32所在目录下运行:
make clean USE_FAST=1 make -j make install
编译安装完成后,在binutils目录下生成了快速编译产生的可执行文件objdump等,与上一步类似,将binutils目录下的可执行文件objdump、readelf等复制到/home/XXX/target/fast/目录下,并重命名为objdump.fast、readelf.fast等。
-
对objdump -d进行测试,在/home/XXX/目录下建立种子文件夹/input/,放入ELF可执行文件,作为种子对objdump进行测试,这里选取AFL提供的ELF文件格式的初始种子。
在Angora目录下运行:
./angora_fuzzer -I /home/zhangtuoning/input -o /home/zhangtuoning/output -t /home/zhangtuoning/target/taint/objdump.taint -- /home/zhangtuoning/target/fast/objdump.fast -d @@
6、实验思考
近年来,学术界对Fuzzer技术有较为密切的关注。 因为本文是出自18年的S&P会议,所以我也关注了同年的另外两篇来自该会议的 CollAFL: Path Sensitive Fuzzing
、T-Fuzz: fuzzing by program transformation
关于fuzzer的文章。其中T-Fuzz提出的改进方法是直接将目标程序中的校验语句给去除,然后进行fuzzing测试,找到了bug之后,再看看触发bug的这些输入会不会通过校验测试,这样,从发现的bug里面验证是否输入合法,相比于正向的去求解总共有哪些合法的解,复杂性降低。 而本文提出的用上下文敏感的方式进行分支覆盖等方法,也是对测试深度、速度等的优化。
在复现过程中深刻的体会到,做科研绝不是纸上谈兵的事情,有了好的idea之后,更难得是如何实现以及如何验证。在这一点上,我觉得Angora的开发团队做得很棒。由于本人是第一次接触fuzzing技术,光是弄懂论文的内容就花了不少功夫,也就没有仔细研究源码,不过由此带来的科研思考是值得的。
7、学习总结&课程建议
不管是疫情下的学习生活,还是这门课本身,这段日子回忆起来都是难忘的吧。
本科阶段上网络攻防课的时候总是囫囵吞枣,“原理?原理和我有什么关系?我要做一辈子的工具党!”那个时候就是抱着这种心态完成了各种实验,以至于这学期上课的状态就是:嗯这个我学过,但我不会(理直气壮 后来大四毕业去面试了几个大厂,后知后觉的才发现知识不会说谎,前辈的话还是得听。虽然时至今日我仍然经常怀疑自己是不是不适合读研,但好在已经在思想上与自己和解,因为当你意识到你学的所有总有一天会回报你的时候,就不会再以应付的心态去完成作业了。
这学期的实验真的挺多的,也常常会抱怨会吐槽,但学到了很多是真的,自学的能力越来越强了也是真的。所以在此想对王老师说一声感谢,也想对在电脑面前一坐就是一整天抓耳挠腮的自己说一声感谢。接下来,又是新的学习开始了。
关于这门课的建议,首先非常赞同老师以实践为主的教学方式,希望老师在之后的教学过程中,优化实验深度与广度,对于实验的验收及课堂验收形式做一些改进。其实有感受到老师在不断的优化,但个人觉得还是应该稍微穿插一些理论的讲解,因为我们有时候查阅很多资料都理解不了的内容,可能老师一句点拨就豁然开朗而且印象更加深刻。其次希望老师可以结合上课内容推荐一些研究点。
最后,再次感谢老师的辛苦付出【毕竟我们做了多少东西,老师就要批改多少东西啊啊啊啊
不断学习,不断进步,共勉。