본문 바로가기

My Study/Programming&Theory

2가지 DPC 루틴

 디바이스 드라이버를 만들 때 우리는 DPC루틴을 많이 사용합니다.
interrupt가 발생했을 때, 또는 KeSetTimerEx를 사용해 일정 시간마다 모듈을 호출할 때 .. 등등이 있죠.
저도 많은 경우 사용해 보질 않아서 예를 많이 들진 못하겠네요.

이 때 DPC루틴 호출을 할 때 DPC 루틴을 DPC 큐에 넣게 되는데 이러한 DPC 루틴의 종류는 2가지가 있습니다.

1. CustomDpc
이 Dpc루틴은 KDEFERRED_ROUTINE 형태를 따라야 합니다.
VOID CustomDpc(
  __in      struct _KDPC *Dpc,
  __in_opt  PVOID DeferredContext,
  __in_opt  PVOID SystemArgument1,
  __in_opt  PVOID SystemArgument2
)
{ ... }
이런식으로 만들어 주면 되죠.

이 DPC 루틴을 사용할 때는 KeInitializeDpcKeInsertQueueDpc 이 2가지 함수를 사용하게 됩니다.
VOID KeInitializeDpc(
  __out     PRKDPC Dpc,
  __in      PKDEFERRED_ROUTINE DeferredRoutine,
  __in_opt  PVOID DeferredContext
);
Dpc 오브젝트를 초기화 해주는 함수입니다.
BOOLEAN KeInsertQueueDpc(
  __inout   PRKDPC Dpc,
  __in_opt  PVOID SystemArgument1,
  __in_opt  PVOID SystemArgument2
);
초기화된 Dpc 오브젝트 안에 있는 CustomDpc를 요청하는 함수입니다.
함수 이름만 봐도 CustomDpc를 DPC큐에 넣는 작업을 하는 함수인 것을 알수 있습니다.

이 CustomDpc는 커널이 제공하는 함수이고 일반적으로 사용되는 DPC 루틴입니다.

2. DpcForIsr 
이 Dpc루틴은 IO_DPC_ROUTINE 형태를 따라야 합니다.
VOID DpcForIsr(
  __in      PKDPC Dpc,
  __in      struct _DEVICE_OBJECT *DeviceObject,
  __in      struct _IRP *Irp,
  __in_opt  PVOID Context
)
{ ... }
위와 같이 되죠.

이 DPC 루틴을 사용할 때는 IoInitializeDpcRequestIoRequestDpc 두 함수를 사용하게 됩니다.
앞에 Io 붙은걸로봐서 해당 함수들은 IO관리자가 제공하는 함수라는 것을 알수 있습니다.
VOID IoInitializeDpcRequest(
  __in  PDEVICE_OBJECT DeviceObject,
  __in  PIO_DPC_ROUTINE DpcRoutine
);
Dpc루틴을 호출하기 위한 초기화 작업
VOID IoRequestDpc(
  __in  PDEVICE_OBJECT DeviceObject,
  __in  PIRP Irp,
  __in  PVOID Context
);
초기화된 Dpc루틴을 Dpc큐에 넣는다.

이 DPC루틴의 사용 용도는 이렇습니다.
보통 하드웨어에서 CPU에게 처리를 요구할 때 interrupt라는 것을 보냅니다.
이 때 interrupt를 받은 CPU는 현재 작업 중인 것을 중단하고 해당 interrupt를 처리하게 됩니다.
보통 하드웨어 interrupt의 IRQL은 3 이상이므로 스케쥴러보다 더 우선권을 가지고 있습니다.

그러면 어느정도 interrupt도 양심이 있으면 최대한 빨리 자기 할 것만 하고 다시 CPU를 반납해야 합니다.
그렇지 않으면 아마 해당 interrupt가 발생할 때마다 컴퓨터가 버벅 댈 것입니다.
그렇기 때문에 해당 interrupt가 발생했을 때 정말 그 시기 꼭 해야하는 작업이 아니면
DPC루틴을 만들어 해당 DPC루틴을 DPC큐에 넣어버립니다. 이러면 모든 하드웨어 interrupt 처리가 끝났을 때
DPC 처리기가 DPC큐에 있는 루틴들을 처리 할 것입니다.

이 때 사용되는 루틴이 바로 DpcForIsr 입니다.
참고로 IoInitializeDpcRequest 함수는 PASSIVE_LEVEL에서 호출되어야 하며 IoRequestDpc 함수는 DIRQL에서 호출되어야 합니다. DIRQL은 IRQL 0~31 중에서 3~26 값을 뜻합니다. 즉, 하드웨어 인터럽트 루틴에서 사용해야 된다는 말입니다.
또한 DPC는 2번 이상 요청이 이뤄질 수 없습니다. 즉IoRequestDpc함수가 호출 됬을 때 DPC큐에 들어간 DPC 루틴이 처리가 안된 상황에서 IoRequestDpc함수가 또 호출 될 경우 나중에 등록된 DPC루틴은 유효하지가 않습니다.

아무튼 이 경우에 사용되는 루틴이지만 다른 경우에도 사용될것 같습니다. 이 부분은 잘 모르므로 패스... =_=;
보통 IDT후킹을 해 키로거를 만드는 경우 DpcForIsr을 필히 사용해야 합니다. 그렇지 않을 경우..!
키보드로 글씨를 쓰면 컴퓨터가 버벅대는 현상을 볼 수 있습니다. :)

웃긴건 IoInitializeDpcRequest 함수 내부에서는 KeInitializeDpc 함수를 사용하고 있고 IoRequestDpc 메크로 내부에서도 KeInsertQueueDpc 함수를 사용하고 있습니다.



추가적으로 PS/2 키로거를 대충 만들어 보고 있는데 IDT 후킹을 한 후 후킹된 함수 내부로 들어오게 되면 그 때 IRQL은 2 값을 가지게 됩니다. DISPATCH_LEVEL인거죠. 보통 거기서 IoRequestDpc 함수를 호출하는데 IoRequestDpc함수는 DIRQL에서 호출되어야 안정되게 작동합니다. 싱글 코어 기반 환경에서는 무리 없이 잘 돌아가지만 듀얼 코어 이상 즉, SMP환경에서 IoRequestDpc함수를 호출하게 되면 일정 확율로 IRQL_NOT_LESS_OR_EQUAL : BSOD가 뜨게 됩니다.
이 현상 때문에 키로거를 제대로 못 만들고 있습니다. 
 혹시 이에대한 해결방안이나 아이디어가 있으시다면 도움 좀 부탁드리겠습니다~ ^^;

감사합니다.