2023年12月11日发(作者:)

【渲染逆向】HookD3DAPI

防止被跨省,只放一个预览

前言

  RenderDoc等一系列抓帧工具的原理,是在运行前,在图形API初始化之前将自己的dll注入到目标程序中,并hook一系列图形API,得到

API调用时的参数。如果在图形API初始化之后Hook,很可能出现无法检测到图形API(打开UI后,上面的API检测显示none)。

  我们自己写的图形程序如果没加保护,能直接用RenderDoc注入进去。

注入方式

  不管是注入自己写的dll,还是renderdoc等已经写好的dll,都需要一个注入器。现有的注入器,例如RemoteDLL就很不错,简单轻便:

RemoteDLL

  打开程序后,选择目标进程,然后选择DLL,点击注入。

  注意:注入目标程序、注入器、DLL,三者的32位、64位必须一致,并且如果目标程序有管理员权限,注入器没有,就可能导致注入器找不

到目标程序。

  这只是基本需求,但图形API的特殊性,很多函数需要在图形API初始化之前Hook,这就需要在程序打开的第一时间注入dll,因此我们需要

自己实现一个注入器。

  实现基本注入器需要引入的头文件:

#include

#include

#include

  

  首先我们设定要打开程序的exe路径、命令行参数、以及工作目录,后两者并不是必须的。命令行参数自然不必多说,工作目录一般是我们自

己写Visual Studio时,编译链接出来的exe才会和工作路径不一致,后两者如果没有特殊需求,都可以是NULL。

TCHAR szExePath[] = TEXT("你的exe路径");//你的EXE路径

TCHAR szForceDX12Cmdline[] = TEXT("-force-d3d12");//你的命令行参数,这个事例参数是强制unity游戏以d3d12运行,可以根据需要更改

TCHAR szWorkspace[] = TEXT("程序的工作目录");//程序的工作目录

  然后利用CreateProcess创建进程,打开程序:

//CreateProcess的返回值

BOOL bSuccess = FALSE;

//CreateProcess传出的进程信息

PROCESS_INFORMATION pi;

STARTUPINFO si;

ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

ZeroMemory(&si, sizeof(STARTUPINFO));

= sizeof(STARTUPINFO);

s |= STARTF_USESTDHANDLES;

bSuccess = CreateProcess(

szExePath,//exe路径

szForceDX12Cmdline,//命令行参数

NULL,

NULL,

TRUE,

0,

NULL,

szWorkspace,//工作路径

&si,

&pi

);

if (!bSuccess)

{

std::cout << "创建失败" << std::endl;

}

else

{

std::cout << "成功,进程号为:" << essId << std::endl;

}

  这样就能打开进程(如果成功),并通过PROCESS_INFORMATION对象得到进程信息。

  然后我们写一个注入方法,参数是dll的地址和目标进程号:

BOOL Inject(LPCTSTR DLLPath, DWORD ProcessID)

  我们选择远程线程注入方法。每个进程之间的空间彼此隔离,注入器无法操控目标进程的空间,但可以通过开启一个远程线程的方法。

  我对远程线程注入并不能描述的很明白,但网上资料有很多,这里放上我的代码:

BOOL Inject(LPCTSTR DLLPath, DWORD ProcessID)

