시간이 나서, Timer DPC에 관해서 정리해보고자 글을 쓴다.

  먼저 Timer DPC 라는 제목을 살펴보자. Timer와 DPC 2개로 분리가 된다. Timer는 뭔가 시간이 흐른다는것을 예상해볼 수 있고, DPC는 Deferred Procedure Call 의 약자로써, 지연된 처리의 호출이라고 해석할 수 있다. 사실 Timer의 의미는 대략적으로 예상해볼 수 있는데,  DPC는 뭐가 도대체 지연된 호출인가? 지연된 호출이 왜 필요하단 말인가? 하고 궁금증이 생길 수 있다.

  DPC를 이해하기 위해서는 IRQ 우선순위와 커널의 인터럽트 처리 메커니즘을 알고있어야 하는데, 일반적으로 외부 하드웨어에서 CPU에 인터럽트를 보내면 커널은 이를 인지하여 , 해당 하드웨어에 맞는 ISR을 적절히 선택하여 인터럽트 처리를 수행한다. 그런데 보통 이러한 인터럽트 서비스 루틴은 해당 하드웨어의 요청에 정말 필요한 크리티컬 코드들로만 짧게 구성되어져 있는데, 이는 어느 특정 인터럽트 서비스 루틴으로 인해, 다른 인터럽트 처리의 방해가 없도록 하기 위해서이다. 여기서 갑자기 방해가 왜 나올까? 라고 생각해볼수가 있는데, 여기에서 인터럽트 우선순위 레벨(IRQL) 을 생각해볼 수 있다. 예를 들어 시스템에서 특정 하드웨어가 있는데 , 이 하드웨어는 매우 크리티컬한 요소라서 최우선적으로 처리되어야 할 놈이라고 가정해보자. 이 하드웨어는 IRQL26 우선순위를 부여받고, ISR을 처리하게된다고 가정했을때,  ISR에서 빨리 끝내주지 않고 , 질질 끌게되면 다른 우선순위 낮은 하드웨어(예 : 키보드) 가 인터럽트 처리를 요청했을때, 우선순위 높은 애가 아직 CPU를 부여잡고 있으므로, 기다릴수 밖에 없을것이다.  키보드 빨리빨리 입력해야하는데 , 키보드가 드득드득 지연되면서 입력되는것을 상상해본적이 있는가? 
  그럼 이 문제를 해결하려면 어떻게 해야할까? 참 이런거 생각한사람들은 머리가 좋은것 같다. (어찌보면 단순한가?)  해결법은 ISR을 최대한 단순하고 중요한 코드들로만 구성하고, 만약에 ISR에서 못다한 그리 중요하지않은 추가적인 작업이 필요할 경우, DPC라는 녀석을 요청하는것이다. DPC는  DIRQL이상의 하드웨어 인터럽트 처리가 완료되고나서 , PASSIVE_LEVEL로 떨어지기 전에 DISPATCH_LEVEL에서 작동하는 구간인데, SW Interrupt(PASSIVE_LEVEL, APC_LEVEL , DISPATCH_LEVEL) 중에서는 가장 높은 레벨에 속한다. 이렇게 되면 하드웨어 인터럽트 처리가 완료되고 난다음에 DPC가 처리되기 때문에 , 다른 하드웨어의 인터럽트를 처리하는데 별 무리가 없을것이다.


  이제 Timer DPC의 의미를 다시 한번 생각해보자. DPC는 HW ISR처리가 완료되고난다음, DISPATCH_LEVEL에서 작동하는 SW INT인데 , 이것이 Timer와 합쳐지면 어떤 관련이 있을까? 사실 필자도 이것에 관해서 딱히 책에서 찾아본적은 없는데, 가만히 생각해보면 DPC는 SW INT이므로 , "커널 Timer의 힘을 빌어서 주기적으로 발생되는 SW INT" 라고 생각해도 크게 나쁜것 같지는 않다.

본격적으로 윈도우 커널에서 제공하는 Timer 관련 함수들중 자주 사용되는 함수들을 알아보자.
 
 
1. NTKERNELAPI VOID KeInitializeTimer ( __out PKTIMER Timer );

2. NTKERNELAPI BOOLEAN KeSetTimerEx ( __inout PKTIMER Timer, __in LARGE_INTEGER DueTime, __in LONG Period, __in_opt PKDPC Dpc );

 3. NTKERNELAPI BOOLEAN KeCancelTimer ( __inout PKTIMER )
 

 
 1. 타이머를 사용하기에 앞서 , 초기화를 수행해주는 함수이다.  ExAllocatePool을 이용하여 SystemPool에 KTIMER만큼 할당한뒤, 사용하는것을 권장.

