특정 모듈이 올라왔을 때 해당 스레드 컨텍스트를 포함하는 프로세스 주소 공간에 새로운 메모리를 할당하려 합니다.

유저 영역에선 VirtualAlloc 으로 할당하겠지만 커널에선 ZwAllocateVirtualMemory 로 할당하겠지요.


일단 특정 모듈이 올라왔을 때를 잡으려면 PsSetLoadImageNotifyRoutine 함수를 써주면 됩니다.


NTSTATUS PsSetLoadImageNotifyRoutine(

  __in  PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine

);


위 함수로 NotifyRoutine을 등록하면 바이너리 이미지(dll, exe..) 가 메모리에 로드될 때마다 NotifyRoutine이 호출됩니다.


콜백 함수 원형은 다음과 같습니다.


VOID

  (*PLOAD_IMAGE_NOTIFY_ROUTINE)(

    __in_opt PUNICODE_STRING  FullImageName,

    __in HANDLE  ProcessId,

    __in PIMAGE_INFO  ImageInfo

    );


FullImageName 은 드라이브 경로를 제외한 해당 모듈의 절대경로가 넘어옵니다.

ProcessId는 로드되는 모듈의 프로세스 ID를 나타냅니다.

ImageInfo는 로드되는 모듈의 베이스 주소, 이미지 사이즈 등의 정보들이 있습니다.


그리고 해당 콜백루틴으로 들어왔을 때 컨텍스트는 ProcessId 로 넘어온 프로세스 입니다.


전 여기서 제가 원하는 모듈이 올라오면 해당 모듈 내부에 메모리 공간을 할당하고 거기에 특정 작업을 하려고 했습니다.

일단 제가 사용하려는 함수의 주소를 커널에서 얻기 위해서 kernel32.dll 이 로드될 때까지 기다렸다가 할당하려고 했습니다.


간략한 슈도코드를 보면

if( ProcessId == 타겟 프로세스 )

{

if( FullImageName == "kernel32.dll" )

{

ZwAllocateVirtualMemory(...)

}

}

이러한 형태입니다.


하지만 ZwAllocateVirtualMemory 함수를 CALL 한순간 DeadLock이 걸린 것처럼 그 다음 명령어로 제어가 오질 않더군요.

생성되어 있는 프로세스를 봐도 프로세스 목록에는 있지만 어떠한 창도 뜨지않으며 대기되어 있는 상태로 멈춰있었습니다.

Process Explorer에서 kill 해봐도 죽지도 않고 반응도 없더군요. 프로세스에 가해지는 어떠한 이벤트도 다 씹어버리는 상태..


다시 OS를 재부팅하고 이번엔 kernel32.dll 이 로드될 때까지 기다리는 것이 아닌 ntdll.dll 이 로드될 때까지 기다려보았습니다.

kernel32.dll보다 ntdll.dll이 더 먼저 로드되므로 아까보다 더욱 빨리 ZwAllocateVirtualMemory 가 호출되겠지요.


음..?! 이번엔 ZwAllocateVirtualMemory 가 성공적으로 리턴을 하였습니다.


kernel32.dll 모듈이 올라왔을 땐 DeadLock 상태로 빠져버리더니 ntdll.dll 모듈이 올라왔을 땐 성공적으로 호출이 되다니..

뭔가 다르니 이러한 결과가 발생했겠지요.


먼저 해당 프로세스에서 ntdll.dll이 로드되고 kernel32.dll이 로드되는데 kernel32.dll이 로드될 때 ntdll.dll에선 추가적으로 특정 코드가 더 실행될것입니다.


구글링 해보니 EPROCESS 구조체 내부의 특정 비트가 의심이 됩니다.


nt!_EPROCESS

   .....

   +0x0e0 VirtualSize      : Uint4B

   +0x0e4 SessionProcessLinks : _LIST_ENTRY

   +0x0ec DebugPort        : Ptr32 Void

   +0x0f0 ExceptionPortData : Ptr32 Void

   +0x0f0 ExceptionPortValue : Uint4B

   +0x0f0 ExceptionPortState : Pos 0, 3 Bits

   +0x0f4 ObjectTable      : Ptr32 _HANDLE_TABLE

   +0x0f8 Token            : _EX_FAST_REF

   +0x0fc WorkingSetPage   : Uint4B

   +0x100 AddressCreationLock : _EX_PUSH_LOCK

   +0x104 RotateInProgress : Ptr32 _ETHREAD

   +0x108 ForkInProgress   : Ptr32 _ETHREAD

   +0x10c HardwareTrigger  : Uint4B

   +0x110 PhysicalVadRoot  : Ptr32 _MM_AVL_TABLE

   .....


EPROCESS 구조체 내부의 AddressCreationLock 라는 멤버변수입니다.

ntdll.dll이 로드될 때는 저 값이 0 이더니

kernel32.dll이 로드될 땐 저 값이 1 이었습니다.


그러면 저 비트가 언제 수정되는지 감지하기 위해 Hardware BP를 걸고 콜백함수 들어오기 전 마지막으로 수정되는 때를 봐보았습니다.

( ntdll.dll이 로드되고 kernel32.dll 이 로드되기 전 상황 )


Breakpoint 5 hit

nt!MiMapViewOfSection+0x1ad:

