Programming/Kernel / Driver2008. 12. 5. 16:37

어떻게 하면 프로세스 리스트에서 숨겨진 프로세스만
골라낼 수 있을까?
이건 많은 이들이 궁금해 하는 주제인듯 하여,
아주 심플한 소스와 함께 한번 소개해 보겠다.
뭐, 별건 아니고 그냥 말그대로 숨겨진 프로세스를
효과적으로 잡아 내는것에 대한 소스이다.

1. 전역 변수 PEPROCESS SE_Proc; 를 만든다.

2. DriveEntry에 다음과 같은줄을 추가했다.
------------------------------------------------------
NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath ) {
...
...
SE_Proc = PsGetCurrentProcess();
...
...
}

3. ZwQuerySystemInformation()을 SSDT Hook하여
다음 함수에 연결하였다.
-----------------------------------------------------------
NTSTATUS NTAPI NewZwQuerySystemInformation(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength
)
{
NTSTATUS rc;
CHAR Attack_Process_Name[PROCNAMELEN];
PEPROCESS eproc,currproc;
PLIST_ENTRY start_plist,plist_hTable = NULL;
DWORD *d_pid;
extern PsOffset_HandleTable,PsOffset_HandleList,PsOffset_hPID;
extern SE_Proc;
char *nameptr;
CHAR Hided_Process_Name[PROCNAMELEN];

InterlockedIncrement( &G_nLockUseCounter );
GetProcessName( Attack_Process_Name );

eproc = SE_Proc;
plist_hTable = (PLIST_ENTRY)((*(DWORD*)((DWORD)eproc +
PsOffset_HandleTable)) + PsOffset_HandleList);
start_plist = plist_hTable;

rc = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) (
SystemInformationClass,
SystemInformation,
SystemInformationLength,
ReturnLength );

if( NT_SUCCESS( rc ) )
{

if(5 == SystemInformationClass)
{
struct _SYSTEM_PROCESSES *curr = (struct _SYSTEM_PROCESSES *)SystemInformation;
struct _SYSTEM_PROCESSES *prev = NULL;

while(curr)
{
ANSI_STRING process_name;
RtlUnicodeStringToAnsiString( &process_name, &(curr->ProcessName), TRUE);
if( (255 > process_name.Length) && (0 < process_name.Length) )
{
d_pid = (DWORD*)(((DWORD)plist_hTable + PsOffset_hPID)
- PsOffset_HandleList);
if(curr->ProcessId != *d_pid)
{
DbgPrint("---Hided process---");
nameptr = (PCHAR)eproc + gProcessNameOffset;
strncpy(Hided_Process_Name, nameptr, NT_PROCNAMELEN);
Hided_Process_Name[NT_PROCNAMELEN] = 0;
DbgPrint("ProcessName:%s",Hided_Process_Name);
DbgPrint("ProcessId:0x%X",*d_pid);
DbgPrint("EPROCESS:0x%X",eproc);
DbgPrint("-------------------");
}

if(0 == strncmp( process_name.Buffer, MYPROCESS, NT_PROCNAMELEN))
{
DbgPrint("[Alarm] ProcessScan Detected\n");
DbgPrint("Called by %s\n",Attack_Process_Name);

if(prev)
{
if(curr->NextEntryDelta)
{
prev->NextEntryDelta += curr->NextEntryDelta;
}
else
{
prev->NextEntryDelta = 0;
}
}
else
{
if(curr->NextEntryDelta)
{
(char *)SystemInformation += curr->NextEntryDelta;
}
else
{
SystemInformation = NULL;
}
}
}
}
RtlFreeAnsiString(&process_name);
prev = curr;
if(curr->NextEntryDelta) ((char *)curr += curr->NextEntryDelta);
else
{
if(start_plist != plist_hTable)
{
do
{
d_pid = (DWORD*)(((DWORD)plist_hTable + PsOffset_hPID)
- PsOffset_HandleList);
if(*d_pid == 0)
{
plist_hTable = plist_hTable->Flink;
break;
}
PsLookupProcessByProcessId(*d_pid,&eproc);
nameptr = (PCHAR)eproc + gProcessNameOffset;
strncpy(Hided_Process_Name, nameptr, NT_PROCNAMELEN);
Hided_Process_Name[NT_PROCNAMELEN] = 0;
DbgPrint("---Hided process---");
DbgPrint("ProcessName:%s",Hided_Process_Name);
DbgPrint("ProcessId:0x%X",*d_pid);
DbgPrint("EPROCESS:0x%X",eproc);
DbgPrint("-------------------");
plist_hTable = plist_hTable->Flink;

}while(start_plist != plist_hTable);
}
curr = NULL;
}
}
}
}
InterlockedDecrement( &G_nLockUseCounter );
return rc;
}
----------------------------------------------------------

