'프로세스 보호'에 해당되는 글 2건

  1. 2008.12.05 프로세스 보호
  2. 2008.12.03 프로세스를 보호하는 법.
Programming/Kernel / Driver2008. 12. 5. 17:03

커널단에서 프로세스를 보호하는 방법에 대해서 요약 작성 및 정리한 자료입니다.

 

 프로세스를 보호하는 드라이버를 거의 완벽에 가까울 정도로(?) 코딩하는 방법은 없을 지 고찰한 결과 아래와 같은 결론에 도달하게 되었습니다.

 목표 프로세스를 보호하는 방법

 

- 유저 모드 후킹

 

일단 기본적으로 프로세스를 조작하는 API인 NTDLL.DLL의 NtOpenProcess, NtTerminateProcess, NtReadVirtualMemory, NtWriteVirtualMemory, KERNEL32.DLL의 DebugActiveProcess 등이 후킹되어야 할 것입니다.

그리고 WINSTA.DLL의 WinStationTerminateProcess를 후킹하여 Terminal Service를 이용한 프로세스 종료를 차단합니다.

 

- 커널 후킹

 

ZwOpenProcess/ObReferenceObjectByPointer/PsLookupProcessByProcessId

ZwOpenProcess는 ObReferenceObjectByPointer를 호출하고 이는 다시 PsLookupProcessByProcessId를 호출하는데, 이들을 후킹하면 프로세스를 여는걸 어느정도 방지할 수 있을겁니다.

 

KeAttachProcess / KeStackAttachProcess

일단 프로세스의 메모리를 읽고 쓸땐 이 API를 사용하여 Page를 대상 프로세스의 Page로 전환해야하는데, 이를 방지하는 역할을 합니다.

 

ZwReadVirtualMemory/ZwWriteVirtualMemory/ZwTerminateProcess

프로세스의 메모리를 보호하며, 프로세스를 종료로부터 보호합니다.

 

ZwOpenThread/ZwGetContextThread/ZwSetContextThread/ZwTerminateThread

쓰레드를 보호합니다. 또한 쓰레드의 종료를 차단합니다.

 

ZwCreateFile (IoCreateFile)

이 것을 후킹하여 드라이버 혹은 프로세스 파일에 접근하는걸 차단할 수 있습니다.

경우에 따라선 ntoskrnl.exe를 접근하는 것을 막아 후킹을 해제하는걸 막아줄 수 있습니다.

 

ZwQuerySystemInformation

이 것을 후킹하면 프로세스를 숨길 수 있습니다. 밑에서 소개하는 DKOM보다는 안정적입니다.

 

- Internal 함수 후킹

 

PspTerminateThreadByPointer:

프로세스 종료의 가장 근본적인 API로 쓰레드를 종료시키는 가장 저수준의 API입니다. 따라서 이 함수를 후킹해놓으면 프로세스를 함부로 종료할 수 없습니다. 이 API는 NtTerminateThread()에서 ObfDereferenceObject()전에 호출되므로 코드바이트로부터 이를 추적해나갈 수 있습니다.

 

KiAttachProcess:

KeAttachProcess/KeStackAttachProcess의 Internal 함수로 후킹할 수 있으면 해놓는게 좋습니다. KeAttachProcess()에서 호출하는데, 이것이 이미 후킹되어있었을 수도 있으므로, 파일로부터 구해와야 합니다. 이 방법은 제가 블로그에 쓴 적 있습니다.

참고: [Unexported Symbol] KiAttachProcess() 주소값 구하기.

 

- DKOM

 

적절한 DKOM은 강력한 무기가 될 수 있습니다. 아래와 같은 방법을 사용함으로써 프로세스를 효과적으로 보호할 수 있습니다.

 

1. DebugPort = NULL

디버거를 무력화하여 예외 처리를 프로세스에게 넘기는 방법으로, 디버거를 무력화시키는 방법중 하나입니다. 이 값이 NULL인지 아닌지 체크하는 방법도 비교적 괜찮은 방법이나, NT4/2000계열에서는 간혹 쓰레기값이 들어있는 경우가 있기 때문에 권장하지 않습니다.

 

EPROCESS에서의 DebugPort의 Offset 구하는 방법은 대략 아래와 같이 코딩할 수 있습니다.

 

