Reversing/Kernel

[Window Device Driver] 3. IOCTL

IOCTL

  • Input / Output ConTroL
  • 응용 프로그램에서 장치 드라이버와 직접 통신할 수 있는 장치 입출력 제어 인터페이스
  • DeviceIoControl 함수에서 이를 제공
  • 제어코드 기반으로 드라이버에서 수행할 작업을 지정하여 응용프로그램이 이를 드라이버에 전송, 동작 수행을 제어할 수 있음.

DeviceIoControl

  • 컨트롤 코드를 다양한 장치로 보낼 수 있는 범용 인터페이스
  • 각 제어코드는 드라이버에서 수행할 작업을 나타냄
  • ioapiset.h → Windows.h 안에 포함
  • IRP (IO Request Packet)에 의해 전송됨.
    • 드라이버에선 DrirverObject→MajorFunction[] 에 의해 수신되어 동작 수행 정의함.
  • 양방향성을 가져 유저모드 어플리케이션과 커널 드라이버 모두에서 실행 가능.
BOOL DeviceIoControl(
  [in]                HANDLE       hDevice, //디바이스 핸들
  [in]                DWORD        dwIoControlCode, //명령을 위한 컨트롤 코드 
  [in, optional]      LPVOID       lpInBuffer, //입력 데이터 버퍼 포인터
  [in]                DWORD        nInBufferSize, //입력 데이터 버퍼 크기 
  [out, optional]     LPVOID       lpOutBuffer, //출력 버퍼 포인터
  [in]                DWORD        nOutBufferSize, //출력 데이터 버퍼 크기 
  [out, optional]     LPDWORD      lpBytesReturned, //출력 버퍼에 저장될 데이터 크기
  [in, out, optional] LPOVERLAPPED lpOverlapped //OVERLAPPED 구조체 포인터
);
  • IO Control Code 는 32bit로 구성되어있음.

  • Device Type : 장치 유형
  • Required Access : 해당 IOCTL 코드의 버퍼 권한 정의
    • FILE_ANY_ACCESS
    • READ_DATA
    • WRITE_DATA
  • Function Code : 동작 코드 번호
    • MS에서 0~2047까지의 번호를 미리 정의함
    • 커스텀시엔 2048(0x800)부터 사용
  • Transfer Type (Method): 드라이버 호출자와 드라이버간 데이터 전달 방법 정의
    • METHOD_BUFFERD
    • METHOD_IN_DIRECT,OUT_DIRECT
    • METHOD_NEITHER
  • CTL_CODE 매크로의 인자로 넘어갈땐 Type,Code,Method,Access 순으로 넘어감
  • IOCTL CODE는 유저 어플리케이션과 드라이버 모두에 동일하게 선언해주어야 똑같이 동작함.

IRP Major Function Codes

  • 유저모드 어플리케이션에서 실행되는 동작들에 대응하여 호출될 이벤트 콜백 함수 개념.
  • 각 정의된 이벤트 코드에 따라 대응하여 호출될 함수를 지정할 수 있다.
  • 몇가지 예시
    • CREATE : 핸들 등의 생성 관련 동작 대응
    • DEVICE CONTROL : IOCTL 이벤트(DeviceIOControl함수 동작)에 대응
    • CLOSE : 핸들 등을 닫는 동작에 대응
  • 예를 들어, 유저모드 어플리케이션에서 핸들을 생성하여 DeviceIOControl을 보낸다고 하면 드라이버에서는 CREATE와 DEVICE CONTROL 에 대응할 함수를 준비해야 한다.

IOCTL 예제 프로그램 구상

위 내용을 정리하자면,

  1. 유저모드 어플리케이션과 장치 드라이버는 IOCTL 인터페이스를 통해 통신
  2. 장치 드라이버에는 유저모드 어플리케이션에서 일어나는 동작 이벤트에 대응하는 IRP_MJ 함수들이 정의됨
  3. IRP_MJ 함수 중 DEVICE_CONTROL 함수에 의해 IOCTL 통신 이벤트에 대응할 동작을 정의할 수 있음.

