본문 바로가기
Malware/파이썬으로 배우는 Anti-Virus 구조와 원리

[Anti-Virus] 9장, 플러그인 백신 엔진 개발하기

by Y06 2021. 9. 25.

  • init: 플러그인 엔진을 초기화한다.
  • uninit: 플러그인 엔진을 종료한다.
  • scan: 악성코드를 검사한다.
  • disinfected: 악성코드를 치료한다.
  • viruslist: 플러그인 엔진이 진단/치료 가능한 악성코드의 목록을 알려준다.
  • getinfo: 플러그인 엔진의 주요 정보를 알려준다

KavMain 클래스는 기본적으로 플러그인 엔진이라면 반드시 가져야 한다. 만약 KarMain 클래스가 선언되어 있지 않다면 백신 커널은 비정상적인 플러그인 엔진이라고 규정한다.

 

■ init 함수

: 플러그인 엔진 초기화 시점에 백신 커널에 의해 호출된다. 이 시점에서 플러그인 엔진은 각자의 악성코드 패턴 파일을 로딩, 필요한 메모리 확보 등의 일을 처리할 수 있다.

 

  • plugins_path: 플러그인 엔진 초기화

악성코드 패턴 파일을 로딩하려면 앗엍코드 패턴 파일의 위치가 필요하다. 따라서 init 함수의 인자값으로 plugins_path가 존재한다. 이는 백신 커널이 악성코드 패턴 파일의 존재 위치를 플러그인 엔진에게 알려주는 역할을 한다.

 

 

dummy 플러그인 엔진은 Dummy Test 악성코드만을 진단/치료하기 때문에 별도의 악성코드 패턴 파일이 존재하지 않는다. 그래서 악성코드 패턴을 로딩할 일도 없고 메모리를 할당하는 일도 필요 없는 형태다. 하지만 여기에서는 새로운 변수에 데이터를 넣는(일종의 데이터 할당) 형태를 보여주고 있다.

 

virus_name 멤버 변수에는 dummy 플러그인 엔진이 진단/치료하는 악성코드의 이름을 dummy_patern 멤버 변수에는 악성코드 진단 문자열을 저장했다.

 

플러그인 엔진의 초기화 작업이 문제가 없다면 0을 리턴하려 백신 커널에게 정상적으로 초기화가 되었음을 알린다.

 

■ uninit 함수

: init 함수와는 반대의 성격을 가진다. 백신 커널이 플러그인 엔진에게 이제 잠시 뒤 백신 엔진 전제가 종료될 것을 알려주는 시점에서 호출된다.

 

따라서 만약 init 함수에서 악성코드 패턴 파일에서 악성코드 패턴 로딩, 메모리 할당 등의 작업이 있었다면, uninit 함수에서는 로딩된 악성코드 패턴 및 할당된 메모리를 해제해야 한다.

 

플러그인 엔진이 종료를 위한 uninit 함수는 인자값이 없다. 대체로 할당된 메모리들은 KavMain 클래스 내부의 멤버 변수로 접근 가능한 형태로 둘 예정이다.

 

init 함수에서 할당된 virus_name, dummy_pattern 멤버 변수에 할당된 메모리를 del 명령어를 사용하여 해제하고 있다. 플러그인 엔진이 종료되면 성공의 의미로 0을 백신 커널에게 리턴한다.

 

■ scan 함수

: 악성코드를 검사하는 부분이 실제 소스코드의 상당부분을 차지한다. 따라서 scan 함수는 플러그인 엔진에서 가장 핵심에 해당한다.

 

scan 험수의 인자값은 고려할 부분이 많다. 첫 번째로 악성코드를 검사하기 위해서는 파일 이름(filename)이 필요하다. 따라서 백신 커널은 플러그인 엔진에게 악성코드 검사 대상 파일 이름을 알려준다.

 

하지만 하나의 플러그인 엔진만 존재한다면 이는 문제가 없지만 수십 개의 플러그인을 가진 백신 커널을 생각해보자. 이 경우 백신 커널은 순차적으로 플러그인 엔진에게 악성코드를 검사해보라고 파일 이름을 알려줄 것이다. 이때 플러그인 엔진들은 파일을 읽기 위해 파일을 열고 닫기를 반복하게 된다. 이는 백신 엔진의 검사 속도를 저하시키는 요인이 된다.

 