{

HANDLE hProcess = nullptr;

hProcess = OpenProcess(PROCESS_ALL_ACCESS, TRUE, ProcessID);

if (!hProcess)

{

std::cout << "打开目标进程句柄失败" << std::endl;

return FALSE;

}

SIZE_T PathSize = (_tcslen(DLLPath) + 1) * sizeof(TCHAR);

LPVOID StartAddress = VirtualAllocEx(hProcess, NULL, PathSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

if (!StartAddress)

{

std::cout << "申请路径地址空间失败" << GetLastError() << std::endl;

return FALSE;

}

if (!WriteProcessMemory(hProcess, StartAddress, DLLPath, PathSize, NULL))

{

std::cout << "传入路径地址空间失败" << std::endl;

return FALSE;

}

PTHREAD_START_ROUTINE pfnStartAddress = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(_T("")), "LoadLibraryW");

if (!pfnStartAddress)

{

std::cout << "获取LoadLibraryW函数地址失败" << std::endl;

return FALSE;

}

HANDLE hThread = CreateRemoteThreadEx(hProcess, NULL, NULL, pfnStartAddress, StartAddress, NULL, NULL, NULL);

if (!hThread)

{

std::cout << "打开远程线程失败" << std::endl;

return FALSE;

}

WaitForSingleObject(hThread, INFINITE);

CloseHandle(hThread);

CloseHandle(hProcess);

return TRUE;

}

  大概是找到的LoadLibraryW方法的地址,并调用加载DLL。

  最后注入就完事了:

TCHAR RenderDocDll[] = TEXT("我的的地址");

if (!Inject(RenderDocDll, essId))

std::cout << "创建远程线程失败" << std::endl;

else

std::cout << "成功创建远程线程" << std::endl;

CloseHandle(ss);

CloseHandle(d);

  这样就能通过远程注入的方式,将renderdoc注入图形API程序中,分析渲染过程。

  但假如有人不止想做这么多呢?

  在在SwapChain执行Present前,对RenderTarget进行一次后处理,岂不是能做出类似调色个功能?

  如果把人物渲染DrawCall的深度测试关闭,岂不是就能透视?

  很多外挂或图形调整插件都是这么做的。我们也可以自己实现一个dll,用来hook图形API。

inline hook DLL

  首先要引入一票头文件:

#include

//提供_beginthreadex函数

#include

//用于拍摄快照,检查当前进程已经加载了哪些dll

#include

#include

//d3d的头文件

#include

#include

#include

//STL

#include

#include

#include

#include

  d3dx11tex.h是我用来保存RT的,需要在微软下载DirectX2010 SDK安装,这里面d3d相关的库都需要链接:

#pragma comment(lib, "")

#pragma comment(lib, "")

#pragma comment(lib, "")

  如果找不到链接lib文件,还要到项目属性>链接器>常规>附加库目录中,把lib目录填入。(不知道在哪就用everything找,找不到就上网查一

查安装)。

  除此外我也不打算手动实现inline hook(因为菜),所以hook交给微软的hook库Detours就好了。下载源码后,打开VS (2017)的开发人员

命令提示符(可以在VS工具菜单/命令行/开发者命令提示中找到),进入src目录下,输入nmake命令,在得到的include目录下得到detours.h

头文件,并链接lib.X64目录下。

  dll的main函数:

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, LPVOID)

{

DisableThreadLibraryCalls(hInstance);

switch (fdwReason)

{

case DLL_PROCESS_ATTACH:

_beginthreadex(nullptr, 0, init, nullptr, 0, nullptr);

break;

}

return TRUE;

}

  DLL_PROCESS_ATTACH是当dll注入进程后调用。

  init函数用于初始化,我们还未实现,现在就来实现init函数。

unsigned int __stdcall init(void* data)

{

return 0;

}

  嗯,这就是基础的init函数,假如在里面写一个MessageBox,用注入器注入后,就可以弹出一个对话框。为了方便我们调试,可以在目标进

程中打开一个对话框:

bool OpenConsole()

{

if (AllocConsole()) {

freopen("CONOUT$", "w", stdout);

SetConsoleTitle(L"Debug Console");

SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_RED);

std::cout << "Hello Inject!" << std::endl;

return true;

}

return false;

}

  接下来要Hook D3D11的方法,有两种,一种是类似D3D11CreateDevice这样,作用域是全局的函数,另一种是类似

IDXGISwapChain::Present这样的成员函数。先说后者。

VMT Hook

  我不清楚怎么找一个成员函数的地址,或许直接用类名ClassName::*MethodName,不过D3D这些成员函数都有些特殊,它们都是虚函

