mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
191 lines
5.2 KiB
Python
191 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Keygen for Ragnarok.exe crackme based on bytecode analysis."""
|
|
|
|
import argparse
|
|
|
|
FNV_INIT = 0x811c9dc5
|
|
FNV_PRIME = 0x1000193
|
|
LCG_MULT = 0x41c64e6d
|
|
LCG_ADD = 0x3039
|
|
TARGET = 0x13371337cafebabe
|
|
MASK_64 = 0xFFFFFFFFFFFFFFFF
|
|
MASK_32 = 0xFFFFFFFF
|
|
|
|
|
|
def fnv1a_hash(username: str) -> int:
|
|
"""Compute FNV-1a hash of username.
|
|
|
|
Binary does: hash = (byte ^ hash) * prime
|
|
Note the XOR happens BEFORE multiply.
|
|
"""
|
|
hash_value = FNV_INIT
|
|
for char in username:
|
|
hash_value = ((ord(char) ^ hash_value) * FNV_PRIME) & MASK_32
|
|
return hash_value
|
|
|
|
|
|
def lcg_next(state: int) -> int:
|
|
"""Perform one LCG step."""
|
|
return (state * LCG_MULT + LCG_ADD) & MASK_32
|
|
|
|
|
|
def extract_coefficients(username: str) -> tuple:
|
|
"""Generate C1-C4 and PRNG offsets from username hash.
|
|
|
|
From FUN_140001bf0 decompilation:
|
|
- Two LCG steps per coefficient
|
|
- Coefficient = (state1 >> 16 & 0x7fff) | (state2 & 0x7fff0000)
|
|
- This creates a 30-bit value with bit 15 always 0
|
|
"""
|
|
state = fnv1a_hash(username)
|
|
|
|
# C1: two LCG steps, combine bits
|
|
state1 = lcg_next(state)
|
|
state2 = lcg_next(state1)
|
|
c1 = ((state1 >> 16) & 0x7fff) | (state2 & 0x7fff0000)
|
|
state = state2
|
|
|
|
# C2
|
|
state1 = lcg_next(state)
|
|
state2 = lcg_next(state1)
|
|
c2 = ((state1 >> 16) & 0x7fff) | (state2 & 0x7fff0000)
|
|
state = state2
|
|
|
|
# C3
|
|
state1 = lcg_next(state)
|
|
state2 = lcg_next(state1)
|
|
c3 = ((state1 >> 16) & 0x7fff) | (state2 & 0x7fff0000)
|
|
state = state2
|
|
|
|
# C4
|
|
state1 = lcg_next(state)
|
|
state2 = lcg_next(state1)
|
|
c4 = ((state1 >> 16) & 0x7fff) | (state2 & 0x7fff0000)
|
|
state = state2
|
|
|
|
# Additional PRNG values used as offsets in bytecode (p1-p4)
|
|
state = lcg_next(state)
|
|
p1 = (state >> 16) & 0x7fff
|
|
|
|
state = lcg_next(state)
|
|
p2 = (state >> 16) & 0x7fff
|
|
|
|
state = lcg_next(state)
|
|
p3 = (state >> 16) & 0x7fff
|
|
|
|
state = lcg_next(state)
|
|
p4 = (state >> 16) & 0x7fff
|
|
|
|
return (c1, c2, c3, c4, p1, p2, p3, p4)
|
|
|
|
|
|
def solve_serial(coeffs: tuple, s1: int, s2: int, s3: int) -> int:
|
|
"""Solve for S4 given coefficients and S1, S2, S3.
|
|
|
|
From VM trace, the actual equation is:
|
|
R0 = (C1 ^ S1 + p1) + ((C2 + S2) ^ p2) + (C3 - S3 + p3) + ((C4 ^ S4) ^ p4)
|
|
|
|
Each serial part is processed differently:
|
|
- S1: XOR with C1, then ADD p1
|
|
- S2: ADD to C2, then XOR with p2
|
|
- S3: SUB from C3, then ADD p3
|
|
- S4: XOR with C4, then XOR with p4
|
|
|
|
Solving for S4:
|
|
TARGET = partial + ((C4 ^ S4) ^ p4)
|
|
(C4 ^ S4) ^ p4 = TARGET - partial
|
|
C4 ^ S4 = (TARGET - partial) ^ p4
|
|
S4 = C4 ^ ((TARGET - partial) ^ p4)
|
|
"""
|
|
c1, c2, c3, c4, p1, p2, p3, p4 = coeffs
|
|
|
|
# Compute known contributions from S1, S2, S3
|
|
r1 = (c1 ^ s1) + p1
|
|
r2 = (c2 + s2) ^ p2
|
|
r3 = (c3 - s3 + p3)
|
|
|
|
partial = (r1 + r2 + r3) & MASK_64
|
|
|
|
# Solve for S4
|
|
needed = (TARGET - partial) & MASK_64
|
|
s4 = c4 ^ (needed ^ p4)
|
|
|
|
return s4
|
|
|
|
|
|
def verify_serial(username: str, s1: int, s2: int, s3: int, s4: int) -> tuple:
|
|
"""Verify serial produces target value. Returns (result, matches)."""
|
|
coeffs = extract_coefficients(username)
|
|
c1, c2, c3, c4, p1, p2, p3, p4 = coeffs
|
|
|
|
# Emulate actual VM computation
|
|
r1 = (c1 ^ s1) + p1
|
|
r2 = (c2 + s2) ^ p2
|
|
r3 = (c3 - s3 + p3)
|
|
r4 = (c4 ^ s4) ^ p4
|
|
|
|
r0 = (r1 + r2 + r3 + r4) & MASK_64
|
|
|
|
return r0, r0 == TARGET
|
|
|
|
|
|
def generate_serial(username: str) -> str:
|
|
"""Generate complete serial for given username."""
|
|
coeffs = extract_coefficients(username)
|
|
s1 = s2 = s3 = 0x1337
|
|
s4 = solve_serial(coeffs, s1, s2, s3)
|
|
return f"{s1:X}-{s2:X}-{s3:X}-{s4:X}"
|
|
|
|
|
|
def main():
|
|
"""CLI entry point."""
|
|
parser = argparse.ArgumentParser(description='Ragnarok.exe Keygen')
|
|
parser.add_argument('username', nargs='?', help='Username to generate serial for')
|
|
parser.add_argument('-v', '--verbose', action='store_true', help='Show debug info')
|
|
args = parser.parse_args()
|
|
|
|
if args.username:
|
|
username = args.username
|
|
else:
|
|
username = input("Enter username: ").strip()
|
|
|
|
if not username:
|
|
print("Error: Username cannot be empty")
|
|
return 1
|
|
|
|
serial = generate_serial(username)
|
|
print(f"\n{'='*50}")
|
|
print(f" Username: {username}")
|
|
print(f" Serial: {serial}")
|
|
print(f"{'='*50}")
|
|
|
|
# Verify the generated serial
|
|
s1 = s2 = s3 = 0x1337
|
|
coeffs = extract_coefficients(username)
|
|
s4 = solve_serial(coeffs, s1, s2, s3)
|
|
result, matches = verify_serial(username, s1, s2, s3, s4)
|
|
|
|
if args.verbose:
|
|
c1, c2, c3, c4, p1, p2, p3, p4 = coeffs
|
|
print(f"\n[Debug] FNV-1a hash: 0x{fnv1a_hash(username):08X}")
|
|
print(f"[Debug] C1=0x{c1:08X} C2=0x{c2:08X} C3=0x{c3:08X} C4=0x{c4:08X}")
|
|
print(f"[Debug] p1=0x{p1:04X} p2=0x{p2:04X} p3=0x{p3:04X} p4=0x{p4:04X}")
|
|
print(f"[Debug] S4=0x{s4:016X}")
|
|
print(f"[Debug] Result: 0x{result:016X}")
|
|
print(f"[Debug] Target: 0x{TARGET:016X}")
|
|
|
|
if matches:
|
|
print("\n[OK] Serial verified!")
|
|
else:
|
|
print(f"\n[FAIL] Serial verification failed!")
|
|
print(f" Expected: 0x{TARGET:016X}")
|
|
print(f" Got: 0x{result:016X}")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
sys.exit(main() or 0)
|