특징 추출
데이터라벨링까지 마쳤다면 이제 각 데이터의 특징 추출을 한다.
특징이란 각 데이터가 가지는 다른 데이터와 구별되는 요소로서, 이를 통해 악성코드 또는 정상프로그램의 차이점을 파악하여 모델이 학습하도록 한다.
예) 악성코드의 경우 특정한 라이브러리를 import 한다 , 이런 기능을 사용한다 등
이 책에서 말하는 특징추출 방법은 세 가지가 있다.
첫번째는 pe헤더 이다.
pe헤더에는 방금 말했던 것처럼 어떤 라이브러리를 import하는지에 대한 정보가 담겨있다.
이를 통해 프로그램이 사용하는 기능이 무엇인지 추측할 수 있다.
ClaMP라는 오픈소스를 사용하여 특징추출을 하고 csv파일을 생성해 본다.
책에서 제공하는 실습자료인 pe_hearders.py를 사용하면 된다.
오픈소스인 CLamP에서 제공하는데, 현재 새로운 추출코드가 있으나 프로젝트 호환성을 위해 기존의 코드를 사용하도록 한다.
혹시 새 코드를 사용하실 분들은 아래의 url에서 소스코드를 다운로드할 수 있다.
https://github.com/urwithajit9/ClaMP
먼저 PJ1_malware 폴더를 통째로 우분투의 /home/stud 에 복사한다.
pe_headers.py 코드는 파이썬 2를 기반으로 작성되어 있으므로 파이썬 3 환경에 맞지 않는다.
그래서 print구문에 괄호를 집어넣는 등의 간단한 수정을 해주었다.
그런데.. 그 뒤부터 무한 오류의 굴레에 빠졌다....
! 아래부터는 모든 오류 내용을 기록하여 포스트가 상당히 기니 이 점 참고해 주시길 바랍니다.!
오류사항
이제 python3 pe_headers.py 명령어를 입력해 실행해 보았다.
pefile 패키지가 설치되어있지 않다고 했다. 분명 이전환경 구축 때 설치 했었는데 모듈이 없다고 하니 다시 설치해 주었다.
yara도 없다 하여 설치해 줬다.
실행해 보니libyara.so 파일이 자꾸 없다고 한다.
책을 확인해보니 같은 문제가 있었으며 find 명령어로 libyara.so 파일을 찾아 해당 디렉터리로 이동시켜주면 해결이 된다고 기술되어 있었다.
sudo find / -name libyara.so
그런데 야라파일을 find로 검색해도 없다고 나왔다.
libyara.so는 보통 /usr/lib 또는 /usr/local/lib 에 위치한다. 찾아보았으나 보이지 않았다.
구글에 검색을 해보니 비슷한 문제로yara, 또는 yara-python 패키지를 재설치하니 생겼다고 하여 설치를 해보았지만 …
sudo apt-get install yara
pip install yara-python
그래도 생성되지 않았다..
그래서 마지막으로 chat gpt에 질의를 해서 해결방법을 찾아냈다.
터미널을 다시 닫고 재시작한 뒤 아래 명령어들을 순서대로 입력해주어야 한다.
conda activate mlsec_3811
sudo apt-get install automake libtool make gcc
./bootstrap.sh
./configure
make
sudo make install
pip uninstall yara
pip install yara-python
cd /usr/local/lib
드디어 libyara.so 가 보인다!
이제 이 파일을 이동시킨다.
cp /usr/local/lib/libyara.so /home/stud/.conda/envs/mlsec_3811/lib/libyara.so
이제 실행이 될까?
cd..
cd PJ1_malware
cd 2-feature_eng
/home/stud/normal_labled (특징을 추출할 파일이 있는 절대 경로를 입력)
normal_pe.csv (추출할 csv파일 이름을 입력)
0 (악성코드라면 1, 정상프로그램이라면 0)
'Structure' object has no attribute 'BaseOfData'
읽어 들인PE파일에 해당하는 속성(헤더)이 없을 경우 나타난다. 무시해도 된다.
실행이 잘 되나 했더니 도중에 멈추어버리고 말았다.
특징추출추 csv파일을 열어보니 md5 특성이제대로 추출되지 않은 것을 확인할 수 있었다.
<bound method pe_features.get MD5 of <__main__.pe_features object at 0xffff 991094f0>
찾아보니 getMD5 메서드를 호출할 때 파일 경로를 매개변수로 넣어주지 않아 생기는 문제였다.
잘못된 코드:
hash_ = features.getMD5 # 메서드 자체를 참조함 (괄호가 없음)
올바른 코드:
hash_ = features.getMD5(filepath) # getMD5 메서드를 호출할 때 filepath을 매개변수로 전달
또 다른 에러로그
검색해 보니unicodedecodeerror 오류는 UTF-8로 디코딩할 수 없는 바이트(0x90)들을 포함하고 있기 때문에 발생한다고 한다.
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x90 in position 7: invalid start byte
특징추출한 파일을 앞으로 사용할 것에 대비해서 그냥 오류를 무시하고 진행할 것인지, 아니면 최대한 살려서 추출할 것인지에 대해 고민했는데, 최대한 살리기로 결정을 했다.
그러기 위해서는 파일 디코딩 시utf-8 대신 utf-16 으로 디코딩하도록 코드를 수정해야 한다.
최종 수정한 코드
import csv,os,pefile
import yara
import math
import hashlib
class pe_features():
IMAGE_DOS_HEADER = [
"e_cblp",
"e_cp",
"e_cparhdr",
"e_maxalloc",
"e_sp",
"e_lfanew"]
FILE_HEADER= ["NumberOfSections","CreationYear"] + [ "FH_char" + str(i) for i in range(15)]
OPTIONAL_HEADER1 = [
"MajorLinkerVersion",
"MinorLinkerVersion",
"SizeOfCode",
"SizeOfInitializedData",
"SizeOfUninitializedData",
"AddressOfEntryPoint",
"BaseOfCode",
"BaseOfData",
"ImageBase",
"SectionAlignment",
"FileAlignment",
"MajorOperatingSystemVersion",
"MinorOperatingSystemVersion",
"MajorImageVersion",
"MinorImageVersion",
"MajorSubsystemVersion",
"MinorSubsystemVersion",
"SizeOfImage",
"SizeOfHeaders",
"CheckSum",
"Subsystem"]
OPTIONAL_HEADER_DLL_char = [ "OH_DLLchar" + str(i) for i in range(11)]
OPTIONAL_HEADER2 = [
"SizeOfStackReserve",
"SizeOfStackCommit",
"SizeOfHeapReserve",
"SizeOfHeapCommit",
"LoaderFlags"] # boolean check for zero or not
OPTIONAL_HEADER = OPTIONAL_HEADER1 + OPTIONAL_HEADER_DLL_char + OPTIONAL_HEADER2
Derived_header = ["sus_sections","non_sus_sections", "packer","packer_type","E_text","E_data","filesize","E_file","fileinfo"]
def __init__(self,source,output,label):
self.source = source
self.output = output
self.type = label
self.rules= yara.compile(filepath='./peid.yara')
def file_creation_year(self,seconds):
tmp = 1970 + ((int(seconds) / 86400) / 365)
return int(tmp in range (1980,2016))
def FILE_HEADER_Char_boolean_set(self,pe):
tmp = [pe.FILE_HEADER.IMAGE_FILE_RELOCS_STRIPPED,
pe.FILE_HEADER.IMAGE_FILE_EXECUTABLE_IMAGE,
pe.FILE_HEADER.IMAGE_FILE_LINE_NUMS_STRIPPED,
pe.FILE_HEADER.IMAGE_FILE_LOCAL_SYMS_STRIPPED,
pe.FILE_HEADER.IMAGE_FILE_AGGRESIVE_WS_TRIM,
pe.FILE_HEADER.IMAGE_FILE_LARGE_ADDRESS_AWARE,
pe.FILE_HEADER.IMAGE_FILE_BYTES_REVERSED_LO,
pe.FILE_HEADER.IMAGE_FILE_32BIT_MACHINE,
pe.FILE_HEADER.IMAGE_FILE_DEBUG_STRIPPED,
pe.FILE_HEADER.IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP,
pe.FILE_HEADER.IMAGE_FILE_NET_RUN_FROM_SWAP,
pe.FILE_HEADER.IMAGE_FILE_SYSTEM,
pe.FILE_HEADER.IMAGE_FILE_DLL,
pe.FILE_HEADER.IMAGE_FILE_UP_SYSTEM_ONLY,
pe.FILE_HEADER.IMAGE_FILE_BYTES_REVERSED_HI
]
return [int(s) for s in tmp]
def OPTIONAL_HEADER_DLLChar(self,pe):
tmp = [
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_NX_COMPAT ,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_NO_ISOLATION,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_NO_SEH,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_NO_BIND,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_WDM_DRIVER,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_APPCONTAINER,
pe.OPTIONAL_HEADER.IMAGE_DLLCHARACTERISTICS_GUARD_CF
]
return [int(s) for s in tmp]
def Optional_header_ImageBase(self,ImageBase):
result= 0
if ImageBase % (64 * 1024) == 0 and ImageBase in [268435456,65536,4194304]:
result = 1
return result
def Optional_header_SectionAlignment(self,SectionAlignment,FileAlignment):
return int(SectionAlignment >= FileAlignment)
def Optional_header_FileAlignment(self,SectionAlignment,FileAlignment):
result =0
if SectionAlignment >= 512:
if FileAlignment % 2 == 0 and FileAlignment in range(512,65537):
result =1
else:
if FileAlignment == SectionAlignment:
result = 1
return result
def Optional_header_SizeOfImage(self,SizeOfImage,SectionAlignment):
return int(SizeOfImage % SectionAlignment == 0)
def Optional_header_SizeOfHeaders(self,SizeOfHeaders,FileAlignment):
return int(SizeOfHeaders % FileAlignment == 0 )
def extract_dos_header(self,pe):
IMAGE_DOS_HEADER_data = [ 0 for i in range(6)]
try:
IMAGE_DOS_HEADER_data = [
pe.DOS_HEADER.e_cblp,
pe.DOS_HEADER.e_cp,
pe.DOS_HEADER.e_cparhdr,
pe.DOS_HEADER.e_maxalloc,
pe.DOS_HEADER.e_sp,
pe.DOS_HEADER.e_lfanew]
except Exception as e:
print(e)
return IMAGE_DOS_HEADER_data
def extract_file_header(self,pe):
FILE_HEADER_data = [ 0 for i in range(3)]
FILE_HEADER_char = []
try:
FILE_HEADER_data = [
pe.FILE_HEADER.NumberOfSections,
self.file_creation_year(pe.FILE_HEADER.TimeDateStamp)]
FILE_HEADER_char = self.FILE_HEADER_Char_boolean_set(pe)
except Exception as e:
print(e)
return FILE_HEADER_data + FILE_HEADER_char
def extract_optional_header(self,pe):
OPTIONAL_HEADER_data = [ 0 for i in range(21)]
DLL_char =[]
OPTIONAL_HEADER_data2 = [ 0 for i in range(6)]
try:
OPTIONAL_HEADER_data = [
pe.OPTIONAL_HEADER.MajorLinkerVersion,
pe.OPTIONAL_HEADER.MinorLinkerVersion,
pe.OPTIONAL_HEADER.SizeOfCode,
pe.OPTIONAL_HEADER.SizeOfInitializedData,
pe.OPTIONAL_HEADER.SizeOfUninitializedData,
pe.OPTIONAL_HEADER.AddressOfEntryPoint,
pe.OPTIONAL_HEADER.BaseOfCode,
pe.OPTIONAL_HEADER.BaseOfData,
self.Optional_header_ImageBase(pe.OPTIONAL_HEADER.ImageBase),
self.Optional_header_SectionAlignment(pe.OPTIONAL_HEADER.SectionAlignment,pe.OPTIONAL_HEADER.FileAlignment),
self.Optional_header_FileAlignment(pe.OPTIONAL_HEADER.SectionAlignment,pe.OPTIONAL_HEADER.FileAlignment),
pe.OPTIONAL_HEADER.MajorOperatingSystemVersion,
pe.OPTIONAL_HEADER.MinorOperatingSystemVersion,
pe.OPTIONAL_HEADER.MajorImageVersion,
pe.OPTIONAL_HEADER.MinorImageVersion,
pe.OPTIONAL_HEADER.MajorSubsystemVersion,
pe.OPTIONAL_HEADER.MinorSubsystemVersion,
self.Optional_header_SizeOfImage(pe.OPTIONAL_HEADER.SizeOfImage,pe.OPTIONAL_HEADER.SectionAlignment),
self.Optional_header_SizeOfHeaders(pe.OPTIONAL_HEADER.SizeOfHeaders,pe.OPTIONAL_HEADER.FileAlignment),
pe.OPTIONAL_HEADER.CheckSum,
pe.OPTIONAL_HEADER.Subsystem]
DLL_char = self.OPTIONAL_HEADER_DLLChar(pe)
OPTIONAL_HEADER_data2= [
pe.OPTIONAL_HEADER.SizeOfStackReserve,
pe.OPTIONAL_HEADER.SizeOfStackCommit,
pe.OPTIONAL_HEADER.SizeOfHeapReserve,
pe.OPTIONAL_HEADER.SizeOfHeapCommit,
int(pe.OPTIONAL_HEADER.LoaderFlags == 0) ]
except Exception as e:
print(e)
return OPTIONAL_HEADER_data + DLL_char + OPTIONAL_HEADER_data2
def get_count_suspicious_sections(self,pe):
result=[]
tmp =[]
benign_sections = set(['.text','.data','.rdata','.idata','.edata','.rsrc','.bss','.crt','.tls'])
for section in pe.sections:
#tmp.append(section.Name.decode().split('\x00')[0])
tmp.append(section.Name.decode('latin-1').split('\x00')[0])
non_sus_sections = len(set(tmp).intersection(benign_sections))
result=[len(tmp) - non_sus_sections, non_sus_sections]
return result
def check_packer(self,filepath):
result=[]
matches = self.rules.match(filepath)
try:
if matches == [] or matches == {}:
result.append([0,"NoPacker"])
else:
result.append([1,matches['main'][0]['rule']])
except:
result.append([1,matches[0]])
return result
def get_text_data_entropy(self,pe):
result=[0.0,0.0]
for section in pe.sections:
#s_name = section.Name.decode().split('\x00')[0]
s_name = section.Name.decode('latin-1').split('\x00')[0]
if s_name == ".text":
result[0]= section.get_entropy()
elif s_name == ".data":
result[1]= section.get_entropy()
else:
pass
return result
def get_file_bytes_size(self,filepath):
with open(filepath, "rb") as f:
byteArr = list(f.read())
fileSize = len(byteArr)
return byteArr,fileSize
def cal_byteFrequency(self,byteArr,fileSize):
freqList = []
for b in range(256):
ctr = 0
for byte in byteArr:
if byte == b:
ctr += 1
freqList.append(float(ctr) / fileSize)
return freqList
def get_file_entropy(self,filepath):
byteArr, fileSize = self.get_file_bytes_size(filepath)
freqList = self.cal_byteFrequency(byteArr,fileSize)
ent = 0.0
for freq in freqList:
if freq > 0:
ent += - freq * math.log(freq, 2)
return [fileSize,ent]
def get_fileinfo(self,pe):
result=[]
try:
FileVersion = pe.FileInfo[0].StringTable[0].entries['FileVersion']
ProductVersion = pe.FileInfo[0].StringTable[0].entries['ProductVersion']
ProductName = pe.FileInfo[0].StringTable[0].entries['ProductName']
CompanyName = pe.FileInfo[0].StringTable[0].entries['CompanyName']
FileVersionLS = pe.VS_FIXEDFILEINFO.FileVersionLS
FileVersionMS = pe.VS_FIXEDFILEINFO.FileVersionMS
ProductVersionLS = pe.VS_FIXEDFILEINFO.ProductVersionLS
ProductVersionMS = pe.VS_FIXEDFILEINFO.ProductVersionMS
except Exception as e:
result=["error"]
else:
FileVersion = (FileVersionMS >> 16, FileVersionMS & 0xFFFF, FileVersionLS >> 16, FileVersionLS & 0xFFFF)
ProductVersion = (ProductVersionMS >> 16, ProductVersionMS & 0xFFFF, ProductVersionLS >> 16, ProductVersionLS & 0xFFFF)
result = [FileVersion,ProductVersion,ProductName,CompanyName]
return int(result[0] != 'error')
def write_csv_header(self):
filepath = self.output
HASH = ['filename', 'MD5']
header = HASH + self.IMAGE_DOS_HEADER + self.FILE_HEADER + self.OPTIONAL_HEADER + self.Derived_header
header.append("class")
with open(filepath,"w", newline='') as csv_file:
writer = csv.writer(csv_file, delimiter=',')
writer.writerow(header)
def extract_all(self,filepath):
data =[]
try:
pe = pefile.PE(filepath)
except Exception as e:
print("{} while opening {}".format(e,filepath))
else:
data += self.extract_dos_header(pe)
data += self.extract_file_header(pe)
data += self.extract_optional_header(pe)
num_ss_nss = self.get_count_suspicious_sections(pe)
data += num_ss_nss
packer = self.check_packer(filepath)
data += packer[0]
entropy_sections = self.get_text_data_entropy(pe)
data += entropy_sections
f_size_entropy = self.get_file_entropy(filepath)
data += f_size_entropy
fileinfo = self.get_fileinfo(pe)
data.append(fileinfo)
data.append(self.type)
return data
def write_csv_data(self,data):
filepath = self.output
with open(filepath,"a", newline='') as csv_file:
writer = csv.writer(csv_file, delimiter=',')
writer.writerow(data)
def getMD5(self, filepath):
with open(filepath, 'rb') as fh:
m = hashlib.md5()
while True:
data = fh.read(8192)
if not data:
break
m.update(data)
return m.hexdigest()
def create_dataset(self):
self.write_csv_header()
count = 0
for file in os.listdir(self.source):
filepath = os.path.join(self.source, file)
data = self.extract_all(filepath)
hash_ = self.getMD5(filepath)
print("hash: ", hash_)
data.insert(0, hash_)
data.insert(0, file)
self.write_csv_data(data)
count += 1
print("Successfully Data extracted and written for {}.".format(file))
print("Processed {} files".format(count))
def main():
source_path = input("Enter the path of samples (ending with /) >> ")
output_file = input("Give file name of output file (.csv) >> ")
label = input("Enter type of sample (malware(1)|benign(0)) >> ")
features = pe_features(source_path, output_file, label)
features.create_dataset()
if __name__ == '__main__':
main()
수정점
printf 를 파이썬 3에 맞게 수정했다
디코딩시 utf-16을 사용하도록 수정하였다.
hash 메소드 호출 시 파일 경로를 매개변수로 전달한다.
긴가민가한 마음으로 다시 코드를 실행해 보았다.
드디어! 모든 특징추출이 완료되었다.
참고
too many matches for string 경고
YARA 규칙을 사용하여 PE 파일을 분석하는 과정에서 발생한 경고이다. rules.match(filepath) 함수가 호출될 때 특정 YARA 규칙에 정의된 특정 문자열 패턴이 너무 많이 일치되어 처리하는데 오래 걸린다는 이야기이다.
-> 프로그램에러가 아니며 꽤 오래 걸리지만 기다리고 있으면 특징이 추출된다.