따라서, 서로 통신하는 두 프로그램을 만들려면 아래 내용들이 들어가야 한다.

  1. 유저모드 어플리케이션에서는 드라이버에 보낼 IOCTL 코드를 생성하고 이를 DeviceIoControl 함수로 보냄
  2. 장치 드라이버에서는 이에 대응할 IRP_MJ_DEVICE_CONTROL 함수를 정의

최소한의 동작만 수행하는 예제 프로그램을 만들어보자.

UserApp

  • 유저모드 프로그램에서는 드라이버에 간단한 텍스트 메시지를 전송한다
  • 성공적으로 메시지를 전송한 다음, 드라이버에서 온 응답을 출력한다.
    • 우선은 메시지 송수신 대신 성공 로그만 띄우자

동작을 위해 다음과 같은 순서로 프로그램이 진행된다.

  1. IOCTL 코드 매크로 선언
  2. CreateFile 함수 실행하여 장치 핸들 반환
    • 여기서 드라이버의 심볼릭 링크를 인자로 받아 핸들을 생성한다.
  3. DeviceIoControl 함수 실행하여 장치 핸들에 대해 IOCTL 코드와 메시지 전송 및 반환값 받아옴
  4. 받아온 반환값 출력

Driver

  • 드라이버에서는 유저모드 프로그램으로부터 메시지를 수신한다.
  • 수신한 메시지를 드라이버 디버깅 메시지로 출력한다.
    • 우선은 메시지 송수신 대신 성공 로그만 띄우자
  • 유저모드 프로그램에 응답을 전송한다.

동작을 위해 다음과 같은 순서로 프로그램이 진행된다.

  1. IoCreateDevice 함수 실행하여 PDRIVER_OBJECT 로 생성될 드라이버 오브젝트 지정
    • PDRIVER_OBJECT 가 첫 인자로 들어가 마지막 인자로 PDEVICE_OBJECT 포인터가 반환된다.
    • 드라이버에서 PDEVICE_OBJECT를 가지고 있어야 IO 관리자가 전달하는 IRP를 전달받을 수 있다.
  2. IoCreateSymbolicLink 함수 실행
    • PDEVICE_OBJECT의 DeviceName을 SymolicLinkName으로 유저모드 어플리케이션에 노출시켜 연결한다.
    • 심볼릭 링크는 유저모드 프로그램이 드라이버의 핸들(PDEVICE_OBJECT)를 더 쉽게 사용할 수 있게 해주는 역할을 한다.
    • 이름 인자는 유니코드 문자열로 전달되어야 함
    • 심볼릭 링크 인자로 전달될 경로는 DosDevices 하위경로
    • 디바이스 이름 인자로 전달될 경로는 Device 하위경로
  3. IRP Major 함수 대기
    • DriverUnload 대응 함수
      • 드라이버 언로드될 때 실행
      • 생성한 핸들과 오브젝트를 제거한다.
        • 이 경우는 생성된 DeviceObject, 심볼릭 링크를 제거해줘야함
        • 해당 값들을 변수로 넘길 수 없으니 전역변수 활용하여 넘겨주는게 좋음.
    • DEVICE_CONTROL 대응 함수
      • IRP 통신으로 전달받은 IOCTL 코드에 따라 동작을 수행한다.
      • IoGetCurrentIrpStackLocation() 함수 실행
        • 전달받은 irp 패킷 내부의 디바이스 스택 중 가장 최 상단에 위치한 구조체 포인터를 반환한다.
        • 현재 디바이스 스택의 가장 최근 위치(현재 위치)를 찾는 동작을 한다.
        • 해당 구조체 안에 IOCTL 컨트롤 코드 , inBuflength/Outbuflength 등의 정보들이 담겨있다.
      • 현재 스택위치에서 찾아온 IOCTL 컨트롤 코드에 따른 분기문 동작을 수행한다.
    • CREATE 대응 함수
      • CREATE 이벤트 발생시 대응하여 실행
      • CreateFile 함수 실행시에도 대응하여 호출된다.
      • 선언하지 않으면 디폴트로 STATUS_INVALID_DEVICE_REQUEST 가 반환되어 Create 종류 함수가 제대로 동작하지 못한다.
        • 직접 확인해보자.
      • 함수 정상 동작시 STATUS_SUCCESS 를 반환하기 위해 호출된다.
    • IRP 패킷을 사용시에 공통적으로 IoCompleteRequest 함수가 항상 호출된다.
      • 전달된 IRP 패킷을 이용한 모든 I/O 요청 동작이 완료되었음을 알리는 역할을 한다.
      • I/O 매니저에 IRP를 반환한다.

