一个Web访问记录的成分是比较固定的,每个部分(方法、路径、参数、HTTP头、Cookie等)都有比较好的结构化特点。因此可以把Web攻击识别任务抽象为文本分类任务,而且这种思路应用在了安全领域,如有监督的攻击识别[1]、 XSS识别[2] 等。文本分类任务中常用的向量化手段有词袋模型(Bag of Word,BOW)、TF-IDF模型、词向量化(word2vec)等,兜哥的文章[3]已经做了详细的讲解。
经过对Web日志特点的分析,本文认为使用TF-IDF来对样本进行向量化效果更好。一是经过标准化后请求参数的值仍会有非常多的可能性,这种情况下词袋模型生成的特征向量长度会非常大,而且没法收缩;二是每个请求中参数个数有大有小,绝大多数不超过10个,这个时候词向量能表达的信息非常有限,并不能反映出参数value的异常性;三是TF-IDF可以表达出不同请求同一参数的值是否更有特异性,尤其是IDF项。
举个例子, ?ipAddr=8.8.8.8 是一个查询IP详细信息的页面(真实存在),在某一段时间内收到了10000个请求,其中9990个请求中ipAddr参数值是符合xx.xx.xx.xx这个IP的格式的,通过0×2中提到的标准化之后,也就是9990个请求的ipAddr参数为n+.n+.n+.n+ (当然这里做了简化,数字不一定为多位)。此外有10个请求的ipAddr是形如alert('XSS')、'or '1' = '1之类的不同的攻击Payload。
经过TF-IDF向量化后,那9900个请求ipAddr=n+.n+.n+.n+这一项的TF-IDF值:
TF-IDF normal = TF * IDF = 1 * log(10000/(9990+1)) = 0.001
而出现ipAddr=alert('XSS')的请求的TF-IDF值:
TF-IDF abnormal = TF * IDF = 1 * log(10000/(1+1)) = 8.517
可以看出异常请求参数value的TF-IDF是远大于正常请求的,因此TF-IDF可以很好地反映出参数value的异常程度。
熟悉TF-IDF的同学一定有疑问了,你这TF-IDF的字典也会很大呀,如果样本量很大而且有各式各样的参数value,你的特征向量岂不是稀疏得不行了?对于这个问题,我有一个解决方案,也就是将所有的TF-IDF进一步加以处理,对参数key相同的TF-IDF项进行求和。设参数key集合为K={k1, k2, …, kn},TF-IDF字典为集合x={x1, x2, …, xm}。则每个参数key的特征值为:
vn = ∑TF-IDFxn xn∈{x | x startswith ‘kn=’}
具体代码在vectorize/vectorizer.py中:
for path, strs in path_buckets.items(): if not strs: continue vectorizer = TfidfVectorizer(analyzer='word', token_pattern=r"(?u)\b\S\S+\b") try: tfidf = vectorizer.fit_transform(strs) #putting same key's indices together paramindex = {} for kv, index in vectorizer.vocabulary.items(): k = kv.split('=')[0] if k in param_index.keys(): param_index[k].append(index) else: param_index[k] = [index] #shrinking tfidf vectors tfidf_vectors = [] for vector in tfidf.toarray(): v = [] for param, index in param_index.items(): v.append(np.sum(vector[index])) tfidf_vectors.append(v) #other features other_vectors = [] for str in strs: ov = [] kvs = str.split(' ')[:-1] lengths = np.array(list(map(lambda x: len(x), kvs))) #param count ov.append(len(kvs)) #mean kv length ov.append(np.mean(lengths)) #max kv length ov.append(np.max(lengths)) #min kv length ov.append(np.min(lengths)) #kv length std ov.append(np.std(lengths)) other_vectors.append(ov) tfidf_vectors = np.array(tfidf_vectors) other_vectors = np.array(other_vectors) vectors = np.concatenate((tfidf_vectors, other_vectors), axis=1)