특정 모듈이 올라왔을 때 해당 스레드 컨텍스트를 포함하는 프로세스 주소 공간에 새로운 메모리를 할당하려 합니다.
유저 영역에선 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
이번엔 콜백루틴이 호출됬을 때 콜스택입니다.
# 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도 이렇게 되어 있겠지요.
'My Study > Programming&Theory' 카테고리의 다른 글
ZwProtectVirtualMemory 의 악몽 (11) | 2013.07.04 |
---|---|
Driver에서 ZwReadVirtualMemory 사용하기 (0) | 2013.06.30 |
Windows Clipboard : GetClipboardData (2) | 2013.06.06 |
Windows Clipboard : OpenClipboard (2) | 2013.06.03 |
ERROR C4996 (8) | 2013.05.23 |