20.UE-游戏逆向-绘制所有对象坐标
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动!
本次游戏没法给
内容参考于:微尘网络安全
上一个内容:19.UE-游戏逆向-屏幕坐标转换
上一个内容中找到了ProjectWorldLocationToScreen函数,在ProjectWorldLocationToScreen函数中调用了ProjectWorldToScreen函数,然后ProjectWorldLocationToScreen函数第一个参数是一个this,也就是一个APlayerController类型,如下图APlayerController从UWorld中得到
接下来找APlayerController在UWorld中的偏移,所以打开之前Dump的类代码,如下图UGameInstance在0x180位置
然后是UGameInstance中的 LocalPlayers,它在0x38位置,注意它是TArray类型是一个数组,需要进行加0x0取第一个数据
然后进入ULocalPlayer,但是在下图红框位置看不到我们需要的PlayerController,这是因为PlayerController在父类中创建的,也就是在UPlayer类中
在0x30位置,所以得到APlayerController类型的公式是(((0x180)+0x38)+0x0)+0x30
然后开始写代码,首先修改下图红框的代码,添加一个&
然后再写下图红框的代码,核心逻辑就是得到Aactor然后从Aactor中得到三维坐标,然后找到UE中三维坐标转屏幕坐标的函数,然后进行转换打印就可以了
效果图:三维坐标转成屏幕坐标后,实现了透视
完整代码
// 引入主头文件(包含程序所需的基础定义、函数声明等)
#include "main.h"// 查找UE游戏窗口:UE引擎窗口类名固定为"UnrealWindow",获取窗口句柄(HWND)用于后续UI绘制和消息处理
HWND hWnd = FindWindowA("UnrealWindow", NULL);// DirectX 11渲染相关组件(用于在游戏画面上叠加UI)
static ID3D11Device* g_pd3dDevice = nullptr; // DX11设备:负责创建渲染资源(如纹理、缓冲区)
static ID3D11DeviceContext* g_pd3dDeviceContext = nullptr; // DX11设备上下文:负责执行渲染命令(如绘制UI)
static IDXGISwapChain* g_pSwapChain = nullptr; // 交换链:连接显卡与窗口,负责将渲染结果显示到屏幕
static ID3D11RenderTargetView* g_mainRenderTargetView = nullptr; // 渲染目标视图:UI绘制的"画布",最终会被交换链显示
static DWORD64* VirtaulTable; // 交换链的虚函数表:存储DirectX函数地址,Hook通过修改这里实现// GWorld:游戏世界(UWorld类)的全局指针地址
// 解析:游戏主程序模块基地址(通过GetModuleHandle获取) + 0x4BDAAC0(UWorld在模块中的偏移)
// UWorld是UE的核心类,管理整个游戏世界(包含关卡、玩家、实体等所有核心数据)
DWORD64 GWorld = (DWORD64)GetModuleHandleA("xxx-Win64-Shipping.exe") + 0x4BDAAC0;// FVector:UE中表示3D空间坐标的结构体(如角色在地图中的X/Y/Z位置)
struct FVector
{float X; // X轴坐标(左右方向)float Y; // Y轴坐标(前后方向)float Z; // Z轴坐标(上下方向)
};// FVector2D:UE中表示2D屏幕坐标的结构体(如UI元素在屏幕上的位置)
struct FVector2D
{float X; // 屏幕水平坐标(左→右)float Y; // 屏幕垂直坐标(上→下)
};// 函数指针:UE内置的"3D世界坐标转2D屏幕坐标"函数(ProjectWorldToScreen)
// 功能:将游戏中3D位置(如角色位置)转换为玩家视角下的屏幕2D位置(用于在屏幕对应位置画标签)
// 参数:Player(玩家控制器指针,提供视角基准)、WorldLocation(3D世界坐标)、ScreenLocation(输出的2D屏幕坐标)、是否相对玩家视口
typedef bool (WINAPI* FN_ProjectWorldToScreen)(void* Player, FVector& WorldLocation, FVector2D& ScreenLocation, bool bPlayerViewportRelative);// DirectX交换链的ResizeBuffers函数指针:处理窗口大小变化时的渲染资源调整
// 当游戏窗口缩放时,需要重新创建渲染目标,否则UI会显示异常
typedef HRESULT(STDMETHODCALLTYPE* ResizeBuffers)(IDXGISwapChain* This, // 交换链自身指针UINT BufferCount, // 缓冲数量(画面缓存的个数)UINT Width, // 窗口新宽度UINT Height, // 窗口新高度DXGI_FORMAT NewFormat, // 新的像素格式(如RGBA8)UINT SwapChainFlags); // 交换链标志(额外配置)
ResizeBuffers MyResizeBuffers; // 保存原始的ResizeBuffers函数(后续需要调用以保证窗口正常缩放)// DirectX交换链的Present函数指针:渲染流程的最后一步,负责将画面显示到屏幕
// 我们通过Hook这个函数,在游戏显示画面时插入自己的UI绘制逻辑
typedef HRESULT(STDMETHODCALLTYPE* Present)(IDXGISwapChain* This, // 交换链自身指针UINT SyncInterval, // 同步间隔(0表示立即显示,不等待显示器刷新)UINT Flags); // 显示标志(无特殊需求填0)
Present MyPresent; // 保存原始的Present函数(最后需要调用它让游戏画面正常显示)// 自定义的Present钩子函数:游戏每次调用Present显示画面时,会先执行这里的逻辑
HRESULT VtPresent(IDXGISwapChain* This,UINT SyncInterval,UINT Flags) {// 初始化ImGui(轻量级UI库)的新帧:准备绘制新的UI内容(类似"准备一张透明贴纸")ImGui_ImplDX11_NewFrame();ImGui_ImplWin32_NewFrame();ImGui::NewFrame();// 创建一个ImGui窗口(标题为"Hello, world!",用于显示调试信息)ImGui::Begin("Hello, world!");// 步骤1:获取游戏中所有Actor(实体,如角色、道具、NPC等)的列表地址// 内存偏移解析:// GWorld(UWorld*)-> 0x30:UWorld类的CurrentLevel成员(ULevel*类型,指向当前加载的关卡)// -> 解引用(*)得到ULevel的地址 -> 0x98:ULevel类的Actors成员(TArray<AActor*>类型,存储关卡中所有实体)的Data指针(数组首地址)// -> 解引用得到Actors数组的首地址(Aactor变量,即所有实体的指针列表)DWORD64 Aactor = *(DWORD64*)(*(DWORD64*)(*(DWORD64*)GWorld + 0x30) + 0x98);// 步骤2:获取Actor列表的元素数量(即关卡中有多少个实体)// 内存偏移解析:// 同上,ULevel的Actors成员(TArray)的Num成员(元素数量)偏移为0xA0int Num = *(int*)(*(DWORD64*)(*(DWORD64*)GWorld + 0x30) + 0xa0);// 步骤3:获取"3D坐标转2D屏幕坐标"的函数地址(游戏内置函数,通过模块基地址+偏移定位)FN_ProjectWorldToScreen My_ProjectWorldLocationToScreen = (FN_ProjectWorldToScreen)((DWORD64)GetModuleHandleA("xxxx-Win64-Shipping.exe") + 0x2BB56F0);// 步骤4:获取玩家控制器(APlayerController):相当于"玩家的眼睛",用于计算坐标时以玩家视角为基准// 内存偏移解析:// GWorld(UWorld*)-> 0x180:UWorld类的PlayerControllers成员(TArray<APlayerController*>类型,存储所有玩家控制器)// -> 解引用得到TArray的地址 -> 0x38:TArray的Data成员(指向玩家控制器数组的首地址)// -> 解引用得到玩家控制器数组的首地址 -> 0x0:数组中第一个元素的偏移(即第一个玩家控制器的指针,通常是本地玩家)// -> 解引用得到第一个APlayerController的地址 -> 0x30:APlayerController自身实例的指针(用于后续坐标转换)void* Player = (void*)*(DWORD64*)(*(DWORD64*)(*(DWORD64*)(*(DWORD64*)GWorld + 0x180) + 0x38) + 0x0) + 0x30);// 遍历所有Actor(逐个处理每个实体)for (int i = 0; i < Num; i++){// 过滤无效实体:// 1. Aactor + 8*i:第i个Actor的指针地址(每个指针占8字节)// 2. *(DWORD64*)(Aactor + 8*i) != 0:指针不为空(实体存在)// 3. *(DWORD64*)(*(DWORD64*)(Aactor + 8*i) + 0x130) != 0:实体的根组件存在(根组件存储位置信息)// 其中0x130是AActor类的RootComponent成员(USceneComponent*类型,实体的根组件,必含位置数据)if ((Aactor + 8 * i) != 0 && *(DWORD64*)(Aactor + 8 * i) != 0 && *(DWORD64*)(*(DWORD64*)(Aactor + 8 * i) + 0x130) != 0) {// 步骤5:获取当前Actor的3D世界坐标(从根组件中读取)FVector MyFVector = { 0,0,0 }; // 初始化3D坐标// 解析:// 根组件(USceneComponent*)的RelativeLocation成员(FVector类型,存储实体的位置)// 0x1D0:RelativeLocation.X(X轴坐标)// 0x1D4:RelativeLocation.Y(Y轴坐标)// 0x1D8:RelativeLocation.Z(Z轴坐标)MyFVector.X = *(float*)(*(DWORD64*)(*(DWORD64*)(Aactor + 8 * i) + 0x130) + 0x1d0);MyFVector.Y = *(float*)(*(DWORD64*)(*(DWORD64*)(Aactor + 8 * i) + 0x130) + 0x1d4);MyFVector.Z = *(float*)(*(DWORD64*)(*(DWORD64*)(Aactor + 8 * i) + 0x130) + 0x1d8);// 步骤6:将3D世界坐标转换为屏幕2D坐标(使用玩家控制器的视角)FVector2D MyFVector2D = { 0,0 }; // 初始化2D坐标My_ProjectWorldLocationToScreen(Player, MyFVector, MyFVector2D, 1); // 最后一个参数1表示坐标相对玩家视口// 步骤7:在屏幕对应位置绘制黄色文本"AM"(标记该实体)ImGui::GetForegroundDrawList()->AddText({ MyFVector2D.X,MyFVector2D.Y }, ImColor(255, 255, 0), "AM");}}// 结束ImGui窗口绘制(完成当前帧UI绘制)ImGui::End();// 将ImGui绘制的UI内容渲染到屏幕(把"贴纸"贴到游戏画面上)ImGui::Render();g_pd3dDeviceContext->OMSetRenderTargets(1, &g_mainRenderTargetView, nullptr); // 设置渲染目标ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); // 渲染UI数据// 调用原始的Present函数,让游戏画面正常显示(保留游戏原本的画面)return MyPresent(This, SyncInterval, Flags);
}// ImGui的窗口消息处理函数:用于处理UI的输入事件(如鼠标点击、键盘输入)
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
WNDPROC myWNDPROC; // 保存原始的窗口消息处理函数(用于后续还原消息处理)// 自定义的窗口消息钩子:拦截窗口消息,让ImGui能响应输入(如拖动UI窗口、输入文字)
LRESULT WINAPI WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{// 让ImGui先处理消息(如果是UI相关事件,如点击ImGui窗口,则由ImGui处理)if (ImGui_ImplWin32_WndProcHandler(hWnd, msg, wParam, lParam))return true;// 非UI事件,调用原始的窗口消息处理函数(保证游戏本身的操作正常,如移动、跳跃)return ::CallWindowProc(myWNDPROC, hWnd, msg, wParam, lParam);
}// 初始化函数:第一次调用Present时执行,用于设置ImGui和完成Hook
HRESULT Init(IDXGISwapChain* This,UINT SyncInterval,UINT Flags) {// 从交换链获取DirectX设备和设备上下文(用于后续创建UI渲染资源)This->GetDevice(_uuidof(g_pd3dDevice), (void**)&g_pd3dDevice);g_pd3dDevice->GetImmediateContext((ID3D11DeviceContext**)&g_pd3dDeviceContext);// 创建渲染目标视图(UI绘制的"画布",关联到游戏窗口的后台缓冲)ID3D11Texture2D* pBackBuffer; // 后台缓冲(游戏画面的原始数据存储在这里)This->GetBuffer(0, IID_PPV_ARGS(&pBackBuffer)); // 获取交换链的第一个缓冲g_pd3dDevice->CreateRenderTargetView(pBackBuffer, nullptr, &g_mainRenderTargetView); // 创建渲染目标pBackBuffer->Release(); // 释放临时缓冲(已通过渲染目标关联)// 初始化ImGui(仅执行一次,避免重复初始化)static bool is = true;if (is) {is = false;// Hook窗口消息处理函数(让ImGui能接收鼠标/键盘输入)myWNDPROC = (WNDPROC)SetWindowLongPtrA(hWnd, GWLP_WNDPROC, (LONG_PTR)WndProc);ImGui::CreateContext(); // 创建ImGui上下文(核心数据结构)ImGui::StyleColorsDark(); // 设置UI主题为深色ImGui_ImplWin32_Init(hWnd); // 初始化ImGui的Win32平台支持(关联窗口)}// 初始化ImGui的DirectX 11支持(关联DX设备和上下文,用于绘制UI)ImGui_ImplDX11_Init(g_pd3dDevice, g_pd3dDeviceContext);printf("HOOK成功"); // 打印日志,提示Hook已完成// 将交换链虚函数表中的Present函数地址替换为自定义的VtPresent(完成Hook)// 后续游戏调用Present时,会先执行VtPresent中的逻辑(即我们的UI绘制)VirtaulTable[8] = (DWORD64)VtPresent;return MyPresent(This, SyncInterval, Flags); // 调用原始Present,显示第一帧画面
}// 窗口大小变化钩子:处理窗口缩放时的资源释放(避免UI渲染错误)
HRESULT VtResizeBuffers(IDXGISwapChain* This,UINT BufferCount,UINT Width,UINT Height,DXGI_FORMAT NewFormat,UINT SwapChainFlags) {// 窗口大小变化时,释放ImGui和DirectX的相关资源(旧资源尺寸不匹配新窗口)if (g_pd3dDevice) {g_pd3dDevice->Release(); // 释放DX设备g_pd3dDevice = nullptr;g_mainRenderTargetView->Release(); // 释放旧渲染目标ImGui_ImplDX11_Shutdown(); // 关闭ImGui的DX支持VirtaulTable[8] = (DWORD64)Init; // 暂时恢复Present为Init,下次调用时重新初始化资源}// 调用原始的ResizeBuffers函数,让游戏正常处理窗口大小变化(调整自身渲染资源)return MyResizeBuffers(This, BufferCount, Width, Height, NewFormat, SwapChainFlags);
}// 线程入口函数:负责初始化DirectX环境并完成Hook
DWORD Go(LPVOID lpThreadParameter) {// 初始化交换链描述(定义DirectX交换链的配置,如缓冲数量、像素格式、关联窗口等)DXGI_SWAP_CHAIN_DESC sd;ZeroMemory(&sd, sizeof(sd)); // 清空结构体,避免随机值影响配置sd.BufferCount = 2; // 双缓冲(减少画面闪烁)sd.BufferDesc.Width = 0; // 宽度随窗口自动调整sd.BufferDesc.Height = 0; // 高度随窗口自动调整sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 像素格式:RGBA各8位(标准彩色)sd.BufferDesc.RefreshRate.Numerator = 60; // 刷新率60Hzsd.BufferDesc.RefreshRate.Denominator = 1;sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH; // 允许切换显示模式(如全屏/窗口)sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 缓冲用途:渲染目标输出(存储画面)sd.OutputWindow = hWnd; // 关联到游戏窗口sd.SampleDesc.Count = 1; // 采样数1(不开启抗锯齿)sd.SampleDesc.Quality = 0;sd.Windowed = TRUE; // 窗口模式(非全屏)sd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; // 交换后丢弃旧缓冲(提高性能)// 创建DirectX 11设备和交换链(用于后续Hook渲染函数)UINT createDeviceFlags = 0; // 设备创建标志(无特殊需求填0)D3D_FEATURE_LEVEL featureLevel; // 支持的DX特性等级const D3D_FEATURE_LEVEL featureLevelArray[2] = { D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, }; // 优先DX11, fallback到DX10HRESULT res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, createDeviceFlags,featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain,&g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);// 如果硬件不支持DX11,尝试使用软件模拟驱动(WARP)if (res == DXGI_ERROR_UNSUPPORTED)res = D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, createDeviceFlags,featureLevelArray, 2, D3D11_SDK_VERSION, &sd, &g_pSwapChain,&g_pd3dDevice, &featureLevel, &g_pd3dDeviceContext);if (res != S_OK) // 创建失败则退出线程return false;// 获取交换链的虚函数表(DirectX函数通过虚表调用,修改虚表实现Hook)VirtaulTable = *(DWORD64**)g_pSwapChain; // 虚函数表是交换链的第一个成员(偏移0x0)MyPresent = (Present)VirtaulTable[8]; // 保存原始Present函数(虚表中索引8是Present)// 修改虚函数表的内存保护属性(默认只读,需改为可写才能修改函数地址)DWORD a; // 用于接收旧的保护属性VirtualProtect(VirtaulTable, 1, PAGE_EXECUTE_READWRITE, &a); // 改为可读可写可执行VirtaulTable[8] = (DWORD64)Init; // 将Present函数地址替换为Init(第一次调用Present时执行初始化)// 同理Hook ResizeBuffers函数(虚表中索引13是ResizeBuffers)MyResizeBuffers = (ResizeBuffers)VirtaulTable[13]; // 保存原始ResizeBuffersVirtaulTable[13] = (DWORD64)VtResizeBuffers; // 替换为自定义的VtResizeBuffersreturn 0; // 线程执行完成
}