利用機器學習進行惡意程式碼分類

wyzsk發表於2020-08-19
作者: bindog · 2015/08/20 10:23

最近在Kaggle上微軟發起了一個惡意程式碼分類的比賽,並提供了超過500G的資料(解壓後)。有意思的是,取得第一名的隊伍三個人都不是搞安全出身的,所採用的方法與我們常見的方法存在很大不同,展現了機器學習在安全領域的巨大潛力。在仔細讀完他們的程式碼和相關的論文後,我簡單的進行了一些總結與大家分享。

需要指出的是,(1)比賽的主題是惡意程式碼的分類,不是病毒查殺(2)比賽採用的方法是純靜態分析的方法,不涉及行為分析等動態分析方法。

因此這不意味著這個方法能夠取代現有的方法,但是瞭解它能夠為安全研究人員提供一個嶄新的思路,至於能否在工業界應用仍待進一步研究。

0x01 總覽


背景

80年代末期,隨著惡意程式碼的誕生,反惡意程式碼軟體(或稱反病毒軟體)隨之誕生。這個時期的惡意程式碼所採用的技術較為簡單,這使得對應的檢測技術也較為容易,早期的反病毒軟體大都單一的採用特徵匹配的方法,簡單的利用特徵串完成檢測。隨著惡意程式碼技術的發展,惡意程式碼開始在傳播過程中進行變形以躲避查殺,此時同一個惡意程式碼的變種數量急劇提升,形態較本體也發生了較大的變化,反病毒軟體已經很難提取出一段程式碼作為惡意程式碼的特徵碼。在這種情況下,廣譜特徵碼隨之誕生,廣譜特徵碼將特徵碼進行了分段,透過掩碼位元組對需要進行比較的和不需要進行比較的區段進行劃分。然而無論是特徵碼掃描還是廣譜特徵,都需要在獲得惡意程式碼樣本後,進行特徵的提取,隨後才能進行檢測,這使得對惡意程式碼的查殺具有一定的滯後性,始終走在惡意程式碼的後面。為了針對變種病毒和未知病毒,啟發式掃描應運而生,啟發式掃描利用已有的經驗和知識對未知的二進位制程式碼進行檢測,這種技術抓住了惡意程式碼具有普通二進位制檔案所不具有的惡意行為,例如非常規讀寫檔案,終結自身,非常規切入零環等等。啟發式掃描的重點和難點在於如何對惡意程式碼的惡意行為特徵進行提取。特徵碼掃描、查詢廣譜特徵、啟發式掃描,這三種查殺方式均沒有實際執行二進位制檔案,因此均可歸為惡意程式碼靜態檢測的方法。隨著反惡意程式碼技術的逐步發展,主動防禦技術、雲查殺技術已越來越多的被安全廠商使用,但惡意程式碼靜態檢測的方法仍是效率最高,被運用最廣泛的惡意程式碼查殺技術。

資料格式

微軟提供的資料包括訓練集、測試集和訓練集的標註。其中每個惡意程式碼樣本(去除了PE頭)包含兩個檔案,一個是十六進位制表示的.bytes檔案,另一個是利用IDA反彙編工具生成的.asm檔案。如下圖所示

enter image description here

方法簡述

Kaggle比賽中最重要的環節就是特徵工程,特徵的好壞直接決定了比賽成績。在這次Kaggle的比賽中冠軍隊伍選取了三個“黃金”特徵:惡意程式碼影像、OpCode n-gramHeaders個數,其他一些特徵包括ByteCode n-gram,指令頻數等。機器學習部分採用了隨機森林演算法,並用到了xgboostpypy加快訓練速度。

本文主要關注惡意程式碼影像和OpCode n-gram,以及隨機森林演算法的應用。

0x02 惡意程式碼影像


