FPS 게임을 하다 보면 종종 불법 프로그램 사용자를 만날 수 있는데
그 중 제일 많이 보이는 핵을 손에 꼽아 말한다면 "월핵"이라 할 수 있다.
FPS 게임을 망치는 주범이며 월핵은 무엇이길래 불법 프로그램 사용자들이 주로 사용할까?
요번에는 FPS 게임이면 자주 발견 할 수 있는 "월핵" 에 대해서 이야기를 다뤄보자 한다.
'월핵 ?'
월핵은 영어 단어로 벽을 뜻하는 "Wall"에서 따왔다. 벽을 뚫고 상대방을 본다. 라는 개념인데
Wallhack(월핵)은 벽을 통과해 상대방 Character(캐릭터) 모델을 투시할 수 있는 핵이다.
FPS 게임에서 상대방 (적군)의 위치를 알고 있다면 플레이가 매우 쉬워질 것이다.
즉슨 게임의 승패를 좌우할 만큼 FPS 게임에서의 상대방의 위치는 매우 중요한 요소로 작용되므로
단지 게임을 승리하기 위해서 월핵을 사용하는 유저 수는 급증하기 시작한다.
현재 게임사에서는 이 "월핵" 때문에 많은 골칫거리를 앓고 있다.
넥*사의 서*어택 게임 같은 경우 "핵 근절하기 캠페인" 실시 이후 6일 만에 불법 프로그램 사용 건수가
1만 2000건에 달했을 정도로 사용자 수가 많았으며
https://news.mt.co.kr/mtview.php?no=2022051118352941718
"자동조준·무적"…'서든어택' 게임핵 팔아 2700만원 챙겼다 - 머니투데이
[theL] 대포통장·대포폰 이용하다 혐의 추가온라인 슈팅게임 "서든어택" 내에서 부정행위를 돕는 해킹 프로그램(게임핵)을 유통하다 구속된 남성이 징역형 집행유예를 선고받았다.서울중앙지법
news.mt.co.kr
실제 게임사에서 불법프로그램 유포자를 처벌하는 사례도 많이 나오고 있다.
https://news.mt.co.kr/mtview.php?no=2013072917250385361&outlink=1&ref=%3A%2F%2F
게임사는 지금 핵(hack)과의 전쟁 중 - 머니투데이
# IT보안업체와 소규모 게임업체에서 2년 동안 일한 고모씨(29)는 FPS(1인칭 총싸움)게임 월핵을 제작해 판매했다. 이에 그치지 않고 MMORPG(다중접속역할수행게임) 귀속아이템을 판매 가능하도록 변
news.mt.co.kr
'그럼 월핵 의 동작원리가 무엇일까?'
월핵 사용자가 급증하면서 월핵의 동작원리에 대해 궁금해 하는 사람이 많아지고
월핵은 왜 못 막을까?라는 의구심을 가지는 유저가 많아지면서 FPS 게임유저들에게 많이 주목받고 있다.
그럼 월핵의 동작원리는 무엇일까?
대표적으로는 게임 그래픽스를 담당하는 그래픽 모듈을 조작하여
벽 너머 있는 적을 투시 할 수 있게끔 적용하는 방법이 있겠다.

그래픽 모듈에서는 게임 화면에서 여러 물체를 그려주기 위해
Rendering(랜더링)을 담당하는 모듈이 존재한다.
여기서 ZBuffer(z-buffering)라는 화면에 그려지는 객체들의 깊이 정보를 저장하는 버퍼가 있는데
ZBuffer에 대한 자세한 설명은 해당 위키에서 설명해주고 있다.
https://ko.wikipedia.org/wiki/Z_%EB%B2%84%ED%8D%BC%EB%A7%81
Z 버퍼링 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. Z 버퍼 데이터 컴퓨터 그래픽스에서 Z 버퍼링(z-buffering)은 3차원 그래픽스의 이미지 심도 좌표 관리 방식이며, 일반적으로는 하드웨어적으로 처리되나 이따금은
ko.wikipedia.org

