lain32 - The Real Folk Blues

lain32.egloos.com

포토로그 마이가든



PostQueuedCompletionStatus 함수에 실패 여부도 고려해야 할까? 소켓 프로그래밍

일단 결론부터 말씀드리자면 소켓 프로그래밍을 할 때 PostQueuedCompletionStatus 함수에
실패여부도 반드시 고려해야 합니다. ( 매우 불행하게도... )

이전에 포스팅한 SetEvent와는 다르게 IOCP에서는 매우 비중이 큰 문제입니다.

일단 MSDN에 해당 이 함수는 언제 실패하는지에 대한 어떠한 내용도 나와 있지 않습니다.
그냥 GetLastError로 보라는것 뿐입니다.

IOCP라는 함수가 커널에 밀접하게 관련있다라던지 여러가지말들을 하는 사람들이 많지만
말은 그렇게 할뿐 실제로 어떻게 관련이 있는지는 대부분 정확히 당사자들도 알지 못 합니다.

그렇다면 IOCP 함수중에 PostQueuedCompletionStatus 함수에 실패여부를 알기에 앞서
IOCP를 생성하는 함수인 CreateIoCompletionPort 함수부터 먼저 보겠습니다.
현재 상황으로써는 IOCP가 윈도우즈에 구현되는 매우 특수한 모델로 보입니다.
( 즉, 소켓 프로그래밍 개발자에 입장에서 본다면 IOCP 함수들은 소켓 프로그래밍을 위해서
  태어난 함수들처럼 보일지도 모르겠습니다. 그만큼 편리하고 좋기 때문에 특수한 모델로 보일것입니다.
  물론 전체적으로 볼 때 비동기 I/O를 하기 위해서 만든것이지만.. )

CreateIoCompletionPort 함수는 내부적으로 NtCreateIoCompletion 함수를 호출하게 되는데
다음과 같이 생겼습니다. ( 디컴파일 결과만 보여줍니다. )

NTSTATUS __stdcall NtCreateIoCompletion(PHANDLE IoCompletionHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, ULONG NumberOfConcurrentThreads)
{
  void *v4; // ecx@2
  PHANDLE v5; // edi@2
  char v7; // al@1
  int v8; // eax@6
  int v9; // esi@7
  HANDLE *v10; // eax@7
  int PreviousMode; // [sp+1Ch] [bp-1Ch]@1
  CPPEH_RECORD ms_exc; // [sp+20h] [bp-18h]@4
  int v13; // [sp+10h] [bp-28h]@6
  void *v14; // [sp+18h] [bp-20h]@7

  v7 = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  LOBYTE(PreviousMode) = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  if ( v7 )
  {
    v5 = IoCompletionHandle;
    v4 = IoCompletionHandle;
    if ( (unsigned int)IoCompletionHandle >= (unsigned int)MmUserProbeAddress )
      v4 = MmUserProbeAddress;
    *(_DWORD *)v4 = *(_DWORD *)v4;
    ms_exc.disabled = -2;
  }
  else
  {
    v5 = IoCompletionHandle;
  }
  v8 = ObCreateObject(
         PreviousMode,
         (int)IoCompletionObjectType,
         (int)ObjectAttributes,
         PreviousMode,
         0,
         48,
         0,
         0,
         (int)&v13);
  IoCompletionHandle = (PHANDLE)v8;
  if ( v8 >= 0 )
  {
    v9 = v13;
    KeInitializeQueue(v13, NumberOfConcurrentThreads);
    *(_DWORD *)(v9 + 40) = 0;
    *(_BYTE *)(v9 + 44) = 0;
    v10 = (HANDLE *)ObInsertObject(v9, 0, DesiredAccess, 0, 0, &v14);
    IoCompletionHandle = v10;
    if ( (signed int)v10 >= 0 )
    {
      if ( (_BYTE)PreviousMode )
        *v5 = v14;
      else
        *v5 = v14;
    }
  }
  return (NTSTATUS)IoCompletionHandle;
}

함수를 열자마자 바로 실체가 나옵니다.
KeInitializeQueue 를 호출하는 것을 볼 수 있습니다.
즉, IOCP를 초기화하는 과정은 어떠한 특수한 함수를 호출하는것도 아니며 그냥 커널에 있는 함수인
KeInitializeQueue 함수를 호출하며 결국엔 커널에 큐잉모델을 단지 이용하는 것일뿐입니다.

그렇다면 PostQueuedCompletionStatus 함수는 구지 안봐도 어떠한 함수를 호출할지 뻔하지만
그래도 최종적으로 호출되는 코드는 다음과 같습니다.

NTSTATUS __stdcall NtSetIoCompletion(HANDLE IoCompletionHandle, ULONG CompletionKey, ULONG CompletionValue, NTSTATUS Status, ULONG Information)
{
  signed int v5; // esi@1
  NTSTATUS v7; // eax@1
  int AccessMode; // [sp+4h] [bp-8h]@1
  PVOID Object; // [sp+8h] [bp-4h]@1

  LOBYTE(AccessMode) = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  v7 = ObReferenceObjectByHandle(IoCompletionHandle, 2u, IoCompletionObjectType, AccessMode, &Object, 0);
  v5 = v7;
  if ( v7 >= 0 )
  {
    v5 = IoSetIoCompletion((int)Object, CompletionKey, CompletionValue, Status, Information, 1);
    ObfDereferenceObject();
  }
  return v5;
}