VOID CalcHandleTableOffset(VOID)
{
 PEPROCESS Proc = IoGetCurrentProcess();
 HANDLE PID = PsGetCurrentProcessId();
 PVOID GuessHandleTable = NULL;
 int i = 0;
 for(i = 0; i < PAGE_SIZE; i+=4)
 {
  GuessHandleTable = *(PVOID *)((PCHAR)Proc + i);
  if(MmIsAddressValid(GuessHandleTable))
  {
   if(*(PHANDLE)((PCHAR)GuessHandleTable + 0x08) == PID)
   {
    HANDLE_TABLE_OFFSET = i;
    return;
   }
  }
 }
}

 

CalcHandleTableOffset();

DEBUG_PORT_OFFSET = HANDLE_TABLE_OFFSET - 8;

 

2. EPROCESS->ActiveProcessLinks Unlinking

프로세스를 숨겨서 보호합니다. 추가적으로 EPROCESS의 UniqueProcessId를 이상한 값으로 바꾸면 핸들을 얻는것을 어느정도 막을 수 있습니다. (물론 관련 ETHREAD 구조체에서 PID를 얻을수도 있기 때문에 이 방법이 꼭 좋은건 아닙니다.)

만약 프로세스 ID를 바꾸었다면 프로세스 종료 전에 복구하여야합니다.(그렇지 않으면 CID_HANDLE_DELETION 블루스크린을 맛보시게 될겁니다.)

그리고, 추가적으로 PspCidTable이라는 Unexported symbol에서 프로세스 Offset을 NULL로 만들어놓으면, 근본적으로 핸들을 얻을 수가 없을겁니다. PspCidTable은 PsLookupProcessByProcessId()에서 ExMapHandleToPointer(Ex)를 호출하기 전에 참조하는 심볼로, 바이트 코드를 트래버싱하거나, XP 이상에서는 KPCR->KdVersionBlock의 PspCidTable 필드를 참조해서 구할 수도 있습니다.

 

3. EPROCESS::HandleTable Unlinking

숨겨진 프로세스를 찾는 대표적인 방법으로는 크게 두가지가 있습니다. EPROCESS::HandleTable를 traverse 하거나 PspCidTable을 트레버싱하는 방법입니다. 물론, ETHREAD를 이용하거나 기타 여러가지 방법(brute-force 등등)을 이용하면 뚫리긴 합니다만, HandleTable을 끊음으로서 더욱 찾기 힘들게 할 수 있습니다.

 

위에서 언급한 ActiveProcessLinks와 핸들 테이블은 아래와 같은 LIST_ENTRY 구조입니다.

typedef struct _LIST_ENTRY {
    PLIST_ENTRY Flink;
    PLIST_ENTRY Blink;
} LIST_ENTRY, *PLIST_ENTRY;

 

 드라이버를 효과적으로 숨기는 방법

 

1. PsLoadedModuleList unlinking

PsLoadedModuleList는 DRIVER_OBJECT 구조체의 DriverSection에 의해 Point됩니다. 그리고 아래와 같은 구조를 가지고 있습니다.(출처: rootkit.com 책)

typedef struct _MODULE_ENTRY
{
 LIST_ENTRY ModuleListEntry;
 DWORD Unknown1[4];
 DWORD BaseAddress;
 DWORD DriverStart;
 DWORD Unknown2;
 UNICODE_STRING Driver_Path;
 UNICODE_STRING Driver_Name;
} MODULE_ENTRY, *PMODULE_ENTRY;

 

ModuleListEntry를 순회해서 BaseAddress가 DriverObject->DriverStart와 같은 엔트리를 찾아내서 ModuleListEntry를 끊어주면 숨겨줄 수 있을겁니다.

 

2. IoGetDeviceObjectPointer Hook

심볼릭 링크로부터 DEVICE_OBJECT를 얻는 대표적인 방법으로 이를 후킹하면 DEVICE_OBJECT를 얻는걸 막을 수 있고, 결과적으로 드라이버를 2차적인 루트킷 스캐너로부터 숨길 수 있게됩니다. DEVICE_OBJECT는 DRIVER_OBJECT를 포인트하기 때문에, 필수적으로 후킹해야합니다.

 

3. ZwOpenKey() Hook

드라이버 서비스 키를 traversing 하여 드라이버 정보를 얻는것을 차단할 수 있습니다.

 

4. IoCreateFile() Hook

드라이버 파일을 접근하지 못하게 함으로써 드라이버에 대한 접근을 차단합니다. ZwCreateFile()은 IoCreateFile을 직접 호출하므로, IoCreateFile()을 후킹하면 탐지되기가 더 어려우며, 그만큼 효과를 발휘하게 됩니다.

 