83286ca6 8b4508          mov     eax,dword ptr [ebp+8]

83286ca9 c744241001000000 mov     dword ptr [esp+10h],1

83286cb1 66ff8f86000000  dec     word ptr [edi+86h]

83286cb8 8d8800010000    lea     ecx,[eax+100h]                            ; EPROCESS 의 AddressCreationLock 주소 가져옴

83286cbe 894c2420        mov     dword ptr [esp+20h],ecx

83286cc2 8bd1            mov     edx,ecx

83286cc4 f00fba2a00      lock bts dword ptr [edx],0                        ; 여기서 값 변경

83286cc9 7308            jae     nt!MiMapViewOfSection+0x1b7 (83286cd3)

( bts bitBase , n : bitBase의 비트 n을 선택하여 그 비트를 Carry 플래그로 복사하고 비트 n을 1로 설정한다. - 처음보는 명령어...ㅠㅠ )


1: kd> dt _EX_PUSH_LOCK 86f58ad0+0x100

nt!_EX_PUSH_LOCK

   +0x000 Locked           : 0y1

   +0x000 Waiting          : 0y0

   +0x000 Waking           : 0y0

   +0x000 MultipleShared   : 0y0

   +0x000 Shared           : 0y0000000000000000000000000000 (0)

   +0x000 Value            : 1

   +0x000 Ptr              : 0x00000001 Void


1: kd> kn

 # ChildEBP RetAddr  

00 95423b58 83286e3a nt!MiMapViewOfSection+0x1ad

01 95423b88 83287599 nt!MmMapViewOfSection+0x2a

02 95423c04 8307d1ea nt!NtMapViewOfSection+0x204

03 95423c04 77d670b4 nt!KiFastCallEntry+0x12a


저 값이 1로 딱 변한 순간 콜스택을 봐보니 NtMapViewOfSection 함수가 실행되었네요.


이번엔 콜백루틴이 호출됬을 때 콜스택입니다.

 # ChildEBP RetAddr  

00 95423a0c 832ae39c DllInjectionInKernelSys!LoadImageNotifyRoutine+0x69     ; 콜백루틴

01 95423a34 8329627f nt!PsCallImageNotifyRoutines+0x62

02 95423ae8 83286d4a nt!MiMapViewOfImageSection+0x670

03 95423b58 83286e3a nt!MiMapViewOfSection+0x22e

04 95423b88 83287599 nt!MmMapViewOfSection+0x2a                                    ; 위 콜스택 1,2,3과 이번 콜스택 4,5,6 같음

05 95423c04 8307d1ea nt!NtMapViewOfSection+0x204

06 95423c04 77d670b4 nt!KiFastCallEntry+0x12a


즉, NtMapViewOfSection 함수는 EPROCESS 의 AddressCreationLock 값을 변경하고 함수를 빠져나가지 않은 상태로 바로 콜백 루틴을 호출한다는 것입니다.


실제로 테스트를 해보니 AddressCreationLock이 0으로 설정되어 있으면 ZwAllocateVirtualMemory가 성공하고

1로 설정되어 있으면 DeadLock이 걸리더군요.


kernel32.dll이 로드 됬을 때 호출 된 콜백 루틴 내부에서 해당 비트를 0으로 설정하고 진행하니 ZwAllocateVirtualMemory가 DeadLock이 걸리지 않고 잘 진행됨을 알 수 있었습니다.


 결론!!

PsSetLoadImageNotifyRoutine 로 등록한 콜백루틴에서 ntdll.dll이 아닌 다른 모듈이 로드 된 경우

해당 컨텍스트 프로세스 영역에 메모리 할당할 땐 EPROCESS 구조체의 AddressCreationLock 멤버변수를 확인한다.

확인 결과 1로 셋팅되어 있다면 콜백루틴 내부에서 0으로 바꾸고 메모리 할당 후 다시 1로 바꾼다.


메모리 할당 위해 잠깐 0으로 바꾼다고해서 오작동이 일어날 것 같진 않네요 ..그럴까요?.. =_=;;


* 위 모든 테스트는 Windows 7 x86 에서 진행되었습니다.


(추가)

Windows 8부터는 위 문제가 해결되었습니다.

PsCallImageNotifyRoutines 루틴이 호출 될 때에는 EPROCESS->AddressCreationLock 값은 무조건 0입니다.

그렇다고 해당 멤버변수를 안쓰는게 아니라 멤버변수를 써야하는 루틴의 위치가 옮겨 진 것입니다.


8185f234 8bc3            mov     eax,ebx

8185f236 8d4dc0          lea     ecx,[ebp-40h]

8185f239 e831310900      call    nt!PsCallImageNotifyRoutines (818f236f)        콜백 호출

8185f23e 8bcb            mov     ecx,ebx

8185f240 e83036deff      call    nt!ObfDereferenceObject (81642875)

8185f245 8b5df4          mov     ebx,dword ptr [ebp-0Ch]

8185f248 53              push    ebx

8185f249 e86d020000      call    nt!MmUnsecureVirtualMemory (8185f4bb)        해당 비트 값 설정


이렇게 콜백 호출 후 해당 비트를 설정하게 됩니다.

당연히 Windows 8.1도 이렇게 되어 있겠지요.




Posted by Ezbeat