위의 그림과 같이 3개의 도형이 있다 가정하고 어떤 식으로 동작하는지 설명한다.
도형 A , B , C 순서대로 그려주고 있고 랜더링 되는 화면을 보면
실제 Zbuffer에 저장된 z 값에 따라 실제 출력에선 삼각형이 사각형을 가리는 형태로 출력이 된다.
이 처럼 Zbuffer에 저장된 z 값을 비교하여 현재 픽셀을 먼저 그려주고 가려질 픽셀은 그려주지 않는 판단을 하여
특정 물체를 랜더링 할 때 Zbuffer은 주된 용도로 사용된다.
만약 특정 물체를 랜더링 할 때 Zbuffer 기능을 비활성화시킨다면 어떻게 될까?
Zbuffer 기능을 비활성화하게 되면 해당 물체를 랜더링 해야 할지 여부를 파악할 수 없어서
모든 물체들을 항상 화면 앞에 그려줄 것이다.

그렇다면 모든 물체가 아닌 적 플레이어만 구분하여 Zbuffer 기능을 비활성화하면 어떨까?
적 플레이어만 벽 위에 그려지게 되면서 상대방 캐릭터만 벽을 뚫고 볼 수 있게 될 것이다.
이것이 ZBuffer을 이용한 기본적인 월핵의 원리이다.
ZBuffer 기능을 비 활성화 하기 위해선 랜더링 흐름을 제어해야 되는데 이를 위해
그래픽 라이브러리 함수를 후킹 하여 ZBuffer 기능을 비활성화시킨다.
게임마다 그래픽 라이브러리가 다르기 때문에 월핵을 구현하는 방법도 조금씩 달라진다.
해당 글에선 D3D9 버전의 라이브러리를 사용하는 프로그램의 월핵을 다뤄보자 한다.
'Hooking'
그래픽 모듈을 후킹 하기 위해선 후킹 할 주소를 먼저 알아내는 과정이 필요하다.
Direct3D 라이브러리는 CreateDevice 함수를 통해 인터페이스를 생성하고
IDirect3DDevice9 인터페이스를 사용하여 그래픽 함수들을 호출하여야 한다.
랜더링 함수를 후킹 하기 위해 DrawIndexedPrimitive의 함수 주소를 구하고
해당 함수에 DetourFunc을 설치하여 월핵을 제작한다.
DrawIndexedPrimitive 함수 주소를 구하기 위해선 vtable(virtual method table)의 주소를 찾아야 한다.
vtable은 가상 함수 테이블이며 함수들에 대한 포인터들의 배열들을 가리킨다.
가상 메소드 테이블 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 가상 메소드 테이블(영어: virtual method table, virtual function table, virtual call table, 디스패치 테이블, vtable, 또는 vftable)은 동적 디스패치(또는 런타임 메소드 바인딩)
ko.wikipedia.org
따라서 vtable 주소와 vtable index를 구하게 되면 Direct3D9 함수의 Address를 구할 수 있게 된다.

vtable에 저장돼있는 함수 주소를 변경해
원래 함수 대신 내가 작성한 function를 실행할 수 있게 하는 작업을 vtable hook이라고 한다.
우선 DLL Injection 기법이 내부 메모리에 접근하기 쉬우므로
앞서 1편에 언급하였던 DLL Injection 기법을 사용하여 vtable Hook을 설치할 것이다.

외부 프로세스에 DLL을 삽입하여 DrawIndexedPrimitive 함수 주소에 내 Hooking(후킹)코드를 설치하고
랜더링 흐름을 제어할 수 있다.
DrawIndexedPrimitive 함수는 인덱스 되어있는 Vertex를 기본으로 랜더링 하는 함수이다.
즉 물체의 랜더링을 담당하는 함수이며 DrawIndexedPrimitive 함수를 타깃으로
vtable hooking을 하여 Zbuffer를 비활성화시킬 수 있다.
vtable 주소를 찾기 위해 우선 CreateDevice 함수를 기점으로 찾는다.