這個概念最早是2011年由加利福尼亞大學的NatarajKarthikeyan在他們的論文 Malware Images: Visualization and Automatic Classification 中提出來的,思路非常新穎,把一個二進位制檔案以灰度圖的形式展現出來,利用影像中的紋理特徵對惡意程式碼進行聚類。此後,有許多研究人員在這個思路基礎上進行了改進和探索。就目前發表的文章來看,惡意程式碼影像的形式並不固定,研究人員可根據實際情況進行調整和創新。

國內這方面的研究較少,去年在通訊學報上有一篇《基於紋理指紋的惡意程式碼變種檢測方法研究》,是由北京科技大學的韓曉光博士和北京啟明星辰研究院等合作發表的,目測也是僅有的一篇。

本節介紹最簡單的一種惡意程式碼影像繪製方法。對一個二進位制檔案,每個位元組範圍在00~FF之間,剛好對應灰度圖0~255(0為黑色,255為白色)。將一個二進位制檔案轉換為一個矩陣(矩陣元素對應檔案中的每一個位元組,矩陣的大小可根據實際情況進行調整),該矩陣又可以非常方便的轉換為一張灰度圖。

python程式碼如下

#!python
import numpy
from PIL import Image
import binascii
def getMatrixfrom_bin(filename,width):
    with open(filename, 'rb') as f:
        content = f.read()
    hexst = binascii.hexlify(content)  #將二進位制檔案轉換為十六進位制字串
    fh = numpy.array([int(hexst[i:i+2],16) for i in range(0, len(hexst), 2)])  #按位元組分割
    rn = len(fh)/width
    fh = numpy.reshape(fh[:rn*width],(-1,width))  #根據設定的寬度生成矩陣
    fh = numpy.uint8(fh)
    return fh
filename = "your_bin_filename"
im = Image.fromarray(getMatrixfrom_bin(filename,512)) #轉換為影像
im.save("your_img_filename.png")

利用該程式碼生成的幾種病毒樣本影像如下所示

enter image description here

用肉眼可看出,同一個家族的惡意程式碼影像在紋理上存在一定的相似性,不同的惡意程式碼家族是有一定區別的。如何用計算機發現和提取這些紋理的相似特徵用以分類呢?這就需要用到計算機視覺裡的一些技術了。在NatarajKarthikeyan的論文中採用的是GIST特徵GIST特徵常用於場景分類任務(如城市、森林、公路和海灘等),用一個五維的感知維度來代表一個場景的主要內容(詳情請參考文獻[xx])。簡單來說,輸入影像,輸出為對應的GIST描述符,如下圖所示

enter image description here

matlab實現裡面每個影像的GIST描述符都用一個向量表示,最後用SVM完成分類訓練。

enter image description here

這已經遠遠超出了場景識別所能做的。不過,國外有學者利用一些類似前文生成那種不規則影像來欺騙深度學習模型,如下圖所示

enter image description here

詳情請參考@王威廉老師的微博。當然,二者並沒有什麼直接關聯,因為基於深度學習的影像識別系統的訓練資料是一些有意義的影像。但這是一個非常有意思的巧合,至於基於深度學習的影像識別能否用於惡意程式碼影像的特徵提取和分類,我認為是一個潛在的研究點,所能做的也不侷限於此,如果有做深度學習的朋友可以夥同做安全的朋友一起研究交流。

0x03 OpCode n-gram


n-gram是自然語言處理領域的概念,早期的語音識別技術和統計語言模型與它密不可分。n-gram基於一個簡單的假設,即認為一個詞出現的機率僅與它之前的n-1個詞有關,這個機率可從大量語料中統計得到。例如“吃”的後面出現“蘋果”或“披薩”的機率就會比“公路”的機率大(正常的語料中基本不會出現“吃公路”這種組合),可以看出n-gram在一定程度上包含了部分語言特徵。

n-gram應用於惡意程式碼識別的想法最早由Tony等人在2004年的論文N-gram-based Detection of New Malicious Code 中提出,不過他們的方法是基於ByteCode的。2008年Moskovitch等人的論文Unknown Malcode Detection Using OPCODE Representation 中提出利用OpCode代替ByteCode更加科學。

