계층 드라이버

Windows Driver Model은 계층 드라이버 아키텍처를 갖습니다. 간단하게 말하면, 여러 개의 드라이버로 이루어진 드라이버 체인에 새로운 드라이버를 끼워 넣을 수 있다는 말입니다.

거의 모든 하드웨어 장치마다 그것을 지원하기 위한 드라이버 체인이 존재하게 되는데, 드라이버 체인에서 가장 낮은 계층의 드라이버는 하드웨어 장치와 버스를 직접 처리하고, 가장 높은 계층의 드라이버는 데이터를 구조화 하고 에러 코드를 처리하며, 요청된 명령을 다음 계층의 드라이버로 전달하기 위해 좀 더 하드웨어에 연관된 명령으로 구체화 시킵니다.

Windows 에서는 하드웨어와 드라이버 그리고 어플리케이션이 이러한 구조를 갖고 통신을 하기 때문에, 키보드 데이터를 가로채고 싶다면 우리가 할 일은 키보드 드라이버 위의 계층에 우리가 만든 드라이버를 올리기만 하면 되는 것입니다.

실제로 키보드 드라이버를 제작하여 우리가 원하는 결과를 얻어 낼 수도 있겠지만, 이 방식을 채택할 경우, 키보드 장치, 즉 하드웨어 디바이스에 대한 정보가 필요하며, 이러한 정보를 토대로 데이터의 가공 뿐만이 아니라 실제 디바이스의 제어까지 개발하여야 하므로 훨씬 어렵고 힘든 작업이 됩니다.

계층 드라이버에서 가로채는 키보드 데이터는 하드웨어 디바이스 드라이버에서 이미 IRP 형태로 변환된 데이터 입니다.

우리가 할 일은 아주 간단합니다. 실제 디바이스를 생성하고, 해당 디바이스 그룹에 생성한 디바이스를 추가하는 것입니다.

먼저 키보드에 값이 입력되었을 때, 즉 키보드를 눌렀을 때 이 정보가 어떻게 처리되는 가를 살펴보겠습니다.

1. 사용자가 키보드를 누르면 입력된 키보드 데이터를 읽기 위한 읽기 요청이 발생합니다.

2. 이 요청에 대해 IRP가 생성됩니다.

3. 생성된 IRP는 디바이스 체인을 타고 내려가서 8042 controller (keyboard controller 입니다.) 에 도착합니다.

4. 8042 드라이버가 키보드 버퍼에서 키보드 데이터를 가져오면 그에 해당하는 scan code IRP에 넣습니다. (scan code는 키보드의 어느 키가 눌렸는지를 나타내는 숫자입니다)

5. IRP는 다시 디바이스 체인을 거슬러 상위 계층으로 이동합니다.

이런 순서를 거쳐서 키보드 데이터가 처리되게 되는데, 우리가 이 곳에서 데이터를 가로챌 수 있는 곳은 두 곳이 있습니다. 바로 3 , 5번 입니다. 즉 간단히 정리하면, IRP가 한 계층에서 다른 계층으로 이동하게 될 때 그 IRP를 변경하거나, 그에 대해 다른 작업을 수행할 수 있다는 것입니다.

이런 것이 가능한 이유는 Windows Driver Model 자체가 계층화 아키텍처를 채택하고 있으며, IRP는 한 디바이스 체인에 속해 있는 모든 드라이버에 전달되기 때문입니다.

새로운 명령이 요청되면 I/O 매니저는 그것을 위한 새로운 IRP를 생성합니다. 이 IRP에는 IO_STACK_LOCATION 이라는 공간이 있는데, IRP 헤더에는 이 배열의 현재 인덱스 값을 저장하고 있습니다. 이 IO_STACK_LOCATION 은 각각의 드라이버를 가리키고 있습니다.

드라이버가 다음 계층의 드라이버로 IRP를 전달할 때는 “IoCallDriver” 루틴을 사용합니다.

IoCallDriver 루틴이 가장 먼저 수행하는 작업은 IRP의 Current Stack Location 값을 하나 줄이는 것입니다.

기본적으로 필터 드라이버는 아래 계층의 드라이버와 동일한 Major Function을 제공해야 합니다.

정확하게 동일한 Major Function을 제공하는 여러 방법이 있으나, 주로 자신이 필요한 함수를 제외하고는 Irp를 skip 하는 방법을 사용합니다.

 

IoSkipCurrentIrpStackLocation() 의 경우 stack location 주소를 증가 시킵니다. 그러나 IoCallDriver에서 다시 Stack 주소를 감소 시키기 때문에, stack 주소는 그대로 보존 됩니다.

