本帖最后由 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多个一句话。
准备
首先准备好几个文件夹 一个放好绿色文件:
一个是webshell
下载安装好nltk,到时候做分词用
清洗数据
所谓清洗数据,我们不希望php的注释/**/ // #这种被机器学习解析,因此我们要清洗掉这些东西
代码如下:
- def flush_file(pFile): # 清洗php注释
- file = open(pFile, 'r', encoding='gb18030', errors='ignore')
- read_string = file.read()
- file.close()
- m = re.compile(r'/\*.*?\*/', re.S)
- result = re.sub(m, '', read_string)
- m = re.compile(r'//.*')
- result = re.sub(m, '', result)
- m = re.compile(r'#.*')
- result = re.sub(m, '', result)
- return result
复制代码
效果:
让我们得到data frame:
- # 得到webshell列表
- webshell_files = os_listdir_ex("Z:\\webshell", '.php')
- # 得到正常文件列表
- normal_files = os_listdir_ex("Z:\\noshell", '.php')
- label_webshell = []
- label_normal = []
- # 打上标注
- for i in range(0, len(webshell_files)):
- label_webshell.append(1)
- for i in range(0, len(normal_files)):
- label_normal.append(0)
- # 合并起来
- files_list = webshell_files + normal_files
- label_list = label_webshell + label_normal
复制代码
记得打乱数据
- # 打乱数据,祖传代码
- state = np.random.get_state()
- np.random.shuffle(files_list) # 训练集
- np.random.set_state(state)
- np.random.shuffle(label_list) # 标签
复制代码
合在一起,返回一个data_frame
- data_list = {'label': label_list, 'file': files_list}
- return pd.DataFrame(data_list, columns=['label', 'file'])
复制代码
效果:
文件熵获取
文件熵,又叫做文件复杂度文件越混乱,熵也越大 一般php文件不会很混乱 而webshell往往因为加密等东西搞
的文件乱七八糟的,这是一个检测特征,这边使用网上抄来的方法计算文件熵。
- # 得到文件熵 https://blog.csdn.net/jliang3/article/details/88359063
- def get_file_entropy(pFile):
- clean_string = flush_file(pFile)
- text_list = {}
- _sum = 0
- result = 0
- for word_iter in clean_string:
- if word_iter != '\n' and word_iter != ' ':
- if word_iter not in text_list.keys():
- text_list[word_iter] = 1
- else:
- text_list[word_iter] = text_list[word_iter] + 1
- for index in text_list.keys():
- _sum = _sum + text_list[index]
- for index in text_list.keys():
- result = result - float(text_list[index])/_sum * \
- math.log(float(text_list[index])/_sum, 2)
- return result
复制代码
文件长度获取
针对一句话木马或者包含木马 他们长度就是很小的 所以文件长度也能作为一个特征:
- def get_file_length(pFile): # 得到文件长度,祖传代码
- fsize = os.path.getsize(pFile)
- return int(fsize)
复制代码
合并起来
现在常规特征已经搞好了 合并这些常规特征
- data_frame = get_data_frame()
- data_frame['length'] = data_frame['file'].map(
- lambda file_name: get_file_length(file_name)).astype(int)
- data_frame['entropy'] = data_frame['file'].map(
- lambda file_name: get_file_entropy(file_name)).astype(float)
复制代码
归一化:
我们要把他们变成-1 1之间区间的 这样不会特别影响网络,如果不变网络就会出现很大幅度的落差
- # 归一化这两个东西
- scaler = StandardScaler()
- data_frame['length_scaled'] = scaler.fit_transform(
- data_frame['length'].values.reshape(-1, 1), scaler.fit(data_frame['length'].values.reshape(-1, 1)))
- data_frame['entropy_scaled'] = scaler.fit_transform(
- data_frame['entropy'].values.reshape(-1, 1), scaler.fit(data_frame['entropy'].values.reshape(-1, 1)))
复制代码
自然语言处理
由于我之前并不是特别专门学过自然语言处理 所以这里可能会
有点错误 以后我发现有错误了再改:
首先我们要通过nltk这个库分词:
- clean_string = flush_file(pFile)
- word_list = nltk.word_tokenize(clean_string)
复制代码
请记住 nltk需要单独下载分词库 国内网络你懂的 我是手动下载了然后放到了nltk的支持目录
然后我们得过滤掉不干净的词 避免影响数据库:
- # 过滤掉不干净的
- word_list = [
- word_iter for word_iter in word_list if word_iter not in english_punctuations]
复制代码
这是我的不干净的词库:
- english_punctuations = [',', '.', ':', ';', '?',
- '(', ')', '[', ']', '&', '!', '*', '@', '#', '
- 然后初始化标注器 提取出文本字典
- [code]keras_token = keras.preprocessing.text.Tokenizer() # 初始化标注器
- keras_token.fit_on_texts(word_list) # 学习出文本的字典
复制代码
如果顺利 这些单词长这样:
上面是一句话webshell,下面是正常文本
通过texts_to_sequences 这个function可以将每个string的每个词转成数字
- sequences_data = keras_token.texts_to_sequences(word_list)
复制代码
然后我们把它扁平化 别问我为什么要用C的写法写惯了。当时写的时候没有注意到
然后写出来了才发现python有封装了。
- word_bag = []
- for index in range(0, len(sequences_data)):
- if len(sequences_data[index]) != 0:
- for zeus in range(0, len(sequences_data[index])):
- word_bag.append(sequences_data[index][zeus])
复制代码
到此为止 自然语言处理函数已经完成 我们看看效果:
- # 导入词袋
- data_frame['word_bag'] = data_frame['file'].map(
- lambda file_name: get_file_word_bag(file_name))
复制代码
由于keras要求固定长度 所以让我们填充他,固定长度为1337 超过1337截断(超过1337个字符的 单词 不用说肯定
是某个骇客想把大马变免杀马 低于1337个字符用0填充:
- vectorize_sequences(data_frame['word_bag'].values)
复制代码
让我们看看效果:
zzzz 全是0 还是别看了 反正数据就长这样
构造网络
重头戏来了 构造一个textCnn+二分类的混合网络:
首先是构造textCNN
长这样
词嵌入层 -> 卷积层 + 池化层(我这边抄的只有3个) -> 全连接合并这三个
首先是词嵌入 也叫做入口:
- input_1 = keras.layers.Input(shape=(1337,), dtype='int16', name='word_bag')
- # 词嵌入(使用预训练的词向量)
- embed = keras.layers.Embedding(
- len(g_word_dict) + 1, 300, input_length=1337)(input_1)
复制代码
请注意,我输入的数据模型是1337个float,所以shape=1337
然后生成三个卷积+池化层
- cnn1 = keras.layers.Conv1D(
- 256, 3, padding='same', strides=1, activation='relu')(embed)
- cnn1 = keras.layers.MaxPooling1D(pool_size=48)(cnn1)
复制代码
然后把这些拼接起来:
- cnn = keras.layers.concatenate([cnn1, cnn2, cnn3], axis=1)
- flat = keras.layers.Flatten()(cnn)
- drop = keras.layers.Dropout(0.2)(flat)
复制代码
让他输出一个sigmoid
- model_1_output = keras.layers.Dense(
- 1, activation='sigmoid', name='TextCNNoutPut')(drop)
复制代码
第一层好了 连起来长这样:
- # 进来的file length_scaled entropy_scaled word_bag
- # 第一网络是一个TextCNN 词嵌入-卷积池化*3-拼接-全连接-dropout-全连接
- input_1 = keras.layers.Input(shape=(1337,), dtype='int16', name='word_bag')
- # 词嵌入(使用预训练的词向量)
- embed = keras.layers.Embedding(
- len(g_word_dict) + 1, 300, input_length=1337)(input_1)
- # 词窗大小分别为3,4,5
- cnn1 = keras.layers.Conv1D(
- 256, 3, padding='same', strides=1, activation='relu')(embed)
- cnn1 = keras.layers.MaxPooling1D(pool_size=48)(cnn1)
- cnn2 = keras.layers.Conv1D(
- 256, 4, padding='same', strides=1, activation='relu')(embed)
- cnn2 = keras.layers.MaxPooling1D(pool_size=47)(cnn2)
- cnn3 = keras.layers.Conv1D(
- 256, 5, padding='same', strides=1, activation='relu')(embed)
- cnn3 = keras.layers.MaxPooling1D(pool_size=46)(cnn3)
- # 合并三个模型的输出向量
- cnn = keras.layers.concatenate([cnn1, cnn2, cnn3], axis=1)
- flat = keras.layers.Flatten()(cnn)
- drop = keras.layers.Dropout(0.2)(flat)
- model_1_output = keras.layers.Dense(
- 1, activation='sigmoid', name='TextCNNoutPut')(drop)
- # 第一层好了
复制代码
第二层,自己做的一个简易分类用来根据长度+熵做二分类
输入shape为2(长度&熵)
- input_2 = keras.layers.Input(
- shape=(2,), dtype='float32', name='length_entropy')
- model_2 = keras.layers.Dense(
- 128, input_shape=(2,), activation='relu')(input_2)
复制代码
没什么特别的:
- model_2 = keras.layers.Dropout(0.4)(model_2)
- model_2 = keras.layers.Dense(64, activation='relu')(model_2)
- model_2 = keras.layers.Dropout(0.2)(model_2)
- model_2 = keras.layers.Dense(32, activation='relu')(model_2)
- model_2_output = keras.layers.Dense(
- 1, activation='sigmoid', name='LengthEntropyOutPut')(model_2)
复制代码
拼接两个网络:
- model_combined = keras.layers.concatenate([model_2_output, model_1_output])
- model_end = keras.layers.Dense(64, activation='relu')(model_combined)
- model_end = keras.layers.Dense(
- 1, activation='sigmoid', name='main_output')(model_end)
复制代码
不得不说keras是真的强大,,,
我们希望输出是sigmoid而且只有一个值(是否是webshell),因此最后一层就是1
别忘了定义输入输出
- # 定义这个具有两个输入和输出的模型
- model_end = keras.Model(inputs=[input_2, input_1],
- outputs=model_end)
- model_end.compile(optimizer='adam',
- loss='binary_crossentropy', metrics=['accuracy'])
复制代码
总体网络架构如下:
跑一下试试?
对于免杀样本 是有很好地检测率
正常文件误报率也很低
个人认为 这个神经网络可以认为是跟人看写php法一样的如果webshell的单词熵写的
跟正常文件差不多 就会没办法
而且样本还是太少了 遇到一句话混合到正常文件的情况完全没有办法,
个人认为还是要增加样本+与传统检测引擎混合使用 |