具體來說,一個二進位制檔案的OpCode n-gram如下圖所示

enter image description here

針對這次Kaggle比賽提供的資料,用python提取出其n-gram特徵

#!python
import re
from collections import *
# 從.asm檔案獲取Opcode序列
def getOpcodeSequence(filename):
    opcode_seq = []
    p = re.compile(r'\s([a-fA-F0-9]{2}\s)+\s*([a-z]+)')
    with open(filename) as f:
        for line in f:
            if line.startswith(".text"):
                m = re.findall(p,line)
                if m:
                    opc = m[0][10]
                    if opc != "align":
                        opcode_seq.append(opc)
    return opcode_seq
# 根據Opcode序列,統計對應的n-gram
def getOpcodeNgram(ops ,n = 3):
    opngramlist = [tuple(ops[i:i+n]) for i in range(len(ops)-n)]
    opngram = Counter(opngramlist)
    return opngram
file = "train/0A32eTdBKayjCWhZqDOQ.asm"
ops = getOpcodeSequence(file)
opngram = getOpcodeNgram(ops)
print opngram
# output
# Counter({('mov', 'mov', 'mov'): 164, ('xor', 'test', 'setnle'): 155...

0x04 決策樹和隨機森林


決策樹

決策樹在我們日常生活中無處不在,在眾多機器學習的書籍中提到的一個例子(銀行預測客戶是否有能力償還貸款)如下圖所示

enter image description here

在這個在決策樹中,非葉子結點如“擁有房產”、“是否結婚”就是所謂的特徵,它們是依靠我們的知識人工提取出來的特徵。但如果對某個領域不瞭解,特徵數量又較多時,人工提取特徵的方法就不可行了,需要依靠演算法來尋找合適的特徵構造決策樹。

限於篇幅,決策樹的構造等過程本文不進行展開,網上相關資源非常多。(只要能夠充分理解熵和資訊增益的概念,決策樹其實非常簡單)

隨機森林

隨機森林是一個非常強大的機器學習方法,顧名思義,它是用隨機的方式建立一個森林,森林裡面有很多的決策樹組成,隨機森林的每一棵決策樹之間是沒有關聯的。在得到森林之後,當有一個新的輸入樣本進入的時候,就讓森林中的每一棵決策樹分別進行一下判斷,預測這個樣本應該屬於哪一類(對於分類演算法),然後看看哪一類被選擇最多,就預測這個樣本為那一類。

隨機森林的思想與Adaboost裡面的弱分類器組合成強分類器的思想類似,是一種“集體智慧”的體現。例如,屋子裡面有n個人,每個人作出正確判斷的機率為p(p略高於0.5,這時每個人可視為一個弱分類器),他們判斷的過程獨立互不影響,最終以多數人的判斷為準。這裡我們不從數學上來推導,類比拋硬幣,對一枚均勻的硬幣,拋n次的結果中,正面和反面的次數是差不多的。而對一枚不均勻的硬幣,若出現正面的機率略大於反面,拋n次的結果中出現正面次數比反面次數多的機率就會很大。所以即使每個分類器的準確度不高,但是結合在一起時就可以變成一個強分類器。

enter image description here

如圖所示,將訓練資料有放回的抽樣出多個子集(即隨機選擇矩陣中的行),當然在特徵選擇上也可以進行隨機化(即隨機選擇矩陣中的列,圖中沒有體現出來),分別在每個子集上生成對應的決策樹

enter image description here

決策過程如下圖所示(忽略畫風不一致的問題...)

enter image description here

0x05 冠軍隊伍的實現細節


ASM檔案影像

但是在Kaggle比賽中冠軍隊伍採用的方法並不是從二進位制檔案生成的影像,也不是從.bytes檔案,竟然是從.asm檔案生成的影像,他們也沒有使用GIST特徵,而是使用了前800個畫素值作為特徵,讓人非常費解。

