跳转至

基于机器学习的恶意URL检测

目录

概述

本次实践虽题为”基于机器学习“,但目前也只更新到使用TF-IDF提取特征,使用逻辑回归和SVM模型的进度。不过千里之行始于足下嘛,后面再更啦🐦

提出问题

首先我们需要将安全问题进行抽象,也就是针对现状提出问题。在假定的这个业务背景下,我们发现:

  • 恶意URL存在特定几种类型1

  • 特定类型恶意URL在文本上存在普遍的词汇特征,例如钓鱼URL中常见"login", "account", "sigin"等关键词

因此我们尝试使用机器学习算法对恶意URL进行检测分析。

数据处理

样本选择

  • malicious-URLs
    • malicious-URLs 在Github上面一个 使用机器学习去检测恶意URL的项目 ,里面有一个训练集,有做标记是正常的URL还是恶意的URL
    • 内容类型:文本样本
    • 是否标记:是
    • 是否特征化:否
    • 使用范围:入侵检测、异常流量、WAF

数据清洗

由于样本本身为处理好的标记数据,所以在数据格式和脏数据上无需处理(真实情况下可能正好相反:-()。

编写数据帧提取函数:

def csv_data_read(csv_file_path):
    # 为减少训练时间,可只取头部10W条,但一定需要先打乱样本)
    # df_csv = pd.read_csv(csv_file_path).head(100000)
    df_csv = pd.read_csv(csv_file_path)
    urls = []
    labels = []
    for index, row in df_csv.iterrows():
        urls.append(row["url"])
        labels.append(row["label"])
    return urls, labels

编写对URL的数据清洗函数:

def url_tokenize(url):
    """
    对URL进行清洗,删除斜线、点、和com,进行分词
    :param url:
    :return:
    """
    web_url = url.lower()
    dot_slash = []
    slash = str(web_url).split('/')
    for i in slash:
        r1 = str(i).split('-')
        token_slash = []
        for j in range(0,len(r1)):
            r2 = str(r1[j]).split('.')
            token_slash = token_slash + r2
        dot_slash = dot_slash + r1 + token_slash
    urltoken_list = list(set(dot_slash))
    white_words = ["com", "http:", "https:", ""]
    for white_word in white_words:
        if white_word in urltoken_list:
            urltoken_list.remove(white_word)
    return urltoken_list

特征提取

我们首先加载数据集:

    grep_csv_file_path = "../../data/data-0x3/grey-url.csv"
    black_csv_file_path = "../../data/data-0x3/black-url.csv"
    grey_urls, y = csv_data_read(grep_csv_file_path)

我们使用TF-IDF算法提取URL的特征,并将数据帧划分为训练集和测试集:

    url_vectorizer = TfidfVectorizer(tokenizer=url_tokenize)
    x = url_vectorizer.fit_transform(grey_urls)
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)

注:TF-IDF

TF-IDF(Term Frequency – Inverse Document Frequency),即词频-逆文档频率。在计算上为词频和逆文档频率的乘积。计算方法如下:

  • 计算词频(TF)
    • 某个词在文章中出现的次数/文章的总词数
    • 即某个词在这段文字中出现得越多,TF就越大
  • 计算逆文档频率(IDF)
    • log(语料库的文档总数/包含该词的文档数+1)
    • 某个词在普遍情况下越常见,分母大,IDF也约趋于0
  • 计算TF-IDF
    • TF-IDF = TF * IDF
    • TF-IDF越大,说明词在这段文章中越重要,但因为有IDF的存在,又能避免把“是”、”的“、“和”等停用词的TF-IDF值降低

在应用上:将文章分词,计算TF-IDF,按照其值大小降序排列,排名靠前的即文章的关键词

模型选择

接下来,我们对数据集使用逻辑回归模型,并将将拟合后的模型和向量保存为本地文件,便于重复使用

    # 对训练集和测试集执行逻辑回归
    l_regress = LogisticRegression(solver='liblinear')
    l_regress.fit(x_train, y_train)
    l_score = l_regress.score(x_test, y_test)
    # print("测试拟合分数为:{0}".format(l_score))

    file_mode = "../../model/model-0x3/model.pkl"
    dump_model_object(file_mode)
    file_vector = "../../model/model-0x3/vector.pl"
    dump_model_object(file_vector)

此外,我们也可以使用支持向量机模型:

def practice_svm(x_train, x_test, y_train, y_test):
    """
    实践SVM算法识别恶意URL
    :param x_train:
    :param x_test:
    :param y_train:
    :param y_test:
    :return:
    """
    model_svm = SVC()
    # 注意:SVM训练可能较慢,注意样本的数量
    model_svm.fit(x_train, y_train)
    svm_score = model_svm.score(x_test, y_test)
    print("测试拟合分数为:{0}".format(svm_score))
    model_svm_save = model_svm

    """
    保存训练好的模型和向量
    """
    file_mode = "../../model/model-0x3/model_svm.pkl"
    dump_model_object(file_mode, model_svm_save)

效果评估

在“特征提取”部分我们采用train_test_split方法进行随机划分训练集和测试集,进行交叉验证,再次回顾代码为:

x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)

使用线性回归模型,最后得到测试拟合分数为:0.9599966703530615

