← back to posts

5 - MediterraneanPotions: Decrypting a Flutter App's Encrypted Database

Overview

MediterraneanPotions was a Flutter challenge built around a single encrypted asset: potions.hive, a Hive key-value database where every record was AES-CBC encrypted. The AES key was hardcoded in the app’s native Dart binary. The Hive frame format embedded a per-record IV.

The challenge was to find the key, understand the binary format, and decrypt all records.

First Impressions

The APK was a Flutter application. That changed the decompilation landscape immediately: the main application logic was compiled into libapp.so as native Dart code, not Java bytecode. JADX produced almost nothing useful beyond the Android framework glue.

The first step for Flutter reverse engineering is always strings. Running strings against libapp.so surfaced the critical lead quickly:

1potions.hive
2ENCRYPTED assets/potions.hive

The asset file flutter_assets/assets/potions.hive was encrypted. The binary also referenced it explicitly as ENCRYPTED — something the Dart code checked at load time — which confirmed the encryption was intentional and handled natively, not through a Java wrapper.

Finding the Key

Further string extraction from libapp.so and cross-referencing the encryption references led to the AES key hardcoded in the binary:

1ce1d6b479aba5029fc77bbdadaee7f1f

This was the only plausible 32-hex-character string near the potions.hive and encryption references in the binary. Rather than being derived at runtime from device state or a server value, the key was static — a significant weakness that made offline decryption straightforward.

Frida provided an alternative confirmation path. The frida_hook.js and frida_potions.js scripts hooked the AES primitives at runtime, capturing the key and IV as the app loaded records from its database. The key observed dynamically matched the static extraction.

The Hive Binary Format

Hive stores records as a sequence of variable-length frames. Each frame had the following layout:

1[frame_len : 4 bytes, little-endian]
2[flags     : 1 byte]
3[entry_key : 4 bytes, little-endian]
4[iv        : 16 bytes]
5[ciphertext: frame_len - 25 - 4 bytes]
6[crc32     : 4 bytes]

The IV was embedded inline at bytes 9–25 of each frame. The ciphertext ran from byte 25 to four bytes before the frame end. The final four bytes were a CRC32 checksum over the frame contents.

Decryption

With the key and frame layout in hand, decrypt.py was a direct implementation of the walk-and-decrypt loop:

 1import struct
 2from Crypto.Cipher import AES
 3
 4KEY = bytes.fromhex("ce1d6b479aba5029fc77bbdadaee7f1f")
 5
 6with open('MediterraneanPotions/resources/assets/flutter_assets/assets/potions.hive', 'rb') as f:
 7    data = f.read()
 8
 9pos = 0
10while pos < len(data):
11    frame_len = struct.unpack_from('<I', data, pos)[0]
12    if frame_len == 0:
13        break
14    frame = data[pos : pos + frame_len]
15    entry_key = struct.unpack_from('<I', frame, 5)[0]
16    iv  = frame[9:25]
17    ct  = frame[25 : frame_len - 4]
18    cipher = AES.new(KEY, AES.MODE_CBC, iv=iv)
19    pt = cipher.decrypt(ct)
20    pad = pt[-1]
21    if 1 <= pad <= 16:
22        pt = pt[:-pad]
23    print(f"Key {entry_key}: {pt}")
24    pos += frame_len

Running this over the asset file printed all stored records in plaintext. One of them was the flag.

Frida for the Dynamic Path

The Frida scripts included with the challenge showed the general technique for cases where static key extraction is not viable — for example, if the key had been derived from device identifiers or a server value at runtime.

frida_hook.js hooked the AES decryption primitive in libapp.so and logged both the key bytes and the decrypted plaintext to the console. frida_potions.js added a higher-level hook that walked the Hive frames and dumped the decrypted content as records were loaded by the app.

For this specific challenge the static approach was faster, but the Frida path demonstrated that even if the key had been ephemeral, the plaintext was always observable at the point where the app used it.

The Malware Triage Pass

The workshop’s malware_triage.py — a generic androguard-based static analysis script — ran over the APK before any of the above. It produced a quick picture of the app’s permissions, activities, services, and certificate. The output was most useful for confirming the Flutter packaging structure and establishing that the APK held no suspicious Java-layer logic worth following, so the analysis could focus on libapp.so and the encrypted asset from the start.

Key Techniques

TechniqueWhere
Flutter reverse engineeringMain logic in libapp.so; Java layer is minimal Android glue
Hardcoded AES keyFound via string extraction from libapp.so near encryption references
Hive binary frame parsingIV embedded per-record at bytes 9–25 within each frame
AES-128-CBC decryptionStatic key; per-record IV from frame header
Frida for dynamic confirmationHooks AES primitives at runtime; valid when static extraction is blocked
series
BotConf 2026 Android Workshop