SSDT Hooking을 해보자!!!
 
SSDT(System Service Dispatch Table)은 Native API의 주소를 가지고 있기때문에

시스템콜에 의해 Native API를 호출할때 참고된다.

즉, SSDT를 후킹하게되면 특정 API를 호출할때 마다 내가 원하는 일을 중간에 끼어넣어

하고싶은 일을 할 수 있는것이다.

내부적인 이야기를 하자면

KiSystemService(System Service Dispatcher) 가 KiServiceTable(SSDT)을 참조한다.

KiServiceTable는 커널에 의해 export되지 않았기 때문에 내부구조를 알 수 없다.

But!!!
 
KeServiceDescriptorTable (ntoskrnl.exe)에 의해 export된 구조체 SDT(ServiceDescriptorTable)내에

KiServiceTable(SSDT)에 대한 정보가 들어있다.

그러므로 첫번째 해야할 일은 SSDT의 주소를 알아야 하는데 SSDT는 export 되어있지 않으므로

약간 돌아가서 KeServiceDescriptorTable을 통해 SSDT의 주소를 알아내야한다.


#pragma pack(1)
typedef struct ServiceDescriptorEntry      //KeServiceDescriptorTable을 위한 구조체
{
 unsigned int *ServiceTableBase;       //각서비스별 매핑함수 포인터들의 목록이 저장된 테이블의 포이터
 unsigned int *ServiceCounterTableBase;     
 unsigned int NumberOfServices;       //이 테이블이 가지고있는 서비스의 번호
 unsigned char *ParamTableBase;       //각 서비스별 파라미터의 크기를 나타내는 값을 가진 테이블의 포인터
}SSDT_ENTRY;
#pragma pack()

이렇게 KeServiceDescriptorTable을 위한 그릇을 만들고


//KeServiceDescriptorTable(ntoskrnl.exe)를 ntdll.dll에서 import해옴
__declspec(dllimport) SSDT_ENTRY KeServiceDescriptorTable;

이렇게 ntdll.dll에서 import 해와서 그릇에 담는다.

이제 NativeAPI를 후킹할 수 있게 되었다.

NativeAPI에 접근할 때 보통은
 
KeServiceDescriptorTable.ServiceBase[*(PULONG)((PUCHAR))func+1)]의 형식으로 접근을 하게 되는데

복잡하기 때문에 #define 문을 이용해서 다음과 같이 간단히 해준다.


//KeServiceDescriptorTable.ServiceBase[*(PULONG)((PUCHAR)_func+1)]는 각 서비스별 매핑 함수 포인터들의 목록이 저장된 테이블의 포인터
//즉 NativeAPI에 접근가능
#define SYSTEM_SERVICE(_func) KeServiceDescriptorTable.ServiceTableBase[*(PULONG)((PUCHAR)_func+1)]
#define SYSCALL_INDEX(_Function) *(PULONG)((PUCHAR)_Function+1)

즉 SYSTEM_SERVICE(_func)의 간단한 형태로 접근이 가능하다.

SYSTEM_SERVICE(_func)는 ntoskrnl.exe에서 제공하는 Zw*함수의 주소를 입력받고
 
그에 맞는 SSDT에서의 Nt*함수의 주소를 구하는데 사용된다.

SYSCALL_INDEX(_Function)은 Zw*함수의 주소를 입력받고 해당함수의 SSDT에서의 인덱스 번호를 구하는데 사용한다.

지금 부터는 후킹할 함수를 정해서 함수를 후킹하는 일만 남았는데 안타깝게도 그냥하면 되지 않는다.

왜냐하면 보통 합법적인(?) 프로그램은 SSDT영역의 메모리 영역을 변경할 일이 없기 때문에

SSDT를 보호하기 위하여 SSDT는 Read only속성을 갖는다. 즉 보통은 SSDT의 영역에 쓸수 없다.
 
하지만 이를 가능하게 하는 방법이 두가지가 있다.

첫번째 방법은 CR0 트릭이며, 두번째 방법은 MDL(Memory Descriptor List)를 이용하는 방식이다.
 
MDL방식은 메모리영역의 시작주소와 크기, 그 메모리 영역을 소유한 프로세스, 해당 메모리 영역의 플래그 정보가 포함되며

SSDT영역을 표현하는 MDL을 만들면 MDL을 통해 쓰기가 가능한 방식이다. 이 방식에 대해서는 자세한 것을 모르므로

여기까지 ^^;; (나중에 MDL이라는 것에 대해 자세히 다루어야겠다 ㅡ.,ㅡ)

다른 방법은 CR0트릭을 이용하는 방식이다.

CR0트릭은 CR0의 WP플레그를 조작하는 방식으로 쓰기를 할 수 있다.


//CR0의 WP플레그만 0으로 만들어 특정영역을 Write가능하게만드는 마스크
//2진수로 0x0FFFEFFFF는 11111111111111101111111111111111이고 WP는 17번째이므로
#define CR0_MASK 0x0FFFEFFFF

