Programming/Kernel / Driver2008. 12. 3. 11:00
이번에는 실질적으로 드라이버에 훅을 걸어보겠습니다.
 
훅의 원리는 의외로 간단한데 훅을 걸려고 하는 드라이버 포인터를 얻어와서 훅 드라이버에
연결시키기만 하면 됩니다.
 
일단 훅을 걸려고하는 드라이버 포인터를 얻어오겠습니다.
 
 
NTSTATUS ObOpenObjectByName(IN POBJECT_ATTRIBUTES ObjectAttributes, IN ULONG  OPTIONAL,
    IN ULONG AccessMode, IN ULONG OPTIONAL2, IN ULONG DesiredAccess OPTIONAL, IN OUT PVOID ParseContext OPTIONAL,
    OUT PHANDLE Handle);
 
PDRIVER_OBJECT SearchDriverObject(PUNICODE_STRING pUni)
{
    NTSTATUS st;
    HANDLE Handle;
    UNICODE_STRING Uni;
    OBJECT_ATTRIBUTES ObjectAttributes;
    PDRIVER_OBJECT Object;
 
    InitializeObjectAttributes( &ObjectAttributes, pUni, OBJ_CASE_INSENSITIVE, NULL, NULL );
 
    st = ObOpenObjectByName( &ObjectAttributes, 0L, 0L, 0L, 0L, 0L, &Handle );
 
    if( st != STATUS_SUCCESS )
        return (PDRIVER_OBJECT)0;
 
    st = ObReferenceObjectByHandle( Handle, 0x80000000, NULL, 0, &Object, NULL );
 
    if( st != STATUS_SUCCESS )
    {
        ZwClose( Handle );
        return (PDRIVER_OBJECT)0;
    }
 
    ZwClose( Handle );
    ObDereferenceObject( Object );
    return Object;
}
 
 
위의 SearchDriverObject 함수는 '디바이스 구조와 원리'에서 발췌했습니다.
 
 
PDRIVER_OBJECT pDriver;
 
RtlInitUnicodeString(&Uni, L"\\Driver\\Serial");
pDriver = SearchDriverObject(&Uni);
 
이렇게 입력하면 시리얼 드라이버 포인터를 얻어오게 됩니다.
 
 
이제 원래 드라이버 포인터를 전역변수로 담아 놓습니다. 드라이버 포인터를 담아 놓는 이유는
후킹드라이버를 종료시켰을때를 위해서 입니다.
 
BackupReadHandler = pDriver->MajorFunction[IRP_MJ_READ];
BackupWriteHandler = pDriver->MajorFunction[IRP_MJ_WRITE];
 
그리고 새로운 함수포인터를 집어 넣습니다.
 
pDriver->MajorFunction[IRP_MJ_READ]  = ReadHandler;
pDriver->MajorFunction[IRP_MJ_WRITE]  = WriteHandler;
 
이 새로운 함수의 작성은 해당 드라이버의 DDK의 샘플을 참고로 작성하시면 됩니다. 물론 그대로 가져와 쓰셔도 됩니다.
 
PDRIVER_OBJECT pDriver;
PDEVICE_OBJECT BackupReadHandler;
PDEVICE_OBJECT BackupWriteHandler;
 
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT  DriverObject, IN PUNICODE_STRING RegistryPath)
{
    PDEVICE_OBJECT devobject = 0;
    UNICODE_STRING devlink,devname;
 
    DriverSearch();
   
    DriverObject->MajorFunction[IRP_MJ_READ]  = ReadHandler;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]=DrvClose;
    DriverObject->DriverUnload = DrvUnload;
 
    RtlInitUnicodeString(&devname,devicename);
    RtlInitUnicodeString(&devlink,devicelink);
 
    IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, FILE_DEVICE_UNKNOWN, 0, FALSE, &devobject);
    IoCreateSymbolicLink(&devlink,&devname);
 
    return 0;
}
 
void DriverSearch()
{
    RtlInitUnicodeString(&Uni, L"\\Driver\\Serial");
    pDriver = SearchDriverObject(&Uni);
 
    BackupReadHandler = pDriver->MajorFunction[IRP_MJ_READ];
    BackupWriteHandler = pDriver->MajorFunction[IRP_MJ_WRITE];
}
 
void DrvUnload(IN PDRIVER_OBJECT driver)
{
    UNICODE_STRING devlink;
 
    RtlInitUnicodeString(&devlink,devicelink);
 
    pDriver->MajorFunction[IRP_MJ_READ] = BackupReadHandler
    pDriver->MajorFunction[IRP_MJ_WRITE] = BackupWriteHandler
  
    IoDeleteSymbolicLink(&devlink);
    IoDeleteDevice(driver->DeviceObject);
}
 
이렇게 3부로 나뉘어진 디바이스 드라이버 훅에 모든 설명이 끝났습니다.
좀 더 많은 부분을 설명하고 싶었는데 아쉽네요. 내공이 부족한 부분도 있고... 자료도 그렇고
 
특히 키보드나 마우스같은 입력장치 드라이버의 경우 IRP_MJ_WRITE 가 존재하지 않기때문에
버퍼에 직접 접근해서 값을 넣어줘야 합니다.
 
ntdd8042.h
 
IOCTL_INTERNAL_I8042_HOOK_KEYBOARD
IOCTL_INTERNAL_I8042_KEYBOARD_WRITE_BUFFER
 
사실 처음부터 그런것을 만들려고 했으면 필터드라이버 쪽이 더 편하지 않았나 싶네요.
WDM쪽은 너무 생소해서 해당분야에 종사하시는 분이 아니면 공부하는데도 한계가 있구요.
저도 시간 날때마다 조금씩 공부하는데 상당히 난해합니다....
 
이번 강좌를 통해서 WDM이 좀 더 쉽게 다가왔으면 하는 바램입니다.
 
 
참고 서적
 
EXPLOITING SOFTWARE : How to break code
MICROSOFT WINDOWS DRIVER MODEL
API로 배우는 Windows 구조와 원리
윈도우즈 드라이버 모델 WDM
디바이스 드라이버 구조와 원리 그리고 제작 노하우

출처 : 데브피아
Posted by skensita
Programming/Kernel / Driver2008. 12. 3. 10:56

이번에는 직접 후킹드라이버의 기본 프레임을 작성해 보겠습니다.
 

여러분이 WDM에 어느정도 기본 지식이 있다는 가정하에 진행되겠습니다.


아래는 일반적인 드라이버의 엔트리 부분입니다.
 

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
    UINT i;
 
    UNREFERENCED_PARAMETER (RegistryPath);
 
    for (i = 0 ; i <= IRP_MJ_MAXIMUM_FUNCTION ; i++)
        DriverObject->MajorFunction[i] = OnStubDispatch;
 
   
    DriverObject->MajorFunction [IRP_MJ_CREATE] = CreateHandler;
    DriverObject->MajorFunction [IRP_MJ_CLOSE] = CloseHandler;
    DriverObject->MajorFunction [IRP_MJ_PNP] = PNPHandler
    DriverObject->MajorFunction [IRP_MJ_POWER] = PowerHandler;
    DriverObject->MajorFunction [IRP_MJ_INTERNAL_DEVICE_CONTROL] = ControlHandler;
   
 
    DriverObject->DriverUnload = OnUnload;
    DriverObject->DriverExtension->AddDevice = AddDevice;
 
    return STATUS_SUCCESS;
}
 
하지만 우리가 시도하려는 후킹드라이버는 드라이버 엔트리를 진입하기는 하지만 정상적인 시작이 아니기 때문에
대부분의 함수 포인터를 채울 필요가 없습니다.
 
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DrvClose;
DriverObject->DriverUnload = DrvUnload;
 
를 남기고 모두 지워 버리세요. SCM을 이용해서 드라이버를 생성시키고 시작하는 과정에는
IRP_MJ_CREATE, DriverObject->DriverExtension->AddDevice 함수포인터에 접근하지 않습니다.

그리고 드라이버엔트리에 직접 IoCreateDevice 으로 디바이스를 생성시킵니다.
 
 
한가지 재미있는 점은 드라이버엔트리는 DSP = 0 , Ring 0 의 권한을 가지고 있습니다.
때에 따라서 악명높은 CIH바이러스와 같이 바이오스를 지워버릴수도 직접 제어 할 수도 있습니다.
 
이야기를 다시 돌려서 생성시킨 장치는 IoCreateSymbolicLink로 등록 시킵니다.
 
 
 
이제 드라이버를 다시 작성하면
 
 
 
const WCHAR devicename[]=L"\\Device\\HookDriver";
const WCHAR devicelink[]=L"\\DosDevices\\HOOKDRIVER";
 
 
NTSTATUS DriverEntry(IN PDRIVER_OBJECT  DriverObject, IN PUNICODE_STRING RegistryPath)
{
    PDEVICE_OBJECT devobject = 0;
    UNICODE_STRING devlink,devname;
 
    RtlInitUnicodeString(&devname,devicename);
    RtlInitUnicodeString(&devlink,devicelink);
 
    IoCreateDevice(DriverObject, 256, &devname, FILE_DEVICE_UNKNOWN, 0, FALSE, &devobject);
    IoCreateSymbolicLink(&devlink,&devname);
 
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = DrvClose;
    DriverObject->DriverUnload = DrvUnload;
 
    return STATUS_SUCCESS;
}
 
void DrvUnload(IN PDRIVER_OBJECT driver)
{
    UNICODE_STRING devlink;
 
    RtlInitUnicodeString(&devlink,devicelink);
 
    IoDeleteSymbolicLink(&devlink);
    IoDeleteDevice(driver->DeviceObject);
}
 
NTSTATUS DrvClose(IN PDEVICE_OBJECT device,IN PIRP Irp)
{
    Irp->IoStatus.Information=0;
    Irp->IoStatus.Status=0;
    IoCompleteRequest(Irp,IO_NO_INCREMENT);
 
    return 0;
}
 
과 같이 작성이 되겠습니다. 이제 후킹드라이버의 기본 프레임이 완성되었습니다.
 
드라이버 작성내용은 되도록 DDK관련 문서를 참고하셔서 보시는 편이 도움이 되십겁니다.

출처 : 데브피아

Posted by skensita
Programming/Kernel / Driver2008. 12. 3. 10:53

후킹은 프로그램 기술중에서 가장 멋진기술입니다.

하지만 일반적인 후킹(SetWindowsHookEx)은 윈도우가 제공하는 범위내에서 가능한것들이고 때때로 그것보다
아래 단계에서 후킹을 하고싶을때가 있는데 특히 Filemon이나 Packet sniffer 같은 멋진 프로그램들은
SetWindowsHookEx 가지고는 구현 불가능합니다. 여러분이 후킹에 목말라 있다면