5. ** 잡소리 **

보호된 프로세스로부터 드라이버 언로드 메시지가 왔을 때만 드라이버 언로드 핸들러를 설정합니다. 이는, 제 3자가 드라이버를 그냥 언로드하는걸 막을 수 있습니다. 

그리고 DeviceIoControl 메시지는 보호 대상 혹은 클라이언트의 메시지만 신뢰하는 것이 가장 좋습니다. (즉 말하자면 나머지는 쌩~ 까버리자는거죠.)

끝으로..

저는 위 방법들이 꼭 최선 해결책이라고 생각하진 않으며..

더 좋은 방어법이 있거나, 혹은 이를 우회하는 공격법이 존재할 수 있습니다.

그러므로, 자신만의 공격법이나 방어법을 만드는 것이 가장 좋은 방법이라고

생각이 됩니다.

Posted by skensita
Programming/Kernel / Driver2008. 12. 3. 11:05

특정 프로세스를 보호하는 데에는 여러 가지 방법이 존재합니다.

일반적으로 하는 SDT Hook 이외에 여러 방법들이 존재하는데, 지금부터 그 방법들을 알아봅시다.


1. ZwXxx() Hook

일반적으로 많이 쓰이는 방법이라 생각하는데, 대부분 이러한 함수들을 후킹함으로써 프로세스를 보호합니다.

ZwOpenProcess() : 프로세스 핸들을 얻어오지 못하게 해서 프로세스에 접근하지 못하게 합니다.

ZwTerminateProcess() : 프로세스 핸들을 얻지 못하게 했다 해도, 프로세스 핸들을 얻어오는 또다른 방법은

ObOpenObjectByPointer()/ObOpenObjectByName() 을 호출하는 방법이 있습니다. 따라서 프로세스를 종료하지 못하게 막으려면 이 함수를 후킹해야 합니다.

ZwCreateThread() : 프로세스에 새로운 쓰레드를 만드는 함수인데, 아래와 같은 방법으로 프로세스를 종료할 수 있으므로 이 함수를 후킹해야 프로세스를 종료/접근 에서 막을 수 있습니다.

(간단하게 알기 쉽게 CreateThread()를 이용해서 설명하겠습니다.)

DWORD ProcessTerminateRoutine(PVOID pContext)

{

       return TerminateProcess((HANDLE)0xFFFFFFFF, 0);

}

 void RemoteTerminateProcess(HANDLE hProcess)

{

         HANDLE hThread=CreateThread(hProcess, ..., ProcessTerminateRoutine, ...);

         CloseHandle(hThread);

}

ZwOpenThread() : ZwOpenProcess()를 후킹했다 해도, 쓰레드에 접근 가능하다면 쓰레드 핸들을 얻어 온 후, 쓰레드를 종료해 버리면 프로세스는 종료됩니다. 그리하여 이 함수 역시 후킹할 필요가 있습니다.

ZwTerminateThread() : ZwOpenThread()를 후킹하는 이유와 거의 일치합니다.

ZwQuerySystemInformation() : 이 함수로 시스템의 정보를 알 수 있습니다. 그리고 프로세스/스레드 정보 역시 이 함수를 통해 얻을 수 있습니다(리스트로). 그리하여 이 함수를 후킹하여 SystemInformationBuffer에서 보호하려는 프로세스와의 링크를 끊어 주면 프로세스를 은폐함으로써 프로세스를 보호할 수 있습니다.

이외에도 ZwReadVirtualMemory()/ZwWriteVirtualMemory()/ZwQueryInformationProcess() 등등을 후킹할 필요도 있습니다.


이 방법들의 단점은, 여러 개의 함수를 후킹해야 한다는 점과 SDT Hook일 경우, SDT를 Restore하거나 Relocate되었을 경우, 혹은 이미지 파일에서 주소를 구해올 경우 무력화될수 있다는 점입니다.


2. EPROCESS 구조체의 ActiveProcessLinks 링크를 끊는다.

EPROCESS 구조체에는 ActiveProcessLinks가 있는데, 이것을 Tracing하면 프로세스 리스트를 얻을 수 있습니다.

그러나, 이 Link를 끊는다면, 프로세스는 은폐하여 보이지 않게 되어 프로세스를 보호할 수 있습니다.

이 방법의 단점은, Handle Table을 Tracing 할 경우, 혹은 무작위로 OpenProcess() 시도시, 들킬 수 있다는 것이 단점입니다.


