Attacking A Weakness In Common Variable Protections
While many single player games keep their data completely unprotected and setup in a manner that is ‘game hacking’ friendly, others have opt’d to try and deter cheating, even when the experience does not affect others. Over the years, there has been many different changes to how games are coded and designed. Some of these changes have both directly and indirectly have been aimed at protecting a game from tampering, both from an anti-cheating standpoint, as well as a anti-piracy one.
There has been many different usages of more advanced techniques over the years, but they generally were used in more specific purposes like anti-piracy/anti-tampering. Not so much for anti-cheating. I won’t be diving into those topics, and in turn, those implementations in this post.
This past week I was asked to take a look at a single player game made by a Japanese developer. It is a bullet-hell style game that has no multiplayer. However, this developer has opt’d to protect the game from cheating. Because of how the developer has reacted towards English speaking people, I will leave the name of the game out of this post. I don’t want to affect his support for the game for others by exposing the weakness in the design of his anti-cheat measures. This is more of a generalized topic as other games and engines have made similar setups that can be attacked in the same manner.
Before we dive directly into this specific weakness, lets take a quick minute to take a look at some other basic anti-cheat measures that games have done in the past to protect variable values.
1. Basic Math Based Obfuscations
In the past, a lot of games have taken the approach of trying to ‘hide’ values by changing how the stored value is handled. This has been done in a handful of different ways. For example:
- value = (real - 0xFF)
- value = (real - 0xFFFF)
- value = (real - 0xFFFFFFFF)
- These methods above abuse underflowing/overflowing when the expected value falls within a specific range that is not outside the bounds of the signed maximum. I’ve seen this method used a few times in Korean games.
- value = (real + 50)
- This method does a basic addition onto the real value. For example, some of the old GTA games used a higher than normal base value for things like the players health.
- value = (real * 7)
- value = (real * 8)
- These methods were common back when Flash was popular. They had some other techniques, but these were the main two last ones that became part of the variable system. Values were stored using a multiplier to hide the real data.
- value = ((real & BITMASK) << SHIFTMASK)
- This method is mainly used when packing values together into a single value.
- value = (real ^ MASK)
- This method is used by a lot of more modern games now where values are obfuscated using an XOR key.
And many more. These, or often times combined together into more in depth obfuscations, are generally the more commonly seen means of protecting values in games. The cost-performance is pretty low so the overhead isn’t bad to use these kinds of setups on all values.
2. Type Change ‘Obfuscation’
I don’t consider this much of a protection. However, I do feel it should be mentioned since it has been used in the past as a means to try and hide variables/values on some games.
For example, a players entity has a health value that can be a range between 0 and 200. A simple and direct storage for this could be a single byte. (uint8_t
or similar.) However, in some games this can still land up being stored in a larger type such as a uint16_t
or uint32_t
. This does not directly protect the value though. Instead, some games have moved the value into a float
or double
type and then use the range of 0 to 1.0 to handle the value. That means that a value of 0.5 in this example would be 100 health.
This is also a common method now because it allows for easier usage of the health value to be applied to things such as a health bar fill percentage as most texture math related things use values of 0 to 1.
3. Indirect Value Type ‘Obfuscation’
Again, I don’t always consider this style a protection per-say, but it can be used as one. This type of protection is in the form of using a different style value altogether for something that would be assumed to be something else.
These would be things such as:
- The use of an enum instead of a raw value.
- The use of a simplified value instead of a combined set of values.
- For example, on 2D tile games, your position on a map is generally based on a grid. Your X and Y on that grid can be simplified into a 1D array index.
- The use of a string instead of a numeric value (or vice-versa).
- For example, your health could be stored as an actuall string like “100” instead of the integer value, then the use of string comparison functions could be used.
- The use of a hash of a value. (Often times used with strings.)
- For example, games that make use of hash maps for storage of variable data. The key is often just a hash of a variable names string, rather than the storage of the actual string.
Specific Target Protection - Randomized Subtraction Key Encoding
On to our target. The game in question, again, is a bullet-hell style shooter game. To give you an example of what I mean by ‘bullet-hell’, take a look at this game on Steam: https://store.steampowered.com/app/377860/Mushihimesama/
(This is not the game I am targeting, just an example of the style of game it is.)
The developer of the game I am targeting has been pretty outspoken about not wanting people to mod the game or cheat. For whatever reason, he is very anti-cheating even though it is a single player game that has no means of an internet connection for any multiplayer purposes. That said, each time someone decides to share any means of cheating, he seems to go about adding said variables to his list of ‘protected’ values that use the system I will be outlining here.
To start, let’s take a look at how the game is creating these protected values. This is the function that is used when allocating a ProtectedValue
type. (I’ve removed some junk code and variable definitions to slim the code down some.)
int __thiscall sub_6BA680(int this, int a2, int a3)
{
if ( a2 >= 5 )
return 0;
if ( !(++*(_DWORD *)(this + 60) % 10) || (v5 = this + 4 * (a2 + 10), !*(_DWORD *)v5) )
{
v5 = this + 4 * (a2 + 10);
if ( *(_DWORD *)v5 )
{
j__free(*(void **)(this + 4 * (a2 + 10)));
*(_DWORD *)v5 = 0;
}
v16 = rand() % 3 + 2;
v6 = operator new(4 * v16);
*(_DWORD *)v5 = v6;
if ( !v6 )
sub_401570();
**(_DWORD **)v5 = rand() % (int)&unk_5F5E101;
*(_DWORD *)(*(_DWORD *)v5 + 4) = rand() % (int)&unk_5F5E101;
v7 = v16;
if ( v16 >= 3 )
{
*(_DWORD *)(*(_DWORD *)v5 + 8) = rand() % (int)&unk_5F5E101;
v7 = v16;
}
if ( v7 >= 4 )
*(_DWORD *)(*(_DWORD *)v5 + 12) = rand() % (int)&unk_5F5E101;
if ( *(_DWORD *)(this + 4 * a2 + 20) )
{
j__free(*(void **)(this + 4 * a2 + 20));
*(_DWORD *)(this + 4 * a2 + 20) = 0;
}
v8 = operator new(4u);
*(_DWORD *)(this + 4 * a2 + 20) = v8;
if ( !v8 )
sub_401570();
}
v9 = *(_BYTE *)(this + 64) == 0;
*(int *)(this + 60) %= 10;
if ( v9 )
{
v13 = a3;
}
else
{
v10 = operator new(4u);
Block = v10;
if ( !v10 )
sub_401570();
v17 = (int *)operator new(4u);
if ( !v17 )
sub_401570();
*v10 = sub_6BA8B0(4);
v11 = sub_6BA8B0(3);
*v17 = v11;
v12 = *v10;
v13 = a3;
if ( v12 <= v11 )
{
if ( a3 < v12 )
v13 = v12;
if ( v13 > v11 )
v13 = v11;
}
j__free(Block);
j__free(v17);
}
v14 = *(_DWORD **)v5;
*(_DWORD *)(this + 4 * a2) = v13;
**(_DWORD **)(this + 4 * a2 + 20) = v13 + *v14;
*(_BYTE *)(a2 + this + 65) = 1;
return 1;
}
Here, the game is allocating some memory to hold both the ‘real’ value which is passed in a3
as well as an XOR key that is generated every time the variable is read or written to. (Yes, this function is ran, on all variables that use it, every time the value is accessed.)
There two methods that the value may be accessed, one is a general lookup, the other is an inlined setup of the same thing:
int __thiscall sub_6BA860(int this)
{
if ( !*(_BYTE *)(this + 67) )
return 0;
if ( this == -40 || (v3 = *(_DWORD **)(this + 0x30)) == 0 || (v4 = *(_DWORD **)(this + 0x1C)) == 0 )
sub_401590();
v1 = *(_DWORD *)(this + 8);
if ( v1 != *v4 - *v3 )
{
MessageBoxA(0, "Falisified Parameter", "Failed", 0);
abort();
}
return v1;
}
if ( byte_3DD9CDB )
{
if ( !dword_3DD9CC8 )
sub_401590();
if ( !dword_3DD9CB4 )
goto LABEL_598;
v32 = dword_3DD9CA0;
if ( dword_3DD9CA0 != *(_DWORD *)dword_3DD9CB4 - *(_DWORD *)dword_3DD9CC8 )
{
MessageBoxA(0, "Falisified Parameter", "Failed", 0);
abort();
}
}
else
{
v32 = 0;
}
With all three of these chunks of code, we can better understand how the anti-cheat method is setup.
Both the method sub_6BA860
and the inline example above show us how the ProtectedValue
object is being accessed when the value is being read and how its validated. We can see that the real value is stored in the protected object as well as the two values that are used with the randomizer to ‘validate’ the value isn’t tampered with.
In this above example that is inlined, that is part of the players ‘Score’ value. So we’ll use that as our example to explain what is happening.
The inline example shows us a static ProtectedValue
layed out in memory so the offsets seen and used in the actual method call are different. Since the addresses are known at compile time, they are just hard-coded by the compiler. Taking a look at the method sub_6BA860
, we can see how the object is read for validation, which also tells us how its allocated:
if ( this == -40 || (v3 = *(_DWORD **)(this + 0x30)) == 0 || (v4 = *(_DWORD **)(this + 0x1C)) == 0 )
sub_401590();
v1 = *(_DWORD *)(this + 8);
if ( v1 != *v4 - *v3 )
First, we can observe that the object is being checked for valid pointers at +1C
and +30
. These hold the two rand()
mask values that are used to create a validator.
-
+8
holds the original real value. -
+1C
holds the maskedrand()
value that is made with:real_value + rand()
-
+30
holds the originalrand()
value generated whensub_6BA680
was called. - Both
+1C
and+30
share the same base randomized value.
The rand() values are limited to a maximum of 100000000.
If we then take a look at the inline example, we have three addresses of importance:
-
dword_3DD9CA0
- This is technically at+8
of the real object, meaning the base is:dword_3DD9C98
-
dword_3DD9CB4
- Is+1C
of the basedword_3DD9C98
-
dword_3DD9CC8
- Is+30
of the basedword_3DD9C98
This helps show that the value usage is consistent. It also shows us what the real base pointer of the type is for any inline usages.
Any time the value is now accessed, it is then deallocated and rebuilt to keep the rand()
key constantly changing. By having the rand()
key constantly changing, this prevents a seed attack where if the value was only generated once, we could poison the srand()
seed call and force the allocation to always have an expected return when calling rand()
.
This does however create a bit of overhead as the game is constantly allocating and deallocating small blocks of memory for every single variable that is using this protected type.
The way the game is then updating the value for the score each frame is like this:
// Gets the 'Score' high-end protected value..
v4 = sub_6BA860((int)(v1 + 662)) * (unsigned __int64)(unsigned int)&unk_5F5E100;
// Gets the 'Score' low-end protected value..
v5 = (int)(v1 + 644);
v6 = v4 + sub_6BA860(v5);
// Combine the score into its full value and add any new amount to it..
v7 = __CFADD__((_DWORD)v6, *Block);
*Block += v6;
Block[1] += HIDWORD(v6) + v7;
// Calculate the remainder from the high-end/low-end parts..
v8 = *(_QWORD *)Block % (__int64)(unsigned int)&unk_5F5E100;
// Store the remainder in the 'Score' low-end protected value..
sub_6BA680(v5, 0, v8);
sub_6BA680(v5, 1, v8);
sub_6BA680(v5, 2, v8);
// Calculate the quotient from the high-end/low-end parts..
v9 = *(_QWORD *)Block / (__int64)(unsigned int)&unk_5F5E100;
// Store the quotient in the 'Score' high-end protected value..
sub_6BA680((int)(this + 662), 0, v9);
sub_6BA680((int)(this + 662), 1, v9);
sub_6BA680((int)(this + 662), 2, v9);
The game making use of (__int64)(unsigned int)&unk_5F5E100
here is just to represent the number 100000000
which is how much each part of the ‘Score’ value can hold as a maximum.
This is called constantly, every frame even if we have no new score to add to our current score as it is updating our displayed data as well.
Targeting The Weakness - Part 1 - Man In The Middle Attacking Value Requests
The first attack we can make on this anti-cheat setup is attacking the value request function sub_6BA860
.
Warning: While this attack method is ok for most of the games variables, it wont work on everything. Not all the values make use of this function, such as when inlines are used specifically. Because of that, this is not the best place to attack the anti-cheat. But, I still wanted to demonstrate how to do it anyway for users that wanted to see more than one method of attacking the protection. {: .notice–warning}
Even if you are a beginner into game hacking / reversing, it should be fairly obvious using logical reasoning as to what the weakness is here. Before the real value is returned, it is first checked and validated if it has been tampered with by this line:
if ( v1 != *v4 - *v3 )
As we reviewed above, this is checking the real value against the rand()
value result by subtracting the two values. This is effectively just doing:
if (real_value != ((real_value + rand_value) - rand_value))
So we can just remove the check and have it always be true. However, we want to make sure that we allow any inline lookups elsewhere to also be valid. So in order to do that we need to think further.
We know that the value is always expecting the real one to be compared against the two rand()
values. One of which holds the real value added to that rand()
value. Here is were we can also attack. Instead of having the values be masked by the rand()
, we can just remove the rand()
part altogether. By removing that value, we can turn the check into:
if (real_value != (real_value + 0) - 0)
Which will always be true.
The assembly of this function looks like this:
0054A860 - 80 79 43 00 - cmp byte ptr [ecx+43],00 { 0 } ; Checks and ensures the ProtectedValue is initialized.
0054A864 - 75 05 - jne 0054A86B ; Jumps if valid.
0054A866 - 33 C9 - xor ecx,ecx ; Returns 0..
0054A868 - 8B C1 - mov eax,ecx ; Returns 0..
0054A86A - C3 - ret ; Returns 0..
0054A86B - 8D 41 28 - lea eax,[ecx+28] ; Tests if the main randomizer value pointer is valid..
0054A86E - 85 C0 - test eax,eax ; ----
0054A870 - 75 05 - jne 0054A877 ; Jumps if the pointer is valid..
0054A872 - E9 196DD4FF - jmp 00291590 ; Jumps to an error handler when pointer is invalid..
0054A877 - 8B 51 30 - mov edx,[ecx+30] ; Get the pointer to the base rand() value..
0054A87A - 85 D2 - test edx,edx ; Tests if the pointer is valid..
0054A87C - 0F84 0E6DD4FF - je 00291590 ; Jumps to an error handler when pointer is invalid..
0054A882 - 8B 41 1C - mov eax,[ecx+1C] ; Get the pointer to the calculated rand() value.. (real_value + rand())
0054A885 - 85 C0 - test eax,eax ; Tests if the pointer is valid..
0054A887 - 74 E9 - je 0054A872 ; Jumps to an error handler when pointer is invalid..
0054A889 - 8B 00 - mov eax,[eax] ; Read the calculated rand() value..
0054A88B - 2B 02 - sub eax,[edx] ; Subtract from the base rand() value..
0054A88D - 8B 49 08 - mov ecx,[ecx+08] ; Read the real value..
0054A890 - 3B C8 - cmp ecx,eax ; Compare the real value to the subtracted result..
0054A892 - 74 D4 - je 0054A868 ; Jump to the return if equal/valid..
0054A894 - 6A 00 - push 00 { 0 } ; <Error prompt detecting a tampered value..>
0054A896 - 68 90A97300 - push 0073A990 { ("Failed") }
0054A89B - 68 B4A97300 - push 0073A9B4 { ("Falisified Parameter") }
0054A8A0 - 6A 00 - push 00 { 0 }
0054A8A2 - FF 15 5C137100 - call dword ptr [0071135C] { ->757634D0 } ; Calls MessageBoxA(...)
0054A8A8 - E9 B19D1A00 - jmp 006F465E ; Calls abort(...)
0054A8AD - CC - int 3
So before we make our patch, we need to be sure of a few things:
- We want to check and make sure the pointers are valid for editing, we can do the same check as the game with
lea eax,[ecx+28] > test eax, eax
- We want to remove the
rand()
value from both variables in+1C
and+30
.- We know that
+1C
still needs to hold the real value then if we are subtracting from 0, so we must update it still. - We know that
+30
can be 0 since we are no longer using therand()
value.
- We know that
With that, we can make our patch. For this example I am just going to use Cheat Engine’s auto-assembler for the proof of concept.
Here is the auto-assembler script to make this patch:
[ENABLE]
aobscanmodule(pGetProtectedValuePtr,Game.exe,8D 41 28 85 C0 75 ?? E9 ?? ?? ?? ?? 8B 51 30 85 D2)
alloc(cave,$1000,pGetProtectedValuePtr)
label(return)
label(skip)
cave:
// Original code, test if the pointers are valid to read from..
lea eax,[ecx+28]
test eax,eax
je skip
// M.I.T.M. - Reset the value object to have a rand() mask of 0..
push eax
push edx
mov eax,[ecx+8] // Get the original real value..
mov edx,[ecx+1C] // Get the first rand() pointer..
mov [edx],eax // Set the first rand() pointer value to the real value..
mov edx,[ecx+30] // Get the second rand() pointer..
mov [edx],0 // Set the second rand() pointer value to 0..
pop edx
pop eax
// Restore original code and check again to ensure flags are set properly..
lea eax,[ecx+28]
test eax,eax
skip:
jmp return
pGetProtectedValuePtr:
jmp cave
return:
registersymbol(pGetProtectedValuePtr)
[DISABLE]
pGetProtectedValuePtr:
db 8D 41 28 85 C0
unregistersymbol(pGetProtectedValuePtr)
dealloc(cave)
Now any variable that is reading its value will be seen as unprotected. Again, this is not perfect, so we’ll attack the allocator instead next.
Here are before/after pictures of the ‘Score’ low-end value with the M.I.T.M script turned off and then on:
Before:
After:
Targeting The Weakness - Part 2 - Man In The Middle Attacking Value Allocation/Creation
Since the game makes heavy use of the values in various parts that are inlined, not all usages will land up using the function that requests the protected value. Instead, we can attack the allocation function as all protected values do use that to setup their data and then update through it. This allows us to remove the randomizers altogether and just have the value always be treated as a ‘self-backed’ protected value instead of using the subtraction and rand()
nonsense.
For this setup, we will be attacking: sub_6BA680
which I showed above.
The assembly for this function is a bit overkill so I’ll break it into the parts that matter to us.
First, we want to attack each of the 4 instances of when rand()
is used for the actual value obfuscation. Those are:
// Part 1
0054A711 - E8 50AB1A00 - call 006F5266 - Calls rand()
0054A716 - 99 - cdq - Converts to quadword..
0054A717 - B9 01E1F505 - mov ecx,05F5E101 - Limits the random range to a maximum of 100000000
0054A71C - F7 F9 - idiv ecx - Limits the random range to a maximum of 100000000
0054A71E - 8B 03 - mov eax,[ebx] - Reads the pointer..
0054A720 - 89 10 - mov [eax],edx - Stores the random value..
// Part 2
0054A722 - E8 3FAB1A00 - call 006F5266 -
0054A727 - 99 - cdq -
0054A728 - B9 01E1F505 - mov ecx,05F5E101 -
0054A72D - F7 F9 - idiv ecx -
0054A72F - 8B 03 - mov eax,[ebx]] -
0054A731 - 89 50 04 - mov [eax+04],edx -
// Part 3
0054A734 - 8B 45 FC - mov eax,[ebp-04]
0054A737 - 83 F8 03 - cmp eax,03 { 3 }
0054A73A - 7C 15 - jl 0054A751
0054A73C - E8 25AB1A00 - call 006F5266
0054A741 - 99 - cdq
0054A742 - B9 01E1F505 - mov ecx,05F5E101 { (0) }
0054A747 - F7 F9 - idiv ecx
0054A749 - 8B 03 - mov eax,[ebx]
0054A74B - 89 50 08 - mov [eax+08],edx
// Part 4
0054A74E - 8B 45 FC - mov eax,[ebp-04]
0054A751 - 83 F8 04 - cmp eax,04 { 4 }
0054A754 - 7C 12 - jl 0054A768
0054A756 - E8 0BAB1A00 - call 006F5266
0054A75B - 99 - cdq
0054A75C - B9 01E1F505 - mov ecx,05F5E101 { (0) }
0054A761 - F7 F9 - idiv ecx
0054A763 - 8B 03 - mov eax,[ebx]
0054A765 - 89 50 0C - mov [eax+0C],edx
0054A768 - 8B 44 BE 14 - mov eax,[esi+edi*4+14]
0054A76C - 85 C0 - test eax,eax
0054A76E - 74 11 - je 0054A781
I’ve commented the first block, the rest are similar so you should be able to follow along as needed. The jist here is we want to replace each of the rand() calls and range limit handling with just the value 0. This way each randomizer call is always using 0 as the result. To do that, I made another M.I.T.M. attack script with Cheat Engine. This new one for the allocator function looks like this:
[ENABLE]
aobscanmodule(pAllocProtectedValuePtr1,Game.exe,E8 50 AB 1A 00 99 B9 01 E1 F5 05 F7 F9)
aobscanmodule(pAllocProtectedValuePtr2,Game.exe,E8 3F AB 1A 00 99 B9 01 E1 F5 05 F7 F9)
aobscanmodule(pAllocProtectedValuePtr3,Game.exe,E8 25 AB 1A 00 99 B9 01 E1 F5 05 F7 F9)
aobscanmodule(pAllocProtectedValuePtr4,Game.exe,E8 0B AB 1A 00 99 B9 01 E1 F5 05 F7 F9)
alloc(cave1,$1000,pAllocProtectedValuePtr1)
alloc(cave2,$1000,pAllocProtectedValuePtr2)
alloc(cave3,$1000,pAllocProtectedValuePtr3)
alloc(cave4,$1000,pAllocProtectedValuePtr4)
label(return1)
label(return2)
label(return3)
label(return4)
cave1:
mov eax, 0 // Force the rand() to 0.. (quotient)
mov edx, 0 // Force the rand() to 0.. (remainder)
jmp return1
cave2:
mov eax, 0 // Force the rand() to 0.. (quotient)
mov edx, 0 // Force the rand() to 0.. (remainder)
jmp return2
cave3:
mov eax, 0 // Force the rand() to 0.. (quotient)
mov edx, 0 // Force the rand() to 0.. (remainder)
jmp return3
cave4:
mov eax, 0 // Force the rand() to 0.. (quotient)
mov edx, 0 // Force the rand() to 0.. (remainder)
jmp return4
pAllocProtectedValuePtr1:
jmp cave1
nop 8
return1:
pAllocProtectedValuePtr2:
jmp cave2
nop 8
return2:
pAllocProtectedValuePtr3:
jmp cave3
nop 8
return3:
pAllocProtectedValuePtr4:
jmp cave4
nop 8
return4:
registersymbol(pAllocProtectedValuePtr1)
registersymbol(pAllocProtectedValuePtr2)
registersymbol(pAllocProtectedValuePtr3)
registersymbol(pAllocProtectedValuePtr4)
[DISABLE]
pAllocProtectedValuePtr1:
db E8 50 AB 1A 00 99 B9 01 E1 F5 05 F7 F9
pAllocProtectedValuePtr2:
db E8 3F AB 1A 00 99 B9 01 E1 F5 05 F7 F9
pAllocProtectedValuePtr3:
db E8 25 AB 1A 00 99 B9 01 E1 F5 05 F7 F9
pAllocProtectedValuePtr4:
db E8 0B AB 1A 00 99 B9 01 E1 F5 05 F7 F9
dealloc(cave1)
dealloc(cave2)
dealloc(cave3)
dealloc(cave4)
unregistersymbol(pAllocProtectedValuePtr1)
unregistersymbol(pAllocProtectedValuePtr2)
unregistersymbol(pAllocProtectedValuePtr3)
unregistersymbol(pAllocProtectedValuePtr4)
By doing this, we simply replace all the rand() setups of each part of the protected value and thus, create again, a self-backed protected value allowing us to easily edit the values.
Note: By doing this, we are still required to edit both the base real value and the protected value that is stored in +1C
.
Other Games, Similar Approach
So we’ve successfully attacked the protected value setup for this game. This same principle can be applied to many other games as well. A very common engine that lands up seeing this kind of usage now is games made with Unity. There are tons of asset-store based ‘anti-cheat’ solutions that offer very similar protected value solutions in the form of a custom C# type.
As an example of another game that uses a similar setup, the game Anima: The Reign Of Darkness
on Steam. It is written in Unity and makes use of a protected value type for various types. These include:
- ObscuredBool
- ObscuredByte
- ObscuredChar
- ObscuredDecimal
- ObscuredDouble
- ObscuredFloat
- ObscuredInt
- ObscuredLong
- ObscuredQuaternion
- ObscuredSByte
- ObscuredShort
- ObscuredString
- ObscuredUInt
- ObscuredULong
- ObscuredUShort
- ObscuredVector2
- ObscuredVector2Int
- ObscuredVector3
- ObscuredVector3Int
Each of these types all share the same setup and style of usage making patching them pretty easy. For example, ObscuredInt
looks like this:
public struct ObscuredInt : IObscuredType, IFormattable, IEquatable<ObscuredInt>, IComparable<ObscuredInt>, IComparable<int>, IComparable
{
[SerializeField]
private int currentCryptoKey;
[SerializeField]
private int hiddenValue;
[SerializeField]
private bool inited;
[SerializeField]
private int fakeValue;
[SerializeField]
private bool fakeValueActive;
private ObscuredInt(int value)
{
currentCryptoKey = GenerateKey();
hiddenValue = Encrypt(value, currentCryptoKey);
bool existsAndIsRunning = ObscuredCheatingDetector.ExistsAndIsRunning;
fakeValue = (existsAndIsRunning ? value : 0);
fakeValueActive = existsAndIsRunning;
inited = true;
}
public static int Encrypt(int value, int key)
{
return value ^ key;
}
public static int Decrypt(int value, int key)
{
return value ^ key;
}
public static ObscuredInt FromEncrypted(int encrypted, int key)
{
ObscuredInt result = default(ObscuredInt);
result.SetEncrypted(encrypted, key);
return result;
}
public static int GenerateKey()
{
return RandomUtils.GenerateIntKey();
}
public int GetEncrypted(out int key)
{
key = currentCryptoKey;
return hiddenValue;
}
public void SetEncrypted(int encrypted, int key)
{
inited = true;
hiddenValue = encrypted;
currentCryptoKey = key;
if (ObscuredCheatingDetector.ExistsAndIsRunning)
{
fakeValueActive = false;
fakeValue = InternalDecrypt();
fakeValueActive = true;
}
else
{
fakeValueActive = false;
}
}
public int GetDecrypted()
{
return InternalDecrypt();
}
public void RandomizeCryptoKey()
{
hiddenValue = InternalDecrypt();
currentCryptoKey = GenerateKey();
hiddenValue = Encrypt(hiddenValue, currentCryptoKey);
}
private int InternalDecrypt()
{
if (!inited)
{
currentCryptoKey = GenerateKey();
hiddenValue = Encrypt(0, currentCryptoKey);
fakeValue = 0;
fakeValueActive = false;
inited = true;
return 0;
}
int num = Decrypt(hiddenValue, currentCryptoKey);
if (ObscuredCheatingDetector.ExistsAndIsRunning && fakeValueActive && num != fakeValue)
{
KeepAliveBehaviour<ObscuredCheatingDetector>.Instance.OnCheatingDetected();
}
return num;
}
public static implicit operator ObscuredInt(int value)
{
return new ObscuredInt(value);
}
public static implicit operator int(ObscuredInt value)
{
return value.InternalDecrypt();
}
public static implicit operator ObscuredFloat(ObscuredInt value)
{
return value.InternalDecrypt();
}
public static implicit operator ObscuredDouble(ObscuredInt value)
{
return value.InternalDecrypt();
}
public static explicit operator ObscuredUInt(ObscuredInt value)
{
return (uint)value.InternalDecrypt();
}
public static ObscuredInt operator ++(ObscuredInt input)
{
return Increment(input, 1);
}
public static ObscuredInt operator --(ObscuredInt input)
{
return Increment(input, -1);
}
private static ObscuredInt Increment(ObscuredInt input, int increment)
{
int value = input.InternalDecrypt() + increment;
input.hiddenValue = Encrypt(value, input.currentCryptoKey);
if (ObscuredCheatingDetector.ExistsAndIsRunning)
{
input.fakeValue = value;
input.fakeValueActive = true;
}
else
{
input.fakeValueActive = false;
}
return input;
}
public override int GetHashCode()
{
return InternalDecrypt().GetHashCode();
}
public override string ToString()
{
return InternalDecrypt().ToString();
}
public string ToString(string format)
{
return InternalDecrypt().ToString(format);
}
public string ToString(IFormatProvider provider)
{
return InternalDecrypt().ToString(provider);
}
public string ToString(string format, IFormatProvider provider)
{
return InternalDecrypt().ToString(format, provider);
}
public override bool Equals(object obj)
{
if (obj is ObscuredInt)
{
return Equals((ObscuredInt)obj);
}
return false;
}
public bool Equals(ObscuredInt obj)
{
if (currentCryptoKey == obj.currentCryptoKey)
{
return hiddenValue.Equals(obj.hiddenValue);
}
return Decrypt(hiddenValue, currentCryptoKey).Equals(Decrypt(obj.hiddenValue, obj.currentCryptoKey));
}
public int CompareTo(ObscuredInt other)
{
return InternalDecrypt().CompareTo(other.InternalDecrypt());
}
public int CompareTo(int other)
{
return InternalDecrypt().CompareTo(other);
}
public int CompareTo(object obj)
{
return InternalDecrypt().CompareTo(obj);
}
}
The same idea applies here. We can attack the protection to remove the obfuscation. (Ignoring the other flaws/weaknesses that are obvious in this protection.)
While there is a bit more work involved here, the main things we’d want to attack are:
- Encrypt - Remove the xor and just return the value.
- Decrypt - Remove the xor and just return the value.
- InternalDecrypt - Replace the method body to just return the real value.
By edited just these three functions for each of the Obscured
types, you will effectively remove the obfuscation for all usages.
You don’t need to even edit the other functions in each of the types because they are all internally calling each other. So if the main 3 functions that actually deal with the value are patched, then in turn the other function usages will all be using the proper expected value and pass any checks. For games like this, you can use various means of editing/injecting into Mono and .NET processes.
Some nice frameworks for .NET/Mono patching are:
- Harmony - https://github.com/pardeike/Harmony
- Fody - https://github.com/Fody/Fody
- Mono.Cecil - https://github.com/jbevain/cecil
You can also use Cheat Engine’s built-in Mono tools for dealing with JIT manipulation and altering functions as you see fit.
Closing Thoughts
While this post does cover how to attack the anti-cheat of the specific game, the end result is not anything too useful, other than just being able to manipulate the values easier in Cheat Engine. For a more full-scale cheat/bypass, I’d opt. to make an injected hook that gets rid of the protected value setup altogether and patches the inline usages. However, for a simple trainer setup, you can just also have the injected hook properly edit both the real value and the rand()+real value addresses as needed any time the user wishes to cheat a specific value.
This post was more of an overview on how you can look at, think about, and attack these types of protections and show their weakness.
I hope you enjoyed the fairly long read / topic. :)
Comments