좀 더 이 글을 읽어서 자신이 원하는 해답을 얻기를 바랍니다.


필요한 것


Visual Studio
Driver Development Kit


디바이스장치에 접근하는 방법은 몇가지가 있습니다. 필터 드라이버를 작성하는것과 드라이버를 후킹하는것인데
여기에서는 필터드라이버에 관한 내용은 다루지 않습니다. 필터드라이버는 현재 WDM책마다 최소한 한번 이상은
다루기 때문에 필터드라이버를 작성하시려면 책을 추천해 드립니다.

저는 윈도우 API중에서 SCM(Service control manager)을 이용해서 드라이버를 후킹하고 지우는 방법을 골랐는데
이유는 간단합니다. 재부팅이 무척 싫고 작업시간도 배로 늘어나기 때문에, 필터드라이버는 설치하면 재부팅을 해야합니다.


아래는 SCM을 이용해 드라이버를 등록하고 제거 하는방법이다.

BOOL CDeviceControlDlg::CreatDevice(char* sc_name, char* sc_path)
{
        SC_HANDLE hSCMan = NULL;
        SC_HANDLE hService = NULL;

        if((hSCMan = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS)) == 0)
        {
                TRACE("서비스를 열수 없습니다.");
                return FALSE;
        }

        if((hService = CreateServiceA(hSCMan, sc_name, sc_name, SC_MANAGER_ALL_ACCESS, SERVICE_KERNEL_DRIVER,
                SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, sc_path, 0, 0, 0, 0, 0)) == 0)
        {
                if(GetLastError() == ERROR_SERVICE_EXISTS)
                        TRACE("이미 설치되어있습니다.\n");
                else if(GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE)
                        TRACE("서비스가 제거되도록 설정되어 있어 생성불가.\n");

                CloseServiceHandle(hSCMan);
                return FALSE;
        }

        StartService(hService, 0, 0);
        TRACE("설치완료\n");
        CloseServiceHandle(hService);
        CloseServiceHandle(hSCMan);
        return TRUE;
}

BOOL CDeviceControlDlg::UnloadDevice(char* sc_name)
{
        DWORD dwError = ERROR_SUCCESS;
        SC_HANDLE hSCMan = NULL;
        SC_HANDLE hService = NULL;
        SERVICE_STATUS serviceStatus;

        if((hSCMan = OpenSCManager(0, 0, SC_MANAGER_ALL_ACCESS)) == 0)
        {
                TRACE("서비스를 열수 없습니다.");
                return FALSE;
        }

        if((hService = OpenServiceA(hSCMan, sc_name, SERVICE_ALL_ACCESS)) == 0)
        {
                TRACE("서비스가 설치되지 않았습니다.\n");
                CloseServiceHandle(hSCMan);
                return FALSE;
        }

        ControlService(hService,SERVICE_CONTROL_STOP,&serviceStatus);

        if(DeleteService(hService) == 0)
        {
                if(GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE)
                        TRACE("서비스가 이미 제거 설정되었습니다.\n");
        }
        else
            TRACE("서비스가 제거되었습니다. 프로그램을 종료해야합니다.\n");

        CloseServiceHandle(hSCMan);
        return TRUE;
}


SCM을 이용해서 디바이스를 등록하는 과정은 약간의 제약이 있는데 한개의 프로세스 스레드에서
서비스를 제거시 바로 제거되는것이 아니라 서비스 제거 마킹만 할뿐 실질적으로 프로세스를 종료해야
서비스가 제대로 제거됩니다. 제거 마킹상태에서는 다시 서비스가 생성되지 않습니다.

또 주목해야할 부분은

CreateServiceA(hSCMan, sc_name, sc_name, SC_MANAGER_ALL_ACCESS, SERVICE_KERNEL_DRIVER,
                SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, sc_path, 0, 0, 0, 0, 0))

이 부분에서 5 번째와 6번째 파라메터 부분

5번째는 커널드라이버이기 때문에 당연히 SERVICE_KERNEL_DRIVER
라고 넣었겠지만 6번째는 어째서 SERVICE_AUTO_START를 사용하지 않고

SERVICE_DEMAND_START 넣은후에 StartService(hService, 0, 0); 서비스 시작을 해야만 하였는가...

그 부분에 대해서는 아직 내공이 부족해서 답을 얻지 못했는데
다만 SERVICE_AUTO_START 를 했을경우 드라이버 엔트리포인트에 진입하지 못한다는것을 확인 했기 때문에
이 글을 읽는 분들이라면 이 같은 실수는 하지 않길 바랍니다.


사용자 삽입 이미지


이제 윈도우 DDK에서 제공되는 Device Tree라는 툴로 후킹 드라이버가 성공적으로 올라간것을 확인할수 있습니다.

출처 : 데브피아

Posted by skensita
Programming/Kernel / Driver2008. 12. 3. 10:10

※ 참고 : 이 글은 "User32!SetWindowsHookExA/W 를 후킹하지 않는다" 와 "오직 커널 레벨에서만 막는다(?)"는 가정하에 작성되었습니다.


보통 메시지 훅을 할 때는, User32.dll에 Export 되어있는 SetWindowsHookExA/SetWindowsHookExW 라는 API를 이용합니다.

[물론 아닌 경우도 있겠지만, 적어도 저는 이걸 이용합니다!]


이 API를 이용해서 DLL을 Injection을 할 수도 있습니다.

아래와 같은 경우죠.


DLL InjectTarget.dll의 Export된 함수인 MsgProc를 HOOKPROC로 사용하는 경우

INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR lpCmdLine, INT nCmdShow)

{

      PVOID hHook;

      PVOID AddressOfFunction=(PVOID)GetProcAddress(LoadLibrary("injectTarget.dll"), "MsgProc");

      hHook=(PVOID)SetWindowsHookEx(......, AddressOfFunction, ......);

      ......중략......

}


그런데, 이상한 것은, SetWindowsHookExA/SetWindowsHookExW는 다른 함수와는 달리, 프로세스에 별다른 접근을 하지 않고

바로 SYSENTER를 합니다.

그리하여, 프로세스를 숨겨버린다 해도 SetWindowsHookEx()를 막기는 힘듭니다.

심지어 자기자신을 숨기고, KeAttachProcess/KeStackAttachProcess마저 후킹하는 nProtect마저도

SetWindowsHookEx()를 이용한 DLL Injection은 막지 못하였습니다. [지금은 바뀌었는지 모르겠지만]

필자는 프로세스를 보호하기 위해 커널 모드에서 PsLookupXxx 루틴과 KeXxxAttachProcess 루틴, 심지어는 ObXxx 루틴까지 후킹을 했으나

윈도우 프로세스의 경우 SetWindowsHookEx를 막아낼 수 없었습니다.


그 이유를 지금부터 알아보기 위해, SetWindowsHookExA/W를 차차 살펴보기로 합시다.


USER32!SetWindowsHookExA를 보면......

사용자 삽입 이미지


USER32!SetWindowsHookExW를 보면......

사용자 삽입 이미지


위와 같이, USER32.DLL의 SetWindowsHookA/W는 내부적으로 USER32.DLL::SetWindowsHookExAW를 호출합니다.


이제는 SetWindowsHookExAW를 보면......

사용자 삽입 이미지


SetWindowsHookAW는 USER32!_SetWindowsHookEx를 호출하는군요.


이어서 _SetWindowsHookEx를 봅시다.

사용자 삽입 이미지


_SetWindowsHookEx는 결과적으로 USER32!NtUserSetWindowsHookEx를 호출합니다.

아마도 USER32!NtUserSetWindowsHookEx는 NTDLL!NtXxx 루틴과 비슷한 일을 하는것으로 예상됩나다.


드디어, NtUserSetWindowsHookEx를 보는군요.

사용자 삽입 이미지


자 이것을 전부 정리(?) 해 보면,

SetWindowsHookExA/SetWindowsHookExW->SetWindowsHookExAW->_SetWindowsHookEx->NtUserSetWindowsHookEx 순이 되는군요.

그렇다면, Win32K!NtUserSetWindowsHookEx를 확인해 봐야겠군요.


자, Win32K!NtUserSetWindowsHookEx를 봅시다.

사용자 삽입 이미지


자, 보이십니까???

Win32K!NtUserSetWindowsHookEx는 내부적으로 Internal 함수인 Win32k!zzzSetWindowsHookEx를 호출합니다.


Win32k!zzzSetWindowsHookEx를 한번 보면......

사용자 삽입 이미지

여기서는 별다른 특별한 것은 보이지 않았습니다.


다음 장을 봅시다.

사용자 삽입 이미지

으으으음. AddHmodDependency()라...... 웬지 이 함수를 보면 뭔가 알 수 있을 것 같기도 한데.......

계속해서 봅시다.

사용자 삽입 이미지

으음.....zzzJournalAttach같은 내부함수도 보이는군요.

그나저나, 이 함수는 어디서 끝나는 걸까요.....하악하악

사용자 삽입 이미지

드디어 끝났습니다! [zzzSetWindowsHookEx가 이리 길줄이야......OTL]

으음....수많은 내부함수들이 보이는군요.

그나저나, 코드가 상당히 길어 분석이 좀 까다롭게 되었군요.


어쩔 수 없이, W2K 소스 참조.

