이미 많은 드라이버 개발자분들께서  잘 아시다시피, 프로세스와 쓰레드의 생성과 종료 감시를 위해서 , 커널단에서 NotifyRoutine을 등록함으로써, 적절한 처리를 수행할 수 있다. (콜백루틴은 그 종류가 여러가지가 있지만 여기서는 Process와 Thread만 살펴보기로 한다.) 그런데, 필자도 이때까지 사용해오면서 정확히 어느 시점에 NotifyRoutine이 호출되는지는 모르고, 그냥 생성과 종료시점에 호출이 되는구나 정도로만 이해하고 있었다.

  어느날 VS2010을 열어놓고 키보드를 갈기고 있던중, PsSetCreateProcessNotifyRoutine, PsSetThreadNotifyRoutine 에 의하여 등록된 콜백루틴은 대체 커널의 어느부분에서 최초로 호출이 이루어질까? 라는 생각이 들어서, 디버깅을 해보기로 했다. 디버깅을 위해서 코드를 아주 씸플하게 작성했다.

VOID
ProcessNotifyRoutine
(
IN HANDLE  ParentId,
IN HANDLE  ProcessId,
IN BOOLEAN  Create
)
{
__asm int 3h //breakpoint exception for debugging

return;
}

당연히 디버거가 커널에 붙어있는 상태여야하며 , 디버거없이 냅다 로드시켰다간 프로세스의 생성 또는 종료시점에 공포의 BSOD를 보게 될것이다.
이제 드라이버를 로드시키고 , 프로세스를 하나 살며시 띄워서 Breakpoint가 걸리게끔 유도 해주었다.

 Break instruction exception - code 80000003 (first chance)
h00kdrlv3r!ProcessNotifyRoutine+0x2:
f8bc098c cc              int     3

이제 현재 상태에서 스택을 살펴보자.

 0: kd> kb
ChildEBP RetAddr  Args to Child              
b21e6b80 805d24e5 000006b4 00000cf0 00000001 h00kdrlv3r!ProcessNotifyRoutine+0x2 [d:\program\h00kdrlv3r\h00kdrlv3r\process.c @ 21]
b21e6cc4 805d3144 0007d934 001f03ff 00000000 nt!PspCreateThread+0x3a7
b21e6d3c 8054367c 0007d934 001f03ff 00000000 nt!NtCreateThread+0xfc
b21e6d3c 7c93e514 0007d934 001f03ff 00000000 nt!KiFastCallEntry+0xfc
0007d560 7c93d1ba 7c7e9ea7 0007d934 001f03ff ntdll!KiFastSystemCallRet
0007d5a8 7c9446f9 0007d6b4 0007d8a0 00000004 ntdll!ZwCreateThread+0xc
0007dfb0 7c7d2362 00000000 001654ac 00163214 ntdll!RtlIntegerToUnicode+0x11d

ZwCreateThread가 호출이되고 , KiFastCallEntry를 통과해서 , NtCreateThread, PspCreateThread를 호출한다음 드라이버 영역이 호출되었다. 그렇다면 PspCreateThread 내부에서 콜백루틴을 호출했다는건데, PspCreateThread 내부를 간단히 살펴보기로 했다.

 0: kd> uf /c nt!PspCreateThread
nt!PspCreateThread (805d213e)
 ........................
  nt!PspCreateThread+0x386 (805d24c4):
    call to nt!ExReferenceCallBackBlock (8060f514)
  nt!PspCreateThread+0x392 (805d24d0):
    call to nt!ExGetCallBackBlockRoutine (805a1e54)
  nt!PspCreateThread+0x3a5 (805d24e3):
    unresolvable call: call    eax
  nt!PspCreateThread+0x3ab (805d24e9):
    call to nt!ExDereferenceCallBackBlock (8060f616)
  nt!PspCreateThread+0x3ef (805d252d):
  ..............................

PspCreateThread 함수의 규모가 생각보다 상당히 커서, 앞뒤부분은 짜르고 PspCreateThread+0x3a7 부근만 살펴보기로 했다.
call eax  를 보니 특별히 어떤 심볼을 가진 함수나 Import 함수를 내부적으로 호출하진 않고, 바로 PspCreateThread 내부에서 함수포인터를 가져와서 호출하는 방식이다. 그런데 MSDN에서 살펴보니 PsSetCreateProcessNotifyRoutine  API는  Windows 2000에서도 사용이 가능하다고 한다. 그렇다면 예전에 유출되었던 윈도우2K 소스코드에서 PspCreateThread를 살펴보면 어느시점에 호출되는지 어느정도 정확히 알아낼 수 있을것 같다. (윈도우 NT4 소스코드에서도 확인은 가능하다.)

 PspCreateThread의 소스코드는  "\win2k\private\ntos\ps\create.c" 에서 확인할 수 있다.
먼저 PspCreateThread의 형태를 살펴보자.
 

NTSTATUS
PspCreateThread(
    OUT PHANDLE ThreadHandle,
    IN ACCESS_MASK DesiredAccess,
    IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
    IN HANDLE ProcessHandle,
    IN PEPROCESS ProcessPointer,
    OUT PCLIENT_ID ClientId OPTIONAL,
    IN PCONTEXT ThreadContext OPTIONAL,
    IN PINITIAL_TEB InitialTeb OPTIONAL,
    IN BOOLEAN CreateSuspended,
    IN PKSTART_ROUTINE StartRoutine OPTIONAL,
    IN PVOID StartContext
    ) ;
