吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 8535|回复: 56
收起左侧

[游戏安全] 通杀爆改 Unity FPS 游戏系列-第三章:il2cpp mono 差异

  [复制链接]
lyl610abc 发表于 2023-9-18 20:04
本帖最后由 lyl610abc 于 2024-12-22 22:02 编辑

索引

通杀爆改 Unity FPS 游戏系列-序章

通杀爆改 Unity FPS 游戏系列-第一章

通杀爆改 Unity FPS 游戏系列-第二章

通杀爆改 Unity FPS 游戏系列-第四章

本章内容

本篇先给出 mono 和 il2cpp 的一些差异 (理论)

然后再以 windows mono 包为例实现功能(实际):

  • 全屏改血

  • 全屏秒杀

最后对比第二章中 il2cpp 包,再次说明 il2cpp 和 mono 差异(理论结合实际)


理论

mono 和 il2cpp 最大的差异点在于它们的编译方式

编译方式

编译方式 说明
il2cpp AOT Ahead Of Time 提前编译
mono JIT Just In Time 即时编译

对不熟悉的小伙伴们看到这里可能依旧一头雾水,这里再引入一个词:IL

IL

IL 全称:Intermediate Language ,中间语言

所谓的中间,自然意味着只是个临时的过渡的内容,并不是我们真正去执行的代码

这里就不贴过于官方的定义了,谈谈我个人的理解

作用

IL 的作用就是为了实现跨平台

不同平台对应的底层代码(汇编)并不相同

下面给出通常平台下对应的汇编,windows 也有 arm 版本,也有模拟器运行的安卓是 x86,这里说的是通常情况

重点在于说明不同平台的底层代码并不相同,如果想要实现跨平台需要编译四个对应平台的可执行文件(汇编代码)

32位汇编 64位汇编 对应可执行文件(例)
Windows x86 x64 UnityPlayer.dll (32 位 + 64位)
Android armeabi-v7a arm64-v8a libunity.so  (32位 + 64位)

联系

编译的路径是:

我们编写游戏的 C# 代码 → IL → 对应平台可执行的汇编代码

而 AOT 和 JIT 的区别就在于

AOT:提前编译,IL 只是在编译的时候中转一下,最终编译出来的游戏包就是对应平台的可执行文件,不包含 IL

JIT:即时编译,IL 是包含在最终生成的游戏包的,游戏在运行过程中动态地将 IL 编译成对应平台的可执行代码


再来个例子类比下:

同一道菜,比如西红柿炒鸡蛋,不同人(不同平台)的口味不同(汇编代码),有的人喜欢甜口(x86),有的人喜欢咸口(armeabi-v7a)

在上菜前,直接把盐或者糖放了,然后再问客人要吃什么口味再上对应的菜,叫 AOT ,提前编译

在上菜后,问客人要吃什么口味,然后再放对应的盐或者糖,叫做 JIT ,即时编译

IL 是还没有放盐和放糖的西红柿炒蛋,加盐和加糖相当于是编译成对应客人想要的菜


再加一个编译优化的概念

用上面的例子来说,就是放盐和放糖只是让菜符合客人的口味,而放多少,放哪里 就算是编译优化了

这里先点一下,后面会具体说明


实际

全屏改血

功能

依旧按照第二章的方式来操作

先下双击 fps.AI 下的 Unity.FPS.AI.EnemyController.Update 函数,跳转到函数头部,然后快捷键下断点(这部分在第一章和详细演示过,这里稍微简略些)

image-20230914095713979

查看寄存器和堆栈数据

image-20230914100208176


根据函数原型可以推断出堆栈内容的含义:(忘记如何推断的可以回顾 通杀爆改 Unity FPS 游戏系列-第一章 中的改血部分,说明了函数原型和对应堆栈的联系)

内容 含义
ESP 1EABF33B 完成调用后要返回的内存地址
ESP+4 1A1DED00 我们想要的指向 EnemyController 的内存地址

把得到的地址丢进 Structure dissect 验证 ESP+4 对应的地址就是 EnemyController

image-20230914101318305


整理下前面得到的信息:

同时记下要改的 CurrentHeatlh 对应的偏移:

内容
ESP 返回地址
ESP+4 EnemyController
EnemyController+0060 m_Health
m_Health+20 CurrentHealth

开始 Auto assemble 写脚本,关于脚本部分在第二章已经说得比较详细了,这里不再赘述

