d3lulu — Reverse Engineering Writeup
Challenge
Our resident AI went a bit delulu and tried to create an uncrackable password vault, but here is the twist: the AI suffered a massive hallucination.
We are given d3lulu.zip containing a stripped 64-bit ELF d3lulu that takes a single argument (a "validation key"):
$ ./d3lulu test123
[*] Initialising localized stack verification context...
^ymv||dys/+k.qx@o/.qk@ujlk@~@w+ssj|.q+k./q>>bFor (almost) any input it prints garbage. The flag is hidden somewhere in this mechanism.
Static analysis
Entry point / hash (0x10c0)
mov rbx, [rsi+8] ; argv[1]
call strlen
...
mov edi, 0x811c9dc5 ; FNV-1a 32-bit seed
loop:
movzx ecx, [rdx] ; next byte of input
xor edi, ecx
imul edi, edi, 0x1000193 ; FNV prime
cmp rdx, rax
jne loop
call 0x12d0 ; the "validation" function, edi = hashSo it computes the classical FNV-1a 32-bit hash of the argument and hands it to the validation routine.
Validation routine (0x12d0)
It copies a table of 46 doubles from .rodata (0x20a0) onto the stack, then does the following:
uint32_t hv = fnv1a(input); // previously computed
double value = (double)(hv ^ 0x5fdc6344) + 1.0;
// per table entry (until a negative value, the -1.0 terminator):
for (double drive : table) {
int c = (int)floor(drive / 3.0) + 33;
value = 1.01 * value + 0.01;
if (value > 5.5) c ^= 0x1f;
putc(c, stdout);
}The printable doubles in the table decode (every table value is a nice multiple of 3 off 33):
| drive | c |
|---|---|
| 96.0 | 65 -> 'A' |
| 207.0 | 102 -> 'f' |
| 243.0 | 114 -> 'r' |
| ... | ... |
Reading the whole table gives the plain flag characters:
Africc{fl04t1ng_p01nt_just_a_h4lluc1n4t10n!!}The catch: each character is XORed with 0x1f whenever the internal value grows above 5.5. That is why the program prints ^ymv||dyI... (garbage) for ordinary inputs — the characters c ^ 0x1f correspond exactly to the corrupted output we observed:
raw 'A' ^ 0x1f = '^'
raw 'f' ^ 0x1f = 'y'
raw 'r' ^ 0x1f = 'm'
...Condition for a clean output
value = (hv ^ 0x5fdc6344) + 1.0 grows monotonically via value = 1.01*value + 0.01. Once it exceeds 5.5 it stays above, so the XOR corrupts a suffix (or, for big hashes, everything).
The output stays clean only if the value never exceeds 5.5, which is best when the initial value is minimal, i.e. value = 1.0, requiring:
hv ^ 0x5fdc6344 == 0 -> hv == 0x5fdc6344In other words the "validation key" must any string whose FNV-1a-32 hash equals 0x5fdc6344. Any string with that hash keeps value = 1.0... small enough (< 5.5) the entire loop, so no XOR corrupts anything and the raw flag is printed as-is.
Solution
- Reverse the print loop (or simply decode
drive/3 + 33for every positive table entry). - Recover the flag:
Africc{fl04t1ng_p01nt_just_a_h4lluc1n4t10n!!}Validation key property: an input hashing to 0x5fdc6344 makes the binary print the flag directly.