SKIP TO MAIN CONTENT

[ WRITEUP NODE / FIELD REPORTS ]

SOLVED CHALLENGES & FIELD ANALYSIS

SECURITY RESEARCH KNOWLEDGE TECHNIQUES
B3S/WRITEUPS/AFRICC-QUALIFIERS-2027-D3LULU-WRITEUP
← BACK TO ARCHIVE
EVENT: INDEPENDENTCATEGORY: Reverse EngineeringPOINTS: 100 PTS

AFRICC Qualifiers 2027 - D3lulu Writeup

AUTHORED BY:@bealthguy8/15/2026

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>>b

For (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 = hash

So 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 == 0x5fdc6344

In 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

  1. Reverse the print loop (or simply decode drive/3 + 33 for every positive table entry).
  2. Recover the flag:
Africc{fl04t1ng_p01nt_just_a_h4lluc1n4t10n!!}

Validation key property: an input hashing to 0x5fdc6344 makes the binary print the flag directly.