数,地址存储在虚表中。

  根据C++对象内存布局,如果对象有虚函数,那么对象最前面就是虚表指针vfptr,我们可以用x64dbg,或VS的命令行查看

IDXGISwapChain的内存布局:解决方案属性>C++>命令行>添加 /d1 reportSingleClassLayoutIDXGISwapChain >应用>编译项目,就能看

到SwapChain的布局:

IDXGISwapChain内存布局

  可以看到其中的18个方法,对应继承结构:

IDXGISwapChain : public IDXGIDeviceSubObject

IDXGIDeviceSubObject : public IDXGIObject

IDXGIObject : public IUnknown

IUnknown

  为了方便查表,我们可以把生成的表单粘贴下来做成枚举:

//D3D_VMT_Indices.h

//VMT是Virtual Method Table的缩写

enum class IDXGISwapChainVMT{

QueryInterface,

AddRef,

Release,

SetPrivateData,

SetPrivateDataInterface,

GetPrivateData,

GetParent,

GetDevice,

Present,

GetBuffer,

SetFullscreenState,

GetFullscreenState,

GetDesc,

ResizeBuffers,

ResizeTarget,

GetContainingOutput,

GetFrameStatistics,

GetLastPresentCount

};

  在DX11中,除了SwapChain外,最常用的还有ID3D11Device和ID3D11DeviceContext的虚表方法,用同样的方法写出这两个类的虚表

枚举。

  要Hook这些虚函数,先要获取它们的虚表指针,我们获取不到目标程序创建的Device、Context、SwapChain对象,但好笑的是,相同类

型的对象共用一个虚表指针的地址,所以我们可以创建一个Device、Context、SwapChain,虽然这些不能用于渲染,但可以得到虚表指针,然

后Hook其中的虚函数,当D3D程序内部Device等对象调用这些函数时,会自己把自己送给我们。

  当然,当前的任务还是获取虚表指针,为此我们创建这三个对象:

void** g_pDeviceVMT = nullptr;

void** g_pSwapchainVMT = nullptr;

void** g_pDeviceContextVMT = nullptr;

//用于创建Device、SwapChain、Context,只要能成功创建出来,参数是随意的

bool GetD3D11VMT()

{

//这些对象只是为了获取虚表,并不需要被使用

ID3D11Device* l_pDevice = nullptr;

IDXGISwapChain* l_pSwapchain = nullptr;

ID3D11DeviceContext* l_pDeviceContext = nullptr;

DXGI_SWAP_CHAIN_DESC scd;

ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));

Count = 1;

= DXGI_FORMAT_R8G8B8A8_UNORM;

= 1920;

= 1080;

Usage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

g = DXGI_MODE_SCALING_UNSPECIFIED;

neOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;

= DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

Window = GetForegroundWindow();

tor = 60;

nator = 1;

fect = DXGI_SWAP_EFFECT_DISCARD;

= 1;

y = 0;

ed = ((GetWindowLongPtr(GetForegroundWindow(), GWL_STYLE) & WS_POPUP) != 0) ? false : true;

D3D_FEATURE_LEVEL featLevel;

HRESULT hr = D3D11CreateDeviceAndSwapChain(

nullptr,

D3D_DRIVER_TYPE_REFERENCE,

nullptr,

0,

nullptr,

0,

D3D11_SDK_VERSION,

&scd,

&l_pSwapchain,

&l_pDevice,

&featLevel,

nullptr

);

if (FAILED(hr))

{

std::cout << "创建D3D11Device和SwapChain失败" << std::endl;

return false;

}

l_pDevice->GetImmediateContext(&l_pDeviceContext);

//获取虚表

g_pSwapchainVMT = *(void***)l_pSwapchain;

g_pDeviceVMT = *(void***)l_pDevice;

g_pDeviceContextVMT = *(void***)l_pDeviceContext;

std::cout << "获取虚表成功" << std::endl;