백신 엔진의 검사 속도가 느려지지 않으려면 각 플러그인 엔진의 파일을 열고 닫기를 최소화 하는 방법을 모색해야 한다. 결국 백신 커널이 파일을 열고 각 플러그인 엔진에게 파일의 핸들을 전달하는 방법을 사용하는 것이다. 모든 플러그인 엔진이 악성코드 검사를 마치면 백신 커널이 파일을 닫는 방법을 사용하여 파일을 한 번만 열고 닫기를 할 수 있다. 그래서 scan 함수의 인자값으로 파일의 핸들(filehandle)도 추가적으로 넣어두었다.

 

인자값의 사용 빈도 때문에 filehandle, filename 순으로 배치했다.

 

파일 핸들이 있음에도 파일 이름을 사용하는 이유는 백신 엔진이 검사하는 방법 중 특히 스파이웨어(Spyware)의 경우 악성코드 진단 문자열로 검사하지 않고 단순히 파일 이름만으로 진단하는 경우도 있기 때문에 파일 이름도 인자 값에 그대로 두는 것이 좋다.

 

위의 사진에서는 파일 핸들이 아닌 파일 이름을 이용해서 악성코드를 검사하는 모습을 보여주고 있다. try ~ except가 존재하는 이유는 파일 관련 연산을 처리할 때 혹시라도 파일을 제대로 열지 못하거나 읽기가 불가능할 경우 등 파일 입출력 시 예외적인 상황에서 scan 함수가 비정상 종료되는 것을 막기 위해서다.

 

파일을 열고 init 함수에서 정의한 dummy_pattern 멤버 변수에 넣어둔 악성코드 진단 문자열 길이만큼 파일에서 buf로 읽는다. 그냥 fp.read()만 사용하게 되면 용량이 큰 동영상 파일의 경우 모든 파일 내용을 buf로 읽으려고 시도할 것이다. 이렇게 되면 백신 엔진의 검사 속도에도 문제가 발생하기 때문에 가급적 파일을 읽을 때에는 읽을 크기를 지정하는 것이 좋다.

 

buf에 파일의 내용이 담겼으니 악성코드 진단 문자열과 비교하여 같은지를 체크한다. 악성코드 진단 문자열과 일치한다면 악성코드가 발견되었으니 결과를 백신 커널에게 알려준다. 백신 커널에게 알려줘야 할 내용은 악성코드 발견 여부(True면 악성코드 발견, False명 악성코드 미발견), 발견한 악성코드의 이름(self.virus_name), 그리고 악성코드 ID(0)이다.

 

악성코드 발견 여부와 발견한 악성코드의 악성코드 이름은 이해하는 데 문제 없을 듯하다. 하지만 악성코드 ID는 이해하기 어려울 것이다. 이는 악성코드 치료와 관련된 부분이다.

 

백신 커널이 dummy 플러그인 엔진에게 악성코드를 검사해보라고 요청했다. 만약 dummy 플러그인 엔진이 악성코드를 검사하지 못했다면 백신 커널은 다음 플러그인 엔진에게 악성코드 검사를 요청하게 된다. 만약 dummy 플러그인 엔진이 악성코드를 발견했다면 다음 단계는 치료이다. 백신 커널이 dummy 플러그인 엔진에게 파일에 악성코드가 있으니 치료하라고 요청하게 될 텐데, 문제는 dummy 플러그인이 하나 이상의 악성코드를 진단할 수 있다면 어떤 악성코드에 대한 치료 방법을 선택해야 할지 알 수 없다. 이때를 위해 악성코드 ID가 필요하다. 악성코드 ID는 disinfect 함수에서 다시 한 번 등장한다.

 

악성코드를 발견하지 못했을 때는 악성코드 발견 여부는 False이며 발견한 악성코드는 없으니 악성콛 이름도 없다 또한, 악성코드 ID도 -1로 리턴하게 된다.

 

■ disinfect 함수

: 악성코드를 치료한다. disinfect 함수의 인자값을 살펴보도록 한다.

 

악성코드를 치료하기 위해서는 파일 이름(filename)이 필요하다. 또한 이 파일에 어떤 악성코드에 감염되었는지 알아야 그에 맞는 치료 방법을 택할 수 있다. 따라서 scan 함수에서 리턴한 악성코드 ID(malware_id)가 인자값으로 필요하다.

 

악성코드를 치료하는 작업이다 보니 역시 예외적인 상황이 발생할 수 있다. 그래서 try ~ except으로 처리한다.

악성코드 치료는 malware_id에 따라 달라지는데, 우선 Dummy Test 악성코드는 파일을 삭제함으로써 치료가 끝난다. 그래서 scan 함수에서도 Dummy Test 악성코드를 진단했을 경우 악성코드 ID로 0을 리턴했다.

 

