DirectX 후킹 방식과 과정에 대해 다시 정리해보면,
- EndScene 함수 후킹
- 한번의 랜더링이 끝날 때 마다 EndScene 함수가 호출됨
- 해당 함수를 후킹하면 매 렌더 프레임마다 원하는 동작 수행 가능
- 주로 이를 통해 화면 위에 그림을 그릴 수 있음.
- 따라서 EndScene 함수의 주소가 필요함.
- VTable 시작주소 탐색
- EndScene 함수는 D3d9.dll의 VTable 공간에 담겨있음.
- CreateDevice 함수 어셈블리 코드 패턴 탐색을 진행하여 VTable 시작주소를 얻어올 수 있음.
- VTable 찾아 인덱스 번호 참조하여 EndScene 함수 시작주소 가져올 수 있음.
- Detour 후킹
- 원본함수의 실행코드 시작부분을 패치하여 코드 흐름을 바꾸는 Inline 후킹의 한 종류
- 원본함수의 코드를 Inline 패치
- 트램폴린 코드로 점프
- 후킹 함수 실행
- 트램폴린 코드로 복귀 - 인자, 스택, 메모리 정리등을 수행
- 다시 원본함수로 복귀 - 없었던 일인 것 처럼.
- detour 함수 내에 트램폴린 구성하는 어셈블리와 코드 패치 기능 존재.
- 후킹함수 별도로 선언
- DirectX 함수의 실행
- 윈도우 핸들 , D3D 객체, 인터페이스를 가져와 d3d 디바이스 생성
- 생성한 디바이스를 인자로 함수 실행 → 해당 D3D 디바이스에 그림이 그려짐.
- Dummy Device Method
- 기존에 생성된 디바이스의 디바이스 오브젝트를 그대로 사용하기 위함.
- D3D vTable은 프로세스 핸들 기준으로 생성됨
- 더미 디바이스 생성시 해당 프로세스의 vTable이 새로 생성한 디바이스에 공유됨
- 해당 디바이스의 vTable 복사하여 가져오면 모든 함수 포인터 갖게됨
(요약본도 길다)
이렇게 하면 해당 프로세스의 모든 D3D 함수를 사용할 수 있게된다.
EndScene Hooking - Dummy Device Method
- 프로젝트 세팅 : 프로젝트 - 속성 - 구성속성 - 링커 - 입력 -
d3d9.lib
추가
Dummy Device를 이용한 EndScene 후킹부터 재개한다.
- D3D 인스턴스 및 dummy device 생성
- 생성된 d3d device의 ptable 복사
- 복사한 ptable에서 EndScene 함수 주소 가져옴
- EndScene 함수에 후킹 진행
위와 같은 순서로 동작한다.
우선 3번까지만 만들어 EndScene 함수의 주소를 제대로 가져오는지부터 확인한다.
DWORD HookMain() {
FILE* pFile = nullptr;
if (AllocConsole()) {
freopen_s(&pFile, "CONIN$", "rb", stdin);
freopen_s(&pFile, "CONOUT$", "wb", stdout);
freopen_s(&pFile, "CONOUT$", "wb", stderr);
}
else
{
return 0;
}
//dummy device hooking
d3dhelper::GetD3D9Device(dtable, sizeof(dtable));
DWORD64 pEndScene = (DWORD64)dtable[42];
cout << "pEndScene : " << setbase(16) << pEndScene << endl;
return 0;
}
~~~
case DLL_PROCESS_ATTACH:
CloseHandle(CreateThread(nullptr, 0, (LPTHREAD_START_ROUTINE)HookMain, hModule, 0, nullptr));
이때 후킹 함수는 CreateThread 함수 통해 실행시킨 후 CloseHandle 실행, 단 한번만 돌게 만든다.
코드 빌드 후 DLL 인젝션 진행하여 함수 주소가 제대로 나오는지 확인하면
pEndScene : 7ff9934ba7e0
해당 위치를 디버거에서 따라가보면
D3D9.dll+A7E0 40 53 push rbx
D3D9.dll+A7E2 48 83 EC 40 sub rsp, 40h
D3D9.dll+A7E6 48 C7 44 24 28 FE FF FF+ mov [rsp+48h+var_20], 0FFFFFFFFFFFFFFFEh
D3D9.dll+A7EF 48 8B D9 mov rbx, rcx
위와같은 위치를 찾아가며 , 해당 위치는 CD3DBase::EndScene
임이 확인된다.
이제 EndScene 함수에 후킹을 걸어 화면 위에 오버레이를 그려볼건데, 다음 순서로 Detours 후킹이 진행된다.
1. 원본함수 실행시 후킹함수로 점프
2. 후킹함수 동작 종료시 트램폴린 코드로 점프
3. 트램폴린 코드실행 - 원본함수 동작 일부 수행
4. 원본함수로 복귀 - 없었던 일인 것 처럼.
위 동작대로 수행하기 위해선 아래 코드의 작성이 필요하다
- 원본함수 동작 덮어씌우는 코드
- 목표함수의 주소값을 직접적으로 덮어씌운다.
- 바이트 코드 남아도는 만큼은 0x90으로 채워준다.
- 트램폴린 동작 생성하는 코드
- 트램폴린에는 목표함수로 점프하는 코드가 포함되어있다.
- 트램폴린 생성 후 트램폴린 코드의 주소를 반환한다.
- 이때 점프할 위치는 (원본함수 + 덮어씌운 길이 ) 위치
- 목표함수 코드
- 동작이 완료되면 트램폴린 코드로 점프한다
- 트램폴린 코드는 원본함수 동작일부 + 원본함수로 점프 하는 동작을 수행한다.
테스트에 사용한 프로그램
https://github.com/joeyGumer/Triangle-example-directX9
- X64 Debug 로 빌드하여 테스트 진행하였다.
- 후킹된 EndScene 함수
00007FF9934BA7E0 | 48:B8 E0109591F97F0000 | mov rax,<d3dhooking2.long __cdecl hook::hEndScene(struct IDirect3DDevice9 * __ptr64)> |
00007FF9934BA7EA | FFE0 | jmp rax |
- 후킹 함수 내부
00007FF991951108 | 5B | pop rbx |
00007FF991951109 | 48:FF25 30450000 | jmp qword ptr ds:[<long (__cdecl* __ptr64 oEndScene)(struct IDirect3DDevice9 * __ptr64)>] | HookHeader.h:85
- 트램폴린 영역
0000023451350000 | 40:53 | push rbx |
0000023451350002 | 48:83EC 40 | sub rsp,40 |
0000023451350006 | 48:C74424 28 FEFFFFFF | mov qword ptr ss:[rsp+28],FFFFFFFFFFFFFFFE |
000002345135000F | 48:8BD9 | mov rbx,rcx |
0000023451350012 | 50 | push rax |
0000023451350013 | 48:B8 F2A74B93F97F0000 | mov rax,d3d9.7FF9934BA7F2 |
000002345135001D | 48:870424 | xchg qword ptr ss:[rsp],rax | [rsp]:render_frame+F5
0000023451350021 | C3 | ret |
- 트램폴린 함수 ret 시점 RSP
000000F0E88FF4D8 00007FF9934BA7F2 d3d9.00007FF9934BA7F2
- 점프할 위치는 원본 EndScene 함수
00007FF9934BA7F1 | 90 | nop |
00007FF9934BA7F2 | 48:8BC1 | mov rax,rcx |
00007FF9934BA7F5 | 4C:8D41 08 | lea r8,qword ptr ds:[rcx+8] |
00007FF9934BA7F9 | 48:F7D8 | neg rax |
제대로 후킹이 걸리는걸 확인했으니 , 화면위에 그림을 올려보자.
D3d9 Dummy Device 후킹 완료.
Dx11의 경우
DirectX11의 경우 9와 다른점이 몇가지 있다.
- 더미디바이스 생성을 위해 CreateDevice 함수 대신 D3D11CreateDeviceAndSwapChain 함수 사용
- swapchain vTable을 이용
- EndScene 함수 대신 Present 함수 후킹
d311.lib
추가
- 경우에 따라서
d3dcompiler.lib
도 필요함.
그 이유는
- D3D11CreateDevice 함수는 DeviceInterface를 반환하지 않아 이를 통해 vTable을 바로 받아오는 동작이 불가하다.
- d311은 화면을 한번 그리는 동작이 종료될 때 IDXGISwapChain::Present 함수가 실행된다.
- Present 함수 포인터는 SwapChain vTable에 존재한다. (인덱스 8번)
- 따라서 D3D11CreateDeviceAndSwapChain 함수를 실행해 vTable을 가지고 있는SwapChain 포인터를 받아와 사용한다.
따라서 d3d9 을 사용하는 부분 코드를 수정해야한다.
- CreateDevice 함수가 0x887A0004 로 인해 실패하는 경우, 두번째 인자인 Driver Type을 바꿔가면서 함수를 실행해보자.
- D3D_DRIVER_TYPE_REFERENCE → D3D_DRIVER_TYPE_HARDWARE 로 바꿔서 실행 성공함.
Present 함수는 dxgi.dll 에 CDXGISwapChain::Present로 선언되어있다.
.text:0000000180002A40 48 89 5C 24 10 mov [rsp-8+arg_8], rbx
.text:0000000180002A45 48 89 74 24 20 mov [rsp-8+arg_18], rsi
.text:0000000180002A4A 55 push rbp
.text:0000000180002A4B 57 push rdi
.text:0000000180002A4C 41 56 push r14
.text:0000000180002A4E 48 8D 6C 24 90 lea rbp, [rsp-70h]
.text:0000000180002A53 48 81 EC 70 01 00 00 sub rsp, 170h
앞에서부터 12바이트 만큼 후킹 걸고 진행해보면
매 Scene 랜더마다 후킹 함수가 호출되는게 확인된다.
D3d11 후킹 완료.
Github
https://github.com/synod2/d3d9_Endscene_DummyDevice_X64
https://github.com/synod2/d3d11_Present_DummyDevice_X64
참고 문서
https://guidedhacking.com/threads/d3d9-endscene-hook-template-using-dummy-device.14008/
https://docs.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present
https://guidedhacking.com/threads/d3d11-vtable-indexes.3813/
https://github.com/guided-hacking/GH_D3D11_Hook