signed int __stdcall IoSetIoCompletion(int a1, int a2, int a3, int a4, int a5, int a6)
{
  return IoSetIoCompletionEx(a1, a2, a3, a4, a5, a6, 0);
}

igned int __stdcall IoSetIoCompletionEx(int a1, int a2, int a3, int a4, int a5, int a6, int a7)
{
  void *v7; // eax@1
  signed int v8; // esi@1

  v7 = (void *)a7;
  v8 = 0;
  if ( a7 || (v7 = IopAllocateMiniCompletionPacket(1, a6), v7) )
  {
    *((_DWORD *)v7 + 3) = a2;
    *((_DWORD *)v7 + 4) = a3;
    *((_DWORD *)v7 + 5) = a4;
    *((_DWORD *)v7 + 6) = a5;
    KiInsertQueue(v7, 0, 0);
  }
  else
  {
    v8 = -1073741670;
  }
  return v8;
}

보시다시피 KiInsertQueue를 호출합니다. ( 참고로 KeInsertQueue 함수는 KiInsertQueue를 내부적으로 호출하므로
거의 동일하다고 보시면 됩니다. )

그렇다면 이제 PostQueuedCompletionStatus 함수에 실패여부도 고려를 해야 할 상황입니다.
가장 첫 번째로 IOCP핸들을 잘 못 넘겼을 경우인데 그 부분은 제외하겠습니다.
또한 KeInsertQueue 함수자체도 에러코드를 리턴하지 않습니다.
하지만 여기서 중요한 것은 위에서 IopAllocateMiniCompletionPacket 함수를 호출하는 부분입니다.
다음과 같이 생겼습니다.

PVOID __stdcall IopAllocateMiniCompletionPacket(int a1, char a2)
{
  SIZE_T ST04_4_0; // ST04_4@0
  ULONG ST08_4_0; // ST08_4@0
  EX_POOL_PRIORITY ST0C_4_0; // ST0C_4@0
  int ST10_4_0; // ST10_4@0
  int ST14_4_0; // ST14_4@0
  int ST18_4_0; // ST18_4@0
  int ST1C_4_0; // ST1C_4@0
  PVOID result; // eax@2
  POOL_TYPE v10; // ebx@2
  int v11; // edi@2
  int v12; // esi@2
  int v13; // esi@3
  PVOID v14; // [sp+10h] [bp-1Ch]@2

  if ( a1 == 3 )
  {
    v10 = 0;
    ST0C_4_0 = 0;
    ST08_4_0 = 544236361;
    ST04_4_0 = 40;
    goto LABEL_8;
  }
  v11 = *MK_FP(__FS__, 32);
  v12 = *(_DWORD *)(*MK_FP(__FS__, 32) + 1496);
  ++*(_DWORD *)(v12 + 12);
  result = (PVOID)RtlpInterlockedPopEntrySList(ST10_4_0, ST14_4_0);
  v14 = result;
  v10 = 0;
  if ( !result )
  {
    ++*(_DWORD *)(v12 + 16);
    v13 = *(_DWORD *)(v11 + 1500);
    ++*(_DWORD *)(v13 + 12);
    result = (PVOID)RtlpInterlockedPopEntrySList(ST18_4_0, ST1C_4_0);
    v14 = result;
    if ( !result )
    {
      ++*(_DWORD *)(v13 + 16);
      if ( a2 )
      {
        a1 = 2;
        v14 = ExAllocatePoolWithQuotaTag(0, 0x1Cu, 0x20706349u);
        result = v14;
        goto LABEL_9;
      }
LABEL_8:
      result = ExAllocatePoolWithTagPriority(v10, ST04_4_0, ST08_4_0, ST0C_4_0);
LABEL_9:
      if ( result == (PVOID)v10 )
        return result;
      goto LABEL_10;
    }
  }
LABEL_10:
  *((_DWORD *)result + 2) = a1;
  return result;
}

첫 번째로 고려해야 할 함수는 RtlpInterlockedPopEntrySList 입니다.
이 함수는 다음과 같이 생겼습니다.

.text:00439378 ; __fastcall RtlpInterlockedPopEntrySList(x)
.text:00439378                 public @RtlpInterlockedPopEntrySList@4
.text:00439378 @RtlpInterlockedPopEntrySList@4 proc near
.text:00439378                                         ; CODE XREF: MiAllocateMdlPagesByLists(x,x,x,x,x,x,x)+183p
.text:00439378                                         ; KeAllocateInterrupt(x)+46p ...
.text:00439378
.text:00439378 var_C           = dword ptr -0Ch
.text:00439378
.text:00439378                 push    ebx             ; ExInterlockedPopEntrySList
.text:00439379                 push    ebp
.text:0043937A                 mov     ebp, ecx
.text:0043937C                 sub     esp, 4
.text:0043937F                 xor     eax, eax
.text:00439381                 mov     [esp+0Ch+var_C], eax
.text:00439381 @RtlpInterlockedPopEntrySList@4 endp ; sp-analysis failed

전혀 고려대상이 아닙니다.
단순한 인라인 함수정도로 생각하면 될것입니다.
( 함수가 좀 길어서 다 올리지 않았습니다. 보시면 맨 마지막에 스택분석 실패라고 나온것을 볼 수 잇죠..
  밑에 코드가 사실은 더 있습니다. )

