'KiDeliverApc'에 해당되는 글 1건

  1. 2010.05.04 [윈도우 NT] KiDeliverApc 동작 분석 4
Windows OS2010. 5. 4. 02:04
반응형
KiDeliverApc 내부 동작에 대한 설명입니다.
물론 심심해서 이런 분석을 하게 된 것은 아닙니다.
하도 문제가 안풀리다 보니 답답해서 커널이라도 분석해 보자 하다가 이렇게 되었습니다. ^^

얼마전 시스템 행이 걸리는 문제를 만났는데 아래와 같은 콜스택만 보일 뿐 어떻게 원인을 찾아야 할지 난감한 상태였습니다.

THREAD 8956e660  Cid 0c18.0bcc  Teb: 7ffdd000 Win32Thread: e2975360 WAIT: (Suspended) KernelMode Non-Alertable
SuspendCount 1
...
    Owning Process            899cd5a8       Image:      taskmgr.exe
    Attached Process          N/A            Image:         N/A
...
    ChildEBP RetAddr  
    aad52c18 80504d50 nt!KiSwapContext+0x2f (FPO: [Uses EBP] [0,0,4])
    aad52c24 804fcf40 nt!KiSwapThread+0x8a (FPO: [0,0,0])
    aad52c4c 8050448c nt!KeWaitForSingleObject+0x1c2 (FPO: [5,5,4])
    aad52c64 80500dfa nt!KiSuspendThread+0x18 (FPO: [3,0,0])
    aad52cac 80504d6e nt!KiDeliverApc+0x124 (FPO: [3,10,0])
    aad52cc4 804fcf40 nt!KiSwapThread+0xa8 (FPO: [0,0,0])
    aad52cec 805c108c nt!KeWaitForSingleObject+0x1c2 (FPO: [5,5,4])
    aad52d50 8054289c nt!NtWaitForSingleObject+0x9a (FPO: [Non-Fpo])
    aad52d50 7c93e514 nt!KiFastCallEntry+0xfc (FPO: [0,0] TrapFrame @ aad52d64)
    00dbff84 7c802532 ntdll!KiFastSystemCallRet
    00dbff98 0100d90d kernel32!WaitForSingleObject+0x12 (FPO: [2,0,0])
    00dbffb4 7c80b699 taskmgr!WorkerThread+0x3a (FPO: [1,0,4])
    00dbffec 00000000 kernel32!BaseThreadStart+0x37 (FPO: [Non-Fpo])


콜스택의 마지막이 KiSwapContext라 처음엔 그냥 컨텍스트 스위칭인가보다 생각했는데 자세히 보니 KiSwapThread가 콜스택 상에서 두 번 보이는 상태였습니다.

중간에 보이는 KiSwapThread가 일반적인 스케쥴링에 의한 호출이구요.
그 위로 보이는 KiDeliverApc부터가 약간 생소한 부분이었는데요.
KiSuspendThread를 호출하여 다시 대기 상태로 들어가는 흐름입니다.

프로세스는 taskmgr.exe인데 이런 상태가 되면 작업관리자가 뜨다가 말고 행이 걸려 버립니다. 콜스택으로 보면 KiSuspendThread에 의한 결과로 Suspend가 걸려버리는 것 같네요.

문제는 누가 KiSuspendThread를 호출했느냐는 것입니다.

콜스택에서는 KiDeliverApc가 호출한 것으로 보입니다.
여기서 DeliverApc란 뜻을 좀 이해해야 하는데요. (음냐... 복잡해 지는데...)

윈도우에서 스레드 스케쥴링을 할 때는 DISPATCH_LEVEL이라는 우선 순위 개념(IRQL)을 두어서 스케쥴러가 사용하구요.
실행할 스레드가 정해지면 APC_LEVEL로 내려서 이 스레드에 큐잉된 APC(Asynchronous Procedure Call)를 수행하게 합니다.
그리고 나서 PASSIVE_LEVEL로 내려줘서 원래 스레드 코드가 실행되게 하지요.

KiDeliverApc는 큐잉된 APC 들을 실행해 주는 역할을 합니다.
KiDeliverApc의 파라미터로 APC를 전달할 거라고 상상해 봤는데 함수 선언을 보니 파라미터가 매우 단순하므로 그건 아니구요.

VOID
NTAPI
KiDeliverApc(IN KPROCESSOR_MODE DeliveryMode,
             IN PKEXCEPTION_FRAME ExceptionFrame,
             IN PKTRAP_FRAME TrapFrame)


그래서 KiDeliverApc를 분석했는데 요약하면 다음과 같습니다.

KiDeliverApc()
{
    PKTHREAD Thread = KeGetCurrentThread();

    //
    // 현재 스레드Thread의 ApcState.ApcListHead에 달려있는 APC가 있으면 
    // 꺼내서 실행하고 리스트를 계속 확인하여 모두 없어질 때까지 실행한다.

    //
    while (!IsListEmpty(&Thread->ApcState.ApcListHead[KernelMode]))
    {
        ApcListEntry = Thread->ApcState.ApcListHead[KernelMode].Flink;
        Apc = CONTAINING_RECORD(ApcListEntry, KAPC, ApcListEntry);

        KernelRoutine = Apc->KernelRoutine;
        NormalRoutine = Apc->NormalRoutine;

        KernelRoutine(Apc, ...);
        NormalRoutine(...);
    }
}


핵심은 Thread->ApcState.ApcListHead에 어디선가 APC를 추가한다는 것입니다.
KiSuspendThread가 NormalRoutine으로 등록되어 실행된 것이었구요.

