뭐 난 마비노기는 아예 하지도 않지만 , 지인 몇명이 하고있고,
최근에는 후킹에 관심을 쪼까 가지고 있어서~

최근 들어서 Ahnlab의 핵쉴드에도 Named Mutant를 기반으로 한 다중실행 방지기법이 도입되었다.
그 결과  , 핵쉴드를 채용한 대부분의 게임은 일반적인 방법으론 Multiple Execution이 불가능하게 되었는데...

뭐 대충 검색을 통해서 멀티로 마비노기를 하는사람들의 글을 검색해보니...
일명    멀티노기    라는  누가 만든 프로그램이 막혀버려서,

Named Mutex  다중실행 방지방법을 알긴 아는사람이  핵쉴드 언패킹은 못하겠고,  커널 후킹도 못하겠고 하니,
Sysinternal의 Process Explorer 를 이용하여  , Mutant핸들을 강제로 종료시키는 방법을 전파했나보다.
인터넷에 퍼진 방법을 적용하면 뭐 잘 되긴 하지만 ,  실제로는 2개의 Mutant만 종료해주면 되는데,
방법을 살펴보면 그와 관계없는 , 즉 실질적으로  Thread Synchronous에 사용되는  정말 목적이 있는 Mutant까지 종료하는것으로 판단된다. 그러면 Application이  Instable 해질수가 있을텐데...

뭐 잡담은 여기까지 하고,


이 방법이 통하는 원리는,
첫번째로 실행한 클라이언트의 Named Mutant 핸들을 Close 해버리면,
두번째 이상부터 실행되는 클라이언트는 당연히  첫번째 클라이언트가 생성해놓은 Named Mutant의 Exist 여부를 알수없기 때문에,
두번째 클라이언트 역시 새로운 Mutant를 생성하게 되고 , 정상적으로 게임이 실행되는것이다.

마비노기 클라이언트를 분석 해본결과 ,  Mutant 를 이용해서 다중실행방지를 하는  순서는 대략 아래의 과정임을 알수 있었다.
(마비노기 런쳐에서 FindWindow 를 이용해서  다중실행을 감지하는 방법은  너무도 간단하기 때문에  ,여기선 그냥 생략한다.)


아래는 마비노기에서  사용하고 있는 방법을 필자 생각대로 아무렇게나 나열해본 코드이다.