그 다음에 마지막으로 고려해야 할 대상은 ExAllocatePoolWithQuotaTag 함수입니다.
( ExAllocatePoolWithTagPriority 함수도 호출하는데 이 함수는 메모리가 부족하 경우 NULL을 리턴합니다. )
이 함수는 특이하게도 메모리가 부족해서 함수가 실패할 경우 NULL을 리턴하는게 아니라
예외 핸들러를 실행시킵니다.
해당 함수가 디컴파일이 되어서 안 보이겠지만 사실은 예외 핸들러가 작성되어 있습니다.

PAGE:00669EDA ; __stdcall IopAllocateMiniCompletionPacket(x, x)
PAGE:00669EDA _IopAllocateMiniCompletionPacket@8 proc near
PAGE:00669EDA                                         ; CODE XREF: IoSetIoCompletionEx(x,x,x,x,x,x,x)+14p
PAGE:00669EDA                                         ; IoAllocateMiniCompletionPacket(x,x)+9p
PAGE:00669EDA
PAGE:00669EDA var_1C          = dword ptr -1Ch
PAGE:00669EDA ms_exc          = CPPEH_RECORD ptr -18h
PAGE:00669EDA arg_0           = dword ptr  8
PAGE:00669EDA arg_4           = byte ptr  0Ch
PAGE:00669EDA
PAGE:00669EDA                 push    0Ch
PAGE:00669EDC                 push    offset dword_44C338
PAGE:00669EE1                 call    __SEH_prolog4
PAGE:00669EE6                 cmp     [ebp+arg_0], 3
PAGE:00669EEA                 jz      loc_669F74
PAGE:00669EF0                 mov     edi, large fs:20h
PAGE:00669EF7                 mov     esi, [edi+5D8h]
PAGE:00669EFD                 inc     dword ptr [esi+0Ch]
PAGE:00669F00                 mov     ecx, esi
PAGE:00669F02                 call    @RtlpInterlockedPopEntrySList@4 ; RtlpInterlockedPopEntrySList(x)
PAGE:00669F07                 mov     [ebp+var_1C], eax
PAGE:00669F0A                 xor     ebx, ebx
PAGE:00669F0C                 cmp     eax, ebx
PAGE:00669F0E                 jnz     short loc_669F88
PAGE:00669F10                 inc     dword ptr [esi+10h]
PAGE:00669F13                 mov     esi, [edi+5DCh]
PAGE:00669F19                 inc     dword ptr [esi+0Ch]
PAGE:00669F1C                 mov     ecx, esi
PAGE:00669F1E                 call    @RtlpInterlockedPopEntrySList@4 ; RtlpInterlockedPopEntrySList(x)
PAGE:00669F23                 mov     [ebp+var_1C], eax
PAGE:00669F26                 cmp     eax, ebx
PAGE:00669F28                 jnz     short loc_669F88
PAGE:00669F2A                 inc     dword ptr [esi+10h]
PAGE:00669F2D                 cmp     [ebp+arg_4], bl
PAGE:00669F30                 jz      short loc_669F6A
PAGE:00669F32                 mov     [ebp+arg_0], 2
PAGE:00669F39                 mov     [ebp+ms_exc.disabled], ebx
PAGE:00669F3C                 push    20706349h       ; Tag
PAGE:00669F41                 push    1Ch             ; NumberOfBytes
PAGE:00669F43                 push    ebx             ; PoolType
PAGE:00669F44                 call    _ExAllocatePoolWithQuotaTag@12 ; ExAllocatePoolWithQuotaTag(x,x,x)
PAGE:00669F49                 mov     [ebp+var_1C], eax
PAGE:00669F4C                 mov     [ebp+ms_exc.disabled], 0FFFFFFFEh
PAGE:00669F53                 jmp     short loc_669F65
PAGE:00669F55 ; ---------------------------------------------------------------------------
PAGE:00669F55
PAGE:00669F55 loc_669F55:                             ; DATA XREF: .text:0044C34Co
PAGE:00669F55                 xor     eax, eax
PAGE:00669F57                 inc     eax
PAGE:00669F58                 retn
PAGE:00669F59 ; ---------------------------------------------------------------------------
PAGE:00669F59
PAGE:00669F59 loc_669F59:                             ; DATA XREF: .text:0044C350o
PAGE:00669F59                 mov     esp, [ebp+ms_exc.old_esp]
PAGE:00669F5C                 mov     [ebp+ms_exc.disabled], 0FFFFFFFEh
PAGE:00669F63                 xor     ebx, ebx
PAGE:00669F65
PAGE:00669F65 loc_669F65:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+79j
PAGE:00669F65                 mov     eax, [ebp+var_1C]
PAGE:00669F68                 jmp     short loc_669F84
PAGE:00669F6A ; ---------------------------------------------------------------------------
PAGE:00669F6A
PAGE:00669F6A loc_669F6A:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+56j
PAGE:00669F6A                 push    ebx
PAGE:00669F6B                 push    20706349h
PAGE:00669F70                 push    1Ch
PAGE:00669F72                 jmp     short loc_669F7E
PAGE:00669F74 ; ---------------------------------------------------------------------------
PAGE:00669F74
PAGE:00669F74 loc_669F74:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+10j
PAGE:00669F74                 xor     ebx, ebx
PAGE:00669F76                 push    ebx             ; Priority
PAGE:00669F77                 push    20706349h       ; Tag
PAGE:00669F7C                 push    28h             ; NumberOfBytes
PAGE:00669F7E
PAGE:00669F7E loc_669F7E:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+98j
PAGE:00669F7E                 push    ebx             ; PoolType
PAGE:00669F7F                 call    _ExAllocatePoolWithTagPriority@16 ; ExAllocatePoolWithTagPriority(x,x,x,x)
PAGE:00669F84
PAGE:00669F84 loc_669F84:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+8Ej
PAGE:00669F84                 cmp     eax, ebx
PAGE:00669F86                 jz      short loc_669F8E
PAGE:00669F88
PAGE:00669F88 loc_669F88:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+34j
PAGE:00669F88                                         ; IopAllocateMiniCompletionPacket(x,x)+4Ej
PAGE:00669F88                 mov     ecx, [ebp+arg_0]
PAGE:00669F8B                 mov     [eax+8], ecx
PAGE:00669F8E
PAGE:00669F8E loc_669F8E:                             ; CODE XREF: IopAllocateMiniCompletionPacket(x,x)+ACj
PAGE:00669F8E                 call    __SEH_epilog4
PAGE:00669F93                 retn    8
PAGE:00669F93 _IopAllocateMiniCompletionPacket@8 endp ; sp-analysis failed



