教程面向有C\C++基础的人,最好还要懂一些Windows编程知识
代码一律用Visual Studio 2013编译,如果你还在用VC6请趁早丢掉它...
写这个教程只是为了让玩家更好地体验所爱的单机游戏,顺便学到些逆向知识,我不会用网络游戏做示范,请自重

本章我们要实现hook游戏的渲染过程,把渲染自机换成播放bad apple(只是我看了别人的视频想自己做一个,没什么实际意义,但是可以说明怎么修改指定物体的渲染)

效果如图,压缩GIF后出现了色差请见谅...实际效果没有色差的

本章源码使用了FFmpeg 3.1.5解码视频,具体看我上一篇文章:使用FFmpeg+GDI解码和播放视频,在本文使用时记得把FFmpeg的DLL放到游戏目录下(为什么不用DirectShow呢,因为用COM组件在dllmain里会各种死锁崩溃...)

本章使用东方绀珠传 1.00b 日文版演示,不保证在其他版本有效...

寻找物体特征

有了上一章虚函数表hook的基础我们已经可以hook DrawPrimitive等渲染函数了,但是还无法分辨当前正在渲染什么物体

CE已经提供了解决方案,看到菜单里的D3D了吧,打开游戏进程后把Hook Direct3D勾上,然后打开Snapshot handler

先改一下设置,我建议把Progressive snapshots勾上,这样更容易看清整个画面渲染过程

回到游戏后按一下F6,CE会把这帧所有的渲染结果记录下来

找到渲染目标物体的那张快照,选中它,点Done

可以看出这是通过DrawPrimitiveUP渲染的,还可以浏览渲染时的堆栈,我不是很推荐在这里比较堆栈,因为每次调用渲染函数调用栈都有可能不同,CE的类型识别会错误

来看看堆栈:

偏移量为0到40都是CE的D3DHOOK模块的栈,从44开始是游戏程序的部分,偏移量为44的是游戏调用DrawPrimitiveUP返回的地址(因为是004开头的,很容易猜出)

用IDA跳转到那个地址验证一下:

同理继续往上翻调用栈,然后通过逆向分析寻找有什么可利用的特征

我还是技术不够只能整理出这些,这是角色为灵梦时的调用栈,其他角色还会不同...

471A8D
主循环,调用渲染4724CD
BeginScene、EndScene401690
传入this,后面this都是这个,第一个this = [[[0x4E9A54] + 0x40]+0x24]
对其中一个edi,[edi+8]指向下层函数4872F2
push 14,貌似自机特有487B9C
esi = [this + 0x1CA01D4 + 14 * 0x608],不为NULL则push到下一层4818AE
switch  ([[this + 0x1CA01D4 + 14 * 0x608] + 0x18] >> 25) & 0x1F
case    147E36E
设置顶点坐标、SetTexture47DA73
SetRenderState47E499
SetTextureStageState、SetFVF、DrawPrimitiveUP

经过分析,那个push 14可以利用,看看它在栈里的位置:

0xE4 - 0x44 = 0xA0,所以我们hook DrawPrimitiveUP后在函数开头获取esp寄存器再加上0xA0就是这个14在栈中的地址了

另外物体特征不限于调用栈里的信息,有时可以用纹理或顶点信息作为特征,本例中没用到

Hook DrawPrimitiveUP

这里用上一章的虚函数表hook

     // hook D3D渲染hookVTable(g_pDevice, 83, MyDrawPrimitiveUPWrapper, &RealDrawPrimitiveUP);// ......__declspec(naked) // 不让编译器自动加代码破坏栈