3. PspCidTable의 주소를 구해서 프로세스의 흔적을 지워버린다.

PspCidTable에는 프로세스의 핸들에 대한 정보가 담겨 있습니다.

그런데, PspCidTable에서 프로세스의 핸들을 지워 버린다면, ZwOpenProcess()로 프로세스를 열지라도, 핸들이 지워졌기 때문에 프로세스 핸들을 얻어 오는 데에 실패할 것입니다.

결국, 프로세스는 보호됩니다.(FU_RootKit이 사용했던 방법입니다.)

이 방법의 단점은 PspCidTable의 주소를 직접 구해 와야 한다는 것과, 이 방법으로도 프로세스를 완전히 흔적을 없앨 수 없습니다.


4. ObOpenObjectByName() Hook

프로세스 핸들을 얻어 오는 방법 중 하나는 ObOpenObjectByName()을 호출하여 이름으로 프로세스 핸들을 얻어 오는 것입니다.

이 함수를 후킹한다면, 특정 이름의 Object를 여는 것을 막아서 프로세스를 보호할 수 있을 것입니다.

단점은 핸들이 어떤 Type의 핸들인지를 알 수 있어야 한다는 것(ObjectType이 NULL일 때), ObjectName을 어떻게 구분지을 것인지를 알아야 하고, 좀더 막으려면 PsLookupXxx()등의 함수를 보조적으로 후킹해야 한다는 것이 있습니다.


5. ObOpenObjectByPointer() Hook

이 함수 역시 프로세스 핸들을 얻어 올 수 있습니다. 프로세스 핸들을 얻을 때, Parameter 중 Object에는 EPROCESS의 포인터가 들어가고, ObjectType에는 PsProcessType이 들어갑니다(NULL이 들어가기도 한다). 그리하여 후킹한 후 내부적으로 인자를 검사해서 프로세스를 여는 것일 경우 막으면 프로세스를 보호할 수 있습니다.

단점은 핸들이 어떤 Type의 핸들인지를 알 수 있어야 한다는 것(ObjectType이 NULL일 때)과 좀더 막으려면 PsLookupXxx()등의 함수를 보조적으로 후킹해야 한다는 것이 있습니다.(4번과 비슷)


6. KeAttachProcess()/KeStackAttachProcess() Hook

이 함수는 프로세스에 Attach할 수 있는 함수입니다. Attach하면.....일단 프로세스에 접근 할 수 있을 테니 종료도 가능할 것입니다.

그러므로 이 함수를 후킹할 필요가 있습니다.(GameGuard에서 후킹하는 함수라죠)

단점은 인자에 EPROCESS 구조체의 포인터가 필요하므로 PsLookupXxx()등의 함수도 후킹해야 합니다.


7. PspTerminateProcess() Hook

이 함수는 ntoskrnl.exe가 내부적으로 가지고 있는 하나의 내부함수로서, 프로세스를 종료할 때 쓰이나, ZwTerminateProcess()와의 차이점은 첫번째 인자에 ProcessHandle 대신 Process Object의 포인터(EPROCESS 포인터)가 들어간다는 것입니다.

이 함수는 Unexported된 함수이며, 주소를 직접 구해 오는 수밖에 없습니다. 이 함수를 후킹하면 프로세스를 종료에서 보호할 수 있습니다.

단점은 무엇보다도 PspTerminateProcess()의 주소를 직접 구해와야 하는데에 있을거라 생각됩니다. (필자가 시도중)


8. PspTerminateThreadByPointer() Hook

이 함수는 ntoskrnl::NtTerminateProcess()과 ntoskrnl::NtTerminateThread()가 내부적으로 호출하는 함수이고, PspTerminateProcess() 역시 이 함수를 내부적으로 사용합니다.

PspTerminateProcess()는 PsGetNextProcessThread()를 사용하여 프로세스의 Next Thread Object Pointer를 얻어 온 후(ETHREAD의 포인터), PspTerminateThreadByPointer()를 호출하여 프로세스가 소유하는 쓰레드를 하나하나씩 종료합니다.

이 함수 역시 Unexported된 함수이며, 주소를 직접 구해 와야 합니다. 이 함수는 PspTerminateProcess() 조차도 내부적으로 호출하는 함수이므로 후킹하면 프로세스/쓰레드를 종료에서 보호할 수 있습니다.

단점 역시 7번과 비슷합니다.


이상으로 몇몇 안되지만, 프로세스를 보호하는 여러 방법과 그 단점에 대해 알아보았습니다.

Posted by skensita