cheat engine 处理 dotnet 平台的游戏
cheat engine 处理 dotnet 平台的游戏
特指不是使用 mono 运行时的游戏,而是使用通常 dotnet 运行时,常见的有 XNA、FNA

§定位方法、属性访问器

《Phoenix Force》这个游戏为例,以下脚本通过将 PhoenixForce.Source.GameScene.Player::Hit 重写为直接返回,实现了玩家无敌效果。

lua{$STRICT}
[ENABLE]
{$LUA}
local defined = 'define(cheat1_entry,0)';
if syntaxcheck then return defined end;
if LaunchDotNetInterface() == false then return defined end;
--local address = dotnet_findDotNetMethodAddress('PhoenixForce.Source.GameScene','Player','Hit','PhoenixForceXNA.exe');
local dataCollector = getDotNetDataCollector();
if dataCollector == nil then return defined end;
local domainList = dataCollector.enumDomains();
if #domainList < 1 then return defined end;
local domainHandle = 0;
for i1=1,#domainList,1 do
--这里对比的是可执行文件名,也就是 dotnet 的主域
  if domainList[i1]['Name'] == 'PhoenixForceXNA.exe' then
    domainHandle = domainList[i1]['DomainHandle'];
  end;
end;
if domainHandle == 0 then return defined end;
local moduleList = dataCollector.enumModuleList(domainHandle);
if #moduleList < 1 then return defined end;
local moduleHandle = 0;
local moduleId = 0;
for i1=1,#moduleList,1 do
--这里对比的是 assembly-namespace,只是 dotnet 默认恰好与可执行文件名一致
  if extractFileName(moduleList[i1]['Name']) == 'PhoenixForceXNA.exe' then
    moduleHandle = moduleList[i1]['ModuleHandle'];
    moduleId = i1;
  end;
end;
if moduleHandle == 0 then return defined end;
if moduleId == 0 then return defined end;
local classList = dataCollector.enumTypeDefs(moduleHandle);
if #classList < 1 then return defined end;
local classTypeDefToken = 0;
for i1=1,#classList,1 do
--这里对比的是 class-namespace
  if classList[i1]['Name'] == 'PhoenixForce.Source.GameScene.Player' then
    classTypeDefToken = classList[i1]['TypeDefToken'];
  end;
end;
if classTypeDefToken == 0 then return defined end;
local methodList = dataCollector.getTypeDefMethods(moduleHandle,classTypeDefToken);
if #methodList < 1 then return defined end;
local methodToken = 0;
for i1=1,#methodList,1 do
--这里对比的是 method-name
  if methodList[i1]['Name'] == 'Hit' then
    methodToken = methodList[i1]['MethodToken'];
  end;
end;
if methodToken == 0 then return defined end;
local methodEntryPoint = dotnet_getMethodEntryPoint(moduleId,methodToken);
if methodEntryPoint == 0 then return defined end;
return string.format('define(cheat1_entry,%x)',methodEntryPoint);

{$ASM}
registersymbol(cheat1_entry)

cheat1_entry:
  DB C3

[DISABLE]
cheat1_entry:
  DB 55

unregistersymbol(*)

《Panzer Paladin》这个游戏为例,以下脚本通过将 Paris.Game.Actor.Player::Hurt 的返回值修改为 false,实现了玩家无敌效果。

lua//Paris.Game.Actor.Player::Hurt
{$STRICT}
[ENABLE]
{$LUA}
local defined = [[
define(cheat2_entry,0)
define(cheat2_target1,0)
]];
if syntaxcheck then return defined end;
if LaunchDotNetInterface() == false then return defined end;
local dataCollector = getDotNetDataCollector();
if dataCollector == nil then return defined end;
local domainList = dataCollector.enumDomains();
if #domainList < 1 then return defined end;
local domainHandle = 0;
for i1=1,#domainList,1 do
--这里对比的是可执行文件名,也就是 dotnet 的主域
  if domainList[i1]['Name'] == 'PanzerPaladin.exe' then
    domainHandle = domainList[i1]['DomainHandle'];
  end;
end;
if domainHandle == 0 then return defined end;
local moduleList = dataCollector.enumModuleList(domainHandle);
if #moduleList < 1 then return defined end;
local moduleHandle = 0;
local moduleId = 0;
for i1=1,#moduleList,1 do
--这里对比的是 assembly-namespace,只是 dotnet 默认恰好与可执行文件名一致
  if extractFileName(moduleList[i1]['Name']) == 'PanzerPaladin.exe' then
    moduleHandle = moduleList[i1]['ModuleHandle'];
    moduleId = i1;
  end;
end;
if moduleHandle == 0 then return defined end;
if moduleId == 0 then return defined end;
local classList = dataCollector.enumTypeDefs(moduleHandle);
if #classList < 1 then return defined end;
local classTypeDefToken = 0;
for i1=1,#classList,1 do
--这里对比的是 class-namespace
  if classList[i1]['Name'] == 'Paris.Game.Actor.Player' then
    classTypeDefToken = classList[i1]['TypeDefToken'];
  end;
end;
if classTypeDefToken == 0 then return defined end;
local methodList = dataCollector.getTypeDefMethods(moduleHandle,classTypeDefToken);
if #methodList < 1 then return defined end;
local methodToken = 0;
for i1=1,#methodList,1 do
--这里对比的是 method-name
  if methodList[i1]['Name'] == 'Hurt' then
    methodToken = methodList[i1]['MethodToken'];
  end;
end;
if methodToken == 0 then return defined end;
local methodEntryPoint = dotnet_getMethodEntryPoint(moduleId,methodToken);
if methodEntryPoint == 0 then return defined end;
--这里多了一个步骤:获取另一个地址;因为 dotnet 每次 JIT 后的代码并不相同,所以生成的字节也不一样
--最常见的就是 JIT 顺序不同导致的 jmp 目标发生改变,可能导致从 jmp BYTE 变成 jmp QWORD
--这里是用 8D 65 F4 5B 5E 5F 5D C3 这一字节数组去找地址,memoryScanner 其实就是 CE 主界面上的搜索框
local memoryScanner = createMemScan();
memoryScanner.setOnlyOneResult(true);
--firstScan(搜索模式,搜索类型,浮点处理方式,搜索值1,搜索值2,起始地址,结束地址,内存标记,对齐标记,忘了,搜索值是否 16 进制字符串,忘了,忘了,忘了)
memoryScanner.firstScan(soExactValue,vtByteArray,rtTruncated,'8D 65 F4 5B 5E 5F 5D C3',nil,methodEntryPoint,methodEntryPoint+1024,'+X+W*C',fsmNotAligned,nil,true,true,false,false);
memoryScanner.waitTillDone();
local target1 = memoryScanner.Result;
memoryScanner.destroy();
if target1 == nil then return defined end;
return string.format([[
define(cheat2_entry,%x)
define(cheat2_target1,%x)
]],methodEntryPoint,target1);
{$ASM}
assert(cheat2_entry+09,33 C0 89 45 E8 89 45 EC)
registersymbol(cheat2_entry)

cheat2_entry+0B:
  jmp cheat2_target1
  nop

[DISABLE]
cheat2_entry+0B:
  DB 89 45 E8 89 45 EC

unregistersymbol(*)

§定位静态字段

待更新。

§定位类实例、字段

待更新;找到类实例就等于找到了非静态字段,此过程非常麻烦。

作者
ragnaroks
发布时间
2022-06-29
更新时间
2022-07-07
创作协议