뭐, 이게 끝이다.
사용한 방법을 간단히 설명하자면,
ZwQuerySystemInformation()으로
프로세스 리스트를 구해오고자 할떄,
해당 프로세스 번째의 Pid와
HandleTable을 Tracing하면서
해당 번쨰의 Pid가 같은지 비교한다.
같지 않으면 거기엔 Process가 Hide가 된것이다.
ZwQuerySystemInformation()으로 구해진
프로세스 리스트가 끝났을떄,
아직 HandleTable의 Tracing이 아직 끝나지
않은 상태라면, 그 뒤로 숨겨진 프로세스들이 있다고 볼 수 있다.
그럼으로 그들을 출력해준다.
그런데, 마지막 프로세스는 Process Scan을 행한 프로세스
자신인데, PID는 0이다. 그럼으로 PID가 0일때는
출력하지 않고 멈춘다.

출력양식은 다음과 같다.
-------------------------------------------
---Hided process---
ProcessName:SomethingHided.exe
ProcessId:0xB47
EPROCESS:0x80485064
-------------------
-------------------------------------------

이 코드에서는 ZwQuerySystemInformation()내에서
처리하는 방식으로 했지만,
단순히 숨겨져 있는 프로세스 목록만 알고 싶다면,
HandleTable과 EPROCESS의 pListEntry를
이용하는 것이 좋을 것이다.

이 방법에서 조금 더 나아가면,
숨겨진 프로세스를 골라낼 수 있을 뿐만 아니라,
curr->NextEntry에 struct _SYSTEM_PROCESSES
형태로 데이터를 집어넣고 연결하여 프로세스 리스트에
반영되게 할 수도 있다. :)

-본자료는 듀얼님이 작성하신것으로 스크랩글임을 밝혀둡니다
Posted by skensita
Programming/Kernel / Driver2008. 12. 3. 11:38

음......

흔히들 디바이스 드라이버를 이용한 커널 레벨 후킹을 할 때에는,

모든 동일한 프로세스가 Kernel32.dll의 API를 호출할 때,

API가 실질적으로 호출하는 내부함수의 주소를

NTOSKRNL의 KeServiceDescriptorTable에서 참조하는 것을 이용하여

KeServiceDescriptorTable.ServiceTableBase[함수 인덱스 번호]

를 수정하여 자신의 함수주소로 바꾸어 놓습니다.


예를 들어, ZwOpenProcess()를 후킹한다 가정하면

그 코드는 아래와 같습니다.


중요한 부분만 올려놓았습니다.


#include <ntddk.h>


#define PROTECT_PROCESS_ID                   456

typedef struct _SERVICE_DESCRIPTOR_TABLE {
         unsigned int *ServiceTableBase;
         unsigned int *ServiceCounterTableBase;
         unsigned int NumberOfServices;
         unsigned char *ParamTableBase;
} SERVICE_DESCRIPTOR_TABLE;


//import 선언

NTKERNELAPI

SERVICE_DESCRIPTOR_TABLE

KeServiceDescriptorTable;


NTKERNELAPI

HANDLE

NTAPI

PsGetProcessId(

         IN PEPROCESS Process);


//함수에 해당하는 주소를 KeServiceDescriptorTable에서 쉽게 알수 있게 해주는 매크로

#define SYSTEMSERVICE(_function)  KeServiceDescriptorTable.ServiceTableBase[*(PULONG)((PUCHAR)_function+1)]

typedef NTSTATUS (NTAPI *ZWOPENPROCESS)(

        OUT PHANDLE ProcessHandle,

        IN ACCESS_MASK DesiredAccess,

        IN POBJECT_ATTRIBUTES ObjectAttributes,

        IN PCLIENT_ID ClientId OPTIONAL);


ZWOPENPROCESS PrevZwOpenProcess; //원래의 함수주소를 담기위한 것


NTSTATUS ZwOpenProcessHandler(

        OUT PHANDLE ProcessHandle,

        IN ACCESS_MASK DesiredAccess,

        IN POBJECT_ATTRIBUTES ObjectAttributes,

        IN PCLIENT_ID ClientId OPTIONAL)