/*
  * Global Variable
  * /
HANDLE hGlobalHShieldMutex;

hGlobalHShieldMutex = OpenMutexA(MUTEX_ALL_ACCESS,FALSE,"Global\\?磵HxFV`rZxF?퐹xv");
if(hGlobalHShieldMutex || GetLastError() == ERROR_ACCESS_DENIED)
{
MessageBoxA(NULL,"핵쉴드가 실행중입니다","에러",MB_OK);
return 518;
}
else
{
hGlobalHShieldMutex = CreateMutexA(NULL,FALSE,"Global\\?磵HxFV`rZxF?퐹xv");
if(hGlobalHShieldMutex == NULL)
{
MessageBoxA(NULL,"핵쉴드 초기화 작업에 실패했습니다","Error",MB_OK);
return 518;
}
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
MessageBoxA(NULL,"핵쉴드가 실행중입니다","Error",MB_OK);
return 518;
}

HANDLE hGlobalDevcatMutex = CreateMutexA(NULL,FALSE,"Global\\Nexon,DevCat,Mabinogi");
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
MessageBoxA(NULL,"마비노기가 이미 실행중입다","Error",MB_OK);
TerminateProcess(GetCurrentProcess(),0);
return;
}
if(hGlobalDevcapMutex == NULL)
{
HANDLE hLocalDevcatMutex = CreateMutexA(NULL,FALSE,"Nexon,DevCat,Mabinogi");
if(GetLastError() == ERROR_ALREADY_EXISTS || hLocalDevcatMutex == NULL)
{
MessageBoxA(NULL,"마비노기가 실행중입니다.","Error",MB_OK);
return;
}
}
}

(위 코드는 필자가 분석 결과를 토대로 지어낸 슈도코드이기 때문에, 원본과 비교했을때 정확하지 않을 수 있음)

분석 결과 ,  핵쉴드 뮤턴트를 검색하여 , 먼저 핵쉴드가 실행중인지 판단한다.
핵쉴드가 실행중이지 않다면 , 글로벌 디브캣 뮤턴트를 생성하고 , 여기서 뮤턴트 생성에 실패하면,
세번째 로컬 뮤턴트를 생성하려고 시도한다.
로컬 뮤턴트 생성에도 실패하면 최종적으로 다중실행이라고 판단하고 종료한다.

개발진 측에서 다중실행 방지에 뭔가 목숨건것 같은 느낌이 든다.


뭐 무력화 하는방법은...
세번째 로컬 뮤턴트는 볼필요없고 , 결국은 첫번째 글로벌 핵쉴드 뮤턴트와 , 두번째 글로벌 디브캣 뮤턴트만 따내버리면 그만이다..
여기서 생각할수 있는 방법이 , 뮤턴트는 Kernel Object이기 때문에 , Kernel을 따내는것이 가장 손쉬운 방법일 것이다.
ZwClose 를 이용한  Mutant Force Close 도 생각해볼 수 있지만,

뭐 초간단하게 조작하는 방법은 어떤것이 있을까...?
개발진 측이 다중실행을 방지하려고 도입한 기법을  되래 역으로 이용하는 방법이 여기 있으니.......
(사실 어디서 퍼온 방법이 아니고 , 모니터 뚫어지게 쳐다보다가 갑자기 생각난 방법임 -_-;)


그 방법은  Named Mutant를 생성하려고 전달된  ObjectAttributes->ObjectName 을
프로세스가 NtCreateMutant를 호출할때마다  Randomize 하게 실시간으로 생성하는 임의의 Mutant Name으로 바꿔버리는 것이다.
그에 해당하는 코드는 아래와 같다.

ULONG CPU_CYCLE_TIME_SINCE_RESET_OR_POWERON;
__asm
{
PUSH EAX
PUSH EDX
RDTSC  //Read Time Stamp Counter  (save to EDX:EAX)
MOV CPU_CYCLE_TIME_SINCE_RESET_OR_POWERON , EAX
POP EDX
POP EAX
}
RtlIntegerToUnicodeString(CPU_CYCLE_TIME_SINCE_RESET_OR_POWERON , 16 , ObjectAttributes->ObjectName);
status = ((NTCREATEMUTANT)OldNtCreateMutant)(
MutantHandle,
DesiredAccess,
ObjectAttributes,
InitialOwner);
DbgPrint(" status : %d",status);
return status;

컴퓨터 전원을 켠뒤,  또는 Asynchronous Reset 이 일어난뒤 , 
CPU의 Cycle Time은  0부터 쭉~ 증가하게 되는데, 이것을 이용한  Named Mutant를 조작하는 방법이다.
RDTSC 명령을 실행하면 
IA32_TIME_STAMP_COUNTER MSR  레지스터에 저장된 타임 스탬프 카운터 값을 읽어서,
EDX:EAX 에  현재 타임 스탬프 카운터 값을 저장한다.
그 후에 그 값을 뮤턴트 네임으로 대체하는 방식이다.

보통 RDTSC는 하드웨어 유틸에서 클럭의 주파수 계산이나 , 
유저모드 API 에서는 GetTickCount , 
커널모드 API 에서는  NtQueryPerformanceCounter 등의  정밀카운터 관련 연산이나
또는 항상 값이 변하는 성질을 이용해서 Randomize 알고리즘에서  근본 소스로 사용되는 경우가 많다.


하나의 예를 들어서 ,  QueryPerformanceCounter를 살펴보자.
우리가 보통 많이 사용하는 QueryPerformanceCounter를  유저모드에서 호출하게되면,
 Kernel에서 NtQueryPerformanceCounter를 호출한다. 
NtQueryPerformanceCounter에서 내부적으로 호출하고있는 함수들의 목록은 아래와 같다.


nt!NtQueryPerformanceCounter (80619682)
  nt!NtQueryPerformanceCounter+0x7 (80619689):
    call to nt!_SEH_prolog (8053db80)
  nt!NtQueryPerformanceCounter+0x3b (806196bd):
    call to nt!ExRaiseDatatypeMisalignment (80616066)
  nt!NtQueryPerformanceCounter+0x65 (806196e7):
    call to nt!ExRaiseDatatypeMisalignment (80616066)
  nt!NtQueryPerformanceCounter+0x7b (806196fd):
    call to hal!KeQueryPerformanceCounter (806edb94)
  nt!NtQueryPerformanceCounter+0xbc (8061973e):
    call to hal!KeQueryPerformanceCounter (806edb94)
  nt!NtQueryPerformanceCounter+0xde (80619760):
    call to nt!_SEH_epilog (8053dbbb)

Hardware Abstraction Layer 에서 제공하고있는 KeQueryPerformanceCounter를 이용하고있음을 볼수있다.
따라서 또 들어가보자.


hal!KeQueryPerformanceCounter:
806edb94 8bff            mov     edi,edi
806edb96 55              push    ebp
806edb97 8bec            mov     ebp,esp
806edb99 5d              pop     ebp
806edb9a ff2548356f80    jmp     dword ptr [hal!HalpHeapStart+0xc (806f3548)]
806edba0 cc              int     3
806edba1 cc              int     3
806edba2 cc              int     3

 hal!HalpHeapStart+0xc 를 dword ptr 만큼 참조하여 , 그 포인터로 점프를 한다.
hal!HalpHeapStart+0xc 에는 그럼 디스어셈블 코드가 있는것은 아닐테고 , 어떠한 포인터값으로 존재할것이다.
어떤것이 있는지 살펴보았더니,


806f3548  806e6c78 hal!HalpAcpiTimerQueryPerfCount
806f354c  806edbb8 hal!HalpAcpiTimerSetTimeIncrement

HalpAcpiTimerQueryPerfCount  라는  커널API가 있다.
이 부분으로 따라가보자.



hal!HalpAcpiTimerQueryPerfCount:
806e6c78 a0dcf06e80      mov     al,byte ptr [hal!HalpUse8254 (806ef0dc)]
806e6c7d 0ac0            or      al,al
806e6c7f 752d            jne     hal!HalpAcpiTimerQueryPerfCount+0x36 (806e6cae)
806e6c81 8b4c2404        mov     ecx,dword ptr [esp+4]
806e6c85 0bc9            or      ecx,ecx
806e6c87 7412            je      hal!HalpAcpiTimerQueryPerfCount+0x23 (806e6c9b)
806e6c89 64a1a4000000    mov     eax,dword ptr fs:[000000A4h]
806e6c8f 648b15a8000000  mov     edx,dword ptr fs:[0A8h]
806e6c96 8901            mov     dword ptr [ecx],eax
806e6c98 895104          mov     dword ptr [ecx+4],edx
806e6c9b 0f31            rdtsc
806e6c9d 640305ac000000  add     eax,dword ptr fs:[0ACh]
806e6ca4 641315b0000000  adc     edx,dword ptr fs:[0B0h]
806e6cab c20400          ret     4
806e6cae 8b4c2404        mov     ecx,dword ptr [esp+4]
806e6cb2 0bc9            or      ecx,ecx

위 코드에서 확인할수있다시피 , 결국은  그 근본은 RDTSC라는 것이다.
RDTSC를 실행하여 , EDX : EAX에 타임스탬프카운터를 얻어온뒤에 , 
eax는  dword ptr fs:[0ACh]와 ADD 연산을 하고 ,  edx는 dword ptr fs:[0B0h]와 ADD 연산을 한뒤 , 그값을 반환한다.  
(결과가 너무 허무한가?)

참고로 RDTSC Instruction은   
x86 CPU의  Control Register 4 의  2번 Bit인 
Time Stamp Disable (TSD)가  1로 세트되어져 있으면 ,  Ring0에서만 써먹을 수 있고,
TSD가 0으로 클리어 되어져 있으면 ,  모든 권한의 레벨에서 사용할 수 있다.
이 권한을 제어하기 위해선 CR4를 컨트롤이 필요한데 , 
CR4에 접근하기 위한 특권레벨은 Ring 0 에서만 가능하므로 , 당연히 드라이버가 필요하다.

아래는  IA32 Manual 3A의  2-23에서 발췌한 내용이다.

TSDTime Stamp Disable (bit 2 of CR4) — Restricts the execution of the RDTSC instruction (including RDTSCP instruction if CPUID.80000001H:EDX[27] = 1) to procedures running at privilege level 0 when setallows RDTSC instruction(including RDTSCP instruction if CPUID.80000001H:EDX[27] = 1) to be executed at any privilege level when clear.


뭐 결론적으로 
위 방법을 응용하면 , 현재 보안솔루션이 채택된 대부분 게임들의 멀티로더 만들기가 가능해질 것이다.
n사의 GG는 초기화작업에서 SSDT Restore를 수행한다고 들었는데 , 뭐 개인적으로 분석해보지는 않아서 가능할지 여부는 확실하지않다.
이때까지 유저모드에서  ReadProcessMemory , WriteProcessMemory ,  CreateFile-WriteFile 만을 이용해서 멀티 만드신분들은 쬐까 참고하셔도 될듯...
제 블로그 눈빠지게 모니터링 하시는 보안요원들께도 좋은 정보가 됐으면 좋겠네영
저작자 표시 비영리 변경 금지
신고
by Sone 2009.11.23 02:31
사실 거창하게 입문이라고 하기보다는 , MFC 공부할 생각도 없었는데,  어찌저찌 하다가 이렇게 만들고 싶은 생각이 들어서,
하루종일 책보고 삽질하면서 만든 프로그램...(사실 틀 잡는것은 빠른시간에 다 완료했지만 , 기능 구현 문제때문에 삽질을 오래한듯)

덕분에 뭐 의도하지않게 MFC에 대해서 조금이나마 맛을 볼수 있었다.




이름하여  테일즈위버 멀티로더 !
사실 이 프로그램 만들기전에는 ,  Windows API만 무식하게 때려박아서 , 각종 리턴값으로 흐름을 제어하는식으로  로더를 제작했었지만,
본인이 봐도 너무 불편한것 같아서.. 마음 먹고 책 뒤져가면서 만들어보았다.

아주 잘 작동한다!  (렌더링타입 빼고)
렌더링타입이 적용되지 않는 문제는 .....
사실 렌더링타입은  Config.DAT의 내용을 바꿔준다음  인자로 전달되는 숫자만 적절히 잘 주면 될줄 알았는데,
아직 분석하지않은 그 무언가가 또 있는듯 하다...

뭐 렌더링타입 설정은 솔직히 있으나마나... 필요없으므로..


**************** 수정 *****************

렌더링 타입 변경도 아주 잘 작동한다......
역시나 리버스 엔지니어링을 통한 분석 완료.



추가적으로 ? 를 체크하면 비밀 기능 작동이........
저작자 표시 비영리 변경 금지
신고
by Sone 2009.08.29 02:29
| 1 |