위 코드를 이용한다. 위 CR0_MASK를 보면 0x0FFFEFFFF 이라고 되어 있는데 주석에 따라

위를 2진수로 표현하면 11111111111111101111111111111111이고 WP는 17번째이므로 WP를 0으로 unset하여

쓰기가 가능하다.


void Set_WriteProtect()
{
 __asm
 {
  push eax;
  mov eax, cr0;
  or eax, CR0_MASK;
  mov cr0, eax;
  pop eax;
 }
}
void Unset_WriteProtect()
{
 __asm
 {
  push eax;
  mov eax, cr0;
  and eax, CR0_MASK;
  mov cr0, eax;
  pop eax;
 }
}

이렇게 간단한 어셈블리 코드로 가능하게 된다.

이렇게 SSDT의 장벽(?)을 뛰어넘고 ^^

후킹할 함수를 후킹하면 되는데

후킹할 함수는 ZwQuerySystemInformation 함수를 예로 들겠다.

ZwQuerySystemInformation은 ntdll.dll에서 export시키고 있기 때문에


//ntdll.dll에서 export되어있기 때문에 import해서 사용할 수 있음
NTSYSAPI        //NTSYSAPI ntdef.h에서DECLSPEC_IMPORT로 정의
NTSTATUS        //NTSTATUS는 리턴형
NTAPI         //NTAPI ntdef.h에서 STDCALL로 정의
ZwQuerySystemInformation(ULONG SystemInformationCLass,
                        PVOID SystemInformation,
                        ULONG SystemInformationLength,
                        PULONG ReturnLength);

위와 같은 코드로 import할 수 있다. 위와 같이 함수의 원형을 정의해 주고

//후킹후 동작할 함수
NTSTATUS NewZwQuerySystemInformation(
            IN ULONG SystemInformationClass,
            IN PVOID SystemInformation,
            IN ULONG SystemInformationLength,
            OUT PULONG ReturnLength);

후킹 후 동작할 함수도 역시 똑같이 정의해 준다.

후킹을 하면 다시 원래의 함수로 되돌려야하기때문에


typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION)(
            ULONG SystemInformationCLass,
                        PVOID SystemInformation,
                        ULONG SystemInformationLength,
                        PULONG ReturnLength
);
ZWQUERYSYSTEMINFORMATION        OldZwQuerySystemInformation;

위와 같이 원래 함수를 저장할 함수 포인터를 만들어야함.


OldZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)(SYSTEM_SERVICE(ZwQuerySystemInformation)); //원래주소 저장하기
 
 
 //CR0의 WP값을 0으로 만들어서  ReadOnly를 해제함.
 Unset_WriteProtect();
 
 //함수의 주소를 바꿔서 후킹시작
(ZWQUERYSYSTEMINFORMATION)InterlockedExchange((PLONG)&SYSTEM_SERVICE(ZwQuerySystemInformation),(LONG)NewZwQuerySystemInformation);

이렇게 InterlockedExchange()를 통해 주소를 바꿔서 후킹을 시작하고


NTSTATUS NewZwQuerySystemInformation(
            IN ULONG SystemInformationClass,
            IN PVOID SystemInformation,
            IN ULONG SystemInformationLength,
            OUT PULONG ReturnLength)
{
   NTSTATUS ntStatus;
   //원래의 함수를 실행시킴
   ntStatus = ((ZWQUERYSYSTEMINFORMATION)(OldZwQuerySystemInformation)) (
     SystemInformationClass,
     SystemInformation,
     SystemInformationLength,
     ReturnLength );
   if( NT_SUCCESS(ntStatus)) // 원래함수 수행시 에러가 없으면
   {
    //원하는 루틴
    DbgPrint("Success!!\n");
   }
   else
   {
    return STATUS_UNSUCCESSFUL;
   }
   return ntStatus;
}

이렇게 후킹후 하고싶은 일을 정의해 주면 된다.

그리고 후킹이 끝났으면


//후킹해제
 (ZWQUERYSYSTEMINFORMATION)InterlockedExchange((PLONG)&SYSTEM_SERVICE(ZwQuerySystemInformation), (ULONG)OldZwQuerySystemInformation);

다시 원래의 함수주소를 되돌려주면 된다.

이렇게 하면 SSDT 후킹이 끝난다.



(출처)웹서핑을 통해 여러 블로그를 참고했으며, 루트킷 책을 참고하기도 함.....
신고
Posted by kyh1026
LINUX/System & Security2008.06.25 23:19

시스템 래핑 커널모듈을 이용한 네트워크관리 발표자료

신고
Posted by kyh1026
Web/recommend2008.06.25 23:02

http://www.orkspace.net/

Security, Shellcode등 유용한 문서가 많은곳


 

신고
Posted by kyh1026