빌드 및 실행

1. 기본형

각각 드라이버와 프로그램을 빌드하여 실행하였을때, 정상적인 IOCTL 이벤트가 실행되었다면 IOCTL EVENT 디버깅 메시지가 나올 것.

아주 좋다. CreateFile 함수에 의해 CREATE 이벤트까지 뜬게 보인다.

2. CREATE 오류 확인

이번엔 위에서 봤던 CREATE 대응이 정의되지 않은 경우는 어떻게 되는지 확인해보자.

 DriverObject->DriverUnload = UnloadMyDriver;
 DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = myIOCTL;
 //DriverObject->MajorFunction[IRP_MJ_CREATE] = createHandler;

다른건 다 그대로 두고, CREATE에만 주석을 씌워놓았다.

createfile이 제대로 되지 않아 핸들을 얻어오는데 실패하였다면 유저모드 어플리케이션에서 에러 메시지를 띄울 것이다.

핸들을 얻어오지 못해 에러메시지가 뜨는게 확인된다.

3. 메시지 상호 전달

이제 유저모드와 커널모드 간 메시지를 전달해보자.

  • 모드에서 넘어온 데이터는 Irp->AssociatedIrp.SystemBuffer 에 들어있다.
    • 받아와서 출력만 하면됨
  • 유저한테 넘길 데이터는 조금 복잡한 과정을 거친다
    • 정의한 IOCTL 코드의 TransferType에 따라 다른 방식으로 전달된다.
    • METHOD_BUFFERED 로 정의된 경우 outbuf도 Irp->AssociatedIrp.SystemBuffer 를 통해 전달된다.
    • 문자열이 복사되면 IOCTL 종료시에 I/O 매니저에 의해 자동으로 넘어가는 방식.
    • Irp->IoStatus.Information 에는 irp에서 넘어온 문자열 길이와 실제 복사한 문자열 길이를 비교하여 더 큰쪽을 넣어줘야한다.

출력을 해봤는데, 길이는 정상적으로 넘어왔지만 문자열은 한글자씩만 나오는 상황이 발생했다.

원인을 분석하기 위해 배열에서 한글자씩 뽑아보게 해보니 아래와 같은결과가 출력된다.

OutMsgLength = (ULONG) wcslen(OutMsg)*2 +1;
//유저로 넘길 메시지 
OutBuf = (PWCHAR) irp->AssociatedIrp.SystemBuffer;
//메시지 복사 
RtlCopyBytes(OutBuf, OutMsg, OutBufLength);
//전달받은 outbufLen과 실제 전달될 문자열의 길이 비교하여 전달될 메시지 길이 조정
irp->IoStatus.Information = (OutBufLength < OutMsgLength? OutBufLength : OutMsgLength);

중간중간 한글자씩 공백이 생겨서 나오는걸로 보이는데, 원인은 유저모드 어플리케이션에서 보낸 문자열 자료형이 WCHAR이기 때문.

와이드타입 문자열이어서 한글자가 2바이트씩 차지하고, 1바이트 영문자 다음은 공백처리 되어버려서 PCHAR로 수신한 문자열이 한글자씩밖에 인식하지 못하는 것.

inbuf = (PWCHAR) irp->AssociatedIrp.SystemBuffer;
DbgPrintEx(DPFLTR_IHVDRIVER_ID, 0, "data from user : %S\n",inbuf);

inbuf 변수를 와이드타입으로 선언하고 캐스팅도 마찬가지로 해준다음 %S 서식문자로 출력하면 메시지가 잘 나온다.

다만 이때 유저모드에서 문자열 길이를 딱 스트링만큼 정해놨기 때문에, 와이드타입 특성상 전체 길이의 1/2밖에 못나온다. 유저모드에서 InputBufferLength를 원래 문자길이의 두배로 지정해줘야 제대로 갈듯.

두배로 늘려주니까 잘 나온다.

마찬가지로, 드라이버에서 유저모드에 메시지를 보내주는 동작도 수행해보자.

