基于机器学习的webshell检测踩坑小记

  • A+
所属分类:安全闲碎

目录

“文章部分内容引用自兜哥作品”

0x01 机器学习衡量指标

0x02 数据集

0x03 特征提取

0x04 模型训练及检测

0x05 预测新样本

0x06 优化检测

前言

本人是合合信息(划重点——“OCR+AI大数据公司”)安全部门的一名成员,受到公司AI氛围的感染,对于不会AI技术的我深感不适,便决心学习一波AI技术。AI是什么?我们把AI定义为让计算机模仿人类智能来处理事情的技术,目前,AI技术已经在有限的简单领域内取得非常大的成功。强行举个栗子,有研究人员写了一个在《打砖块》游戏中打到最高分的AI程序,它可以自己学习进步,并在2个半小时内就打得比人类玩家更好。

研究人员运行了这个程序,让他们惊讶的是,这个程序发展出了一套软件里没有写过的策略。它会专攻砖块上的一个点,直到凿出洞来,再穿到墙后。在这套策略下,计算机无需不停移动球板,工作量降到了最低。同时,这也让球板漏接球的可能性降到最低。

我们要知道,计算机是看不见游戏界面上的小球、球板和砖块的。它“看见”的只是一堆数字。它知道自己能控制的变量,以及如何运用变量来提高分数。
AI在安全领域也是处在起步阶段,很多安全问题都没有很好的AI解决方案,今天我们就来实践一个安全里面的二分类问题——webshell的识别检测。

什么是webshell?

Webshell是一种基于web应用的后门程序,是黑客通过服务器漏洞或其他方式提权后,为了维持权限所部署的权限木马。Webshell的危害非常大,通常为网站权限,可以对网站内容进行随意修改、删除,甚至可以进一步利用系统漏洞对服务器提权,拿到服务器权限。

解决方法:
webshell检测方法较多,有静态检测、动态检测、日志检测、语法检测、统计学检测等等。本文重点讨论webshell的静态检测方法,静态检测也是最主要的检测手段。

静态检测通过匹配特征码、特征值、危险函数函数来查找webshell的方法,只能查找已知的webshell,并且误报率漏报率会比较高,但是如果规则完善,可以减低误报率,但是漏报率必定会有所提高。优点是快速方便,对已知的webshell查找准确率高,部署方便,一个脚本就能搞定。缺点漏报率、误报率高,无法查找0day型webshell,而且容易被绕过。

机器学习最大的优点是它具有泛化能力,也就是可以举一反三。基于机器学习的webshell检测可以有效的提升准确率和减低漏报率。本文使用的算法为兜哥的opcode+tfidf算法。如下是常规检测和机器学习检测率的比较。

1.jpg

0x01 机器学习衡量指标

混淆矩阵

2.jpg

混淆矩阵(Confusion Matrix)又被称为错误矩阵,通过它可以直观地观察到算法的效果。在二分类问题中,可以用一个2×2的矩阵表示,如下表所示。其中,FP为表示实际为假预测为真,也就是误报,FN表示实际为真预测为假,也就是漏报。

准确率和召回率

3.jpg
机器学习中最常用的指标就是准确率和召回率。准确率也叫查准率,要提高查准率就要降低误报。召回率也叫查全率,要提高查全率就要降低漏报。

F1-Score

4.jpg
人们通常使用准确率和召回率这两个指标,来评价二分类模型的分析效果。但是当这两个指标发生冲突时,我们很难在模型之间进行比较。此时可以使用F1-score来进行综合评判。

0x02 数据集

所用数据集均来源于github,需要两类数据,黑名单和白名单。白名单可以从以下链接获取。

https://github.com/topics/php?o=desc&s=stars

黑名单使用以下项目:

https://github.com/tennc/webshellhttps://github.com/ysrc/webshell-samplehttps://github.com/tanjiti/webshellSample

由于github下载速度较慢,以下是github中star排名前350的php项目的白名单和12个webshell项目的黑名单的备份。测试算法时可以只需取部分项目即可。

链接: https://pan.baidu.com/s/12FWAN-jIWTqjOlwB5V0DuQ 提取码: 31bi

0x03 特征提取

1.词袋和TF-IDF模型

词袋模型是指将句子或文本中的每个单词看成一个集合,并统计其出现的次数。词袋模型被广泛应用在文件分类,词出现的频率可以用来当作训练分类器的特征。对一篇文章进行特征化,最常见的方式就是词袋。

TF-IDF(词频-逆文本频率指数)模型是一种用以评估一个词对于一个文件集或一个语料库中的其中一份文件的重要程度。一个字词的重要性随着它在文件中出现的次数成正比增加,同时会随着它在语料库中出现的频率成反比下降。

