杀手5逆向分析(三)

杀手5逆向分析(三)
bahadir这一期主要分析对NPC造成伤害函数以及后座力函数。使用的工具有IDA Pro和Cheat Engine,还有利用Class Informer插件来简化工作量。
一,后座力函数的分析
1.定位函数
每次找指定的函数之前都建议用 Class Informer 插件来进行关键字搜索。比如我们这里需要查找跟后座力相关的类,那么我们就搜索关键字 Recoil 便能搜寻到相关的类:
很幸运地只搜到了叫 ZHM5WeaponRecoil 的类,看名称就知道是跟武器后座力有关的。双击打开可以看到虚函数表,每一个虚函数点进去找到最长的那一个,F5 生成 C++ 伪代码之后就可以分析了。
2.函数伪代码大致分析
可以发现这个函数是通过 __thiscall 方式调用的,接受两个参数一个是this指针(指向武器对象),另一个是浮点数(可能与射击时间或强度有关)。把此函数命名为 weaponRecoil :
1 | void __thiscall weaponRecoil(int this, float a2); |
现在通过 Cheat Engine 的动态分析可以确定的是 this+0x38 / 0x3C 分别是 X/Y轴当前视角的偏移值。第一个判断就涉及这两个参数,要是判断为真就会让视角不进行任何修改。
1 | if ( 1.0 == *(float *)(this + 0x24) && 1.0 == *(float *)(this + 0x14) )// if true no recoil |
this + 0x24 和 this + 0x14 这两个浮点数为1的情况下会执行重置视角偏移会变成无后座力的效果,在Cheat Engine 中删掉这个判断的话的确变成了无后座力(视角不会发生任何抖动)。那这里的两个参数可能是后座力恢复进度和后座力控制系数之类的参数。
要是有后座力效果,那就会执行else的内容。一开始就从输入参数当中提取某个值进行了计算并累积了这个值,可以称之为当前累计后座力。
1 | a2 = (double)*(__int64 *)(LODWORD(a2) + 0x18) / 1048576.0; |
下面的判断可以推断出 this + 0xC 为后座力的阈值,只有超过这个阈值之后才会有后座力效果:
1 | if ( *(float *)(this + 0xC) <= v5 ) // 检查是否超过阈值 |
3.函数算法分析
下面就是一系列详细的数学计算过程了,以下是大致分析没有进行动态分析验证,以供参考:
1 | else |
想要实现无后座力只需要把最开始的两个跳转指令Nop掉就行了:
1 | 0095ECFD jp short loc_95ED1F |
4.游戏后座力机制
- 后座力一般会影响玩家的X,Y视角(上下,左右)
- 后座力通常会随着时间进行恢复
- 连续射击情况下会累积后座力大小
- 后座力通常有阈值和最大限制值
二,造成伤害函数的分析
1.定位函数
在游戏中收到NPC伤害后执行的函数跟玩家对NPC造成伤害执行的函数是不同的。在部分游戏中只要是造成伤害都会使用同一个处理造成伤害的函数来处理,但是在杀手5中都是单独处理的。为了找到给对方造成伤害的函数我还是通过 Class Informer 插件进行关键字 Damage 的搜索,但是搜出来了4个:
ZHM5SplashDamageEntity :可能负责范围伤害(如爆炸)的传播与计算。
ZHitmanDamageOverTime :可能处理持续性伤害(如中毒/燃烧)
ZHM5ForwardDamageToHitmanEntity :可能是伤害事件转发器,将外部伤害传递给Hitman实体,非计算核心
ZHM5DamageCounterEntity :可能是伤害统计计数器(如成就系统),记录而非计算
这个几个类的虚函数每一个都点进去一个个查看进行动态调试没有找到关于玩家对NPC造成伤害的具体函数。因此改变了方向,跟搜索玩家生命值函数的方法一样。我们已经知道生命值是浮点数计算的,那么伤害值也应该是浮点数,还有最大生命值为100。
通过一下方法找到NPC生命值参数:
- 在游戏中找到伤害比较低的武器
- 找一个固定的满生命值的NPC后通过Cheat Engine搜索100(浮点数)
- 对着NPC的腿部打一枪(为了让造成的伤害最小化防止NPC死亡)
- 搜索类型选择 减少的数值 继续搜索
- 对其他NPC造成伤害后搜索类型选择 未变化的值(为了减少搜索结果)
- 重复3 - 6 步骤,在保证NPC不死亡的情况下找到NPC的血量值
等找到了NPC的血量之后锁定此值并锁定,可以开很多枪进行验证是否正确。之后通过 Cheat Engine 的 Find out what writes to this address 功能找到相应的汇编代码。
2.函数伪代码分析
将此汇编代码的地址复制到 IDA 当中,可以发现这个函数是通过 __thiscall 方式调用的,接受两个参数一个是this指针(指向NPC实体对象),另一个是浮点数(玩家对NPC造成的伤害值)。把此函数命名为 applyDamage :
1 | void __thiscall applyDamage(DWORD this, float damage); |
第一行代码就是对生命值变量的修改,根据上下文可以知道 this + 0xA8 为NPC当前生命值,this + 0xA4 可能是NPC的最大生命值:
1 | enemy_Health = damage + *(float *)(this + 0xA8); |
新生命值 = 当前生命值 + 伤害值(应当为负),通过动态分析可以证实 damage 变量为负的浮点数。这样加上一个负值实际上是在减少生命值。
紧接着是保存NPC的最大生命值到临时变量后更新NPC目前的生命值:
1 | v2 = *(float *)(this + 0xA4); |
后续就是一个 if 判断:
- 最大生命值大于0(NPC活着)
- 新计算出来的生命值要小于最大生命值
1 | if ( v2 > 0.0 && *(float *)(this + 0xA4) > (double)enemy_Health ) |
如果满足以上两个条件的话,把当前生命值设为最大生命值。这就变得有点奇怪了,因为这个函数是计算玩家对NPC造成的伤害函数而这个判断是让NPC处于无敌状态的判断。为了解释这个异常行为通过动态分析查看 this + 0xA4 的值,发现这里的最大生命值一直都是显示为0,所以说此判断不会被正常执行。
我根据上面的判断条件对 this + 0xA4 的值进行修改并且让他大于当前生命值就能发现NPC变成了无敌状态。可以推断这里的判断可能是为了给某些NPC加上无敌效果(比如在过场动画中),或者是某种”第一次受伤”触发器(第一次受到伤害时恢复为满血),也有可能是开发者为了调试而留下的调试代码。
最后一个判断是个简单的检测生命值是否低于0,如果是则设置为0。这样就防止生命值为负,确保NPC死时生命值正好为0而不是负的:
1 | if ( *(float *)(this + 0xA8) <= 0.0 ) |
3.实际效果分析
在游戏当中,这个函数的行为是:
- 玩家攻击NPC后传入NPC对象和伤害值
- NPC的生命值减少
- 如果NPC的生命值为0或以下,NPC死亡并把生命值设为0
- 在特殊情况下,可能会使NPC无敌
4.函数完整代码
1 | void __thiscall applyDamage(DWORD this, float damage) |
因为逆向工程大部分都是搞猜测验证来进行的,所以文章中有错误猜测或错误分析的部分请在评论区纠正。有任何私人问题可以联系我的私人邮箱,邮箱地址在首页。谢谢观看!