return true;

}

  此时我们就可以通过Detours Hook虚函数了,我这里演示下Present的Hook流程:

//定义Present的类型,注意因为是成员虚函数,第一个参数要传入对象的地址

using vfn_SwapChain_Present = HRESULT(WINAPI*) (IDXGISwapChain* pThis, UINT SyncInterval, UINT Flags);

//原来Present的地址

vfn_SwapChain_Present oPresent = nullptr;

//替换Present的方法

HRESULT WINAPI HookFuncSwapChainPresent(IDXGISwapChain* pThis, UINT SyncInterval, UINT Flags)

{

//输出一句话,并调用原来的Present方法

std::cout << "Hook Present" << std::endl;

return oPresent(pThis, SyncInterval, Flags);

}

bool HookPresent()

{

void** p_SwapChain_VMT = g_pSwapchainVMT;

oPresent = (vfn_SwapChain_Present)(p_SwapChain_VMT[(UINT)IDXGISwapChainVMT::Present]);

DetourTransactionBegin();

DetourUpdateThread(GetCurrentThread());

//主要是这一句,将原来的Present替换成我们的Present

DetourAttach((PVOID*)&oPresent, HookFuncSwapChainPresent);

DetourTransactionCommit();

return true;

}

  这样就能做不少事,例如我们可以Hook IDXGISwapChain::Present和ID3D11DeviceContext::DrawIndexed

DrawIndexedInstanced,从而统计每渲染一帧的DrawCall数量。

  我们也可以试着像RenderDoc那样,将每一个DrawCall后的RT保存下来。

  我们可以Hook ID3D11CreateDeviceAndSwapChain直接获取Device和SwapChain并全局保存,但这个方法一会再提,我们先偷个懒,

在Present第一次运行的时候,通过传递过来的SwapChain的GetDevice方法获取Device,然后通过Device的GetImmediateContext方法获取

Context:

//全局变量

IDXGISwapChain* g_pSwapchain = nullptr;

ID3D11Device* g_pDevice = nullptr;

ID3D11DeviceContext* g_pContext = nullptr;

bool IsInit()

{

return (g_pDevice != nullptr) && (g_pSwapchain != nullptr) && (g_pContext != nullptr);

}

void InitD3D(IDXGISwapChain* pSwapChain)

{

if (!IsInit())

{

g_pSwapchain = pSwapChain;

pSwapChain->GetDevice(__uuidof(ID3D11Device), (void**)&g_pDevice);

g_pDevice->GetImmediateContext(&g_pContext);

}

}

//上面声明的 HookFuncSwapChainPresent 方法内加上

if (!IsInit())

{

InitD3D(pThis);

}

  然后写一些截屏的逻辑

//全局变量

bool doCapture = false;

bool Capturing = false;

int gCaptureNum = 0;

void TriggerCapture()

{

doCapture = true;

}

//上面声明的 HookFuncSwapChainPresent 中加入:

if (Capturing)//如果上一帧在截屏,关闭截屏

Capturing = false;

if (doCapture)//如果要截屏

{

doCapture = false;

Capturing = true;

gCaptureNum = 0;

std::cout << "截帧" << std::endl;

}

  要保存RT,就要获取当前RT,我们可以通过Hook Context的OMSetRenderTargets方法,来维护一个全局当前的RT变量。虽然下面的方

法看起来一团乱麻,但和上面一开始Hook Present的套路完全一样

//RT数怎么也大不过8吧?如果大过也没事,反正不会频繁分配空间

std::vector g_ppRenderTargetView(8);

using vfn_DeviceContext_OMSetRenderTargets = void(WINAPI*)(ID3D11DeviceContext*,

__in_range(0, D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT) UINT NumViews,

__in_ecount_opt(NumViews) ID3D11RenderTargetView* const* ppRenderTargetViews,

__in_opt ID3D11DepthStencilView* pDepthStencilView);

