Anyways, I'll make it short and sweet. The TLDR is: it's the same as C1.
It's a true team effort where everyone contributed some useful piece, be it software, reverse engineering knowledge or anything else. It's heartwarming to see people come together for a game they're passionate about. Especially a shout out to Robin_Jadoul, who put a SMT solver program together in Python with z3 just to help crack the hashes in reasonable time.
Cheats are stored in the balls 0059098c
At this memory location is a table of sorts, presumably much like the table that can be found in the Dethrace project. https://github.com/dethrace-labs/dethra ... c#L79-L122
(Thanks to jeff-1amstudios for pointing me here!). There are two parts of a hash called code and code2, a function and an argument for said function.
Code and code2 and why they matter
The table contains code & code2, these can be seen as hashes that are calculated as the user presses keys on the keyboard. We all know if you wait 1 second, whatever cheat you were typing, you have to start over. C2 uses the same logic for calculating these hashes. This algorithm (which is batshit insane) can be found here:
https://github.com/dethrace-labs/dethra ... put.c#L237
So, the cheat table simply stores the expected hashes for every cheat. That's how you link "WETWET" or any other string to an actual cheat.
You press buttons on your keyboard. As you do so, code & code2, which are essentially hashes derived from your keyboard input, get calculated. These values for code & code2 get compared with code & code 2 in the cheats table. If there's a match, huzzah you're a cheating bastard.
What this means for Carmageddon 2
The same algorithm from C1/Dethrace can be used to translate keypresses into code & code2 hashes for C2. The resulting values can then be searched at 0059098c where the cheat table is located. It presumably uses the same system as C1/Dethrace, where the code & code2 values are mapped to cheats.
Cesm had actually found this cheat table in the same location in the past (if you want to find it easier, search for the 'Cheating off' string, then scroll up a bit). C2 Cheat Editor, which Cesm had made, basically moved blocks around in this cheat table, essentially swapping cheats around. But now we know what the actual data blocks contain; they contain the code and code2 hashes, plus a function and an argument for said function. Assuming it's just like C1's table in the first link of this post, at least, but it sure does look like it.
As a result, in theory it should be possible to press any number of keys in C1/Dethrace, grab the resulting code and code2 values, then apply them to the C2 binary (I think they're stored in reverse). That way you'd end up with custom cheat strings. This hasn't actually been tried yet, however. So far, we've just been able to succesfully find matches in the C2 binary for these hashes based off of C2 cheat strings using the C1 code/code2 hashing algorithm.
What's next?
Espyo is writing a brute forcer because he wants to find the Steel Gonad O' Death's keystrokes. There is the problem that the string could be any length and there could be hash collisions, making searching more difficult.
We could use means of elimination to exclude all known cheats from the 0059098c table, leaving just the Steel Gonad O' Death's hash. Then we could attempt brute forcing and maybe dictionary attacking the resulting string to find the possible original cheat string. That is, assuming the Steel Gonad O' Death is inside the cheat table to begin with.
This has been a fun first journey for me into reverse engineering. I want to thank Espyo, Toshiba-3, Cesm, jeff-1amstudios, Trent, maarten and anyone else who contributed.
I'll leave you with 2 attachments which are a hash calculator made by Espyo, as well as the hashes/functions/arguments for C2 that Espyo has uncovered thus far, so big props to him for making that!
Edit: maarten hinted that the cheat database may actually start at 00590970, meaning the CSV is missing 3 entries
Edit 2: Steel gonad's codes are 4B054B60 and 6B6736CB. If anyone wants to try their hand at cracking them using Espyo's/Dethrace's algorithm, feel free. The problem is that a brute-force approach scales exponentially.. This is to be researched ><
Edit 3: If you play with the hash calculation, I found that the first part of the hash goes up the longer the string. Because of this, I could determine the string is at least 18, probably 19+ characters in length. Smaller strings will never get you to '6B' at the beginning. The second part of the hash also moves in a linear fashion (higher letters = higher last hash). The problem is that the two are interdependent, but surely this property of the hashes can be exploited?
Edit 4: Robin_Jadoul from the DECIPHER Discord server managed to find some strings that map to the Gonad hashes:
(17) ezpzkballxxewazon (as noted on the wiki)
(18) vruftjjethasxmdlbg <- Verified Gonad cheat to work ingame! Thanks Espyo for checking!
(16) tenzxhlkyqtbrvyo <- Verified Gonad cheat to work ingame! Thanks Espyo for checking!
(15) uwsqyzhxtpduvyn <- Verified Gonad cheat to work ingame! Verified by myself/Espyo!
(16) dzzkcakyyuomxtyn
(16) tenzxhlkyqtbrvyo
(17) nmqyufpfxfrjcmgwu
(17) mqdsqhkyqmkemdyut
(17) kayymenmhpkpfmwsx
(17) imjmhtwsagxekmwvv
(17) usjlwcguwokdsrjxc
(17) cdtezggqqmlrfyyym
(17) lrqbkoojyykgkgwos
(17) vunkrsgzyyhdqchan
(17) lwxwisnccsulqckye
(18) oeyodtchssqcemniew
(18) uboyihsaaecyyieqqo
(18) bgvlxqoapesyggmbom
(18) jzyghdrckwokenkipf
(18) kfbjrbeaqmmmdryszs
(18) qpheeokaqkxskmxeik
(18) vvxykpcjmsiqkgahac
(18) brbqckwrwsaovgfdze
(18) yabaiiakzkyokmtyap
(18) nhdxpknewatdebtkzn
Edit 5: Here's the program that Robin_Jadoul had put together to crack the hashes. I rewarded him for donating his knowledge to put this nifty thing together!!!
Version w/ optional crib support (search constraint):
Code: Select all
# Python 3.10 with pip install z3-solver
from z3 import *
from math import floor, ceil
code2_target = 0xa11ee75d
code_target = 0xf805eddd
def length_bounds_estimates():
sum = code_target >> 21
return (floor(sum / (22 + 25)), ceil(sum / 22))
def char_to_val(x):
return ord(x.lower()) - ord("a") + 22
def step(code, code2, sum, inp):
sum += inp
code += (inp << 11)
code = LShR(code, 17) + (code << 4)
code2 = LShR(code2, 29) + inp*inp + (code2 << 3)
return (code, code2, sum)
def final(code, code2, sum):
return (LShR(code, 11) + (sum << 21), code2)
def main(LEN, crib):
for offset in range(LEN - len(crib) + 1):
keys = [z3.BitVec(f"k_{i}", 32) for i in range(LEN)]
s = Solver()
for k in keys:
s.add(k >= 22)
s.add(k < 22 + 26)
code = code2 = sum = BitVecVal(0, 32)
for k in keys:
code, code2, sum = step(code, code2, sum, k)
for c, k in zip(crib, keys[offset:]):
s.add(k == char_to_val(c))
code, code2 = final(code, code2, sum)
s.add(code == code_target)
s.add(code2 == code2_target)
found = False
while (t := s.check()) == sat:
print(f"Crib '{crib}' at offset {offset}, length {LEN}: ", end="")
print("".join(chr(s.model()[k].as_long() + ord('a') - 22) for k in keys))
s.add(Or(*[k != s.model()[k].as_long() for k in keys]))
found = True
print(f"Crib '{crib}' at offset {offset}, length {LEN}: {t}")
if not crib:
break # don't needlessly loop when no crib
if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} length [crib]")
exit(1)
print(f"Length bound estimates min/max: {length_bounds_estimates()}")
main(int(sys.argv[1]), (sys.argv + [""])[2])
Code: Select all
# Python 3.10 with pip install z3-solver
from z3 import *
code2_target = 0x6B6736CB
code_target = 0x4B054B60
for LEN in range(12, 29):
keys = [z3.BitVec(f"k_{i}", 32) for i in range(LEN)]
def step(code, code2, sum, inp):
sum += inp
code += (inp << 11)
code = LShR(code, 17) + (code << 4)
code2 = LShR(code2, 29) + inp*inp + (code2 << 3)
return (code, code2, sum)
def final(code, code2, sum):
return (LShR(code, 11) + (sum << 21), code2)
s = Solver()
for k in keys:
s.add(k >= 22)
s.add(k < 22 + 26)
code = code2 = sum = BitVecVal(0, 32)
for k in keys:
code, code2, sum = step(code, code2, sum, k)
code, code2 = final(code, code2, sum)
s.add(code == code_target)
s.add(code2 == code2_target)
found = False
while (t := s.check()) == sat:
print(f"Potential cheat found of length {LEN}: ", end="")
print("".join(chr(s.model()[k].as_long() + ord('a') - 22) for k in keys))
s.add(Or(*[k != s.model()[k].as_long()]))
found = True
else:
if not found:
print("length", LEN, t)
I'm putting a list together of cribs that might be useful:
test - from testes, testicles, etc.. many puns can be made from it - Thank you Trent!
ball - from balls, ballz ?
bollock - fitting UK word for balls
Edit 7: Everything with length 17, 18 (no crib) is exhausted, so no need to look for strings with length 17, 18 anymore. (actually this isn't certain if they were fully exhausted, as Robin didn't see the crib strings appear in the non-crib one)