/*++

Routine Description:

    This routine creates and initializes a thread object. It implements the
    foundation for NtCreateThread and for PsCreateSystemThread.

Arguments:  ....

++*/

파라메터가 엄청 많다. 그래도 파라메터 하나하나를 살펴보면 그다지 어렵지는 않음을 확인할 수 있다. 
프로세스 생성과정에서 Initialize Thread를 생성하는데, 그때 내부적으로 PspCreateThread가 호출되는것으로 생각된다.
이제 핵심부만 간단히 살펴보자.



.................

   //
     // If the process does not have its part of the client id, then
    // assign one
    //

    if ( !Process->UniqueProcessId ) {       //Unique한 PID가 지정되어져 있지않다면  1
        CidEntry.Object = Process;
        CidEntry.Attributes = 0;
        Process->UniqueProcessId = ExCreateHandle(PspCidTable,&CidEntry);   //PID를 지정해주고 2
        ExSetHandleTableOwner( Process->ObjectTable, Process->UniqueProcessId );
        if (!Process->UniqueProcessId) {
            PsUnlockProcess(Process);

            MmDeleteKernelStack(KernelStack, FALSE);
            ObDereferenceObject(Process);
            ObDereferenceObject(Thread);

            return STATUS_UNSUCCESSFUL;
        }

        if (PspCreateProcessNotifyRoutineCount != 0) {     //Callback Routine이 하나이상 등록되있다면 3
            ULONG i;

            for (i=0; i<PSP_MAX_CREATE_PROCESS_NOTIFY; i++) {
                if (PspCreateProcessNotifyRoutine[i] != NULL) {
                    (*PspCreateProcessNotifyRoutine[i])( Process->InheritedFromUniqueProcessId,
                                                         Process->UniqueProcessId,
                                                         TRUE
                                                       );
                }
            }
        }
    } 

.......
 
자, 그럼 좀더 자세히 살펴보자.

 1. 디스크립션을 보니  프로세스가 PID를 가지고 있지 않으면, 하나를 할당한다고 되어져 있다. IF문 스코프를 가만히 살펴보니 이 조건에 걸리지 않으면 Process Notiry Routine도 호출되지 않는다.  이 조건에 해당되는 경우를 가만히 생각해보면, 프로세스가 생성될때라는것을 알수 있다. 즉, Unique PID가 없는 시점은 , 프로세스가 처음 생성될때의 시점이며, 만약에 프로세스가 이미 생성되어져 있었다면 UniquePID는 최초 호출된 PspCreateThread에 의해서 이미 지정되어져 있었을 것이다. 그러면 Process Callback Routine이 호출되지 않게 된다. 결과적으로 Process Callback Routine은 프로세스가 처음에 생성될때만 호출될 것이다.  다른 말로 표현하면 Process의 Initialize Thread를 생성하는 시점이고,  이때는 EPROCESS에  PID가 Assign 되어져 있지 않기 때문에,  고유 PID 지정을 PspCreateThread 내부에서 처리해주면서 Process Callback도 호출하는것이다. (자세한것은 WinDBG에서 라이브디버깅으로 적절한 위치에 BP를 걸어두고 EPROCESS를 확인하면 되겠다.)
PID 지정은 PspCreateProcess 등에서 처리할줄 알았는데 의외다.

2.  PID가 없다면 ExCreateHandle을 호출하여  Unique한 PID를 지정해준다. 

3. 그 후에 Process Callback Routine이 하나이상 배열에 존재한다면 배열을 인덱스의 처음부터 시작해서 등록된 순서대로 호출한다. 

그럼 만약 이미 생성되어져 있는 프로세스의 EPROCESS->UniquePID를 NULL로 지정해버리고, 그 후에 CreateThread를 이용해서 쓰레드를 생성해버리면 어떻게 될까?  과연 이미 생성되어져 있는 프로세스 내부에서 CreateThread를 호출했을 경우, 쓰레드가 생성될때마다 Process Callback Routine이 호출될것인가?  이 내용은 다음에....-_-;;; (현재는 귀차니즘이 발동해서)
 



다음으로 Thread Notify Routine을 살펴보자.  이건 간단하다.
본문상에는 서술하지 않았지만 , 코드를 살펴보면 Process Callback이 호출된 다음, 컨텍스트와 StartRoutine을 초기화 해주고KiInitializeThread 를 호출하고 난뒤,  Thread Ready List에 집어넣기전에, Thread Callback Routine을 호출하는 부분이 존재한다.

     //
    // Notify registered callout routines of thread creation.
    //

    if (PspCreateThreadNotifyRoutineCount != 0) {
        ULONG i;

        for (i=0; i<PSP_MAX_CREATE_THREAD_NOTIFY; i++) {
            if (PspCreateThreadNotifyRoutine[i] != NULL) {
                (*PspCreateThreadNotifyRoutine[i])( Thread->Cid.UniqueProcess,
                                                    Thread->Cid.UniqueThread,
                                                    TRUE
                                                   );
            }
        }
    }

그냥 특별한 다른 조건은 존재하지 않고, Thread Notify Routine이 하나이상 등록되어져 있다면 , Thread Callback을 호출한다.

결과적으로 유저가 만약에  Process와 Thread 콜백을 둘다 등록해놓았을 경우, 프로세스가 처음에 생성될때는 Process,Thread 둘다 호출이 될것이다.



ps. Terminate 될때의 호출시점은 다루지 않았는데, 그것은 다음에.... 
저작자 표시 비영리 변경 금지
신고
by Sone 2011.05.21 00:09
| 1 |