8.0 KiB
Ragnarok Keygen Writeup
Crackme: Ragnarok Author: (crackmes.one author) Difficulty: Custom VM + Anti-Debug Tools Used: Ghidra, Python, ghidra-cli
Overview
Ragnarok is a keygen challenge featuring:
- Yggdrasil: A custom stack-based virtual machine with 12 opcodes
- Heimdall: Anti-debugging protection that subtly corrupts execution
- Dynamic validation: Bytecode generated based on username
- Linear algebra: Under-constrained equation system
The goal is to create a keygen that generates valid serial keys for any username without patching or brute force.
Initial Analysis
Binary Information
- File: Ragnarok.exe (164 KB, Windows PE 64-bit)
- Functions: 571 identified by Ghidra
Finding Key Functions
I started by searching for interesting strings:
[Forge] > Enter Name:
[Forge] > Enter Key:
[*] Consult the Norns...
[VM] Stack Overflow!
The ribbon tightens! %s has forged Gleipnir!
The [VM] Stack Overflow! string led me to the VM dispatcher at 0x140001850.
Program Flow
Tracing from main (0x140001010):
- Display banner/story
- Read username into buffer
- Read serial key into buffer
- Call validation function
0x140001e10 - Display success or failure message
The Yggdrasil Virtual Machine
VM Structure (0x140001850)
The VM uses a simple architecture:
- 9 registers: R0-R8 (64-bit each)
- Stack: 1024 entries
- Instruction pointer and stack pointer
Opcode Table
| Opcode | Mnemonic | Format | Description |
|---|---|---|---|
| 0x00 | HALT | 00 |
Stop execution |
| 0x01 | LOAD | 01 reg imm64 |
Load 64-bit immediate into register |
| 0x02 | MOV | 02 dst src |
Copy register to register |
| 0x03 | ADD | 03 dst src |
dst += src |
| 0x04 | SUB | 04 dst src |
dst -= src |
| 0x05 | XOR | 05 dst src |
dst ^= src |
| 0x06 | MUL | 06 dst src |
dst *= src |
| 0x07 | PUSH | 07 reg |
Push register to stack |
| 0x08 | POP | 08 reg |
Pop stack to register |
| 0x09 | JMP | 09 addr64 |
Unconditional jump |
| 0x0A | JZ | 0A addr64 |
Jump if R0 == 0 |
| 0x0B | NOP | 0B |
No operation |
Serial Validation (0x140001e10)
The validation function:
-
Parses serial as 4 hex values separated by non-alphanumeric characters
- Example:
AAAA-BBBB-CCCC-DDDD - Each part can be up to 16 hex digits (64-bit)
- Example:
-
Initializes VM context via
0x140001b90- R0-R8 set to 0
- Serial parts loaded into R5-R8
-
Generates bytecode via
0x140001bf0based on username -
Executes VM via
0x140001850 -
Checks result: Success if
R0 == 0x13371337CAFEBABE
Bytecode Generator (0x140001bf0)
PRNG Seeding
The username is hashed using FNV-1a:
uint32_t fnv1a_hash(char *username) {
uint32_t hash = 0x811c9dc5; // FNV offset basis
while (*username) {
hash = (*username ^ hash) * 0x1000193; // FNV prime
username++;
}
return hash;
}
LCG PRNG
The hash seeds a Linear Congruential Generator:
uint32_t lcg_next(uint32_t state) {
return state * 0x41c64e6d + 0x3039;
}
Coefficient Extraction
Four coefficients (C1-C4) are generated, each from two LCG steps:
state1 = lcg_next(state);
state2 = lcg_next(state1);
coefficient = ((state1 >> 16) & 0x7fff) | (state2 & 0x7fff0000);
This creates a 30-bit value with bit 15 always 0.
Four additional PRNG values (p1-p4) are extracted:
state = lcg_next(state);
p_value = (state >> 16) & 0x7fff;
Generated Bytecode Structure
The bytecode performs these operations:
LOAD R1, C1 ; Load coefficient 1
LOAD R2, C2 ; Load coefficient 2
LOAD R3, C3 ; Load coefficient 3
LOAD R4, C4 ; Load coefficient 4
XOR R1, R5 ; R1 = C1 ^ S1 (serial part 1)
LOAD R0, p1 ; Load PRNG offset
ADD R1, R0 ; R1 = (C1 ^ S1) + p1
ADD R2, R6 ; R2 = C2 + S2
LOAD R0, p2
XOR R2, R0 ; R2 = (C2 + S2) ^ p2
SUB R3, R7 ; R3 = C3 - S3
LOAD R0, p3
ADD R3, R0 ; R3 = (C3 - S3) + p3
XOR R4, R8 ; R4 = C4 ^ S4
LOAD R0, p4
XOR R4, R0 ; R4 = (C4 ^ S4) ^ p4
MOV R0, R1 ; Start accumulation
ADD R0, R2 ; R0 = R1 + R2
ADD R0, R3 ; R0 = R0 + R3
ADD R0, R4 ; R0 = R0 + R4
HALT
The Equation
From the bytecode analysis, the final equation is:
R0 = (C1 ^ S1 + p1) + ((C2 + S2) ^ p2) + (C3 - S3 + p3) + ((C4 ^ S4) ^ p4)
Where:
- C1-C4: PRNG-derived coefficients (from username)
- S1-S4: Serial parts (user input)
- p1-p4: PRNG-derived offsets
- Target:
R0 == 0x13371337CAFEBABE
Solving for S4
Since we have 4 unknowns and 1 equation, we can fix S1, S2, S3 and solve for S4:
TARGET = partial + ((C4 ^ S4) ^ p4)
where partial = (C1 ^ S1 + p1) + ((C2 + S2) ^ p2) + (C3 - S3 + p3)
Solving:
(C4 ^ S4) ^ p4 = TARGET - partial
C4 ^ S4 = (TARGET - partial) ^ p4
S4 = C4 ^ ((TARGET - partial) ^ p4)
Heimdall Anti-Debug (0x140001fc0)
The anti-debug system uses three checks:
- IsDebuggerPresent() - Windows API
- Timing check - rdtsc before/after a loop, fails if > 100000 cycles
- PEB.BeingDebugged - Direct PEB flag check
If any check triggers, Heimdall injects additional bytecode:
LOAD R5, 0xBADF00D
ADD R0, R5
This corrupts the calculation, making the serial fail even if mathematically correct.
Bypass: Static analysis avoids triggering Heimdall entirely.
Keygen Implementation
Python Keygen
#!/usr/bin/env python3
"""Keygen for Ragnarok.exe crackme."""
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:
"""FNV-1a hash of username."""
h = FNV_INIT
for c in username:
h = ((ord(c) ^ h) * FNV_PRIME) & MASK_32
return h
def lcg_next(state: int) -> int:
"""LCG PRNG step."""
return (state * LCG_MULT + LCG_ADD) & MASK_32
def extract_coefficients(username: str) -> tuple:
"""Extract C1-C4 and p1-p4 from username."""
state = fnv1a_hash(username)
coeffs = []
for _ in range(4):
s1 = lcg_next(state)
s2 = lcg_next(s1)
coeffs.append(((s1 >> 16) & 0x7fff) | (s2 & 0x7fff0000))
state = s2
prng_vals = []
for _ in range(4):
state = lcg_next(state)
prng_vals.append((state >> 16) & 0x7fff)
return tuple(coeffs + prng_vals)
def generate_serial(username: str) -> str:
"""Generate valid serial for username."""
c1, c2, c3, c4, p1, p2, p3, p4 = extract_coefficients(username)
# Fix S1, S2, S3
s1 = s2 = s3 = 0x1337
# Compute partial sum
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 f"{s1:X}-{s2:X}-{s3:X}-{s4:X}"
if __name__ == "__main__":
import sys
username = sys.argv[1] if len(sys.argv) > 1 else input("Username: ")
print(f"Serial: {generate_serial(username)}")
Sample Output
| Username | Serial |
|---|---|
| test | 1337-1337-1337-13371336A65FDE72 |
| Admin | 1337-1337-1337-133713373C0E7ACF |
| Odin | 1337-1337-1337-133713370BCC283D |
All serials verified working in Ragnarok.exe.
Conclusion
The key insights for solving Ragnarok:
- Static analysis avoids Heimdall anti-debug entirely
- The VM is straightforward once opcodes are identified
- The equation uses mixed operations (XOR, ADD, SUB) per serial part
- Under-constrained system allows fixing 3 values and solving for 1
- Careful bytecode tracing reveals the exact computation
The Norse mythology theme was a nice touch - from Yggdrasil (the world tree/VM) to Heimdall (the watchful guardian/anti-debug) to forging Gleipnir (the unbreakable chain/valid serial).
Odin would be proud.
Files
keygen.py- Python keygen scriptwriteup.md- This writeup