Insomni’hack + writeup GigPanG echo


This year's Insomni'hack CTF was even better than the previous years, with a higher level both on the challenges side and on the participating teams side. The excellent Dragon Sector finished first, followed by StratumAuhuur, HackingForBeers, More Smoked Leet Chicken, and int3pids. Scores details can be found on CTFtime.

This year I played in the one-shot HackingForBeers team, with a couple of friends who like me happened to be teamless for that CTF. I'd like to thank them for the fun, as well as all the other participants, and the staff from SCRT for the effort they put in making this happen.

My first CTF writeup won't be on a very difficult challenge, though no team managed to solve it after 10 hours of CTF. Our team spent quite some time on it, and we were frustrated to feel so close to the solution. I later asked the organizers for some hints, and finally managed to solve it! (I would've never found without hints though.) Here's how it goes:


We were given a an audio file void.wav of 2293678 bytes, of type RIFF (little-endian) 16-bit stereo 44100Hz. A tool like binwalk reveals the signature of a ZIP archive at offset 0x22fdfc, and that of a second ZIP archive at offset 0x22fe3b. And yeah, we initially missed the hint in the camelcase title.

We got the passphrase, not

Decompressing the archive at offset 0x22fdfd yields two files:

  •, the second ZIP detected, of 244 bytes, containing a password-protected file flag
  • passphrase, a 23-byte file containing the ASCII string 5tr0ng&[email protected].

We naively attempted to decrypt the alleged flag using the given passphrase, and of course it failed. We then attempted to crack the ZIP file using standard techniques: bruteforce, dictionary attacks, etc., and failed again. We listened to the audio, looked for some patterns in the hex dump, attempted to modify the ZIP metadata, etc. etc. and failed again and again.


The solution was in the LSBs of the audio payload, but with caveats: most wave files have a 44-byte header, so one could assume that data would be encoded starting from the 45th byte. However there was bug in the steganography tool used to create the challenge: the 44-byte offset was considered twice, so that the payload’s encoding actually started at byte 88 instead of 44.

Also, one had to convert the bit sequence to bytes by ordering bits in the right order (first bits becoming the least significant). The following Python script lazily achieves this with a look-up table, and is straightforward to follow:

b2b = [
        0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0,
        0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
        0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8,
        0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
        0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4,
        0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
        0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec,
        0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
        0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2,
        0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
        0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea,
        0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
        0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6,
        0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
        0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
        0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
        0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1,
        0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
        0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9,
        0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
        0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5,
        0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
        0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed,
        0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
        0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3,
        0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
        0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb,
        0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
        0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7,
        0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
        0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef,
        0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,

import bitarray

fi = open('void.wav', 'rb')
data =

off_0 = 88 
off_e = 0x22fdfc

data = data[off_0:off_e]
data = [ ord(x) for x in data ]

bits = [ (b&1) for b in data ]

bytez = bitarray.bitarray(bits).tobytes()
bytez = [ chr(b2b[ord(b)]) for b in bytez ]
bytez = bytearray(bytez)

fo = open('decoded', 'wb')

The 286645-byte file decoded produced by this script starts with bytes 8c 0d 04 09, and for some reason is recognized by the file utility as a DOS executable (COM). These executables do not enforce a signature, and can start execution at offset zero. It was thus likely a false alarm by file. Running gpg on the decoded file however prompts us for a password! So let's try 5tr0ng&[email protected]:

 $ gpg decoded 
 gpg: AES256 encrypted data
 gpg: encrypted with 1 passphrase
 gpg: decoded: unknown suffix
 gpg: [don't know]: invalid packet (ctb=3d)
 gpg: mdc_packet with invalid encoding
 gpg: decryption failed: invalid packet
 gpg: [don't know]: invalid packet (ctb=48)

The clear file obtained contains the string xqiyNkBLFogDyGXAh5ex6QAJoqe04BcnezGcNXiCdEs75SO1pWqzjCBWo8gGxrNo, which is the passphrase for our flag: the file in thus gives us the precious flag:

You were able to listen to the echo of the void



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s