函数get_feature_by_tfidf的作用是传文本数组进去,可以返回一个特征矩阵,原理就是使用词袋和TF-IDF模型计算文本特征。

def get_feature_by_tfidf(x, max_features=None):
cv = CountVectorizer(ngram_range=(3, 3), decode_error="ignore",
max_features=max_features,token_pattern=r'\b\w+\b',
min_df=1, max_df=1.0)
x = cv.fit_transform(x).toarray()
transformer = TfidfTransformer(smooth_idf=False)
transformer = transformer.fit_transform(x)
x = transformer.toarray()
return x

2.opcode模型
opcode是计算机指令的一部分,也叫字节码,一个php文件可以抽取出一个指令序列,如ADD、ECHO、RETURN。由于直接对php文件使用词袋和TF-IDF进行模型训练会消耗大量计算资源,使用opcode模型进行降维可以有效提升模型效率和模型的准确率。由于opcode只关心操作指令,不关心函数名、定义等,因此可以有效的检测一些加密、混淆的代码。

要使用opcode模型,需要装一个vld的扩展,需要注意的是不同版本的vld支持不同版本的PHP,如vld 0.11.1版本支持PHP 5.4,vld 0.13.0支持PHP 5.6。下载链接如下:

Windows:http://pecl.php.net/package/vld/0.14.0/windows
Linux:http://pecl.php.net/package/vld

Windows下载的是dll文件,只需将其中的php_vld.dll放到PHP安装目录/ext目录下,编辑php.ini文件添加extension=php_vld.dll即可。
Linux的安装命令如下,安装完后配置php.ini,将extension=vld.so添加进去。

tar zxvf vld-0.xx.x.tgz
cd vld-0.xx.x
phpize
./configure
make && make install

安装完成后,使用php -dvld.active=1 -dvld.execute=0 1.php即可获取1.php文件的opcode。对于的opcode序列为ECHO RETURN,如下图所示:

5.jpg

对于一句话的opcode序列为BEGIN_SILENCE FETCH_R FETCH_DIM_R INCLUDE_OR_EVAL END_SILENCE RETURN,如下图所示:

6.jpg

函数load_php_opcode的作用是提取一个php文件的opcode序列。

def load_php_opcode(php_filename):
try:
output = subprocess.check_output(['php.exe', '-dvld.active=1',
'-dvld.execute=0', php_filename],
stderr=subprocess.STDOUT)
tokens = re.findall(r'\s(\b[A-Z_]+\b)\s', output)
t = " ".join(tokens)
return t
except:
return " "

由于提取opcode会消耗较多的时间,函数load_php_opcode_from_dir_with_file会提取一个文件夹中所有php文件的opcode到指定的文件中,作为训练和预测时的特征文件。

def load_php_opcode_from_dir_with_file(dir, file_name):
print "load php opcode from dir => " + dir
for root, dirs, files in os.walk(dir):
for filename in files:
if filename.endswith('.php'):
try:
full_path = os.path.join(root, filename)
file_content = load_php_opcode(full_path)
with open(file_name, "a+") as f:
f.write(file_content + "\n")
except:
continue

0x04 模型训练及检测

1.朴素贝叶斯算法

本文使用16667个样本,其中白样本为14467条,黑样本为2200条,白样本远大于黑样本,以下是使用朴素贝叶斯算法的代码。

def do_gnb(x, y):
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.4,
random_state=0)
clf = GaussianNB()
clf.fit(x_train, y_train)
joblib.dump(clf, 'model/gnb.pkl')
y_pred = clf.predict(x_test)
do_metrics(y_test, y_pred)

经测试,使用朴素贝叶斯算法的准确率为54.5%,召回率为98.4%,其中,有14个样本漏报,715个样本误报,效果较差。对于黑白样本不均,不推荐使用朴素贝叶斯算法。

2.随机森林算法

以下是随机森林的算法代码实现。

def do_rf(x, y):
x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.4,
random_state=0)
clf = RandomForestClassifier(n_estimators=50)
clf.fit(x_train, y_train)
joblib.dump(clf, 'model/rf.pkl')
y_pred = clf.predict(x_test)
do_metrics(y_test, y_pred)
使用随机森林算法的准确率为94.4%,召回率为91.5%,效果相比朴素贝叶斯好。

7.jpg

0x05 预测新样本

1.预测单个文件

需要注意的是,由于采用TF-IDF模型进行特征提取,在预测新样本时,要加载训练时的黑白样本特征作为语料库,进而计算出新样本的特征矩阵。加载语料库的代码如下:

def get_old_data():
white_file_list = []
black_file_list = []
with open('black_opcodes.txt', 'r') as f:
for line in f:
black_file_list.append(line.strip('\n'))