( C 언어 코딩을 할 때 if 나 while, for 문을 사용할 때 { } 를 사용하지 않는 분들이 있는데, 디바이스 드라이버 개발 시에는 매우 위험합니다. 몇몇 함수들은 매크로로 정의되어 있는데 예를 들면

If(어느조건)

매크로함수()

이런 경우에 매크로 함수의 구성이

매크로함수 동작 1

매크로함수 동작 2

이런 식으로 연속적으로 구성되어 있는 경우 if 에 중괄호가 없다면, 매크로 함수 동작 1만 수행되기 때문에 치명적인 오류를 발생시킬 수 있습니다. ( 드라이버의 경우 Blue Screen! )

키보드 필터 드라이버

다른 모든 드라이버와 마찬가지로 드라이버 엔트리 루틴이 가장 먼저 호출됩니다.

드라이버 엔트리에서 중요한 부분을 차지하고 있는 Major Function Array 에 함수를 설정해 줍니다. 이 예제에서는 Read 와 Unload 만을 사용하기 때문에 나머지 Major Function 은 Skip 되도록 설정하여도 됩니다.

IoCreateDevice() 를 이용하여 Device Object를 생성합니다. 이 때 디바이스 네임은 NULL 즉, No NAME 입니다. 그리고 FILE_DEVICE_KEYBOARD로 설정합니다, (이에 대한 다른 값들은 DDK 참조)

새로 생성한 디바이스 오브젝트의 플래그 값을 설정합니다. 키보드는 BUFFERED_IO 를 사용합니다. 다른 플래그 값은 DeviceTree 툴을 이용하여 확인할 수 있습니다.

새로 생성한 디바이스는 디바이스 체인 상 KeyboardClass0 위에 위치할 것입니다. 이를 위해 IoAttachDevice()를 이용합니다. 한 단계 아래의 계층의 디바이스 오브젝트 주소는 deviceExtension->DeviceObject 에 저장합니다. 이 값은 IRP를 체인상의 다음 계층으로 전달할 때 사용합니다.

IRP를 처리하는 함수의 IRQ레벨은 DISPATCH 레벨이므로 파일 관련 처리가 불가능 합니다. 따라서 Worker Thread를 qufehh로 생성하여 획득한 키보드의 버퍼 데이터를 로그 파일에 저장 합니다. ( Worker Thread의 IRQ 레벨은 PASSIVE 레벨입니다. )

C:에 sig2log.txt 라는 파일을 만드는 과정입니다.

위의 과정을 모두 마치면, 우리의 드라이버는 키보드 디바이스 체인에 포함되게 되며, 사용자가 입력한 데이터를 IRP를 통해 전달 받게 됩니다. 키보드 데이터에 대한 READ 요청, 즉 Key 입력이 발생하게 되면, IRP_MJ_READ와 연결된 함수가 호출되게 됩니다.

키보드 컨트롤러로 READ 요청이 이뤄지면 이 함수가 호출되지만, 이 시점의 IRP는 키보드 데이터가 포함되어 있지 않습니다. 요청된 IRP의 처리가 모두 이뤄져야 IRP는 데이터를 포함할 수 있습니다. 즉 우리가 키보드 데이터를 획득할 수 있는 시점은 DispatchRead() 가 아니라 별도의 Completion Routine 이며, 이 루틴을 설정하지 않으면 처리 완료된 IRP를 전달 받지 못합니다. 위 소스에서 살펴보면 OnReadCompletion() 을 완료 루틴으로 설정하였습니다.

OnReadCompletion() 에서는 IRP의 상태값을 확인 후 STATUS_SUCCESS 이면 해당 IRP에 대한 처리가 성공적으로 이뤄져서 키보드 데이터를 포함하고 있음을 나타냅니다. 키보드 데이터는 IRP의 SystemBuffer 를 통해 전달되며 배열의 크기는 IoStatus.Information을 통해 전달됩니다. Completion Routine 도 DISPATCH_LEVEL 이므로 파일 처리가 허용되지 않습니다. 그래서 Linked List를 이용하여 Worker Thread에 데이터를 전달하고 Worker Thread에서 이 데이터에 대한 처리를 담당합니다. 이 Linked List에 대한 동기화를 구현하지 않으면 두 개의 thread가 동시에 접근할 수 있기 때문에 치명적인 문제가 발생할 수 있습니다. (어플리케이션과 다르게 드라이버는 바로 재부팅됩니다…)

Worker Thread의 루틴 중 한 부분입니다. 동기화를 위해 세마포어를 기다리고 Linked list에서 가장 처음 엔트리를 가져옵니다. 또한 커널 Thread는 외부에서 종료할 수 없고, 오직 자신에 의해서만 종료 될 수 있기 때문에 worker thread가 종료되어야 하는지 여부를 나타내는 플래그를 사용합니다.

Driver Unload 루틴입니다. IoDetachDevice() 를 이용하여 우리의 디바이스를 디바이스 체인에서 제거합니다.

Worker thread를 종료 할 수 있도록 플래그를 설정하고, Waitforxxx 로 블록된 상태를 해제합니다. 그리고 KeWaitForSingleObject를 이용하여 worker thread가 종료되기를 기다립니다.

파일을 닫고 마무리 작업을 수행합니다.

위 소스는 Clandestiny 라는 사람이 작성한 KLOG 를 참조하였으며, 키보드 필터 드라이버의 정석을 따르고 있는 간단한 소스라 채택하였습니다.

드라이버는 DriverMonitor 로 Load 한 후, 키보드 입력을 하면 C: 에 sig2log.txt 파일에 자신이 입력한 키보드 데이터를 저장합니다. 악용할 경우 강력한 기능을 수행할 수 있기 때문에 공부 목적으로만 사용합시다..

많은 경우가 그러하지만, 드라이버의 경우 자신이 직접 개발해 보지 않으면, 그 구조를 이해하는데 상당히 힘들어 집니다. 본 문서를 참조하여 프로젝트를 생성하여 문서에 나와있지 않은 여러 부분을 포함하여 직접 개발해 보는 것이 가장 좋을 것으로 생각됩니다. 이 키보드 필터 드라이버를 이해하면, 키보드 마우스 필터 드라이버는 손쉽게 작성할 수 있으며, 각각의 장치의 데이터를 가공, 변조 하는 것도 손쉽게 할 수 있습니다. USB 나 기타 장치의 경우는 프로토콜 자체가 복잡하고 알아야 될 주변 지식이 많기 때문에 손쉽게 할 수는 없지만, 그 아래 깔려 있는 기본 지식 자체는 동일 하기 때문에 많은 도움이 될 드라이버입니다.

Posted by wakira
,