2. 타이머 설정&시작 함수인데, 옵션으로 DPC루틴도 파라메터로 전달할수 있어서, 주기적으로 처리해야할 작업이 있을경우 요긴하게 쓸수 있다. 참고로 , KeSetTimer 함수와 KeSetTimerEx 함수간의 차이가 존재하는데,  KeSetTimer 함수는 Signal이 한번만 발생해서 DPC루틴이 한번만 호출되고 끝나게 되고,  KeSetTimerEx는 LONG Period만큼 주기적으로 DPC루틴이 호출되게 된다.
 
 
3. 타이머를 취소하는 함수로써 , 본인 코드 구성에 따라, 필요에따라 UnloadRoutine 같은곳에 넣어준다.


필자는 따로 편리하게 쓰기 위해서 함수를 정의한뒤, 

PKTIMER
InitializeTimer(VOID)
{
PKTIMER Timer;

Timer = (PKTIMER)ExAllocatePool(NonPagedPool, sizeof(KTIMER));
if(Timer == NULL)
{
return NULL;
}

KeInitializeTimer(Timer);
return Timer;
}

PKDPC
SetTimer
(
IN PKTIMER Timer,
IN LONG Period, 
IN OPTIONAL KDEFERRED_ROUTINE TimerDpcRoutine, 
IN OPTIONAL PVOID DpcRoutineContext
)
{
LARGE_INTEGER TimePeriod;
PKDPC DpcObj;

DpcObj = NULL;

if(TimerDpcRoutine != NULL)
{
DpcObj = (PKDPC) ExAllocatePool(NonPagedPool, sizeof(KDPC));
if(DpcObj == NULL)
{
return NULL;
}
KeInitializeDpc(DpcObj, TimerDpcRoutine, DpcRoutineContext);
}

TimePeriod.QuadPart = -100;
KeSetTimerEx(Timer, TimePeriod, Period, DpcObj);

return DpcObj;
}
 
VOID
ReleaseTimer(IN PKTIMER Timer, IN OPTIONAL PKDPC DpcObj)
{
KeCancelTimer(Timer);
ExFreePool(Timer);

if(DpcObj != NULL)
{
ExFreePool(DpcObj);
}
}


DriverEntry에  다음과 같은 코드를 넣어주었다.


TimerObj = InitializeTimer();
if(TimerObj == NULL)
{
return STATUS_UNSUCCESSFUL;
}
TimerDpcObj = SetTimer(TimerObj, 1000, TimerDpcRoutine, NULL);
if(!TimerDpcObj)
{
return STATUS_UNSUCCESSFUL;
}


또한 UnloadRoutine에는 다음과 같은 코드를 넣어주었다.

ReleaseTimer(TimerObj, TimerDpcObj);
 


이제 TimerDpcRoutine을 살펴보자.

 VOID TimerDpcRoutine(
IN PKDPC Dpc,
IN OPTIONAL PVOID DeferredContext,
IN OPTIONAL PVOID SystemArgument1,
IN OPTIONAL PVOID SystemArgument2
)
{
DBG_PRINT1("Warning! This routine executes on DISPATCH_LEVEL!\n");

if(IsSSDTAlreadyHooked(
(PBYTE)ZwSetInformationThread, 
(PBYTE)NewZwSetInformationThread, 
(PDWORD)KeServiceDescriptorTable.KiServiceTable) == FALSE)
{
disableWP_CR0();
OldZwSetInformationThread = (ZwSetInformationThreadPtr) hookSSDT((PBYTE)ZwSetInformationThread, 
(PBYTE)NewZwSetInformationThread, 
(PDWORD)KeServiceDescriptorTable.KiServiceTable);
enableWP_CR0();
}
return;
}

Timer DPC를 이용해서 간단하게 주기적으로 SSDT훅을 거는 코드를 구성해보았다. 이렇게 코드를 구성하면 , Timer DPC Detect 기능이 없는 루트킷 디텍터에서 SSDT를 Restore 한다고 해도, 1초마다 훅이 걸려있는지 검사해서 다시 훅을 걸어버리기 때문에, 악성 루트킷에서 요긴하게 사용될법 하다.
주의할점은, DPC Routine에서 Zw Routine은 호출하면 안된다는것이다. (Zw Routine은  PASSIVE_LEVEL에서만 호출이 가능하다!)

 

테스트 결과 잘 작동이 되었다.
아래는 스크린 샷.


드라이버를 시작한 모습.  DriverEntry에서 훅을 거는 코드를 작성하진 않았고 , TimerDpcRoutine에 훅을 거는 코드를 삽입했다. 따라서 , 훅은 TimerDpcRoutine에 의해서 걸린것이다.


SSDT Restore를 수행하려고 시도중인 모습.


SSDT Restore를 수행해도 바로 훅이 다시 걸린다. Period를 적절히 조절해주면 Performance도 어느정도 확보할 수 있을것 같다.


 
저작자 표시 비영리 변경 금지
신고
크리에이티브 커먼즈 라이선스
Creative Commons License
by Sone 2011.05.05 11:30
| 1 |

티스토리 툴바