차례

1. 개요

.... 1.1 배경지식

.... 1.2 문서 정리

2. 드라이버 기본

.... 2.1 컴퓨터 설정

.... 2.2 드라이버 개발 환경

.... 2.3 도구

.... 2.4 DriverEntry / DriverUnload

3. 자가 보호

4. 스캐닝

.... 4.1 파일 시그니처 기반 스캐닝

.... 4.2 행위 기반 스캐닝

5. 프로세스 / 스레드 생성 보호

6. 레지스트리 보호

7. ELAM 및 Protected Process

8. 유저 모드 애플리케이션과의 통신

9. 기타

.... 9.1 네트워크 보호



[ 소스 코드 : https://github.com/101196/simpleAVdriver ]





1. 개요

1.1 개요

  악성코드 분석을 위주로 공부하고 있지만 개인적으로 안티바이러스가 어떻게 구현되는지에 대해서도 항상 궁금해 하여 왔다. 어떻게 보면 크게 관련이 없는 분야라고 할 수도 있겠지만 적어도 내 분야 중에서 주요한 부분을 차지한다고 생각하기 때문에 메커니즘에 대해서 공부하고 정리하려고 한다. 사실 더 관심이 갔던 이유는 찾기 정말 힘들었기 때문이다. 만약 제대로 정리된 자료들이 많았다면 그것만 정리하면서 공부하고 그 정도로만 알려고 했을 것이다.


  여기서는 배경 지식과 함께 전체적인 방식을 설명한다. 이 문서 외에도 깃허브를 통해 간단한 형태로 구현한 소스 코드도 같이 제공한다. 이미 있는 자료들을 기반으로 최소한 간략하게 만든 것으로서 원본 링크도 같이 제공한다. 실질적인 구현에 관한 것은 소스 코드의 주석을 통해 정리하기로 할 것이고 이 문서에서는 전체적인 방식 및 흐름을 정리하기로 한다. 


  각 내용들은 안티바이러스를 구현하는데 필요한 기능들을 차례대로 설명한다. 참고로 드라이버 개발과 관련해서도 정리할 수 밖에 없는데 순수하게 직접 구현한 안티바이러스와 관련된 내용에 한정해서 정리한다. 디바이스 드라이버와 관련된 자료들은 종종 보임에도 불구하고 순수하게 안티바이러스 관련 드라이버의 내용은 많이 없는 편이다. 물론 아주 상세하게 정리하는 것은 의미도 없고 능력도 되지 않기 때문에 아주 기초적인 것은 안다는 가정 하에 그리고 충분히 MSDN 등의 자료들을 보고도 알 수 있는 내용들은 제외하고 나머지의 내용을 적기로 한다. 이것들은 어차피 공부하면서 보아야 되기도 하고 정리도 잘 되어 있으며 시간을 가장 많이 낭비하는 것은 의도치 않은 내용들일 것이기 때문이다.


  안티바이러스는 물론이고 드라이버 개발 경력도, 주 분야가 개발인 것도 아닌 학생으로서 공부한 자료를 올린다는 것이 맞는 것인지는 모르겠다. 하지만 공부하는 입장에서 그리고 찾아보며 알아가고 구현해 보는데 너무 많은 시간을 보낸 입장에서 혹시나 공부하는 사람들이 적어도 검색에 필요한 키워드라도 하나 찾아갈 수 있지 않을까 하는 생각으로 올려본다. 다시한번 말하지만 전문가가 아닌 공부한 내용을 올린 것이다. 전체적인 수준은 당연하고 심지어 틀린 부분이 있을 수도 있다. 모르는 부분도 많이 있고 그렇게 적어 놓았으므로 읽는데 참고하길 바란다.



1.2 배경 지식

  안티바이러스에 조금의 관심이라도 있다면 인터넷에 널려있는 자료인 파일 시그니처 기반 안티바이러스 소스 코드들을 보면서 우리가 사용하는 안티바이러스가 단순하게 이러한 형태로 구현되어 있을 것이라고는 생각하지 않을 것이다. 아마도 20여 년 전에는 비슷한 형태를 가지고 있지 않았을까 싶다. 윈도우 XP 즈음에는 안티바이러스도 루트킷과 비슷한 형태를 갖었다고 한다. 인터넷에는 커널 모드에서의 후킹을 이용한 루트킷 관련 자료들이 많이 존재한다. 간략하게 구현적인 내용만 보자면 안티바이러스도 이것과 비슷하다고 한다. 그리고 이 정도까지도 인터넷에는 수많은 자료들이 존재한다.


  이제 최신 형태를 알아보겠다. 윈도우 비스타의 x64 버전부터는 많은 부분이 달라지게 되었고 내가 찾던 부분도 이쪽이었지만 아직도 인터넷에서는 관련 자료들을 찾기가 매우 힘든 편이다. 아마 이유가 있을 것이다. 보안 관련 정보는 대부분 공격과 관련된 정보이고 과거에도 보안 프로그램과 관련된 직접적인 내용이 많았다기 보다는 안티바이러스가 루트킷의 형태와 비슷하므로 굳이 찾아보면 어느 정도는 비슷하게라도 찾아볼 수 있었을 것 같다. 하지만 최신 윈도우 버전부터는 많은 부분이 달라져서 공격 입장에서 즉 루트킷이 그렇게 많이 활동하는것 같지는 않아 보인다. 그래서 공격자의 입장과는 다른 순수 보안 프로그램 구현과 관련된 내용이 인터넷에 많이 돌아다닐 일이 없어진 것이 아닌가 싶다.


  어쨌든 이제 비스타 x64 이후부터의 변화에 대해서 알아보려고 한다. 첫 번째는 KPP(Kernel Patch Protection)이다. 우리는 앞에서 루트킷이 커널 영역에서 후킹을 사용한다고 했다. 이것과 관련된 내용은 많이 있을 것이므로 간략하게 말하자면 KPP 다시 말해서 패치가드는 후킹의 대상이 되는 SSDT 같은 부분을 주기적으로 모니터링하여 변경이 일어나지 않게 하는 방식이다. 변경이 인식되는 즉시 BSOD가 뜨기 때문에 루트킷은 물론이고 이 영역을 후킹해서 보안 목적으로 사용하던 안티바이러스도 커널 코드 후킹을 사용할 수 없게 되었다.


  다른 하나는 KMCS(Kernel Mode Code Signing)이다. 이름과 같이 이제부터는 서명된 드라이버만 커널에 로드할 수 있게 되었다. 루트킷도 드라이버이기 때문에 이것을 우회하지 않는 이상 루트킷이 활동하기 어려워졌다. 물론 KPP와 달리 안티바이러스의 입장에서는 큰 상관은 없는 부분이다.


  마지막으로 필터 관리자가 커널에 추가되었는데 안티바이러스의 경우 이것을 사용하는 파일시스템 미니필터 드라이버를 개발함으로써 과거 후킹을 통해 파일들을 검사하던 것보다 더 개선된 방식을 사용할 수 있게되었다.


  지금까지 언급한 것들을 정리하자면 과거에는 커널 모드에서 후킹을 사용해서 악성코드 자체 프로세스 및 레지스트리를 보호하였을 것이고 파일 및 프로세스 모니터링도 수행하였다. 하지만 KPP로 인해서 커널 후킹이 불가능해졌고 마이크로소프트는 대신 파일시스템 미니필터 드라이버 및 여러 콜백 함수들을 제공하여 원래 사용하던 기능들을 제공해 준다. 





2. 드라이버 기본

2.1 컴퓨터 설정

  처음 공부하는 입장에서는 드라이버 개발과 관련해서 시작하는것 부터 까다롭다. 개발 환경에 대해서 설명하기 이전에 실행 대상이 되는 현재 컴퓨터부터 말해보겠다. 이것은 윈도우 10 x64이며 현재 테스트 모드로 실행 중이다. 앞에서도 언급하였듯이 KMCS로 인해서 본인이 개발한 드라이버를 본인 컴퓨터에 로드하는 것도 서명이 되어있어야만 가능하기 때문에 서명 정책을 사용하지 않게 설정한 것이다.


  기본적으로는 관리자로 명령 프롬프트를 열고 이곳에 다음 명령들을 입력하면 된다.

> bcdedit.exe -set loadoptions DDISABLE_INTEGRITY_CHECKS

> bcdedit.exe -set TESTSIGNING ON


  조금 된 컴퓨터의 경우 이 두 명령을 통해 "작업을 완료했습니다."라는 결과를 얻을 수 있다. 이후 컴퓨터를 재부팅(종료하고 다시 켜는 것이 아니다)하면 바탕화면의 오른쪽 아래 부분에 테스트 모드라는 글씨가 삽입되어 있는 것을 볼 수 있다.


  하지만 두 번째 입력에서 "요소 데이터를 설정하는 동안 오류가 발생했습니다. 값은 보안 부팅 정책에 의해 보호되며 수정 또는 삭제할 수 없습니다."라는 오류가 뜰 때가 있다. 일반적으로 최신 컴퓨터의 경우 강화된 보안을 위해 바이오스(BIOS) 펌웨어가 아닌 UEFI 펌웨어를 사용하는데 이것을 통해 Secure Boot(안전 부팅)가 지원된다. 어쨌든 이 설정을 없애주어야 한다. 컴퓨터 부팅 시 "F2" 또는 "DEL" 키를 계속 누르다 보면 UEFI 부팅 메뉴를 볼 수 있다. 이것은 설치된 것마다 다르겠지만 나의 컴퓨터의 경우 "부팅 옵션" 메뉴에서 "Safe Booting"을 OFF시켰다. 이제 아까처럼 명령어를 입력하면 오류가 뜨지 않을 것이고 재부팅 이후 테스트 모드를 확인할 수 있게 된다.


  반대의 경우 원상복구하는 명령들은 다음과 같다. 재부팅을 하고 다음에는 UEFI를 on시키면 된다.

> bcdedit.exe -set loadoptions ENABLE_INTEGRITY_CHECKS

> bcdedit.exe -set TESTSIGNING OFF



2.2 드라이버 개발 환경

  나의 경우 지원 문제로 인해 Visual Studio 2015와 WDK 10을 설치했다. WDK 10의 경우 아직 Visual Studio 2017을 지원하지 않았기 때문이다. 프로젝트의 경우 "템플릿 -> Visual C++ -> Windows Driver"까지 고른다. 여기서는 두 가지를 사용할 것이다. 일반적인 대부분의 것들은 "Legacy -> Empty WDM Driver"를 고른 후에 개발하면 된다. 하지만 미니필터 드라이버의 경우 "Devices -> Filter Driver: Filesystem Mini-filter"를 골라야 한다. 여기서 다루는 예제 중 미니필터 드라이버를 제외하고 다른 것들만 간단하게 테스트해보고 싶다면 WDM으로 간단하게 개발하면 될 것이고 미니필터 드라이버를 포함하여 전체적으로 개발하고자 한다면 미니필터 드라이버를 고른다. 이것은 미니필터 드라이버 뿐만 아니라 다른 추가적인 내용을 넣을 수도 있기 때문이다.


  현재 컴퓨터가 x64이기 때문에 Release 모드 및 x64를 선택했다. 소스 코드의 경우 생성할 때 확장자를 c로 설정한다. 이제부터 여러 설정을 추가하도록 하겠다. 사실 개발하고 로드하고 하면서 여러 개의 에러가 뜨는데 물론 쉽게 검색하여 설정할 수 있지만 여기서는 정리하면서 미리 설정하도록 한다. 


  먼저 "프로젝트 -> 속성" 항목을 연다. "Driver Settings -> Target OS Versiion"은 드라이버가 로드될 컴퓨터의 운영체제를 선택할 수 있다. 내 컴퓨터는 윈도우 10이기 때문에 윈도우 10으로 설정한다. 다른 컴퓨터에서 드라이버를 설치할 경우 윈도우 7 같이 다른 버전을 설정할 수도 있다. 


  다음으로 "inf2Cat -> UseLocalTime"를 "예" 즉 "/uselocaltime"으로 설정한다. 이것을 설정하지 않으면 "Inf2Cat, signability test failed." 에러를 볼 수 있다. 마지막으로 "링커 -> 명령줄"의 추가 옵션 부분에 "/integritycheck"를 입력한다. 뒤에서 사용할 확장 함수들의 경우(Ex가 붙은) 무결성 검사 옵션을 사용하지 않으면 STATUS_ACCESS_DENIED 에러가 발생하여 해당 함수를 사용할 수 없게 된다.


  현재 개발하는 것이 파일시스템 미니필터 드라이버라면 .inf 파일을 수정할 필요가 있다. 대부분 실제 회사에서 개발하는 입장에서야 손댈 것이 많아보이지만 테스트하는 입장에서는 에러가 뜨지 않게만 하면 될 것으로 보인다. 에러 코드는 다른 것과 다르게 직접적인데 Class, ClassGuid 부분을 위의 주석에 보이는 형태로 수정하면 된다. 그대로 복사해서 붙여넣기 해도 테스트하는데에는 지장이 없다.


  마지막으로 "C/C++ -> 일반 -> 경고를 오류로 처리"를 아니요(/WX-)로 설정한다. 능력의 부족으로 최대한 좋게 만드려 해도 코드가 조잡하므로 간단한 경고들은 무시하려고 했다.



2.3 도구

  드라이버 개발 및 테스트를 위해서는 Visual Studio 2015와 WDK 10 외에도 여러 도구들을 알아야 한다. 먼저 드라이버 로더의 경우 개인적으로 다음 링크의 오픈 소스 프로그램을 사용한다. [ https://github.com/tandasat/DrvLoader ] 물론 로드 기능밖에 없긴 하지만 Legacy, WDM 뿐만 아니라 파일 시스템 미니필터 드라이버도 간단하게 로드할 수 있다. 해당 소스 코드를 컴파일하고 사용할 때는 관리자 모드로 명령 프롬프트를 열고 인자로 드라이버 바이너리의 경로를 주면 된다. 파일 시스템 미니필터 드라이버를 설치할 때는 --filter 등의 옵션을 주면 성공적으로 설치해 준다. 참고로 미니필터 드라이버의 경우 소스 코드를 보면 Altitude(고도)가 370000으로 설정되어 있으므로 컴파일 시에 수정하면 된다. 나의 경우에는 굳이 수정하지 않고 테스트했다. 참고로 명령 프롬프트에서 fltmc를 입력하면 현재 설치되어 있는 필터 드라이버들과 Altitude를 볼 수 있다. 


  원래는 sc를 이용해서 옵션으로 create를 주는 방법도 있지만 DvrLoader를 사용하는 것이 훨씬 간단하기 때문에 이것을 사용한다. 대신 다른 명령어는 sc를 사용하기로 한다. DvrLoader를 통해 드라이버를 로드하면 자동으로 sc create 뿐만 아니라 sc start도 사용된다. 즉 드라이버 로드 이후 바로 시작하는 것이다. 우리는 등록한 드라이버를 삭제하거나 잠시 언로드 시킬 필요가 있을 것이다. 다음은 myDriver를 설치한 이후 설치된 드라이버의 이름이 맞는지 확인해 보고 stop, 다시 start 그리고 delete까지 수행해 보겠다.


> sc query myDriver

  myDriver로 설치된 것이 맞다면 기본 정보들이 보일 것이다.

> sc stop myDriver

  드라이버를 중지시킨다.

> sc start myDriver

> sc stop myDriver

  드라이버 재실행 이후 다시 중지시킨다.

> sc delete myDriver

  드라이버를 삭제한다. 참고로 위처럼 드라이버를 stop시킨 후에 삭제해야 한다. 참고로 create 옵션은 다음과 같이 사용한다. 하지만 미니필터 드라이버의 경우 복잡해지기 때문에 그냥 DvrLoader를 사용하기로 한다.

> sc create [drivername] binPath= [path] type= kernel


  지금까지 기본 개발 뿐 아니라 로드와 언로드를 알아보았다. 하지만 우리는 드라이버가 제대로 동작하는지 확인하기 위해서 DbgPrint(), DbgPrintEx() 등의 함수를 이용해 메시지를 받아 볼 것이다. 이 메시지들은 DebugView를 이용해서 확인할 수 있다. 참고로 "Capture" 항목에서 [ Capture Kernel, Enable Verbose Kernel Output, Capture Events ]를 설정해야 메시지를 받아 볼 수 있다. 뒤에서는 DLL을 로드하게 될 것인데 우리는 이것이 제대로 로드되었는지 확인하기 위해 dllmain()에서 OutputDebugStringA()를 이용해 메시지를 출력하게 할 것이다. 이 메시지는 "Capture" 항목에서 [ Capture Win32, Capture Events ]를 설정하면 볼 수 있다.


  마지막으로 Process Explorer를 알아보겠다. 이것은 프로세스 종료 시도 시에나 DLL 인젝션을 수행했을 때 DLL이 제대로 로드되었는지를 확인할 때 사용한다. "View -> Lower Pane View -> DLLs"를 클릭하면 아래에 DLL 창이 뜬다. 이제 특정 프로세스를 클릭하면 아래에 해당 프로세스에서 로드한 DLL들의 목록을 볼 수 있다. 프로세스 종료는 해당 프로세스를 마우스 오른쪽 키로 클릭한 후 "Kill Process"를 클릭하면 된다.



2.4 DriverEntry / DriverUnload

  앞에서도 설명했듯이 아주 기초적인 내용은 적지 않기로 한다. 물론 개인적인 정리를 포함하기도 하기 때문에 뜬금없는 내용은 있을 것이다. 프로그램 작성 시 인자를 선언만 하고 참조를 하지 않는 경우에는 C4100 에러가 발생한다. 물론 앞에서 "경고를 오류로 처리"를 아니오로 설정하였기 때문에 컴파일하는데는 지장이 없겠지만 UNREFERENCED_PARAMETER 매크로를 이용해서 참조하지 않을 경우에도 warning을 없앨 수 있다.


  이것 외에도 여기에는 보이지 않지만 다른 곳에서 자주 사용되는 내용을 정리하겠다. 먼저 NTSTATUS는 시스템에서 내부적으로 사용하는 오류 코드이다. 대표적인 값으로 STATUS_SUCCESS가 있는데 이것은 0x00000000 즉 성공을 의미한다. 일반적으로 특정 함수를 실행시키고 반환 값을 NTSTATUS 변수로 받는다. 이후 NT_SUCCESS 매크로에 해당 변수를 인자로 넣음으로써 반환 값이 성공했는지 여부를 확인한다. 이 매크로는 NTSTATUS 값이 STATUS_SUCCESS 같이 성공에 해당하는 값인 경우에는 TRUE를, 실패의 경우에는 FALSE를 반환한다. NTSTATUS 값들 중에 성공을 의미하는 값이 하나가 아니기 때문에 NT_SUCCESS 매크로를 이용해서 간단하게 성공 여부를 TRUE / FALSE로 확인할 수 있다.


  마지막으로 RtlInitUnicodeString() 함수도 자주 사용된다. 유저 모드에서와는 달리 드라이버에서는 이 함수를 이용해서 유니코드 문자열을 초기화해서 사용하는 경우가 많은 것 같다.


  DriverEntry()에서는 먼저 DriverUnload를 등록할 것이다. 그리고 Installxxx 형태의 함수들을 호출하는데 이것은 우리가 다룰 주제들 즉 각 기능들을 설치하는 함수들이다. 반대로 DriverUnload()에서는 이렇게 설치한 기능들을 제거한다. 드라이버 로드 시에 DriverEntry()가 호출되면서 이 기능들이 설치되고, 드라이버 언로드 시에 DriverUnload()가 호출되면서 설치한 기능들을 제거할 것이다. 


  소스 코드는 크게 3개로 나누었다. 하나는 파일시스템 미니필터 드라이버(miniFilter), 두 번째는 행위기반 분석을 위해 후킹 DLL을 삽입하는 방식(BehaviorBased), 마지막으로 프로세스 보호 및 자가 보호 그리고 레지스트리 보호(threeProtection)이다. 3개를 통합하려고 했지만 전혀 안정적으로 보이지 않아서 포기했다. 





3. 자가 보호

  코드 상으로 보면 DriverEntry의 InstallSelfProtect()와 DriverUnload()의 UnInstallSelfProtect()가 관련된 내용이다. 실제 함수는 SelfProtect.h에 구현되어 있다. 여기서 사용하는 함수는 ObRegisterCallbacks()와 ObUnRegisterCallbacks()이다.


  ObRegisterCallbacks() 함수는 스레드, 프로세스 그리고 데스크탑 핸들 오퍼레이션 시에 호출되는 콜백 루틴들을 등록해 준다. 구체적으로 예를들어 보자면 어떤 프로세스가 보호하려는 프로세스를 종료시키고 싶다고 하자. 이 경우에는 특정한 행위를 수행하기 위해 먼저 핸들을 얻을 것이다. 등록된 콜백 함수는 이렇게 핸들을 얻을 때 호출된다. 핸들을 얻을 때는 대상에 대한 접근 권한을 설정하는데 이 콜백 함수는 얻으려는 접근 권한 중에서 특별한 것들을 제거할 수 있다. 이로써 다른 프로세스가 보호받는 프로세스를 종료하기 위해 핸들을 얻고 종료시키려고 하지만 종료할 수 있는 권한이 제거되어 있기 때문에 종료가 불가능하게 된다. 물론 Handle Operation 이전 뿐만 아니라 이후에 호출되는 콜백 루틴도 등록할 수 있다.


  이 예제에서는 구체적으로 이름이 "calc.exe"인 프로세스에 대한 핸들을 얻을 때 PROCESS_TERMINATE, PROCESS_VM_READ, PROCESS_VM_WRITE, PROCESS_VM_WRITE 권한을 제거한다. PROCESS_TERMINATE 권한 제거로 인해 다른 프로세스에서 이것을 종료시킬 수 없게 된다. 나머지 3개는 DLL 인젝션 시에 많이 본 권한일 것이다. DLL 인젝션 시에는 DLL을 삽입할 프로세스의 메모리를 조작할 필요가 있기 때문에 해당 권한이 필요했다. 하지만 이 권한을 제거함으로써 보호받는 프로세스는 다른 프로세스에 의해 종료될 수도, DLL 인젝션 공격을 받을 수도 없게 된다.


  개인적으로 이 함수는 아직까지 완벽하게 이해를 하지 못했다. 먼저 PROCESS_TERMINATE 권한을 제거하더라도 직접 종료가 가능한 것으로 보인다. 그리고 명령 프롬프트에서 taskkill 명령어를 이용한 종료도 가능한 것 같다. Process Explorer를 통해 제거하는 경우에는 종료가 불가능한 것으로 나온다. 그리고 나머지 세 권한도 이해하지 못하는 부분이 있다. "calc.exe" 프로세스를 먼저 실행시킨 후 드라이버를 로드하면 해당 권한을 얻는 것이 불가능한 것은 맞는 것 같다. 하지만 드라이버를 먼저 로드한 이후에 해당 프로세스를 실행하면 실행이 제대로 되지 않는다. Process Explorer를 보면 해당 프로세스가 Suspended 상태로 존재한다. 이것을 Resume 시키면 제대로 실행되며 이후에 종료가 불가능한 것으로 보아 제대로 동작하는 것 같지만 실행 자체가 제대로 되지 않으며 그 이유를 아직도 모르겠다. 뭔가 잘못된 부분이 있든지 아니면 프로세스 실행 시에 위의 세 권한이 필요한 것인지 잘 모르겠다.


  마지막으로 Altitude 즉 고도에 관한 설정도 존재하는데 현재 개발 중인 드라이버가 미니필터 드라이버가 아니라면야 아무 값을 넣어도 상관 없어 보인다. 그리고 미니필터 드라이버라고 하더라도 값이 달라도 큰 차이는 없는 것 같다. 이것과 관련된 강제적인 사항을 아직 찾지는 못했다.





4. 스캐닝

4.1 파일 시그니처 기반 스캐닝

  처음 파일시스템 미니필터 드라이버를 오픈하면 800라인이 넘는 스켈레톤 코드가 보인다. 여기서는 이것 대신 최소화된 내용인 예제 코드를 이용하도록 한다. 기본 방식은 아주 간단하다. minifilter driver registration structure를 설정한 후 FltRegisterFilter()에 인자로 넣고 미니필터 드라이버를 등록한다. 이후 FltStartFiltering()을 이용해 필터링을 시작한다. Registration 구조체에 등록한 것들 중 중요한 부분은 FLT_OPERATION_REGISTRATION 구조체들의 배열이다. 이 예제에서는 IRP_MJ_CREATE, IRP_MJ_SET_INFORMATION의 경우에 PFLT_PRE_OPERATION_CALLBACK 루틴을 등록하였다. 자세히 설명하자면 파일의 쓰기 및 삭제에 해당하는 IRP에 대해 우리가 구현한 콜백 함수를 등록하겠다는 의미이며 PFLT_PRE_OPERATION_CALLBACK이므로 이 행위 이전에 호출되게 된다. 


  일반적으로 백신에서는 필터링을 통해 파일을 검사하고 허용할지 여부를 판단하겠지만 이 예제에서는 단지 DbgPrint()로 어떤 파일에 어떤 행위가 발생하였는지를 출력하고자 한다. 등록한 루틴 즉 PreOperationCallback()을 살펴보면 IRP_MJ_CREATE의 경우 FILE_WRITE_DATA 또는 FILE_APPEND_DATA일 때 process_irp() 함수를 호출하고, IRP_MJ_SET_INFORMATION의 경우 DeleteFile일 때 process_irp()를 호출한다. process_irp()에서는 각 상황에 맞게 어떤 이름의 파일에서 쓰기 또는 삭제 행위가 일어났는지를 DbgPrint()로 보여준다.



4.2 행위 기반 스캐닝

  행위 기반 즉 휴리스틱 스캐닝은 크게 두 가지로 나뉜다. 하나는 정적 휴리스틱이며 다른 하나는 동적 휴리스틱이다. 정적 휴리스틱의 경우에는 파일을 분석하여 의심스러운 특징을 찾아내어 판단하는 방식이다. 동적 휴리스틱의 경우에는 두 가지 방식으로 나뉘는 것 같다. 


  하나는 HIPS(Host based Intrusion Prevention System)에서 사용되는 방식이다. 이 기능을 순수하게 휴리스틱 방식으로 사용하는 안티바이러스가 있는가 하면 HIPS로 분리하여 보여주는 안티바이러스도 있는 것으로 보인다. 이것은 API 함수들을 후킹하여 실시간으로 어떤 함수들이 사용되는지를 검사하여 의심스러운 행위로 보일 때 탐지하는 방식이다. 


  다른 하나는 에뮬레이터를 이용한 방식이다. 안티바이러스에서는 에뮬레이터 외에도 샌드박스라고도 불리는데 그냥 같은 의미로 사용되는 개념인 것 같다. 에뮬레이터는 굳이 동적 휴리스틱 방식 외에도 다양하게 사용된다. 즉 실행 파일 에뮬레이팅 외에도 셸 코드 분석에 그리고 실행 파일 언패킹에도 사용된다. 어쨌든 에뮬레이터 내에서 실행 파일을 실행시키고 그 행위를 분석하는 방식이다.


  여기서는 앞에서 HIPS로 설명한 부분 즉 PsSetLoadImageNotifyRoutine()을 이용한 방식을 설명하도록 한다. PsSetLoadImageNotifyRoutine() 함수는 이미지가 로드될 때 호출되는 콜백 루틴을 등록한다. 이 예제에서는 이미지 로드 시 로드된 이미지가 ntdll.dll인지 먼저 검사한다. ntdll.dll인 경우 Kernel to User land APC injection 방식을 이용한다. 아직 제대로 정리하진 못했지만 유저 모드에서 APC를 이용한 전형적인 DLL 인젝션 방식과 비슷해 보인다. 구체적으로는 ntdll.dll의 LdrLoadDll() 함수를 호출시키는데 인자로 삽입하길 원하는 DLL의 경로를 넣는다. 


  이 예제에서는 x64 DLL의 경우 System32 폴더에, x86 DLL의 경우 SysWOW64 폴더에 각각 이름을 InjectionMitigationDLLx64.dll 그리고 InjectionMitigationDLLx86.dll로 정하고 넣어 놓는다. 개인적으로는 dllmain()에서 OutputDebugStringA()를 이용해 메시지를 출력하게 함으로써 dll이 제대로 로드되었는지를 확인하였다. 또는 Process Explorer로 현재 로드된 DLL들을 확인해 볼 수 있다.


  이것은 예제일 뿐이고 검색하여 얻은 정보에 의하면 안티바이러스에서는 이러한 방식으로 ntdll.dll이 로드될 때 후킹 엔진이 구현된 DLL을 삽입시킨다. 삽입된 DLL은 로드될 때 dllmain()이 실행되는데 악의적인 행위에 사용될 가능성이 있는 API 함수들을 후킹한다. 즉 어떤 API 함수들이 호출되는지에 대하여 보고받기 위해 후킹하는 것이다. 안티바이러스는 각 후킹된 API들이 호출되는 그 행위를 이용해 분석하여 행위 기반 분석을 수행한다. 





5. 프로세스 / 스레드 생성 보호

  코드 상으로 보면 DriverEntry의 InstallPsProtect()와 DriverUnload()의 UnInstallPsProtect()가 관련된 내용이다. 실제 함수는 PsProtect.h에 구현되어 있다. 여기서 사용하는 함수는 PsSetCreateProcessNotifyRoutineEx()이다. 이것은 PsSetCreateprocessNotifyRoutine()의 확장된 버전이다. 이 함수로 등록한 콜백 루틴은 프로세스 생성 직전에 호출된다. 참고로 두 번째 인자의 경우 설치 시에는 FALSE, 제거 시에는 TRUE를 입력한다.


  이 예제에서는 간단하게 프로세스 실행 전에 호출되어 이 프로세스가 특정한 이름을 갖는지 검사한 후에 실행 여부를 판단한다. PsSetCreateThreadNotifyRoutineEx() 및 PsSetCreateThreadNotifyRoutine()도 프로세스가 아니라 스레드라는 것만 빼면 같은 내용이다. 이 예제에서는 이 4가지 함수들 중에서 PsSetCreateProcessNotifyRoutineEx()만 사용하였다.


  일반적으로 안티바이러스에서는 이 함수를 이용해서 (어느 안티바이러스 업체에서는 PsSetCreateThreadNotifyRoutine()만 사용하는 것 같다) 프로세스가 실행될 때 마다 이 콜백 함수를 이용하여 검사한 후에 실행 여부를 판단할 것이다. 물론 여기서 사용하는 예제처럼 검사에 이름을 이용하지는 않을 것이다.





6. 레지스트리 보호

  코드 상으로 보면 DriverEntry의 InstallRegMonitor()와 DriverUnload()의 UnInstallRegMonitor()가 관련된 내용이다. 실제 함수는 RegMonitor.h에 구현되어 있다. 여기서 사용하는 함수는 CmRegisterCallbacksEx()와 CmUnRegisterCallback()이다. 참고로 CmRegisterCallbacksEx()는 CmRegisterCallbacks()의 확장 버전이며 여기서는 이 확장 버전을 사용한다.


  이 함수는 Registry Operation 시에 호출되는 콜백 루틴을 등록시켜 준다. 이 예제에서는 콜백이 호출된 경우 어떤 operation을 수행하려고 하는지 검사한다. 해당 operation이 RegNtPreCreateKeyEx, RegNtPreOpenKeyEx라면 다시 말해서 특정 프로세스에서 해당 키를 생성하거나 읽으려는 operation이 요청되었다면 그 키가 우리가 보호하려는 키인지 검사한 후에 맞다면 STATUS_ACCESS_DENIED를 반환시킨다.


  안티바이러스의 경우 드라이버 뿐만 아니라 서비스도 실행 중일 것인데 악성코드가 이것들의 정보를 담고 있는 레지스트리를 삭제하거나 수정하려는 시도를 할 수 있다. 테스트를 위해서 regedit.exe를 실행한 후에 보호하고 있는 레지스트리 키를 클릭하면 접근이 거부되었다는 것을 확인할 수 있다.





7. ELAM 및 Protected Process

  윈도우 8부터 Secure Boot라는 기능이 도입되었다. 이것은 윈도우 부팅 설정 및 구성 요소들을 보호하고 ELAM(Early Launch Anti-Malware) 드라이버를 로드한다. ELAM 드라이버는 다른 boot-start 드라이버들보다 먼저 실행되어 다른 드라이버들을 검사하는 역할을 한다. 즉 다른 드라이버들의 무결성을 검사하며 부트 드라이버가 수정되었는지 확인하여 부트킷을 방지한다.

  안티바이러스의 유저 모드 서비스는 과거부터 지금까지 악성코드의 공격 대상이 되어 왔다. 윈도우 8.1부터는 Protected Service라는 개념을 통해 유저 모드 서비스를 보호한다. 이것은 System Protected Process로서 실행되는 서비스이다. 안티바이러스 벤더들은 ELAM 드라이버를 이용해 사용자 모드 서비스를 Protected Service로 실행한다. 이를 위해서 ELAM 드라이버의 리소스 섹션에는 해당 바이너리(서비스)를 서명하는데 사용되는 인증서에 대한 정보가 있어야 한다. 부트 타임 시 이 리소스 섹션이 추출되어 인증 정보를 검증하고 사용자 모드 서비스를 등록하면 서비스는 Protected Service로서 실행된다.

  권한 관련 문서에서 설명한 내용이지만 간략하게 언급하자면 관리자 권한인 High Integrity Level을 가진 프로세스의 경우 SeDebugPrivilege를 활성화할 수 있고 이를 통해 System Integrity Level을 가진 프로세스에 코드 인젝션을 수행할 수 있다. 다시 말해서 Protected Process는 관리자 권한으로 실행된 프로세스로부터의 공격을 방어하기 위한 메커니즘이다. Protected Process는 PROCESS_ALL_ACCESS 뿐만 아니라 PROCESS_CREATE_PROCESS, PROCESS_CREATE_THREAD, PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE 등의 접근 권한이 거부된다.





8. 유저 모드 애플리케이션과의 통신

  이 내용은 예제 코드의 communication 부분에 정리되어 있다. 기본적인 사항을 정리한 후 예제와 함께 설명할 것이다. 유저 모드 애플리케이션과 커널 모드 드라이버가 통신하는 방법 중에서 IOCTL 명령을 이용한 방식을 설명한다. IOCTL 명령은 둘 사이의 통신을 위해 프로그래머가 정의한 명령이다. IRP는 일종의 데이터 구조체로서 드라이버에서는 IRP의 종류에 맞게 이것을 처리하는 함수가 필요하다. I/O 관리자는 애플리케이션의 요청이 있을 때 IRP를 생성해 드라이버 오브젝트의 MajorFunction[] 필드를 이용해 해당 디스패치 루틴으로 IRP를 전달(dispatch)해 준다. 그래서 Dispatch 루틴으로 불린다. 즉 드라이버가 처리하는 IRP 명령어들을 위한 디스패치 루틴들이다.


  애플리케이션은 드라이버와 통신하기 위해서는 CreateFile()하는데 필요한 핸들을 구해야 한다. 이것을 위해 드라이버는 디바이스 이름, 이것의 심볼릭 링크 그리고 디바이스 오브젝트에 대한 포인터가 필요하며 이것을 전역으로 설정한다. 이후 IoCreateDevice()로 디바이스를 그리고 IoCreateSymbolicLink()를 통해 심볼릭 링크를 생성한다. IoCreateDevice()로 디바이스 생성 이후 IoCreateSymbolicLink()로 디바이스에 심볼릭 링크를 지정하는 것이다. 마지막으로 처리할 IRP에 대한 Major Function 즉 디스패치 루틴을 등록한다. IRP_MJ_CREATE의 경우에는 애플리케이션이 CreateFile()로 디바이스 드라이버의 핸들을 얻을 때 발생한다. IRP_MJ_CLOSE는 애플리케이션이 CloseHandle()을 사용할 때 발생한다. 여기서 중요하게 다룰 것은 IRP_DEVICE_CONTROL이다. 이것은 애플리케이션에서 deviceIoControl()을 이용해서 드라이버와 통신할 때 발생한다. 먼저 CTL_CODE 매크로를 이용해 커스텀 컨트롤 코드를 정의한다. 이것은 애플리케이션에서도 똑같이 정의할 것이며 애플리케이션에서 이 컨트롤 코드를 보낼 시에 특정 행위를 수행한다. 이 예제에서는 애플리케이션에서 커스텀 컨트롤 코드를 받은 경우에 같이 받은 데이터를 DbgPrint()로 보여주고 동시에 데이터를 애플리케이션으로 보낸다.


  애플리케이션의 경우 CreateFilte()을 통해 통신할 디바이스의 핸들을 얻는다. 이후 DeviceIoControl()을 통해 드라이버와 통신한다. 이 함수는 2개의 버퍼를 제공하며 디바이스 드라이버와 양방향으로 통신이 가능하다. 드라이버에서와 같이 CTL_CODE 매크로를 이용해 커스텀 컨트롤 코드를 정의한다. DeviceIoControl()에는 커널에서 받은 데이터를 저장할 버퍼도 갖는다. 이 예제에서는 애플리케이션에서 커스텀 컨트롤 코드와 함께 드라이버로 데이터를 보냄과 동시에 드라이버로부터 데이터를 읽는다.




9. 기타

9.1 네트워크 보호

  안티바이러스에서도 기본적으로 방화벽 기능이 제공된다. 과거에는 NDIS 및 TDI 필터 같은 드라이버를 개발해야 했지만 최근 비스타 부터는 WFP(Windows Filtering Platform)라는 플랫폼을 제공한다. 이것은 네트워크 필터링 애플리케이션을 만들기 위해 제공되는 플랫폼으로서 API와 시스템 서비스들의 집합이다. 참고로 과거의 NDIS / TDI는 다른 필터 드라이버들보다 개발하기 위한 난이도가 있었지만 WFP라는 간단한 개발 플랫폼이 제공됨으로 인해 최근 버전의 윈도우에서 돌아가는 안티바이러스 프로그램들은 이것을 사용한다고 한다.



Posted by SanseoLab

블로그 이미지
Malware Analyst
SanseoLab

태그목록

공지사항

Yesterday
Today
Total

달력

 « |  » 2024.4
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30

최근에 올라온 글

최근에 달린 댓글

글 보관함