CreateDevice는 Direct3D 객체를 생성해서 그 객체의 인터페이스인 IDirect3D9를 리턴해준다.
6번째 인수인 **ppReturnedDeviceInterface에서 IDirect3DDevice9 인터페이스를 가지고 있는
포인터가 담겨서 돌아오므로
CreateDevice 함수에서 ppReturnedDeviceInterface 값이 설정되는 영역을 찾아가면
vtable 주소를 찾을 수 있다.

vtable 주소를 구했으면 매번 게임을 실행할 때마다 자동으로 vtable 주소를 구하여
후킹 하는 작업을 하기 위해 Pattern Scanning 이라는 방법을 사용한다.
Pattern Scanning은 게임이 업데이트 되거나 새로 Release 될떄 offset 주소가 바뀌게 되는데
해당 Opcode(명령코드)의 Byte Pattern을 따온 Signature를 기반으로
Memory 영역에서 Scan하여 해당 주소를 찾을 수 있는 방법이다.
Pattern array를 구하는 방법은 간단하다.
vtable 의 Byte Code는 C7 06 38 26 90 67 89 86 28 32 00 00 89 86 이다.
하지만 매번 d3d9.dll 모듈이 프로세스에 로드될 때마다 offset이 매번 달라지므로
달라지는 부분을 ? 로 바꿔주면. C7 06 ? ? ? ? 89 86 ? ? ? ? 89 86 가 되고
해당 array Pattern으로 매번 vtable을 찾을 수 있다.
Pattern Scanning을 할 때 사용되는 함수는 다음과 같이 사용한다.
bool DataCompare(const BYTE* Data, const BYTE* HexMask, const char* MatchMask)
{
for (;*MatchMask;++MatchMask, ++Data, ++HexMask)
{
if (*MatchMask == 'x' && *Data != *HexMask)
{
return false;
}
}
return (*MatchMask) == NULL;
}
DWORD FindPattern(DWORD Address, DWORD Len, BYTE* HexMask, char* MatchMask)
{
for (DWORD i = 0; i < Len; i++)
{
if (DataCompare((BYTE*)(Address + i), HexMask, MatchMask))
{
return (DWORD)(Address + i);
}
}
return NULL;
}
D3D9 라이브러리를 후킹 하기 위해 GetModuleHandle() 함수를 호출하여 모듈 핸들을 구해온다.
GetModuleHandle 함수는 지정된 모듈의 핸들을 반환해 주는데 해당 함수로 모듈의 Base 값을 구할 수 있다.
D3d9Base = (DWORD)GetModuleHandle(L"d3d9.dll");
while (!D3d9Base)
{
D3d9Base = (DWORD)GetModuleHandle(L"d3d9.dll");
Sleep(100);
}
DWORD TempAdd = FindPattern(D3d9Base, 0x128000, (BYTE*) "\xC7\x06\x00\x00\x00\x00\x89\x86\x00\x00\x00\x00\x89\x86", "xx????xx????xx");
vtable 주소를 구했으면 vtable index를 이용하여 함수 주소를 찾아야 되기 때문에
공개된 vtable index를 참고하여 DrawIndexedPrimitive 함수의 vtable index를 구한다.
공개된 vtable index는 다음과 같다.
....
SetNPatchMode // 79
GetNPatchMode // 80
DrawPrimitive // 81
DrawIndexedPrimitive // 82
DrawPrimitiveUP // 83
DrawIndexedPrimitiveUP // 84
ProcessVertices // 85
CreateVertexDeclaration // 86
SetVertexDeclaration // 87
GetVertexDeclaration // 88
SetFVF // 89
GetFVF // 90
CreateVertexShader // 91
SetVertexShader // 92
GetVertexShader // 93
....
DrawIndexedPrimitive의 vtable index는 "82" 인 것을 확인했다.
이제 DIP(DrawIndexedPrimitive) 함수의 주소를 구했으니 내 Hooking Code를 설치해야 한다.
Inline Hooking을 하기 위해 DetourFunc 함수를 사용한다.
DetourFunc에 대한 자세한 내용은 DetourFunc 논문에서 확인할 수 있다.
void* DetourFunc(PBYTE src, const PBYTE dst, const int len)
{
DWORD dwback;
BYTE* jmp = (BYTE*)malloc(len + 5);
VirtualProtect(jmp, len + 5, PAGE_EXECUTE_READWRITE, &dwback);
VirtualProtect(src, len, PAGE_READWRITE, &dwback);
memcpy(jmp, src, len);
jmp += len;
jmp[0] = 0xE9;
*(DWORD*)(jmp + 1) = (DWORD)(src + len - jmp) - 5;
src[0] = 0xE9;
*(DWORD*)(src + 1) = (DWORD)(dst - src) - 5;
for (int i = 5; i < len; i++)
{
src[i] = 0x90;
}
VirtualProtect(src, len, dwback, &dwback);
return (jmp - len);
}
typedef HRESULT(WINAPI* oDrawIndexedPrimitive)
(
LPDIRECT3DDEVICE9 vDevice, D3DPRIMITIVETYPE Type,
INT BaseVertexIndex, UINT MinIndex, UINT NumVertices,
UINT StartIndex, UINT PrimitiveCount
); oDrawIndexedPrimitive pDrawIndexedPrimitive = NULL;
//정의한 DrawIndexedPrimitive 함수원형의 변수로 pDrawIndexedPrimitive 초기화
D3d9VTable = (DWORD*)*(DWORD*)(TempAdd + 2);
OrigDrawIndexedPrimitive = (DrawIndexedPrimitive_t)DetourFunc((BYTE*)D3d9VTable[82], (BYTE*)DrawIndexedPrimitiveHook, 7);
//원래 DrawIndexedPrimitive 함수 주소 저장
//DrawIndexedPrimitive 함수에 DrawIndexedPrimitiveHook 후킹 설치
Sleep(1);

