개인공부

[보안] 나만의 백신 만들기 - 시즌2

쭁쭁이 2018. 5. 20. 16:46

나만의 백신 만들기가 시즌2로 돌아왔다~~ 는 과제..

사실 과제는 진작에 끝냈고 이걸 쓴건 5월 20일 정도였는데, 아직 블로그 글 작성에 익숙치 않다보니 어쩌다가 다 날라가버려서... 똑같은 내용을 다시 쓰자니 귀찮아서 미루고 있었는데, 저번 글에 다음 편을 올려달라는 댓글을 써주신 분이 계셔서 다시 쓸 마음이 생겼다.


이제 저번에 만들어 본 백신보다 조금 더 백신다운 백신을 만들어보자.

이전 글에선 

1. 전용백신 개발하기

2. 통합백신 만들기

를 해봤다. 

이번엔 지난 번에 만들었던 백신을 기능 단위로 쪼개는 것에서부터 시작해보자.


3. 백신 분해하기


(1) 악성코드 패턴 분리

지금 우리가 만든 백신은 악성코드 패턴과 진단 모듈, 치료 모듈이 모두 함께 있기 때문에 새로운 악성코드가 등장하여 악성코드 패턴을 추가하고 싶으면 백신 전체를 다시 컴파일하여 새로 배포해야 한다. 매우 비효율적이다. 따라서 악성코드 패턴만 따로 파일로 분리해보자.


먼저, 기존에 갖고 있는 패턴을 patterns.db 라는 파일에 다음과 같은 형식으로 써준다.

악성코드 이름:MD5해시값:파일크기

EICAR Test:44d88612fea8a8f36de82e1278abb02f:68
Malware1:4d764cac3968e3bf0cf060993a5f2711:31
Malware2:5f781e356fe12c7cb1491af0df37eeb4:34


그 다음, 파일 이름을 받아 악성코드 패턴을 한 줄씩 읽어서 return해주는 loadDB라는 함수를 만든다.

# 악성코드 패턴 파일 읽어오기
def loadDB(fileName):
patterns = []
fp = open(fileName, 'rb')
while True:
line = fp.readline()
if not line: break # 파일을 끝까지 읽으면 중단

line = line.strip() # 뒤에 붙어있는 엔터키 제거
patterns.append(line)
fp.close()

return patterns

그 다음으로 makeDB함수에서 패턴들을 지워주고 loadDB 함수를 통해 패턴들을 얻어와서 :(콜론)을 기준으로 잘라주고 우리가 원래 사용했던 dictionary형태로 만들어준다.

(파일 크기는 int로 형변환하여 넣어줘야 한다는 것에 주의)

# 악성코드 정보를 모아놓은 Database
def makeDB(fileName):
malwareDB = {}

patterns = loadDB(fileName)
for pattern in patterns: # 파일로부터 한 줄씩
p = pattern.split(':') # 콜론 기준으로 자르기
information = [p[1], int(p[2])] # 각 악성코드에 대한 해시값과 파일 크기
name = p[0] # 악성코드 이름

malwareDB[name] = information # dictionary에 추가해줌

return malwareDB

파일 이름을 넣어줘야 하니 vaccine 함수(진단, 치료 모듈)의 앞 부분은 다음과 같이 바꿔준다.

def vaccine(fileLocation):
malwareDB = makeDB('patterns.db')
sizeDB = map(lambda value: value[1], malwareDB.values())


(2) 패턴파일 암호화(Optional)

patterns.db 파일은 백신의 신뢰성을 좌우할 매우 중요한 파일이 되었다. 만일 공격자가 우리 시스템에 들어와 이 파일 내용을 바꿔버린다면, 백신은 악성코드를 진단하지 못할 뿐만 아니라 엉뚱한 파일을 악성코드라고 진단하게 될 것이다. 따라서 patterns.db를 우리만 열 수 있게 암호화해줘야 한다. 따라서 이 파일에 대한 암호화 도구를 만들어보자.


encryptor.py 라는 파일을 새로 만들고 다음과 같이 작성해준다.

