电脑疯子技术论坛|电脑极客社区

微信扫一扫 分享朋友圈

已有 2735 人浏览分享

机器学习之keras基于TextCNN的webshell识别

[复制链接]
2735 0
本帖最后由 zhaorong 于 2021-6-8 16:31 编辑

本来想用zend直接解析PHP opcode然后做xxoo的,然后看了一会zend源码 发现PHP真的是 动态 语言,,,比如eval
base64_encode(“xxxx”)) opcode只能看到eval base64_encode 其他的要动态执行才行~ 那样就复杂了需要
追踪入口 加数据做流追踪等等 实在是太麻烦,所以还是换成了传统的语义分析。

我们要做什么?

我打算使用一个 TextCNN 与 一个普通的二分类网络来分别做。TextCNN主要是用来检测单词数组 普通二分
类网络用于检测一些常规特征比如 文件熵(aka 文件复杂度) 文件大小(某些一句话几KB)
为了方便我这边仅仅使用php 当然,任何都可以.样本数量是1W左右自己写了一个一句话变种生成器居然
有部分过了主流防火墙哈哈哈哈生成了1000多个一句话。

准备

首先准备好几个文件夹 一个放好绿色文件:

QQ截图20210608154710.png

一个是webshell

777777778.png

下载安装好nltk,到时候做分词用

清洗数据

所谓清洗数据,我们不希望php的注释/**/ // #这种被机器学习解析,因此我们要清洗掉这些东西
代码如下:
  1. def flush_file(pFile):  # 清洗php注释
  2.     file = open(pFile, 'r', encoding='gb18030', errors='ignore')
  3.     read_string = file.read()
  4.     file.close()
  5.     m = re.compile(r'/\*.*?\*/', re.S)
  6.     result = re.sub(m, '', read_string)
  7.     m = re.compile(r'//.*')
  8.     result = re.sub(m, '', result)
  9.     m = re.compile(r'#.*')
  10.     result = re.sub(m, '', result)
  11.     return result
复制代码

效果:

QQ截图20210608154914.png

让我们得到data frame:
  1. # 得到webshell列表
  2.     webshell_files = os_listdir_ex("Z:\\webshell", '.php')
  3.     # 得到正常文件列表
  4.     normal_files = os_listdir_ex("Z:\\noshell", '.php')
  5.     label_webshell = []
  6.     label_normal = []
  7.     # 打上标注
  8.     for i in range(0, len(webshell_files)):
  9.         label_webshell.append(1)
  10.     for i in range(0, len(normal_files)):
  11.         label_normal.append(0)
  12.     # 合并起来
  13.     files_list = webshell_files + normal_files
  14.     label_list = label_webshell + label_normal
复制代码

记得打乱数据
  1. # 打乱数据,祖传代码
  2.     state = np.random.get_state()
  3.     np.random.shuffle(files_list)  # 训练集
  4.     np.random.set_state(state)
  5.     np.random.shuffle(label_list)  # 标签
复制代码

合在一起,返回一个data_frame
  1. data_list = {'label': label_list, 'file': files_list}
  2.     return pd.DataFrame(data_list, columns=['label', 'file'])
复制代码

效果:
5103.png

文件熵获取

文件熵,又叫做文件复杂度文件越混乱,熵也越大 一般php文件不会很混乱 而webshell往往因为加密等东西搞
的文件乱七八糟的,这是一个检测特征,这边使用网上抄来的方法计算文件熵。
  1. # 得到文件熵 https://blog.csdn.net/jliang3/article/details/88359063
  2. def get_file_entropy(pFile):
  3.     clean_string = flush_file(pFile)
  4.     text_list = {}
  5.     _sum = 0
  6.     result = 0
  7.     for word_iter in clean_string:
  8.         if word_iter != '\n' and word_iter != ' ':
  9.             if word_iter not in text_list.keys():
  10.                 text_list[word_iter] = 1
  11.             else:
  12.                 text_list[word_iter] = text_list[word_iter] + 1
  13.     for index in text_list.keys():
  14.         _sum = _sum + text_list[index]
  15.     for index in text_list.keys():
  16.         result = result - float(text_list[index])/_sum * \
  17.             math.log(float(text_list[index])/_sum, 2)
  18.     return result
复制代码

文件长度获取

针对一句话木马或者包含木马 他们长度就是很小的 所以文件长度也能作为一个特征:
  1. def get_file_length(pFile):  # 得到文件长度,祖传代码
  2.     fsize = os.path.getsize(pFile)
  3.     return int(fsize)