이쯤에서 PostQueuedCompletionStatus 함수가 실패할 경우는 다음과 같이 요약됩니다.
첫 번째로 핸들이 잘 못 될 경우 ( 그러나 프로그램 로직상에 문제로 발생할 수 있기 때문에 무시 )
두 번째로 시스템 메모리가 부족 할 경우..

시스템 메모리가 부족 할 경우에는 프로그래머의 잘 못으로 일어나는 현상이 아니므로
PostQueuedCompletionStatus 함수는 반드시 실패할 수 있기 때문에 큐를 대기하는 쪽에서는
이부분을 염두해 두어야 합니다. ( 물론 이렇게 코드를 작성하기가 매우 매우 힘들겠지만.. )

SetEvent 함수에 실패 여부도 고려해야 할까? 소켓 프로그래밍

요즘 알지도 못 하면서 소켓 프로그래밍에 접근하고 있는데 이 글을 읽고 있는 분들도 아시다시피
일반적인 애플리케이션이 아닌 소켓을 사용하는 프로그램에 경우에는 그 안정성이 매우 중요합니다.

특히나 서버 애플리케이션에 경우에는 상당한 안정성이 보장되야 하죠..
그에 따라 일반적인 애플리케이션을 만들때와 다르게 에러처리가 거의 완벽에 가까워야 합니다.
( 만약에 처리할 수 없는 예외가 발생한다면 극단적인 경우에도 로그라도 하나 남겨둬야 겠죠.. )

작업을 하다가 다음에 예제코드와 비슷한 코드를 작성하게 되었습니다.

thread1
SetEvent(.......); // 이벤트를 시그널해줍니다.

thread2
WaitForSingleObject(.....); // 시그널 될 때까지 기다립니다.

매우 전형적인 코드입니다.
thread2에서는 기다리고 thread1는 이벤트를 시그널해줍니다.

그러나 여기서 한가지 문제가 생긴 부분은 너무 극단적인 상황까지 고려하는게 아닌가 생각되지만
SetEvent 함수에 원형은 다음과 같습니다.

BOOL WINAPI SetEvent(
__in HANDLE hEvent
);

위에서 가장 중요한 부분은 리턴값이 BOOL이라는 것입니다.
즉, 이 함수는 실패할 수 있습니다.

실패할 수 있는 경우에 대해서 생각해보면 바로 떠오르것은 핸들을 잘 못 넘겼을 경우입니다.
하지만 그 외에 경우가 생긴다면 골치아파집니다.
특히나 소켓 프로그래밍이라면 상당히 짜증나겠죠.
예를 들어 메모리가 부족할 경우에 SetEvent가 실패해버리면 다른쪽에 스레드는 무한대기할지도 모릅니다.
물론 메모리가 부족할 경우라는 것은 만약에 그럴지도 모른다는 경우이고 MSDN에 경우에는
GetLastError를 통해서 리턴값이 FALSE일 경우에는 에러코드를 확인하라고 나왔으며 정작
어떠한 경우에 에러가 발생할 수 있는지는 적혀있지 않습니다.

하지만 우리는 최악에 상황까지 고려해야할 경우 저 함수가 어떠한 경우에 실패할 수 있는지에 대해서
알아보고 그에 대한 상황에 대처할 수 있어야 할것입니다.

그렇다면 저 함수가 어떠한 에러코드를 리턴하는지 알려면 결국엔 내부구현을 알아야 합니다.
따라서 해당 함수에 내부구현을 지금부터 따라가보겠습니다.

우선 SetEvent 함수는 Kernel32.dll에 있습니다.
다음과 같이 말입니다.

