[Game] 2d游戏透视实现

chd透视实现

简介

这是一篇简单的d3d游戏内部透视的实现过程,实现的是2d游戏的透视效果,同理可以实现3d fps游戏的透视,绘制思路差不多,只是不是找坐标而是找矩阵,得到屏幕系和世界坐标系的变换矩阵,不多赘述。
本文实现了对未在屏幕内的物体的定位显示,帮助玩家判断地图位置和怪物位置。

基本思路

找到世界系下人物坐标和传送门坐标,两差值即可得到传送门相对人物坐标系的坐标,再找到屏幕坐标系下人物坐标,即可利用相对坐标绘制目标位置。

绘制上,利用inline hook,hook d3d的endscene函数,跳转到坐标内存读取的函数,绘制结果,最后恢复hook。

此外,虽然可以不hook,不inject就可以在全局上开一个新进程进行绘制,但是游戏绘制的窗口在所有窗口的最前面,会挡着其他程序的显示,所以后面就用inline hook改进成了内部绘制。

找坐标

世界系下的坐标可以利用小地图的数值进行查找,浮点数,根据CE的访问地址可以一步一步得到基址。

分析得到:

世界系下,

传送门x = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC]

传送门y = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC+ 04]

自己x = [[[[LaTaleClient.exe+565254] + 7C] + F4] + 08]

自己y = [[[[LaTaleClient.exe+565254] + 7C] + F4] + 08 + 04]

屏幕系下的坐标可以利用QQ截图功能大致估计一个结果,浮点搜索即可:

屏幕系下,

传送门x = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC]

传送门y = [[[[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + B8] + 244] +BC + 04]

自己x = [[[[LaTaleClient.exe+565254] + 7C] + 104] + 32C]

自己y = [[[[LaTaleClient.exe+565254] + 7C] + 104] + 32C + 04]

此外,可以用类似方法,在怪物走到屏幕边缘的时候,得到地图内怪物的坐标:

世界系下,

怪物x = [[[[[LaTaleClient.exe+565254] + 24] - 4 * i] + 68] + 08]

怪物y = [[[[[LaTaleClient.exe+565254] + 34] - 4 * i] + 4C] + 08 + 04]

怪物标志位 = [[[[[LaTaleClient.exe+565254] + 24] - 4 * i] + 68] + 04 + 18]

通过怪物标志位判断是否是怪物,为0表示非怪物单位。

绘制

拿到所有坐标基址后,考虑如何绘制。

一个简单的思路是新起一个单独的进程,创建一个凌驾于游戏窗口之上的透明窗体,在透明窗体内进行绘制,缺点是该窗体会在其他窗体上方,会遮挡其他窗体,优点就是写起来特别简单。。

最开始是实现了这一版的,后来觉得实在不妥,就写了dll版的内部绘制。

主要思路是,hook d3d的endscene函数,执行绘制函数,恢复之。

HRESULT __stdcall GUI::EndScene_(IDirect3DDevice9* direct3ddevice9) {
  static bool is_first_call_ = true;
  if (is_first_call_) {
    is_first_call_ = false;
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGui::StyleColorsDark();
    ImGuiIO& io = ImGui::GetIO();
    (void)io;
    io.IniFilename = nullptr;
    io.LogFilename = nullptr;
    io.Fonts->AddFontFromFileTTF(
        "C:/Users/Administrator/AppData/Local/Microsoft/Windows/Fonts/"
        "1640582872194027.otf",
        14.0f, nullptr, io.Fonts->GetGlyphRangesChineseFull());
    ImGui_ImplWin32_Init(FindWindow(NULL, "LaTale Client"));
    ImGui_ImplDX9_Init(direct3ddevice9);

    prev_func_ = (WNDPROC)SetWindowLongA(FindWindow(NULL, "LaTale Client"),
                                         GWL_WNDPROC, (ULL)ProcFunc);
  }
  hook_Endcene_->Restore();

  ImGui_ImplDX9_NewFrame();
  ImGui_ImplWin32_NewFrame();
  ImGui::NewFrame();
  ImGui::Begin(u8"彩虹ar", nullptr,
               ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoTitleBar |
                   ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoCollapse |
                   ImGuiWindowFlags_NoMove);
  ImGui::SetWindowPos(ImVec2(0, 0), ImGuiCond_Always);
  ImGui::Text(u8"彩虹ar");
  ImGui::Checkbox(u8"传送门透视", &EnableShow);
  ImGui::End();
  auto draw_list_ptr = ImGui::GetBackgroundDrawList();
  gamer_->SetDrawList(draw_list_ptr);

  if (EnableShow) {
    gamer_->Run();
  }
  ImGui::EndFrame();
  ImGui::Render();

  ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());

  HRESULT result = direct3ddevice9->EndScene();
  hook_Endcene_->Modify();

  return result;
}