PHOOK zzzSetWindowsHookEx(
    HANDLE hmod,
    PUNICODE_STRING pstrLib,
    PTHREADINFO ptiThread,
    int nFilterType,
    PROC pfnFilterProc,
    DWORD dwFlags)
{
    ACCESS_MASK amDesired;
    PHOOK       phkNew;
    TL          tlphkNew;
    PHOOK       *pphkStart;
    PTHREADINFO ptiCurrent;

    /*
     * Check to see if filter type is valid.
     */
    if ((nFilterType < WH_MIN) || (nFilterType > WH_MAX)) {
        RIPERR0(ERROR_INVALID_HOOK_FILTER, RIP_VERBOSE, "");
        return NULL;
    }

    /*
     * Check to see if filter proc is valid.
     */
    if (pfnFilterProc == NULL) {
        RIPERR0(ERROR_INVALID_FILTER_PROC, RIP_VERBOSE, "");
        return NULL;
    }

    ptiCurrent = PtiCurrent();

    if (ptiThread == NULL) {
        /*
         * Is the app trying to set a global hook without a library?
         * If so return an error.
         */
         if (hmod == NULL) {
             RIPERR0(ERROR_HOOK_NEEDS_HMOD, RIP_VERBOSE, "");
             return NULL;
         }
    } else {
        /*
         * Is the app trying to set a local hook that is global-only?
         * If so return an error.
         */
        if (!(abHookFlags[nFilterType + 1] & HKF_TASK)) {
            RIPERR0(ERROR_GLOBAL_ONLY_HOOK, RIP_VERBOSE, "");
            return NULL;
        }

        /*
         * Can't hook outside our own desktop.
         */
        if (ptiThread->rpdesk != ptiCurrent->rpdesk) {
            RIPERR0(ERROR_ACCESS_DENIED,
                   RIP_WARNING,
                   "Access denied to desktop in zzzSetWindowsHookEx - can't hook other desktops");

            return NULL;
        }

        if (ptiCurrent->ppi != ptiThread->ppi) {
            /*
             * Is the app trying to set hook in another process without a library?
             * If so return an error.
             */
            if (hmod == NULL) {
                RIPERR0(ERROR_HOOK_NEEDS_HMOD, RIP_VERBOSE, "");
                return NULL;
            }

            /*
             * Is the app hooking another user without access?
             * If so return an error. Note that this check is done
             * for global hooks every time the hook is called.
             */
            if ((!RtlEqualLuid(&ptiThread->ppi->luidSession,
                               &ptiCurrent->ppi->luidSession)) &&
                        !(ptiThread->TIF_flags & TIF_ALLOWOTHERACCOUNTHOOK)) {

                RIPERR0(ERROR_ACCESS_DENIED,
                        RIP_WARNING,
                        "Access denied to other user in zzzSetWindowsHookEx");

                return NULL;
            }

            if ((ptiThread->TIF_flags & (TIF_CSRSSTHREAD | TIF_SYSTEMTHREAD)) &&
                    !(abHookFlags[nFilterType + 1] & HKF_INTERSENDABLE)) {

                /*
                 * Can't hook console or GUI system thread if inter-thread
                 * calling isn't implemented for this hook type.
                 */
                 RIPERR1(ERROR_HOOK_TYPE_NOT_ALLOWED,
                         RIP_WARNING,
                         "nFilterType (%ld) not allowed in zzzSetWindowsHookEx",
                         nFilterType);

                 return NULL;
            }
        }
    }

    /*
     * Check if this thread has access to hook its desktop.
     */
    switch( nFilterType ) {
    case WH_JOURNALRECORD:
        amDesired = DESKTOP_JOURNALRECORD;
        break;

    case WH_JOURNALPLAYBACK:
        amDesired = DESKTOP_JOURNALPLAYBACK;
        break;

    default:
        amDesired = DESKTOP_HOOKCONTROL;
        break;
    }

    if (!RtlAreAllAccessesGranted(ptiCurrent->amdesk, amDesired)) {
         RIPERR0(ERROR_ACCESS_DENIED,
                RIP_WARNING,
                "Access denied to desktop in zzzSetWindowsHookEx");

         return NULL;
    }

    if (amDesired != DESKTOP_HOOKCONTROL &&
        (ptiCurrent->rpdesk->rpwinstaParent->dwWSF_Flags & WSF_NOIO)) {
        RIPERR0(ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION,
                RIP_WARNING,
                "Journal hooks invalid on a desktop belonging to a non-interactive WindowStation.");

        return NULL;
    }

#if 0
    /*
     * Is this a journal hook?
     */
    if (abHookFlags[nFilterType + 1] & HKF_JOURNAL) {
        /*
         * Is a journal hook of this type already installed?
         * If so it's an error.
         * If this code is enabled, use PhkFirstGlobalValid instead
         *  of checking phkStart directly
         */
        if (ptiCurrent->pDeskInfo->asphkStart[nFilterType + 1] != NULL) {
            RIPERR0(ERROR_JOURNAL_HOOK_SET, RIP_VERBOSE, "");
            return NULL;
        }
    }
#endif

    /*
     * Allocate the new HOOK structure.
     */
    phkNew = (PHOOK)HMAllocObject(ptiCurrent, ptiCurrent->rpdesk,
            TYPE_HOOK, sizeof(HOOK));
    if (phkNew == NULL) {
        return NULL;
    }

    /*
     * If a DLL is required for this hook, register the library with
     * the library management routines so we can assure it's loaded
     * into all the processes necessary.
     */
    phkNew->ihmod = -1;
    if (hmod != NULL) {

#if defined(WX86)

        phkNew->flags |= (dwFlags & HF_WX86KNOWNDLL);

#endif

        phkNew->ihmod = GetHmodTableIndex(pstrLib);

        if (phkNew->ihmod == -1) {
            RIPERR0(ERROR_MOD_NOT_FOUND, RIP_VERBOSE, "");
            HMFreeObject((PVOID)phkNew);
            return NULL;
        }

        /*
         * Add a dependency on this module - meaning, increment a count
         * that simply counts the number of hooks set into this module.
         */
        if (phkNew->ihmod >= 0) {
            AddHmodDependency(phkNew->ihmod);
        }
    }

    /*
     * Depending on whether we're setting a global or local hook,
     * get the start of the appropriate linked-list of HOOKs.  Also
     * set the HF_GLOBAL flag if it's a global hook.
     */
    if (ptiThread != NULL) {
        pphkStart = &ptiThread->aphkStart[nFilterType + 1];

        /*
         * Set the WHF_* in the THREADINFO so we know it's hooked.
         */
        ptiThread->fsHooks |= WHF_FROM_WH(nFilterType);

        /*
         * Set the flags in the thread's TEB
         */
        if (ptiThread->pClientInfo) {
            BOOL fAttached;

            /*
             * If the thread being hooked is in another process, attach
             * to that process so that we can access its ClientInfo.
             */
            if (ptiThread->ppi != ptiCurrent->ppi) {
                KeAttachProcess(&ptiThread->ppi->Process->Pcb);
                fAttached = TRUE;
            } else
                fAttached = FALSE;

            ptiThread->pClientInfo->fsHooks = ptiThread->fsHooks;

            if (fAttached)
                KeDetachProcess();
        }

        /*
         * Remember which thread we're hooking.
         */
        phkNew->ptiHooked = ptiThread;

    } else {
        pphkStart = &ptiCurrent->pDeskInfo->aphkStart[nFilterType + 1];
        phkNew->flags |= HF_GLOBAL;

        /*
         * Set the WHF_* in the SERVERINFO so we know it's hooked.
         */
        ptiCurrent->pDeskInfo->fsHooks |= WHF_FROM_WH(nFilterType);

        phkNew->ptiHooked = NULL;
    }

    /*
     * Does the hook function expect ANSI or Unicode text?
     */
    phkNew->flags |= (dwFlags & HF_ANSI);

    /*
     * Initialize the HOOK structure.  Unreferenced parameters are assumed
     * to be initialized to zero by LocalAlloc().
     */
    phkNew->iHook = nFilterType;

    /*
     * Libraries are loaded at different linear addresses in different
     * process contexts.  For this reason, we need to convert the filter
     * proc address into an offset while setting the hook, and then convert
     * it back to a real per-process function pointer when calling a
     * hook.  Do this by subtracting the 'hmod' (which is a pointer to the
     * linear and contiguous .exe header) from the function index.
     */
    phkNew->offPfn = ((ULONG_PTR)pfnFilterProc) - ((ULONG_PTR)hmod);

#ifdef HOOKBATCH
    phkNew->cEventMessages = 0;
    phkNew->iCurrentEvent  = 0;
    phkNew->CacheTimeOut = 0;
    phkNew->aEventCache = NULL;
#endif //HOOKBATCH

    /*
     * Link this hook into the front of the hook-list.
     */
    phkNew->phkNext = *pphkStart;
    *pphkStart = phkNew;

    /*
     * If this is a journal hook, setup synchronized input processing
     * AFTER we set the hook - so this synchronization can be cancelled
     * with control-esc.
     */
    if (abHookFlags[nFilterType + 1] & HKF_JOURNAL) {
        /*
         * Attach everyone to us so journal-hook processing
         * will be synchronized.
         * No need to DeferWinEventNotify() here, since we lock phkNew.
         */
        ThreadLockAlwaysWithPti(ptiCurrent, phkNew, &tlphkNew);
        if (!zzzJournalAttach(ptiCurrent, TRUE)) {
            RIPMSG1(RIP_WARNING, "zzzJournalAttach failed, so abort hook %#p", phkNew);
            if (ThreadUnlock(&tlphkNew) != NULL) {
                zzzUnhookWindowsHookEx(phkNew);
            }
            return NULL;
        }
        if ((phkNew = ThreadUnlock(&tlphkNew)) == NULL) {
            return NULL;
        }
    }

    UserAssert(phkNew != NULL);

    /*
     * Later 5.0 GerardoB: The old code just to check this but
     *  I think it's some left over stuff from server side days.
    .* Let's assert on it for a while
     * Also, I added the assertions in the else's below because I reorganized
     *  the code and want to make sure we don't change behavior
     */
    UserAssert(ptiCurrent->pEThread && THREAD_TO_PROCESS(ptiCurrent->pEThread));

    /*
     * Can't allow a process that has set a global hook that works
     * on server-side winprocs to run at background priority! Bump
     * up it's dynamic priority and mark it so it doesn't get reset.
     */
    if ((phkNew->flags & HF_GLOBAL) &&
            (abHookFlags[nFilterType + 1] & HKF_INTERSENDABLE)) {

        ptiCurrent->TIF_flags |= TIF_GLOBALHOOKER;
        KeSetPriorityThread(&ptiCurrent->pEThread->Tcb, LOW_REALTIME_PRIORITY-2);

        if (abHookFlags[nFilterType + 1] & HKF_JOURNAL) {
            ThreadLockAlwaysWithPti(ptiCurrent, phkNew, &tlphkNew);
            /*
             * If we're changing the journal hooks, jiggle the mouse.
             * This way the first event will always be a mouse move, which
             * will ensure that the cursor is set properly.
             */
            zzzSetFMouseMoved();
            phkNew = ThreadUnlock(&tlphkNew);
            /*
             * If setting a journal playback hook, this process is the input
             *  provider. This gives it the right to call SetForegroundWindow
             */
            if (nFilterType == WH_JOURNALPLAYBACK) {
                gppiInputProvider = ptiCurrent->ppi;
            }
        } else {
            UserAssert(nFilterType != WH_JOURNALPLAYBACK);
        }
    } else {
        UserAssert(!(abHookFlags[nFilterType + 1] & HKF_JOURNAL));
        UserAssert(nFilterType != WH_JOURNALPLAYBACK);
    }



    /*
     * Return pointer to our internal hook structure so we know
     * which hook to call next in CallNextHookEx().
     */
    DbgValidateHooks(phkNew, phkNew->iHook);
    return phkNew;
}