따라서 disinfect 함수에서 악성코드 ID가 0인 경우는 파일을 삭제하여 악성코드를 치료한다. 악성코드 치료가 성공이라면 True를 리턴한다.

 

try ~ except 블록 외부는 악성코드 치료가 실패했을 때 실행된다. 따라서 False를 리턴하여 악성코드 치료 실패임을 백신 커널에게 알린다.

 

■ listvirus 함수

: 플러그인 엔진이 진단/치료 가능한 악성코드의 목록을 알려준다. 그 니유는 백신 엔진의 진단/치료하는 악성코드의 수 혹은 목록을 원하는 고객 때문이다. 물론 이 정보는 백신 업체의 경우 영업 혹은 마켓팅 용도로 사용하기도 한다.

 

listvirus 함수는 고객 지원을 위해서도 요긴하게 사용된다. 어떤 악성코드를 오진하는 사례가 발생했다면 어떤 플러그인 엔진에서 오진하는지 listvirus 함수를 통해 금방 알아낼 수 있다.

 

어떤 악성코드를 진단/치료하는지에 대해서 알기 위한 용도이므로 특별히 인자값이 없다.

간단하게 vlist 이름의 리스트 자료형이 존재하고 여기에 진단/치료 가능한 악성코드 이름을 등록한다. dummy 플러그인 엔진이 진단/치료하는 악성코드의 이름은 'Dummy-Test-File (not a virus)'이다. 이 문자열은 virus_name 멤버 변수에 저장되어 있다. 진단/치료 가능한 악성코드의 이름을 모두 vlist에 등록했다면 이제 리턴하면 된다.

 

■ getinfo 함수

: 플러그인 엔진의 주요 정보(플러그인 엔진의 제작자, 해당 플러그인의 역할, 플러그인 엔진의 버전 등)를 알려준다. 오진 발생 시 어떤 버전의 플러그인 엔진에서 발생했는지, 누구에 의해서개발되었는지 등을 쉽게 알 수 있다.

 

getinfo 함수도 listvirus 함수와 동일하게 어떤 제작자에 의해서 개발되었는지 등에 대해서 알기 위한 용도이므로 특별히 인자값이 없다.

 

 

eicar.py

# -*- coding:utf-8 -*-
import os

class KavMain:
    
    #플러그인 엔진을 초기화 한다.
    def init(self, plugins_path):
        #진단/치료하는 악성코드 이름
        self.virus_name = 'Dummy-Test-File (not a virus)'
        #악성코드 패턴 등록
        self.dummy_pattern = 'Dummy Engine test file - KICOM Anti-Virus Project'

        return 0 #플러그인 엔진 초기화 성공
    
    # 플러그인 엔진을 종료한다.
    def uninit(self):
        del self.virus_name #메모리 해제(악성코드 이름 관련)
        del slef.dummy_pattern #메모리 해제(악성코드 패턴)

        return 0 # 플러그인 엔진 종료 성공

    #악성코드를 검사한다.
    def scan(self, filehandle, filename):
        try:
            #파일을 열어 악성코드 패턴만큼 파일에서 읽는다.
            fp = open(filename)
            buf = fp.read(len(self.dummy_pattern)) #패턴은 49 Byte 크기
            fp.close()

            #악성코드 패턴을 비교한다.
            if buf == self.dummy_pattern:
                #악성코드 패턴이 같다면 결과 값을 리턴한다.
                return True, self.virus_name, 0

        except IOError:
            pass

        #악성코드를 발견하지 못했음을 리턴한다.
        return False, '', -1

    #악성코드를 치료한다.
    def disinfect(self, filename, malware_id):
        try:
            #악성코드 진단 결과에서 받은 ID 값이 0인가?
            if malware_id == 0:
                os.remove(filename) #파일 삭제
                return True #치료 완료 리턴

        except IOError:
            pass

        return False #치료 실패 리턴


    #플러그인 엔진이 진단/치료 가능한 악성코드의 리스트를 알려준다.
    def viruslist(self):
        vlist = list() #리스트형 변수 선언

        vlist.append(self.virus_name) #진단/치료하는 악성코드 이름 등록

        return vlist


    #플러그인 엔진의 주요 정보를 알려준다.
    def getinfo(self):
        info = dict() #사전형 변수 선언

        info['author'] = 'Kei Choi' #제작자
        info]'version'] = '1.0' #버전
        info['title'] = 'EICAR Scan Engine' #엔진 설명
        info['kmd_name'] = 'eicar' #엔진 파일 이름

        return info