OutMsgLength = (ULONG) wcslen(OutMsg)*2 +1;
//유저로 넘길 메시지 
OutBuf = (PWCHAR) irp->AssociatedIrp.SystemBuffer;
//메시지 복사 
RtlCopyBytes(OutBuf, OutMsg, OutBufLength);
//전달받은 outbufLen과 실제 전달될 문자열의 길이 비교하여 전달될 메시지 길이 조정
irp->IoStatus.Information = (OutBufLength < OutMsgLength? OutBufLength : OutMsgLength);

유저모드 프로그램에서 와이드 타입으로 받을거니 PWCHAR로 캐스팅해줘야 하고, 동일하게 시스템 버퍼에 값을 복사해주면 잘 간다.


Github

https://github.com/synod2/Kernel_Hooking/tree/main/Window%20Device%20Driver/MyDriver2

 

GitHub - synod2/Kernel_Hooking: windows Kernel Hooking

windows Kernel Hooking. Contribute to synod2/Kernel_Hooking development by creating an account on GitHub.

github.com


참고 문서

https://docs.microsoft.com/ko-kr/windows/win32/devio/device-input-and-output-control-ioctl- MSDN IOCTL

 

장치 입력 및 출력 제어 (IOCTL) - Win32 apps

DeviceIoControl 함수는 응용 프로그램에서 장치 드라이버와 직접 통신할 수 있는 장치 입력 및 출력 제어 (IOCTL) 인터페이스를 제공 합니다.

docs.microsoft.com

https://docs.microsoft.com/ko-kr/windows/win32/api/ioapiset/nf-ioapiset-deviceiocontrol - MSDN DeviceIoControl

 

DeviceIoControl function (ioapiset.h) - Win32 apps

Sends a control code directly to a specified device driver, causing the corresponding device to perform the corresponding operation.

docs.microsoft.com

https://ezbeat.tistory.com/286 - IOCTL

 

DeviceIoControl로 디바이스와 연결

이번에 해볼 내용은 유저어플에서 DeviceIoControl함수를 사용해 로드된 디바이스와 통신을 하는것입니다. 가장 간단하게 유저어플에서 해당 함수를 호출해 디바이스를 작동시켜보는 것만 해보겠

ezbeat.tistory.com

https://shhoya.github.io/windows_ioctlcode.html - IOCTL Code

 

I/O Control Code | Shh0ya Security Lab

I/O Control Code [0x00] Concept I/O Control Code(IOCTLs) 는 유저모드 애플리케이션과 드라이버 간의 통신 또는 드라이버 스택 내 드라이버 간의 내부 통신에 사용됩니다. I/O Control Code 는 IRP 를 이용하여 전송

shhoya.github.io

https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/irp-major-function-codes - IRP Major Function Codes

 

IRP Major Function Codes - Windows drivers

IRP Major Function Codes

docs.microsoft.com

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-iocreatedevice - IoCreateDevice

 

IoCreateDevice function (wdm.h) - Windows drivers

The IoCreateDevice routine creates a device object for use by a driver.

docs.microsoft.com

https://heroeskdw.tistory.com/entry/함수IoCreateDevice - IoCreateDevice

 

[함수]IoCreateDevice

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 NTSTATUS IoCreateDevice(     _In_  PDRIVER_OBJECT DriverObject,     _In_  ULONG DeviceExtensionSize,     _In_opt_ PUNICODE_STRING DeviceNam..

heroeskdw.tistory.com

https://heroeskdw.tistory.com/entry/함수IoCreateSymbolicLink - IoCreateSymbolicLink

 

[함수]IoCreateSymbolicLink

1 2 3 4 5 NTSTATUS IoCreateSymbolicLink(     _In_ PUNICODE_STRING SymbolicLinkName,     _In_ PUNICODE_STRING DeviceName     ); Colored by Color Scripter cs [파라미터] SymbolicLinkNam..

heroeskdw.tistory.com

https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-mmgetsystemaddressformdlsafe - MmGetSystemAddressForMdlSafe

 

MmGetSystemAddressForMdlSafe - Windows drivers

Learn more about: MmGetSystemAddressForMdlSafe

docs.microsoft.com