Development/Windows

Windows I/O Manager

geunyeong 2021. 11. 19. 23:25

Table of Contents

    Abstract

    Windows는 다양한 외부 디바이스와 Windows 시스템 상에서 동작하는 유저 모드 어플리케이션나 운영체제, 외부 디바이스간 원활하고 통일된 통신 방식을 위해 I/O Manager를 통한 I/O 인터페이스를 제공하고 있다. I/O Manager는 유저 모드 어플리케이션이 요청한 I/O를 IRP라는 I/O 요청서로 패키징해 드라이버에게 전달하고, 드라이버는 요청 내용을 적절히 처리해 디바이스로 전달한다. 

    본 글에서는 이러한 I/O 과정에서 중요한 Windows 커널 요소들을 정리해봤다.

    Windows I/O Components

    I/O Manager

    EDR을 포함해 많은 보안 프로그램들은 파일 시스템에서 발생하는 이벤트들을 모니터링 할 필요가 있다. 파일 접근을 감시하는 방법으로 CreateFile 같은 파일 접근 API를 후킹하는 방법도 있지만 시스템 상의 모든 프로세스를 후킹해야 한다는 문제가 있고, API 후킹으로 인한 성능 저하도 문제가 된다. 또한 시스템 프로세스에 삽입한 후킹 DLL이 문제라도 일으킨다면 상상하기도 싫을 것이다. 결국 Windows 커널의 힘을 빌리는 게 최선이다.

    컴퓨터는 외부 세계와의 상호작용을 위해 다양한 외부 디바이스와 I/O를 주고받는다. 마우스, 키보드를 포함해 오디오, 모니터, 디스크, 네트워크 인터페이스 등 다양한 디바이스와 통신해야 하며, 디바이스와 운영체제 간 소프트웨어적 연결을 제공하는 것이 드라이버다. 유저 모드 어플리케이션이나 운영체제가 어떤 데이터를 보내면 이 데이터가 신호로 변환되고, 외부 디바이스와 연결된 버스로 내보내진다. 작업에 따라서는 외부 디바이스가 보낸 데이터를 받을 수도 있다. 이 과정에서 드라이버는 버스로 내보낼 적절한 신호를 만드는 소프트웨어라고 볼 수 있다. 때문에 드라이버를 만드는 사람에게 I/O는 매우 중요한 요소다.

    Windows 시스템의 커널에는 I/O 관리자라는 요소가 있다. Windows 시스템에서 발생하는 모든 I/O는 I/O 관리자에 의해 IRP(I/O Request Packet)라는 구조체에 관련 데이터가 담기고 드라이버에게 전달된다. 드라이버는 IRP에 담긴 데이터를 보며 적절히 처리한 후 다시 I/O 관리자에게 반환한다. 이 과정을 I/O가 발생한 디바이스에 묶여있는 모든 드라이버와 반복한다. 그리고 최종적으로 유저 모드의 어플리케이션 혹은 외부 디바이스로 데이터를 전달한다. 만약 유저 모드 어플리케이션이 파일을 쓰는 작업을 했다면 파일 시스템 드라이버에게는 파일 이름과 디스크에 쓸 바이너리 데이터들이 전달될 것이다.

    유저 모드 어플리케이션과 서브 시스템, I/O 관리자 구조(MSDN)

    이처럼 I/O 관리자는 유저 모드와 디바이스 드라이버 인터페이스 간의 통신을 담당하는 Windows 커널 구성 요소다. 유저 모드 어플리케이션이 보낸 I/O 요청을 받아 디바이스 드라이버에 전달하고 그 결과를 받아 외부 디바이스로 전달하며, 외부 디바이스가 보낸 데이터를 다시 디바이스 드라이버에 전달하고 최종적으로 유저 모드 어플리케이션에게 반환한다. I/O 관리자는 통신과 더불어 드라이버 개발자가 드라이버를 만들기 위해 필요한 인터페이스를 제공하는 역할도 하는 셈이다.

    서브시스템에서 파일을 열 때 일어나는 과정(MSDN)

    IRP

    IRP는 I/O Request Packet이라는 뜻으로 외부 디바이스와 Windows 커널 간의 통신을 위한 데이터들이 모여있는 구조체다(정의는 wdm.h에 존재한다). I/O 관리자는 디바이스 드라이버로 전달되는 대부분의 요청은 IRP로 패키지하며, 이를 드라이버에게 전달한다. 각 디바이스마다 관련된 드라이버들의 집합인 디바이스 스택이 존재하며 I/O 관리자는 이 스택의 최상위 드라이버부터 IRP를 전달하는데, 스택의 최하단 드라이버까지 이 작업을 반복한다. 

    //
    // I/O Request Packet (IRP) definition
    //
    
    typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
        CSHORT Type;
        USHORT Size;
    
        //
        // Define the common fields used to control the IRP.
        //
    
        //
        // Define a pointer to the Memory Descriptor List (MDL) for this I/O
        // request.  This field is only used if the I/O is "direct I/O".
        //
    
        PMDL MdlAddress;
    
        // ...
    
        //
        // I/O status - final status of operation.
        //
    
        IO_STATUS_BLOCK IoStatus;
    
        // ...
    
        //
        // Stack state information.
        //
    
        CHAR StackCount;
        CHAR CurrentLocation;
    
        // ...
    
        //
        // Note that the UserBuffer parameter is outside of the stack so that I/O
        // completion can copy data back into the user's address space without
        // having to know exactly which service was being invoked.  The length
        // of the copy is stored in the second half of the I/O status block. If
        // the UserBuffer field is NULL, then no copy is performed.
        //
    
        PVOID UserBuffer;
    
        // ...
    
                struct {
    
                    // ...
    
                    union {
    
                        //
                        // Current stack location - contains a pointer to the current
                        // IO_STACK_LOCATION structure in the IRP stack.  This field
                        // should never be directly accessed by drivers.  They should
                        // use the standard functions.
                        //
    
                        struct _IO_STACK_LOCATION *CurrentStackLocation;
    
                        // ...
                    };
                };
    
                //
                // Original file object - pointer to the original file object
                // that was used to open the file.  This field is owned by the
                // I/O system and should not be used by any other drivers.
                //
    
                PFILE_OBJECT OriginalFileObject;
    
            } Overlay;
    
            // ...
    
        } Tail;
    
    } IRP;
    
    typedef IRP *PIRP;

    IRP에는 디바이스 스택의 몇번째에 위치한 상태에서 작업 중인지를 나타내는 CurrentStackLocation이나 접근하는 디바이스 혹은 파일의 이름이 담긴 FileObject 멤버가 존재한다. 이 외에도 드라이버가 작업 결과를 담는 IoStatus와 유저 모드 어플리케이션의  I/O 요청(생성, 읽기, 쓰기 등) 타입도 함께 들어있다. 만약 파일 생성이나 개방 과정을 모니터링 하고 싶다면 IRP_MJ_CREATE, 일기나 쓰기라면 IRP_MJ_READ, IRP_MJ_WRITE 요청에 대한 콜백을 작성하면 된다.

    IRP Major Function

    앞서 IRP는 Windows 시스템에서 발생한 I/O 요청에 대한 요청서같은 것이라고 했다. 때문에 어떤 작업에 대한 요청인지에 대한 정보도 담겨있다. 이렇게 주요 작업의 종류를 나타낸 걸 IRP Major Function Code라고 하고, 주요 작업에 대한 요청이 들어왔을 때 호출되는 콜백 함수를 IRP Dispatch Routines이라고 한다. IRP Major Function Code는 wdm.h에 정의되어 있다. 주요 Function Code는 IRP_MJ_CREATE(생성/개방), IRP_MJ_READ(읽기), IRP_MJ_WRITE(쓰기), IRP_MJ_QUERY_INFORMATION(속성 읽기), IRP_MJ_SET_INFORMATION(속성 설정, 파일 삭제 시에도 이 Code로 들어온다) 등이 있다.

    //
    // Define the major function codes for IRPs.
    //
    
    
    #define IRP_MJ_CREATE                   0x00
    #define IRP_MJ_CREATE_NAMED_PIPE        0x01
    #define IRP_MJ_CLOSE                    0x02
    #define IRP_MJ_READ                     0x03
    #define IRP_MJ_WRITE                    0x04
    #define IRP_MJ_QUERY_INFORMATION        0x05
    #define IRP_MJ_SET_INFORMATION          0x06
    #define IRP_MJ_QUERY_EA                 0x07
    #define IRP_MJ_SET_EA                   0x08
    #define IRP_MJ_FLUSH_BUFFERS            0x09
    #define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
    #define IRP_MJ_SET_VOLUME_INFORMATION   0x0b
    #define IRP_MJ_DIRECTORY_CONTROL        0x0c
    #define IRP_MJ_FILE_SYSTEM_CONTROL      0x0d
    #define IRP_MJ_DEVICE_CONTROL           0x0e
    #define IRP_MJ_INTERNAL_DEVICE_CONTROL  0x0f
    #define IRP_MJ_SHUTDOWN                 0x10
    #define IRP_MJ_LOCK_CONTROL             0x11
    #define IRP_MJ_CLEANUP                  0x12
    #define IRP_MJ_CREATE_MAILSLOT          0x13
    #define IRP_MJ_QUERY_SECURITY           0x14
    #define IRP_MJ_SET_SECURITY             0x15
    #define IRP_MJ_POWER                    0x16
    #define IRP_MJ_SYSTEM_CONTROL           0x17
    #define IRP_MJ_DEVICE_CHANGE            0x18
    #define IRP_MJ_QUERY_QUOTA              0x19
    #define IRP_MJ_SET_QUOTA                0x1a
    #define IRP_MJ_PNP                      0x1b
    #define IRP_MJ_PNP_POWER                IRP_MJ_PNP      // Obsolete....
    #define IRP_MJ_MAXIMUM_FUNCTION         0x1b

    IRP Dispatch Routine은 Driver Object의 MajorFunction 멤버에 등록한 IRP Major 작업이 들어왔을 때 호출되는 콜백 함수다. 함수 포인터 타입명은 PDRIVER_DISPATCH며 wdm.h에 정의되어 있다.

    //
    // Define driver dispatch routine type.
    // The default is that it can be called <= DISPATCH
    // because it might be called from another driver.
    // See also below.
    //
    
    _Function_class_(DRIVER_DISPATCH)
    _IRQL_requires_max_(DISPATCH_LEVEL)
    _IRQL_requires_same_
    typedef
    NTSTATUS
    DRIVER_DISPATCH (
        _In_ struct _DEVICE_OBJECT *DeviceObject,
        _Inout_ struct _IRP *Irp
        );
    
    typedef DRIVER_DISPATCH *PDRIVER_DISPATCH;

    Driver Object

    드라이버가 시스템에 로드되면 I/O 관리자는 해당 드라이버에 대한 드라이버 오브젝트를 만들어 드라이버의 main 함수 격인 DriverEntry 함수에 전달한다. 이러한 Driver Object는 유저 모드 어플리케이션과 통신하는데 사용되는 WDF 디바이스 등을 만들거나 드라이버가 언로드 될 때 호출할 Clean 함수를 등록하는 데 사용된다. 다만 파일 시스템 드라이버처럼 로드는 가능하지만 언로드는 불가능한 드라이버도 존재한다.

    드라이버 오브젝트는 DRIVER_OBJECT라는 일므의 구조체로 관리된다. 이는 wdm.h에 정의되어 있으며, 앞서 말했듯 드라이버 언로드 루틴을 등록(DriverUnload 멤버)하거나 IRP_MJ 루틴을 등록(MajorFunction 멤버)하는 데 사용된다. 

    typedef struct _DRIVER_OBJECT {
        CSHORT Type;
        CSHORT Size;
    
        //
        // The following links all of the devices created by a single driver
        // together on a list, and the Flags word provides an extensible flag
        // location for driver objects.
        //
    
        PDEVICE_OBJECT DeviceObject;
        
        // ...
    
        //
        // The driver name field is used by the error log thread
        // determine the name of the driver that an I/O request is/was bound.
        //
    
        UNICODE_STRING DriverName;
    
        // ...
    
        //
        // The following section describes the entry points to this particular
        // driver.  Note that the major function dispatch table must be the last
        // field in the object so that it remains extensible.
        //
    
        PDRIVER_INITIALIZE DriverInit;
        PDRIVER_STARTIO DriverStartIo;
        PDRIVER_UNLOAD DriverUnload;
        PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
    
    } DRIVER_OBJECT;
    typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;

    IRP Major Function을 등록하는 MajorFunction 멤버를 보면 배열임을 알 수 있다. IRP_MJ_MAXIMUM_FUNCTION은 27로 정의되어 있으며, 1을 더함으로써 총 28개 IRP Major Function(0~27)을 등록할 수 있다. MajorFunction의 멤버의 첫번재 요소(0번 인덱스)에 DRIVER_DISPATCH 타입 함수를 넣으면 IRP_MJ_CREATE 요청이 들어올 때 해당 함수가 불리게 된다.

    #include <ntddk.h>
    
    NTSTATUS
    MyIrpMjCreateRoutine(
    	_In_    PDEVICE_OBJECT device_object,
    	_Inout_ PIRP irp
    );
    
    NTSTATUS
    DriverEntry(
    	_In_ PDRIVER_OBJECT driver_object,
    	_In_ PUNICODE_STRING regisry_path
    )
    {
    	driver_object->MajorFunction[IRP_MJ_CREATE] = MyIrpMjCreateRoutine;
    	return STATUS_SUCCESS; 
    }

    Device Object

    Windows 시스템이 디바이스를 나타내기 위해 만드는 디바이스 오브젝트다. 하나 이상의 디바이스 오브젝트는 각 디바이스와 연결되는데, 디바이스 오브젝트의 역할은 각 디바이스에 대한 작업의 대상 역할을 한다. 기본적으로 커널 모드 드라이버는 작업을 담당할 디바이스에 대해 디바이스 오브젝트를 생성해야 한다(만들지 않아도 되는 일부 예외도 존재한다).

    디바이스 오브젝트와 I/O 관리자 간의 관계(MSDN)

    앞서 드라이버 오브젝트와 마찬가지로 디바이스 오브젝트 또한 DEVICE_OBJECT라는 이름의 구조체로 관리된다. 디바이스 오브젝트는 각 드라이버가 등록한 IRP_MJ 루틴이 호출될 때 해당 루틴의 파라메터로 IRP와 함께 전달된다. 

    typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
        CSHORT Type;
        USHORT Size;
        LONG ReferenceCount;
        struct _DRIVER_OBJECT *DriverObject;
        struct _DEVICE_OBJECT *NextDevice;
        struct _DEVICE_OBJECT *AttachedDevice;
        struct _IRP *CurrentIrp;
        // ...
        DEVICE_TYPE DeviceType;
        CCHAR StackSize;
        
        // ...
    
        struct _DEVOBJ_EXTENSION  *DeviceObjectExtension;
        PVOID  Reserved;
    
    } DEVICE_OBJECT;
    
    typedef struct _DEVICE_OBJECT *PDEVICE_OBJECT;