注,参数介绍:

  • test_size:测试集在总样本中的占比
  • random_state:随机数的种子,也可以理解为该组随机数的编号。规则是:种子不同时,产生不同的随机数;种子相同时,在不同实例下也产生相同的随机数。比如在上面的语句中,test_size为0.2,即选择总样本的20%作为测试集,但是如何选择呢?random_state就指定了:按照“第42种”规则选择这20%随机的数据。

使用支持向量机模型时,发现在数据量较大的情况下,该模型的运算速度较慢,因此在实验环境下不得已减少了训练样本的数量,但也导致了拟合分数的降低,所以就不展示在样本缩水情况下的测试拟合分数了。同时,也了解到这是传统二分类SVM在面对大数据量时的弊端,并且随着集成学习的成熟,SVM现在“普遍用于集成学习中基模型的构建”[2],而不是作为唯一的分类模型使用。

完整代码

"""
Author: Toky
Description: 基于机器学习的恶意URL检测
"""
import copy
import pickle

import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression


def csv_data_read(csv_file_path):
    # 为减少训练时间,可只取头部10W条,但一定需要先打乱样本)
    # df_csv = pd.read_csv(csv_file_path).head(100000)
    df_csv = pd.read_csv(csv_file_path)
    urls = []
    labels = []
    for index, row in df_csv.iterrows():
        urls.append(row["url"])
        labels.append(row["label"])
    return urls, labels


def url_tokenize(url):
    """
    对URL进行清洗,删除斜线、点、和com,进行分词
    :param url:
    :return:
    """
    web_url = url.lower()
    dot_slash = []
    slash = str(web_url).split('/')
    for i in slash:
        r1 = str(i).split('-')
        token_slash = []
        for j in range(0,len(r1)):
            r2 = str(r1[j]).split('.')
            token_slash = token_slash + r2
        dot_slash = dot_slash + r1 + token_slash
    urltoken_list = list(set(dot_slash))
    white_words = ["com", "http:", "https:", ""]
    for white_word in white_words:
        if white_word in urltoken_list:
            urltoken_list.remove(white_word)
    return urltoken_list


def dump_model_object(file_path, model_object):
    """
    使用pickle将内存中的对象转换为文本流保存为本地文件
    :param file_path:
    :return:
    """
    with open(file_path, "wb") as f:
        pickle.dump(model_object, f)
    f.close()


if __name__ == '__main__':
    """
    加载数据集
    """
    grep_csv_file_path = "../../data/data-0x3/grey-url.csv"
    black_csv_file_path = "../../data/data-0x3/black-url.csv"
    grey_urls, y = csv_data_read(grep_csv_file_path)

    """
    使用TF-IDF算法提取关键词特征,并将数据帧划分为训练集和测试集
    """
    url_vectorizer = TfidfVectorizer(tokenizer=url_tokenize)
    url_vectorizer_save = copy.deepcopy(url_vectorizer)
    x = url_vectorizer.fit_transform(grey_urls)
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=42)

    """
    对数据帧执行逻辑回归,将拟合后的模型和向量保存
    """
    # 对训练集和测试集执行逻辑回归
    l_regress = LogisticRegression(solver='liblinear')
    l_regress.fit(x_train, y_train)
    l_score = l_regress.score(x_test, y_test)
    print("测试拟合分数为:{0}".format(l_score))

    file_mode = "../../model/model-0x3/model.pkl"
    dump_model_object(file_mode, l_regress)
    file_vector = "../../model/model-0x3/vector.pl"
    dump_model_object(file_vector, url_vectorizer_save)

概念补充

逻辑回归

逻辑回归模型通过逻辑函数对数据进行分类,通常包括用于估计逻辑模型结果的独立二元变量。相比于线性回归,逻辑回归处理的分类问题,输出的结果为离散值;而线性回归解决的是回归问题输出的是连续值。

详细解读可以参考文章[1],注意,虽然该算法在用起来时显得非常简单,但是其原理中的细节部分还是很多的,感兴趣可以仔细研究一下。

支持向量机(SVM)

支持向量机(Surport Vector Machine, SVM)同样用于分类,是一个二元分类算法,但修改后也支持多分类问题。支持向量机通过在高维空间中创建最佳超平面来实现,这个超平面创建的划分被称为类。

对于分类问题本质的理解,就是我们需要找到一个划分的超平面,让数据尽可能多地分布在这个平面的两侧,从而实现分类的效果。但在实际数据下,往往存在多个超平面,那么此时我们怎么取舍呢?就是比较容易分类错误的数据点,而这些点就是离平面很近的点,因为离平面很远的点是相差很大的,基本不会存在分类错误的情况。而SVM的核心思想就是如此,找到离平面很近的、容易分类错误的点,然后想办法让这些数据点离平面距离变远。那些离超平面很近的点也就被称为支持向量(Support Vector)。

详细的数学原理,可以参考文章[2],该算法有比较完备的数学理论支撑的,但详细的数理和推倒也相对比较复杂,因此也可以看自己需要进行学习(~~其实就是我看不懂,不献丑来推导了~~)。

Reference

[1]【机器学习】逻辑回归(非常详细),阿泽,https://zhuanlan.zhihu.com/p/74874291

[2] 05 SVM - 支持向量机 - 概念、线性可分,白尔摩斯,https://www.jianshu.com/p/410a56129757