음......단순한 구조체 조작만 하는 것 같지는 않아보이고

AddHmodDependency(), HMAllocObject(), ... 등이 뭔가를 하는 것 같은데 말이죠.

음 어쨌거나, SetWindowsHookEx()는 유저 모드에서는 물론, 커널 모드에서도 객체를 직접 수정하거나 Win32K의 또다른 내부함수를 호출하는 것이었기 때문에

SetWindowsHookEx()를 막을 수 없었던 것 같습니다.

HOOK 구조체를 초기화해도, 이미 Inject된 DLL이 언로드될것 같지는 않고 말이죠.

또한, 후킹되었다면 WIN32K!zzzUnhookWindowsHookEx()를 호출하는 방법도 있겠지만, 이 방법은 일단

zzzUnhookWindowsHookEx의 주소를 얻어와야 하므로, 굉장히 어렵습니다.

지금으로서는 Win32K!NtUserSetWindowsHookEx를 후킹하거나, Win32K!zzzSetWindowsHookEx를 후킹하는 수밖에 없는 것 같습니다.

(UserMode Hook은 제외)


Win32K!NtUserSetWindowsHookEx에서 Win32K!zzzSetWindowsHookEx를 호출하므로, 주소를 얻기는 쉽습니다.

실제로 제가 Win32K!zzzSetWindowsHookEx를 후킹해서 쓰고 있으나, 아직까지 별 문제는 일어나지 않았습니다.

Posted by skensita
Programming/Kernel / Driver2008. 11. 30. 16:35


WDM 기반 USB 지원 드라이버 제작
Posted by skensita
Programming/Kernel / Driver2008. 11. 30. 16:31

우선 DOS사용자 분들은 왜 DDK가 필요하신지 궁금할겁니다.
DDK(Device Development Kit)을 제가 처음 접하게 된것은 1996년 겨울입니다.
이때는 자료도 없어서 고생을 많이 했었죠...
지금이야 자료가 널려서 제가 설자리가 없다는게 문제지만요.
하여튼 DDK는 MS에서 내어놓은 윈도우에서 하드웨어를 관리해주기위한
하나의 드라이버를 만들기 위한 Software package입니다.

그럼 왜 이 DDK가 필요한지 부터 알아야겠죠....
DOS시절과는 달리 윈도우에서는 하드웨어를 사용자가 직접관리하지 않고
OS가 해줍니다. 즉 이를 Kernel이라고 하죠...리눅스와 마찬가지로요...
그러나 이 Kernel이라는 것이 사용자가 쉽게 손을 델수 없게 MS에서는
만들어 놓았기때문에 Kernel과 통신을 할수 있는 부분이 필요하게 되었습니다. 즉 개발자가 드라이

버라는것을 만들어 OS에 등록하면 이를 인식해서
필요한 처리를 해주는 것이죠...즉 메모리 관리아 IO에 관련하여 여러가지
처리들을 해주게 됩니다.

그러나 사용자들의 문제는 여기에서만 그치지 않습니다.
분명 문법은 C또는 C++인데 이게 그처럼 쉽지 많은 않다는 것이죠...

I/O를 위해서는 사용자가 보지 못하는 여러계층이 있습니다.
Hardware이외에 HAL이라는 하드웨어 Access계층과 Interrupt 서비스 그리고 Procedure Call등을 할

수 있는 계층이죠. 이를 통해 I/O를 관리하고
명령을 내릴때는 DriverEntry를 통하고 Dispatch Routine, Unload등을 통해 동작을 하게 됩니다.

즉 I/O Manager는 DriverEntry를 통해 초기화 하고 dispatch Routine를 통해 명령을 전달하고,

Interrupt Service를 통해 OS에 인터럽트가 걸리는걸 처리하고 Defered Procedure Call통해 I/O

Manager에게 하드웨어의 메세지를 전달하게 됩니다.
그리고 마지막으로 Unload를 통해 드라이버를 Unloading를 하게 되지요...
이러한 과정은 일반 Win32프로그램 제작하는 프로그래머들에게는 알지 못하는 생소한 과정입니다

.(Developing Windows NT Device Driver 의 Chapter1참조)

그러나 이 DDK가 C의 상식을 벗어 나는 것은 아니기때문에
꼮 C/C++의 기초를 다지고 있어야 합니다.

다음은 I/O의 User-Level Overview를 하도록 하죠...
가장 중요한 부분입니다.


안녕하세요....
글을 올리기에 좀 오래 걸리는군요...

이번시간엔 User-Level API를 공부해 보도록 하죠...

DDK의 API는 C/C++처럼 그리 복잡하지 않습니다.
그렇다고 단순한것도 아니죠...

이유는 이렇습니다.
처음 C를 접하던 시절에 printf, #include와 같은 문법이 익숙하지 않고
아에 이걸 왜 쓰는지조자 모르는 상황에서 배울때와의 심정이 같습니다.

그런데 이 부분이 DDK를 하면서 또한 번 드끼게 되죠....
바로 그것은 CreateFile, ReadFile, WriteFile, DeviceIOControl
CloseHandle들의 함수 때문이죠...

우선 왜 이러한 함수들이 사용되는지 부터 알아야 겠죠....

대부분의 중요 함수에 File이라는 약자가 붙습니다.
이유는 간단합니다. 하드웨어와 통신을 하기 위한 하나의 맵핑파일을
만드는 것입니다.우리가 서로 다른 프로세서 들끼리 정보를 주고 받기위해서
공유된 맵핑 파일을 만드는것과 동일한 원리죠...
좀 틀리다면 이는 드라이버 구동을 위한 공유된 머신이름이라던지
또는 공유 메모리 들을 지정하게 됩니다.

그리고 장비 자체가 register에 들어 가기때문에
device name같은 부분들이 들어가게 됩니다.

그럼 하나씩 들어가볼가요...

우선 CreateFile은 하드웨어 정의에 의한 path를 이용해서 맵핑파일을 샌성하고 이를 통해 핸들을

생성하게 됩니다. 즉 자하철 타고 문들 통과하기 위해
티켓을 하나 생성한다고 생각하시면 됩니다. 따라서 하드웨어와 I/O를 하기 위한 매니저를 하나 생

성하는 것이죠...

ReadFile와 WriteFile의 경우는 생성된 티켓을 이용해서 이곳에 어떠한 데이터를 중것인가, 또는

받을것인가를 지정하는 부분입니다. 그러나 이부분이 I/O에 어떠한 행동을 직접주는 것은 아니기때

문에 데이터를 넣고 빼기 위한 하나의 호출에 불과 합니다.

세번째는 DeviceIOControl입니다. 이 함수가 실제 일을 진행한다고 할수 있는데요.. 이 부분이 되

면 이는 위에서 언급된 함수들을 통해 하드웨어의 버퍼를 통해 데이터를 집어 넣거나 가져오게 되

는 것이죠...

마지막으로 CloseHandle는 원하는 작업이 끝나면 핸들을 넘겨줌으로써 작업을 마치게 됩니다.

지금까지 말했지만 잘 이해가 안가시죠...
말은 잘황하게 했지만 실질적으로 어떻게 이 부분들이 그냥 선언해 놓은 것만으로 동작을 하는지

모든 분들이 궁금해 할겁니다.

그러나 저는 이렇게 말하고 싶습니다. 이전 강좌에서도 말했듯이
드라이버를 사용자가 컨트롤 하는게 아닙니다 즉 커널이 알아서
컨드롤을 해주는 것이죠...따라서 OS와 하드웨가 어떻게 통신을 할것인지
하나의 인터페이스 규약만을 만들어 주는것에 불과 합니다.

그리고 OS에서는 이 함수들을 불러다가 필요한 부분에 호출하는 것이구요...
따라서 드라이버 하나만 가지고 어플리케이션까지 만들수 있는게 아니라
따로 드라이버를 제어하는 응용 프로그램을 제작하셔야 합니다.

즉 이부분은 이 APP를 만들때 사용하는 부분입니다.
시작하기 전에 printf문을 예로 들었습니다.
나만의 하드웨어는 자신만의 인터페이스를 가지고 있기때문에
이를 위해서는 printf라는 '문자열을 화면상에 뿌려라!'
하나의 함수 하나를 만들어 주기 위한 하나의 인터페이스 함수인것이죠...

즉 하드웨어와 통신하기 위해 초기화 하는 함수를 만든다면
int CHardware::OpenController()
{
   RhndFile = CreateFile(
                                        ".SHINGIRUDev",               // Open the

Device "file"
                                        GENERIC_READ,
                                        FILE_SHARE_READ,
                                        NULL,
                                        OPEN_EXISTING,
                                        0,
                                        NULL);
               
    if(RhndFile == INVALID_HANDLE_VALUE)       // Was the read device opened?
    {
        printf( "Unable to open the read device.n");
        return error;
    }

    WhndFile = CreateFile(
                    ".SHINGIRUDev",           // Open the Device "file"
                    GENERIC_WRITE,
                    FILE_SHARE_WRITE,
                    NULL,
                    OPEN_EXISTING,
                    0,
                    NULL);

    if(WhndFile == INVALID_HANDLE_VALUE)       // Was the write device opened?
    {
        printf( "Unable to open the write device.n");
                return error;
    }

        return 0;
}
이런식으로 SHINGIRUDev라는 이름으로 레지스터에 등록된 이름으로 파일을 하나 만드는 경우 입니

다.

그리고 여기에 Read, Write부분을

int CHardware::PortWrite(unsigned long port, unsigned long data)
{
    InputBuffer.PortNumber = port;                                // Get the port number
    DataValue = data;                                                        // Get the data

to be written.

    IoctlCode = IOCTL_SHINGIRU_WRITE_PORT_UCHAR;
    InputBuffer.CharData = (UCHAR)DataValue;
    DataLength = offsetof(SHINGIRU_WRITE_INPUT, CharData)+sizeof(InputBuffer.CharData);

    IoctlResult = DeviceIoControl(
                        WhndFile,                // Handle to device
                        IoctlCode,              // IO Control code for Write
                        &InputBuffer,           // Buffer to driver.  Holds port & data.
                        DataLength,             // Length of buffer in bytes.
                        NULL,                   // Buffer from driver.   Not used.
                        0,                      // Length of buffer in bytes.
                        &ReturnedLength,        // Bytes placed in outbuf.  Should be 0.
                        NULL                    // NULL means wait till I/O completes.
                        );


    if(IoctlResult)          // Did the IOCTL succeed?
    {       
                return 0;
    }else {
                MessageBox( NULL, "Ioctl failed, unknown error.", "ERROR!", MB_ICONERROR |

MB_OK );
                return error;
    }

}


또는