Hook

之所以可以hook d3d的endscene函数,是因为IDirect3DDevice9类的函数是虚函数,因此一定会在类对象的第一个字节维护一个虚函数表的地址,我们获取虚函数表,根据虚函数顺序即可得到该函数在表里的位置,从而得到函数地址。拿到函数地址之后,将函数的前几个字节改为jmp指令,跳转到我们的内存读取和绘图的函数,然后跳转回去执行原来的函数。

例如,IDirect3DDevice9类的虚函数有下列顺序,因此如果要hook Reset函数就可以修改虚函数表里下标为16的函数指令。

我们hook修改的指令长度是5个字节,因为是32位的游戏,地址长度是4字节,指令占1字节。我们将函数的前5个字节修改为jmp xxx,及无条件跳转到xxx地址,jmp的参数是地址的offset,所以后四个字节对应的数据就是:

offset = target_address - ori_address - 5

code中汇编的jmp对应的字节码为:\xe9,故这么写。
短跳转(Short Jmp,只能跳转到256字节的范围内),对应机器码:EB
近跳转(Near Jmp,可跳至同一段范围内的地址),对应机器码:E9
远跳转(Far Jmp,可跳至任意地址),对应机器码: EA

此外,修改指令前需要先修改内存位置的读写属性,确保可以修改。对应VirtualProtect。

hook.h

class Hooker {
  using Ptr = std::shared_ptr<Hooker>;
  static const int INST_LEN = 5;

 private:
  u_char inst_ori_[INST_LEN];
  u_char inst_tgt_[INST_LEN];
  ULL addr_ori_ = 0;
  ULL addr_tgt_ = 0;

  DWORD ModAttr(const ULL& addr, DWORD attr = PAGE_EXECUTE_READWRITE);

 public:
  Hooker() = default;
  ~Hooker() = default;
  virtual Ptr Create();
  virtual bool Init(const ULL& addr_ori, const ULL& addr_tgt);
  virtual bool Modify();
  virtual bool Restore();
};

hook.cpp

#include "hook.h"
Hooker::Ptr Hooker::Create() { return std::make_shared<Hooker>(); }

bool Hooker::Init(const ULL& addr_ori, const ULL& addr_tgt) {
  addr_ori_ = addr_ori;
  addr_tgt_ = addr_tgt;

  inst_tgt_[0] = '\xe9';
  int offset = addr_tgt_ - (addr_ori_ + INST_LEN);

  memcpy(&inst_tgt_[1], &offset, INST_LEN - 1);  // byte copy
  DWORD attr = ModAttr(addr_ori_);
  memcpy(inst_ori_, reinterpret_cast<void*>(addr_ori_), INST_LEN);
  return ModAttr(addr_ori_, attr);
}

DWORD Hooker::ModAttr(const ULL& addr, DWORD attr) {
  DWORD old_attr;
  VirtualProtect(reinterpret_cast<void*>(addr), INST_LEN, attr, &old_attr);
  return old_attr;
}

bool Hooker::Modify() {
  DWORD attr = ModAttr(addr_ori_);
  memcpy(reinterpret_cast<void*>(addr_ori_), inst_tgt_, INST_LEN);
  ModAttr(addr_ori_, attr);
  return true;
}
bool Hooker::Restore() {
  DWORD attributes = ModAttr(addr_ori_);
  memcpy(reinterpret_cast<void*>(addr_ori_), inst_ori_, INST_LEN);
  ModAttr(addr_ori_, attributes);
  return true;
}

因此hook这里可以简单实现为:

ULL* direct3d9_table = (ULL*)*(ULL*)i_direct3d9;
ULL* direct3ddevice9_table = (ULL*)*(ULL*)i_direct3ddevice9;
hook_Reset_->Init(direct3ddevice9_table[16], ULL(Reset_));
hook_Endcene_->Init(direct3ddevice9_table[42], ULL(EndScene_));
hook_Reset_->Modify();
hook_Endcene_->Modify();

最后,inject可以参考https://bbs.pediy.com/thread-269910.htm#msg_header_h1_6,实现一个简单的注入程序。

效果

代码

Github:https://github.com/aoru45/latale_location_draw

posted @ 2023-01-02 22:33  aoru45  阅读(682)  评论(0编辑  收藏  举报