Cracking 逃离鸭科夫
锁血
参考教程:https://www.bilibili.com/video/BV1vok1BFE9Z
这篇文章以一个具体案例为切入点,简单梳理游戏逆向中的一些基础概念,并走通一套相对完整的逆向分析与破解流程。
Disclaimer: 《逃离鸭科夫》是单机游戏,本文内容仅用于学习和研究参考。
先启动 Duckov,然后在主界面使用 Cheat Engine 附加到游戏进程。
在菜单中选择 Mono -> Activate mono features。
接着打开 Mono -> .Net Info。
弹出的窗口里会显示 .NET 相关的各种信息,从 Images、Classes 到具体的方法。每一个方法都会被映射到当前进程中的实际地址。
如上图所示,我们的目标是“锁血”,也就是让敌人无法对玩家单位造成伤害。因此可以先把目标方法锁定为 Health 类中的 Hurt 方法。由于这个类会被所有单位继承,所以不能简单粗暴地直接 patch;必须加上一层判断逻辑,只在目标是玩家时跳过伤害,而对其他敌方单位仍然保持正常行为。
双击目标方法,打开反汇编视图:
Health:Hurt - 55 - push rbp
Health:Hurt+1- 48 8B EC - mov rbp,rsp
Health:Hurt+4- 48 81 EC 90060000 - sub rsp,00000690 { 1680 }
Health:Hurt+b- 48 89 5D C8 - mov [rbp-38],rbx
Health:Hurt+f- 48 89 75 D0 - mov [rbp-30],rsi
Health:Hurt+13- 48 89 7D D8 - mov [rbp-28],rdi
Health:Hurt+17- 4C 89 65 E0 - mov [rbp-20],r12
Health:Hurt+1b- 4C 89 6D E8 - mov [rbp-18],r13
Health:Hurt+1f- 4C 89 75 F0 - mov [rbp-10],r14
Health:Hurt+23- 4C 89 7D F8 - mov [rbp-08],r15
函数起始地址是 194B2942670。
记下这个地址后,再用 IDA Pro 附加到进程里看伪代码。毕竟原始反汇编又长又杂,直接硬读体验并不好。
直接跳转到地址 194B2942670,然后反编译。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
__int64 __fastcall sub_194B2942670(__int64 a1, __int64 a2)
{
/* var decl fields omitted */
v67[0] = 0i64;
v67[1] = 0i64;
v68 = 0i64;
v69 = 0i64;
v59 = 0.0;
v70 = 0i64;
v71 = 0i64;
if ( (unsigned int)((__int64 (__fastcall *)(_QWORD, _QWORD))loc_194A87D3CE7)(unk_194FE20FF60, 0i64)
&& *(_BYTE *)(unk_194FE20FF60 + 97i64) )
{
return 0i64;
}
if ( *(_BYTE *)(a1 + 125) )
return 0i64;
if ( *(_BYTE *)(a1 + 116) )
return 0i64;
if ( (unsigned int)((__int64 (__fastcall *)(_QWORD, _QWORD))loc_194A87D3CE7)(*(_QWORD *)(a2 + 112), 0i64)
&& *(float *)(a2 + 104) > (double)((float (*)(void))unk_194B27520CE)() )
{
((void (__fastcall *)(__int64, _QWORD, _QWORD, _QWORD))unk_194B2944986)(
a1,
*(_QWORD *)(a2 + 112),
*(_QWORD *)(a2 + 56),
*(int *)(a2 + 100));
}
// Omitting following logic
其中,if ( *(_BYTE *)(a1 + 125) ) 对应的是 Health 类中的字段:
07d invincible System.Boolean
074 isDead System.Boolean
一旦命中这两个条件,后续就不会再对该单位继续造成伤害。
剩下的逻辑分析起来还是比较费劲,所以这里换用 DnSpy,直接去看 JIT 之前的 IL 层代码。
把 Escape from Duckov\Duckov_Data\Managed\TeamSoda.Duckov.Core.dll 直接拖进 DnSpy,然后找到 Health::Hurt() (见附录)
看到这里,基本已经接近源码级别的信息了。
重点看这一段:
1
2
3
4
if (!damageInfo.ignoreDifficulty && this.team == Teams.player)
{
damageInfo.damageValue *= LevelManager.Rule.DamageFactor_ToPlayer;
}
可以看出,它的判断逻辑是 this.team == Teams.player。换句话说,我们只需要读取 team 字段并据此做判断就够了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
// Token: 0x0200007A RID: 122
public enum Teams
{
// Token: 0x040003ED RID: 1005
player,
// Token: 0x040003EE RID: 1006
scav,
// Token: 0x040003EF RID: 1007
usec = 3,
// Token: 0x040003F0 RID: 1008
bear,
// Token: 0x040003F1 RID: 1009
middle,
// Token: 0x040003F2 RID: 1010
lab,
// Token: 0x040003F3 RID: 1011
all,
// Token: 0x040003F4 RID: 1012
wolf
}
枚举默认从 0 开始,因此 Teams::player 的值就是 0。
回到 CE 里继续查字段:
050 team Teams
也就是说,这个字段位于 0x50 偏移处。
接下来就是写 hook。常见做法有两个:
- 直接用 CE 写注入脚本。
- 用 Frida 做动态注入。
Frida那个其实更原始一点,需要自己去allocate memory,自己从头到尾把code injection的pipeline走完。所以我这里直接用CE做了。
CE Auto Assembler注入
思路很简单:在函数 prologue 之后注入 hook,并加入如下判断:
1
2
3
4
if is_player():
invincible=True # set invincible to True.
else:
continue # resume normal control flow
打开 Memory Viewer -> Tools -> Auto Assembler。
这里的目标是做 Code Injection,所以直接使用 Code Injection Template 就行,省事很多。
我这里选择在下面这条指令处注入:
1
Health:Hurt+2a - 48 89 95 A8F9FFFF - mov [rbp-00000658],rdx
注入最好在第一个basic block,重点在于必须确保此时 rcx 仍然指向 this 指针。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
alloc(newmem,2048,Health:Hurt+2a)
label(returnhere)
label(originalcode)
label(exit)
label(is_other)
newmem: //this is allocated memory, you have read,write,execute access
//place your code here
push rdx
mov edx, [rcx+50]
cmp edx, 0
jne is_other
// is player
mov byte ptr [rcx+7d], 1
is_other:
pop rdx
originalcode:
mov [rbp-00000658],rdx
exit:
jmp returnhere
Health:Hurt+2a:
jmp newmem
nop 2
returnhere:
无限子弹+full auto+no reload+no burst cooling+ignore duration
这里的目标:
- 无限子弹。具体而言定义为:在触发fire的时候,子弹数量不会减少。
- 全自动。将所有的武器全部换成全自动.
- No Burst Cooling.取消武器的过热冷却。代码里叫“burstCounter”。
- Ignore Duration.武器的耐久度将不会影响武器使用,具体而言是攻击。
- No Reload.没有reload time. Reload在切换新武器的时候还是会有的,因此这里也有点必要。
下面用DnSpy看代码。好处是没啥混淆,因此就能直接跟源码一样看了。
这里搜索ItemAgent_Gun,这个类里面一堆函数。先分析UpdateStates() method:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// ItemAgent_Gun
// Token: 0x06000835 RID: 2101 RVA: 0x000249C4 File Offset: 0x00022BC4
public void UpdateStates()
{
// if is loading, then just skip
if (this.GunItemSetting.LoadingBullets)
{
return;
}
if (this.triggerThisFrame && this.ShootSpeed >= 5f)
{
this.triggerBuffer = true;
this.triggerThisFrame = false;
}
switch (this.gunState)
{
// the gun is cooling.
// if the cooling time is finished, then go back to ready state.
// TODO: can be made unconditionally.
case ItemAgent_Gun.GunStates.shootCooling:
this.stateTimer += Time.deltaTime;
if (this.stateTimer >= this.burstCoolTime)
{
this.TransToReady();
return;
}
break;
//
case ItemAgent_Gun.GunStates.ready:
{
// this flag seems to be checking whether the gun is shot or not.
bool flag = false;
ItemSetting_Gun.TriggerModes currentTriggerMode = this.GunItemSetting.currentTriggerMode;
// TODO: can be patched to nop.
if (this.BulletEmpty)
{
this.TransToEmpty();
}
// if full auto
else if (currentTriggerMode == ItemSetting_Gun.TriggerModes.auto)
{
if (this.triggerInput)
{
flag = true;
}
}
// if semi auto or the gun is in "bolt" mode, and also it is triggered in this frame.
else if ((currentTriggerMode == ItemSetting_Gun.TriggerModes.semi || currentTriggerMode == ItemSetting_Gun.TriggerModes.bolt) && (this.triggerBuffer || this.triggerThisFrame))
{
this.triggerThisFrame = false;
this.triggerBuffer = false;
flag = true;
}
// 如果flag==true,就fire!
if (flag)
{
// CORE!
this.TransToFire(this.triggerThisFrame);
return;
}
// auto reload logic.
// this field is set from below.
if (this.needAutoReload)
{
this.needAutoReload = false;
this.CharacterReload(null);
return;
}
break;
}
case ItemAgent_Gun.GunStates.fire:
this.triggerBuffer = false;
// TODO:这个BulletCount condition需要patch
if (base.Holder && this.BulletCount <= 0)
{
this.muzzleIndex = ((this.muzzleIndex == 0) ? 1 : 0);
this.TransToEmpty();
return;
}
// TODO: 需要patch
// 里面的muzzle可能只是用于控制枪口动画的(比如枪口火焰等),不重要
if (this.burstCounter >= this.BurstCount)
{
this.muzzleIndex = ((this.muzzleIndex == 0) ? 1 : 0);
this.TransToBurstCooling();
return;
}
this.TransToBurstEachShotCooling();
return;
case ItemAgent_Gun.GunStates.burstEachShotCooling:
this.stateTimer += Time.deltaTime;
// TODO: make unconditional
if (this.stateTimer >= this.burstShotTimeSpace)
{
this.TransToFire(false);
return;
}
break;
case ItemAgent_Gun.GunStates.empty:
// empty无所谓,不用管
if (this.needAutoReload)
{
this.needAutoReload = false;
this.CharacterReload(null);
return;
}
if ((this.triggerThisFrame || this.triggerBuffer) && base.Holder != null)
{
this.triggerThisFrame = false;
this.triggerBuffer = false;
base.Holder.TryToReload(null);
return;
}
break;
case ItemAgent_Gun.GunStates.reloading:
this.triggerBuffer = false;
this.stateTimer += Time.deltaTime;
// TODO: 最简单的patch可能是将ReloadTime设为很小的值
if (this.stateTimer < this.ReloadTime)
{
this.loadBulletsStarted = false;
return;
}
if (!this.loadBulletsStarted)
{
this.loadBulletsStarted = true;
this.StartLoadBullets();
return;
}
if (!this.GunItemSetting.LoadingBullets)
{
if (this.GunItemSetting.LoadBulletsSuccess)
{
this.PostReloadSuccessSound();
}
this.needAutoReload = (this.GunItemSetting.reloadMode == ItemSetting_Gun.ReloadModes.singleBullet && !this.GunItemSetting.IsFull());
this.loadBulletsStarted = false;
if (this.GunItemSetting.BulletCount > 0 && this.loadedVisualObject != null)
{
this.loadedVisualObject.SetActive(true);
}
Action onLoadedEvent = this.OnLoadedEvent;
if (onLoadedEvent != null)
{
onLoadedEvent();
}
this.TransToReady();
}
break;
default:
return;
}
}
还有另一个部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// Token: 0x0600083A RID: 2106 RVA: 0x00024E7C File Offset: 0x0002307C
private void TransToFire(bool isFirstShot)
{
// TODO: Patch
if (this.BulletEmpty)
{
return;
}
// TODO: Patch
if (base.Item.Durability <= 0f)
{
return;
}
this.gunState = ItemAgent_Gun.GunStates.fire;
Vector3 vector = this.muzzle.forward;
if (base.Holder && base.Holder.CharacterMoveability > 0.5f)
{
Vector3 currentAimPoint = base.Holder.GetCurrentAimPoint();
currentAimPoint.y = 0f;
Vector3 position = base.Holder.transform.position;
position.y = 0f;
Vector3 position2 = this.muzzle.position;
position2.y = 0f;
if (Vector3.Distance(position, currentAimPoint) > Vector3.Distance(position, position2) + 0.1f)
{
vector = base.Holder.GetCurrentAimPoint() - this.muzzle.position;
vector.Normalize();
}
}
if (this.overrideShootPoint)
{
vector = this.overrideShootTargetPoint - this.muzzle.position;
vector.Normalize();
}
if (base.Holder && !base.Holder.IsMainCharacter)
{
this.SearchTraceTarget();
}
for (int i = 0; i < this.ShotCount; i++)
{
Vector3 vector2 = vector;
float num = this.ShotAngle;
bool flag = num > 359f;
if (flag)
{
num -= num / (float)this.ShotCount;
}
float num2 = -num * 0.5f;
float num3 = num / ((float)this.ShotCount - 1f);
if ((float)this.ShotCount % 2f < 0.01f && flag)
{
num2 -= num3 * 0.5f;
}
if (this.ShotCount > 1)
{
vector2 = Quaternion.Euler(0f, num2 + (float)i * num3, 0f) * vector;
}
Vector3 localPosition = this.muzzle.localPosition;
localPosition.y = 0f;
float magnitude = localPosition.magnitude;
this.shootSpeedGainOverShoot = Mathf.MoveTowards(this.shootSpeedGainOverShoot, this.ShootSpeedGainByShootMax, this.ShootSpeedGainEachShoot);
this.ShootOneBullet(this.muzzle.position, vector2, this.muzzle.position - magnitude * vector2);
if (base.Holder != null)
{
AIMainBrain.MakeSound(new AISound
{
fromCharacter = base.Holder,
fromObject = base.gameObject,
pos = this.muzzle.position,
soundType = SoundTypes.combatSound,
fromTeam = base.Holder.Team,
radius = this.SoundRange
});
}
}
this.PostShootSound();
this.scatterBeforeControl = Mathf.Clamp(this.scatterBeforeControl + this.ScatterGrow, this.DefaultScatter, this.MaxScatter);
if (base.Holder)
{
this.AimRecoil(vector);
if (base.Holder == LevelManager.Instance.MainCharacter)
{
LevelManager.Instance.InputManager.AddRecoil(this);
}
this.GunItemSetting.UseABullet();
base.Holder.TriggerShootEvent(this);
if (this.BulletEmpty && this.GunItemSetting.autoReload)
{
this.needAutoReload = true;
}
if (base.Holder.IsMainCharacter && LevelManager.Instance.IsRaidMap)
{
base.Item.Durability = Mathf.Max(0f, base.Item.Durability - this.bulletDurabilityCost);
}
}
this.StartVisualRecoil();
Action onShootEvent = this.OnShootEvent;
if (onShootEvent != null)
{
onShootEvent();
}
if (this.GunItemSetting.BulletCount <= 0 && this.loadedVisualObject != null)
{
this.loadedVisualObject.SetActive(false);
}
if (this.muzzleFxPfb)
{
UnityEngine.Object.Instantiate<GameObject>(this.muzzleFxPfb, this.muzzle.position, this.muzzle.rotation).transform.SetParent(this.muzzle);
}
if (this.shellParticle)
{
this.shellParticle.Emit(1);
}
this.burstCounter++;
if (base.Holder && base.Holder.IsMainCharacter)
{
CameraShaker.Shake(-this.muzzle.forward * 0.07f, CameraShaker.CameraShakeTypes.recoil);
Action<ItemAgent_Gun> onMainCharacterShootEvent = ItemAgent_Gun.OnMainCharacterShootEvent;
if (onMainCharacterShootEvent == null)
{
return;
}
onMainCharacterShootEvent(this);
}
}
OK我承认是有点乱了。现在直接开始总结:
Patch 无限子弹
直接 NOP 掉 TransToFire 里的 this.GunItemSetting.UseABullet() 调用即可。子弹数永远不减少,BulletEmpty 和 BulletCount <= 0 这些检查自然不会触发,不需要逐个去堵。
1
2
// NOP this call
this.GunItemSetting.UseABullet();
JIT里面在:
ItemAgent_Gun:TransToFire+1df6 - 49 BB 90D80B39CD010000 - mov r11,ItemSetting_Gun:UseABullet { (-326416180) } ItemAgent_Gun:TransToFire+1e00 - 41 FF D3 - call r11
Full Auto
Patch UpdateStates 中 ready 分支的 trigger mode 判断,让 flag 无条件为 true,或者将 currentTriggerMode 强制设为 auto:
1
2
3
4
5
6
// 原始逻辑根据 trigger mode 区分 auto/semi/bolt
// Patch: 无论什么模式,只要 triggerInput 就 flag = true
if (this.triggerInput)
{
flag = true;
}
具体方法是:
1
2
3
4
5
6
7
8
// 把这个else if改成一个direct jump.
else if (currentTriggerMode == ItemSetting_Gun.TriggerModes.auto)
{
if (this.triggerInput)
{
flag = true;
}
}
这里是整个switch-case的开头的dispatcher,是一个jump table:
1
2
3
4
5
6
7
8
9
10
ItemAgent_Gun:UpdateStates+89 - 4C 63 B6 24010000 - movsxd r14,dword ptr [rsi+00000124]
ItemAgent_Gun:UpdateStates+90 - 41 83 FE 06 - cmp r14d,06 { 6 }
ItemAgent_Gun:UpdateStates+94 - 0F83 D2050000 - jae ItemAgent_Gun:UpdateStates+66c
ItemAgent_Gun:UpdateStates+9a - 49 8B C6 - mov rax,r14
ItemAgent_Gun:UpdateStates+9d - 48 C1 E0 03 - shl rax,03 { 3 }
ItemAgent_Gun:UpdateStates+a1 - 8B C8 - mov ecx,eax
ItemAgent_Gun:UpdateStates+a3 - 48 B8 00D8BF2FCD010000 - mov rax,000001CD2FBFD800 { (1CD2FBFCF95) }
ItemAgent_Gun:UpdateStates+ad - 48 03 C1 - add rax,rcx
ItemAgent_Gun:UpdateStates+b0 - 48 8B 00 - mov rax,[rax]
ItemAgent_Gun:UpdateStates+b3 - FF E0 - jmp rax
这个table的每个元素是8字节长(从shl rax, 3看出),然后head在000001CD2FBFD800。
这样的话我们要找的是case ItemAgent_Gun.GunStates.ready:,这个值其实是1.
整个表dump出来:
000001CD2FBFCF95 000001CD2FBFD025 000001CD2FBFD128 000001CD2FBFD20F 000001CD2FBFD2A8 000001CD2FBFD340
第二个也就是index=1了。000001CD2FBFD025
ItemAgent_Gun:UpdateStates+145 - 33 C0 - xor eax,eax ItemAgent_Gun:UpdateStates+147 - 48 0FB6 F8 - movzx rdi,al ItemAgent_Gun:UpdateStates+14b - 48 8B CE - mov rcx,rsi ItemAgent_Gun:UpdateStates+14e - 49 BB 0097B72FCD010000 - mov r11,ItemAgent_Gun:get_GunItemSetting { (-326416299) } ItemAgent_Gun:UpdateStates+158 - 41 FF D3 - call r11 ItemAgent_Gun:UpdateStates+15b - 48 8B C8 - mov rcx,rax ItemAgent_Gun:UpdateStates+15e - 83 38 00 - cmp dword ptr [rax],00 { 0 } ItemAgent_Gun:UpdateStates+161 - 48 8D 64 24 00 - lea rsp,[rsp+00] ItemAgent_Gun:UpdateStates+166 - 49 BB 30D8BF2FCD010000 - mov r11,ItemSetting_Gun:get_currentTriggerMode { (-326416299) } ItemAgent_Gun:UpdateStates+170 - 41 FF D3 - call r11 ItemAgent_Gun:UpdateStates+173 - 4C 8B F8 - mov r15,rax ItemAgent_Gun:UpdateStates+176 - 48 8B CE - mov rcx,rsi ItemAgent_Gun:UpdateStates+179 - 48 8D 64 24 00 - lea rsp,[rsp+00] ItemAgent_Gun:UpdateStates+17e - 49 BB B0D9BF2FCD010000 - mov r11,ItemAgent_Gun:get_BulletEmpty { (-326416299) }
和ready的开头部分一致:
1
2
3
4
5
6
7
8
case ItemAgent_Gun.GunStates.ready:
{
bool flag = false;
ItemSetting_Gun.TriggerModes currentTriggerMode = this.GunItemSetting.currentTriggerMode;
if (this.BulletEmpty)
{
this.TransToEmpty();
}
相关代码应该在这:
1
2
3
4
5
6
7
8
ItemAgent_Gun:UpdateStates+19e - 45 85 FF - test r15d,r15d
ItemAgent_Gun:UpdateStates+1a1 - 75 1A - jne ItemAgent_Gun:UpdateStates+1bd
ItemAgent_Gun:UpdateStates+1a3 - 0FB6 86 ED000000 - movzx eax,byte ptr [rsi+000000ED]
ItemAgent_Gun:UpdateStates+1aa - 85 C0 - test eax,eax
ItemAgent_Gun:UpdateStates+1ac - 0F84 44000000 - je ItemAgent_Gun:UpdateStates+1f6
ItemAgent_Gun:UpdateStates+1b2 - B8 01000000 - mov eax,00000001 { 1 }
ItemAgent_Gun:UpdateStates+1b7 - 48 0FB6 F8 - movzx rdi,al
ItemAgent_Gun:UpdateStates+1bb - EB 39 - jmp ItemAgent_Gun:UpdateStates+1f6
这里r15d是currentTriggerMode,因此这里面的就是currentTriggerMode==0(auto)的情况。
然后+ED偏移渠道的应该是this.triggerInput,如果是1就让flag=true。(flag现在存在rdi里面)
之后jump到+1f6. +1f6:
ItemAgent_Gun:UpdateStates+1f6 - 85 FF - test edi,edi ItemAgent_Gun:UpdateStates+1f8 - 74 1E - je ItemAgent_Gun:UpdateStates+218 ItemAgent_Gun:UpdateStates+1fa - 0FB6 96 EE000000 - movzx edx,byte ptr [rsi+000000EE] ItemAgent_Gun:UpdateStates+201 - 48 8B CE - mov rcx,rsi ItemAgent_Gun:UpdateStates+204 - 66 90 - nop 2 ItemAgent_Gun:UpdateStates+206 - 49 BB C0340B39CD010000 - mov r11,ItemAgent_Gun:TransToFire { (-326416299) } ItemAgent_Gun:UpdateStates+210 - 41 FF D3 - call r11 ItemAgent_Gun:UpdateStates+213 - E9 54040000 - jmp ItemAgent_Gun:UpdateStates+66c ItemAgent_Gun:UpdateStates+218 - 0FB6 86 28010000 - movzx eax,byte ptr [rsi+00000128]
就是检测flag==1?true的话就进入里面的逻辑,call TransToFire.
所以现在的逻辑可以设计的简单粗暴:将+1a1改成nop:
ItemAgent_Gun:UpdateStates+1a1 - 75 1A - jne ItemAgent_Gun:UpdateStates+1bd
这个是判断currentTriggerMode的,因此改成nop就没有这个的判断了。然后就只剩下if (this.triggerInput),而这个显然是需要保留的。不能在没trigger的情况下也发射。
No Reload
UpdateStates:
1
2
3
4
5
6
// 最简单的patch可能是将ReloadTime设为很小的值
if (this.stateTimer < this.ReloadTime)
{
this.loadBulletsStarted = false;
return;
}
这个在reloading,也就是entry 5,000001CD2FBFD340
开头:
ItemAgent_Gun:UpdateStates+460 - C6 86 F0000000 00 - mov byte ptr [rsi+000000F0],00 { 0 } ItemAgent_Gun:UpdateStates+467 - F3 0F10 86 1C010000 - movss xmm0,[rsi+0000011C] ItemAgent_Gun:UpdateStates+46f - F3 0F5A C0 - cvtss2sd xmm0,xmm0 ItemAgent_Gun:UpdateStates+473 - F2 0F11 45 D0 - movsd [rbp-30],xmm0 ItemAgent_Gun:UpdateStates+478 - 48 8D 64 24 00 - lea rsp,[rsp+00] ItemAgent_Gun:UpdateStates+47d - 90 - nop ItemAgent_Gun:UpdateStates+47e - 49 BB 8E9338F4CC010000 - mov r11,000001CCF438938E { (73) } ItemAgent_Gun:UpdateStates+488 - 41 FF D3 - call r11 ItemAgent_Gun:UpdateStates+48b - F3 0F5A C8 - cvtss2sd xmm1,xmm0 ItemAgent_Gun:UpdateStates+48f - F2 0F10 45 D0 - movsd xmm0,[rbp-30] ItemAgent_Gun:UpdateStates+494 - F2 0F58 C1 - addsd xmm0,xmm1 ItemAgent_Gun:UpdateStates+498 - F2 0F5A E8 - cvtsd2ss xmm5,xmm0 ItemAgent_Gun:UpdateStates+49c - F3 0F11 AE 1C010000 - movss [rsi+0000011C],xmm5 ItemAgent_Gun:UpdateStates+4a4 - F3 0F10 86 1C010000 - movss xmm0,[rsi+0000011C] ItemAgent_Gun:UpdateStates+4ac - F3 0F5A C0 - cvtss2sd xmm0,xmm0 ItemAgent_Gun:UpdateStates+4b0 - F2 0F11 45 D8 - movsd [rbp-28],xmm0 ItemAgent_Gun:UpdateStates+4b5 - 48 8B CE - mov rcx,rsi ItemAgent_Gun:UpdateStates+4b8 - 48 8D 64 24 00 - lea rsp,[rsp+00] ItemAgent_Gun:UpdateStates+4bd - 90 - nop ItemAgent_Gun:UpdateStates+4be - 49 BB 52D6BF2FCD010000 - mov r11,000001CD2FBFD652 { (73) } ItemAgent_Gun:UpdateStates+4c8 - 41 FF D3 - call r11 ItemAgent_Gun:UpdateStates+4cb - F3 0F5A C8 - cvtss2sd xmm1,xmm0 ItemAgent_Gun:UpdateStates+4cf - F2 0F10 45 D8 - movsd xmm0,[rbp-28] ItemAgent_Gun:UpdateStates+4d4 - 66 0F2F C8 - comisd xmm1,xmm0 ItemAgent_Gun:UpdateStates+4d8 - 0F86 0C000000 - jbe ItemAgent_Gun:UpdateStates+4ea
最后的jbe应该就是判断if (this.stateTimer < this.ReloadTime)后的跳转。
这里的patch很简单:jbe改成jmp,强制跳转。直接无视前面的conditional check。
No Burst Cooling
只需要两个 patch。Patch 掉 burstCounter >= BurstCount 之后,枪永远不会进入 shootCooling 状态,所以那个分支不用管。
实测发现不能直接阻止进入 burst cooling。原因:triggerInput 的检查只在 ready 状态里做。如果枪永远不进入 shootCooling,控制流就会卡在 fire → burstEachShotCooling → fire → ... 的死循环里,永远回不到 ready,松开扳机也停不下来。
正确做法:让枪正常进入 shootCooling,但让 TransToReady() 无条件执行(跳过 stateTimer >= burstCoolTime 的等待),这样枪能瞬间回到 ready 重新检查 trigger 状态。
UpdateStates(阻止进入 burst cooling):(已停用)
1
2
3
4
5
6
7
// 原本想直接阻止进入 burst cooling,但会导致控制流死循环
if (this.burstCounter >= this.BurstCount)
{
this.muzzleIndex = ((this.muzzleIndex == 0) ? 1 : 0);
this.TransToBurstCooling();
return;
}
这个在fire(0x2)里:000001CD2FBFD128
ItemAgent_Gun:UpdateStates+2d4 - 0F8C 36000000 - jl ItemAgent_Gun:UpdateStates+310
简单方式是直接把jl 改成jmp,这样就保证不会满足(已停用,原因见上)if (this.burstCounter >= this.BurstCount)。
UpdateStates(让 burst cooling 瞬间完成):
需要 patch shootCooling 分支(jump table index 0,入口 000001CD2FBFCF95),让 TransToReady() 无条件执行:
1
2
3
4
5
6
7
8
case ItemAgent_Gun.GunStates.shootCooling:
this.stateTimer += Time.deltaTime;
// Patch: 跳过这个条件,直接 TransToReady()
if (this.stateTimer >= this.burstCoolTime)
{
this.TransToReady();
return;
}
TODO: 需要从 CE 反汇编中确认 shootCooling 分支内条件跳转的具体偏移。
UpdateStates(消除每发之间的间隔冷却):
1
2
3
4
5
6
// Patch: make unconditional,最大化射速
if (this.stateTimer >= this.burstShotTimeSpace)
{
this.TransToFire(false);
return;
}
这个是optional的,可以保留现有射速。主要是防止射太多把游戏也卡死了。
Ignore Duration
TransToFire:
1
2
3
4
5
// Patch
if (base.Item.Durability <= 0f)
{
return;
}
ItemAgent_Gun:TransToFire+116 - 49 BB F085432DCD010000 - mov r11,ItemStatsSystem.Item:get_Durability { (-326416299) } ItemAgent_Gun:TransToFire+120 - 41 FF D3 - call r11 ItemAgent_Gun:TransToFire+123 - F3 0F5A C0 - cvtss2sd xmm0,xmm0 ItemAgent_Gun:TransToFire+127 - 66 0F57 C9 - xorpd xmm1,xmm1 ItemAgent_Gun:TransToFire+12b - 66 0F2F C8 - comisd xmm1,xmm0 ItemAgent_Gun:TransToFire+12f - 72 05 - jb ItemAgent_Gun:TransToFire+136 ItemAgent_Gun:TransToFire+131 - E9 89230000 - jmp ItemAgent_Gun:TransToFire+24bf ItemAgent_Gun:TransToFire+136 - C7 86 24010000 02000000 - mov [rsi+00000124],00000002 { 2 }
ItemAgent_Gun:TransToFire+12f的jb改成jmp即可。
CE Auto Assembler 脚本
锁血的 code injection 在前面已经写过了,这里只写武器相关的 patch。
注意:所有偏移基于 JIT 地址,每次启动游戏可能变化。如果偏移失效,需要重新在 CE 反汇编中确认。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
[ENABLE]
// ========== 无限子弹 ==========
// NOP UseABullet() call in TransToFire
// mov r11, UseABullet (10 bytes) + call r11 (3 bytes) = 13 bytes
ItemAgent_Gun:TransToFire+1df6:
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
nop
// ========== Full Auto(暂时停用,排查crash) ==========
// // NOP the jne that checks currentTriggerMode != auto
// ItemAgent_Gun:UpdateStates+1a1:
// nop
// nop
// ========== No Reload(暂时停用,排查crash) ==========
// // Change jbe (conditional) to jmp (unconditional)
// // Original: 0F 86 0C000000 (jbe +0C)
// // Patched: 90 E9 0C000000 (nop + jmp +0C)
// ItemAgent_Gun:UpdateStates+4d8:
// db 90 E9
// ========== No Burst Cooling(已停用) ==========
// 不能阻止进入 burst cooling(会导致 fire 死循环,松开扳机也停不下来)
// 正确做法:让 shootCooling 里的 TransToReady() 无条件执行
// TODO: 需要从 CE 确认 shootCooling 分支内条件跳转的具体偏移
//
// 旧方案(已停用):
// ItemAgent_Gun:UpdateStates+2d4:
// db E9 37 00 00 00 90
// ========== Ignore Duration ==========
// Change jb (conditional) to jmp (unconditional)
// Original: 72 05 (jb +05)
// Patched: EB 05 (jmp +05)
ItemAgent_Gun:TransToFire+12f:
db EB 05
[DISABLE]
// Restore original bytes
ItemAgent_Gun:TransToFire+1df6:
db 49 BB 90 D8 0B 39 CD 01 00 00
ItemAgent_Gun:TransToFire+1e00:
db 41 FF D3
// ItemAgent_Gun:UpdateStates+1a1:
// db 75 1A
// ItemAgent_Gun:UpdateStates+4d8:
// db 0F 86
// 旧方案(已停用):
// ItemAgent_Gun:UpdateStates+2d4:
// db 0F 8C 36 00 00 00
ItemAgent_Gun:TransToFire+12f:
db 72 05
重量
CharacterMainControl:UpdateWeightState
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if (weightStates != this.weightState)
{
this.weightState = weightStates;
this.RemoveBuffsByTag(Buff.BuffExclusiveTags.Weight, false);
switch (weightStates)
{
case CharacterMainControl.WeightStates.light:
this.AddBuff(GameplayDataSettings.Buffs.Weight_Light, this, 0);
return;
case CharacterMainControl.WeightStates.normal:
break;
case CharacterMainControl.WeightStates.heavy:
this.AddBuff(GameplayDataSettings.Buffs.Weight_Heavy, this, 0);
return;
case CharacterMainControl.WeightStates.superHeavy:
this.AddBuff(GameplayDataSettings.Buffs.Weight_SuperHeavy, this, 0);
return;
case CharacterMainControl.WeightStates.overWeight:
this.AddBuff(GameplayDataSettings.Buffs.Weight_Overweight, this, 0);
break;
default:
return;
}
}
把switch-tablepatch成永远作用于light即可。
CharacterMainControl:UpdateWeightState+1a2 - 0F83 F3000000 - jae CharacterMainControl:UpdateWeightState+29b CharacterMainControl:UpdateWeightState+1a8 - 48 8B 45 E0 - mov rax,[rbp-20] CharacterMainControl:UpdateWeightState+1ac - 48 C1 E0 03 - shl rax,03 { 3 } CharacterMainControl:UpdateWeightState+1b0 - 8B C8 - mov ecx,eax CharacterMainControl:UpdateWeightState+1b2 - 48 B8 C08DB6AC7B020000 - mov rax,0000027BACB68DC0 { (27BACB68C64) } CharacterMainControl:UpdateWeightState+1bc - 48 03 C1 - add rax,rcx CharacterMainControl:UpdateWeightState+1bf - 48 8B 00 - mov rax,[rax] CharacterMainControl:UpdateWeightState+1c2 - FF E0 - jmp rax
0000027BACB68C64 0000027BACB68D3B 0000027BACB68C98 0000027BACB68CD0 0000027BACB68D08
这里选择
CharacterMainControl:UpdateWeightState+1bc - 48 03 C1 - add rax,rcx
改成nop.这样就强制只跳进第一个case,也就是light case。
1
2
3
4
5
6
7
8
9
10
11
12
[ENABLE]
CharacterMainControl:UpdateWeightState+1bc:
nop
nop
nop
[DISABLE]
CharacterMainControl:UpdateWeightState+1bc:
db 48 03 C1
金钱
Economy:EconomyManager:Pay:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private static bool Pay(long amount, bool accountAvaliable = true, bool cashAvaliale = true)
{
long num = accountAvaliable ? EconomyManager.Money : 0L;
long num2 = cashAvaliale ? EconomyManager.Cash : 0L;
if (num + num2 < amount)
{
return false;
}
long num3 = amount;
if (accountAvaliable)
{
if (num > amount)
{
num3 = 0L;
EconomyManager.Money -= amount;
}
else
{
num3 -= num;
EconomyManager.Money = 0L;
}
}
if (cashAvaliale && num3 > 0L)
{
ItemUtilities.ConsumeItems(451, num3);
}
if (amount > 0L)
{
Action<long> onMoneyPaid = EconomyManager.OnMoneyPaid;
if (onMoneyPaid != null)
{
onMoneyPaid(amount);
}
}
return true;
}
我的想法是把amount param hook了,强制改成0.
IsEnough:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static bool IsEnough(Cost cost, bool accountAvaliable = true, bool cashAvaliale = true)
{
long num = accountAvaliable ? EconomyManager.Money : 0L;
long num2 = cashAvaliale ? EconomyManager.Cash : 0L;
if (num + num2 < cost.money)
{
return false;
}
if (cost.items != null)
{
foreach (Cost.ItemEntry itemEntry in cost.items)
{
if ((long)ItemUtilities.GetItemCount(itemEntry.id) < itemEntry.amount)
{
return false;
}
}
}
return true;
}
强制返回true
还有get_Money()方法:
Duckov.Economy.EconomyManager:get_Money - 55 - push rbp Duckov.Economy.EconomyManager:get_Money+1- 48 8B EC - mov rbp,rsp Duckov.Economy.EconomyManager:get_Money+4- 48 83 EC 20 - sub rsp,20 { 32 } Duckov.Economy.EconomyManager:get_Money+8- 48 B8 488B581BAB010000 - mov rax,000001AB1B588B48 { (0) } Duckov.Economy.EconomyManager:get_Money+12- 48 8B 08 - mov rcx,[rax] Duckov.Economy.EconomyManager:get_Money+15- 33 D2 - xor edx,edx Duckov.Economy.EconomyManager:get_Money+17- 48 8D AD 00000000 - lea rbp,[rbp+00000000] Duckov.Economy.EconomyManager:get_Money+1e- 49 BB D0705929AC010000 - mov r11,UnityEngine.Object:op_Equality { (-326416299) } Duckov.Economy.EconomyManager:get_Money+28- 41 FF D3 - call r11 Duckov.Economy.EconomyManager:get_Money+2b- 85 C0 - test eax,eax Duckov.Economy.EconomyManager:get_Money+2d- 74 04 - je Duckov.Economy.EconomyManager:get_Money+33 Duckov.Economy.EconomyManager:get_Money+2f- 33 C0 - xor eax,eax Duckov.Economy.EconomyManager:get_Money+31- EB 11 - jmp Duckov.Economy.EconomyManager:get_Money+44 Duckov.Economy.EconomyManager:get_Money+33- 48 B8 488B581BAB010000 - mov rax,000001AB1B588B48 { (0) } Duckov.Economy.EconomyManager:get_Money+3d- 48 8B 00 - mov rax,[rax] Duckov.Economy.EconomyManager:get_Money+40- 48 8B 40 40 - mov rax,[rax+40] Duckov.Economy.EconomyManager:get_Money+44- 48 8D 65 00 - lea rsp,[rbp+00] Duckov.Economy.EconomyManager:get_Money+48- 5D - pop rbp Duckov.Economy.EconomyManager:get_Money+49- C3 - ret
对应为Money里的get方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static long Money
{
get
{
if (EconomyManager.Instance == null)
{
return 0L;
}
return EconomyManager.Instance.money;
}
private set
{
long arg = EconomyManager.Money;
if (EconomyManager.Instance == null)
{
return;
}
EconomyManager.Instance.money = value;
Action<long, long> onMoneyChanged = EconomyManager.OnMoneyChanged;
if (onMoneyChanged == null)
{
return;
}
onMoneyChanged(arg, value);
}
}
这里也可以hook,强制返回一个long类型(int64)中很大的值。
附录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// Health
// Token: 0x06000408 RID: 1032 RVA: 0x00011DB0 File Offset: 0x0000FFB0
public bool Hurt(DamageInfo damageInfo)
{
if (MultiSceneCore.Instance != null && MultiSceneCore.Instance.IsLoading)
{
return false;
}
if (this.invincible)
{
return false;
}
if (this.isDead)
{
return false;
}
if (damageInfo.buff != null && UnityEngine.Random.Range(0f, 1f) < damageInfo.buffChance)
{
this.AddBuff(damageInfo.buff, damageInfo.fromCharacter, damageInfo.fromWeaponItemID);
}
bool flag = LevelManager.Rule.AdvancedDebuffMode;
if (LevelManager.Instance.IsBaseLevel)
{
flag = false;
}
float num = 0.2f;
float num2 = 0.12f;
CharacterMainControl characterMainControl = this.TryGetCharacter();
if (!this.IsMainCharacterHealth)
{
num = 0.1f;
num2 = 0.1f;
}
if (flag && UnityEngine.Random.Range(0f, 1f) < damageInfo.bleedChance * num)
{
this.AddBuff(GameplayDataSettings.Buffs.BoneCrackBuff, damageInfo.fromCharacter, damageInfo.fromWeaponItemID);
}
else if (flag && UnityEngine.Random.Range(0f, 1f) < damageInfo.bleedChance * num2)
{
this.AddBuff(GameplayDataSettings.Buffs.WoundBuff, damageInfo.fromCharacter, damageInfo.fromWeaponItemID);
}
else if (UnityEngine.Random.Range(0f, 1f) < damageInfo.bleedChance)
{
if (flag)
{
this.AddBuff(GameplayDataSettings.Buffs.UnlimitBleedBuff, damageInfo.fromCharacter, damageInfo.fromWeaponItemID);
}
else
{
this.AddBuff(GameplayDataSettings.Buffs.BleedSBuff, damageInfo.fromCharacter, damageInfo.fromWeaponItemID);
}
}
bool flag2 = UnityEngine.Random.Range(0f, 1f) < damageInfo.critRate;
damageInfo.crit = (flag2 ? 1 : 0);
if (!damageInfo.ignoreDifficulty && this.team == Teams.player)
{
damageInfo.damageValue *= LevelManager.Rule.DamageFactor_ToPlayer;
}
float num3 = damageInfo.damageValue * (flag2 ? damageInfo.critDamageFactor : 1f);
if (damageInfo.damageType != DamageTypes.realDamage && !damageInfo.ignoreArmor)
{
float num4 = flag2 ? this.HeadArmor : this.BodyArmor;
if (characterMainControl && LevelManager.Instance.IsRaidMap)
{
Item item = flag2 ? characterMainControl.GetHelmatItem() : characterMainControl.GetArmorItem();
if (item)
{
item.Durability = Mathf.Max(0f, item.Durability - damageInfo.armorBreak);
}
}
float num5 = 1f;
if (num4 > 0f)
{
num5 = 2f / (Mathf.Clamp(num4 - damageInfo.armorPiercing, 0f, 999f) + 2f);
}
if (characterMainControl && !characterMainControl.IsMainCharacter && damageInfo.fromCharacter && !damageInfo.fromCharacter.IsMainCharacter)
{
CharacterRandomPreset characterPreset = damageInfo.fromCharacter.characterPreset;
CharacterRandomPreset characterPreset2 = characterMainControl.characterPreset;
if (characterPreset && characterPreset2)
{
num5 *= characterPreset.aiCombatFactor / characterPreset2.aiCombatFactor;
}
}
num3 *= num5;
}
if (damageInfo.elementFactors.Count <= 0)
{
damageInfo.elementFactors.Add(new ElementFactor(ElementTypes.physics, 1f));
}
float num6 = 0f;
foreach (ElementFactor elementFactor in damageInfo.elementFactors)
{
float factor = elementFactor.factor;
float num7 = this.ElementFactor(elementFactor.elementType);
float num8 = num3 * factor * num7;
if (num8 < 1f && num8 > 0f && num7 > 0f && factor > 0f)
{
num8 = 1f;
}
if (num8 > 0f && !this.Hidden && PopText.instance)
{
GameplayDataSettings.UIStyleData.DisplayElementDamagePopTextLook elementDamagePopTextLook = GameplayDataSettings.UIStyle.GetElementDamagePopTextLook(elementFactor.elementType);
float size = flag2 ? elementDamagePopTextLook.critSize : elementDamagePopTextLook.normalSize;
Color color = elementDamagePopTextLook.color;
PopText.Pop(num8.ToString("F1"), damageInfo.damagePoint + Vector3.up * 2f, color, size, flag2 ? GameplayDataSettings.UIStyle.CritPopSprite : null);
}
num6 += num8;
}
damageInfo.finalDamage = num6;
if (this.CurrentHealth < damageInfo.finalDamage)
{
damageInfo.finalDamage = this.CurrentHealth + 1f;
}
this.CurrentHealth -= damageInfo.finalDamage;
UnityEvent<DamageInfo> onHurtEvent = this.OnHurtEvent;
if (onHurtEvent != null)
{
onHurtEvent.Invoke(damageInfo);
}
Action<Health, DamageInfo> onHurt = Health.OnHurt;
if (onHurt != null)
{
onHurt(this, damageInfo);
}
if (this.isDead)
{
return true;
}
if (this.CurrentHealth <= 0f)
{
bool flag3 = true;
if (!LevelManager.Instance.IsRaidMap && !this.CanDieIfNotRaidMap)
{
flag3 = false;
}
if (!flag3)
{
this.SetHealth(1f);
}
}
if (this.CurrentHealth <= 0f)
{
this.CurrentHealth = 0f;
this.isDead = true;
if (LevelManager.Instance.MainCharacter != this.TryGetCharacter())
{
this.DestroyOnDelay().Forget();
}
if (this.item != null && this.team != Teams.player && damageInfo.fromCharacter && damageInfo.fromCharacter.IsMainCharacter)
{
EXPManager.AddExp(this.item.GetInt("Exp", 0));
}
UnityEvent<DamageInfo> onDeadEvent = this.OnDeadEvent;
if (onDeadEvent != null)
{
onDeadEvent.Invoke(damageInfo);
}
Action<Health, DamageInfo> onDead = Health.OnDead;
if (onDead != null)
{
onDead(this, damageInfo);
}
base.gameObject.SetActive(false);
if (damageInfo.fromCharacter && damageInfo.fromCharacter.IsMainCharacter)
{
Debug.Log("Killed by maincharacter");
}
}
return true;
}
Ideas
- 能不能把 Cheat Engine 和 MCP 结合起来?
- 是否可以定制 Cheat Engine,做一些简单的检测绕过?
- 能不能把
.Net Info自动导出来?目前 CE 本身没有提供这个能力。 - 是否可以写一个小脚本,把伪代码里很长的变量定义区折叠掉,方便阅读?