int CHardware::PortRead(unsigned long port)
{
    IoctlCode = IOCTL_SHINGIRU_READ_PORT_UCHAR;
    DataLength = sizeof(DataBuffer.CharData);
           
        PortNumber = port;                                                        // Get the

port number to be read

    IoctlResult = DeviceIoControl(
                            RhndFile,            // Handle to device
                            IoctlCode,          // IO Control code for Read
                            &PortNumber,                // Buffer to driver.
                            sizeof(PortNumber),        // Length of buffer in bytes.
                            &DataBuffer,        // Buffer from driver.
                            DataLength,         // Length of buffer in bytes.
                            &ReturnedLength,    // Bytes placed in DataBuffer.
                            NULL                // NULL means wait till op. completes.
                            );

        if(IoctlResult)
        {   
                unsigned long   Data;

        if(ReturnedLength != DataLength)
        {       
                        MessageBox( NULL, "ReturnedLength != DataLength.", "ERROR!",

MB_ICONERROR | MB_OK );
                        return error;
        }else {
                        Data = DataBuffer.CharData;

                        return Data;
                }
        }else {
                  MessageBox( NULL, "Ioctl failed, unknown error.", "ERROR!", MB_ICONERROR |

MB_OK );
               
                return error;
    }
}
이런식으로 I/O를 관리하는 함수를 만드는 것이죠....

보시면 아시겠지만 ReadFile, WriteFile은 없죠?

왜냐면 둘다 처리 하기 위해 Read, Write부분은 모두 CreateFile로 주어 속성만을 바꾸어 주었기

때문에 함수에서는 단순히 핸들만을 가지고 조정하게 됩니다. 그리고
삭제는

int CHardware::CloseController(void)
{
        if (!CloseHandle(RhndFile))                  // Close the Device "file" for read
        {
                MessageBox( NULL, "Failed to close the read device.", "ERROR!", MB_ICONERROR

| MB_OK );
                return error;
        }
        if (!CloseHandle(WhndFile))                  // Close the Device "file" for write
        {
                MessageBox( NULL, "Failed to close the write device.", "ERROR!",

MB_ICONERROR | MB_OK );
                return error;
        }
        return 0;
}
이런식으로 해주어야 합니다.

이 이외에도 보시면 여러 파라미터 들이 있죠.,...
이부분들은 여러분이 만들어낸 하드웨어의 주소라든지
데이터 구조들이기때문에 따러 설명드리지는 않겠습니다.

참고로 제가 예전에 만들었던 부분은

HKEY hkReg;                         // For Reg key

// The following is returned by IOCTL.  It is true if the write succeeds.
BOOL IoctlResult;

// The following parameters are used in the IOCTL call.
HANDLE RhndFile;                //Read Handle to device, obtain from CreateFile
HANDLE WhndFile;                //Write Handle to device, obtain from CreateFile

SHINGIRU_WRITE_INPUT        InputBuffer;        //Input buffer for DeviceIoControl
LONG IoctlCode;
ULONG DataValue;
ULONG DataLength;
ULONG ReturnedLength;        // Number of bytes returned in output buffer
//read
ULONG PortNumber;                // Buffer sent to the driver (Port #)

union{
        ULONG  LongData;
        USHORT ShortData;
        UCHAR  CharData;
}DataBuffer;                                // Buffer received from driver (Data)
       
/*************************************************************************
|    Definitions for ENcoder                                             |
*************************************************************************/
/* Addresses */
const int DATA[6]          = { 0x00, 0x02, 0x04, 0x06, 0x08, 0x0A }; /* data registers of

LS7166         */
const int CONTROL[6] = { 0x01, 0x03, 0x05, 0x07, 0x09, 0X0B }; /* control registers of

LS7166        */
#define LATCH                        8                /* causes LS7166s to latch counter    

    */

/* Commands */
#define MASTER_RESET        0X20        /* master reset command                         */
#define INPUT_SETUP                0X68        /* command to setup counter input         */
#define ADDR_RESET                0X01        /* command to reset address pointer */
#define LATCH_CNTR                0X02        /* command to latch counter                 */
#define CNTR_RESET                0X04        /* command to reset counter                 */
#define PRESET_CTR            0X08        /* transfer preset to counter                 */
const int QUAD_X[5] = {0XC1, 0XC1, 0XC2, 0XC1, 0XC3}; /* quad multiplier */
const int error                = 0XFF;

/*************************************************************************
|    Definitions for DAC                                                 |
*************************************************************************/
const int da_base_addr =        0x0C;
const int ad_base_addr =        0x10;
const int timer_base_addr = 0x14;
const int io_base_addr =        0x18;
const int CH[4] = { 0xFC, 0xFD, 0xFE, 0xFF };

#define DA_LSB        da_base_addr+0
#define DA_MSB        da_base_addr+1
#define DA_CONT        da_base_addr+2
#define DA_CW        da_base_addr+3

#define DA_CW_DATA                        0x80
#define DA_RANK1                        0xBF
#define DA_RANK2                        0xFF

#define DA_CS1                                0xEF
//#define DA_CS2                                0xDF

#define DA_RESET1                        0xBF
//#define DA_RESET2                        0xF7
#define DA_DISABLE        0x3C
이런식으로 매크로 또는 변수가 지정되어 있습니다.

그럼 다음 시간엔 Driver의 데이터 구조들에 대해 알아보겠습니다.
즉 실제 드라이버를 만들기 위한 드라이버 함수들이죠...이부분이 진짜 DDK부분이 되겠죠...


Driver Data Structures입니다.
이는 DDK에서 만들어진 sys파일이 어떻게 하드웨어와 소프트웨어와
데이터를 주고 받는지에 대한 설명입니다.
즉 다음시간에 다룰 DriverEntry에 대한 서론이라고 할수 있죠..

우선 어떻게 Driver가 시작되는지 알아보죠...
우리는 하드웨어를 구입하면 가장 먼저 하는것이
하드웨어를 OS에 등록하는 것입니다.
일반적으로 Driver를 설치한다고 하죠...
이는 Registery와 sys파일 그리고 기타 app들이 있는 몇가지 부분을
설정또는 복사하는 겁니다.

가장 중요한 부분은 Registry는
HKEY_LOCAL_MACHINECurrentControlSetservicesdeivername
형식으로 들어가게 됩니다.

이에 대해서는 추후에 다시 설명 드릴 기회가 있으니 다음에 설명하도록 하죠..

그러면 다음과 같은 구조를 갖게 됩니다.

Service Control Manager  ----- Registry ------- MyDriver.sys

여기서 Service Control Manager란 App라고 보셔도 됩니다.
원래는 이것의 기능은 OS에 드라이버의 메모리 어드레스를 Loading해주는
역활을 하는데요. 이 부분이 사실 App에서 호출을 하기 때문에 그렇습니다.
그리고 Registry를 접속해서 등록된 Driver를 호출하는 거죠...

즉 Registry를 참조하여 어드레스를 얻어서 Service Control Manager가
드라이버 sys파일을 호출하게 됩니다. 이때 호출되는 것이 DriverEntry입니다.
그리고 이렇게 과정을 거치면 Driver Object를 생성하게 되는 데요.
이를 통해 Interface를 하게 되는거죠..

즉 이전 시간에 했던

CreateFile, ReadFile, WriteFile, CloseHandle를 통해
DriverEntry에 Open, Read, Write, Close, Unload등 필요한 명령들을 Loading된 메모리를 통해서

전달하는 것이죠...


그럼 이제 DriverEntry를 들어가기 전에 필요한 몇가지 부분을
살펴 보겠습니다.

DDK는 DRIVER_OBJECT라는 구조체를 가지고 있습니다.
이를 이용해서 DriverEntry에서 MajorFunction을 을 통해
CREATE, CLOSE, READ, WRITE POWER, PNP등 여러가지 기능들을 할수 있는것이죠...

가진 기본적으로

  DriverObject->MajorFunction[IRP_MJ_CREATE]          = SHINGIRUDispatch;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]           = SHINGIRUDispatch;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]  = SHINGIRUDispatch;
    DriverObject->DriverUnload                          = SHINGIRUUnload;
    DriverObject->MajorFunction[IRP_MJ_PNP]            = SHINGIRUDispatchPnp;
    DriverObject->MajorFunction[IRP_MJ_POWER]          = SHINGIRUDispatchPower;
    DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = SHINGIRUDispatchSystemControl;
    DriverObject->DriverExtension->AddDevice           = SHINGIRUAddDevice;

위의 몇가지를 들수 있습니다.
여기서 DriverObject는 DriverEntry에서 PDRIVER_OBJECT로 포인터 형을
받기 때문이녀. IRP_MJ_*로 시작을 하는
몇가지 옵션이 있습니다.
이는 보시면 아시겠지만 MajorFunction을 통해 어떠한 함수들이 호출 된느지를 알려주는 것입니다.

    DriverObject->MajorFunction[IRP_MJ_PNP]            = SHINGIRUDispatchPnp;

위문장을 예로들면 IRP_MJ_PNP를 싱행하기 위해서는 SHINGIRUDispatchPnp함수를 호출하는 것이죠.

3-1에서도 말했다 시피 레지스터를 통해 DriverObject가 만들어 질때 DriverEntry를 통해 장비와

인터페이스를 하게 되는게 DriverEntry에
위와 같은 DriverObject를 만들고 인터페이스를 하는것이죠...

뭐 앞으로 더욱 자세한 내용을 할거기때문에 이는 이쯤에서 설명드리구요..

따라서 이때 각 함수에 IoCreateDevice를 하게 되구요...
이때 DEVICE_OBJECT, 또는 PDEVICE_OBJECT를 통해 IRP나 flag그리고
DeviceExtension, DEVICE_TYPE등을 설정하게 됩니다.
이후에 필요한 작업들을 지시 하는겁니다.

따라서 방금 말한 부분들은 대부분 AddDevice부분에 들어가게 되는것이죠...


NTSTATUS
SHINGIRUAddDevice(
    IN PDRIVER_OBJECT DriverObject,
    IN PDEVICE_OBJECT PhysicalDeviceObject
    )
{
    NTSTATUS                status = STATUS_SUCCESS;
    PDEVICE_OBJECT          deviceObject = NULL;
    PLOCAL_DEVICE_INFO      deviceInfo;
    UNICODE_STRING          ntDeviceName;
    UNICODE_STRING          win32DeviceName;

    PAGED_CODE();

    RtlInitUnicodeString(&ntDeviceName, SHINGIRU_DEVICE_NAME);

    //
    // Create a device object.
    //

    status = IoCreateDevice (DriverObject,
                             sizeof (LOCAL_DEVICE_INFO),
                             &ntDeviceName,
                             SHINGIRU_TYPE,
                             0,
                             FALSE,
                             &deviceObject);

   
    if (!NT_SUCCESS (status)) {

        return status;
    }

..........
}

