Windows 커널 & 드라이버 시리즈 — 13화
IRP 디스패치 루틴 구현 — DeviceIoControl 완전 정복
드라이버와 앱이 대화하는 가장 일반적인 방법이 DeviceIoControl이에요. 앱이 "이 명령을 실행해줘"라고 보내면 드라이버가 처리하는 구조인데, 커스텀 IOCTL 코드를 정의하는 방법부터 드라이버에서 받아서 처리하는 것까지 예제와 함께 살펴볼게요.
IOCTL 코드란?
IOCTL(I/O Control)은 앱이 드라이버에 임의의 명령을 보내는 메커니즘이에요. 파일 읽기/쓰기처럼 정해진 동작이 아니라, 드라이버가 정의하는 커스텀 명령이에요. 예를 들어:
- 안티바이러스 드라이버: "이 프로세스 정보 알려줘"
- GPU 드라이버: "이 그래픽 명령 실행해줘"
- 디스크 드라이버: "이 섹터 암호화해줘"
IOCTL 코드는 32비트 값이고, CTL_CODE 매크로로 만들어요:
// 공유 헤더 파일 (드라이버와 앱이 둘 다 include) #define IOCTL_MYDRV_GET_VERSION CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_MYDRV_DO_SOMETHING CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS) // CTL_CODE(DeviceType, Function, Method, Access) // DeviceType: 디바이스 종류 (0x8000 이상이면 커스텀) // Function: 명령 번호 (0x800 이상이면 커스텀) // Method: 버퍼 전달 방식 // Access: 필요한 접근 권한Method (버퍼 전달 방식)
| Method | 특징 | In 버퍼 | Out 버퍼 |
|---|---|---|---|
| METHOD_BUFFERED | 가장 간단하고 안전. 커널이 자동으로 복사 | SystemBuffer | SystemBuffer |
| METHOD_IN_DIRECT | 입력: 커널 복사, 출력: MDL 직접 매핑 | SystemBuffer | MdlAddress |
| METHOD_OUT_DIRECT | 입력: MDL, 출력: 커널 복사 | MdlAddress | SystemBuffer |
| METHOD_NEITHER | 둘 다 유저 포인터 직접 사용 (위험) | Type3InputBuffer | UserBuffer |
💡 처음엔 METHOD_BUFFERED만 쓰세요
METHOD_BUFFERED는 커널이 알아서 버퍼를 복사해주기 때문에 실수할 여지가 가장 적어요. 성능이 중요한 드라이버에서는 다른 방식을 고려하지만, 입문 단계에서는 BUFFERED로 충분합니다.
드라이버 쪽 구현 — DeviceIoControl 처리 루틴
NTSTATUS DispatchIoControl( _In_ PDEVICE_OBJECT DeviceObject, _In_ PIRP Irp) { UNREFERENCED_PARAMETER(DeviceObject); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); ULONG ioControlCode = stack->Parameters.DeviceIoControl.IoControlCode; PVOID buffer = Irp->AssociatedIrp.SystemBuffer; // METHOD_BUFFERED ULONG inBufLen = stack->Parameters.DeviceIoControl.InputBufferLength; ULONG outBufLen = stack->Parameters.DeviceIoControl.OutputBufferLength; NTSTATUS status = STATUS_SUCCESS; ULONG_PTR info = 0; switch (ioControlCode) { case IOCTL_MYDRV_GET_VERSION: { if (outBufLen < sizeof(ULONG)) { status = STATUS_BUFFER_TOO_SMALL; break; } ULONG version = 0x00010000; // v1.0 RtlCopyMemory(buffer, &version, sizeof(ULONG)); info = sizeof(ULONG); break; } case IOCTL_MYDRV_DO_SOMETHING: { // 입력 데이터 처리 예시 if (inBufLen < sizeof(MY_INPUT_STRUCT)) { status = STATUS_INVALID_PARAMETER; break; } PMY_INPUT_STRUCT input = (PMY_INPUT_STRUCT)buffer; DbgPrint("[MyDrv] Got command: %d\n", input->Command); // ... 처리 로직 break; } default: status = STATUS_INVALID_DEVICE_REQUEST; break; } Irp->IoStatus.Status = status; Irp->IoStatus.Information = info; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }앱 쪽 구현 — DeviceIoControl 호출
// 유저 모드 앱 #include <windows.h> #include "shared.h" // IOCTL 코드 공유 헤더 int main() { HANDLE hDevice = CreateFile( L"\\\\.\\MyDevice", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDevice == INVALID_HANDLE_VALUE) { printf("Open failed: %d\n", GetLastError()); return 1; } // IOCTL_MYDRV_GET_VERSION 호출 ULONG version = 0; DWORD returned = 0; BOOL ok = DeviceIoControl( hDevice, IOCTL_MYDRV_GET_VERSION, // IOCTL 코드 NULL, 0, // 입력 버퍼 (없음) &version, sizeof(version), // 출력 버퍼 &returned, NULL); // NULL = 동기 I/O if (ok) { printf("Driver version: 0x%08X\n", version); } CloseHandle(hDevice); return 0; }DriverEntry에서 DeviceIoControl 핸들러 등록
// DriverEntry에 추가 DriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose; DriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose; DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoControl; // ← 추가입력 검증을 항상 하세요
유저 모드에서 넘어온 버퍼 크기와 내용은 절대 믿으면 안 돼요. 악의적인 앱이나 버그 있는 앱이 이상한 값을 보낼 수 있기 때문에, 드라이버는 방어적으로 작성해야 합니다:
- 입력 버퍼 길이가 기대하는 구조체 크기 이상인지 확인
- 출력 버퍼 길이가 돌려줄 데이터보다 크거나 같은지 확인
- 포인터 필드가 있다면 ProbeForRead/ProbeForWrite로 접근 가능한지 검증
- 정수 오버플로우 가능성 고려 (예: Length + Offset이 최대값을 넘는 경우)
⚠️ 커널 드라이버 취약점의 대부분이 입력 검증 미흡이에요
권한 상승(Privilege Escalation) 공격의 많은 부분이 드라이버의 IOCTL 핸들러에서 입력을 제대로 검증하지 않아 발생해요. 방어적 프로그래밍 습관을 처음부터 들이세요.
✅ 13화 요약
- IOCTL 코드는 CTL_CODE 매크로로 정의하고, 드라이버와 앱이 공유 헤더로 공유합니다.
- METHOD_BUFFERED가 가장 간단하고 안전한 버퍼 전달 방식이에요.
- IoGetCurrentIrpStackLocation으로 IOCTL 코드와 버퍼 크기를 꺼내고, switch로 분기합니다.
- 입력 버퍼 크기와 내용을 항상 검증하는 것이 보안의 기본입니다.
다음 화 — 드라이버 개발의 최대 난관!
14화 — IRQL 완전 정복: Interrupt Request Level의 모든 것 →
#DeviceIoControl #IOCTL #CTL_CODE #디스패치루틴 #입력검증
'Programming > 7. Device Driver' 카테고리의 다른 글
| 드라이버 개발 핵심 - 15화 스핀락과 동기화 기법 (0) | 2026.06.03 |
|---|---|
| 드라이버 개발 핵심 - 14화 IRQL 완전 정복 (0) | 2026.06.02 |
| 드라이버 개발 핵심 - 11화 디바이스 오브젝트와 디바이스 스택 (0) | 2026.05.29 |
| 드라이버 개발 핵심 - 10화 DriverEntry와 드라이버 오브젝트 (0) | 2026.05.28 |
| 드라이버 개발 핵심 - 9화 첫 번째 커널 드라이버 (0) | 2026.05.27 |