【渲染逆向】Hook D3D API

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

前言

  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 <windows.h>
#include <iostream>
#include <tchar.h>

  
  首先我们设定要打开程序的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));
si.cb = sizeof(STARTUPINFO);
si.dwFlags |= STARTF_USESTDHANDLES;
bSuccess = CreateProcess(
    szExePath,//exe路径
    szForceDX12Cmdline,//命令行参数
    NULL,
    NULL,
    TRUE,
    0,
    NULL,
    szWorkspace,//工作路径
    &si,
    &pi
);

if (!bSuccess)
{
    std::cout << "创建失败" << std::endl;
}
else
{
    std::cout << "成功,进程号为:" << pi.dwProcessId << 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("kernel32.dll")), "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;
}

  大概是找到kernel32.dll的LoadLibraryW方法的地址,并调用加载DLL。
  最后注入就完事了:

TCHAR RenderDocDll[] = TEXT("我的renderdoc.dll的地址");
if (!Inject(RenderDocDll, pi.dwProcessId))
    std::cout << "创建远程线程失败" << std::endl;
else
    std::cout << "成功创建远程线程" << std::endl;

CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);

  这样就能通过远程注入的方式,将renderdoc注入图形API程序中,分析渲染过程。
  但假如有人不止想做这么多呢?
  在在SwapChain执行Present前,对RenderTarget进行一次后处理,岂不是能做出类似调色个功能?
  如果把人物渲染DrawCall的深度测试关闭,岂不是就能透视?
  很多外挂或图形调整插件都是这么做的。我们也可以自己实现一个dll,用来hook图形API。

inline hook DLL

  首先要引入一票头文件:

#include <Windows.h>
//提供_beginthreadex函数
#include <process.h>
//用于拍摄快照,检查当前进程已经加载了哪些dll
#include <TlHelp32.h>
#include <tchar.h>
//d3d的头文件
#include <d3d11.h>
#include <dxgi.h>

#include <d3dx11tex.h>

//STL
#include <string>
#include <sstream>
#include <vector>
#include <iostream>

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

#pragma comment(lib, "d3d11.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dx11.lib")

  如果找不到链接lib文件,还要到项目属性>链接器>常规>附加库目录中,把lib目录填入。(不知道在哪就用everything找,找不到就上网查一查安装)。
  除此外我也不打算手动实现inline hook(因为菜),所以hook交给微软的hook库Detours就好了。下载源码后,打开VS (2017)的开发人员命令提示符(可以在VS工具菜单/命令行/开发者命令提示中找到),进入src目录下,输入nmake命令,在得到的include目录下得到detours.h头文件,并链接lib.X64目录下detours.lib
  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外,最常用的还有ID3D11DeviceID3D11DeviceContext的虚表方法,用同样的方法写出这两个类的虚表枚举。
  要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));

    scd.BufferCount = 1;

    scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    scd.BufferDesc.Width = 1920;
    scd.BufferDesc.Height = 1080;

    scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

    scd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
    scd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;

    scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    scd.OutputWindow = GetForegroundWindow();

    scd.BufferDesc.RefreshRate.Numerator = 60;
    scd.BufferDesc.RefreshRate.Denominator = 1;

    scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;

    scd.SampleDesc.Count = 1;
    scd.SampleDesc.Quality = 0;

    scd.Windowed = ((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::PresentID3D11DeviceContext::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<ID3D11RenderTargetView*> 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_ppRenderTargetView.clear();

    for (int i = 0; i < NumViews; ++i)
    {
        ID3D11RenderTargetView* pRenderTargetView = *(ppRenderTargetViews + i);
        if (pRenderTargetView == nullptr)
            continue;

        g_ppRenderTargetView.push_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_ppRenderTargetView.size() == 0)
        return;

    ++gCaptureNum;

    for (int i = 0; i < g_ppRenderTargetView.size(); ++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<DXGI_FORMAT>(rtvDesc.Format) << std::endl;

        HRESULT hr = D3DX11SaveTextureToFile(g_pContext, pSourceResource, D3DX11_IFF_DDS, wss.str().c_str());
        if (SUCCEEDED(hr))
            std::wcout << "Save To " << wss.str() << 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 ID3D11CreateDeviceID3D11CreateDeviceAndSwapChain时获取不到,或许是做了Hook保护?还是因为CreateWindowExWkernel32.dll,地址空间所有进程一样?
  我不清楚,但用了另一种方法成功了:

oD3D11CreateDevice = 
(fn_D3D11CreateDevice)GetProcAddress(GetModuleHandle(_T("d3d11.dll")), "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.dll,但并没有起作用,或许我要去阅读一下RenderDoc的源码。
  相关注入器代码: crossous/RemoteInject

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353