直接给出脚本代码:

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
label(returnhere)
label(originalcode)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here
//随便找个寄存器保存,因为中途需要借助寄存器来赋值,这里也可以改成 ebx ecx ...
push eax
//注意这个地方,原本 EnemyController 的地址应该是 ESP+4 但是因为我们保存了个寄存器,导致位置变化,所以需要再加 4
//这里就相当于是 eax = EnemyController 
mov eax,[esp+4+4]
//这里就相当于是 eax = EnemyController+60 = m_Health
mov eax,[eax+60]
//这里就相当于是 [eax+20]= [m_Health+20] = [CurrentHealth] = 0
mov [eax+20],(float)0
//用完以后要还原回去,保存的是什么寄存器就还原什么寄存器
pop eax
originalcode:
push ebp
mov ebp,esp
push edi
sub esp,74

exit:
jmp returnhere

Unity.FPS.AI.EnemyController:Update:
jmp newmem
nop 2
returnhere:

[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem)
Unity.FPS.AI.EnemyController:Update:
db 55 8B EC 57 83 EC 74
//push ebp
//mov ebp,esp
//push edi
//sub esp,74

对比 il2cpp

使用文本比较工具 Beyond.Compare 对比 il2cpp 的修改代码和 mono 的修改代码

image-20230914105325275


可以看到有 3 个区别:

mono il2cpp 差异说明 补充
mov eax,[eax+60] mov eax,[eax+d8] 结构体偏移有所区别 编译差异
push edi<br/>sub esp,74 push -01 原本要执行的汇编逻辑有所区别 编译差异
Unity.FPS.AI.EnemyController:Update "GameAssembly.dll"+1F4800 要 HOOK 的地址有所差别 JIT 和 AOT 导致

这里重点说明一下要 HOOK 地址的差别

内容 说明 地址
mono Unity.FPS.AI.EnemyController:Update 函数名 JIT 即时编译,每次重开游戏都会变动
il2cpp "GameAssembly.dll"+1F4800 模块名 + 偏移量 AOT 提前编译,固定不变

这里不难看出,无论是 mono 还是 il2cpp ,我们的分析思路都是适用的,不存在所谓的 mono 太旧太过时,il2cpp 较新而有差异分析修改

差异最大的点,反而在 CE 修改器为我们生成的修改模板上,我们自己写的修改逻辑并无差异

PS:如果要我们自己写代码实现这个 HOOK ,mono 确实会比较麻烦,毕竟 mono 的地址是会变动的,得 JIT 后才能拿到


全屏秒杀

死亡函数

在第二章里特地留了个差异点,就是全屏秒杀的死亡函数  fps.Game → Unity.FPS.Game.Health → HandleDeath

只能在 mono 的情况下才会生效,在 il2cpp 时根本不会走到 HandleDeath 这个函数

先使用这个死亡函数实现全屏秒杀,再对比说明原因

现在 .NET Info 里找到 HandleDeath:

image-20230918134527570


双击跳转到对应的汇编,按 F5 下断点,然后打敌人一枪,发现命中后会触发断点:

可以推测出该函数的作用:在敌人扣血时,判断血量是否小于等于 0 ,如果是则执行死亡逻辑

image-20230918134952220


Health死亡

确定了 HandleDeath 的作用后,就可以在全屏改血的基础上,再调用它实现全屏秒杀

通杀爆改 Unity FPS 游戏系列-第二章中已经说明了如何在汇编中调用函数

要注意的点有 2 个:

  • 函数的参数
  • 堆栈平衡

先看函数原型:

System.Void HandleDeath()

是最简单的函数,没有参数也没有返回值

但在第二章中也说过:在汇编中相比函数原型,会多出一个参数:指向该类自身的指针(对 HandleDeath 来说就是 Health)

因此可以先一步给出伪代码:

//给参数
push m_Health
//调用死亡函数
call Unity.FPS.Game.Health:HandleDeath
//堆栈外平衡
add esp,4

有了伪代码,就可以在全屏改血的基础上,加上这部分的逻辑

newmem: //this is allocated memory, you have read,write,execute access
//place your code here
//随便找个寄存器保存,因为中途需要借助寄存器来赋值,这里也可以改成 ebx ecx ...
push eax
//注意这个地方,原本 EnemyController 的地址应该是 ESP+4 但是因为我们保存了个寄存器,导致位置变化,所以需要再加 4
//这里就相当于是 eax = EnemyController
mov eax,[esp+4+4]
//这里就相当于是 eax = EnemyController+60 = m_Health
mov eax,[eax+60]
//这里就相当于是 [eax+20]= [m_Health+20] = [CurrentHealth] = 0
mov [eax+20],(float)0