어차피 암호화는 optional한 부분이므로 생략해도 된다. 또한 암호화 알고리즘을 여기서 설명할 순 없으니 과정만 설명하겠다.


암호화할 파일 불러오기 -> 압축하기 -> Stream Cypher(XOR)로 암호화하기 -> 앞에는 헤더 달기 -> 뒤에는 MD5 해시값 달기 -> 새로운 파일로 만들기

# -*- coding:utf-8 -*- # 한글 설정
import sys
import zlib # 압축을 위해 import
import hashlib


def main():
if len(sys.argv) == 2: # 커맨드 입력이 들어왔으면
inputFile = sys.argv[1] # 입력받은 내용으로 설정
else:
inputFile = raw_input('\nEnter input file : ') # 아니면 사용자에게 입력을 받음

fp = open(inputFile, 'rb')
fbuf = fp.read()
fp.close()

compressed = zlib.compress(fbuf)
cypherText = 'PJY' # 헤더를 달아줌
for c in compressed: # 1byte(한 글자)씩 0xFF와 XOR - 암호화 알고리즘(Stream Cypher)
cypherText += chr(ord(c)^0xFF)

hashValue = cypherText
# 해시를 3번 수행해준다
for i in range(3):
md5 = hashlib.md5()
md5.update(hashValue)
hashValue = md5.hexdigest()

cypherText += hashValue # 해시값을 뒤에 암호화된 내용 뒤에 더해준다
outputFile = inputFile.split(' ')[0] + '.secure' # 새로 만들어질 파일 이름

fp = open(outputFile, 'wb') # 쓰기 모드로 열기
fp.write(cypherText)
fp.close()

print('Completed! %s -> %s' % (inputFile, outputFile))

if __name__ == '__main__':
main()

앞에 헤더는 원하는 이름을 붙이면 되고, 뒤에 MD5 x 3의 해시값을 붙이는 이유는 복호화 시 무결성(파일의 위변조)을 체크하기 위함이다.


이제 실행해서 patterns.db를 암호화해보자.


우앙~ patterns.db.secure 라는 파일을 얻었다.

파일 내용을 보면 왜 암호화하는지 아시겠쥬~?


(3) 패턴파일 복호화(Optional)

암호화까지 잘해놨는데 문제가 있다. 우리의 백신 내에서 patterns.db.secure 파일을 불러오려면 암호화를 풀어야 된다는 것이다.

따라서 백신에 복호화 모듈을 추가하자. 역시 과정만 설명한다.

복호화는 암호화의 역순으로 진행되므로,

뒤의 32바이트를 잘라내어 헤더+암호문/해시값 을 얻어냄 -> 헤더+암호문을 MD5로 3번 해싱 -> 여기서 나온 해시값과 앞의 해시값을 비교하여 다르면 시스템 에러 -> 헤더를 잘라내어 암호문을 얻음 -> 압축 해제 -> 평문을 return

def decryptor(fileName):
try:
fp = open(fileName, 'rb')
fbuf = fp.read()
fp.close()

cypherText = fbuf[:-32] # 뒤에서 32글자만큼을 제외하고 잘라줌

hashValue = cypherText
# 해시를 3번 수행해준다
for i in range(3):
md5 = hashlib.md5()
md5.update(hashValue)
hashValue = md5.hexdigest()

if hashValue != fbuf[-32:]: # 뒤의 32글자와 해시값을 비교하여 다르면
raise SystemError # 시스템 에러를 발생시킴

compressed = ''
for c in cypherText[3:]: # 헤더의 글자 수만큼 제외하고 잘라줌
compressed += chr(ord(c) ^ 0xFF) # XOR의 역함수는 XOR

plainText = zlib.decompress(compressed) # 압축을 풀어 평문을 얻음

return plainText
except: # 오류발생 시
pass # 아무것도 하지 않고

return None # None을 return

decryptor를 이용하여 얻은 내용은 복호화를 수행하고 얻은 문자열이지, 파일의 내용이 아니기 때문에 기존의 readline 함수를 사용할 수 없다. 이러한 경우, StringIO 모듈을 import해서 사용하면 파일을 연 것처럼 문자열을 제어할 수 있다. 좋은 세상이여