이외에도 Adapter Objects, Controller Object, Interrupt Object, Timer Objects, DPC Objects들

이 있습니다. 즉 DriverEntry를 통해 생성된 Device Object들을 통해 로드된 메모리를 기점으로 인

터페이스가 이루어 지는거죠...

Posted by skensita
Programming/Kernel / Driver2008. 11. 30. 16:30
컴퓨터를 사서 처음 하는 것은 운영체제와 장치 드라이버 설치일 것이다. 드라이버를 제대로 설치하지 않으면 장치들은 제대로 작동하지 않고 컴퓨터도 오작동하게 된다. 그래서 드라이버는 잘 만들어야 하는 프로그램이다. 하지만 이 분야를 처음 공부하려는 사람들은 아마도 어렵게 느껴질 것이다. 충분한 자료가 부족하고 단기간에 배울 수 없는 분야이기 때문이다. 이번 연재를 통해 장치 드라이버를 공부하려는 독자들에게 조금이나마 도움이 되고자 한다.
이상섭·곽태진 (devguru, 마이크로소프트웨어)
2002/09/27


장치 드라이버란
장치 드라이버는 ‘장치를 구동하는 프로그램’으로 줄여서 드라이버라고도 부른다. 드라이버는 하드웨어와 소프트웨어(운영체제) 중간에 위치하며, 프로그램 중에서 가장 저수준 레벨에서 처리하는 프로그램으로 볼 수 있다. 일반적으로 운영체제에서 많은 부분이 드라이버로 되어 있다고 봐도 될 것이다.

드라이버는 기본적으로 하드웨어를 제어하고 운영하는 프로그램으로 볼 수 있다. 하드웨어 자원인 메모리, I/O, 인터럽트, DMA를 처리해 사용자가 장치를 사용할 수 있게 한다. 그리고 일반적인 애플리케이션에서는 할 수 없는 일이나 운영체제의 기능을 확장할 때도 드라이버는 필요하다. 예를 들면 바이러스 백신, 보안을 위한 파일 암복호화나 네트워크 패킷 필터링을 하기 위해 드라이버를 이용하기도 한다.

드라이버는 운영체제와 밀접하게 연관되어 작동하기 때문에 운영체제에 따라 다르게 만들어진다. 즉, 윈도우·리눅스·Mac OS마다 드라이버를 새로 만들어야 한다. 윈도우에서도 윈도우 9x 계열(95·98·ME)과 윈도우 NT 계열(NT·2000·XP)은 드라이버를 다르게 만들어야 한다. 따라서 장치를 만드는 하드웨어 업체에서는 실제 소비자를 위해 모든 운영체제에 드라이버를 지원해야 하다보니 개발기간이 길어질 수밖에 없어진다.

윈도우 2000 드라이버의 종류
우리는 이번 연재에서 윈도우 2000용 드라이버를 만든다. 먼저 윈도우 2000에서는 어떤 종류의 드라이버가 있는지 알아보자.

사용자 삽입 이미지


<그림 1>은 윈도우 2000의 드라이버 종류를 나타낸 것이다. 각각의 드라이버가 어떤 것인지 간단히 설명해 보겠다.

◆ 가상 장치 드라이버 : x86 플랫폼에서 하드웨어를 액세스하는 DOS 기반 응용 프로그램을 돌아가게 하는 사용자 모드 요소다. 가상 장치 드라이버(VDD, Virtual Device Driver)는 윈도우 98의 VxD와는 다른 것이니 혼돈하지 말기 바란다.

◆ 커널 모드 드라이버 : 일반적인 장치 드라이버를 총칭한다고 보면 된다.

◆ WDM 드라이버 : 전력 관리와 PnP를 처리하는 커널 모드 드라이버다. 윈도우 98과 2000에서 소스 호환되어 드라이버 개발을 쉽게 할 수 있다. WDM 드라이버는 클래스 드라이버와 미니 드라이버로 나뉜다. 클래스 드라이버는 주로 장치를 클래스라는 종류로 나눠 처리하고, 미니 드라이버는 클래스 드라이버에 하드웨어 장치들마다 다르게 처리할 수 있는 도움을 제공한다.

◆ 비디오 드라이버 : 디스플레이와 프린터를 위한 드라이버다.

◆ 파일 시스템 드라이버 : 일반적인 로컬 하드 디스크나 네트워크로 연결된 파일 시스템을 처리한다.

◆ 레거시 드라이버 : 다른 드라이버의 도움 없이 하드웨어 장치를 직접 제어하는 커널 모드 드라이버다. 이 드라이버는 윈도우 2000에서도 작동하는 예전의 윈도우 NT 드라이버를 포함한다.

이번 연재에서는 윈도우 2000용 장치 드라이버에 중에서 WDM 드라이버에 대한 내용보다는 윈도우 NT 계열의 레거시 드라이버에 대한 내용을 다룰 것이다. WDM 드라이버도 레거시 드라이버 기반 위에 PnP와 전력 관리 부분이 추가된 것이기 때문에 기본적인 내용은 레거시 드라이버만 알아도 이해하는 데 충분하다.

윈도우 2000의 내부구조
장치 드라이버는 운영체제와 밀접하게 연관되어 있다고 말했다. 따라서 운영체제 내부를 알고 있어야 드라이버를 개발하는 데 필요한 내용을 이해하기 더 쉽다. 지금부터 윈도우 2000의 전체적인 구조를 살펴보자. 윈도우 2000의 내부구조를 보면, <그림 2>와 같다.


사용자 삽입 이미지


윈도우의 내부는 크게 커널 모드와 사용자 모드로 나눌 수 있다.

◆ 커널 모드 : 프로세서의 특권 레벨(privileged level)로 프로세서의 모든 명령을 처리할 수 있고, 시스템 자원이나 하드웨어들을 직접 액세스할 수 있다. 장치 드라이버나 운영체제의 코드들이 작동하는 모드이기도 하다.

◆ 사용자 모드 : 일반 응용 프로그램이 동작하는 모드로 커널 모드와는 달리 비특권 레벨(nonprivileged level)이기 때문에 하드웨어나 시스템 자원을 직접 이용할 수 없고 시스템 서비스(API)를 이용해 접근해야 한다. 그렇지 않으면 예외(exception)가 발생해 프로그램이 종료한다.

커널 모드는 다시 Executive, 커널, HAL(Hardware Abstraction Layer) 부분으로 나눌 수 있다.

◆ Executive : 윈도우의 기본이 되는 서비스들을 제공한다. 메모리·프로세스·쓰레드를 관리한다. 특히, Executive를 구성하는 것 중에서 I/O 관리자는 장치 드라이버와 관련해서 많은 부분을 처리한다. 그리고 나머지 구성요소도 드라이버에서 사용하는 서비스를 많이 가지고 있다.

◆ 커널 : 주로 저수준 운영체제 기능을 제공한다. 쓰레드 스케쥴링, 인터럽트와 예외 처리, 멀티 프로세서 동기화 등을 맡는다.

◆ HAL : 하드웨어 추상화 계층으로 플랫폼이 어떤 것이든지 상관없이 운영체계가 작동할 수 있게 한다. 이 계층이 있기 때문에 윈도우 NT 계열 운영체제가 멀티 플랫폼을 지원할 수 있게 된다.


장치 드라이버 개발환경
기본적으로 다음의 개발도구들로 개발한다.

◆ 비주얼 C++ 6.0 : 비주얼 C++의 개발 환경을 이용하기 위해 설치하는 것이 아니라 C 컴파일러나 링커를 이용하기 위해 설치한다.

◆ DDK(Driver Development Kit) : 장치 드라이버 개발에 필수적인 툴로 드라이버 개발에 관련된 라이브러리, 헤더 파일, 예제, 문서(도움말도 포함), 개발에 필요한 프로그램 등이 포함되어 있다. DDK는 OS 버전마다 나오기 때문에 개발하려는 OS에 맞는 DDK를 설치해 개발한다. 우리는 이번 연재에서 윈도우 2000용 드라이버를 만들기 때문에 윈도우 2000 DDK를 설치한다. http://www.microsoft.com/ddk에서 내려받을 수 있다(<화면 1>).


사용자 삽입 이미지