.text:7DD71653 ; BOOL __stdcall SetEventStub(HANDLE hEvent)
.text:7DD71653                 public _SetEventStub@4
.text:7DD71653 _SetEventStub@4 proc near
.text:7DD71653
.text:7DD71653 hEvent          = dword ptr  8
.text:7DD71653
.text:7DD71653                 mov     edi, edi
.text:7DD71655                 push    ebp
.text:7DD71656                 mov     ebp, esp
.text:7DD71658                 pop     ebp
.text:7DD71659                 jmp     short _SetEvent@4 ; SetEvent(x)
.text:7DD71659 ; ---------------------------------------------------------------------------
.text:7DD7165B                 align 10h
.text:7DD71660
.text:7DD71660 ; BOOL __stdcall SetEvent(HANDLE hEvent)
.text:7DD71660 _SetEvent@4:                            ; CODE XREF: SetEventStub(x)+6j
.text:7DD71660                                         ; WerpRecoveryInProgress(x)+7Dp ...
.text:7DD71660                 jmp     ds:__imp__SetEvent@4 ; SetEvent(x)
.text:7DD71660 _SetEventStub@4 endp

익스포트 되어 있는 SetEvent 함수는 SetEventStub 함수를 호출하고 실제 내부에 _SetEvent@4
로 점프하게 됩니다.
그리고 __imp__SetEvent 를 참조하여 다시 어딘가로 점프하게 됩니다. ( 임포트 테이블 )

그 어딘가는 결국엔 Kernelbas.dll 에 있는 SetEvent 함수입니다.
다음과 같이 생겼습니다.

.text:7D86013D ; BOOL __stdcall SetEvent(HANDLE hEvent)
.text:7D86013D                 public _SetEvent@4
.text:7D86013D _SetEvent@4     proc near               ; CODE XREF: OutputDebugStringA(x)+1F7p
.text:7D86013D
.text:7D86013D hEvent          = dword ptr  8
.text:7D86013D
.text:7D86013D                 mov     edi, edi
.text:7D86013F                 push    ebp
.text:7D860140                 mov     ebp, esp
.text:7D860142                 push    0
.text:7D860144                 push    [ebp+hEvent]
.text:7D860147                 call    ds:__imp__NtSetEvent@8 ; NtSetEvent(x,x)
.text:7D86014D                 test    eax, eax
.text:7D86014F                 jl      short loc_7D860156
.text:7D860151                 xor     eax, eax
.text:7D860153                 inc     eax
.text:7D860154                 jmp     short loc_7D86015E
.text:7D860156 ; ---------------------------------------------------------------------------
.text:7D860156
.text:7D860156 loc_7D860156:                           ; CODE XREF: SetEvent(x)+12j
.text:7D860156                 push    eax
.text:7D860157                 call    _BaseSetLastNTError@4 ; BaseSetLastNTError(x)
.text:7D86015C                 xor     eax, eax
.text:7D86015E
.text:7D86015E loc_7D86015E:                           ; CODE XREF: SetEvent(x)+17j
.text:7D86015E                 pop     ebp
.text:7D86015F                 retn    4
.text:7D86015F _SetEvent@4     endp

디컴파일 하면 다음과 같습니다.
BOOL __stdcall SetEvent(HANDLE hEvent)
{
  int v1; // eax@1
  BOOL result; // eax@2

  v1 = NtSetEvent(hEvent, 0);
  if ( v1 < 0 )
  {
    BaseSetLastNTError(v1);
    result = 0;
  }
  else
  {
    result = 1;
  }
  return result;
}

NtSetEvent 함수를 호출하고 있고 값이 0보다 작으면 에러입니다.
당연히 NtSetEvent는 ntdll.dll에 있겠고 시스템 콜을 호출하게 될것입니다.

그리고 그 시스템콜은 ntoskrnl.exe에 있을것이고 이름도 NtSetEvent일것입니다.
그 구현코드는 다음과 같습니다. ( 여기서부터는 디컴파일한 결과만 보여드리겠습니다. )

NTSTATUS __stdcall NtSetEvent(HANDLE EventHandle, PULONG PreviousState)
{
  char v2; // al@1
  PULONG v3; // edi@1
  PULONG v4; // ecx@3
  LONG v5; // esi@7
  NTSTATUS v7; // eax@6
  int AccessMode; // [sp+14h] [bp-20h]@1
  CPPEH_RECORD ms_exc; // [sp+1Ch] [bp-18h]@1
  PVOID Event; // [sp+18h] [bp-1Ch]@6

  v2 = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  LOBYTE(AccessMode) = *(_BYTE *)(*MK_FP(__FS__, 292) + 314);
  ms_exc.disabled = 0;
  v3 = PreviousState;
  if ( PreviousState )
  {
    if ( v2 )
    {
      v4 = PreviousState;
      if ( PreviousState >= MmUserProbeAddress )
        v4 = MmUserProbeAddress;
      *v4 = *v4;
    }
  }
  ms_exc.disabled = -2;
  v7 = ObReferenceObjectByHandle(EventHandle, 2u, ExEventObjectType, AccessMode, &Event, 0);
  PreviousState = (PULONG)v7;
  if ( v7 >= 0 )
  {
    v5 = KeSetEvent((PRKEVENT)Event, 1, 0);
    ObfDereferenceObject();
    if ( v3 )
    {
      if ( (_BYTE)AccessMode )
        *v3 = v5;
      else
        *v3 = v5;
    }
  }
  return (NTSTATUS)PreviousState;
}