HRESULT STDMETHODCALLTYPE MyDrawPrimitiveUPWrapper(IDirect3DDevice9* thiz, D3DPRIMITIVETYPE PrimitiveType, UINT PrimitiveCount, CONST void* pVertexStreamZeroData, UINT VertexStreamZeroStride)
{__asm{pop ecx  // 返回地址出栈push esp // 此时[esp] = thizpush ecx // 恢复返回地址jmp MyDrawPrimitiveUP}
}HRESULT STDMETHODCALLTYPE MyDrawPrimitiveUP(DWORD esp, IDirect3DDevice9* thiz, D3DPRIMITIVETYPE PrimitiveType, UINT PrimitiveCount, CONST void* pVertexStreamZeroData, UINT VertexStreamZeroStride)
{// 判断是不是在渲染自机if (*(DWORD*)(esp + 0x9C) == 14            // 灵梦|| *(DWORD*)(esp + 0x88) == 14      // 其他角色按住shift)                                 // 其他情况我就不知道怎么判断了...{// 实现自己的渲染}return RealDrawPrimitiveUP(thiz, PrimitiveType, PrimitiveCount, pVertexStreamZeroData, VertexStreamZeroStride);
}

这里我调试得到的特征只能用于自机角色是灵梦或其他角色按住shift时的情况,技术有限,其他情况我就不知道怎么判断了...
(东方的渲染做过优化,渲染函数只是向缓冲区里写入数据,直到纹理等渲染状态发生改变时才真正渲染,所以这里的特征有时会失效)

我们要使用内联汇编获取esp寄存器的值,我建议把要用到esp的函数都分为两个部分:

第一个函数MyDrawPrimitiveUPWrapper声明为裸函数防止编译器自动加上push等指令破坏栈,然后用汇编把esp的值作为第一个参数传给MyDrawPrimitiveUP

第二个函数MyDrawPrimitiveUP才是判断正在渲染什么物体,然后实现自己的渲染或直接传给RealDrawPrimitiveUP。因为在第一个函数获取esp之前pop了一次,所以偏移量应该是0xA0 - 4 = 0x9C

关于DllMain死锁及视频解码到纹理的问题

DllMain中是不能创建线程的,也不能等待线程结束,不然会造成死锁,但是我的解码器中要创建线程

这里的解决方法是先子类化游戏窗口(相当于hook窗口过程),然后给窗口发送自定义的消息,在主线程处理消息时完成初始化

CDecoder的析构函数中本来要等待线程结束,这里我也改成了直接TerminateThread防止死锁

BOOL APIENTRY DllMain( HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved)
{switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:// 子类化窗口,拦截窗口消息g_mainWnd = FindWindow(_T("BASE"), _T("搶曽嵁庫揱丂乣 Legacy of Lunatic Kingdom. ver 1.00b")); // 日文编码就是这样...g_realWndProc = (WNDPROC)SetWindowLongPtr(g_mainWnd, GWLP_WNDPROC, (ULONG_PTR)MyWndProc);// 接下来的初始化在主线程完成PostMessage(g_mainWnd, WM_DLL_INIT, 0, 0);break;case DLL_PROCESS_DETACH:// 恢复D3D hookif (*g_ppDevice != NULL && RealDrawPrimitiveUP != NULL)unhookVTable(g_pDevice, 83, RealDrawPrimitiveUP);// 恢复窗口过程if (IsWindow(g_mainWnd))SetWindowLongPtr(g_mainWnd, GWLP_WNDPROC, (ULONG_PTR)g_realWndProc);// 释放if (g_decoder != NULL)delete g_decoder;if (g_texture != NULL)g_texture->Release();if (g_frameBuffer != NULL)delete g_frameBuffer;break;case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:break;}return TRUE;
}LRESULT CALLBACK MyWndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{if (Msg == WM_DLL_INIT) // 在主线程的初始化{// 初始化FFmpeg解码器av_register_all();// 创建解码器g_decoder = new CDecoder("E:\\Bad Apple.avi");g_decoder->SetOnPresent(std::function<void(BYTE*)>(OnPresent));// 创建纹理g_decoder->GetVideoSize(g_videoSize);g_frameBuffer = new BYTE[g_videoSize.cx * g_videoSize.cy * 4];D3DDISPLAYMODE dm;g_pDevice->GetDisplayMode(NULL, &dm);g_pDevice->CreateTexture(g_videoSize.cx, g_videoSize.cy, 1, D3DUSAGE_DYNAMIC, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &g_texture, NULL);float scale1 = 100.0f / g_videoSize.cx;float scale2 = 100.0f / g_videoSize.cy;float scale = scale1 < scale2 ? scale1 : scale2;g_scaledSize.cx = LONG(g_videoSize.cx * scale1);g_scaledSize.cy = LONG(g_videoSize.cy * scale1);// 开始播放g_decoder->Run();// hook D3D渲染hookVTable(g_pDevice, 83, MyDrawPrimitiveUPWrapper, &RealDrawPrimitiveUP);return 0;}else if (Msg == WM_UPDATE_TEXTURE) // 更新纹理{// ......}return CallWindowProc(g_realWndProc, hWnd, Msg, wParam, lParam);
}

然后是解码视频的问题,我是在另一个线程解码视频的,而D3D9不是线程安全的,东方好像也没用D3DCREATE_MULTITHREADED这个flag

所以把解码结果先保存到一个数组,发送消息,在主线程处理消息时再更新纹理

// 把解码出来的RGB数据拷贝到g_frameBuffer
void OnPresent(BYTE* data)
{g_frameBufferLock.lock();memcpy(g_frameBuffer, data, g_videoSize.cx * g_videoSize.cy * 4);g_frameBufferLock.unlock();PostMessage(g_mainWnd, WM_UPDATE_TEXTURE, 0, 0);
}// ......else if (Msg == WM_UPDATE_TEXTURE) // 更新纹理{// D3D9不是线程安全的,这里利用了处理消息时不会渲染D3DLOCKED_RECT rect;g_texture->LockRect(0, &rect, NULL, 0);g_frameBufferLock.lock();for (int y = 0; y < g_videoSize.cy; y++)memcpy((BYTE*)rect.pBits + y * rect.Pitch, g_frameBuffer + y * g_videoSize.cx * 4, g_videoSize.cx * 4);g_frameBufferLock.unlock();g_texture->UnlockRect(0);return 0;}// ......// 东方用的顶点结构
struct THVertex
{FLOAT    x, y, z;D3DCOLOR specular, diffuse;FLOAT    tu, tv;void Set(FLOAT x_, FLOAT y_, FLOAT tu_, FLOAT tv_){x = x_;y = y_;tu = tu_;tv = tv_;}
};// ......// 判断是不是在渲染自机if (*(DWORD*)(esp + 0x9C) == 14          // 灵梦|| *(DWORD*)(esp + 0x88) == 14      // 其他角色按住shift)                                 // 其他情况我就不知道怎么判断了...{// 自己的渲染// 设置纹理坐标static THVertex vertex[6];memcpy(vertex, pVertexStreamZeroData, sizeof(vertex));float x = (vertex[0].x + vertex[5].x) / 2, y = (vertex[0].y + vertex[5].y) / 2; // 中点vertex[0].Set(x - g_scaledSize.cx / 2, y - g_scaledSize.cy / 2, 0, 0); // 左上vertex[1].Set(x + g_scaledSize.cx / 2, y - g_scaledSize.cy / 2, 1, 0); // 右上vertex[2].Set(x - g_scaledSize.cx / 2, y + g_scaledSize.cy / 2, 0, 1); // 左下vertex[3].Set(x + g_scaledSize.cx / 2, y - g_scaledSize.cy / 2, 1, 0); // 右上vertex[4].Set(x - g_scaledSize.cx / 2, y + g_scaledSize.cy / 2, 0, 1); // 左下vertex[5].Set(x + g_scaledSize.cx / 2, y + g_scaledSize.cy / 2, 1, 1); // 右下// 设置纹理IDirect3DBaseTexture9* oldTexture = NULL;thiz->GetTexture(0, &oldTexture);thiz->SetTexture(0, g_texture);// 渲染HRESULT hr = RealDrawPrimitiveUP(thiz, PrimitiveType, PrimitiveCount, vertex, VertexStreamZeroStride);// 恢复thiz->SetTexture(0, oldTexture);return hr;}

效果见本文开头的动图

更新-较完美的方法

之前我犯傻了,其实只要hook 4872F2那个函数就能知道是不是在渲染自机了,这个方法可以完美用于除了早苗以外的自机,至于早苗...效果很差,我实在不知道怎么办了

另外初始化和更新纹理也用了更加简单的方法,具体自己看代码吧

int __fastcall MyRenderPlayer(void* thiz)
{g_renderCount = 0;unhook(RenderPlayer, renderPlayerOldCode);int result = RenderPlayer(thiz);hook(RenderPlayer, MyRenderPlayer, renderPlayerOldCode);return result;
}HRESULT STDMETHODCALLTYPE MyDrawPrimitiveUP(IDirect3DDevice9* thiz, D3DPRIMITIVETYPE PrimitiveType, UINT PrimitiveCount, CONST void* pVertexStreamZeroData, UINT VertexStreamZeroStride)
{if (g_texture == NULL){// 初始化g_device->CreateTexture(g_videoSize.cx, g_videoSize.cy, 1, D3DUSAGE_DYNAMIC, D3DFMT_X8R8G8B8, D3DPOOL_DEFAULT, &g_texture, NULL);}// 判断是不是在渲染自机if (++g_renderCount == 1){// 自己的渲染// 更新纹理if (g_textureNeedUpdate){D3DLOCKED_RECT rect;g_texture->LockRect(0, &rect, NULL, 0);g_frameBufferLock.lock();for (int y = 0; y < g_videoSize.cy; y++)memcpy((BYTE*)rect.pBits + y * rect.Pitch, g_frameBuffer + y * g_videoSize.cx * 4, g_videoSize.cx * 4);g_textureNeedUpdate = false;g_frameBufferLock.unlock();g_texture->UnlockRect(0);}// 设置纹理坐标static THVertex vertex[6];memcpy(vertex, pVertexStreamZeroData, sizeof(vertex));float x = (vertex[0].x + vertex[5].x) / 2, y = (vertex[0].y + vertex[5].y) / 2; // 中点vertex[0].Set(x - g_scaledSize.cx / 2, y - g_scaledSize.cy / 2, 0, 0); // 左上vertex[1].Set(x + g_scaledSize.cx / 2, y - g_scaledSize.cy / 2, 1, 0); // 右上vertex[2].Set(x - g_scaledSize.cx / 2, y + g_scaledSize.cy / 2, 0, 1); // 左下vertex[3].Set(x + g_scaledSize.cx / 2, y - g_scaledSize.cy / 2, 1, 0); // 右上vertex[4].Set(x - g_scaledSize.cx / 2, y + g_scaledSize.cy / 2, 0, 1); // 左下vertex[5].Set(x + g_scaledSize.cx / 2, y + g_scaledSize.cy / 2, 1, 1); // 右下// 设置纹理IDirect3DBaseTexture9* oldTexture = NULL;thiz->GetTexture(0, &oldTexture);thiz->SetTexture(0, g_texture);// 渲染HRESULT hr = RealDrawPrimitiveUP(thiz, PrimitiveType, PrimitiveCount, vertex, VertexStreamZeroStride);// 恢复thiz->SetTexture(0, oldTexture);return hr;}return RealDrawPrimitiveUP(thiz, PrimitiveType, PrimitiveCount, pVertexStreamZeroData, VertexStreamZeroStride);
}// 把解码出来的RGB数据拷贝到g_frameBuffer
void OnPresent(BYTE* data)
{g_frameBufferLock.lock();memcpy(g_frameBuffer, data, g_videoSize.cx * g_videoSize.cy * 4);g_textureNeedUpdate = true;g_frameBufferLock.unlock();
}DWORD WINAPI initThread(LPVOID)
{// 初始化FFmpeg解码器av_register_all();// 创建解码器g_decoder = new CDecoder("E:\\Bad Apple.avi");g_decoder->SetOnPresent(std::function<void(BYTE*)>(OnPresent));// 创建纹理数据缓冲g_decoder->GetVideoSize(g_videoSize);g_frameBuffer = new BYTE[g_videoSize.cx * g_videoSize.cy * 4];float scale1 = 100.0f / g_videoSize.cx;float scale2 = 100.0f / g_videoSize.cy;float scale = scale1 < scale2 ? scale1 : scale2;g_scaledSize.cx = LONG(g_videoSize.cx * scale1);g_scaledSize.cy = LONG(g_videoSize.cy * scale1);// 开始播放g_decoder->Run();// hookhookVTable(g_device, 83, MyDrawPrimitiveUP, (void**)&RealDrawPrimitiveUP);hook(RenderPlayer, MyRenderPlayer, renderPlayerOldCode);return 0;
}BOOL APIENTRY DllMain( HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved)
{switch (ul_reason_for_call){case DLL_PROCESS_ATTACH:// 在另一个线程初始化,避免死锁CloseHandle(CreateThread(NULL, 0, initThread, NULL, 0, NULL));break;case DLL_PROCESS_DETACH:// 恢复hookunhook(RenderPlayer, renderPlayerOldCode);if (g_device != NULL && RealDrawPrimitiveUP != NULL)unhookVTable(g_device, 83, RealDrawPrimitiveUP);// 释放if (g_decoder != NULL)delete g_decoder;if (g_texture != NULL)g_texture->Release();if (g_frameBuffer != NULL)delete g_frameBuffer;break;case DLL_THREAD_ATTACH:case DLL_THREAD_DETACH:break;}return TRUE;
}   

本章完整源码

其他应用

CS这种FPS游戏的透视外挂就是hook了渲染函数然后禁用Z轴缓冲实现的,还有一种方框透视是读取内存中的人物坐标后自己在屏幕上渲染方框

我现在没装CS就用刺客信条看下透视效果吧...

游戏修改器制作教程⑨:修改D3D渲染相关推荐

  1. 游戏修改器制作教程四:用API读写内存

    本教程面向有C\C++基础的人,最好还要懂一些Windows编程知识 代码一律用Visual Studio 2013编译,如果你还在用VC6请趁早丢掉它... 写这个教程只是为了让玩家更好地体验所爱的 ...

  2. 游戏修改器制作教程二:键盘鼠标钩子

    本教程面向有C\C++基础的人,最好还要懂一些Windows编程知识 代码一律用Visual Studio 2013编译,如果你还在用VC6请趁早丢掉它... 写这个教程只是为了让玩家更好地体验所爱的 ...

  3. ce修改器过检测_GG修改器使用教程

    新手学修改教程 第一课:修改器的认知 https://v.qq.com/x/page/c0547cvcojf.html?ptag=2_7.6.0.20170_copy 第二课:精确数值查找 https ...

  4. flash音乐播放器 制作教程

    flash音乐播放器 制作教程 2011年09月24日 [b]请下载最新CMP v2.1正式版:[/b] [b]http://linsu.sz17399.com/cmp21/cmp21.rar[/b] ...

  5. Blender基础:阵列修改器、倒角修改器、镜像修改器

    1.修改器 修改器Modifier,对模型进行修改 相当于一个函数,类似于y=f(x) 演示: 1.选中一个mesh类型的物体 2.修改器属性,添加修改器|倒角修改器   3.点 实时.打开/关闭修改 ...

  6. Blender基础:布尔修改器、线框修改器、表面细分修改器

    目录 1 布尔修改器 1.1 积木滑道(练习) 2 线框修改器 2.1 菱形网面 2.2 实操练习:笔筒 3 表面细分修改器 3.1 细分与卡线 3.2 边线折痕 3.3 三角面.多边面 3.4 练习 ...

  7. Blender基础:曲线修改器、晶格修改器

    目录 1.曲线修改器 2.几个细节 3.半径与倾斜 4.练习锁链 5.晶格 6.晶格修改器 7.练习空间扭曲效果 形变类修改器,使物体产生形变.例如,曲线修改器.晶格修改器 1.曲线修改器 曲线函数修 ...

  8. Blender:用精简修改器和线框修改器秒一个废纸篓

    Blender 2.82 效果图 加上之前做的可乐 步骤 新建柱体 2.进入编辑模式调整一下形状 适当加环线 添加精简修改器 注意选择反细分,并且将迭代次数设为1(默认为0) 然后应用精简修改器 添加 ...

  9. 游戏修改器制作-黑客入门

    工具:SoftICE.金山游侠2002.VC++7.0.PE查看器.SPY++ 测试平台:Window2000 Professional SP2 首先我介绍一下将会用到的工具: 1. SoftICE( ...

最新文章

  1. Maven pom.xml 全配置(二)不常用配置
  2. github如何删除一个repository【找不到settings】
  3. 十年的老代码,你敢动?
  4. ajax环境配置tomcat,jcreator+tomcat环境配置
  5. 求字符串全排列的递归算法
  6. [Web开发] 微软的RSS协议扩展 - FeedSync 介绍 (2)
  7. EMMA 覆盖率工具
  8. 【Git】向Gitee提交代码
  9. QNAP 修复 NAS 备份应用中的严重漏洞
  10. w10恢复出厂设置_Win10系统恢复出厂设置和重装系统有什么区别?
  11. [UE4]关闭自动曝光
  12. python预测模型类型,多变量时间序列的预测和建模指南(附Python代码)
  13. C++for循环经典九九乘法表打印
  14. 推荐Arduino更深入学习:《新概念51单片机C语言教程》-郭天祥(文章内含学习资料供下载)
  15. 66个求职应聘技巧性问答(六)
  16. 大数据hadoop 面试经典题
  17. java encapsulation_Java Encapsulation vs Abstraction
  18. Javascript基础语法总结
  19. Android 二维码扫描(仿微信界面),根据Google zxing
  20. 基于labview开发平台的声音信号采集及处理系统设计(任务书+lunwen+翻译及原文+vi源文件+查重报告)

热门文章

  1. 【IT软技能】零基础如何自学JAVA
  2. Reinforement Learning-chapter1
  3. vue常见项目bug整理
  4. 如何编辑公众号文章(公众号文章写手)
  5. 【Electron Playground】Electron 窗口问题汇总
  6. qt+opencv调用笔记本摄像头
  7. Spring事务失效的几种原因
  8. GPU服务器可租用列表
  9. 神马搜索变身购票神器 8.8元看《美国队长3》
  10. 数字图像处理--1.2数字图像基础