//-------------从这里开始是调用死亡函数的逻辑 -----------------
//改完血以后准备调用死亡函数,再拿到 EnemyController
mov eax,[esp+4+4]
//通过 EnemyController 再拿到 m_Health
mov eax,[eax+60]
push eax
call Unity.FPS.Game.Health:HandleDeath
add esp,4
//-------------从这里结束了调用死亡函数的逻辑 -----------------

//用完以后要还原回去,保存的是什么寄存器就还原什么寄存器
pop eax
originalcode:

最后附上完整的修改逻辑

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
label(returnhere)
label(originalcode)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here
//随便找个寄存器保存,因为中途需要借助寄存器来赋值,这里也可以改成 ebx ecx ...
push eax
//注意这个地方,原本 EnemyController 的地址应该是 ESP+4 但是因为我们保存了个寄存器,导致位置变化,所以需要再加 4
//这里就相当于是 eax = EnemyController
mov eax,[esp+4+4]
//这里就相当于是 eax = EnemyController+60 = m_Health
mov eax,[eax+60]
//这里就相当于是 [eax+20]= [m_Health+20] = [CurrentHealth] = 0
mov [eax+20],(float)0

//-------------从这里开始是调用死亡函数的逻辑 -----------------
//改完血以后准备调用死亡函数,再拿到 EnemyController
mov eax,[esp+4+4]
//通过 EnemyController 再拿到 m_Health
mov eax,[eax+60]
push eax
call Unity.FPS.Game.Health:HandleDeath
add esp,4
//-------------从这里结束了调用死亡函数的逻辑 -----------------

//用完以后要还原回去,保存的是什么寄存器就还原什么寄存器
pop eax
originalcode:
push ebp
mov ebp,esp
push edi
sub esp,74

exit:
jmp returnhere

Unity.FPS.AI.EnemyController:Update:
jmp newmem
nop 2
returnhere:

[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem)
Unity.FPS.AI.EnemyController:Update:
db 55 8B EC 57 83 EC 74
//push ebp
//mov ebp,esp
//push edi
//sub esp,74

OnDie死亡

il2cpp 版本中因为 Health 的 HandleDeath 不会被调用到,所以使用的是  fps.Game.dll → Unity.FPS.AI.EnemyController → OnDie

这个函数在 mono 版本同样是有效的,可以直接调用实现全屏秒杀的效果

实际的分析和逻辑和 il2cpp 别无二致,这里为了精简篇幅,直接贴出对应的代码:

[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
label(returnhere)
label(originalcode)
label(exit)

newmem: //this is allocated memory, you have read,write,execute access
//place your code here
//随便找个寄存器保存,因为中途需要借助寄存器来赋值,这里也可以改成 ebx ecx ...
push eax
//注意这个地方,原本 EnemyController 的地址应该是 ESP+4 但是因为我们保存了个寄存器,导致位置变化,所以需要再加 4
//这里就相当于是 eax = EnemyController
mov eax,[esp+4+4]
//参数是 EnemyController
push eax
//调用 OnDie 函数
call Unity.FPS.AI.EnemyController:OnDie
//堆栈外平衡
add esp,4
//用完以后要还原回去,保存的是什么寄存器就还原什么寄存器
pop eax
originalcode:
push ebp
mov ebp,esp
push edi
sub esp,74

exit:
jmp returnhere

Unity.FPS.AI.EnemyController:Update:
jmp newmem
nop 2
returnhere:

[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem)
Unity.FPS.AI.EnemyController:Update:
db 55 8B EC 57 83 EC 74
//push ebp
//mov ebp,esp
//push edi
//sub esp,74

不难发现,和 il2cpp 版本的区别只有 call 调用函数的区别

代码 说明
il2cpp call GameAssembly.dll+1F2D50 AOT 固定的地址:模块名+偏移量
mono call Unity.FPS.AI.EnemyController:OnDie JIT 不固定的地址:直接使用函数名

分析

这里的分析主要是针对 il2cpp 和 mono 中 Health.HandleDeath 这个死亡函数的调用差异

首先要明确的是:无论是 il2cpp 和 mono,它们的源代码是一样的

下面先给出源代码,然后再结合实际的执行逻辑分析

源代码

下面的源代码来自于 Health.cs ,也就是 Health 这个类中,注释部分为个人补充

//受伤函数,当要扣血时会调用到,参数一个是受到的伤害量,还有一个是伤害源
public void TakeDamage(float damage, GameObject damageSource) 
{
    //是否开启全局无敌,无敌则屏蔽所有受伤判定
        if (GlobalInvincible)
                return;
        float healthBefore = CurrentHealth;
        CurrentHealth -= damage;
        CurrentHealth = Mathf.Clamp(CurrentHealth, 0f, MaxHealth);
        // call OnDamage action
        float trueDamageAmount = healthBefore - CurrentHealth;
        if (trueDamageAmount > 0f) 
        {
                OnDamaged?.Invoke(trueDamageAmount, damageSource);
        }
    //这里为调用死亡函数
        HandleDeath();
}

//这个函数是当人物从高处坠落时摔死用到的,因为有调用到 HandleDeath 所以贴进来
//这次分析和这个函数无关
public void Kill() 
{
        CurrentHealth = 0f;
        // call OnDamage action
        OnDamaged?.Invoke(MaxHealth, null);
        HandleDeath();
}

//死亡函数
void HandleDeath() 
{
    //已经死了就直接返回
        if (m_IsDead)
                return;
        // call OnDie action
    //判定当前血量是否小于等于 0 ,小于等于 0 则调用 OnDie
        if (CurrentHealth <= 0f) 
        {
                m_IsDead = true;
                OnDie?.Invoke();
        }
}

实际执行逻辑

看了前面的源代码,HandleDeath 确实是在每次扣血时就会调用到

但是符合预期的只有 mono 版 ,il2cpp 版却并没有调用到

因此需要进一步分析 il2cpp 实际的代码逻辑

以 TakeDamage 为突破口,查看其实际的逻辑(注意这里分析的是 il2cpp 包)

image-20230918165520821


双击跳转到对应的汇编代码段:

记下此时的偏移量 1F9940

image-20230918170109998


然后使用 IDA Pro 分析这部分逻辑的伪代码

限于篇幅,这里略去操作部分,关于 IDA Pro 分析 Unity 游戏留到后面的篇章

直接给出解析到的伪代码:

image-20230918170544099


int __cdecl sub_101F9940(int a1, float a2)
{
  int result; // eax
  float v4; // xmm0_4
  float v5; // xmm2_4
  int *savedregs; // [esp+0h] [ebp+0h]
  int savedregsb; // [esp+0h] [ebp+0h]
  int savedregsc; // [esp+0h] [ebp+0h]
  float retaddr; // [esp+4h] [ebp+4h]

  if ( !byte_1114E2F7 )
  {
    savedregs = &dword_110E8900;
    ((void (*)(void))sub_10164120)();
    byte_1114E2F7 = 1;
  }
  result = *(_DWORD *)(dword_110E8900 + 92);
  if ( !*(_BYTE *)result )
  {
    v4 = *(float *)(a1 + 32) - a2;
    if ( v4 < 0.0 )
    {
      v4 = 0.0;
    }
    else if ( v4 > *(float *)(a1 + 12) )
    {
      v4 = *(float *)(a1 + 12);
    }
    v5 = *(float *)(a1 + 32) - v4;
    *(float *)(a1 + 32) = v4;
    if ( v5 > 0.0 )
    {
      result = *(_DWORD *)(a1 + 20);
      if ( result )
      {
        retaddr = v5;
        savedregsb = *(_DWORD *)(result + 32);
        result = (*(int (**)(void))(result + 12))();
      }
    }
    if ( !*(_BYTE *)(a1 + 36) && *(float *)(a1 + 32) <= 0.0 )
    {
      result = *(_DWORD *)(a1 + 28);
      *(_BYTE *)(a1 + 36) = 1;
      if ( result )
      {
        savedregsc = *(_DWORD *)(result + 32);
        result = (*(int (**)(void))(result + 12))();
      }
    }
  }
  return result;
}

伪代码里有个明显且关键的地方:

if ( !*(_BYTE *)(a1 + 36) && *(float *)(a1 + 32) <= 0.0 ){
    .....
}

这里可以对应到:

void HandleDeath() {
    ......
    if (CurrentHealth <= 0f) 
        {
                ......
        }
}

结论

il2cpp 包直接将 HandleDeath 原本调用函数的逻辑去掉,换成直接把 HandleDeath 函数里的内容摁过来,所以不再去调用原本的函数

这个地方就要提到前面说的点:编译优化

可以发现 HandleDeath 这个函数本身的代码量很少,并没有多少

函数调用是有消耗的,因为在调用函数前,需要保存上下文信息,调用完后再还原

编译优化,就把原有的函数调用替换成了函数内的逻辑,直接执行,少了一层函数调用

因此 il2cpp 包并不会调用到 HandleDeath


补充

补充下 il2cpp 和 mono 包的目录结构 对比

限于篇幅,这里直接说结论,有兴趣的小伙伴可以自己拿 Demo 的 il2cpp 和 mono 包对比验证

游戏逻辑文件 说明 编译方式
il2cpp GameAssembly.dll 编译成对应平台的可执行文件(动态链接库) AOT
mono FpsDemo_By_lyl610abc_Data\Managed*.dll 编译成 IL 文件,运行过程中会动态再编译 JIT

总结

  • il2cpp 和 mono 的核心差异在于编译方式:AOT 和 JIT
  • il2cpp 和 mono 都会生成 IL 中间代码,区别在于一个直接在打包阶段再编译成对应平台可执行文件(AOT),另一个则是在运行阶段才去编译(JIT)
  • 无论是 il2cpp 还是 mono , 都能够通过 CE 修改器的 mono 功能分析结构,查找实例等
  • il2cpp 定位函数是以 模块名 + 偏移量 (GameAssembly.dll + offset),mono 定位函数则是直接以函数名 (如 Unity.FPS.Game.Health:HandleDeath)
  • il2cpp 由于编译优化可能会产生通过 mono 找到的函数不被调用的情况

本章作为番外,算是稍微解答了一些关于 il2cpp 和 mono 的异同点,希望能对大家有所帮助

不同的编译方式对于分析的影响其实并没有有些人想象的那么多,只要能够抓住本质,基本上是通用的

本章还是留下了一些坑没填:

  • CE 修改器的 mono 功能是怎么实现的
  • 不依赖 CE 修改器自实现修改
  • IDA Pro 分析游戏逻辑

-------------------ヾ(•ω•`)o----------------------------

预告

坑没填完没关系,再挖些坑债多不压身,预告下后面的内容

通过 mono 功能好像没有办法找到人物的坐标,如何实现全图瞬移?

FPS 游戏最常用的功能透视自瞄在 Unity 上如何实现?

附件

最后附上 mono 版的 CE 修改器脚本:点我下载

image-20230918195431994

免费评分

参与人数 24威望 +1 吾爱币 +53 热心值 +23 收起 理由
泊舟 + 1 + 1 谢谢@Thanks!
Passerby + 2 + 1 谢谢@Thanks!
Marythore + 1 + 1 热心回复!
huangmo1 + 1 + 1 用心讨论,共获提升!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
ManaCola + 1 + 1 我很赞同!
悠扬Le逍遥 + 2 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
u_nuts + 1 + 1 用心讨论,共获提升!
skupcll + 1 + 1 用心讨论,共获提升!
gunxsword + 1 + 1 我很赞同!
BonnieRan + 1 + 1 谢谢@Thanks!
wailjf1004 + 1 我很赞同!
Chenda1 + 1 + 1 用心讨论,共获提升!
Atnil + 1 谢谢@Thanks!
vaycore + 1 + 1 谢谢@Thanks!
fcyueshi + 1 + 1 我很赞同!
Tonyha7 + 2 + 1 用心讨论,共获提升!
Bob5230 + 1 + 1 谢谢@Thanks!
allspark + 1 + 1 用心讨论,共获提升!
涛之雨 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
ryanu + 1 + 1 我很赞同!
debug_cat + 2 + 1 硬核
布拉格灬征战 + 1 + 1 我很赞同!
忆魂丶天雷 + 2 + 1 用心讨论,共获提升!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| lyl610abc 发表于 2023-9-18 20:37
hongshao987 发表于 2023-9-18 20:32
楼主搞个二十年前的单机游戏来演示一下吧,不然我等好像看天书。

要从序章开始看,包含了单机游戏的演示
冥界3大法王 发表于 2023-9-18 20:59
那些基础的东西还没整明白呢,直接看懂了感觉就跟被雷劈死一般难。
 楼主| lyl610abc 发表于 2024-6-1 18:33
Youame 发表于 2023-9-18 20:24
看的一脸萌~~
 楼主| lyl610abc 发表于 2023-9-18 20:30

需要结合前面的章节看~~
hongshao987 发表于 2023-9-18 20:32
楼主搞个二十年前的单机游戏来演示一下吧,不然我等好像看天书。
hanchao2021 发表于 2023-9-18 21:19
哈哈哈!认真学习!!!
艾莉希雅 发表于 2023-9-18 22:28
好诶,更新了。不过这章怎么没有正己相关的内容了。
debug_cat 发表于 2023-9-18 22:49
奇怪的知识增加了
张道陵 发表于 2023-9-18 23:23
技术研究很有乐趣
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-12-31 01:19

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表