NtSetEvent에 유출 된 Windows 2000 소스코드는 다음과 같습니다.
NTSTATUS
NtSetEvent (
    IN HANDLE EventHandle,
    OUT PLONG PreviousState OPTIONAL
    )

/*++

Routine Description:

    This function sets an event object to a Signaled state and attempts to
    satisfy as many waits as possible.

Arguments:

    EventHandle - Supplies a handle to an event object.

    PreviousState - Supplies an optional pointer to a variable that will
        receive the previous state of the event object.

Return Value:

    TBS

--*/

{

    PVOID Event;
    KPROCESSOR_MODE PreviousMode;
    LONG State;
    NTSTATUS Status;
#if DBG

    //
    // Sneaky trick here to catch sleazy apps (csrss) that erroneously call
    // NtSetEvent on an event that happens to be somebody else's
    // critical section. Only allow setting a protected handle if the low
    // bit of PreviousState is set.
    //
    OBJECT_HANDLE_INFORMATION HandleInfo;

#endif

    //
    // Establish an exception handler, probe the previous state address if
    // specified, reference the event object, and set the event object. If
    // the probe fails, then return the exception code as the service status.
    // Otherwise return the status value returned by the reference object by
    // handle routine.
    //

    try {

        //
        // Get previous processor mode and probe previous state address
        // if necessary.
        //

        PreviousMode = KeGetPreviousMode();
#if DBG
        if ((PreviousMode != KernelMode) &&
            (ARGUMENT_PRESENT(PreviousState)) &&
            (PreviousState != (PLONG)1)) {
            ProbeForWriteLong(PreviousState);
        }
#else
        if ((PreviousMode != KernelMode) && (ARGUMENT_PRESENT(PreviousState))) {
            ProbeForWriteLong(PreviousState);
        }
#endif

        //
        // Reference event object by handle.
        //

#if DBG
        Status = ObReferenceObjectByHandle(EventHandle,
                                           EVENT_MODIFY_STATE,
                                           ExEventObjectType,
                                           PreviousMode,
                                           &Event,
                                           &HandleInfo);
        if (NT_SUCCESS(Status)) {

            if ((HandleInfo.HandleAttributes & 1) &&
                (PreviousState != (PLONG)1)) {
#if 0
                //
                // This is a protected handle. If the low bit of PreviousState is NOT set,
                // break into the debugger
                //

                DbgPrint("NtSetEvent: Illegal call to NtSetEvent on a protected handle\n");
                DbgBreakPoint();
                PreviousState = NULL;
#endif
            }
        } else {
            if ((KeGetPreviousMode() != KernelMode) &&
                (EventHandle != NULL) &&
                ((NtGlobalFlag & FLG_ENABLE_CLOSE_EXCEPTIONS) ||
                 (PsGetCurrentProcess()->DebugPort != NULL))) {

                Status = KeRaiseUserException(STATUS_INVALID_HANDLE);

            }
        }
#else
        Status = ObReferenceObjectByHandle(EventHandle,
                                           EVENT_MODIFY_STATE,
                                           ExEventObjectType,
                                           PreviousMode,
                                           &Event,
                                           NULL);
#endif

        //
        // If the reference was successful, then set the event object to the
        // Signaled state, dereference event object, and write the previous
        // state value if specified. If the write of the previous state fails,
        // then do not report an error. When the caller attempts to access the
        // previous state value, an access violation will occur.
        //

        if (NT_SUCCESS(Status)) {
            State = KeSetEvent((PKEVENT)Event, ExpEventBoost, FALSE);
            ObDereferenceObject(Event);
            if (ARGUMENT_PRESENT(PreviousState)) {
                try {
                    *PreviousState = State;

                } except(ExSystemExceptionFilter()) {
                }
            }
        }

    //
    // If an exception occurs during the probe of the previous state, then
    // always handle the exception and return the exception code as the status
    // value.
    //

    } except(ExSystemExceptionFilter()) {
        return GetExceptionCode();
    }

    //
    // Return service status.
    //

    return Status;
}


거의 똑같다고 봐도 되겠습니다.
Windows 7에 있는코드인데 윈도우즈2000소스와 거의 똑같습니다.
여기서 PreviousState에 경우에는 인자를 NULL로 넘겼으니 상관이 없습니다.
체크할 필요가 없습니다.

가장 먼저 체크해야 할 부분은 ObReferenceObjectByHandle 함수입니다.
이 함수가 언제 실패하느냐 여부를 알아야 합니다.

WDK 문서를 보면 다음과 같은 에러코드가 정의되어 있습니다.
ObReferenceObjectByHandle

STATUS_SUCCESS <= 성공했습니다.

STATUS_OBJECT_TYPE_MISMATCH <= 핸들에 대한 타입이 잘 못 되었을 경우인데 API에서 해줌으로

이 에러가 날 경우는 없습니다.

STATUS_ACCESS_DENIED <= 접근거부에러인데 우리가 만든 핸들을 우리가 접근하므로 이 에러는 발생하지 않습니다.

STATUS_INVALID_HANDLE <= 핸들이 잘 못 된 경우입니다. 이 에러는 소프트웨어 로직상 핸들을 잘 못 넘겼을 경우에 발생하며

현재 우리가 알려고 하는것은 아주 특수한 경우에 발생할 수 있는 에러가 있는지 알기 위함이므로 무시합니다.


