The Germinator
May 18, 2026
The Germinator
Overview
The Germinator was a reverse-engineering challenge built around a stripped x86-64 Rust ELF named germinator. Running it locally produced a license failure, but the post-flag hint explained the core issue: the embedded license was valid, just not compatible with the local machine profile.
The solve had two parts:
- dump the embedded license JSON and read the first flag from it
- make the runtime fingerprint match the licensed machine profile so the original success path printed the second flag
Concrete flags are redacted in this public version.
Initial Recon
Identify the binary:
file downloads/germinator
Result:
downloads/germinator: ELF 64-bit LSB pie executable, x86-64, dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped
Running it normally failed the license check:
./downloads/germinator
Output:
Germinator 0.1.2b - Copyright (c) MONSATAN Corp
Validating environment...
License is invalid. Please contact your MONSATAN dealer for assistance.
Useful strings exposed both the success and failure paths:
strings -t x -a downloads/germinator | rg 'License is invalid|License is valid|Validating environment|SUPERSEED'
Important offsets included:
9130 %License is valid. Ignition sequence:
a8cd Validating environment...
a8e7 License is invalid. Please contact your MONSATAN dealer for assistance.
Locating The Hash Routine
The validation flow called a hash-like routine at relative address 0x3c050. Because the binary was PIE, the runtime address depended on the load base. In the solve environment, GDB mapped it at 0x555555554000, making the routine address 0x555555590050.
Breaking on that routine and printing arguments showed two interesting inputs:
gdb -q -batch \
-ex 'set debuginfod enabled off' \
-ex 'starti' \
-ex 'b *0x555555590050' \
-ex 'continue' \
-ex 'printf "call1 rsi=%p rdx=%llu r9=%#llx\n", $rsi, $rdx, $r9' \
-ex 'x/s $rsi' \
-ex 'continue' \
-ex 'printf "call2 rsi=%p rdx=%llu r9=%#llx\n", $rsi, $rdx, $r9' \
-ex 'x/s $rsi' \
--args ./downloads/germinator
The first call hashed the local machine fingerprint. The second call hashed the embedded license JSON.
The mode values were useful:
r9 = 0x61a8: machine fingerprint inputr9 = 0x7fe1: embedded license input
Flag 1: Dumping The Embedded License
The second hash call pointed directly at the embedded license JSON. A GDB script dumped it:
gdb -q -batch -x scripts/dump_license_flag1.gdb --args ./downloads/germinator
The JSON contained a licensed machine profile, a validity window, salt/signature fields, and a flag field:
{
"info": {
"os": "Debian GNU/Linux",
"hostname": "63400bb89fc4",
"ram_count": 6597069766656,
"macs": ["00:00:00:00:00:00", "92:73:ed:11:94:f4"],
"cpu": "AWS Graviton4 192-Core Processor"
},
"from": "2026-05-16T00:00:00Z",
"to": "2026-05-19T23:59:59Z",
"flag": "[redacted germinator flag 1]",
"salt": [183, 188, 79, 255, 191, 76, 247, 83, 213, 63, 113, 162],
"sig": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
}
The first flag was simply the license flag field.
Understanding The Hint
The embedded license expected this machine profile:
{"os":"Debian GNU/Linux","hostname":"63400bb89fc4","ram_count":6597069766656,"macs":["00:00:00:00:00:00","92:73:ed:11:94:f4"],"cpu":"AWS Graviton4 192-Core Processor"}
A normal local run produced a completely different host profile, such as an Arch Linux laptop with different RAM, MAC addresses, hostname, and CPU. That mismatch explained why the license failed locally even though it was still valid for the target profile.
The firmware collected profile data from normal Linux sources: hostname, OS release data, CPU and memory information, and network interface data.
Failed Patch Attempt
A tempting shortcut was to patch the branch that printed License is invalid:
39791: 45 38 ee cmp r14b,r13b
39794: 0f 85 28 01 00 00 jne 398c2
398c2: lea rdi,[rip+...] # "License is invalid..."
NOPing the branch avoided the visible failure, but the program then printed:
Something went wrong :(
That proved the check was not cosmetic. Later code needed the correct derived state to decrypt or parse the ignition payload. Skipping the branch did not create the required key material.
Flag 2: Runtime Input Replacement
The important discovery was that the firmware called the same machine-fingerprint hashing mode more than once:
call1 r9=0x61a8 rdx=<host-dependent> # local machine profile
call2 r9=0x7fe1 rdx=406 # embedded license JSON
call3 r9=0x61a8 rdx=<host-dependent> # local machine profile again
Overriding only the first machine-profile call got past the visible license error, but the third call still hashed the real local profile and the ignition path failed. The correct solve was to replace both r9 = 0x61a8 inputs with the licensed info JSON while leaving the r9 = 0x7fe1 license hash untouched.
The GDB solve script did that:
set debuginfod enabled off
starti
b *0x555555590050
continue
printf "machine hash call 1: rsi=%p rdx=%llu r9=%#llx\n", $rsi, $rdx, $r9
set $rsi=(char*)strdup("{\"os\":\"Debian GNU/Linux\",\"hostname\":\"63400bb89fc4\",\"ram_count\":6597069766656,\"macs\":[\"00:00:00:00:00:00\",\"92:73:ed:11:94:f4\"],\"cpu\":\"AWS Graviton4 192-Core Processor\"}")
set $rdx=167
continue
printf "license hash call: rsi=%p rdx=%llu r9=%#llx\n", $rsi, $rdx, $r9
continue
printf "machine hash call 2: rsi=%p rdx=%llu r9=%#llx\n", $rsi, $rdx, $r9
set $rsi=(char*)strdup("{\"os\":\"Debian GNU/Linux\",\"hostname\":\"63400bb89fc4\",\"ram_count\":6597069766656,\"macs\":[\"00:00:00:00:00:00\",\"92:73:ed:11:94:f4\"],\"cpu\":\"AWS Graviton4 192-Core Processor\"}")
set $rdx=167
disable 1
continue
Run it with:
gdb -q -batch -x scripts/solve_flag2.gdb --args ./downloads/germinator
Successful output ended with:
License is valid. Ignition sequence: [redacted germinator flag 2]
Why strdup Matters
An early attempt wrote the licensed JSON directly over the original runtime string buffer. That corrupted heap-managed Rust data and caused allocator failures such as free(): invalid size.
Using strdup was safer because it allocated a new C string and only changed the current function arguments:
rsi: pointer to replacement JSON bytesrdx: replacement JSON length
The program still owned and later freed its original Rust allocation normally.
Why This Works
The solve does not patch the binary and does not skip the success path. It only makes the hash routine see the machine profile that the embedded license expects. As a result:
- the license compatibility check receives the correct machine digest
- the later ignition/decryption step receives the correct derived state
- the original code prints the real ignition sequence
Takeaways
The hint was literal: the license was valid, just bound to another environment. Patching control flow was insufficient because the later success path depended on derived data, not only a branch result. Replacing the fingerprint inputs at the hash boundary preserved the original logic while making the binary believe it was running on the licensed machine.