基于Seq2vec的安卓恶意软件检测,数据集取自CICMalDroid 2020,并进行了特征提取。
最近在做Android恶意软件静态检测的研究,此前发布了两个版本,都对Android恶意软件有很高的识别率,现在尝试用Seq2vec的方法进行Android恶意软件检测。我尝试使用了Bi-LSTM、CNN,发现,Bi-LSTM实在训练太慢,而CNN网络不但训练快,而且训练集上准确度可以达到97%以上,验证集以及测试集准确度都能达到93%以上。
先前版本如下:
Android Malware Detection
Android Malware Detection with N-gram
我们的Android应用数据来自加拿大网络安全研究所的CICMalDroid 2020,该Android应用数据集收录了包括4033个良性软件(Benign)、1512个广告软件(Adware)、2467个网银木马(Banking Malware)、3896个手机风险软件(Mobile Riskware)以及4809个SMS恶意软件。
使用Google提供的反编译工具—Apktool对Apk文件进行反编译,并获取了其中的用于在Dalvik虚拟机上运行的主要源码文件—smali文件,批量反编译以及提取特征的脚本文件见上方的先前版本,这里不再提供。smali是对Dalvik字节码的一种解释,虽然不是官方标准语言,但所有语句都遵循一套语法规范。由于Dalvik指令有两百多条,对此我们进行了分类与精简,去掉了无关的指令,只留下了M、R、G、I、T、P、V七大类核心的指令集合,并且只保留操作码字段,去掉了参数。M、R、G、I、T、P、V七大类指令集合分别代表了移动、返回、跳转、判断、取数据、存数据、调用方法七种类型的指令,具体分类如下图所示。
对此特征提取后的数据集进行统计发现,特征最短长度为10,最长可达到1,104,801,其概率分布如下,可见分布极不均衡且数据长度单位可以万计。
# 下载paddlenlp
#!pip install --upgrade paddlenlp -i https://pypi.org/simple
import os
import numpy as np
import pandas as pd
from functools import partial
from utils import load_vocab, convert_example
from sklearn.model_selection import train_test_split
from matplotlib import pyplot as pltimport paddle
import paddle.nn as nn
import paddle.nn.functional as F
import paddlenlp as ppnlp
from paddlenlp.data import Pad, Stack, Tuple
from paddlenlp.datasets import MapDataset
from Model import CNNModel
import datetime
start=datetime.datetime.now()
除了七大类指令外,原始数据字典还包括了分隔符|
以及填充符#
,数据读取同样依照压缩比率进行词汇的划分,并使用填充符#
进行末位单词的补足。
data_split
: 按照rate
进行数据划分,train_size=origin_size*(1-rate)*(1-rate)
test_size=origin_size*rate
eval_size=origin_size*(1-rate)*rate
vocab_compress
: vocab压缩,dict随着rate指数级增长,即dict_size=vocab_dict_size^rate
,这里rate
设为6
#数据集划分
def data_split(input_file, output_path, rate=0.2):if not os.path.exists(output_path):os.makedirs(output_path)origin_dataset = pd.read_csv(input_file, header=None)[[1,2]] # 加入参数train_data, test_data = train_test_split(origin_dataset, test_size=rate)train_data, eval_data = train_test_split(train_data, test_size=rate)train_filename = os.path.join(output_path, 'train.txt')test_filename = os.path.join(output_path, 'test.txt')eval_filename = os.path.join(output_path, 'eval.txt')train_data.to_csv(train_filename, index=False, sep="\t", header=None)test_data.to_csv(test_filename, index=False, sep="\t", header=None)eval_data.to_csv(eval_filename, index=False, sep="\t", header=None)
if not os.path.exists('dataset'):os.mkdir('dataset')
#这里可以使用data_split函数重新划分数据集,也可以将我已经划分的数据集通过cp的方式复制到dataset文件夹下,两种方式请选择一个
#data_split(input_file='data/data86222/mydata.csv',output_path='dataset', rate=0.2)
!cp data/data86222/train.txt dataset/ && cp data/data86222/eval.txt dataset/ && cp data/data86222/test.txt dataset/
vocab_dict={0:'#',1:'|',2:'M',3:'R',4:'G',5:'I',6:'T',7:'P',8:'V'}
#vocab压缩,dict随着rate指数级增长,即len(dict)=len(vocab_dict)^rate
#默认rate=4,建议可以设置为2、4、6、8,其中8容易爆显存
def vocab_compress(vocab_dict,rate&#61;4):if rate<&#61;0:returnwith open(&#39;dict.txt&#39;,&#39;w&#39;,encoding&#61;&#39;utf-8&#39;) as fp:arr&#61;np.zeros(rate,int)while True:pos&#61;rate-1for i in range(rate):fp.write(vocab_dict[arr[i]])fp.write(&#39;\n&#39;)arr[pos]&#43;&#61;1while True:if arr[pos]>&#61;len(vocab_dict):arr[pos]&#61;0pos-&#61;1if pos<0:returnarr[pos]&#43;&#61;1else:break
rate&#61;6
pad&#61;&#39;&#39;
unk&#61;&#39;&#39;
for i in range(rate):pad&#43;&#61;&#39;#&#39;unk&#43;&#61;&#39;|&#39;
#vocab_compress(vocab_dict,rate)
from paddlenlp.datasets import load_datasetdef read(data_path):with open(data_path, &#39;r&#39;, encoding&#61;&#39;utf-8&#39;) as f:for line in f:l &#61; line.strip(&#39;\n&#39;).split(&#39;\t&#39;)if len(l) !&#61; 2:print (len(l), line)words, labels &#61; line.strip(&#39;\n&#39;).split(&#39;\t&#39;)if len(words)&#61;&#61;0:continueyield {&#39;tokens&#39;: words, &#39;labels&#39;: labels}# data_path为read()方法的参数
train_ds &#61; load_dataset(read, data_path&#61;&#39;dataset/train.txt&#39;,lazy&#61;False)
dev_ds &#61; load_dataset(read, data_path&#61;&#39;dataset/eval.txt&#39;,lazy&#61;True)
test_ds &#61; load_dataset(read, data_path&#61;&#39;dataset/test.txt&#39;,lazy&#61;True)
# 加载词表
vocab &#61; load_vocab(&#39;dict.txt&#39;)
#print(vocab)
为了将原始数据处理成模型可以读入的格式&#xff0c;本项目将对数据作以下处理&#xff1a;
首先使用切词&#xff0c;每隔压缩比率rate
切为一个词&#xff0c;之后将切完后的单词映射词表中单词id。
使用paddle.io.DataLoader
接口多线程异步加载数据。
其中用到了PaddleNLP中关于数据处理的API。PaddleNLP提供了许多关于NLP任务中构建有效的数据pipeline的常用API
API | 简介 |
---|---|
paddlenlp.data.Stack | 堆叠N个具有相同shape的输入数据来构建一个batch&#xff0c;它的输入必须具有相同的shape&#xff0c;输出便是这些输入的堆叠组成的batch数据。 |
paddlenlp.data.Pad | 堆叠N个输入数据来构建一个batch&#xff0c;每个输入数据将会被padding到N个输入数据中最大的长度 |
paddlenlp.data.Tuple | 将多个组batch的函数包装在一起 |
更多数据处理操作详见&#xff1a; https://github.com/PaddlePaddle/PaddleNLP/blob/develop/docs/data.md
下面的create_data_loader
函数用于创建运行和预测时所需要的DataLoader
对象。
paddle.io.DataLoader
返回一个迭代器&#xff0c;该迭代器根据batch_sampler
指定的顺序迭代返回dataset数据。异步加载数据。
batch_sampler
&#xff1a;DataLoader通过 batch_sampler 产生的mini-batch索引列表来 dataset 中索引样本并组成mini-batch
collate_fn
&#xff1a;指定如何将样本列表组合为mini-batch数据。传给它参数需要是一个callable对象&#xff0c;需要实现对组建的batch的处理逻辑&#xff0c;并返回每个batch的数据。在这里传入的是prepare_input
函数&#xff0c;对产生的数据进行pad操作&#xff0c;并返回实际长度等。
# Reads data and generates mini-batches.
def create_dataloader(dataset,trans_function&#61;None,mode&#61;&#39;train&#39;,batch_size&#61;1,pad_token_id&#61;0,batchify_fn&#61;None):if trans_function:dataset_map &#61; dataset.map(trans_function)# return_list 数据是否以list形式返回# collate_fn 指定如何将样本列表组合为mini-batch数据。传给它参数需要是一个callable对象&#xff0c;需要实现对组建的batch的处理逻辑&#xff0c;并返回每个batch的数据。在这里传入的是&#96;prepare_input&#96;函数&#xff0c;对产生的数据进行pad操作&#xff0c;并返回实际长度等。dataloader &#61; paddle.io.DataLoader(dataset_map,return_list&#61;True,batch_size&#61;batch_size,collate_fn&#61;batchify_fn)return dataloader# python中的偏函数partial&#xff0c;把一个函数的某些参数固定住&#xff08;也就是设置默认值&#xff09;&#xff0c;返回一个新的函数&#xff0c;调用这个新函数会更简单。
trans_function &#61; partial(convert_example,vocab&#61;vocab,rate&#61;rate,unk_token_id&#61;vocab.get(unk),is_test&#61;False)# 将读入的数据batch化处理&#xff0c;便于模型batch化运算。
# batch中的每个句子将会padding到这个batch中的文本最大长度batch_max_seq_len。
# 当文本长度大于batch_max_seq时&#xff0c;将会截断到batch_max_seq_len&#xff1b;当文本长度小于batch_max_seq时&#xff0c;将会padding补齐到batch_max_seq_len.
batchify_fn &#61; lambda samples, fn&#61;Tuple(Pad(axis&#61;0, pad_val&#61;vocab[pad]), # input_idsStack(dtype&#61;"int64"), # seq lenStack(dtype&#61;"int64") # label
): [data for data in fn(samples)]train_loader &#61; create_dataloader(train_ds,trans_function&#61;trans_function,batch_size&#61;4,mode&#61;&#39;train&#39;,batchify_fn&#61;batchify_fn)
dev_loader &#61; create_dataloader(dev_ds,trans_function&#61;trans_function,batch_size&#61;4,mode&#61;&#39;validation&#39;,batchify_fn&#61;batchify_fn)
test_loader &#61; create_dataloader(test_ds,trans_function&#61;trans_function,batch_size&#61;4,mode&#61;&#39;test&#39;,batchify_fn&#61;batchify_fn)
使用CNNEncoder
搭建一个CNN模型用于进行句子建模&#xff0c;得到句子的向量表示。
然后接一个线性变换层&#xff0c;完成二分类任务。
paddle.nn.Embedding
组建word-embedding层ppnlp.seq2vec.CNNEncoder
组建句子建模层paddle.nn.Linear
构造多分类器
model&#61; CNNModel(len(vocab),num_classes&#61;5,padding_idx&#61;vocab[pad])model &#61; paddle.Model(model)# 加载模型
#model.load(&#39;./checkpoints/final&#39;)
optimizer &#61; paddle.optimizer.Adam(parameters&#61;model.parameters(), learning_rate&#61;1e-5)loss &#61; paddle.nn.loss.CrossEntropyLoss()
metric &#61; paddle.metric.Accuracy()model.prepare(optimizer, loss, metric)
# 设置visualdl路径
log_dir &#61; &#39;./visualdl&#39;
callback &#61; paddle.callbacks.VisualDL(log_dir&#61;log_dir)
训练过程中会输出loss、acc等信息。这里设置了10个epoch&#xff0c;在训练集上准确率约97%。
model.fit(train_loader, dev_loader, epochs&#61;50, log_freq&#61;50, save_dir&#61;&#39;./checkpoints&#39;, save_freq&#61;1, eval_freq&#61;1, callbacks&#61;callback)
end&#61;datetime.datetime.now()
print(&#39;Running time: %s Seconds&#39;%(end-start))
results &#61; model.evaluate(train_loader)
print("Finally train acc: %.5f" % results[&#39;acc&#39;])
results &#61; model.evaluate(dev_loader)
print("Finally eval acc: %.5f" % results[&#39;acc&#39;])
results &#61; model.evaluate(test_loader)
print("Finally test acc: %.5f" % results[&#39;acc&#39;])
label_map &#61; {0: &#39;benign&#39;, 1: &#39;adware&#39;, 2:&#39;banking&#39;, 3:&#39;riskware&#39;, 4:&#39;sms&#39;}
results &#61; model.predict(test_loader, batch_size&#61;128)predictions &#61; []
for batch_probs in results:# 映射分类labelidx &#61; np.argmax(batch_probs, axis&#61;-1)idx &#61; [idx.tolist()]labels &#61; label_map[i] for i in idxpredictions.extend(labels)
# 看看预测数据前5个样例分类结果
for i in test_ds:print(i)breakfor idx, data in enumerate(test_ds):if idx < 10:print(type(data))
abels)
# 看看预测数据前5个样例分类结果
for i in test_ds:print(i)breakfor idx, data in enumerate(test_ds):if idx < 10:print(type(data))print(&#39;Data: {} \t Label: {}&#39;.format(data[0], predictions[idx]))
CNNEncoder实在是太强了&#xff0c;本次使用1e-5的lr训练了50epoch&#xff0c;然后改为1e-6的lr再做了10次epoch&#xff0c;就达到了上述所说的效果&#xff0c;其中&#xff0c;CNNEncoder的ngram_filter_sizes&#61;(1, 2, 3, 4)
&#xff0c;num_filter&#61;12
就完全足够&#xff0c;若小伙伴有兴趣可以尝试更多的num_filter
&#xff0c;来提高精度
请点击此处查看本环境基本用法.
Please click here for more detailed instructions.