◆ 디버거(WinDbg, Soft-ice) : WinDbg는 DDK에 포함되어 있어서 무료로 사용할 수 있다(http://www.microsoft.com/ddk/debugging/). Soft-ice는 컴퓨웨어 누메가(Compuware Numega)에서 나오는 드라이버스튜디오(DriverStudio)에 포함되어 있다.

◆ 에디터 : 소스를 편집할 에디터가 필요하다.

이제 개발환경이 갖춰졌으면, 실제 드라이버 개발에 필요한 내용으로 들어가자.

 
IRQL이란

IRQL(Interrupt Request Levels)은 윈도우 NT부터 나온 개념으로 단일 CPU에서 동기화를 위한 방법으로 사용하고 있다. IRQL은 0~31까지의 값으로 할당한다. 값이 클수록 높은 레벨을 나타내며 현재 실행되고 있는 플랫폼에 따라서 값들이 정해진다. IRQL은 현재 CPU가 실행되고 있는 레벨이 PASSIVE_LEVEL이라면, 그보다 더 높은 레벨의 IRQL만이 인터럽트될 수 있는 규칙을 가진다. 하드웨어 IRQL은 DIRQL(Device IRQL)로 소프트웨어는 로우 레벨 IRQL(PASSIVE_LEVEL=0, APC_LEVEL=1, DISPATCH_LEVEL=2)로 구분할 수 있다.

사용자 삽입 이미지
소프트웨어 IRQL 레벨이 드라이버와 관련이 있기 때문에 그 부분들에 대해 간단히 알아보면 다음과 같다.

◆ DPC 레벨 : 쓰레드 스케쥴링과 DPC(Deferred Procedure Call) 실행 레벨
◆ APC 레벨 : APC(Asynchronous Procedure Call) 레벨
◆ PASSIVE 레벨 : 일반적인 쓰레드의 실행 레벨
 


장치 드라이버 기초 프로그래밍
커널 모드 드라이버는 응용 프로그램과는 많은 부분에서 다르게 작동한다. 간단하게 살펴보면 드라이버는 운영체제(I/O 관리자)에 의해 호출 받는 루틴의 집합이라고 볼 수 있다. 드라이버의 루틴은 I/O 관리자에 의해 호출 받을 때까지 기다린다. I/O 관리자는 주로 다음과 같은 상황에서 드라이버의 루틴을 호출한다.

◆ 드라이버가 로드될 때(DriverEntry 루틴)
◆ 드라이버가 언로드될 때와 시스템이 셧다운될 때
◆ 응용 프로그램이 I/O 시스템 서비스를 호출했을 때
◆ 공유하는 하드웨어 자원이 드라이버에서 사용될 때
◆ 실제 장치가 동작하는 동안에 다양한 곳에서

앞의 상황 중에서 드라이버가 기본적으로 관심을 가져야 할 상황에 처리해야 할 루틴들을 설명하겠다.

DriverEntry 루틴
모든 프로그램은 제일 처음 실행되는 부분이 있어야 한다. C로 프로그램을 짜면 처음 호출을 받아 실행되는 부분이 main 함수다. 드라이버에서도 main 함수처럼 가장 처음 호출을 받는 부분이 DriverEntry가 된다. DriverEntry는 I/O 관리자에 의해 드라이버 로딩시에 호출된다. DriverEntry 루틴에서 기본적으로 처리해야 할 몇 가지 내용을 알아보자.

① I/O 요청을 처리할 장치 객체 생성
② 처리할 IRP(I/O Request Packet)의 MajorFunction에 해당하는 디스패치 루틴들을 등록
③ 응용 프로그램에서 읽기와 쓰기시에 메모리 전략 선택
④ Win32 응용 프로그램이 드라이버로 접근하기 위해 Win32 서브시스템에 심볼릭 링크 생성

<리스트 1> DriverEntry 루틴
NTSTATUS DriverEntry
( IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
......

   status = IoCreateDevice ( DriverObject, sizeof(MYDE), → ①
     &uniNtNameString, FILE_DEVICE_UNKNOWN, 0, FALSE, &deviceObject);
......
......
   if ( NT_SUCCESS(status) )
   {
     DriverObject->MajorFunction[IRP_MJ_CREATE] = SAMPLECreate; → ②

     // MajorFunction에 디스패치 루틴 등록
     DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]=SAMPLEDeviceControl;

     deviceObject->Flags |= DO_BUFFERED_IO; → ③
......
     status = IoCreateSymbolicLink( &uniWin32NameString, → ④
      &uniNtNameString );
   }

   // 인터럽트 사용시 DPC 초기화 및 사용 객체 초기화 및 메모리 사용을 위함
   // 메모리 할당

   return status;
}

언로드 루틴
일반적으로 드라이버는 로드된 후 시스템이 리부트하기 전까지는 남아 있다. 그러나 드라이버를 사용하다가 언로드해야 할 경우엔 언로드 루틴이 필요하며 DriverEntry에서 언로드 루틴을 등록해야 한다. 언로드 루틴은 I/O 관리자가 드라이버를 메모리에서 제거하기 전에 호출된다. 언로드 루틴에서 기본적으로 처리해야 할 몇 가지 내용을 살펴보자.

◆ 심볼릭 링크를 제거한다.
◆ DeviceObject를 제거한다.

VOID Sample1UnLoad( IN PDRIVER_OBJECT DriverObject )

{
......
// 장치 객체와의 심볼릭 링크를 해제한다.
IoDeleteSymbolicLink( &uniWin32NameString );
IoDeleteDevice( DriverObject->DeviceObject );
}

디스패치 루틴
윈도우 2000에서 I/O는 패킷 드리븐 방식으로 이루어진다. I/O 요청이 있을 때 먼저 I/O 관리자가 그 요청에 해당하는 내용을 가지고 IRP를 만든다. I/O 관리자는 응용 프로그램의 요청(읽기, 쓰기 등)을 받았을 때, 그 요청에 맞는 함수 코드로 바꾼다. 그리고 처리할 요청에 대한 드라이버를 선택하고 적절한 드라이버 내의 디스패치 루틴을 호출한다.

디스패치 루틴은 요청한 내용을 보고 알맞은 처리를 한 후 결과를 I/O 관리자에 반환한다. <표 1>은 응용 프로그램에서 호출하는 함수와 대응되는 드라이버 내의 주요 함수 코드들을 보여준다.

  Win32 API 함수 코드  
  CreateFile IRP_MJ_CREATE  
  CloseHandle IRP_MJ_CLOSE
IRP_MJ_CLEANUP
 
  ReadFile IRP_MJ_READ  
  WriteFile IRP_MJ_WRITE  
  DeviceIoControl IRP_MJ_DEVICE_CONTROL  


<표 1> 응용 프로그램에서 호출하는 함수와 대응되는 드라이버 내의 주요 함수 코드

디스패치 루틴에서 기본적으로 처리해야 할 몇 가지 내용은 다음과 같다.

① 드라이버와 관련 있는 IRP 스택 위치에 포인터를 얻기 위해 IoGetCurrentIrpStackLocation 함수를 호출한다.

② I/O 요청에 대한 매개변수들을 가져온다.

③ IoStatus에 반환할 값들을 IRP에 채운다. Status에는 에러 코드를 채우고, Information에는 적절한 값을 채운다. 일반적으로 읽기시에는 응용 프로그램으로 복사할 데이터 크기를 알려준다.

④ I/O 요청에 대한 모든 처리를 끝내고, IRP를 더 이상 사용하지 않기 위해 IoCompleteRequest를 호출한다.

NTSTATUS Sample1Read( IN PDEVICE_OBJECT pDeviceObject, IN PIRP Irp )
{
......
   // 현재 StackLoacation을 구한다.
   pCurrentStack = IoGetCurrentIrpStackLocation( Irp ); → ①

   // 읽을 바이트 수
   usize = pCurrentStack->Parameters.Read.Length; → ②
......
   Irp->IoStatus.Status = STATUS_SUCCESS; → ③
   Irp->IoStatus.Information = usize;

   IoCompleteRequest( Irp, IO_NO_INCREMENT ); → ④
   return STATUS_SUCCESS;
}

앞서 설명한 루틴들은 일반적인 드라이버에서 기본적으로 쓰이는 것들이다. 이외에도 다른 루틴들도 있지만 나머지 루틴들은 다음 기회에 설명하겠다.

장치 드라이버에는 많은 자료구조가 나온다. 장치 드라이버를 공부하는 데 기본적으로 알아야 할 자료들은 DriverObject, DeviceObject, IRP가 있다. 이런 자료구조들을 아는 것이 드라이버를 이해하는 데 밑거름이 되기 때문이다. 그 중에서 이번 회에는 IRP에 대한 내용을 살펴보겠다.


 

 
DriverObject와 DeviceObject의 연관 관계

직렬 드라이버를 예로 들어 DriverObject와 DeviceObect의 연관 관계를 잠시 설명해보겠다. 우리가 직렬 드라이버를 만든다고 가정해 보자. 다들 알다시피 직렬 드라이버는 COM1, COM2, COM3 등의 이름을 가지고 응용 프로그램에서 접근한다. 그러나 실제로 윈도우 2000에는 COM 개수에 따라 직렬 드라이버가 각각 다르게 존재하는 것이 아니다. 하나의 드라이버가 여러 개의 COM 포트를 관리하고 처리한다. 즉, 드라이버에서는 DriverObject가 하나 존재하고, 대신 COM 포트 개수만큼 DeviceObject를 생성하는 것이다. DriverObject가 같다는 것은 IRP 처리 루틴을 같이 공유한다는 뜻이다.

예를 들어 응용 프로그램에서 COM1을 읽는 명령이 내려왔다고 가정해 보자. Win32 서브시스템(API)을 통해 커널 모드로 진입할 것이다. 그리고 명령은 I/O 관리자에 전달될 것이며, I/O 관리자는 해당 명령을 실행할 수 있는 IRP를 생성해 해당 드라이버, 즉 직렬 드라이버에 전달한다. 그리고 DriverObject의 MajorFunction에 해당하는 디스패치 루틴 함수로 분기해 처리하고, 처리 결과를 I/O 관리자에 돌려준다. 여기서 읽기 처리 루틴이 다음과 같다고 하자.