APC를 추가한 녀석을 찾기 위해 Thread->ApcState.ApcListHead에 메모리 브레이크 포인트를 걸어서 List에 뭔가 들어오자마자 브레이크 포인트가 걸리게 해 봅니다.
ApcState는 KTHREAD 구조체에서 0x34 옵셋에 위치하므로 아래와 같이 설정합니다.

kd> dt _KTHREAD ApcState.
nt!_KTHREAD
   +0x034 ApcState  :
      +0x000 ApcListHead : [2] _LIST_ENTRY
      +0x010 Process   : Ptr32 _KPROCESS
      +0x014 KernelApcInProgress : UChar
      +0x015 KernelApcPending : UChar
      +0x016 UserApcPending : UChar
   +0x138 ApcStatePointer : [2]
   +0x165 ApcStateIndex : UChar

kd> ba w4 (Thread주소+0x34)


브레이크 포인트가 걸렸을 때 콜스택과 코드를 보면 아래와 같은데요.
이건 어떤 프로세스가 최초 생성될 때 SuspendThread가 호출되는 상황이네요.

kd> k
ChildEBP RetAddr 
f9b9bb60 804fdaa3 nt!KiInsertQueueApc+0x79
f9b9bb80 805c7436 nt!KeSuspendThread+0x67
f9b9bcc4 805c7f02 nt!PspCreateThread+0x570
f9b9bd3c 8053ea48 nt!NtCreateThread+0xfc
f9b9bd3c 7c93e514 nt!KiFastCallEntry+0xf8

kd> ub nt!KiInsertQueueApc+0x79
nt!KiInsertQueueApc+0x64:
804ff374 eb3f            jmp     nt!KiInsertQueueApc+0xa5 (804ff3b5)
804ff376 0fbeda          movsx   ebx,dl
804ff379 8d3cdf          lea     edi,[edi+ebx*8]
804ff37c 8b5f04          mov     ebx,dword ptr [edi+4]
804ff37f 8d700c          lea     esi,[eax+0Ch]       ; ApcListEntry
804ff382 893e            mov     dword ptr [esi],edi
804ff384 895e04          mov     dword ptr [esi+4],ebx 
804ff387 8933            mov     dword ptr [ebx],esi ; 리스트에 추가


코드의 마지막 줄에서 리스트에 추가한 것은 KAPC 구조체의 ApcListEntry 필드의 주소입니다. ApcListEntry의 옵셋이 0Ch이므로 위 코드에서 eax는 KAPC 주소라는 것을 알 수 있습니다.

kd> dt _KAPC @eax
nt!_KAPC
   +0x000 Type             : 18
   +0x002 Size             : 48
   +0x004 Spare0           : 0
   +0x008 Thread           : 0x812f1020 _KTHREAD
   +0x00c ApcListEntry     : _LIST_ENTRY [ 0x812f1054 - 0x812f1054 ]
   +0x014 KernelRoutine    : 0x80501ed4     void  nt!KiSuspendNop+0
   +0x018 RundownRoutine   : 0x805277ba     void  nt!PopAttribNop+0
   +0x01c NormalRoutine    : 0x8050230a     void  nt!KiSuspendThread+0
   +0x020 NormalContext    : (null)
   +0x024 SystemArgument1  : (null)
   +0x028 SystemArgument2  : (null)
   +0x02c ApcStateIndex    : 0 ''
   +0x02d ApcMode          : 0 ''
   +0x02e Inserted         : 0 ''


0x01c옵셋의 NormalRoutine에 KiSuspendThread가 보이네요.

하지만 APC는 이 목적 이외에 다른 목적으로도 다양하게 사용되기 때문에 KTHREAD의 ApcState에 메모리 브레이크를 걸어 놓으면 KernelRoutine과 NormalRoutine에 여러가지 다양한 함수가 전달되는 것을 보게 됩니다.

이것을 피하기 위해 804ff37f 주소(즉, nt!KiInsertQueueApc+79)에 브레이크 포인트를 걸되 NormalRoutine이 KiSuspendThread인 경우만 잡도록 조건을 줬습니다. 
 
kd> u nt!KiSuspendThread L1
nt!KiSuspendThread:
8050230a 64a124010000    mov     eax,dword ptr fs:[00000124h]

kd> bp nt!KiInsertQueueApc+79 ".if (poi(@eax+1c)==8050230a) {} .else {gc}"

그러면 KiSuspendThread를 위한 KAPC만 잡히게 됩니다.
잡혔을 때의 콜스택은 다음과 같구요.

kd> k
ChildEBP RetAddr 
f766ecc8 804fdaa3 nt!KiInsertQueueApc+0x79
f766ece8 805cb671 nt!KeSuspendThread+0x67
f766ed28 805cb706 nt!PsSuspendThread+0x6f
f766ed44 805cb8fe nt!PsSuspendProcess+0x28
f766ed58 8053ea48 nt!NtSuspendProcess+0x40
f766ed58 7c93e514 nt!KiFastCallEntry+0xf8
WARNING: Stack unwind information not available. Following frames may be wrong.
00000000 00000000 ntdll!KiFastSystemCallRet


어떤 프로세스가 유저모드에서부터 SuspendProcess를 호출한 것이었고 결국 KeSuspendThread에서 KiInsertQueueApc를 호출해서 APC가 전달된 것이었습니다.

[부록]

KiDeliverApc 분석 내용을 C언어 버전과 Disassembly 버전으로 첨부했습니다.

반응형
Posted by GreeMate