따라서 ObReferenceObjectByHandle함수는 핸들을 잘 못 넘기지 않는 이상은 현재로써 에러가 발생하지 않을것이고

핸들을 잘 못 넘기는건 프로그램의 로직상 문제이므로 무시합니다.


그 다음에 호출하는 함수는 KeSetEvent 함수입니다.

이 함수는 WDK에 문서에 보면 이 함수리턴값이 실패값이 들어간다는 말은 나와 있지 않습니다.

그렇다면 좀 더 확실하게 알기 위해서 KeSetEvent 함수 내부를 보아야 합니다.

내부 코드를 보기전에 PKEVENT는 다음과 같이 생겼습니다.


typedef struct _KEVENT {
    DISPATCHER_HEADER Header;
} KEVENT, *PKEVENT, *PRKEVENT;


typedef struct _DISPATCHER_HEADER {
    union {
        struct {
            UCHAR Type;                 // All (accessible via KOBJECT_TYPE)

            union {
                union {                 // Timer
                    UCHAR TimerControlFlags;
                    struct {
                        UCHAR Absolute              : 1;
                        UCHAR Coalescable           : 1;
                        UCHAR KeepShifting          : 1;    // Periodic timer
                        UCHAR EncodedTolerableDelay : 5;    // Periodic timer
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;

                UCHAR Abandoned;        // Queue
                BOOLEAN Signalling;     // Gate/Events
            } DUMMYUNIONNAME;

            union {
                union {
                    UCHAR ThreadControlFlags;  // Thread
                    struct {
                        UCHAR CpuThrottled      : 1;
                        UCHAR CycleProfiling    : 1;
                        UCHAR CounterProfiling  : 1;
                        UCHAR Reserved          : 5;
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;
                UCHAR Hand;             // Timer
                UCHAR Size;             // All other objects
            } DUMMYUNIONNAME2;

            union {
                union {                 // Timer
                    UCHAR TimerMiscFlags;
                    struct {

#if !defined(_X86_)

                        UCHAR Index             : TIMER_EXPIRED_INDEX_BITS;

#else

                        UCHAR Index             : 1;
                        UCHAR Processor         : TIMER_PROCESSOR_INDEX_BITS;

#endif

                        UCHAR Inserted          : 1;
                        volatile UCHAR Expired  : 1;
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;
                union {                 // Thread
                    BOOLEAN DebugActive;
                    struct {
                        BOOLEAN ActiveDR7       : 1;
                        BOOLEAN Instrumented    : 1;
                        BOOLEAN Reserved2       : 4;
                        BOOLEAN UmsScheduled    : 1;
                        BOOLEAN UmsPrimary      : 1;
                    } DUMMYSTRUCTNAME;
                } DUMMYUNIONNAME;
                BOOLEAN DpcActive;      // Mutant
            } DUMMYUNIONNAME3;
        } DUMMYSTRUCTNAME;

        volatile LONG Lock;             // Interlocked
    } DUMMYUNIONNAME;

    LONG SignalState;                   // Object lock
    LIST_ENTRY WaitListHead;            // Object lock
} DISPATCHER_HEADER;


LONG __stdcall KeSetEvent(PRKEVENT Event, KPRIORITY Increment, BOOLEAN Wait)
{
  LONG result; // eax@2
  void *v5; // ebx@6
  int v6; // edi@6
  LONG v7; // edi@13
  signed int v14; // [sp+10h] [bp-8h]@4
  int v15; // [sp+14h] [bp-4h]@6

  _ESI = Event;
  if ( Event->Header.Type & 0x7F )
  {
    v14 = 0;
LABEL_6:
    v5 = v320;
    LOBYTE(v15) = KeRaiseIrqlToDpcLevel();
    v6 = 0;
    while ( 1 )
    {
      _EAX = _ESI;
      __asm { lock bts dword ptr [eax], 7 }
      if ( !_CF )
        break;
      do
      {
        ++v6;
        if ( v6 & HvlLongSpinCountMask || !(HvlEnlightenments & 0x40) )
          __asm { pause }
        else
          HvlNotifyLongSpinWait(v6);
      }
      while ( (char)*(_DWORD *)&_ESI->Header.Type < 0 );
    }
    v7 = _ESI->Header.SignalState;
    _ESI->Header.SignalState = 1;
    if ( !v7 )
    {
      if ( v14 == v7 )
        KiSignalSynchronizationObject(v5, _ESI);
      else
        KiSignalNotificationObject(v5);
    }
    _EAX = -129;
    __asm { lock and [esi], eax }
    KiExitDispatcher(v5, Wait, 1, Increment, v15);
    return v7;
  }
  result = 1;
  if ( Event->Header.SignalState != 1 || Wait )
  {
    v14 = 1;
    goto LABEL_6;
  }
  return result;
}


내부코드를 보면 Evente 객체에 SignalState 객체를 참고하거나 값을 변경할 뿐 에러를 리턴할 만한 부분은 없습니다.

여기까지 왔다면 이제 결론이 나옵니다.

SetEvent 함수를 사용 할 때 잘 못 된 핸들을 넘기지 않는 이상은 해당 리턴값이 FALSE를 리턴하지 않습니다.

따라서 시스템이 특수한 상황 ( 메모리 부족 ) 에 에러가 발생할 소지가 없으므로 SetEvent 함수는 무조건

성공한다고 가정합니다.

( 주의 : 예제코드에 한해서만 그렇다는 것이며 Event핸들은 여러 프로세스에 의해서 공유되는 코드로 작성하게

  된다면 접근거부 에러도 발생 할 수 있습니다. 그러나 이러한 부분은 프로그래머가 작성하게 되는 코드이므로

  마찬가지로 문제가 발생한다면 로직상에 문제이지 시스템에 특수한 상황에서 발생하는 에러는 아닙니다. )


VirtualBox 3.0.6 r52128 네트워크 공유 폴더 접근 시 BSDO




네트워크 공유 폴더에 접근 시 랜덤하게 위와 같은 블루 스크린을 볼 수가 있습니다.
저같은 경우에는 네트워크 공유 폴더에 접근 후에 파일의 속성을 볼려고 시도를 하면 파일 속성창이
바로 뜨지 않고 딜레이가 생기다가 간혹 블루 스크린이 뜨는 것 같습니다.

밑에 링크를 따라가면 해결책이 나오긴 했는데 워낙에 랜덤하게 발생하는 문제라서 테스트는 해보지는 못 하였네요.
http://justcheckingonall.wordpress.com/2009/01/08/windows-xp-under-virtualbox/

이것말고도 VirtualBox에 경우 블루 스크린이 뜰 때 간혹 화면이 깨진 상태로 GuestOS가 멈춰버리는 문제점도 있는데
아직까지 안정성에는 좀 문제가 있는 것 같습니다. 하지만 속도는 매우 빠르네요..

Delphi 2010에 Format Source 기능 델파이

Delphi 2010에는 다음과 같은 메뉴가 새로 생겼습니다.




Format Source라는 메뉴인데 이 기능은 코드를 정렬해주는 기능입니다.
아주 깔끔하게 코드를 정렬해줍니다.

예를 들어 다음과 같은 개념없는 코드가..




그냥 아무 생각없이 Ctrl + D 를 누르면.....
다음과 같이 바뀌는 것입니다.





커널 디버거 탐지 기법 우회 리버스 엔지니어링

다음 코드 2줄로 우회가 가능합니다.

extern PBOOLEAN KdDebuggerEnabled; // 선언..
*KdDebuggerEnabled = FALSE; // 우회 코드

KdDebuggerEnabled 변수를 직접적으로 접근하지 않는 방식을 사용하여도 이 방법은 통합니다.
왜냐하면 결국엔 그 함수들은 저 전역변수를 참조하게 됩니다.
심지어 WinDbg 조차도 저렇게 값을 임의로 바꾸면 Attach가 안됩니다.

따라서 이러한 경우 커널 디버거는 우회가 되는데 WinDbg로 Attach 하여서 디버깅하는데 조금 문제가 생길것입니다.
따라서 디버거를 탐지하는 코드가 실행되는 경우에는 우선 저렇게 전역변수를 FALSE로 바꾸고
디버거 탐지코드가 실행되었다면 다시 KdDebuggerEnabled 커널 전역변수를 TRUE로 바꿔주면
WinDbg로 Attach 도 되고 디버깅이 가능해집니다.

커널 디버거를 탐지하는 대상 코드는 아래와 같고 이러한 코드는 이전에 말했듯이 내부적으로
위에 커널 전역변수를 사용하므로 우회가 됩니다.

bool __stdcall IsDebugPort(PVOID Object)
{
  void *pZwQueryInformationProcess; // eax@2
  bool result; // eax@4
  signed int bDebug; // [sp+10h] [bp-4h]@1
  int v4; // [sp+Ch] [bp-8h]@2
  UNICODE_STRING DestinationString; // [sp+4h] [bp-10h]@3

  bDebug = 0;
  if ( PsLookupProcessByProcessId(Object, &Object) >= 0
    && ((ObOpenObjectByPointer(Object, 0, 0, 0, 0, 0, &v4), ObfDereferenceObject(Object), pZwQueryInformationProcess = (void *)g_ZwQueryInformationProcess, g_ZwQueryInformationProcess)
     || (RtlInitUnicodeString(&DestinationString, L"ZwQueryInformationProcess"), pZwQueryInformationProcess = MmGetSystemRoutineAddress(&DestinationString), g_ZwQueryInformationProcess = (int)pZwQueryInformationProcess, pZwQueryInformationProcess)) )
  {
    ((int (__stdcall *)(int, signed int, signed int *, signed int, _DWORD))pZwQueryInformationProcess)(
      v4,
      7,
      &bDebug,
      4,
      0);                                                       // 이 함수에 두 번째 인자 ProcessDebugPort를 넘기고 있다.
                                                                // 즉, 이것은 디버그중인지 체크한다고 보면 될것이다.
    result = bDebug == -1;                                      // -1(0xFFFFFFFF) 일 경우 디버깅중인 상태입니다.
  }
  else
  {
    result = 0;
  }
  return result;
}

일부 보안 드라이버에서는 실시간으로 KdDebuggerEnabled 에 직접 접근하여 디버깅중인지 체크하는데
이러한 경우 해당 드라이버에 한해서 IAT만 변조하는 방식으로 우회하여도 됩니다.
그러나 일반적인 경우 드라이버 초기화 작업에서만 디버깅중인지 체크하므로 위에 방법으로 왠만하면 우회가 가능합니다.

1 2 3 4 5 6 7 8