我專門給隊伍裡的Jiwei Liu同學發了一封郵件進行諮詢,他給我的答覆是:GIST特徵與其他特徵綜合使用時影響整體效果,所以他們放棄了GIST特徵,另外使用.asm檔案生成影像純屬意外發現...

至於為什麼是前800個畫素,他的解釋是透過反覆交叉驗證得出的,具體原因不清楚。(在後文的分析中我會談談我的一些看法)

OpCode n-gram

這部分的實現不復雜,他們選取n=4,在具體的特徵選擇上透過計算資訊增益選取每個分類與其他分類區分度最高的750個特徵。

其他特徵

其他一些特徵包括統計Headers,Bytecode n-gram(n=2,3,4),分析指令流(將每個迴圈固定展開5次)來統計指令頻數,這些特徵雖然不像前面提到的特徵那麼有效,但是確實可以在一定程度上提升最終成績。

0x06 實驗


冠軍隊伍的程式碼是為了參加比賽而寫的,時間倉促,又是多人合作完成,導致組織結構很亂,且基本沒有註釋,可讀性較差。更重要的是自己想動手實踐一下,所以按照他們的思路寫了幾個簡單的程式,忽略了一些處理起來比較複雜或者難以理解的過程,程式碼可以在我的github上下載

由於只是做一些簡單的試驗,不需要太多的資料(否則速度會非常慢),我從微軟提供的訓練資料中抽取了大概1/10左右的訓練子集,其中從每個分類的中都隨機抽取了100個樣本(9個分類,每個樣本2個檔案,共1800個檔案),這樣也不需要用到pypyxgboost,只需要用到numpypandasPILscikit-learn這些庫即可

友情提示:要進行這個實驗,首先確保有一個比較大的硬碟,推薦使用Linux系統。

訓練子集

這一步需要提前將完整訓練集解壓好,數量龐大,時間比較久。

#!python
import os
from random import *
import pandas as pd
import shutil
rs = Random()
# 讀取微軟提供的訓練集標註
trainlabels = pd.read_csv('trainLabels.csv')
fids = []
opd = pd.DataFrame()
for clabel in range (1,10):
    # 篩選特定分類
    mids = trainlabels[trainlabels.Class == clabel]
    mids = mids.reset_index(drop=True)
    # 在該分類下隨機抽取100個
    rchoice = [rs.randint(0,len(mids)-1) for i in range(100)]
    rids = [mids.loc[i].Id for i in rchoice]
    fids.extend(rids)
    opd = opd.append(mids.loc[rchoice])
opd = opd.reset_index(drop=True)
# 生成訓練子集標註
opd.to_csv('subtrainLabels.csv', encoding='utf-8', index=False)
# 將訓練子集複製出來(根據實際情況修改這個路徑)
sbase = 'yourpath/train/'
tbase = 'yourpath/subtrain/'
for fid in fids:
    fnames = ['{0}.asm'.format(fid),'{0}.bytes'.format(fid)]
    for fname in fnames:
        cspath = sbase + fname
        ctpath = tbase + fname
        shutil.copy(cspath,ctpath)

特徵抽取

本實驗中只用到了.asm檔案,用到了.asm檔案影像特徵(前1500個畫素)和OpCode n-gram特徵(本實驗取n=3,將總體出現頻數大於500次的3-gram作為特徵保留),實現程式碼與前文基本一致,具體細節可參考完整程式碼。

scikit-learn

因為scikit-learn的存在,將機器學習演算法應用到其他領域變得非常方便快捷。例如我們已經抽取了這些惡意程式碼的OpCode n-gram特徵("3gramfeature.csv"),利用scikit-learn即可快速訓練一個隨機森林

