음......
흔히들 디바이스 드라이버를 이용한 커널 레벨 후킹을 할 때에는,
모든 동일한 프로세스가 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의 함수를 사용하게 하면 될 것입니다.