DrawIndexedPrimitive 영역에서 내 후킹 코드가 설치 된 것을 확인 할 수 있다.
'SetRenderState'
ZBuffer을 비활성화하기 위해선 SetRenderState 함수를 이용한다.
함수의 원형은 다음과 같다.

SetRenderState 함수의 첫 번째 인자로 D3DRS_ZENABLE를 주면 ZBuffer를 변조할 수 있다.
z버퍼를 유효하게 하라면, D3DZB_TRUE , 무효하게 하려면 D3DZB_FALSE를 사용한다.
HRESULT __stdcall DrawIndexedPrimitiveHook(IDirect3DDevice9* vDevice, D3DPRIMITIVETYPE Type, INT BaseVertexIndex, UINT MinIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount)
{
vDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
OrigDrawIndexedPrimitive(vDevice, Type, BaseVertexIndex, MinIndex,
NumVertices, StartIndex, PrimitiveCount);
vDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
return OrigDrawIndexedPrimitive(vDevice, Type, BaseVertexIndex, MinIndex, NumVertices, StartIndex, PrimitiveCount);
}
다음과 같이 Hooking 코드를 작성해 보았다.


D3DRS_ZENABLE 를 D3DZB_FALSE 로 줬을 때 물체들이 통과하여 보이는 것을 확인할 수 있다.
해당 코드에선 어떤 물체를 구분하여 보여줘야 할지에 대한 구별하는 코드가 없어서
현재 바라보는 물체가 모두 투명화되는 상태(?)가 발생하게 된다.
이렇게 되면 내가 원하는 물체만 투시해서 보는 월핵이랑은
거리가 좀 있는 불편한(?) 월핵이라 볼 수 있다.
그래서 특정 물체만 구별 후 통과해서 보이게끔 하기 위해 정점(Stride) 값을 가져와서 구분한다.
Stride는 버퍼 구조체 크기를 가지고 있는 값으로 현재 랜더링 하고 있는 물체의 Stride를 가져와서 구별 후
ZBuffer를 D3DZB_FALSE 로 준다면 우리가 알고 있는 월핵으로 구현이 가능해 보인다.
'GetStreamSource'
Stride 값을 가져오기 위해선 어떻게 해야 될까?

