Reversing/Hooking

[hooking] 7. D3D Hooking : Dummy Device

DirectX의 동작 방식

DirectX를 이용하기 위해선 d3d를 초기화 하고 d3d 디바이스를 생성하는 작업이 우선되어야 한다.

정석적인 d3d 개발의 순서는 다음과 같다

  1. 생성하고자 하는 윈도우 클래스 등록
  2. 윈도우를 생성하고 화면에 표시
  3. 생성된 윈도우의 핸들을 가져와 Direct3D를 초기화
  4. 메시지 루프 및 루프 종료시 초기화한 D3D를 메모리에서 해제
  5. 프로그램 종료

여기서 우리는 이미 생성된 윈도우에 후킹을 걸었으므로 1~2의 과정은 불필요하다. 현재 후킹을 걸어놓은 윈도우의 핸들(HWND)만 가져오면 된다.

4번의 메시지 루프는 openGL 이나 GDI때 사용했던 그 메시지를 가리키는게 맞다. OS가 윈도우 프로세스에 보내는 메시지에 따라 동작시키는 부분이다.

현재 우리는 후킹을 진행하여 Endscene 함수 종료시마다 후킹함수를 동작하게 하였으므로 4번과 5번 또한 구현할 필요가 없다.

/* D3D Device 생성 과정*/
HRESULT DeviceManager::Init()
{
    //디바이스를 생성하기 위한 D3D 객체 생성
    m_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
    if (nullptr == m_pD3D)
        return E_FAIL;

    D3DPRESENT_PARAMETERS d3dpp; //디바이스 생성을 위한 구조체
    ZeroMemory(&d3dpp, sizeof(D3DPRESENT_PARAMETERS)); //반드시 ZeroMemory()로 미리 구조체를 깨끗이 지워야 한다
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;          //가장 효율적인 SWAP 효과
    d3dpp.Windowed = TRUE;                             //창모드로 생성
    d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;           //현재 바탕화면 모드에 맞춰서 후면 버퍼 생성
    d3dpp.EnableAutoDepthStencil = TRUE;
    d3dpp.AutoDepthStencilFormat = D3DFMT_D16;
    d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

    D3DCAPS9 caps;
    int      vp;

    //디바이스를 다음과 같은 설정으로 생성한다
    //1. 디폴트 그래픽 카드를 사용한다 (대부분은 그래픽 카드가 1개)
    //2. HAL 디바이스를 생성한다 (HW 가속장치를 사용하겠다는 의미)
    if (FAILED(m_pD3D->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps))) //주 그래픽카드의 정보를 D3DCAPS9 에 받아온다
        return E_FAIL;

    //3. HW가 정점처리를 지원하는지 확인해 지원하는 경우 HW, 그렇지 않을 경우 SW 처리로 생성 (HW로 생성할 경우 더 높은 성능을 냄)
    if (caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT)
        vp = D3DCREATE_HARDWARE_VERTEXPROCESSING;
    else
        vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING;

    if (FAILED(m_pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, g_hWnd, vp, &d3dpp, &m_pD3DDevice)))
        return E_FAIL;

    return S_OK;
}

3번의 D3D 초기화과정에서는 3D Device 생성을 위해 d3d 객체와 구조체를 선언하고 디바이스의 설정을 제어하는 과정이 들어간다. 그 후 생성된 d3d 객체의 인터페이스인 IDirect3D9을 리턴시키고, 얻어낸 ID3D9 인터페이스로 IDirect3DDevice9을 생성한다.

즉, D3D 객체 - IDirect3D9 인터페이스 - IDirect3DDevice 의 구조를 가진다고 보면 될듯.

그 후 Dircet3D 함수들은 ID3DDevice 의 하위 메소드로 동작하고, 우리가 후킹한 EndScene 함수도 그런식으로 동작한다.

결국 우리가 후킹한 프로그램에서는 객체-인터페이스-디바이스가 이미 모두 생성되어 존재하고 있다는 의미. 만들어져 있으니 새로 만들필요 없이 가져와서 쓰기만 하면 된다.

이때 사용할 방법이 Dummy Device Method이다.


D3D Dummy Device Method

이미 디바이스가 생성되어 돌아가고 있는 프로세스를 후킹했으니, 디바이스를 가져와서 쓰기만 하면된다. 그런데 어떻게?

리버싱을 해봤던 경험으로만 생각하면, Vtable의 위치와 d3d device 하위 메소드들의 인덱스를 모두 알고있으니 거기서 함수의 주소를 가져와서 실행만 하면 되지 않을까? 라고 생각해볼 수 있다. 실제로도 그런식으로 EndScene 함수를 찾아왔으니 말이다.

그러나 이는 범용적으로 쓰기는 어렵다. 함수에 인자를 전달하는 과정등이 필요한데, API의 사용없이 어셈블리만으로 모든 동작을 구현해내는건 굉장히 힘들고 고통스러운 작업이 될 것이다.

