Author: gha32a | CTF: Securinets CTF Qualifications 2026 | Category: Hardware / Forensics / Reverse Engineering | Points: 441 | Date: October 19, 2026
Tools: file, sgdisk, dd, xxd, radare2, Python 3, cpio, cryptsetup, openssl, QEMU + OVMF
TL;DR
A raw disk image holds a LUKS2 vault and an EFI partition whose bootloader has been swapped for a custom one. That loader decodes a custom-packed file (memdisk.pack) into a cpio archive containing the shop's old order records. Those records reveal the vault passphrase (pistachio-37), and the vault holds recipe.enc, an OpenSSL-encrypted file that uses the same passphrase. Two decoy flags are planted along the way, and both are rejected by the scoreboard.
Challenge
A recovered jukebox still boots from its original media, but its loader has been replaced. A vault holds a recipe the owner took care to protect. The shop's old order records may reveal how to reach it.
Provided file: Gelatino.img (135 MB). The GPT header expects 275584 sectors but the image is truncated, which does not affect the useful partitions.
The statement gives three milestones, and the solve follows them in order:
- a loader (boot firmware) was replaced, so I need to understand what it does;
- a vault (encrypted volume) holds the recipe, which is the final flag;
- the shop's old order records are the way in.
A note on method: every stage contains a plausible decoy designed to trap rushed solvers and automated tooling. Checking each clue against the narrative of the statement was what separated real leads from fake ones.
1. Initial triage
gha32a@gelatino:~/ctf$ file Gelatino.img
Gelatino.img: DOS/MBR boot sector; partition 1 : ID=0xee, start-CHS (0x0,0,2),
end-CHS (0x13,31,12), startsector 1, 307199 sectors, extended partition table (last)Partition type 0xEE marks a protective MBR. Its only job is to stop legacy tools from treating a GPT disk as empty, so the real layout lives in the GPT.
1.1 Reading the GPT
gha32a@gelatino:~/ctf$ sgdisk -p Gelatino.img
Warning! Disk size is smaller than the main header indicates!
Caution: invalid backup GPT header, but valid main header; regenerating
backup header from main header.
[...]
Main header: OK Backup header: ERROR
Main partition table: OK Backup partition table: ERROR
Disk Gelatino.img: 275584 sectors, 134.6 MiB
Disk identifier (GUID): 6FDFF7F5-50E7-4C18-9529-D1D4F2611161
Number Start (sector) End (sector) Size Code Name
1 2048 206847 100.0 MiB 8300 gelato-data
2 206848 264191 28.0 MiB EF00 EFIA GPT keeps two copies of its header and partition table: one at the start of the disk and a backup at the very end. The image was cut short, so the backup is missing, but the primary copy is intact and that is all I need.
| # | Start sector | Sectors | Size | Type | Role |
|---|---|---|---|---|---|
| 1 | 2048 | 204800 | 100 MiB | Linux (8300) | gelato-data, the vault |
| 2 | 206848 | 57344 | 28 MiB | EFI System (EF00) | ESP holding the replaced loader |
Layout of Gelatino.img
Proportional bar of the disk image: protective MBR and GPT in sectors 0 to 33, dead space in sectors 34 to 2047 where the first decoy flag was hidden, partition 1 gelato-data (LUKS2, 100 MiB, sectors 2048 to 206847), partition 2 EFI (FAT16, 28 MiB, sectors 206848 to 264191), and the missing backup GPT at the end of the truncated image.
1.2 Mounting by offset, without copying
Sector math (512 bytes per sector) lets me mount partitions straight from the image:
- partition 1 offset:
2048 × 512 = 1,048,576bytes - partition 2 offset:
206848 × 512 = 105,906,176bytes
gha32a@gelatino:~/ctf$ sudo mkdir -p /mnt/p1 /mnt/p2
gha32a@gelatino:~/ctf$ sudo mount -o ro,loop,offset=$((2048*512)),sizelimit=$((204800*512)) \
Gelatino.img /mnt/p1
mount: can't read superblock # expected, see 1.3
gha32a@gelatino:~/ctf$ sudo mount -o ro,loop,offset=$((206848*512)),sizelimit=$((57344*512)) \
Gelatino.img /mnt/p2Everything is mounted read-only so the evidence stays untouched.
1.3 Identifying each partition from raw headers
gha32a@gelatino:~/ctf$ dd if=Gelatino.img bs=512 skip=2048 count=2048 2>/dev/null | file -
/dev/stdin: LUKS encrypted file, ver 2, header size 16384, ID 5, algo sha256,
salt 0xad59e8d6c30a623b..., UUID: cabe2e0d-a07b-42f2-9012-0e706fa734a0, ...
gha32a@gelatino:~/ctf$ dd if=Gelatino.img bs=512 skip=206848 count=2048 2>/dev/null | file -
/dev/stdin: DOS/MBR boot sector, code offset 0x3c+2, OEM-ID "mkfs.fat",
sectors/cluster 4, FAT (16 bit), label: "GELATO"gelato-datais a LUKS2 container, which is the vault from the statement. The mount failure in 1.2 is expected because there is no filesystem to read until the container is unlocked.EFIis a plain FAT16 volume labelledGELATO.
2. Exploring the EFI System Partition
gha32a@gelatino:~/ctf$ find /mnt/p2 -type f -exec ls -l {} \;
-rwxr-xr-x 1 root root 53248 Oct 15 23:10 /mnt/p2/EFI/BOOT/BOOTX64.EFI
-rwxr-xr-x 1 root root 5668864 Oct 15 23:05 /mnt/p2/EFI/BOOT/GRUBX64.EFI
-rwxr-xr-x 1 root root 2064 Oct 16 19:36 /mnt/p2/jukebox/memdisk.pack
-rwxr-xr-x 1 root root 1152 Oct 5 21:48 /mnt/p2/jukebox/vault.b64| File | Observation |
|---|---|
BOOTX64.EFI |
The replaced loader. It sits at the standard UEFI fallback path, so the firmware runs it first. At 52 KiB it is far smaller than a real GRUB. |
GRUBX64.EFI |
A genuine 5.6 MiB GRUB, loaded only after the fake loader accepts its input. |
jukebox/memdisk.pack |
A 2064-byte blob with no recognizable format. |
jukebox/vault.b64 |
A 1152-byte text file whose name suggests base64. |
2.1 Booting the image to confirm the chain
gha32a@gelatino:~/ctf$ qemu-system-x86_64 -m 512 -bios OVMF.fd \
-drive file=Gelatino.img,format=raw -nographic
...
grub> ls
(memdisk) (memdisk) (proc) (proc) (hd0) (hd0) (hd0,gpt2) (hd0,gpt2)
(hd0,gpt1) (hd0,gpt1) (cd0) (cd0)The image boots into a bare grub> prompt with no grub.cfg, so the custom loader hands control to a blank GRUB. The (memdisk) entry shows that the firmware builds an in-memory disk, most likely from memdisk.pack.
3. Decoy #1: plaintext in the dead space between GPT and partition 1
Before any deep analysis, I looked at the gap between the GPT structures and the first partition (sectors 34 to 2047), where a clean image should be empty:
gha32a@gelatino:~/ctf$ dd if=Gelatino.img bs=512 skip=34 count=2014 2>/dev/null | xxd \
| grep -v "0000 0000 0000 0000" | head
00003c00: 5365 6375 7269 6e65 7473 7b31 5f68 3070 Securinets{1_h0p
00003c10: 335f 755f 6430 6e27 745f 7553 335f 7468 3_u_d0n't_uS3_th
00003c20: 3173 5f73 6c30 707d 0a00 0000 0000 0000 1s_sl0p}........The dump offset 0x3c00 is relative to sector 34, which puts the string at absolute offset 0x8000 (32 KiB) in the image.
Flag candidate: Securinets{1_h0p3_u_d0n't_uS3_th1s_sl0p}. The scoreboard rejected it.
The text itself ("I hope you don't use this slop") taunts anyone who blindly scans a raw image, and it has no connection to the boot chain or the vault. Lesson: an artifact that is too easy to find and unrelated to the story of the challenge deserves suspicion.
4. Decoy #2: zero-width Unicode steganography in vault.b64
The name vault.b64 suggests base64, but decoding it fails because the file hides invisible characters. I counted the non-ASCII code points:
from collections import Counter
data = open('/mnt/p2/jukebox/vault.b64', encoding='utf-8').read()
non_ascii = [ch for ch in data if ord(ch) > 127]
print(Counter(non_ascii))
for ch in set(non_ascii):
print(hex(ord(ch)), ch.encode('unicode_escape'))Counter({'\u200b': 195, '\u200c': 173, '\u2063': 2})
0x2063 b'\\u2063' # INVISIBLE SEPARATOR (noise, unused)
0x200c b'\\u200c' # ZERO WIDTH NON-JOINER
0x200b b'\\u200b' # ZERO WIDTH SPACEOnly two of the three characters carry data: U+200B and U+200C form a binary channel, while U+2063 appears twice and acts as noise. Mapping them to bits and regrouping into bytes reveals the hidden message:
mapping = {'\u200b': '1', '\u200c': '0'}
bits = ''.join(mapping[ch] for ch in data if ch in mapping)
n = len(bits) - (len(bits) % 8)
out = bytes(int(bits[i:i+8], 2) for i in range(0, n, 8))
print(out.decode('ascii', errors='replace'))Securinets{z3r0_w1dth_mmm_h0w_d1d_th4t_h4pp3n}Flag candidate: Securinets{z3r0_w1dth_mmm_h0w_d1d_th4t_h4pp3n}. Also rejected.
The visible text of the file (bk 4f2a9c1e tmp cache 8831 do not sync) is filler meant to look like real base64. Two increasingly sophisticated decoys in a row told me that the real path is what the statement describes, the loader and the memdisk, and not vault.b64.
5. Reverse engineering the loader (BOOTX64.EFI)
5.1 Strings
gha32a@gelatino:~/ctf$ r2 -A -q -c 'iz' /mnt/p2/EFI/BOOT/BOOTX64.EFI
...
0 0x0001b000 utf16le GELATO FIRMWARE / checking startup reel\r\n
1 0x0001b058 utf16le Startup reel accepted (%u bytes). Handing off to the jukebox shell.\r\n
2 0x0001b0e8 utf16le Startup reel rejected; firmware status 0x%lx\r\nEFI applications use UTF-16 strings, hence the utf16le entries. These three messages describe the whole story: the fake firmware reads a "startup reel" (memdisk.pack), validates it, and only then hands off to the real GRUB.
5.2 Finding the validation routine
A cross-reference on the first string leads straight to the function that does the work:
[0x00013000]> axt @ 0x0001b000
fcn.000133bb 0x134ee [STRN:r--] lea rax, str.GELATO_FIRMWARE___checking_startup_reel
gha32a@gelatino:~/ctf$ r2 -A -q -c 'pdf @ fcn.000133bb' /mnt/p2/EFI/BOOT/BOOTX64.EFI > fcn.txt5.3 What fcn.000133bb does
Reading the disassembly (key parts are in the appendix), the function performs these steps in order:
- builds the UTF-16 path
\jukebox\memdisk.packon the stack using a series ofmovabsinstructions; - opens and reads the file through the EFI file protocols (
fcn.00018937); - reads the 16-byte header and checks the magic
GLT1byte by byte (cmp al, 0x47/0x4C/0x54/0x31); - reads three little-endian 32-bit fields with
fcn.00013029; - checks that the payload size is a power of two using
(C-1) & C == 0; - runs a decoding loop over the payload (section 5.5);
- verifies that the first 6 decoded bytes equal
070701, the magic of a cpio newc archive; - computes a CRC32 over the first 1536 decoded bytes and compares it with a value from the header;
- on success prints "accepted" and builds the GRUB memdisk, otherwise prints "rejected" and spins in an infinite loop (
jmp 0x13844).
From the replaced loader to the recipe
Flow diagram. BOOTX64.EFI validates memdisk.pack and hands off to GRUBX64.EFI with a memdisk. Then memdisk.pack decodes to a cpio archive holding blends.lst and order_history.txt, which give the passphrase pistachio-37, which opens the LUKS vault holding recipe.enc, which openssl decrypts.
5.4 Helper functions
fcn.00013029: read a little-endian uint32
movzx eax, byte [rdi] ; byte 0
movzx edx, byte [rdi+1] ; byte 1
shl edx, 8
or edx, eax
movzx eax, byte [rdi+2] ; byte 2
shl eax, 0x10
or edx, eax
movzx eax, byte [rdi+3] ; byte 3
shl eax, 0x18
or eax, edx
ret ; returns a little-endian uint32_tfcn.0001307e: 8-bit rotate right (ROR8): (byte >> shift) | (byte << (8 - shift)) masked to 8 bits, with shift reduced to shift & 7. A quick example: ROR8(0x03, 1) moves the low bit to the top and gives 0x81.
CRC routine: the loader folds each decoded byte into a running CRC with crc32_update (fcn.000130ce) and finishes with not eax. That final inversion is the standard CRC-32 (Ethernet/zlib) finalization, so Python's zlib.crc32 produces the same value.
5.5 Header layout and decoding algorithm
Header layout (16 bytes) reconstructed from the loader:
| Bytes | Field | Meaning |
|---|---|---|
| 0 to 3 | GLT1 |
magic |
| 4 to 7 | A = 1536 | number of decoded bytes the CRC covers |
| 8 to 11 | B | expected CRC32 (finalized) |
| 12 to 15 | C = 2048 | size of the encoded payload |
The payload starts at offset 0x10, which gives 16 + 2048 = 2064 bytes, matching the file size.
Decoding rule for each logical position i in [0, 2047]:
raw = payload[(13 * i) mod 2048]
shift = i mod 8
rotated = ROR8(raw, shift)
decoded[i] = rotated XOR ((61 * i - 89) mod 256)The read index 13·i mod 2048 is a permutation: 13 is coprime with 2048, so it is invertible. It scrambles the order of bytes on disk relative to the decoded stream. Combined with the rotation and a position-dependent XOR, this is simple obfuscation that is effective against a casual sequential read but trivial once the code is understood.
5.6 Decoder script
from pathlib import Path
import struct, zlib
blob = Path("memdisk.pack").read_bytes()
assert blob[:4] == b"GLT1"
# header: magic, A, B, C (little-endian uint32 each)
size, expected_crc, span = struct.unpack_from("<III", blob, 4)
payload = blob[16:]
assert len(payload) == span == 2048
def ror8(b, n):
n &= 7
return b if n == 0 else ((b >> n) | (b << (8 - n))) & 0xFF
out = bytes(
ror8(payload[(13 * i) % span], i % 8) ^ ((61 * i - 89) & 0xFF)
for i in range(span)
)
print("magic check:", out[:6]) # must be b'070701'
crc = zlib.crc32(out[:size]) & 0xFFFFFFFF
print(f"expected={expected_crc:#010x} actual={crc:#010x}")
assert out[:6] == b"070701" and crc == expected_crc, "decode failed"
Path("memdisk_decoded.bin").write_bytes(out)Output:
magic check: bytearray(b'070701')
crc32: matches the value stored in the headergha32a@gelatino:~/ctf$ file memdisk_decoded.bin
memdisk_decoded.bin: ASCII cpio archive (SVR4 with no CRC)Matching both the magic and the CRC32 proves the decode is exactly what the loader would accept, not just something that looks plausible.
6. Extracting the cpio archive
The 070701 magic marks the newc cpio format. Each member starts with that 6-character magic followed by metadata fields written as ASCII hexadecimal, which is why file reports an ASCII cpio archive. GRUB uses this format for memory disks.
gha32a@gelatino:~/ctf$ mkdir memdisk_extracted && cd memdisk_extracted
gha32a@gelatino:~/ctf/memdisk_extracted$ cpio -idv < ../memdisk_decoded.bin
blends.lst
order_history.txt
3 blocks
gha32a@gelatino:~/ctf/memdisk_extracted$ ls -l
-rw-r--r-- 1 gha32a gha32a 381 Oct 16 15:34 blends.lst
-rw-r--r-- 1 gha32a gha32a 722 Oct 16 15:34 order_history.txtThese two files are the "old order records" from the statement.
7. Reading the clue
order_history.txt (excerpt):
Mmm... pistachio was always the owner's favorite.
The register wrote tickets as flavor-ticket number.
Molly Carter : 10:12 : vanilla
Ethan Brooks : 10:48 : lemon
...
Filip The Owner : 18:16 : pistachio
...
Lily James : 20:27 : walnutblends.lst (excerpt, alphabetical):
almond
amaretto
banana
...
pecan
pistachio <-- line 37
raspberry
...
yuzuTwo facts come out of this:
- the favorite flavor is pistachio, ordered by "Filip The Owner";
- tickets are written as
flavor-ticket number, so the passphrase has the formpistachio-<N>.
7.1 What "ticket number" means
The hint is ambiguous on purpose.
| Interpretation | Value | Result |
|---|---|---|
Position of the owner's order in order_history.txt (chronological rank) |
18 | fails |
Position of the flavor in the catalog blends.lst (alphabetical) |
37 | works |
The first reading feels natural but is wrong. "Ticket number" refers to the number tied to the flavor itself in the shop's catalog.
blends = open('blends.lst').read().split()
print(blends.index('pistachio') + 1) # -> 378. Cracking the LUKS2 passphrase
8.1 Read-only loop device
gha32a@gelatino:~/ctf$ sudo losetup -r -f --show -o $((2048*512)) --sizelimit $((204800*512)) \
Gelatino.img
/dev/loop2
gha32a@gelatino:~/ctf$ sudo cryptsetup isLuks /dev/loop2 && echo "valid LUKS"
valid LUKSTo inspect the container metadata (cipher, keyslots, KDF) without unlocking it, cryptsetup luksDump /dev/loop2 is the tool to use.
8.2 Why --test-passphrase
cryptsetup open --test-passphrase only checks whether a passphrase unlocks a keyslot. It creates no device mapping and mounts nothing, which makes it ideal for testing many candidates. LUKS2 slows each attempt down on purpose with its key-derivation function, so a short, well-reasoned candidate list beats blind brute force.
8.3 Tester script
#!/bin/bash
# try_passphrases.sh
LOOPDEV=/dev/loop2
FILE="$1"
while IFS= read -r pass; do
echo -n "$pass" | sudo cryptsetup open --test-passphrase "$LOOPDEV" 2>/dev/null
if [ $? -eq 0 ]; then
echo "FOUND: $pass"
exit 0
fi
done < "$FILE"
echo "No passphrase found"8.4 Targeted candidate list
I generated several plausible formats (separator variants, number-first form, case variants):
blends = open('blends.lst').read().split()
pos = blends.index('pistachio') + 1 # 37
candidates = [
f"pistachio-{pos}", f"pistachio {pos}", f"pistachio_{pos}",
f"{pos}-pistachio", f"Pistachio-{pos}", f"PISTACHIO-{pos}",
]
open('candidates.txt', 'w').write('\n'.join(candidates))gha32a@gelatino:~/ctf$ ./try_passphrases.sh candidates.txt
FOUND: pistachio-378.5 Alternative: sweep the ticket number
If you are unsure how to interpret "ticket number", a numeric sweep avoids committing to one theory:
import subprocess
device = "/dev/loop2"
for n in range(1, 101):
cand = f"pistachio-{n}"
r = subprocess.run(
["cryptsetup", "open", "--type", "luks", "--test-passphrase", "--key-file=-", device],
input=cand.encode(), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
if r.returncode == 0:
print("MATCH:", cand)
breakBoth approaches land on pistachio-37.
8.6 Opening and mounting the vault
gha32a@gelatino:~/ctf$ sudo cryptsetup open --readonly /dev/loop2 vault
Enter passphrase: pistachio-37
gha32a@gelatino:~/ctf$ sudo mkdir -p /mnt/vault
gha32a@gelatino:~/ctf$ sudo mount -o ro /dev/mapper/vault /mnt/vault
gha32a@gelatino:~/ctf$ find /mnt/vault -type f -exec ls -l {} \;
-rwxr-xr-x 1 root root 64 Oct 16 15:34 /mnt/vault/recipe.enc9. Decrypting recipe.enc
gha32a@gelatino:~/ctf$ file /mnt/vault/recipe.enc
/mnt/vault/recipe.enc: openssl enc'd data with salted password
gha32a@gelatino:~/ctf$ xxd /mnt/vault/recipe.enc
00000000: 5361 6c74 6564 5f5f 0424 612d 6291 5de4 Salted__.$a-b.].
00000010: d134 bbdd 2c1f 37f2 de8b c333 a8fa e1c8 .4..,.7....3....
00000020: 8681 ea60 9662 8dba a666 7df8 560c 94d9 ...`.b...f}.V...
00000030: 6d6d 1cd4 1c6f 33c3 5641 2453 3f45 754b mm...o3.VA$S?EuKThe file starts with Salted__ followed by an 8-byte salt (04 24 61 2d 62 91 5d e4), which is the signature of openssl enc. In that format the key and IV are derived from the passphrase and salt with the legacy EVP_BytesToKey function (MD5-based, single iteration) unless -pbkdf2 is given. The vault passphrase is reused as the symmetric key, with AES-256-CBC:
gha32a@gelatino:~/ctf$ openssl enc -d -aes-256-cbc -in /mnt/vault/recipe.enc -pass pass:pistachio-37
*** WARNING : deprecated key derivation used.
Using -iter or -pbkdf2 would be better.
Securinets{n3xt_time_buy_s0m3th1ng_h34lthy}If a bad decrypt message follows the output, it only means the PKCS#7 padding check failed on the last block. The flag was already printed earlier in the stream and stays valid. It is not a full decryption failure.
Flag
Securinets{n3xt_time_buy_s0m3th1ng_h34lthy}Solve chain at a glance
| Step | Action |
|---|---|
| Triage | file, sgdisk → GPT with 2 partitions |
| Mounting | offset/sizelimit → LUKS2 (gelato-data) + FAT16 (EFI) |
| Decoy 1 | plaintext in inter-partition gap → fake flag |
| Decoy 2 | zero-width steganography in vault.b64 → fake flag |
| Reverse engineering | radare2 on BOOTX64.EFI → permutation + ROR8 + XOR decoder for memdisk.pack |
| Extraction | cpio → order_history.txt + blends.lst |
| Clue | position of pistachio in the catalog, not in the chronological log |
| Passphrase | pistachio-37 (LUKS, then reused for AES-256-CBC) |
| Final decryption | openssl enc -d -aes-256-cbc |
| Flag | Securinets{n3xt_time_buy_s0m3th1ng_h34lthy} |
Quick reproduction
# 1. mount the EFI partition read-only and decode the startup reel
sudo mount -o ro,loop,offset=$((206848*512)),sizelimit=$((57344*512)) Gelatino.img /mnt/p2
python3 decode_memdisk.py # script from section 5.6
mkdir out && cd out && cpio -idv < ../memdisk_decoded.bin
# 2. open the vault with the passphrase from the catalog position
sudo losetup -r -f --show -o $((2048*512)) --sizelimit $((204800*512)) ../Gelatino.img
sudo cryptsetup open --readonly /dev/loopX vault # pistachio-37
sudo mount -o ro /dev/mapper/vault /mnt/vault
# 3. decrypt the recipe
openssl enc -d -aes-256-cbc -in /mnt/vault/recipe.enc -pass pass:pistachio-37Technical concepts used
- GPT partitioning: protective MBR, primary and backup headers, working with a truncated image.
- Offset mounting: converting sectors to byte offsets and using
offset/sizelimitto avoid copying data. - UEFI boot chain: the role of the ESP and of
EFI/BOOT/BOOTX64.EFIas the default entry point, and what replacing it means. - Unicode steganography: binary encoding with zero-width characters (
U+200B/U+200C) and noise characters (U+2063). - Static reverse engineering:
radare2(iz,axt,pdf), reading x86-64 disassembly, reconstructing a decoder (modular permutation, bit rotation, position-dependent XOR). - CRC32: integrity checking with
zlib.crc32and cross-validating against the value stored in a file header. - cpio newc archives: the
070701format GRUB uses for memory disks. - LUKS2: header structure and
cryptsetup --test-passphrasefor targeted guessing without mounting each time. - OpenSSL
enc: theSalted__format and the legacy EVP_BytesToKey derivation versus-pbkdf2.
What I learned
- Validate every clue against the story of the challenge before submitting a flag. Two decoys in a row existed to catch exactly that mistake.
- Read hints literally and test a numeric range when two interpretations compete, instead of hammering a single theory.
- Confirm a decode with an integrity check (magic bytes plus CRC32), not by eye.
cryptsetup --test-passphrasegives fast, non-destructive passphrase testing, and a small reasoned candidate list beats brute force against a deliberately slow KDF.- Keep the evidence read-only (
romounts, read-only loop devices) throughout an investigation.
Appendix: key parts of fcn.000133bb
; build path \jukebox\memdisk.pack (successive movabs)
...
call fcn.000151d0 ; init path buffer
lea rax, str.GELATO_FIRMWARE_...
call fcn.00017850 ; Print("GELATO FIRMWARE / checking ...")
; open file
call fcn.00018937 ; OpenVolume / Open file
test rax, rax
js 0x1381f ; failure -> rejected
; read 16-byte header
call fcn.0001311c ; Read(buffer, &size)
test rax, rax
js 0x13822
; magic "GLT1"
cmp al, 0x47 ; 'G'
cmp al, 0x4c ; 'L'
cmp al, 0x54 ; 'T'
cmp al, 0x31 ; '1'
; fields A, B, C (LE32) via fcn.00013029
call fcn.00013029 ; A = 1536
call fcn.00013029 ; B = expected CRC32
call fcn.00013029 ; C = 2048
; C must be a power of two
sub rax, 1
and rax, qword [var_30h] ; (C-1) & C == 0 ?
jne 0x1363e
; decode loop, i = 0..C-1
.loop:
mov rax, rdx
add rax, rax
add rax, rdx
shl rax, 2
add rax, rdx ; rax = 13*i
xor edx, edx
div qword [var_30h] ; rdx = (13*i) mod C
...
call fcn.0001307e ; ROR8(raw, i & 7)
mov ecx, 0x3d ; 61
imul eax, ecx
sub eax, 0x59 ; 89
xor eax, edx ; decoded = rotated ^ (61*i - 89)
; magic check "070701" on i = 0..5
; CRC32 accumulation while i < A (1536)
call fcn.000130ce ; crc32_update(crc, byte)
; finalize and compare with header field B
mov eax, [var_14h]
not eax
cmp [var_24h], eax
je 0x13741 ; "accepted" + memdisk handoff