{

         NTSTATUS Status=PrevZwOpenProcess(ProcessHandle, DesiredAccess,

               ObjectAttributes, ClientId);   //원래 함수를 호출

         if(NT_SUCCESS(Status))   //리턴값이 성공을 나타내면 Process Object를 얻음

         {//Process Object=EPROCESS 구조체

                  PEPROCESS Process;   //Process Object

                  NTSTATUS ObjectStatus=ObReferenceObjectByHandle(

                           *ProcessHandle, PROCESS_ALL_ACCESS, NULL, KernelMode,

                           &Process, NULL);  //프로세스 핸들로 EPROCESS의 포인터를 얻음

                  if(NT_SUCCESS(ObjectStatus))  //성공적으로 EPROCESS를 얻었다면

                  {  //EPROCESS의 구조체 내부의 UniqueProcessId를 조사해서

                      //그 프로세스가 보호하려는 프로세스인지 알아낸다.

                      //이러한 일을 해주는 함수가 PsGetProcessId이다.

                      //이 함수는 ddk 헤더파일에 선언되어있지 않으므로 직접 선언해주자.

                      //EPROCESS 구조체 역시 ddk 헤더파일에 선언되어있지 않다.

                      //직접 인터넷을 뒤지면 나올지도 모르나 OS마다 구조체가 다를 수 있으므로

                      //PsGetProcessId()를 호출하는 것이 훨~씬 안정적이다.

                      //PsGetProcessId()를 사용할 수 없다면 직접 프로세스아이디 오프셋을

                      //구해 와야 한다.

                      //참고 : *(ULONG *)((ULONG *)Process+프로세스아이디 오프셋)=

                      //Process->UniqueProcessId

                           if(PsGetProcessId(Process)==(HANDLE)PROTECT_PROCESS_ID)

                           {

                                    //보호하려는 프로세스가 맞으면

                                    //프로세스 핸들을 닫은 후 NULL로 세팅해준다.

                                    ZwClose(*ProcessHandle);

                                    *ProcessHandle=NULL;

                                    //엑세스 거부를 return.

                                    //STATUS_ACCESS_DENIED 값은 LsaNtstatusToWinError()로

                             //ERROR_ACCESS_DENIED로 바뀌어서 SetLastError()의 인자가 된다.

                                    return STATUS_ACCESS_DENIED;

                           }

                  }

         }

         return Status;

}


void SetupHookZwOpenProcess()

{

         //원래의 함수 주소를 저장

         PrevZwOpenProcess=(ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess));

         //Cr0 Register 연산으로 보호를 해제시킨다.

         __asm
         {
                  cli
                  mov eax, cr0
                  and eax, not 10000h
                  mov cr0, eax
         }

         //SDT 수정부분

         (ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess))

                  =ZwOpenProcessHandler;

         //Cr0 Register 연산으로 원래대로 보호시킨다.

         __asm
         {
                  mov eax, cr0
                  or eax, 10000h
                  mov cr0, eax
                  sti
         }

}


지금부터 이러한 방법에 대한 장/단점에 대해 알아봅시다.

장점은 쉽다는 것입니다.

그 때문에 루트킷에서도 널리 쓰이고 있으며, 백신 또한 각각의 프로세스나 파일의 보호/감시를 위해서 쓰고 있습니다.

하지만, 이렇게 쉬운만큼 역시나 단점도 존재합니다.

여러가지 방법으로 인해서 무력화 될 수 있다는 것이죠.

1. \Device\PhysicalMemory에 엑세스하여 ntoskrnl의 이미지에서 주소를 구해와 SDT를 원래대로 돌려놓거나

2. 서비스 테이블을 덮어쓰는 것이므로 커널모드 드라이버를 로드하여 ntoskrnl의 이미지에서 주소를 구해와서 원래 주소로 덮어쓰기한다.

3. KeServiceDescriptorTable.ServiceTableBase를 Relocate시킨다.

이 외에도 몇몇가지 방법이 더 있고 대처방법 역시 있습니다.


우선 첫번째 방법에 대한 대처방법은

ZwMapViewOfSection()/ZwOpenSection()을 후킹하여 \Device\PhysicalMemory일 경우 막아버리면 됩니다.

혹은, Admin의 권한이 없을 때에는, 1번 방법은 통하지 않습니다.

두번째 방법에 대한 대처방법은

ZwLoadDriver()를 후킹하거나

3번 방법처럼 하면 됩니다.

세번째 방법은 실제 디스크 이미지로부터 서비스 함수들의 주소를 각각 구해 배열로 NewKiServiceTable을 만든 후,

이것을 DeviceIoControl로 드라이버에게 넘겨준 후, 드라이버로 하여금 NewKiServiceTable의 함수를 사용하게 하면 될 것입니다.

Posted by skensita