그래서 dummy device 를 생성하는 방법을 이용할 것이다. 더미 디바이스를 만들고, 이미 존재하는 vtable 엔트리를 복사하여 마치 디바이스를 만들어서 쓰는 것 처럼 활용할 수 있다.

정리하자면

  1. 프로세스의 HWND 핸들 받아오기
  2. 받아온 핸들을 통해 device object 생성
  3. vTable 주소 가져와 device에 복사
  4. 복사된 vtable에서 모든 디바이스 오브젝트가 공유됨
  5. 이미 만들어져 있는 D3DDevice를 직접 사용하는 것과 동일한 효과를 낼 수 있음.

그러므로 더미 디바이스를 이용해 D3D 함수를 써볼텐데, 우선 예제코드를 통해 어떤 식으로 동작이 진행되는지 분석해보자.

후킹 프로세스의 HWND 받아오기

//현재 후킹 걸린 프로세스의 HWND 핸들 가져오기 
static HWND window;

BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam)
{
    DWORD wndProcId;
    GetWindowThreadProcessId(handle, &wndProcId);

    if (GetCurrentProcessId() != wndProcId)
        return TRUE; // skip to next window

    window = handle;
    return FALSE; // window found abort search
}

HWND GetProcessWindow()
{
    window = NULL;
    EnumWindows(EnumWindowsCallback, NULL);
    return window;
}

GetProcessWindow() 함수부터 보면, 내부적으로 EnumWindows 함수를 호출 하고 window라는 변수를 반환한다.

EnumWindows()

  • 현재 실행되어있는 프로세스의 목록, 핸들(HWND)받아오는 함수
  • callback 함수를 선언하여 주로 사용함. true 반환시엔 자동으로 다음 프로세스의 HWND를 탐색하고 FALSE 반환시엔 함수 호출을 중단한다.

EnumWindowsCallback 함수는 파라미터로 HWND 핸들을 가져오고, EnumWindwos 함수에서 선언했던 window 변수 값에 접근한다.

GetWindowThreadProcessID

  • HWND 핸들을 인자로 ProcessID를 받아옴.

getCurrentProcessID

  • 현재 프로세스의 PID를 받아옴. 이 경우는 현재 후킹이 걸려있는 프로세스의 PID를 가져올 것

해당 콜백함수는 false를 반환하기 전까지는 반복해서 실행되는 특성을 가지고 있는데 다음과 같이 동작한다고 정리하자.

  1. 전체 프로세스 목록을 돌면서 핸들을 찾고
  2. GetWindowThreadProcessID 함수를 통해 그 핸들을 인자로 PID를 가져 온 다음,
  3. getCurrentProcessID 로 받아온 현재 후킹이 걸린 프로세스의 PID 또한 가져온다.
  4. 2와 3에서 찾은 PID를 비교하여 일치하지 않는다면 True를 반환, 다음 프로세스의 핸들값을 가져온다
  5. 일치한다면 window 변수에 찾은 프로세스의 핸들, 즉 현재 후킹이 걸린 프로세스의 핸들을 저장하고 함수의 실행을 종료한다.

위 코드의 동작을 통해 현재 후킹을 건 프로세스의 HWND 핸들을 받아올 수 있다.

D3D Device Vtable 복사

핸들을 받아왔으니 이제 이를 통해 디바이스 오브젝트를 생성할 차례

bool GetD3D9Device(void ** pTable, size_t Size)
{
    if (!pTable)
        return false;

    IDirect3D9 * pD3D = Direct3DCreate9(D3D_SDK_VERSION);

    if (!pD3D)
        return false;

    IDirect3DDevice9 * pDummyDevice = NULL;

    // options to create dummy device
    D3DPRESENT_PARAMETERS d3dpp = {};
    d3dpp.Windowed = false;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
    d3dpp.hDeviceWindow = GetProcessWindow();

    HRESULT dummyDeviceCreated = pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, d3dpp.hDeviceWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &pDummyDevice);

    if (dummyDeviceCreated != S_OK)
    {
        // may fail in windowed fullscreen mode, trying again with windowed mode
        d3dpp.Windowed = !d3dpp.Windowed;

        dummyDeviceCreated = pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, d3dpp.hDeviceWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &pDummyDevice);

        if (dummyDeviceCreated != S_OK)
        {
            pD3D->Release();
            return false;
        }
    }

    memcpy(pTable, *reinterpret_cast<void***>(pDummyDevice), Size);

    pDummyDevice->Release();
    pD3D->Release();
    return true;
}

void * d3d9Device[119];
if (GetD3D9Device(d3d9Device, sizeof(d3d9Device)))
{
    //hook stuff using the dumped addresses
}