GetStreamSource 함수의 4번째 인자인 pStride에서 Stride 값을 가져올 수 있었다.
이때 더 세부적인 구분이 필요할 경우 NumVertices , primCount 까지 가져와서 구분하는 방법도 있다.
특정 물체의 Stride 값을 찾기 위해서 흔히 "핵 개발자"들은 Stride Logger를 이용하여
구분하고자 하는 물체의 정점 버퍼 크기 값을 찾는다.

Stride Logger 원리는 간단하다.
랜더링 할 물체에 Texture를 입혀서 Stride , NumVertices , primCount 구분 값을 늘려가며
특정 물체에 반응이 오는 Stride 값을 기록한다.
GetStreamSource 호출 후 Stride로 구분하는 코드는 다음과 같이 작성할 수 있다.
RESULT __stdcall DrawIndexedPrimitiveHook(IDirect3DDevice9* vDevice, D3DPRIMITIVETYPE Type, INT BaseVertexIndex, UINT MinIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount)
{
LPDIRECT3DVERTEXBUFFER9 Stream_Data;
UINT Offset, Stride;
if (vDevice->GetStreamSource(0x00000000, &Stream_Data, &Offset, &Stride) == D3D_OK)
Stream_Data->Release();
if (Stride == 0x2C || Stride == 0x30)
{
vDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
OrigDrawIndexedPrimitive(vDevice, Type, BaseVertexIndex, MinIndex,
NumVertices, StartIndex, PrimitiveCount);
vDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
}
return OrigDrawIndexedPrimitive(vDevice, Type, BaseVertexIndex, MinIndex, NumVertices, StartIndex, PrimitiveCount);
}
해당 Code는 Stride == 0x2C 이거나 Stride == 0x30 일시 ZBuffer을 비활성화하는 코드이다.
'Chams Wallhack'
캐릭터 모델에 흔히 색상을 입혀 사용하는 월핵을 Chams(chameleon Wallhack)라고 부른다.

