2편에서는 그래픽 모듈을 후킹 하여 사용하는 월핵에 대해서 알아보았는데
월핵은 무조건 그래픽 모듈을 후킹하는 방법의 월핵만 존재하는가?
꼭 그렇다고 할 순 없다.
그래픽 모듈을 건드리는 방법을 제외하고 상대방의 위치를 알 수 있는 월핵의 종류도 몇 가지 존재한다.
전 편에서 다룬
DirectX 함수를 후킹 하여 ZBuffer를 비활성화하는 그래픽 모듈을 변조하는 월핵이 있고
플레이어 정보가 담긴 게임 구조체의
메모리를 읽어와 플레이어 정보를 화면에 그려주는 ESP (ExtraSensory Perception)라는 월핵이 있다.
또한 물리엔진을 이용한 월핵도 존재한다.
게임 클라이언트 폴더의 Resource 파일에 접근하여
물체를 담당하는 models 파일들을 변조하여 사용하는 월핵도 있다.
이처럼 상대방의 위치를 알아내는 월핵은 여러 방법으로 접근하여 사용 된다는 것을 알 수 있다.
하지만 여러 가지 종류의 월핵 가운데 ESP(ExtraSensory Perception) 핵에 대해서 애기를 다뤄보자 한다.
'ESP 월핵?'
ESP의 뜻은 ExtraSensory Perception의 줄임말이며 해석하면
텔레파시, 투시, 그리고 예지, 오감 이외의 수단을 이용해 외부의 정보를 얻는 능력이다.
라는 뜻이다.
ZBuffer를 변조를 하는 월핵처럼 상대방의 Character(캐릭터) 모델을 그려주는 것은 유사해 보이지만
ESP 월핵은 플레이어 정보를 담고 있는 구조체 메모리를 읽어와 체력, 방어, 닉네임 등등 다양한 정보를
화면에 표시해 준다는 차이점 이 있다.
ESP 월핵은 게임 화면 위에 창을 생성하여 그리는 Overlay 방식이 있고
그래픽 모듈을 후킹 하여 그래픽 라이브러리를 통해 게임 화면에 직접 그리는 방법이 있다.
Overlay
오버레이 (프로그래밍) - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 컴퓨팅에서 오버레이(overlay)란 데이터나 인스트럭션 블록을 다른 블록으로 교체[1]하는 것을 의미한다. 오버레이는 컴퓨터의 메인 메모리보다 큰 프로그램을
ko.wikipedia.org
각기 마다 장점과 단점이 존재하는데 우선 Overlay 방식 같은 경우 게임 화면 위에 그리는 거기 때문에
월핵을 방지하기 위한 게임 내 모니터링 기능이 있다면 이를 우회할 수 있는 장점이 있다.
하지만 Overlay를 이용한 방법은 그래픽 모듈을 후킹 하여 그리는 방법보다
자원을 많이 소모한다는 단점이 있고
주로 Overlay 방식을 사용 할 때 Microsoft에 제공하는 Windows GDI (그래픽 디바이스 인터페이스)을 사용하는데
https://learn.microsoft.com/ko-kr/windows/win32/gdi/windows-gdi
Windows GDI - Win32 apps
Microsoft Windows GDI(그래픽 디바이스 인터페이스)를 사용하면 애플리케이션에서 비디오 디스플레이와 프린터 모두에서 그래픽 및 서식이 지정된 텍스트를 사용할 수 있습니다.
learn.microsoft.com
GDI 방식으로 ESP 월핵을 그리게 되면
Flickering이라는 (깜빡거림 증세)가 나타나는 단점이 존재하게 된다.
(Frame Buffer에 완전히 정보가 저장되지 않은 상태에서 불완전한 내용의 버퍼를 화면에 그리면서 발생한다.)
이 Flickering 현상을 해결할 수 있는 방법은 존재하는데
Double Buffering(이중 버퍼링) 방식을 사용하면 해결이 가능한 것으로 보인다.
해당 글에서는 Overlay 방식이 아닌 그래픽 모듈을 후킹 하여 그리는 방식에 대해 다뤄 볼 것이기 때문에
우선 상대방 정보를 화면에 그리기 위해선 데이터를 가져오는 작업이 먼저 필요하다.
'어떤 데이터?'
우선 물체의 XYZ좌표 (Coord)가 필요하겠고
물체의 화면 좌표를 구하기 위해선 Viewport와 Matrix(행렬)이 필요하다.
또한 여러 가지 정보를 그리기 위해선 닉네임, 생사여부, 체력, 방어 등의 데이터도 가져와야 한다.
이러한 데이터들은 게임 내부에서 주로 구조체(Struct) , 클래스(Class) 형태로 저장되어 있는데
리버스 엔지니어링을 통하여 Memory 내에서 플레이어 정보를 담고 있는 구조체인
Entity list 부분을 찾으면 된다.
다음은 게임 내부에서 Entity list 가정하여 작성한 예시 클래스 코드이다.
게임 메모리 내에서 Entity list를 찾아 포인터 형식으로 플레이어 정보가 담긴 메모리를 읽어와야 한다.
enum xState
{
xS_LIVE,
xS_DEAD,
};
class xPlayerInfo
{
public:
DWORD InGame;
char NickName[0x00000030];
BYTE TeamID;
xState PlayerState;
DWORD Kill;
DWORD Death;
float x,y,z;
};
앞서 1편에 언급한 Internal 방식으로
게임 프로세스 메모리에 접근하여 플레이어 정보를 가져오는 방식을 사용했다.
Internal 방식에 대한 설명은 1편 을 참고하면 될 것 같다.
우선 DirectX Sample 파일을 타깃으로 Entity list를 가져오기 위한 작업을 한다.
Entity list를 가져오려면 어떻게 접근해야 될까?
우선 해당 프로그램은 게임이 아닌 DirectX 에서 제공하는 Sample 파일이므로
체력, 방어, 닉네임 부분을 담당하는 요소가 없기 때문에
캐릭터 좌표를 메모리 내에서 탐색해 보았다.
프로그램에서 캐릭터의 좌표를 그대로 출력해 줘서
CheatEngine으로 따로 변화를 주며 탐색하는 과정 필요 없이 좌표 메모리에 접근할 수 있었다.
CheateEngine의 Find Out What accesses this address (이 메모리 주소에 접근하는 영역)
기능을 이용하여 해당 좌표 메모리가 어디서 참조되는지 추적할 수 있다.
해당 주소지로 이동 하니 'Pos' 라고 친절히 설명 돼있는 정적 문자열이 있는 것을 보아
해당 부분이 캐릭터 좌표를 참조하는 부분이라 볼 수 있다.
주로 XYZ는 구조체에서 이런 식으로 정렬되기 때문에
float x;
float y;
float z;
float 형은 크기가 4byte 이므로 + 4 , + 8 연산을 통하여
플레이어 XYZ 좌표 메모리에 접근할 수 있었다.
[eax+000000A8] X 좌표
[eax+000000AC] Y 좌표
[eax+000000B0] Z 좌표
즉 정리하면 이렇게 되겠다.
eax 레지스터에는 유동 메모리 주소가 담기므로
게임을 껐다 킬 경우에나 새로운 게임으로 이동하게 되면
메모리 주소가 바뀌게 되므로 포인터 주소를 찾아야 한다.
포인터 주소를 찾기 위해 Return Address (해당 함수를 호출한 영역)
의 코드로 이동하여 분석을 해보았다.
[eax+000000A8] 의 eax 값은 mov ecx,[eax+ecx*4] 명령어가 지나고
ecx 레지스터에 담기는 것을 확인했고
[MultiAnimation.exe+6D8E0] < 값 은 PlayerBase 가 되겠다.
mov ecx,[MultiAnimation.exe+6C000] < 값은
프로그램에서 플레이어 의 움직임을 직접 Control 할 수 있는 기능이 존재하는데
조작하려는 플레이어 대상을 바꿀 때마다 [MultiAnimation.exe+6C000] 의 값이 바뀌게 된다.
즉 PlayerIndex 부분이라 볼 수 있다.
4Byte 크기이므로 * 4를 해준 모습
코드로 작성하면 다음과 같다.
for (int i = 0; i < 1; i++)
{
*(DWORD*)(Player + i * 4)
}
프로그램에선 Add Instance 기능이 존재한다.
캐릭터 인스턴스를 생성할 수 있는 기능인데
여러 명의 캐릭터를 그려주기 위해선
한 명의 좌표가 아닌 여러 오브젝트의 좌표를 가져와야 하므로
현재 필드에 있는 플레이어 수만큼 루프를 돌며 좌표를 가져오기 위해
캐릭터 Number를 담당하는 메모리 주소를 찾아야 한다.
이 또한 친절히 'Number of models' 텍스트로 필드에 몇 명이 존재하는지 나타내주고 있길래
CheatEngine으로 Player Count를 스캔해보았으나 나오지가 않는다(?)
그래서 다른 방식으로 'Number of models'라는 문자열에 접근하는 코드를 디버깅해보았다.
플레이어 Number를 출력해주는 함수로 이동해 보니
비트연산을 하는 명령어가 존재한다. 명령어를 해석하면 다음과 같다.
mov eax,[MultiAnimation.exe+6D8E4] // MultiAnimation.exe+6D8E4 의 값을 eax에 넣는다.
sub eax,[MultiAnimation.exe+6D8E0] // eax에 [MultiAnimation.exe+6D8E0] 값을 뺸다.
sar eax,02 //비트를 오른쪽으로 2번 이동시키는 명령어
연산이 끝나면 EAX 레지스터에 Player Number가 담기고
이후에 Number of models %d를 출력시키는 함수의 Parameter로 담기고 호출된다. //push eax
해당 연산을 C언어 코드로 작성하면 다음과 같다.
PlayerCount = (*(DWORD*)PlayerCountBase - *(DWORD*)PlayerCountSub) >> 2;
이후 정리하여 Internal 방식으로 Player들의 좌표를 출력해 주는 코드를 작성해 보았다.
namespace offsets {
DWORD PlayerBase = 0x00EAD8E0;
DWORD PlayerCountBase = 0x00EAD8E4;
DWORD PlayerCountSub = 0x00EAD8E0;
}
struct PlayerHandle {
char Unknown001[168];
float XOffset = {}; //0xA8;
float YOffset = {}; // 0xAC;
float ZOffset = {}; // 0xB0;
};
PlayerCount = (*(DWORD*)PlayerCountBase - *(DWORD*)PlayerCountSub) >> 2;
for (int i = 0; i < PlayerCount; i++) {
Player = *(DWORD*)PlayerBase;
pHandle = reinterpret_cast<PlayerHandle*>(*(DWORD*)(Player + i * 4));
printf("Player Count : %d\n", PlayerCount);
printf("Player%d Coord: %f %f %f\n\n", i+1, pHandle->XOffset, pHandle->YOffset, pHandle->ZOffset);
}
이제 플레이어들의 좌표를 구했으니 해당 좌표에 그려주기만 하면 되는데
플레이어들의 오브젝트 좌표(월드 좌표)는 얻었지만
이를 화면상에서 그리기 위한 화면상 좌표(2D 좌표)가 필요하기 떄문에
월드좌표를 화면 좌표로 변환하는 작업이 필요하다.
우선 게임에서 물체를 바라보는 '카메라'라는 개념이 존재한다.
물체들은 카메라를 통해 화면에 보이게 되는데 이때 물체들은 3차원 월드 좌표가 아닌
카메라 좌표를 기준으로 화면에서 보이기 때문에 이를 카메라 좌표로 변환해주는 작업이 필요하다.
모든 3D 기반 게임에선 Matrix라는 개념이 존재하고 역할에 따라 다양한 Matrix 가 존재한다.
우선 DirectX 라이브러리에서는 내 카메라 시점에 대한 4x4 Matrix(행렬) 값을 관리한다.
게임은 이러한 Matrix를 어딘가 저장해놓는다.
일반적으로 World to Screen 변환이 자주 발생하기 때문에
이 Matrix를 리버스 엔지니어링을 통하여 직접 찾을 수 있다.
우선 ViewMatrix를 직접 찾기 위해선 카메라를 위 아래 움직여가며
pitch 값을 스캔하여 찾을 수 있는데
카메라가 90도 위를 바라볼 때 Z값은 1을 가지고 아래에서 90도를 바라볼 때는 -1을 가지게되는
특성을 활용하여 CheatEngine으로 ViewMatrix 행렬을 찾을 수 있다.
또 다른 방법으로는 ViewMatrix를 생성하는 데 사용되는 함수인
D3DXMatrixLookAtLH() 함수를 후킹 하는 방법이 있다.
pOut: 결과 뷰 매트릭스를 저장할 D3DXMATRIX 구조체의 포인터
함수가 호출되고 난 후에 이 포인터가 가리키는 구조체에 뷰 매트릭스가 저장된다.
pEye: 카메라의 위치를 나타내는 D3DXVECTOR3 포인터.
pAt: 카메라가 바라보는 타겟 위치를 나타내는 D3DXVECTOR3 포인터.
pUp: 카메라의 상단 방향을 나타내는 D3DXVECTOR3 포인터
이 ViewMatrix 안에는 Model, View, Projection Matrix와 곱해져 메모리에 저장되어 있다.
MVPMatrix 행렬을 가져온 뒤 물체의 월드좌표와 행렬 곱셈을 수행하여
카메라 상 XYZ 좌표를 구할 수 있게 된다.
또 다른 방법으로는 Internal 방식으로 D3DXVec3Project() 함수를 호출하여 화면 좌표를 가져올 수 있다.
2번째 파라미터인 pv에 월드좌표가 들어가1번째 파라미터의 pOut에서 pv의 화면좌표가 반환된다.
이때 좌표 변환 시 화면에 물체를 투영하는 과정에서 크기를 조절하는데 필요한
ViewPort 값이 필요한데
ViewPort는 전체 이미지가 표시될 부분을 보여주기 위해 사용되는 화면의 영역이다.
뷰포트 (컴퓨터 과학) - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. -->
ko.wikipedia.org
Viewport , pProjection, pView, pWorld 인자는
GetViewport()
GetTransform()
함수를 통하여 구 할 수 있었다.
D3DVIEWPORT9 구조체는 다음과 같다.
X와 Y : 왼쪽 상단 모서리의 x, y 좌표를 나타낸다.
Width와 Height : 크기를 나타내는 가로와 세로의 크기를 나타낸다.
MinZ와 MaxZ : 사용되는 깊이 값의 최소와 최댓값을 나타낸다.
GetViewport() 함수가 성공적으로 호출되면 pViewport 변수에 D3DVIEWPORT9 구조체 정보가 채워진다.
해당 함수를 호출하려면 Direct3D 디바이스 객체가 필요하다.
함수가 성공적으로 호출되면 pMatrix에 변환에 필요한 행렬
View Matrix, Projection Matrix, World Matrix가 저장된다.
D3DXMATRIX 구조체를 단위행렬로 초기화하는 데 사용되는 함수인
D3DXMatrixIdentity()를 호출하여
World Matrix를 생성할 수 있다.
Viewport , pProjection, pView, pWorld 인자를 다 구했으면 D3DXVec3Project()
함수를 호출하는 코드를 다음과 같이 작성할 수 있다.
vDevice->GetViewport(&pViewport);
vDevice->GetTransform(D3DTS_VIEW, &view_matrix);
vDevice->GetTransform(D3DTS_PROJECTION, &proj_matrix);
//vDevice->GetTransform(D3DTS_WORLD, &world_matrix);
int WorldToScreen(D3DXVECTOR3 pV, D3DXVECTOR3& pOut)
{
int vResult{};
D3DXMATRIX pWorld{};
D3DXMatrixIdentity(&pWorld);
D3DXVec3Project(&pOut, &pV, &pViewport, &proj_matrix, &view_matrix, &pWorld);
if (pOut.z < 1.0f)
vResult++;
return vResult;
}
'어떻게 그려야 하나?'
월드좌표를 화면 좌표를 변환했으니 이제 화면에 그리는 작업이 필요하다.
GDI , OpenGL, DirectX 여러 그래픽 라이브러리로 물체에 선을 그리거나 텍스트를 그릴 수 있다.
이 글에선 DirectX 라이브러리를 이용하여 그리는 작업을 하려 한다.
Direct3D에서 랜더링을 마무리 할 떄 사용하는 EndScene 함수를 후킹하고
DrawText 함수를 호출하여 텍스트를 띄우는 코드를 다음과 같이 작성한다.
if (!pFont_ESP) {
D3DXCreateFontA(vDevice, 0x00000050, 0x00000000, FW_THIN, 0x00000001, 0x00000000, HANGUL_CHARSET, OUT_OUTLINE_PRECIS, PROOF_QUALITY, DEFAULT_PITCH | FF_SWISS, "굴림", &pFont_ESP);
}
RECT pRect;
SetRect(&pRect, 25, 25, 100, 100);
pFont_ESP->DrawTextA(0x00000000, "hello", -0x00000001, &pRect, DT_NOCLIP, D3DCOLOR_ARGB(0xFF, 0xFF, 0xFF, 0xFF));
화면 왼쪽 상단에 "hello" 를 그리는 데 성공한 모습을 볼 수 있다.
후에 모든 플레이어의 좌표를 가져온 뒤 월드좌표를 화면 좌표로 변환 하고
sprintf 함수를 사용하여 변수에 PlayerIndex를 가지는 str 문자열이 저장되도록 해서
캐릭터 위에 Text를 그려주는 전체적인 코드를 완성했다.
PlayerHandle* pHandle{};
PlayerCount = (*(DWORD*)PlayerCountBase - *(DWORD*)PlayerCountSub) >> 2;
for (int i = 0; i < PlayerCount; i++) {
Player = *(DWORD*)PlayerBase;
pHandle = reinterpret_cast<PlayerHandle*>(*(DWORD*)(Player + i * 4));
D3DXVECTOR3 pPos_Convert(pHandle->XOffset, pHandle->YOffset, pHandle->ZOffset);
if (WorldToScreen(pPos_Convert, entPos2D)) {
sprintf_s(Sprintf_Name, "%s %d", "Player",i+1);
DrawString(pPos_Convert.x, pPos_Convert.y, Sprintf_Name, DT_CENTER, D3DCOLOR_ARGB(0xFF, 0xFF, 0x00, 0x00));
}
}
Name ESP도 만든 김에 여러 가지 형태로 ESP를 구현하고 싶어져
ESP 월핵 종류 중 일부인 SnapLine ESP와 Box ESP를 구현해 보았다.
Box ESP는 선을 연결시켜서 캐릭터에 사각형을 그려주는 작업을
하는 ESP를 Box Esp라고 부른다.
SnapLine ESP는 바라보는 화면으로부터 캐릭터 좌표까지
선으로 연결해 주는 ESP Hack을 말한다.
Box ESP와 SnapLine Esp까지 코드는 따로 첨부하지 않았다.
(궁금하면 직접 한번 해보시길)
'마치며'
이번 글에서는 ESP 핵의 동작원리에 대해 알아보고 직접 구현해보는 시간을 가져봤는데
ESP 월핵은 일반적인 ZBuffer를 변조하는 월핵과는 다르게 월드좌표를 화면 좌표로
변환 후 직접 그려줘야 한다는 것을 알 수 있다.
'Reversing > GameHacking' 카테고리의 다른 글
GameHacking 5. 스피드 핵의 동작원리에 대해 (2) | 2023.08.19 |
---|---|
GameHacking 4. 에임봇 핵의 동작원리에 대해 (0) | 2023.08.17 |
GameHacking 2. WallHack의 동작원리 와 사례 (5) | 2023.07.17 |
GameHacking 1. 게임 핵의 동작 원리 와 사례 (10) | 2023.07.17 |