[보안] 나만의 백신 만들기
이번 학기에 보안 수업을 들으며 여러 실습들을 해보고 있지만 내가 가장 재미있게 할 수 있는 실습은 역시 어릴 때부터 관심을 갖고 있었던 '백신 만들기'이다.
관심은 있었으나 너무 어렵다고 생각한 탓에 도전도 안해보고 있었는데, 생각보다 매우 쉬워서 놀랐다. 물론 고차원적인 백신으로 발전한다면 어렵겠지만...
이 글은
비제이퍼블릭 출판사의 <파이썬으로 배우는 Anti-Virus 구조와 원리(이원혁 지음)>
책의 4장에 대한 실습을 진행하고 개인적인 정리를 목적으로 요약한 것임을 밝힌다. 코드는 부분적으로 수정하였다.
따라서 파이썬으로 실습하였고, 파이썬 2.7 버전을 사용하였다. 에디터로는 Pycharm community edition을 사용하였다.
하지만 딱히 파이썬을 몰라도 지장없을 정도로 코드가 단순하다.
이번 실습을 끝내면 결과적으로 완성될 백신의 코드는 아래와 같다.
다소 복잡해보이지만 실제로 짜보면 별 거 없다. 아니, 의외로 너무 쉬워서 놀랄수도??
import hashlib
import os
import sys
def makeDB():
malwareDB = {
'EICAR Test' : ['44d88612fea8a8f36de82e1278abb02f', 68],
'Malware1' : ['4d764cac3968e3bf0cf060993a5f2711', 31],
'Malware2' : ['5f781e356fe12c7cb1491af0df37eeb4', 34]
}
return malwareDB
def searchDB(hashValue, malwareDB):
for key, value in malwareDB.items():
if value[0] == hashValue:
return True, key
return False, ''
def vaccine(fileLocation):
malwareDB = makeDB()
sizeDB = map(lambda value: value[1], malwareDB.values())
fp = open(fileLocation, 'rb')
fileSize = os.path.getsize(fileLocation)
if fileSize not in sizeDB:
print fileLocation, ': Normal File'
return
fbuf = fp.read()
fp.close()
f = hashlib.md5()
f.update(fbuf)
hashValue = f.hexdigest()
isMalware, name = searchDB(hashValue, malwareDB)
if isMalware == True:
print fileLocation, ': Malware(', name, ')'
os.chmod(fileLocation, 0777)
os.remove(fileLocation)
else:
print fileLocation, ': Normal File'
if __name__ == '__main__':
if len(sys.argv) == 2:
fileLocation = sys.argv[1]
else:
fileLocation = raw_input('\nPlease enter your file Location : ')
vaccine(fileLocation)
백신 개발은 매우 큰 프로젝트이기 때문에 분할-정복 기법을 이용해 작게 시작해서 점점 크게 발전시켜가는 접근법을 취해야한다.(Start small!!)
자, 이제 Start small 해보자. 일단 하나의 바이러스에 특화된 전용백신을 개발해보자.
1. 전용백신 개발하기
전용백신 : 하나의 악성코드를 진단 및 치료하는 백신. 해당 악성코드에 대해서는 빠르고 정확하지만 범용성이 떨어진다.
(1) EICAR Test 파일 만들기
백신을 만들라면 악성코드가 있어야하니 악성코드 역할을 해줄 테스트 파일을 만든다.
아래 문장을 메모장에 복사, 붙여넣기 하고 eicar.txt로 저장한다.
참고로, 백신 프로그램이나 Windows smart screen 등이 바이러스로 탐지할 수 있는데, 이 파일을 허용해준다.
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
복사가 안되면 >> 여기서 복사!!
(2) 파일 읽기
# -*- coding:utf-8 -*- # 한글 설정
import os # os 명령어를 수행하기 위해 import
fp = open('malwares\eicar.txt', 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성
fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()
앞으로 malwares라는 폴더에 악성코드를 모아놓겠다.
(3) 패턴 매칭 진단 및 치료
먼저 악성코드로 알려진 문자열과 파일 내용을 비교(String matching)하여 진단해보겠다.
# -*- coding:utf-8 -*- # 한글 설정
import os # os 명령어를 수행하기 위해 import
fp = open('malwares\eicar.txt', 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성
fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()
if fbuf[0:3] == 'X5O': # 버퍼 내용의 첫 세 글자가 X5O이면,
print '악성코드 발견!'
os.chmod('malwares\eicar.txt', 0777) # 파일이 읽기 전용일 경우 chmod를 해주고
os.remove('malwares\eicar.txt') # 파일을 강제 삭제
else:
print '악성코드가 없음'
('X50'이 아니라 'X5O'임에 주의하시길 + fp.close()를 먼저 해주지 않을 경우 에러가 발생하니 주의하시길)
실행하면 '악성코드 발견!'이라고 뜨면서 eicar.txt 파일이 삭제될 것이다.
여기선 세 글자만 봤는데, 이 문자열이 길수록 정상적인 프로그램을 바이러스라고 진단할 확률이 낮아진다. 하지만 변종 바이러스를 진단할 확률은 상대적으로 낮아진다.(trade off)
따라서 V3에서는 하나의 바이러스를 두개의 문자열을 이용해 검사를 한다고 한다.
(4) 해시값 탐지 도입
진단 문자열만 탐지서는 오진이 생길 가능성이 높다. 따라서 해시값을 이용해 진단한다. 여기서는 MD5 해시를 쓴다. 이를 위해 hashlib를 import해주자.(참고로 MD5 해시는 강력하진 않지만 매우 빠른 해시알고리즘)
참고로, MD5 해시를 통해 진단할 수 있는 악성코드는 바이러스 유형을 제외한 파일 그 자체가 악성코드인 트로이목마, 백도어, 웜 등이라고 한다.
해당 파일의 해시값을 확인하기 위해 바이러스토탈에서 해당 파일을 넣어서 각종 해시함수에 대한 해시값을 알아봤다. 우리가 사용할 것은 이 중 MD5 해시값이므로 이를 복사한다.
44d88612fea8a8f36de82e1278abb02f
# -*- coding:utf-8 -*- # 한글 설정
import hashlib # MD5를 구하기 위해 import
import os
fp = open('malwares\eicar.txt', 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성
fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()
f = hashlib.md5() # MD5 hash function
f.update(fbuf) # hashing!
hashValue = f.hexdigest() # 메시지 다이제스트를 얻음(16진수 해시값)
if hashValue == '44d88612fea8a8f36de82e1278abb02f': # EICAR test 파일의 MD5 해시값
print '악성코드 발견!'
os.chmod('malwares\eicar.txt', 0777) # 파일이 읽기전용인 경우 chmod를 해주고
os.remove('malwares\eicar.txt') # 파일을 강제 삭제
else:
print '악성코드가 없음'
EICAR Test를 진단할 수 있는 전용백신을 만들어보았다.
이제 좀 더 많은 악성코드를 진단할 수 있는 백신을 만들어보자.
2. 통합백신 만들기
(1) 악성코드 파일 만들기
여러가지 악성코드를 진단하기 위해서는 일단 여러가지 악성코드가 필요하다.
따라서 일단 몇 개의 임의 악성코드 파일을 직접 만들도록 하자.
malware_example1.txt 파일의 MD5 해시값은 4d764cac3968e3bf0cf060993a5f2711이고,
malware_example2.txt 파일은 5f781e356fe12c7cb1491af0df37eeb4이다.
이 내용과 동일하게 해도 상관없고, 새로 파일을 만들고 MD5를 적용한 뒤 python출력문을 통해 출력해서 알아봐도 된다.(아니면 다른 웹사이트나 툴을 쓰면 됨)
(2) 악성코드 DB 만들기
if~else문을 통해 각 파일의 해시값을 모두 검사할 수도 있지만, 너무 코드량이 많아지고 비효율적이므로 좀 더 스마트한 방법을 써보도록 하자. 일단은 악성코드의 리스트를 만들자. 책에서는 List(배열) 자료구조를 이용했지만 난 Dictionary(사전) 자료구조를 이용해보았다. 따라서 소스코드가 많이 다를 것이다.
먼저, 악성코드를 담을 나만의 데이터베이스를 만들자. DB의 구조는 '악성코드 이름' : '해시값' 형태로 값이 저장되는 Dictionary이다.
# 악성코드 정보를 모아놓은 Database
malwareDB = {
'EICAR Test' : '44d88612fea8a8f36de82e1278abb02f',
'Malware1' : '4d764cac3968e3bf0cf060993a5f2711',
'Malware2' : '5f781e356fe12c7cb1491af0df37eeb4'
}
그 다음으로 이 DB에 특정 해시값이 존재하는지를 검색하기 위한 searchDB 함수를 만들자.
해시값을 input으로 주고, DB에 있는 원소들을 하나씩 뽑아내서 value값이 input과 같으면 True와 악성코드 이름을 return하는 함수이다.
def searchDB(hashValue):
for key, value in malwareDB.items(): # dictionary의 value값들 중 해당 해시값이 존재하면
if value == hashValue:
return True, key # 악성코드 이름과 함께 return
return False, ''
우리가 알고 있는 해시값들을 넣어 테스트를 해보자.
print(searchDB('44d88612fea8a8f36de82e1278abb02f'))
print(searchDB('4d764cac3968e3bf0cf060993a5f2711'))
print(searchDB('5f781e356fe12c7cb1491af0df37eeb4'))
print(searchDB('dcsad')) ------ 결과 ------ (True, 'EICAR Test')
(True, 'Malware1')
(True, 'Malware2')
(False, '')
(3) 진단 모듈 만들기
함수를 잘 만들었으니 이제 파일이름을 입력받아 해당 파일이 악성코드인지 진단할 수 있는 모듈을 만들자.
def vaccine(fileLocation):
fp = open(fileLocation, 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성
fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()
f = hashlib.md5() # MD5 hash function
f.update(fbuf) # hashing!
hashValue = f.hexdigest() # 메시지 다이제스트를 얻음(16진수 해시값)
isMalware, name = searchDB(hashValue) # 구한 해시값을 넣어 악성코드인지, 이름은 뭔지 알아냄
if isMalware == True: # 악성코드면
print fileLocation, ': Malware(', name, ')'
os.chmod(fileLocation, 0777) # 파일이 읽기전용인 경우 chmod를 해주고
os.remove(fileLocation) # 파일을 강제 삭제
else: # 아니면
print fileLocation, ': Normal File'
파일의 경로(이름)를 input으로 받아 진단을 수행하여 악성코드이면 악성코드의 이름을, 아니면 정상 파일이라고 return하는 백신을 만들었다.
이제 커맨드창(cmd)에서 이 백신을 동작시킬 수 있도록 main함수를 만들어보자.
(4) 커맨드창에서 백신 동작시키기
백신의 사용자가 malwareDB에 새로운 내용을 쓰거나 내용을 조작해서는 안될 것이다. 따라서 makeDB라는 함수 DB를 만들고, vaccine 안에서 호출하여 DB를 얻도록 할 것이다.
따라서 위에서 작성한 내용은 모두 다음과 같이 바뀐다.
# -*- coding:utf-8 -*- # 한글 설정
import hashlib # MD5를 구하기 위해 import
import os
import sys
# 악성코드 정보를 모아놓은 Database
def makeDB():
malwareDB = {
'EICAR Test' : '44d88612fea8a8f36de82e1278abb02f',
'Malware1' : '4d764cac3968e3bf0cf060993a5f2711',
'Malware2' : '5f781e356fe12c7cb1491af0df37eeb4'
}
return malwareDB
def searchDB(hashValue, malwareDB):
for key, value in malwareDB.items(): # dictionary의 value값들 중 해당 해시값이 존재하면
if value == hashValue:
return True, key # 악성코드 이름과 함께 return
return False, ''
def vaccine(fileLocation):
malwareDB = makeDB() # DB 가져오기
fp = open(fileLocation, 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성
fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()
f = hashlib.md5() # MD5 hash function
f.update(fbuf) # hashing!
hashValue = f.hexdigest() # 메시지 다이제스트를 얻음(16진수 해시값)
isMalware, name = searchDB(hashValue, malwareDB) # 구한 해시값을 넣어 악성코드인지, 이름은 뭔지 알아냄
if isMalware == True: # 악성코드면
print fileLocation, ': Malware(', name, ')'
os.chmod(fileLocation, 0777) # 파일이 읽기전용인 경우 chmod를 해주고
os.remove(fileLocation) # 파일을 강제 삭제
else: # 아니면
print fileLocation, ': Normal File'
이제 main함수를 만들자.
커맨드창에서 파이썬 인터프리터를 이용하여 백신을 동작시키기 위해서 main함수의 인자로 파일의 경로를 받도록 만든다. 이를 위해 sys를 import해줘야 한다.
인자가 들어오면 인자값으로 fileLocation을 설정해주고, 인자가 들어오지 않으면 사용자에게 직접 입력받는다.
if __name__ == '__main__':
if len(sys.argv) == 2: # 커맨드 입력이 들어왔으면
fileLocation = sys.argv[1] # 입력받은 내용으로 설정
else:
fileLocation = raw_input('\nPlease enter your file location : ') # 아니면 사용자에게 입력을 받음
vaccine(fileLocation) # 백신 모듈 동작
모든 출력문을 영어로 작성한 이유는, 커맨드창에서 실행시킬 때 한글이 깨지기 때문이다.
실행을 시켜보았다. 아주 만족스럽게 동작한다.
<Pycharm에서 실행한 결과>
<커맨드창에서 실행한 결과>
(5) 속도 향상시키기
만약 읽어야하는 파일이 매우 큰 파일이라면, 우리의 백신은 상당히 느려질 것이다. 따라서 파일을 최대한 덜 읽을 수 있도록 최적화를 해줘야 한다. 즉, read() 함수를 최대한 덜 쓰게 만들어주자.
현재 malwareDB에는 해당 악성코드 파일의 이름과 해시값이 들어있다. 파일의 크기가 달라지면 해당 파일의 해시값도 달라지므로 파일의 크기를 함께 저장하도록 하자.
다음과 같이 말이다.
# 악성코드 정보를 모아놓은 Database
def makeDB():
malwareDB = {
'EICAR Test' : ['44d88612fea8a8f36de82e1278abb02f', 68],
'Malware1' : ['4d764cac3968e3bf0cf060993a5f2711', 31],
'Malware2' : ['5f781e356fe12c7cb1491af0df37eeb4', 34]
}
return malwareDB
근데 이렇게 되면, value의 형식이 바뀌었으므로 searchDB 함수도 바뀌어야 한다.
def searchDB(hashValue, malwareDB):
for key, value in malwareDB.items(): # dictionary의 value값들 중 해당 해시값이 존재하면
if value[0] == hashValue:
return True, key # 악성코드 이름과 함께 return
return False, ''
기존에 있던 value == hashValue 라는 코드를 value[0] == hashValue로 바꾸어줬을 뿐이다. value의 타입이 문자열이 아니라 List 타입이 되었고 그 0번째 index에 해시값이 저장되었기 때문이다.
이제 vaccine(진단 모듈)을 바꿔보자.
os.path.getsize() 라는 함수를 이용해 악성코드인지 확인하려는 파일의 크기를 계산하고 이것이 DB에 있는 악성코드들의 size에 포함되는지 안되는지를 확인해야한다.
이를 위해 map함수와 lambda function을 사용하였는데, 쉽게 말하자면 malwareDB의 value들에서 index 1 부분만 모아다가 sizeDB라는 이름으로 저장해주었다.
그리고 이 sizeDB 안에 포함되지 않을 경우(not in), 정상 파일이라고 출력하고 진단을 종료하도록 하면 된다.
파일 크기만 보고 정상 파일이면 파일을 읽지 않고 진단을 마치므로 백신의 속도를 빠르게 할 수 있었다.
def vaccine(fileLocation):
malwareDB = makeDB() # DB 가져오기
sizeDB = map(lambda value: value[1], malwareDB.values()) # value의 index 1인 값들(size들)만 모으기
fp = open(fileLocation, 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성
fileSize = os.path.getsize(fileLocation)
if fileSize not in sizeDB: # sizeDB안에 없으면 정상 파일로 진단
print fileLocation, ': Normal File'
return # 더 이상 진행할 것 없이 종료
fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()
f = hashlib.md5() # MD5 hash function
f.update(fbuf) # hashing!
hashValue = f.hexdigest() # 메시지 다이제스트를 얻음(16진수 해시값)
isMalware, name = searchDB(hashValue, malwareDB) # 구한 해시값을 넣어 악성코드인지, 이름은 뭔지 알아냄
if isMalware == True: # 악성코드면
print fileLocation, ': Malware(', name, ')'
os.chmod(fileLocation, 0777) # 파일이 읽기전용인 경우 chmod를 해주고
os.remove(fileLocation) # 파일을 강제 삭제
else: # 아니면
print fileLocation, ': Normal File'
자, 드디어 백신만들기 시즌1이 끝났드아아ㅏ~~!!!! (시즌 2에서 계속)