vfn_DeviceContext_OMSetRenderTargets oDeviceContext_OMSetRenderTargets = nullptr;

void WINAPI HookFuncDeviceContext_OMSetRenderTargets(ID3D11DeviceContext* pThis,

__in_range(0, D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT) UINT NumViews,

__in_ecount_opt(NumViews) ID3D11RenderTargetView* const* ppRenderTargetViews,

__in_opt ID3D11DepthStencilView* pDepthStencilView)

{

g_();

for (int i = 0; i < NumViews; ++i)

{

ID3D11RenderTargetView* pRenderTargetView = *(ppRenderTargetViews + i);

if (pRenderTargetView == nullptr)

continue;

g__back(pRenderTargetView);

}

return oDeviceContext_OMSetRenderTargets(pThis, NumViews, ppRenderTargetViews, pDepthStencilView);

}

bool HookDeviceContext_OMSetRenderTargets()

{

void** p_DeviceContext_VMT = g_pDeviceContextVMT;

oDeviceContext_OMSetRenderTargets = (vfn_DeviceContext_OMSetRenderTargets)p_DeviceContext_VMT[(UINT)ID3D11DeviceContextVMT::OMSetRenderTargets];

DetourTransactionBegin();

DetourUpdateThread(GetCurrentThread());

DetourAttach((PVOID*)&oDeviceContext_OMSetRenderTargets, HookFuncDeviceContext_OMSetRenderTargets);

DetourTransactionCommit();

return true;

}

  有了RT就能得到资源(Resource),就有办法保存:

void CaptureFrame()

{

if (g_() == 0)

return;

++gCaptureNum;

for (int i = 0; i < g_(); ++i)

{

std::wstringstream wss;

wss << L"我的保存路径Image_"

<< gCaptureNum << "_RT" << i << ".dds";

ID3D11RenderTargetView* view = g_ppRenderTargetView[i];

if (view == nullptr)

continue;

ID3D11Resource* pSourceResource;

view->GetResource(&pSourceResource);

D3D11_RENDER_TARGET_VIEW_DESC rtvDesc;

view->GetDesc(&rtvDesc);

std::cout << "Resource DXGIFormat: " << magic_enum::enum_name() << std::endl;

HRESULT hr = D3DX11SaveTextureToFile(g_pContext, pSourceResource, D3DX11_IFF_DDS, ().c_str());

if (SUCCEEDED(hr))

std::wcout << "Save To " << () << std::endl;

else

std::cout << "截图错误:" << hr << std::endl;

pSourceResource->Release();

}

}

  我试过在某个新游戏(对现在来说)用BMP格式保存,可惜只有渲染UI时能正常保存,PNG也是,不过DDS格式竟然能正常保存。

  然后是体力活,要Hook Context的DrawIndexed和DrawIndexed,如果有必要,还有Draw和DrawInstanced,这些方法中先调用

DrawCall,然后调用CaptureFrame,这里我放下Hook DrawIndexed的事例:

using vfn_DeviceContext_DrawIndexed = void(STDMETHODCALLTYPE*)(ID3D11DeviceContext*, UINT, UINT, UINT);

vfn_DeviceContext_DrawIndexed oDrawIndexed = nullptr;

void STDMETHODCALLTYPE HookFuncDeviceContextDrawIndexed(ID3D11DeviceContext* Context,

UINT IndexCount,

UINT StartIndexLocation,

UINT BaseVertexLocation)

{

oDrawIndexed(Context, IndexCount, StartIndexLocation, BaseVertexLocation);

if (Capturing)

CaptureFrame();

}

bool HookDrawIndexed()

{

void** p_DeviceContext_VMT = g_pDeviceContextVMT;

oDrawIndexed = (vfn_DeviceContext_DrawIndexed)(p_DeviceContext_VMT[(UINT)ID3D11DeviceContextVMT::DrawIndexed]);

DetourTransactionBegin();

DetourUpdateThread(GetCurrentThread());

DetourAttach((PVOID*)&oDrawIndexed, HookFuncDeviceContextDrawIndexed);

DetourTransactionCommit();

return true;

}

