Wall Hack(월핵) : 벽 너머의 적을 보여주어 위치를 알 수 있도록 한다.
구현 방법은 그래픽 렌더링 라이브러리마다 조금씩 다르다.
해당 보고서에서는 Micosoft Direct X로 개발된 게임의 월핵을 구현하는 방법을 소개하고있다.
Z Buffer (Depth Buffer)
D3D9에서 월핵을 구현하기 위해선 렌더링에 사용되는 Z Buffer의 개념을 이해해야 한다.
이는 렌더링할 때 어떤 물체가 보여야 할지에 대한 여부를 판별하기 위해 사용되는 방법 중 하나다.
어떤 물체가 그려질 때 만들어진 픽셀의 깊이 정보(z좌표)를 저장하는 z버퍼(깊이 버퍼)
도형을 S2, S1, S3의 순으로 그리고 있다.
1. 아무것도 그리지 않은 초기 Z Buffer 상태
2. S2 도형을 그리고 난 후
3. S1 도형을 그리고 난 후. S2 도형과 겹치는 부분은 깊이 값을 비교했을 때 Z Buffer에 기록되어 있는 값이 더 크기 때문에(5<10) 해당 영역은 업데이트 되지 않았다.
4. S3 도형을 그리고 난 후 버퍼 상태. S1, S2 도형과 겹치는 부분의 깊이 값을 비교했을 때 버퍼에 기록되어있던 값이 더 작기 때문에 S3 도형의 깊이 값으로 변경되었다.
앞으로 나올 수록 깊이 값이 크고, 뒤로 갈 수록 깊이 값이 작다.
예를 들어, 적 플레이어를 렌더링할 때 Z Buffer 기능을 비활성화하면 렌더링 엔진은 해당 물체가 보여야 할 지의 여부를 구별할 수 없어 물체를 항상 화면에 보여줄 것이다. -> Z Buffer를 사용한 월핵의 기본 원리.
D3D9 Hook
D3D9의 함수를 후킹하기 위해서
1. 후킹할 함수의 주소를 알아내기
// this function initializes and prepares Direct3D for use
void initD3D(HWND hWnd){
d3d = Direct3DCreate9(D3D_SDK_VERSION); // create the Direct3D interface
D3DPRESENT_PARAMETERS d3dpp; // create a struct to hold various device information
ZeroMemory(&d3dpp, sizeof(d3dpp)); // clear out the struct for use
d3dpp.Windowed = TRUE; // program windowed, not fullscreen
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; // discard old frames
d3dpp.hDeviceWindow = hWnd; // set the window to be used by Direct3D
// create a device class using this information and information from the d3dpp stuct
d3d->CreateDevice(D3DADAPTER_DEFAULT,
D3DDEVTYPE_HAL,
hWnd,
D3DCREATE_SOFTWARE_VERTEXPROCESSING,
&d3dpp,
&d3ddev);
}
Direct3D 인터페이스 생성 코드
대부분 export 되어 있는 형태가 아니기 때문에 CreateDevice 함수를 통해 생성된 IDirect3DDevice9 인터페이스를 사용하여 렌더링 함수들을 호출해야한다.
1. 게임 프로세스의 메모리에 접근하기 위해 DLL을 인젝션한다.
DLL Injection 없이 외부 프로세스에서 메모리 관련 WIN API 를 사용하는 방식(External Hook)으로도 월핵 구현이 가능하지만 보통 개발이 편한 DLL Injection을 사용한다.
2. vtable의 주소를 찾는다. (Pattern Matching/Scanning)
IDirect3DDevice9 인터페이스는 CreateDevice 함수를 통해 생성되기 때문에 이 함수를 기점으로 분석해야한다.
CreateDevice 함수는 7번째 인자인 ppReturnedDeviceInterface로 IDirect3DDevice9 인터페이스를 반환한다.
ppReturnedDeviceInterface 변수는 CreateDevice 함수 시작 시 0으로 초기화된 후 var_164 값으로 설정되는데
이 값은 CEnum::CreateDeviceImpl 함수의 8번째 인자에서 설정된다.
LONG __stdcall CEnum::CreateDevice(
CEnum *this, // this 포인터기 때문에 msdn에는 이 부분이 없음
unsigned int a2,
enum _D3DDEVTYPE a3,
HWND a4,
unsigned int a5,
struct _D3DPRESENT_PARAMETERS_ *a6,
struct IDirect3DDevice9 **ppReturnedDeviceInterface)
{
/*
HRESULT CreateDevice(
UINT Adapter,
D3DDEVTYPE DeviceType,
HWND hFocusWindow,
DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS *pPresentationParameters,
IDirect3DDevice9 **ppReturnedDeviceInterface
);
*/
...
v12 = CEnum::CreateDeviceImpl(
(CEnum *)v7,
v8,
a3,
(HWND)*(&var_164 + 1),
a5,
v23,
v22,
(struct IDirect3DDevice9Ex **)&var_164,
v11);
v13 = var_164;
*(&var_164 + 1) = v12;
*ppReturnedDeviceInterface = (struct IDirect3DDevice9 *)var_164; // 이 곳에서 인터페이스 값이 설정됨
...
int __thiscall CEnum::CreateDeviceImpl(
CEnum *this,
unsigned int a2,
enum _D3DDEVTYPE a3,
HWND a4,
unsigned int a5,
struct _D3DPRESENT_PARAMETERS_ *arg_10,
const struct D3DDISPLAYMODEEX *a7,
struct IDirect3DDevice9Ex **ppReturnedDeviceInterface,
struct _D3D9ON12_ARGS *a9
){
if ( hLibModule )
{
v26 = CD3DHal::CD3DHal((CD3DHal *)hLibModule); // v26은 이 함수의 반환값으로 설정됨
goto LABEL_36;
}
...
if ( !v27 )
{
*ppReturnedDeviceInterface = (struct IDirect3DDevice9Ex *)v26; // ppReturnedDeviceInterface이 v26으로 설정됨
return 0;
}
(*(void (__thiscall **)(CD3DHal *, int))(*(_DWORD *)v26 + 696))(v26, 1);
D3DRecordHRESULT(
(size_t)"Failed to initialize D3DDevice. CreateDeviceEx Failed.",
(struct _hrCapture *)0xDEADBEEF,
"windows\\directx\\dxg\\inactive\\d3d9\\d3d\\fe\\d3ddev.cpp",
1068);
ppReturnedDeviceInterface 에 들어가는 값은 v26에서 왔고, v26은 CD3DHal::CD3DHal 함수에서 왔다. CD3DHal::CD3DHal 함수는 아래와 같다.
CD3DHal *__thiscall CD3DHal::CD3DHal(CD3DHal *this){
CD3DHal *v1; // esi
v1 = this;
CD3DBase::CD3DBase(this);
*(_DWORD *)v1 = &CD3DHal::`vftable'; // vtable을 초기화 하는 부분
*((_DWORD *)v1 + 3220) = 0;
*((_DWORD *)v1 + 3218) = 0;
*((_DWORD *)v1 + 3219) = 0;
*((_DWORD *)v1 + 3269) = 0;
*((_DWORD *)v1 + 3272) = 0;
*((_DWORD *)v1 + 3278) = 0;
*((_DWORD *)v1 + 3279) = 0;
*((_DWORD *)v1 + 3280) = 0;
*((_DWORD *)v1 + 3281) = 0;
*((_DWORD *)v1 + 4088) = 0;
*((_DWORD *)v1 + 4091) = 0;
*((_DWORD *)v1 + 4094) = 0;
*((_DWORD *)v1 + 4097) = 0;
*((_DWORD *)v1 + 4100) = 0;
*((_BYTE *)v1 + 16404) = 0;
*((_DWORD *)v1 + 3275) = 0;
return v1;
}
7번쨰 라인에 보이는 &CD3DHal::`vftable' 이 IDirect3DDevice9 인터페이스의 vtable이다.
public: __thiscall CD3DHal::CD3DHal(void) proc near
8B FF mov edi, edi
56 push esi
8B F1 mov esi, ecx
E8 05 73 00 00 call CD3DBase::CD3DBase(void)
33 C0 xor eax, eax
C7 06 24 1D 00 10 mov dword ptr [esi], offset const CD3DHal::`vftable' // 이 부분부터 패턴 시작
89 86 50 32 00 00 mov [esi+3250h], eax
89 86 48 32 00 00 mov [esi+3248h], eax
89 86 4C 32 00 00 mov [esi+324Ch], eax
89 86 14 33 00 00 mov [esi+3314h], eax
89 86 20 33 00 00 mov [esi+3320h], eax
89 86 38 33 00 00 mov [esi+3338h], eax
89 86 3C 33 00 00 mov [esi+333Ch], eax
89 86 40 33 00 00 mov [esi+3340h], eax
89 86 44 33 00 00 mov [esi+3344h], eax
89 86 E0 3F 00 00 mov [esi+3FE0h], eax
89 86 EC 3F 00 00 mov [esi+3FECh], eax
89 86 F8 3F 00 00 mov [esi+3FF8h], eax
89 86 04 40 00 00 mov [esi+4004h], eax
89 86 10 40 00 00 mov [esi+4010h], eax
88 86 14 40 00 00 mov [esi+4014h], al
89 86 2C 33 00 00 mov [esi+332Ch], eax
8B C6 mov eax, esi
5E pop esi
C3 retn
public: __thiscall CD3DHal::CD3DHal(void) endp
CD3DHal::CD3DHal 함수의 코드 부분을 패턴으로써 사용하여 d3d9.dll 상의 메모리를 스캔해 이 함수를 찾고 vtable의 주소를 알아낼 수 있는데, 패턴을 찾기 위해 위처럼 어셈블리어로 확인해보면
vtable을 설정하는 부분부터 추출한 옵코드는 C7 06 24 1D 00 10 89 86 50 32 00 00 89 86이다.
환경에 따라 vtable의 오프셋과 뒤에 따라오는 mov 어셈블리의 오프셋은 달라질 수 있다.
따라서 가변적인 부분을 ??로 치환하면 C7 06 ?? ?? ?? ?? 89 86 ?? ?? ?? ?? 89 86 가 되고, 이것이 최종적으로 vtable을 찾을 때 사용하게 될 패턴이다.
bool bCompare(const BYTE* pData, const BYTE* bMask, const char* szMask){
for(;*szMask;++szMask,++pData,++bMask)
if(*szMask=='x' && *pData!=*bMask )
return false;
return (*szMask) == NULL;
}
DWORD FindPattern(DWORD dwAddress,DWORD dwLen,BYTE *bMask,char * szMask){
for(DWORD i=0; i < dwLen; i++)
if( bCompare( (BYTE*)( dwAddress+i ),bMask,szMask) )
return (DWORD)(dwAddress+i);
return 0;
}
구한 패턴과 FindPattern 함수를 사용하여 다음과 같이 vtable의 주소를 구할 수 있다.
DWORD table = FindPattern(
(DWORD)hModule,
0x128000,
(PBYTE)"\xC7\x06\x00\x00\x00\x00\x89\x86\x00\x00\x00\x00\x89\x86",
"xx????xx????xx" // 가변적인 주소 부분은 ?로 마스킹
);
memcpy(&vTable, (void*)(table+2), 4); // vtable 주소 값을 복사
3. DrawIndexedprimitive 함수의 주소를 구한다.
D3D9에서 물체나 도형을 그릴 때 사용하는 함수. 성능상의 문제로 Direct3D 개발 시 DrawIndexedprimitive 함수를 주로 사용한다.
DIP 함수 주소를 찾을 때는 vtable index를 이용한다. 컴파일 된 d3d9.dll 바이너리는 vtable index가 고정되어 있기 때문에 미리 구하거나 공개된 vtable index를 이용할 수 있다.
#define DRAWINDEXEDPRIMITIVE 82
IDA를 통해 확인해보면 vtable의 주소는 0x10001D24이고 값은 82이므로
.text:10001D24 const CD3DHal::`vftable' dd offset CBaseDevice::QueryInterface(_GUID const &,void * *)
...
.text:10001E6C dd offset CD3DBase::DrawIndexedPrimitive(_D3DPRIMITIVETYPE,int,uint,uint,uint,uint)
0x10001D24 + 82 * 4(32-bit 포인터 크기) == 0x10001E6C
CD3DBase::DrawIndexedPrimitive 함수 포인터의 주소 값과 동일한 걸 확인할 수 있다.
4. 함수에 Inline Hook을 설치한다.
DIP 함수의 주소를 구했으니 이제 함수를 후킹해 Z 버퍼 기능을 비활성화해야한다.
TargetFunction의 프롤로그를 jmp 명령어로 패치하여 후킹함수 DisableZBuffer를 실행하도록 한다.
Trampoline은 후킹했던 함수의 Original Function을 호출하고 싶을 때 사용한다(우측의 oDIP 함수)
Trampoline 코드에 후킹으로 인해 유실됐던 프롤로그를 복사한 후 Original Function+5로 점프하게 해 원본 함수를 사용할 수 있도록 한다.
x86에서 DrawIndexedPrimitive 함수는 호출규약이 호출된 함수가 스택을 정리하는 stdcall이라는 점을 주의해야한다.
후킹 시 호출규약이 다르면 스택 오프셋이 달라지기 때문에 후킹 함수에 꼭 __stdcall이나 WINAPI를 선언하여 호출 규약을 지정해야 한다.
hkDrawIndexedPrimitive 함수에 코드를 작성해 사물이 렌더링되는 시점에 원하는 코드를 실행할 수 있다.
5. 숨겨진 물체를 보이게 한다.
렌더링할 때 숨겨진 물체가 보이게 하는 순서.
1. Z Buffer 비활성화
- DIP 함수를 통해 물체를 그리기 전에 Z Buffer를 비활성화해 물체가 벽 너머에서도 보일 수 있도록
2. oDrawIndexedPrimitive 호출
- Z 버퍼가 비활성화된 상태에서 물체를 그리기 위해 원본 DrawIndexedPrimitive 함수를 호출한다.
3. Z 버퍼 활성화
- Z 버퍼를 재활성화해서 다른 물체가 정상적으로 그려질 수 있도록 한다.
버퍼 활성화/비활성화 시 SetRenderState 함수를 사용한다.
HRESULT SetRenderState(
D3DRENDERSTATETYPE State,
DWORD Value
);
첫 번째 인자에 D3DRS_ZENABLE를, 두 번째 인자에 D3DZB_TRUE / D3DZB_FALSE 주는 것으로 활성화/비활성화 할 수 있다.
vtable = C++ 클래스 멤버함수 중 가상 함수가 존재하는 경우 생성되는 함수 포인터 배열.
IDirect3DDevice9 인터페이스의 vtable에 렌더링 함수들이 정해진 순서대로 존재한다.
-> vtable의 주소만 구하면 모든 가상 함수의 주소를 알 수 있게 된다.
GlassWall
조건 없이 Z Buffer 비활성화 -> DIP 함수를 사용하는 모든 물체가 보이게 된다.
우리가 원하는 대상뿐만 아니라 맵의 모든 오브젝트가 보이게 된다.
물체 구별 방법
Stride 값 이용.
- 정점 버퍼 구조체의 크기를 갖고 있는 값.
-> 현재 렌더링하고 있는 물체의 stride 값을 구해 우리가 원하는 물체의 Stride 값과 같다면 Z Buffer를 비활성화하는 방식으로 구현할 수 있다.
- 구하는 방법
값을 조금씩 증가시키면서 어떤 값일 때 우리가 원하는 물체가 표현되는지 확인하는 방법.
-> 정점 버퍼 구조체의 크기가 크지 않아서 가능.
GetStreamSource 함수 - 정점 버퍼 구조체와 Stride 값을 구할 수 있음.
oDIP 함수 = 항상 호출. Stride 값이 일치하지 않아도 물체는 그려져야 함.
NumVertics 인자 - 더 세부적인 조건으로 물체 필터링. DIP 함수의 인자. 물체의 정점 개수 확인.
벽 뒤에 캐릭터가 보여도 벽 앞에 있는 건지 벽 뒤에 있는 건지 구분하기 어렵다.
-> Chams, '형광 월핵'
색이 있는 월핵. 색을 입히기 위해 물체에 우리가 생성한 텍스쳐를 설정한다.
GenertateTexture 함수는 D3D 함수인 CreateTexture 함수로 텍스처를 생성하는 함수이다.
후킹 과정
1. GenerateTexture 함수로 빨간색, 초록색의 텍스처를 하나씩 생성
2. 우리가 표현하고 싶은 현재 물체 -> z버퍼 비활성화 -> oDIP 함수 호출 -> SetTexture 함수로 물체에 빨간색 설정
-> 해당 물체는 벽 뒤에 가려진 부분 포함 모든 부분이 빨간색으로 나타남.
3. z 버퍼 활성화하고 초록색 텍스처 설정한 후 oDIP 함수 다시 호출하면 시야에 보이는 물체의 부분만 초록색으로 그려지게 됨.
DirectX11(D3D11 버전)
D3D9과 구조가 다르기 때문에 후킹하는 함수와 원리가 조금 다르다.
- DrawIndexedPrimitive -> DrawIndexed 함수 후킹
- SetRenderState 함수를 사용하여 Z버퍼 비활성화 불가능 -> D3D11_DEPTH_STENCIL_DESC 구조체를 통해 Z Buffer를 비활성화 D3D11_DEPTH_STENCIL_DESC구조체의 DepthEnable필드를 False로 설정하면 깊이 테스트가 비활성화되어 엔진이 물체의 깊이를 구별할 수 없게 된다.
- 기존의 Depth Stencil State를 가져온다.
- OMGetDepthStencilState 함수를 통해 가져올 수 있다.
- 가져온 Depth Stencil State의 DepthEnable필드를 False로 설정한다.
- GetDesc 함수를 통해 D3D11_DEPTH_STENCIL_DESC 디스크립터를 가져와 수정할 수 있다.
- 수정한 디스크립터는 CreateDepthStencilState 함수를 호출하여 새로운 Depth Stencil State로 생성할 수 있다.
- Depth Stencil State를 교체한다.
- OMSetDepthStencilState 함수를 사용하여 새로운 Depth Stencil State를 현재 컨텍스트에 적용한다.
DepthEnable 필드를 비/활성화 하면서 원하는 물체만 표현될 수 있도록 해야한다.
'기술스터디' 카테고리의 다른 글
클라우드의 시작과 끝, 클라우드 보안 (0) | 2022.11.29 |
---|---|
디지털 트윈 트렌드 - 1. 디지털 트윈의 정의와 비즈니스 적용 방안 (0) | 2022.11.19 |
ESRC 보안 동향 보고서 - 최신 보안 동향 (0) | 2022.11.13 |
다양한 원격 제어 도구들을 악용하는 공격자들 (0) | 2022.10.31 |
위∙변조 사전 차단 (0) | 2022.10.01 |