GetD3D9Device 함수의 동작을 보자.

  1. IDirect3D 인터페이스 생성
  2. IDirect3DDevice 더미 디바이스 생성을 위한 인자값 조정
    • HWND 핸들이 필요하기 떄문에 GetProcessWindow() 함수를 호출하여 핸들값을 가져온다.
  3. 현재 후킹이 걸린 프로세스의 HWND 핸들을 인자로 더미 디바이스 생성
    • 이 시점에서 현재 프로세스의 D3D 메소드 vtable 주소를 받아온 셈.
  4. 인자로 넘어간 pTable 배열에 더미 디바이스의 vTable 배열을 복사
  5. 생성한 더미 디바이스와 D3D 오브젝트 해제

GetD3D9Device 함수에 포인터 배열을 인자로 넘겨 현재 후킹이 걸린 프로세스의 vtable 배열을 복사해오는 동작을 진행한 것이다. 이제 모든 D3D 메소드 함수들의 함수 포인터를 가져와 사용할 수 있게 되었다.

사실상 이렇게 하면 이전에 한 것 처럼 패턴을 탐색하여 vtable을 가져오는 동작을 하지 않아도 된다. DLL 인젝션만 걸면 EndScene 함수의 주소도 가져올 수 있기 때문.

코드 패턴 탐색이 아닌 더미 디바이스 방식으로 다시 후킹을 걸어보자. 최종적으로 디바이스 vtable을 반환하여 Endscene의 주소를 가져오게 만들면 된다.

//d3d.h
namespace d3dhelper {
    static HWND window;
    void * dtable[119];
    BOOL CALLBACK EnumWindowsCallback(HWND handle, LPARAM lParam) {
        DWORD pid;//current process's PID
        GetWindowThreadProcessId(handle, &pid);

        if (GetCurrentProcessId() != pid)
            return TRUE;
        window = handle;
        return FALSE;    
    }

    HWND GetProcessWindow() {
        window = NULL;
        EnumWindows(EnumWindowsCallback, NULL);
        return window;
    }

    bool GetD3D9Device(void** pTable, size_t Size){
        if (!pTable)
            return false;
        IDirect3D9* pd3d = Direct3DCreate9(D3D_SDK_VERSION);

        if (!pd3d)
            return false;
        IDirect3DDevice9* pdd = NULL; //Dummy Device's pointer

        //dummy device's options
        D3DPRESENT_PARAMETERS d3dpp = {}; //parameters
        d3dpp.Windowed = false;
        d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
        d3dpp.hDeviceWindow = GetProcessWindow(); //get process handle

        //Create Dummy Device
        HRESULT ddc = pd3d->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, d3dpp.hDeviceWindow, D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &pdd);

        memcpy(pTable, *reinterpret_cast<void***>(pdd), Size);

        pdd->Release();
        pd3d->Release();
        return true;
    }

}
dllmain.cpp
int StartD3DHooks()
{
    DWORD vftable_addr, DXBase,ES_addr = NULL;

    FILE* pFile = nullptr;
    DXBase = (DWORD)LoadLibraryA("d3d9.dll");
    while (!DXBase);
    {
        vftable_addr = hook::FindPattern(DXBase, 0x128000,
            (PBYTE)"\xC7\x06\x00\x00\x00\x00\x89\x86\x00\x00\x00\x00\x89\x86\x00\x00\x00\x00", (char *)"xx????xx????xx????");
    } 
    ES_addr = *((BYTE*)vftable_addr + 5)*0x1000000 + *((BYTE*)vftable_addr + 4)*0x10000 + *((BYTE*)vftable_addr + 3)*0x100 + *((BYTE*)vftable_addr + 2)*0x1;
    ES_addr = (DWORD)((DWORD*)ES_addr + 42);
    ES_addr = *(DWORD*)ES_addr + 0xc;

    if (AllocConsole()) {
        freopen_s(&pFile, "CONIN$", "rb", stdin);
        freopen_s(&pFile, "CONOUT$", "wb", stdout);
        freopen_s(&pFile, "CONOUT$", "wb", stderr);
        printf("vtable code addr : %08x \n", vftable_addr);    
        printf("function addr : %08x \n", ES_addr);
    }
    //dummy device hooking
    d3dhelper::GetD3D9Device(d3dhelper::dtable, sizeof(d3dhelper::dtable));
    DWORD pEndScene = (DWORD)d3dhelper::dtable[42];
    printf("dummy Endscene addr : %08x", pEndScene);

    //hook::detour(ES_addr,5);
    return 0;
}

기존 패턴탐색 방식으로 가져온 vtable의 Endscene 주소와 더미디바이스 방식으로 가져온 endscene 주소가 동일하면 성공.

주소를 받아오는 GetD3D9Device 함수 실행 시점에서 자꾸 프로그램이 멈춰서 일단 중단.


참고 문서

https://bananamafia.dev/post/d3dhook/ - EndScene 후킹

https://darkcatgame.tistory.com/6 - d3d9 설치 및 사용

https://hellowoori.tistory.com/4 -d3d 기초

https://guidedhacking.com/threads/get-direct3d9-and-direct3d11-devices-dummy-device-method.11867/ - Dummy Device Method

https://3001ssw.tistory.com/56 - EnumWindows