#!python
from sklearn.ensemble import RandomForestClassifier as RF
from sklearn import cross_validation
from sklearn.metrics import confusion_matrix
import pandas as pd
subtrainLabel = pd.read_csv('subtrainLabels.csv')
subtrainfeature = pd.read_csv("3gramfeature.csv")
subtrain = pd.merge(subtrainLabel,subtrainfeature,on='Id')
labels = subtrain.Class
subtrain.drop(["Class","Id"], axis=1, inplace=True)
subtrain = subtrain.as_matrix()
# 將訓練子集劃分為訓練集和測試集 其中測試集佔40%
X_train, X_test, y_train, y_test = cross_validation.train_test_split(subtrain,labels,test_size=0.4)
# 構造隨機森林 其中包含500棵決策樹
srf = RF(n_estimators=500, n_jobs=-1)
srf.fit(X_train,y_train)  # 訓練
print srf.score(X_test,y_test)  # 測試

實驗結果

這裡只對預測的準確度做一個簡單的評判。

在只應用.asm檔案影像特徵(firstrandomforest.py)或者Opcode n-gram特徵(secondrandomforest.py)的情況下,以及二者相結合的情況(combine.py),準確率如下所示

enter image description here

由於隨機森林訓練的過程中存在一定的隨機性,因此每次結果不一定完全相同,但總的來說,二者結合的準確率通常要高出許多,基本可以達到98%以上的準確率,而且別忘了我們只用了不到1/10的資料

為什麼是前800畫素

觀察.asm檔案的格式,開頭部分其實是IDA生成的一些資訊,如下圖所示

enter image description here

可以目測這個長度已經超出了800個畫素(800個位元組),實際上這800個畫素和反彙編程式碼沒有關係!完全就是IDA產生的一些資訊,更進一步的說,實際上冠軍隊伍的方法壓根與惡意程式碼影像沒有關係,實際上是用到了IDA產生的資訊。

但是仔細觀察這些IDA資訊,貌似長的都差不多,也沒有什麼有價值的資訊,為什麼可以有效區分惡意軟體型別呢?

一個大膽的猜想是微軟提前將這些惡意程式碼分好類,在呼叫IDA進行反彙編的時候是按照每個分類的順序進行的,由於未知的原因可能導致了這些IDA資訊在不同分類上有細微差別,恰好能作為一個非常有效的特徵!

0x07 其他


  • 好的方法總是簡單又好理解的。這次Kaggle比賽也是如此,冠軍隊伍的方法沒有特別難理解的部分。但是請注意:面上的方法並不能體現背後難度和工作量,真正複雜和耗時的部分在於特徵選擇和交叉驗證上。比如他們最終放棄GIST特徵正是經過反覆對比驗證才做出的決定。
  • 這次的Kaggle比賽歸根結底還是比賽,最終目標是取得最好成績,不代表這個方法在實際中一定好用。
  • 放棄GIST特徵告訴我們一個寶貴的經驗,並不是某個特徵好就一定要用,還要考慮它和其他特徵綜合之後的效果。
  • 比賽的資料是去除了PE頭的,而輸入輸出表對分析惡意程式碼是很有幫助的,假如微軟提供的資料包含了PE頭,將輸入輸出表作為特徵,最終的結果應該還能進一步提升。
  • 這個方法的能夠發現一些靜態方法發現不了的變種,但對於未知的新品種依然無能為力(沒有資料,機器學習巧婦難為無米之炊...)
  • 可以嘗試將該方法應用到Android和IOS平臺的惡意程式碼檢測中。

0x08 資源和參考資料


比賽說明和原始資料

https://www.kaggle.com/c/malware-classification/

冠軍隊伍相關資料

本文程式碼

https://github.com/bindog/ToyMalwareClassification

參考資料

Malware Images: Visualization and Automatic Classification

Detecting unknown malicious code by applying classification techniques on OpCode patterns(牆裂推薦)

如何使用GIST+LIBLINEAR分類器提取CIFAR-10 dataset資料集中影像特徵,並用測試資料進行實驗

GIST特徵描述符使用

隨機森林演算法

How Random Forest algorithm works

Supervised Classification with k-fold Cross Validation on a Multi Family Malware Dataset

我的部落格 <http://bindog.github.io/blog/ > 我的郵箱 bindog 艾特 奧特路克 .com 歡迎大家與我交流~

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章