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.hiveThe 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:
1ce1d6b479aba5029fc77bbdadaee7f1fThis 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_lenRunning 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
| Technique | Where |
|---|---|
| Flutter reverse engineering | Main logic in libapp.so; Java layer is minimal Android glue |
| Hardcoded AES key | Found via string extraction from libapp.so near encryption references |
| Hive binary frame parsing | IV embedded per-record at bytes 9–25 within each frame |
| AES-128-CBC decryption | Static key; per-record IV from frame header |
| Frida for dynamic confirmation | Hooks AES primitives at runtime; valid when static extraction is blocked |