with open('white_opcodes.txt', 'r') as f:
    for line in f:
        white_file_list.append(line.strip('\n'))
old_data = white_file_list + black_file_list
return old_data

在训练样本时,程序将所有样本分成了两部分,一部分拿去训练得到模型,一部分使用训练出来的模型拿来做预测,检测该模型的效果。当算法确定后,就需要将所有样本拿来训练获得最终的训练模型。

def do_rf_fin(x, y):
clf = RandomForestClassifier(n_estimators=50)
clf.fit(x, y)
joblib.dump(clf, 'model/rf_fin.pkl')

最后,我们再使用训练出的rf_fin.pk1模型做预测:

if name == 'main':
php_file_name = sys.argv[1]
print 'Checking the file {}'.format(php_file_name)
all_file = get_old_data()
opcode = load_php_opcode(php_file_name)
all_file.append(opcode)
x = get_feature_by_tfidf(all_file)
gnb = joblib.load('save/rf_fin.pkl')
y_p = gnb.predict(x[-1:])
if y_p == [0]:
print 'Not Webshell'
elif y_p == [1]:
print 'Webshell!'

2.预测一个文件夹

if name == 'main':
php_file_dir = sys.argv[1]
log('Checking the dir {}'.format(php_file_dir))
checked_file_num = 0
webshell_file_num = 0

# 准备数据
opc_list, file_list = load_php_opcode_from_dir(php_file_dir)
all_file = get_old_data()
for i in opc_list:
    checked_file_num = checked_file_num + 1
    all_file.append(i)
x = get_feature_by_tfidf(all_file)
# end 准备数据

gnb = joblib.load('model/rf_fin.pkl')
y_p = gnb.predict(x[-checked_file_num:])

for i in range(len(y_p)):
    if y_p[i] == 1:
        webshell_file_num = webshell_file_num + 1
        log(file_list[i] + " => webshell!")
log("All php file number: " + str(checked_file_num) + " | webshell number: " 
    + str(webshell_file_num))

尝试扫描,结果如下:

8.jpg

9.jpg

0x06 优化检测率

1.优化数据集

本文主要从数据集的角度进行检测率的优化。优化数据集分为两方面,一方面是增大数据量,数据量越大,预测的效果越好,本次测试黑样本较少,另一方面是清洗数据集,主要针对的是黑样本的清洗。

在预测的过程中,发现模型会将空文件或者没有php代码的文件识别为webshell,这些文件的opcode序列为ECHO RETURN,回过头找黑名单中opcode序列为ECHO RETURN的webshell文件,发现很多webshell的代码使用的是php短标签,而php.ini没有配置短标签支持,导致这些短标签的webshell不能被php正常解析,最终和普通txt文件的opcode相同,产生了数据集污染,将正常的空文件或文本文件识别成了webshell。如下图所示:

10.jpg

修改php.ini,将短标签解析开启short_open_tag = On,重新进行特征提取,发现还是存在少量ECHO RETURN序列,直接手工去除,再次训练。
使用朴素贝叶斯的效果如下,并没有提高准确率和召回率,还有所下降,说明朴素贝叶斯在此不适用。由于之前去掉了一部分黑名单样本导致黑白样本数量差距变大,导致朴素贝叶斯分类效果变差。

11.jpg

而使用随机森林的准确率和召回率有大幅提升,准确率为97.8%、召回率为96.5%,分别提升了3.4%和5%,随机森林不受黑白样本数量干扰,分类效果较好。

12.jpg

对于数据集还可以继续优化,还存在一些小问题没去发现,在此不再做扩展。

2.优化算法

实践发现,基于这些数据集的情况下,随机森林算法的效果要优于朴素贝叶斯,优化算法可以使用如深度学习的CNN、RNN等算法进行尝试。

3.优化参数

参数优化主要分为算法模型调参和特征提取调参,这里主要可调的参数为N-Gram数和词袋最大特征数。

这里主要介绍一下N-Gram,N-Gram模型是基于“联想”,它的一个特点是某个词的出现依赖于其他若干个词,第二个特点是我们获得的信息越多,预测越准确。例如听到腾讯就想到qq,听到百度,阿里就会想到腾讯,腾讯、qq就相当于2-Gram,百度、阿里、腾讯相当于3-Gram。

上文所使用的是3-Gram,其中参数ngram_range是指定ngram的范围,也可以使用ngram_range=(2,3)表示同时使用2-Gram和3-Gram。

cv = CountVectorizer(ngram_range=(3, 3), decode_error="ignore",
max_features=max_features,token_pattern=r'\b\w+\b',
min_df=1, max_df=1.0)
这里在使用2-Gram进行训练,即ngram_range=(2, 2)得到如下结果,稍稍优于3-Gram。

13.jpg