Read(…)
{
......
// 선행 처리 과정
data = InPort ( 0x378 );
......
return data;


이와 같이 코딩되어 있다면 읽기 디스패치 루틴 실행시 0x378(COM1 포트의 주소) 값을 읽어올 것이고 응용 프로그램은 원하던 결과를 얻을 것이다. 그렇다면 응용 프로그램이 COM2 또는 COM3의 값을 읽게 하는 명령이 직렬 드라이버에 내려온다면 어떻게 될까?

그럼 COM 포트들은 같은 DriverObject를 사용하기 때문에 IRP에 관한 루틴을 공유한다. 그럼 앞과 같은 루틴이라면 COM2, COM3 포트의 값을 읽었을 때도 COM1의 값이 얻어질 것이다. 이런 문제를 해결하기 위해 DeviceExtension을 사용한다. DeviceExtension은 DeviceObject만을 위한 메모리 공간이다. 문제가 되던 data = InPort( 0x378 );을 다음과 같이 바꾸면 될 것이다.

data = InPort( DeviceObject->DeviceExtension->Port );

DeviceExtension에는 Port라는 변수가 있을 것이고, 그 변수에는 각 포트 주소가 기억되어 있다. 이와 같이 처리하면, 읽기 디스패치 루틴을 공유해도 각각의 포트 값을 읽어 올 수 있다.
 


IRP
패킷 드리븐 방식으로 I/O 요청을 처리하는 윈도우 NT 계열 운영체제는 처리해야 할 I/O 요청을 IRP를 이용해 처리한다. IRP 자료구조는 다음과 같이 크게 두 부분으로 나눌 수 있다.

◆ 헤더 : I/O 요청에 대한 다양한 정보가 있다. 요청한 I/O의 타입, 크기, 상태 등을 저장한다. buffered I/O를 할 경우에 Associatedirp에 버퍼 포인터, 다이렉트 I/O를 할 경우엔 MdlAddress에 MDL 포인터 관련 정보를 가지고 있다.

◆ I/O 스택 위치 : 함수 코드와 매개변수들을 담고 있다. I/O 스택은 I/O 요청이 어떤 처리를 하느냐에 따라 스택 크기가 결정된다.

예를 들어 플로피 디스크에 파일을 쓰는 I/O 요청이 있으면 적어도 스택 크기는 두 개가 될 것이다. 하나는 파일 시스템 드라이버를 위한 것이고, 다른 하나는 플로피 디스크 드라이버를 위한 것이기 때문이다.

IRP 버퍼 관리
사용자 응용 프로그램들이 읽기나 쓰기 같은 요청을 했을 경우 사용자 쪽의 데이터 버퍼를 처리하는 방법에 따라서 다음과 같이 분류할 수 있다.

◆ Buffered I/O : I/O 관리자는 먼저 호출한 응용 프로그램의 사용자 버퍼와 같은 크기의 Non-paged 풀(pool) 버퍼를 할당한다. 응용 프로그램에서 쓰기시 I/O 관리자는 IRP를 만들 때 할당한 버퍼로 호출한 사용자 버퍼 데이터를 복사한다. 읽기시에 I/O 관리자는 IRP가 끝났을 때 I/O 관리자가 할당한 버퍼에서 사용자 버퍼로 복사한다. 그리고 I/O 관리자가 할당한 버퍼는 해제한다.

◆ 다이렉트 I/O : I/O 관리자는 먼저 사용자 버퍼에 해당하는 물리 메모리 페이지를 잠근다(lock). 그리고 잠근 페이지에 대한 내용을 설명하기 위한 MDL을 만든다. MDL에는 버퍼에 의해 할당한 물리적인 메모리를 지정한다. 그리고 만약 드라이버가 버퍼의 내용을 접근하려고 하면, 시스템 주소 공간으로 버퍼를 맵해 사용할 수 있다.

◆ Neither I/O : I/O 관리자는 어떤 버퍼 관리도 처리하지 않는다. 대신 버퍼 관리는 장치 드라이버의 discreation으로 남겨지고, 장치 드라이버는 I/O 관리자가 다른 버퍼 관리 타입에서 처리하는 과정을 손수 처리할 수 있도록 선택할 수 있다.

꼭 그런 것은 아니지만 일반적으로 드라이버는 호출한 부분의 데이터의 양이 한 페이지(4KB)보다 작으면 Buffered I/O 방식을 사용하고, 그것보다 클 경우엔 다이렉트 I/O 방식을 사용한다. 그리고 파일 시스템 드라이버는 Neither I/O를 주로 사용한다. 데이터를 파일 시스템 캐시에서 사용자의 버퍼로 복사할 때 버퍼 처리에 오버헤드가 없기 때문이다.

드라이버 컴파일과 실행
드라이버를 만들기 위해서는 다음 파일들이 필요하다.

◆ Makefile : DDK에서 사용하는 실제 makefile을 호출하는 내용이 있다. 내용을 수정할 필요는 없다. 일반적으로 DDK 샘플에 있는 파일을 가져다 쓴다.
◆ Sources : 드라이버 컴파일 환경 및 관련 옵션을 정해준다.
◆ 드라이버 소스 파일 : 장치 드라이버 실제 소스
◆ RC(resource) 파일 : 드라이버 버전 정보를 담은 파일

컴파일
이 파일들이 구성됐으면 DDK에 있는 개발환경 Command 창을 띄운다(<화면 2>).


사용자 삽입 이미지


① 빌드 명령을 내린다. 그리고 나면 원하는 폴더에 *.SYS 파일이 생성된다. 그 파일을 \WINNT\System32\Drivers 폴더에 복사한다.

② 드라이버를 로드하려면 레지스트리에 정보를 기록해야 한다. 레지스트리에 직접 기록하든지, 아니면 간단한 *.reg 파일을 생성해 설치해도 된다. 레지스트리에 들어가는 키 위치를 보면 HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services 밑에 서브키를 생성한다. 키 이름은 드라이버 이름과 같은 이름으로 생성한다. 예를 들면 드라이버 이름이 SAMPLE.sys라면, SAMPLE이라는 키를 만든다. 키가 만들어 졌으면, 이제 키 밑에 값들을 적는다.

Start = 3
ErrorControl = 1
Type = 1

이 값은 드라이버가 윈도우가 시작되면서 올라오는 게 아니고 우리가 언제라도 올리고 내릴 수 있는, 즉 동적으로 작동하기 위해 정한 값이다.

③ 재부팅한다.

장치 드라이버 실행
Command 창을 하나 띄운다. 그리고 net start sample1을 실행한다. ‘서비스를 시작합니다’라는 메시지가 나오면 드라이버가 제대로 로드됐다는 것을 알 수 있다(<화면 3>). 드라이버를 내리고 싶다면 net stop sample1을 실행한다(<화면 4>).


 
사용자 삽입 이미지


사용자 삽입 이미지

[출처] 장치 드라이버 기초와 프로그래밍

Posted by skensita
Programming/Kernel / Driver2008. 11. 30. 16:23

Driver 개발을 처음 하시는 분들은 따라할 만한 가이가 없어서 환경을 구축하는데도 시간이 많이 걸립니다. 개발은 말할것도 없구요 그래서 그동안의 자료를 바탕으로 정리를 해 봤습니다.

아래의 가이드를 따라하시면 기초는 세팅이 되었다고 할 수 있습니다.

예제 소스는 키보드 필터 드라이버입니다. DDK의 포함된 예제소스, Ctrl2CapSource, Filemon 공개소스등을 참고하여 만들었습니다.

사용자 삽입 이미지

[키보드 필터 드라이버 UI]



       - 키보드 필터 드라이버에서 참고할만한 사항은 아래와 같습니다. -

  • 필터 드라이버의 기본구조

  • 드라이버 동적로딩

  • IOCTRL을 통한 User Mode Program 과의 통신(키보드 스캔코드 출력)

  • 후킹 시작시 Ctrl2CapSurce와 같이 Caps Lock키를 Left Ctrl 키로 변경시켜 줌


          이후부터의 내용은 다른 분들의 글을 인용한 부분도 있습니다. 양해 부탁드립니다.

  1. SW 설치
    1. 필수 - DDK (NT, 2000, XP 등 해당 OS DDK)
    2. 옵션 - CompuwareDriverStudio

  2. 빌드 환경

    1. DDK를 통한 Build

      시작메뉴의 프로그램에서 설치된 DDK(Development Kits)에 가보면 Build Envrionment > Checked Build Environment, Free Build Environment 4개가 보인다. 그 중 하나 Checked Build Environment 를 클릭하면 필요한 환경변수 세팅 및 경로 세팅을 한 후 cmd창 하나가 실행된다. 이 상태에서 DDK 설치 폴더의 만만한 샘플 폴더(C:\WINDDK\src\input\kbfiltr)로 직접 이동해서 build라고 명령을 치면 Sources라는 파일(확장자 없음)을 참고하여 C파일들을 컴파일 및 링크까지해서 sys파일이 빌드되어진다.

      (Checked Build, Free Build 랑의 차이는 Debug/Release 차이랑 비슷하고, DDK Help 참조)

    1. VC++ 빌드 환경 사용법

      여러 방법이 있겠지만 제일 간단한 방법은 DriverStudio를 설치하면 생성되는 DDK sources to vcproj Converter(SrcToVcProj) 를 사용한다 .

      요걸 실행해서 Sources라는 파일을 Open, 그리고 메뉴의 Convert>Convert DDK dirs/sources를 실행해서 VC++6 Workspace를 선택해서 Convert를 누르면 해당 폴드에 DDK로 빌드 할 수 있게 환경설정이 된 dsw 파일이 생성된다. Sources파일을 Drag&Drop하면 바로 Convert Dialog가 뜬다. 우리가 새롭게 작성할 .c 파일을 특정 폴더에 만들고 Sources파일(DDKsrc 폴더 참고)을 생성하면 된다.

      우린 요걸 사용해서 Checked, Free 모드로 빌드하면 된다. 물론 VC 컴파일/빌드의 F7 단축키를 사용가능.

      빌드하기 전에 DriverStudio를 설치하면 생성되는 툴바 중 왼쪽에서 3번째 DDK Build Settings를 실행해서 DDK Root Dir을 설정해 주고 빌드.

      또 다른 빌드방법은 DriverStudio를 설치하면 생성되는 툴바 중 왼쪽에서 4번째 버턴(Build With DDK..)으로 할 수 있다.

      이 버턴이 활성화 되게 하려면 SrcToVcProj를 사용하여 dsw를 만들 때 해당 폴더에 makefile 파일(DDKsrc 폴더 참조)이 있으면 활성화 되어 컴파일 할 수 있다.

      , Windows 2000 Device Driver, Windows Driver Model 책의 awx 파일로 프로젝트를 만들어 빌드할 수 있지만 환경이 잘 맞지 않아 빌드가 잘 안되었음.

      SoftICE 환경 설정 및 사용법

  1. SoftICE 환경 설정 및 사용법

    1. 초기 SoftICE Windows 설정한다

      "SoftICE Initiallization Settings..." ->"Initialization string" 에 다음과 같이 세팅한다

      X; SET FONT 2; LINES 60; WL 10; WC 35;

      x : 디버거 모드 종료

      set font 2 : 2번째 폰트를 사용

      lines : 줄 수를 50 라인

      wc : 코드 윈도우 줄 수는 35라인.

      wl : 로컬변수 윈도우 줄 수는 10라인

    1. SoftICE 실행한다 – Start SoftICE

      - cmd 창이 떴다가 사라진다.

    2. Symbol Loader를 실행해서 대상 모듈을 “open”하고 심볼을 “load”한다.

      Open할때 파일 형식을 *.*으로 ㅎ고 해당 .sys파일을 선택한다.

    3. Ctrl + D를 눌러 SoftICE 디버거 창을 띄운다.

    4. 소스 파일을 연다.

      FILE * : 모듈에 관계된 모든 소스파일의 목록을 보여 준다.

      FILE test.c : test.c 파일의 내용을 코드 윈도우에 표시한다.

    1. F6키를 눌러 코드 윈도우로 이동한 다음 원하는 위치에서 F9를 눌러 브레이크 포인트를 설정한다.

      BL : 브레이크 포인트 목록을 표시한다.

      BC * : 모든 브레이크 포인트를 삭제한다.

      U 210 : 210번째 라인으로 이동

      T : step into(F8)

      P : step over(F10)

      G : 실행을 계속한다.

      F7 : excute to here

      WD : data window를 표시

      WW : watch window를 표시

      WATCH xx : 특정 xx 변수 값을 watch window에 표시

      Alt+C : 커서를 코드 윈도우로 이동

      Alt+L : 커서를 로컬변수 윈도우로 이동

      Alt+W : 커서를 watch window로 이동

      Enter : 로컬변수 창등에서 구조체의 내부를 표시

      ww : Watch창을 보이게 함.

      wl : 지역변수창을 보이게 함.

    2. 다시 Ctrl + D를 눌러 디버거 창을 닫는다.

      - 런터임시 BP가 걸리면 디버거 창이 뜬다. 위 단축키로 디버깅을 해보아라.


[키보드 필터 드라이버 소스]


Posted by skensita
Programming/Kernel / Driver2008. 10. 10. 11:17

Visual Studio 6.0 + vmware + windbg 개발환경 설정 정리 문서
Posted by skensita