0. 개요

[ http://sanseolab.tistory.com/13 ]

[ http://sanseolab.tistory.com/33 ]

[ https://github.com/101196/simpleAVdriver ]


  앞의 글들과 참조하면서 손대본 코드들을 통해서 보안 관련 드라이버 개발에 대해서 조금씩 배웠고 이제 간단한 프로젝트를 통해서 구현해보려고 한다. 비슷한 부분도 많고 더 추가된 부분도 많다. 드라이버 개발 환경 구축 및 기본 설정들과 관련해서는 꼭 위의 문서들을 읽어볼 필요가 있으며 여기서는 간단하게 소스 코드 설명만 하려고 한다.


  이 프로젝트의 이름은 ProcLogger이며 간단하게 프로세스의 생성 및 종료를 기록하는 역할 및 기타 기능을 수행한다. 각종 프로세스의 시작 및 종료 및 기타 정보들에 대한 기록을 남기며 보호할 프로세스를 설정할 수 있다. 동시에 드라이버의 자체 레지스트리 보호도 존재한다. 소스 코드는 다음 주소이다.


[ https://github.com/101196/ProcLogger ]





1. Registry Monitor

  Registry Monitor에서는 이 드라이버와 관련된 설정이 들어있는 레지스트리를 보호한다. 정확히 말하자면 아예 이 레지스트리에 대한 오픈을 차단하는 방식이다. 다른 프로세스는 물론이고 regedit.exe에서도 해당 키를 클릭한 경우 접근이 거부되는 것을 알 수 있다. [ ref : https://github.com/markjandrews/CodeMachineCourse/tree/master/source/kerrkt.labs/labs/HideReg ]


  ProcLogger.c를 보면 DriverEntry()에서 InstallRegMonitor()를 통해 등록하며 DriverUnload() 루틴에서 UnInstallRegMonitor()를 통해 제거한다. RegMonitor.h에는 함수 선언만 들어가 있으므로 실질적인 구현은 RegMonitor.c를 보면 된다. 


  InstallRegMonitor() 함수부터 살펴보면 윈도우에서 제공되는 CmRegisterCallbackEx()를 통해 레지스트리 콜백 루틴 즉 RegistryFilterCallback()을 등록하는 것을 볼 수 있다. CmRegisterCallbackEx()는 콜백 루틴을 등록하여 레지스트리에 대한 작업이 이루어지기 전에 이 루틴이 호출될 수 있게 한다. 그러므로 레지스트리에 대한 작업이 이루어질 때 마다 호출되는 이 콜백 루틴이 중요하다. 먼저 CheckProcess() 함수를 호출한다. 이 함수는 레지스트리 작업을 수행하는 프로세스가 services.exe 또는 svchost.exe라면 TRUE를 반환하여 더 이상의 작업을 하지 않는다. 만약 다른 프로세스의 경우에는 다음 조건문이 실행된다. 


  여기서는 Argument1 즉 레지스트리에 대한 작업이 RegNtPreCreateKeyEx 또는 RegNtPreOpenKeyEx인 경우에만 작업을 수행한다. 그리고 Argument2를 통해 RootObject와 CompleteName을 넣고 RegPreOpenKey() 함수를 호출한다. 이것들은 각각 루트 레지스트리 키를 나타내는 레지스트리 키 오브젝트에 대한 포인터와 레지스트리 키의 경로 및 이름을 나타낸다. 참고로 경로는 상대적일 수도 있고 절대 경로일 수도 있다.


  RegPreOpenKey() 함수에서는 앞에서도 말했듯이 경로가 상대 경로일 수도 있고 절대 경로일 수도 있기 때문에 추가적인 작업을 수행한다. 만약 RootObject가 유효하다면 이것은 CompleteName이 전체 경로를 가지고 있다는 의미이므로 간단하게 CheckPolicy() 함수에 이 CompleteName을 넣는다. 아닌 경우에는 복잡한데 CmCallbackGetKeyObjectID()를 이용한다. 이 함수에 RootObject를 넣고 RootObjectName으로 경로를 받아온다. 이후 CompleteName이 유요한지 검사한 후, 유효하다면 받아온 경로 즉 RootObjectName과 CompleteName을 연결하고 CheckPolicy()에 넣어 호출하며 유효하지 않다면 RootObjectName만 넣어서 호출한다.


  지금까지는 CheckPolicy()에 넣을 제대로된 경로를 만드는 작업이었다.  참고로 우리는 "\\REGISTRY\\MACHINE\\System\\CurrentControlSet\\Services\\ProcLogger" 등의 문자열을 미리 정의해 놓았다. 이 레지스트리 값들은 일반적으로 드라이버가 등록될 때 설정 정보가 저장되는 위치이다. CheckPolicy()는 앞에서 정의한 이 문자열들과 인자로 받은 경로를 비교한다. 전체적으로 정리해 보자면 services.exe 또는 svchost.exe가 아닌 프로세스에서 RegNtPreCreateKeyEx 또는 RegNtPreOpenKeyEx의 작업을 수행할 때 이 대상이 되는 레지스트리 값이 ProcLogger 드라이버라면 작업을 거부하는 것이다. 즉 CheckPolicy()에서 Matched가 TRUE가 되며 이 경우 결국 콜백 함수에서는 STATUS_ACCESS_DENIED를 반환하여 접근 거부가 되게 하는 것이다.





2. ProcLogger

  Logger에서는 프로세스의 생성과 종료를 감시하여 텍스트 파일에 저장한다. ProcLogger.c를 보면 DriverEntry()에서 InstallProcLogger()를 통해 등록하며 DriverUnload() 루틴에서 UnInstallProcLogger()를 통해 제거한다. loggers.h에는 함수 선언만 들어가 있으므로 실질적인 구현은 loggers.c를 보면 된다. 


  InstallProcLogger() 함수부터 살펴보면 윈도우에서 제공되는 PsSetCreateProcessNotifyRoutineEx()를 통해 콜백 루틴을 등록한다. 간단하지만 그래도 눈여겨 볼만한 것은 콜백 루틴에서 받는 인자들이다. PEPROCESS Process, HANDLE ProcessId, PPS_CREATE_NOTIFY_INFO인데 각각 생성 또는 종료되는 프로세스에 대한 포인터 그리고 PID 마지막으로 생성되는 경우에만 존재하는 CreateInfo이다. 이것들을 각각 이용할 것이며 CreateInfo의 경우에는 생성될 때만 존재하기 때문에 종료시에는 생성 시 보다 로그에 기록되는 정보가 적을 것이다. 


  소스 코드와 간단한 주석만으로도 쉽게 이해할 수 있는 내용이다. 시간의 경우 KeQuerySystemTime(), ExSystemTimeToLocalTime(), RtlTimeToTimeFields()를 통해 구했다. Integrity Level은 PEPROCESS Process을 인자로 넣고 PsReferencePrimaryToken() 및 SeQueryInformationToken()을 통해 구한 후 값을 비교하여 가각의 레벨에 맞게 정의하였다. 참고로 생성인지 종료인지는 CreateInfo가 NULL 값을 갖는지에 대한 여부로 판단하였다.


  우리는 인자를 통해 프로세스의 PID를 알 수 있으며 CreateInfo->ImageFileName로 생성 시에는 이미지의 경로도 알 수 있다. 그리고 부모 프로세스와 생성자 프로세스의 경우에는 각각 CreateInfo->ParentProcessId, CreateInfo->CreatingThreadId.UniqueProcess를 통해 알 수 있고 이것을 가지고 GetProcessNameFromPid()를 호출하여 프로세스 이름도 알 수 있다.


  로그 파일은 ZwCreateFile() 및 ZwWriteFile()을 통해 기록된다. 결국 로그에는 다음과 같이 기록될 것이며 각각 생성 또는 종료 시에 해당한다.


[ 날짜 및 시간 ] [ 생성 또는 종료 ] [ 이미지 경로 ]  [ 프로세스 이름 (PID) ] [ 부모 프로세스 이름 (부모 PID) ] [ 생성자 프로세스 이름 (생성자 PID) ] [ Integrity Level]

- 날짜 및 시간 :  년:월:일:시:분:초

- 생성 또는 종료 : CREATE / EXIT

- Integrity Level :  LOW / MEDIUM / HIGH / SYSTEM


[ 날짜 및 시간 ] [ 생성 또는 종료 ] [ N/A ]  [ 프로세스 이름 (PID) ] [ N/A ] [ N/A ] [ Integrity Level]

- 날짜 및 시간 :  년:월:일:시:분:초

- 생성 또는 종료 : CREATE / EXIT

- Integrity Level :  LOW / MEDIUM / HIGH / SYSTEM


  실제 결과는 다음과 같다.






3. SelfProtect

  Self Protect에서는 보호하고자 하는 프로세스의 이름을 적는다. 참고로 이전 문서에서도 언급했듯이 드라이버가 로드된 상태에서 해당 프로세스를 실행시키면 제대로 실행이 되지 않기 때문에 드라이버 로드 이전에 프로세스가 미리 실행 중이어야 한다. [ ref : https://github.com/KKamaa/Driver-Loader/tree/master/ProtectDriver/ProtectDriver ]


 ProcLogger.c를 보면 DriverEntry()에서 InstallSelfProtect()를 통해 등록하며 DriverUnload() 루틴에서 UnInstallSelfProtect()를 통해 제거한다. SelfProtect.h에는 함수 선언만 들어가 있으므로 실질적인 구현은 SelfProtect.c를 보면 된다. InstallSelfProtect() 함수부터 살펴보면 윈도우에서 제공되는 ObRegisterCallbacks()를 통해 프로세스 핸들 작업 이전과 이후에 호출되는 루틴을 등록한다. 제거는 ObUnRegisterCallbacks()를 통해 수행한다.


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


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



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

최근에 올라온 글

최근에 달린 댓글

글 보관함