따라서 loadDB 함수를 다음과 같이 바꾼다.

# 악성코드 패턴 파일 읽어오기
def loadDB(fileName):
patterns = []
fbuf = decryptor(fileName) # 복호화
fp = StringIO.StringIO(fbuf) # readline()을 사용할 수 있도록 해줌

while True:
line = fp.readline()
if not line: break # 파일을 끝까지 읽으면 중단

line = line.strip() # 뒤에 붙어있는 엔터키 제거
patterns.append(line)
fp.close()

return patterns


vaccine 모듈도 patterns.db.secure 파일을 읽어오도록 바꿔준다.

def vaccine(fileLocation):
malwareDB = makeDB('patterns.db.secure') # DB 가져오기
sizeDB = map(lambda value: value[1], malwareDB.values()) # value의 index 1인 값들(size들)만 모으기

자, 이렇게 해서 악성코드 패턴 파일을 안전하게 보관하고 사용할 수도 있게 되었다!


(4) 진단 모듈 분리

패턴을 분리했지만 여전히 문제가 남아있다. 신종 악성코드가 나와서 기존의 방식으로 진단하거나 치료할 수 없다면 우리는 새로운 방법을 찾아 백신에 도입해야 한다. 

이번에도 역시 백신 전체를 수정할 필요가 없도록 진단 모듈, 치료 모듈을 분리해서 해당 업데이트본만 배포하면 되도록 만들어보자.


먼저 진단 모듈을 분리해보자.

후에  문자열 진단 모듈도 추가할 것이므로 scanHash라는 이름의 함수를 만들어준다. 기존 vaccine 함수에 있던 내용에서 MD5 해시와 관련된 내용들을 뽑아 searchDB 함수와 합쳐준다. sizeDB를 구하는 부분부터 해시값을 구하는 부분까지 갖고 오면 된다.

그리고 scanner.py 라는 파일을 만들고 scanHash 함수를 옮겨준다.

(필요한 것들 import 하시고...)

# -*- coding:utf-8 -*-    # 한글 설정
import os
import hashlib

# MD5 해시값을 이용한 진단 모듈
def scanHash(fileLocation, malwareDB):
sizeDB = map(lambda value: value[1], malwareDB.values()) # value의 index 1인 값들(size들)만 모으기

fileSize = os.path.getsize(fileLocation)

if fileSize not in sizeDB: # sizeDB안에 해당 파일의 사이즈가 없으면 정상 파일로 진단
return False, ''
fp = open(fileLocation, 'rb') # 반드시 바이너리 모드로 읽어들여 파일객체 생성

fbuf = fp.read() # 파일객체로부터 내용 읽어들여 버퍼에 저장
fp.close()

f = hashlib.md5() # MD5 hash function
f.update(fbuf) # hashing!
hashValue = f.hexdigest() # 메시지 다이제스트를 얻음(16진수 해시값)

for key, value in malwareDB.items(): # dictionary의 value값들 중 해당 해시값이 존재하면
if value[0] == hashValue:
return True, key # 악성코드 이름과 함께 return
return False, ''

이제  해당 모듈을 백신 내에서 포함시켜야 하므로 vaccine.py 파일에 scanner를 import해주고 scanner.scanHash 함수로 결과를 가져온다. 

따라서 vaccine 함수는 다음과 같이 바꿔준다.

import scanner ~~
def vaccine(fileLocation):
malwareDB = makeDB('patterns.db.secure') # DB 가져오기
isMalware, name = scanner.scanHash(fileLocation, malwareDB) # MD5 해시를 이용한 진단 결과

if isMalware == True: # 악성코드면
print fileLocation, ': Malware(', name, ')'
os.chmod(fileLocation, 0777) # 파일이 읽기전용인 경우 chmod를 해주고
os.remove(fileLocation) # 파일을 강제 삭제
else: # 아니면
print fileLocation, ': Normal File'

이렇게 해서 MD5 해시값 진단모듈을 분리해보았다.

하지만 위에서 이야기했듯이 시즌1 초반에 만들었던 문자열 진단 모듈도 추가시킬 것이다.


시즌 3에 계속~~