全局空间函数 Hook

  和上面基本同样的套路,我Hook CreateWindowExW:

using fn_CreateWindowExW = HWND(WINAPI*)(

DWORD, LPCWSTR, LPCWSTR, DWORD, int, int,

int, int, HWND, HMENU, HINSTANCE, LPVOID

);

fn_CreateWindowExW oCreateWindowExW = CreateWindowExW;

HWND WINAPI HookFuncCreateWindowExW(DWORD dwExStyle, LPCWSTR lpClassName,

LPCWSTR lpWindowName, DWORD dwStyle, int X, int Y, int nWidth, int nHeight,

HWND hWndParent, HMENU hMenu, HINSTANCE hInstance, LPVOID lpParam)

{

std::wcout << L"HookCreateWindowExW! WindowName: " << lpWindowName << std::endl;

std::cout << "X: " << X << ", Y:" << Y << ", width: " << nWidth << ", height: " << nHeight << std::endl;

auto res = oCreateWindowExW(

dwExStyle, lpClassName, lpWindowName, dwStyle, X, Y, nWidth, nHeight,

hWndParent, hMenu, hInstance, lpParam);

return res;

}

bool HookCreateWindowExW()

{

DetourTransactionBegin();

DetourUpdateThread(GetCurrentThread());

DetourAttach((PVOID*)&oCreateWindowExW, HookFuncCreateWindowExW);

DetourTransactionCommit();

return true;

}

  嗯,这是能运作的,可惜我Hook ID3D11CreateDevice和ID3D11CreateDeviceAndSwapChain时获取不到,或许是做了Hook保护?

还是因为CreateWindowExW是,地址空间所有进程一样?

  我不清楚,但用了另一种方法成功了:

oD3D11CreateDevice =

(fn_D3D11CreateDevice)GetProcAddress(GetModuleHandle(_T("")), "D3D11CreateDevice");

  我可以用这个开启Debug Layer:

HRESULT WINAPI HookFuncD3D11CreateDevice(

_In_opt_ IDXGIAdapter* pAdapter,

D3D_DRIVER_TYPE DriverType,

HMODULE Software,

UINT Flags,

_In_reads_opt_(FeatureLevels) CONST D3D_FEATURE_LEVEL* pFeatureLevels,

UINT FeatureLevels,

UINT SDKVersion,

_COM_Outptr_opt_ ID3D11Device** ppDevice,

_Out_opt_ D3D_FEATURE_LEVEL* pFeatureLevel,

_COM_Outptr_opt_ ID3D11DeviceContext** ppImmediateContext

)

{

std::cout << "Hook D3D11CreateDevice!" << "Flag: " << Flags << std::endl;

if (Flags == 1)

Flags |= D3D11_CREATE_DEVICE_DEBUG;

HRESULT hr = oD3D11CreateDevice(pAdapter, DriverType, Software, Flags,

pFeatureLevels, FeatureLevels, SDKVersion, ppDevice,

pFeatureLevel, ppImmediateContext

);

return hr;

}

总结

  通过这样的方法,我hook到游戏中并截帧,不过这个方法有很多缺陷,例如那个游戏在渲染数多时,有1500左右DrawCall,因为用了延迟

管线,不少DrawCall都是MRT,最终保存的RT数要乘上3、4倍的DrawCall数,14G左右,尽管是三星SSD硬盘,也运行了3-5分钟,保存下来

的DDS图片也未必是都能看的,VisualStudio和RenderDoc各能读取一些。

  注入器的编写也碰到过一些问题,通过把注入器改成系统文件名解决了……

  如果有办法,我还是想通过注入RenderDoc的方式分析并截帧,可惜现在注入后,能在dll列表中看到,但并没有起作用,或

许我要去阅读一下RenderDoc的源码。