复制代码

合并起来

现在常规特征已经搞好了 合并这些常规特征
  1. data_frame = get_data_frame()
  2. data_frame['length'] = data_frame['file'].map(
  3.     lambda file_name: get_file_length(file_name)).astype(int)
  4. data_frame['entropy'] = data_frame['file'].map(
  5.     lambda file_name: get_file_entropy(file_name)).astype(float)
复制代码

归一化:
我们要把他们变成-1 1之间区间的 这样不会特别影响网络,如果不变网络就会出现很大幅度的落差
  1. # 归一化这两个东西
  2. scaler = StandardScaler()
  3. data_frame['length_scaled'] = scaler.fit_transform(
  4.     data_frame['length'].values.reshape(-1, 1), scaler.fit(data_frame['length'].values.reshape(-1, 1)))
  5. data_frame['entropy_scaled'] = scaler.fit_transform(
  6.     data_frame['entropy'].values.reshape(-1, 1), scaler.fit(data_frame['entropy'].values.reshape(-1, 1)))
复制代码

自然语言处理

由于我之前并不是特别专门学过自然语言处理 所以这里可能会
有点错误 以后我发现有错误了再改:
首先我们要通过nltk这个库分词:
  1. clean_string = flush_file(pFile)
  2.     word_list = nltk.word_tokenize(clean_string)
复制代码

请记住 nltk需要单独下载分词库 国内网络你懂的 我是手动下载了然后放到了nltk的支持目录
然后我们得过滤掉不干净的词 避免影响数据库:
  1. # 过滤掉不干净的
  2.     word_list = [
  3.         word_iter for word_iter in word_list if word_iter not in english_punctuations]
复制代码

