Stennen.github.io / Hacks / Drift Hunters
Final Release - DriftHuntersHack.exe | 16.0KiB (SHA256 checksum: 73b32d342c2edfd9f12b10ce9ecaee01c1e6225e6f7c1a409ed31c4a3d863187)
Part 2
Drift hunters is a car game created in Unity, and therefore it has supported for both PC, WebGL and mobile.
There is money in the game used to buy cars, and this value is obfuscated which makes it fun to change .
The money count is stored in the UnityEngine defined PlayerPrefs
class. This hack will only work on Windows, hence it's easier to access the values from here.
On Windows, the PlayerPrefs
data is stored in the registry, inside the key HKEY_CURRENT_USER\SOFTWARE\author\game\
, or specifically in this case, HKEY_CURRENT_USER\SOFTWARE\studionum43\Drift Hunters\
.
In the WebGL version, this is stored in the IndexedDB
feature.
Looking at the values of the key, we can see that they are obfuscated, both in names and likely in value as well. ?
The value names are likely encoded in some way, but we also need to take in account how Unity processes these value keys.
Let's take the value key gearbox_h2771454977
I found. The game accesses this value by simply referring to it as gearbox
. Unity actually appends that last bit of text (_h2771454977
) for some reason, however the game does not need to care about that.
I have tried to figure this out by simply traversing the values in the key and searching for the money count (DWORD -> BYTES
encoded), with no results.
So, instead I chose to monitor the changes of the keys. The money count increases when the user drifts with a car in-game.
import winreg
def collect_vals():
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"SOFTWARE\studionum43\Drift Hunters", 0, winreg.KEY_READ)
result = ""
for i in range(0, winreg.QueryInfoKey(key)[1]):
val = winreg.EnumValue(key, i)
name, val, typ = val
result += "%s %s\n" % (name, val)
winreg.CloseKey(key)
return result
def dump_values(outFile):
f = open(outFile, "w")
f.write(collect_vals())
f.close()
dump_values("before_run.txt")
# ^^
# Run the game, gain money, then comment the line above, to then uncomment the following line
# V
#dump_values("after_run.txt")
Output looks something like this:
xtf={phzi4_h1210814998 b'0\x00'
xtf={phzi4l(._h744875580 b'0\x00'
xtf=wf(epx_h1035949798 b'0\x00'
xtf=wf(epxl(._h151408972 b'0\x00'
xtf=4qfwrtxh_h1835812432 b'm>>\x00'
...
Run the code (Python), then run the game, and drift a little to gain money, then exit the game
and then comment (put a #
infront of the line) the dump_values("before_run.txt")
line,
and remove the #
from the #dump_values("after_run.txt")
line.
Detecting changes made can be done by creating a dict from these two .TXT
files, then iterating over the items of the dict and checking for news/changed values.
def get_changed_keys():
with open("before_run.txt", "r") as f:
bef = f.readlines()
with open("after_run.txt", "r") as f:
after = f.readlines()
bef_dc = {}
after_dc = {}
for b in bef:
bef_dc[b.split()[0]] = b.split()[1]
for b in after:
after_dc[b.split()[0]] = b.split()[1]
for key, val in after_dc.items():
if key in bef_dc:
if val != bef_dc[key]:
print("Changed key", key, "from", bef_dc[key], "to", val)
else:
print("New key", key, "created with value", val)
get_changed_keys()
The output of the code I got was the following:
Changed key UnityGraphicsQuality_h1669003810 from 0 to 1
Changed key unity.player_session_elapsed_time_h192694777 from b'60520\x00' to b'144488\x00'
Changed key Graphics_h1352112080 from 0 to 1
Changed key tn(;pflrdp;_h1871615787 from b'?}}>m\x00' to b'?,,m<\x00'
Changed key unity.player_session_background_time_h123860221 from b'1700331794706\x00' to b'1700332918921\x00'
This means that the following keys were changed (converted to game referencable form):UnityGraphicsQuality
, unity.player_session_elapsed_time
, unity.player_session_background_time
, and tn(;pflrdp;
.
- The UnityGraphicsQuality
key is the quality of which the game will be running, so we can safely assume it's not related to the in game currency count.
- The unity.player_session_elapsed_time
key is the total time elapsed of the user being in the game in milliseconds, so we can ignore it.
- The unity.player_session_background_time
key also seems somewhat related to the amount of time the user has been in a game, so we can safely ignore this one as well.
The exclusion of these keys brings us down to only one possible key for being the count of the money; the tn(;pflrdp;
value. Now we need to figure out what the value of this key value is encoded, and how we can re-encode it as another value.
Note: At the time I ran the last piece of code, I had the game money go from 25501 to 26619.
Note: I will be using ILSpy to decompile the game.
In ILSpy, click on File -> Open, and navigate to the directory where the Drift Hunters executable is stored.
Then navigate into Drift Hunters_Data
-> Managed
and select the Assembly-CSharp.dll file and click Open
.
To see the defenitions made, click on + next to the file's name and then click on the + on the + {} -
.
You shall now have been greeted by all of the defenitions made by Drift Hunters.
To find the data loading part, navigate into the General
class, and expand the Start
function.
Here you can see the basic functionality of the game setting up the data for a first time player.
In case the 'havefile'
key is not present in the PlayerPrefs
, it will add the car 'AE86'
to the player's inventory and initialize the money count to 25,000
.
Notice how the PLAYERMONEY
property is not accessed from the Unity-Side defined PlayerPrefs, but instead the internally defined PlayerValues
class.
CTRL + Click PlayerValues
to open up the class decompiled.
First, let's take a look at the playerMoney()
function.
public static int playerMoney()
{
return Convert.ToInt32(getValue("PLAYERMONEY"));
}
Great! We have now found out the type of the money, a signed int
, or a 32-bit integer that can be both negative and positive.
Now let's CTRL + CLICK the getValue
function to retrieve its definition.
public static string getValue(string key)
{
string text = string.Empty;
int length = key.Length;
for (int i = 0; i < length; i++)
{
text += codeMap[key[i]];
}
string @string = PlayerPrefs.GetString(text);
string text2 = string.Empty;
int length2 = @string.Length;
for (int j = 0; j < length2; j++)
{
text2 += decodeMap[@string[j]];
}
return text2;
}
Nice! We have now found out how both the name of the key and how the value of it is encoded!
As you can see, it's using its own table for encoding/decoding. Let's CTRL + Click codeMap
to get the encoding map.
private static Dictionary<char, char> codeMap = new Dictionary<char, char>
{
{ 'A', '(' },
{ 'B', 'w' },
{ 'C', 'u' },
{ 'D', 'a' },
{ 'E', 'p' },
{ 'F', 'o' },
{ 'G', 'z' },
{ 'H', 'i' },
{ 'I', 'h' },
{ 'J', 's' },
{ 'K', 'e' },
{ 'L', 'n' },
{ 'M', 'l' },
{ 'N', 'd' },
{ 'O', 'r' },
{ 'P', 't' },
{ 'Q', 'y' },
{ 'R', 'f' },
{ 'S', 'x' },
{ 'T', '4' },
{ 'U', 'q' },
{ 'V', 'b' },
{ 'W', '{' },
{ 'X', '.' },
{ 'Y', ';' },
{ 'Z', ']' },
{ '0', '>' },
{ '1', 'm' },
{ '2', '?' },
{ '3', '0' },
{ '4', ':' },
{ '5', '}' },
{ '6', ',' },
{ '7', '+' },
{ '8', '-' },
{ '9', '<' },
{ ',', ')' },
{ '|', '[' },
{ '_', '=' },
{ '{', '1' },
{ '}', '7' },
{ '-', '*' },
{ '.', '^' }
};
Nice! This obviously means that the decodeMap
map is the same as this but key-value swapped.
encode_table = {
'A': '(', 'B': 'w', 'C': 'u', 'D': 'a', 'E': 'p', 'F': 'o', 'G': 'z',
'H': 'i', 'I': 'h', 'J': 's', 'K': 'e', 'L': 'n', 'M': 'l', 'N': 'd',
'O': 'r', 'P': 't', 'Q': 'y', 'R': 'f', 'S': 'x', 'T': '4', 'U': 'q',
'V': 'b', 'W': '{', 'X': '.', 'Y': ';', 'Z': ']', '0': '>', '1': 'm',
'2': '?', '3': '0', '4': ':', '5': '}', '6': ',', '7': '+', '8': '-',
'9': '<', ',': ')', '|': '[', '_': '=', '{': '1', '}': '7', '-': '*',
'.': '^'
}
Here is the Python version of the codeMap
map.
We can easily make the decode table of off this using this:
decode_table = {val: key for key,val in encode_table.items()}
Using this we can easily port encoding/decoding functions into another language, such as Python in this example:
def drifthunters_decrypt(s):
res = ''
for c in s:
if c == '\x00':
return res
res += decode_table[c]
return res
def drifthunters_encrypt(s):
res = ''
for c in s:
res += encode_table[c]
return res + '\x00'
Let's try it:
>> drifthunters_encrypt("PLAYERMONEY")
tn(;pflrdp;<0x00>
>> drifthunters_decrypt("tn(;pflrdp;\x00")
PLAYERMONEY
As you can see it works flawlessly. Now, we need to find the right value name in the key.
def get_encrypted_valuekey(val):
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'SOFTWARE\studionum43\Drift Hunters', 0, winreg.KEY_READ)
enc_val = drifthunters_encrypt(val)[:-1] # exclude null terminator
for i in range(0, winreg.QueryInfoKey(key)[1]):
val = winreg.EnumValue(key, i)
name, val, typ = val
if name.startswith(enc_val + '_'):
winreg.CloseKey(key)
return drifthunters_decrypt(val.decode('ascii'))
winreg.CloseKey(key)
>> get_encrypted_valuekey("PLAYERMONEY")
26619
Great. We have now successfully retrieved the count of in game money, but what about changing it?
It's as simple as following:
def set_encrypted_valuekey_val(keyname, val):
key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r'SOFTWARE\studionum43\Drift Hunters', 0, winreg.KEY_READ | winreg.KEY_WRITE)
enc_val = drifthunters_encrypt(keyname)[:-1] # exclude null terminator
for i in range(0, winreg.QueryInfoKey(key)[1]):
v = winreg.EnumValue(key, i)
name, _, typ = v
if name.startswith(enc_val + '_'):
winreg.SetValueEx(key, name, 0, typ, drifthunters_encrypt(str(val)).encode())
winreg.CloseKey(key)
return
winreg.CloseKey(key)
raise FileNotFoundError
Now let's try it:
>> get_encrypted_valuekey("PLAYERMONEY")
26619
>> set_encrypted_valuekey_val("PLAYERMONEY", 69)
None
>> get_encrypted_valuekey("PLAYERMONEY")
69
Great! Now for the moment of truth, see if the money count actually changed...
It's safe to say that it did!
Come back for Part 2, when this utility will be packed into a GUI and converted to C!
Part 2