Chams Wallhack은 텍스쳐를 생성하고 물체에 색상을 입혀 사용하는데
흔히들 벽 구분 월핵이라고 부른다.
(해당 사진을 참고하면 벽 뒤에 있는 캐릭터 모델이 노란색 텍스처가 씌워진 모습을 확인할 수 있다)
이는 Original DrawIndexedPrimitive를 호출하기 전에 SetTexture 함수로 물체의 텍스처를 노란색으로 설정하고
Original DrawIndexedPrimitive를 호출 후 SetTexture 함수로 물체의 텍스처를 빨간색으로 설정한다.
즉 벽 뒤에 있는 물체의 텍스처는 노란색으로 랜더링 되고
벽 앞에 있는 물체의 텍스처는 빨간색으로 랜더링 되기 때문에 벽구분 월핵이 가능한 것이다.
물체에 텍스처를 입히는 Code는 다음과 같이 작성할 수 있다.
HRESULT GenerateTexture(IDirect3DDevice9* vDevice,
IDirect3DTexture9** ppD3Dtex, DWORD colour32)
{
if (FAILED(vDevice->CreateTexture(8, 8, 1, 0, D3DFMT_A4R4G4B4,
D3DPOOL_MANAGED, ppD3Dtex, NULL)))
return E_FAIL;
WORD colour16 = ((WORD)((colour32 >> 28) & 0xF) << 12)
| (WORD)(((colour32 >> 20) & 0xF) << 8)
| (WORD)(((colour32 >> 12) & 0xF) << 4)
| (WORD)(((colour32 >> 4) & 0xF) << 0);
D3DLOCKED_RECT d3dlr;
(*ppD3Dtex)->LockRect(0, &d3dlr, 0, 0);
WORD* pDst16 = (WORD*)d3dlr.pBits;
for (int xy = 0; xy < 8 * 8; xy++)
*pDst16++ = colour16;
(*ppD3Dtex)->UnlockRect(0);
return S_OK;
}
HRESULT __stdcall DrawIndexedPrimitiveHook(IDirect3DDevice9* vDevice, D3DPRIMITIVETYPE Type, INT BaseVertexIndex, UINT MinIndex, UINT NumVertices, UINT StartIndex, UINT PrimitiveCount)
{
LPDIRECT3DVERTEXBUFFER9 Stream_Data;
UINT Offset, Stride;
if (Color == 0)
{
GenerateTexture(vDevice, &COROR1, D3DCOLOR_ARGB(255, 255, 0, 0)); //RED
GenerateTexture(vDevice, &COROR2, D3DCOLOR_ARGB(255, 255, 228, 0)); //Yellow
Color = 1;
}
if (vDevice->GetStreamSource(0x00000000, &Stream_Data, &Offset, &Stride) == D3D_OK)
Stream_Data->Release();
if (Stride == 0x2C)
{
vDevice->SetTexture(0, COROR2);
vDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_FALSE);
OrigDrawIndexedPrimitive(vDevice, Type, BaseVertexIndex, MinIndex,
NumVertices, StartIndex, PrimitiveCount);
vDevice->SetRenderState(D3DRS_ZENABLE, D3DZB_TRUE);
vDevice->SetTexture(0, COROR1);
}
return OrigDrawIndexedPrimitive(vDevice, Type, BaseVertexIndex, MinIndex, NumVertices, StartIndex, PrimitiveCount);
}


Sample 파일에 Chams를 테스트한 모습
'WireFrame Wallhack'
또 다른 종류의 월핵인 일명 '와이어 프레임' 이라고도 부른다. 선(Wire)을 뜻하며
공간상에 점들을 지정 후 연결 하여 그리는 작업을 말한다.
SetRenderState 함수 첫번쨰 인자에 D3DRS_FILLMODE 를 주면 물체를 점 형태로 그리거나 SOLID 형태 , WireFrame 형태로 그릴 수 있다.

다음과 같이 구현 할 수 있다.
vDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

'마치며'
이처럼 직접 월핵을 구현해보면서 실제 월핵은 그래픽 모듈을 후킹 하여 동작한다는 것을 알 수 있고
Pattern Scan으로 vtable을 구한 뒤 Direct3D 함수를 찾아내는 방법도 알 수 있었다.
여론으로 월핵을 막을 방법은 수차례 존재 하겠지만
스크린샷 캡처 후 게임 서버로 전송하고 서버에서 비정상 알고리즘으로 검증 후 세션을 끊는 작업,
특정 물체의 ZBuffer 여부를 Check 하는 무결성 검사나 vtable을 찾기 힘들게 숨기는 작업,
d3d9.dll 모듈을 복사하여 Copy 모듈에서 Direct3D 함수들을 호출하는 방법
등등 여러 가지로 방지 코드를 구현할 순 있겠으나
자체적으로 월핵을 막을 수 있는 획기적인 방법은 아직 힘들지 않을까하고 필자는 생각해본다.
'Reversing > GameHacking' 카테고리의 다른 글
| GameHacking 5. 스피드 핵의 동작원리에 대해 (2) | 2023.08.19 |
|---|---|
| GameHacking 4. 에임봇 핵의 동작원리에 대해 (0) | 2023.08.17 |
| GameHacking 3. ESP 핵의 동작원리에 대해 (5) | 2023.07.19 |
| GameHacking 1. 게임 핵의 동작 원리 와 사례 (10) | 2023.07.17 |