这是我的不干净的词库:
  1. english_punctuations = [',', '.', ':', ';', '?',
  2.                             '(', ')', '[', ']', '&', '!', '*', '@', '#', '
  3. 然后初始化标注器 提取出文本字典
  4. [code]keras_token = keras.preprocessing.text.Tokenizer()  # 初始化标注器
  5.     keras_token.fit_on_texts(word_list)  # 学习出文本的字典
复制代码

如果顺利 这些单词长这样:

99.png

上面是一句话webshell,下面是正常文本
通过texts_to_sequences 这个function可以将每个string的每个词转成数字
  1. sequences_data = keras_token.texts_to_sequences(word_list)
复制代码

然后我们把它扁平化 别问我为什么要用C的写法写惯了。当时写的时候没有注意到
然后写出来了才发现python有封装了。
  1. word_bag = []
  2.     for index in range(0, len(sequences_data)):
  3.         if len(sequences_data[index]) != 0:
  4.             for zeus in range(0, len(sequences_data[index])):
  5.                 word_bag.append(sequences_data[index][zeus])
复制代码

到此为止 自然语言处理函数已经完成 我们看看效果:
  1. # 导入词袋
  2. data_frame['word_bag'] = data_frame['file'].map(
  3.     lambda file_name: get_file_word_bag(file_name))
复制代码

98.png

由于keras要求固定长度 所以让我们填充他,固定长度为1337 超过1337截断(超过1337个字符的 单词 不用说肯定
是某个骇客想把大马变免杀马 低于1337个字符用0填充:
  1. vectorize_sequences(data_frame['word_bag'].values)
复制代码

让我们看看效果:
97.png

zzzz 全是0 还是别看了 反正数据就长这样

构造网络

重头戏来了 构造一个textCnn+二分类的混合网络:
首先是构造textCNN
长这样

96.png

词嵌入层 -> 卷积层 + 池化层(我这边抄的只有3个) -> 全连接合并这三个
首先是词嵌入 也叫做入口:
  1. input_1 = keras.layers.Input(shape=(1337,), dtype='int16', name='word_bag')
  2.     # 词嵌入(使用预训练的词向量)
  3.     embed = keras.layers.Embedding(
  4.         len(g_word_dict) + 1, 300, input_length=1337)(input_1)
复制代码

请注意,我输入的数据模型是1337个float,所以shape=1337
然后生成三个卷积+池化层
  1. cnn1 = keras.layers.Conv1D(
  2.         256, 3, padding='same', strides=1, activation='relu')(embed)
  3.     cnn1 = keras.layers.MaxPooling1D(pool_size=48)(cnn1)
复制代码

然后把这些拼接起来:
  1. cnn = keras.layers.concatenate([cnn1, cnn2, cnn3], axis=1)
  2.     flat = keras.layers.Flatten()(cnn)
  3.     drop = keras.layers.Dropout(0.2)(flat)
复制代码

让他输出一个sigmoid
  1. model_1_output = keras.layers.Dense(
  2.         1, activation='sigmoid', name='TextCNNoutPut')(drop)
复制代码

第一层好了 连起来长这样:
  1. # 进来的file length_scaled entropy_scaled word_bag
  2.     # 第一网络是一个TextCNN 词嵌入-卷积池化*3-拼接-全连接-dropout-全连接
  3.     input_1 = keras.layers.Input(shape=(1337,), dtype='int16', name='word_bag')
  4.     # 词嵌入(使用预训练的词向量)
  5.     embed = keras.layers.Embedding(
  6.         len(g_word_dict) + 1, 300, input_length=1337)(input_1)
  7.     # 词窗大小分别为3,4,5
  8.     cnn1 = keras.layers.Conv1D(
  9.         256, 3, padding='same', strides=1, activation='relu')(embed)
  10.     cnn1 = keras.layers.MaxPooling1D(pool_size=48)(cnn1)

  11.     cnn2 = keras.layers.Conv1D(
  12.         256, 4, padding='same', strides=1, activation='relu')(embed)
  13.     cnn2 = keras.layers.MaxPooling1D(pool_size=47)(cnn2)

  14.     cnn3 = keras.layers.Conv1D(
  15.         256, 5, padding='same', strides=1, activation='relu')(embed)
  16.     cnn3 = keras.layers.MaxPooling1D(pool_size=46)(cnn3)
  17.     # 合并三个模型的输出向量
  18.     cnn = keras.layers.concatenate([cnn1, cnn2, cnn3], axis=1)
  19.     flat = keras.layers.Flatten()(cnn)
  20.     drop = keras.layers.Dropout(0.2)(flat)
  21.     model_1_output = keras.layers.Dense(
  22.         1, activation='sigmoid', name='TextCNNoutPut')(drop)
  23.     # 第一层好了
复制代码

第二层,自己做的一个简易分类用来根据长度+熵做二分类
输入shape为2(长度&熵)
  1. input_2 = keras.layers.Input(
  2.         shape=(2,), dtype='float32', name='length_entropy')
  3.     model_2 = keras.layers.Dense(
  4.         128, input_shape=(2,), activation='relu')(input_2)
复制代码

没什么特别的:
  1. model_2 = keras.layers.Dropout(0.4)(model_2)
  2.     model_2 = keras.layers.Dense(64, activation='relu')(model_2)
  3.     model_2 = keras.layers.Dropout(0.2)(model_2)
  4.     model_2 = keras.layers.Dense(32, activation='relu')(model_2)
  5.     model_2_output = keras.layers.Dense(
  6.         1, activation='sigmoid', name='LengthEntropyOutPut')(model_2)
复制代码

拼接两个网络:
  1. model_combined = keras.layers.concatenate([model_2_output, model_1_output])
  2.     model_end = keras.layers.Dense(64, activation='relu')(model_combined)
  3.     model_end = keras.layers.Dense(
  4.         1, activation='sigmoid', name='main_output')(model_end)
复制代码

不得不说keras是真的强大,,,
我们希望输出是sigmoid而且只有一个值(是否是webshell),因此最后一层就是1
别忘了定义输入输出
  1. # 定义这个具有两个输入和输出的模型
  2.     model_end = keras.Model(inputs=[input_2, input_1],
  3.                             outputs=model_end)
  4.     model_end.compile(optimizer='adam',
  5.                       loss='binary_crossentropy', metrics=['accuracy'])
复制代码

总体网络架构如下:

8.png

跑一下试试?

对于免杀样本 是有很好地检测率

7.png

正常文件误报率也很低
个人认为 这个神经网络可以认为是跟人看写php法一样的如果webshell的单词熵写的
跟正常文件差不多 就会没办法
而且样本还是太少了 遇到一句话混合到正常文件的情况完全没有办法,
个人认为还是要增加样本+与传统检测引擎混合使用

您需要登录后才可以回帖 登录 | 注册

本版积分规则

1

关注

0

粉丝

9021

主题
精彩推荐
热门资讯
网友晒图
图文推荐

Powered by Pcgho! X3.4

© 2008-2022 Pcgho Inc.