Monday, April 3, 2017

Hacky Easter 2017 Teaser Write Up

Hey everyone - A friend of mine recently showed me a capture the flag (CTF) event that he was working on, and he looked like he was having fun, so I wanted to give it a try.  The CTF is called Hacky Easter, and it is put on by the folks over at HackingLab.  You can check out the event here. The actual event starts on April 4, 2017, but a teaser was available beforehand.  This write up is going out after the teaser is over so that everyone can have fun with it, but I wanted to document how I approached the problems because it was a lot of fun.  Kudos to the people at HackingLab for creating the challenges and putting it on.

Let's get started.  Click Read More to see my write up.


For each puzzle, you need to find the five character solution.  Then, you need to combine the solutions in the right way to find the final solution.  When necessary, I used Python to help do some of the grunt work.

Riddle 1


         MBD2A !ysaep ,ysaE
All you have to do for this one is reverse the string, and you get:

Easy, peasy! A2DBM

You can also do this using Python if you like:

>>> riddle1='MBD2A !ysaep ,ysaE'
>>> riddle1[::-1]
'Easy, peasy! A2DBM'


Riddle 2


         UGllY2Ugb2YgY2FrZSEgWlhHSUQ=
At first, this looks like a bunch of junk. Since I have seen strings like this before, my first guess is that this is a Base64 encoded string. Base64 is a popular way to encode binary data as a series of printable characters so that it is easier to transmit. If you see weird strings like this, starting with the hypothesis that it is encoded in some way. I found the Wikipedia article on Binary to Text Encoding really helpful. It might not be a bad idea to become familiar with some of the more popular encoding schemes like Base64, Percent Encoding, and hexadecimal (Base16).
Let's try base64 decoding this string. There are online utilities that can do this for you if you do not want to fire up Python or write some code in your favorite programming language. I am going to do it in Python:

>>> import base64
>>> base64.b64decode('UGllY2Ugb2YgY2FrZSEgWlhHSUQ=')
b'Piece of cake! ZXGID'


Piece of cake! ZXGID

Riddle 3


        One for free here: 404 - not found! XIZLS
This one was in white on a white page. One way to find it is to look at the source code of the page (right click, View Source).

One for free here: 404 - not found! XIZLS

Riddle 4


        eval(function(p,a,c,k,e,d)
{e=function(c){return c};if(!''.replace(/^/,String))
{while(c--){d[c]=k[c]||c}k=[function(e){return d[e]}];
e=function(){return'\\w+'};c=1};
while(c--){if(k[c])
{p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('0(\'1\');',2,2,'alert|VYGY6'.split('|'),0,{}))
This is some sort of function call. The use of the word "function" told me it is likely JavaScript. It has been minified which means that all of the things that would make it easy for a human to understand have been stripped out. For example, all of the variable names are one letter, and there are no comments. This saves on the amount of data that needs to be transferred to each client. If you are hosting a web site that gets a lot of traffic, you do not want to pay for unnecessary bytes. Since humans do not need to see the JavaScript behind a website, it will be minified. Another reason to minify is to obfuscate your code so that it will be harder for someone to tell what it does.

For our purposes, we can evaluate the JavaScript using a site like this one at W3Schools. If you are going to run any code from an untrusted source, I recommend doing it in a VM. When we run the JavaScript, we get the answer:

Riddle 5


        3a3ea00cfc35332cedf6e5e9a32e94da
        9d5ed678fe57bcca610140957afab571
        f09564c9ca56850d4cd6b3319e541aee
        5dbc98dcc983a70728bd082d1a47546e
        7fc56270e7a70fa81a5935b72eacbe29
At first, I thought this was a bunch of hexadecimal characters (3a, 3e, a0, and so on), so I tried to convert them from hexadecimal to something readable:

>>> import binascii
# The slashes are line continuation characters in python. They are not part of the string.
>>> riddle5='3a3ea00cfc35332cedf6e5e9a32e94da9d5ed678fe57bcca6101409\
57afab571f09564c9ca56850d4cd6b3319e541aee5dbc98dcc983a70728bd082d1a47\
546e7fc56270e7a70fa81a5935b72eacbe29'
>>> binascii.unhexlify(riddle5)
b':>\xa0\x0c\xfc53,\xed\xf6\xe5\xe9\xa3.\x94\xda\x9d^\xd6x\xfeW\xbc\xcaa
\x01@\x95z\xfa\xb5q\xf0\x95d\xc9\xcaV\x85\rL\xd6\xb31\x9eT\x1a\xee]\xbc\x98
\xdc\xc9\x83\xa7\x07(\xbd\x08-\x1aGTn\x7f\xc5bp\xe7\xa7\x0f\xa8\x1aY5\xb7.\xac\xbe)'


Well, that does not look useful. So I thought about how they were presented. There are five lines of 32 characters (128 bits) each. The letters are all lower case, and there is a mix of numbers and letters. However, I do not see any letters other than a through f. This might be some sort of hash. Every solution up until now has been five characters, and there are five lines, so maybe each line is a hash of a character in the solution. What hash though?

A common 128 bit hash is MD5, so we can try that. Hashes are one way functions meaning you can hash something but you cannot unhash. To see if the hashes match possible characters in the solution, we will have to generate a hash for each possible character. Fortunately, we have only seen uppercase letters and numbers in solutions so far which makes for 36 hashes we have to compute. That should be relatively easy, so let's try it:


>>> import hashlib
>>> import string
>>> hashes = ['3a3ea00cfc35332cedf6e5e9a32e94da', '9d5ed678fe57bcca610140957afab571',
'f09564c9ca56850d4cd6b3319e541aee', '5dbc98dcc983a70728bd082d1a47546e',
'7fc56270e7a70fa81a5935b72eacbe29']
>>> for character in string.ascii_uppercase:
...     hash_of_char = hashlib.md5(bytes(character, encoding='ascii')).hexdigest()
...     if hash_of_char in hashes:
...             print('Hash {} is {}'.format(hash_of_char, character))
... 
Hash 7fc56270e7a70fa81a5935b72eacbe29 is A
Hash 9d5ed678fe57bcca610140957afab571 is B
Hash 3a3ea00cfc35332cedf6e5e9a32e94da is E
Hash f09564c9ca56850d4cd6b3319e541aee is Q
Hash 5dbc98dcc983a70728bd082d1a47546e is S


If we reorder our solutions in the order that the hashes were presented, we get the solution: EBQSA.

Riddle 6


        --- -. . / -- --- .-. . / .... . .-. . ---... / .--- .- --- -- -.--

This looks like morse code, so I used a morse code translator. It gives us our solution: ONE MORE HERE: JAOMY.

Riddle 7


        Hwldp wx, Euxwh! QYAVL

You might be tempted to take the last five characters as the solution, but this one is probably more complicated than that because all of the solutions so far have been readable. We have printable characters, no numbers, and punctuation. With that set of characters, I do not think it is a common encoding scheme like Base64 or even Base32. It might be a shift cipher (a cipher where each character is shifted in the alphabet a certain number of times). For example, a shift cipher where every character is shifted three places forward in the alphabet would turn the string "ABC" into "DEF"

There are tools online that can perform shift ciphers on input, but we are going to use Python. I am going to use the maketrans function to make translation tables for all 26 upper case letters. There are 25 total translations we need to test (0 - 25 where 0 means the characters shift no positions and 25 meaning the characters shift 25 characters to the right (A becomes Z, B becomes A)). It may be easier to think about if you assign a number to each letter of the alphabet based on its position. 1 is A, 2 is B, and so on until you get to Z which is 26. If we have a shift of +3, A goes from 1 to 4, B from 2 to 5. If we go over 26, we start back at 1, so Z in that example would become 26 + 3 = 29 - 26 = 3 (or 29 modulo 26 = 3).

For maketrans, we are going to build 26 translation tables. For a given number or shift, the translation table will be the string of A..Z starting at the letter in the position of the shift and ending with the last number / shift of letters in the string A..Z. For example, with a shift of +3, the table will be A..Z starting with position 3 which is D (Python starts counting at 0). We then take the rest of the letters until W. That will give us 23 characters. For the last three characters, we need the last three characters of the alphabet (XYZ). Once we have built all of the tables, we will translate our puzzle string using those tables, and get our answer (hopefully). Here is the code:

>>> import string
>>> tables = [str.maketrans(string.ascii_uppercase,string.ascii_uppercase[i:]+string.ascii_uppercase[:i]) for i in range(26)]
>>> riddle7 = 'Hwldp wx, Euxwh! QYAVL'.upper()
>>> for shift, table in enumerate(tables):
...     print('+{}: {}'.format(shift,riddle7.translate(table)))
... 
+0: HWLDP WX, EUXWH! QYAVL
+1: IXMEQ XY, FVYXI! RZBWM
+2: JYNFR YZ, GWZYJ! SACXN
+3: KZOGS ZA, HXAZK! TBDYO
+4: LAPHT AB, IYBAL! UCEZP
+5: MBQIU BC, JZCBM! VDFAQ
+6: NCRJV CD, KADCN! WEGBR
+7: ODSKW DE, LBEDO! XFHCS
+8: PETLX EF, MCFEP! YGIDT
+9: QFUMY FG, NDGFQ! ZHJEU
+10: RGVNZ GH, OEHGR! AIKFV
+11: SHWOA HI, PFIHS! BJLGW
+12: TIXPB IJ, QGJIT! CKMHX
+13: UJYQC JK, RHKJU! DLNIY
+14: VKZRD KL, SILKV! EMOJZ
+15: WLASE LM, TJMLW! FNPKA
+16: XMBTF MN, UKNMX! GOQLB
+17: YNCUG NO, VLONY! HPRMC
+18: ZODVH OP, WMPOZ! IQSND
+19: APEWI PQ, XNQPA! JRTOE
+20: BQFXJ QR, YORQB! KSUPF
+21: CRGYK RS, ZPSRC! LTVQG
+22: DSHZL ST, AQTSD! MUWRH
+23: ETIAM TU, BRUTE! NVXSI
+24: FUJBN UV, CSVUF! OWYTJ
+25: GVKCO VW, DTWVG! PXZUK

There is a lot of junk in there, but if we look at +23, we have our solution: ETIAM TU, BRUTE! NVXSI. The solution is a reference to a Caesar cipher which is the type of shift cipher we have talked about where the number used to shift the characters is constant for the whole message. You could have a cipher where the number of shifts per character is different.

Riddle 8


        84 97 107 101 32 116 104 105 115 58 32 71 89 53 84 70
This is just a bunch of numbers. However, they have something in common. They all fall between 32 and 116. This means they fit nicely within the printable characters in the ASCII character set. Each number in ASCII represents a character. ASCII characters are represented by 8 bits which gives 2^8 or 256 possible characters. Not all of those are printable, and the most common printable characters lie between 32 and 126. If we get out an ASCII table, we can translate the message to get our solution: Take this: GY5TF

Riddle 9


        Just a bit: /2mi4AMj
I thought this was a bit shift or something like that, but that did not pan out. I spent a while on this one trying to flip or shift bits in the string. After a while, I was getting nowhere, so I took a break and browsed the Internet. I saw a shortened URL that looked a little similar (letters and numbers). Then I thought back to the clue a bit...bit.ly is a URL shortener.

So I tried going to bit.ly/2mi4AMj, and this is what I got:
This is probably the puzzle I liked the least because it was very obtuse. However, we got the solution: 5DFME

Riddle 10


        No comment.
At first, this does not seem very useful, but there are often interesting things in the comments of HTML pages. If we look there, we find out solution: A43JN


Riddle 11


        👻👽👻👻👻👻👽👽👻👽👻👻👽👽👽👽👻👽👻👻👽👽👽👻👻👽👻👻👻👽👽👽
        👻👽👻👽👻👻👽👻👻👽👻👻👻👻👻👽👻👽👻👽👻👽👻👻👻👽👻👽👻👻👽
        👽👻👻👽👻👻👻👻👽👻👻👽👻👻👻👻👻👻👽👻👻👽👽👽👻👻👻👽👽👻👽
        👻👽👻👽👻👽👽👻👻👻👻👽👻👻👻👽👽👽👻👽👻👻👽👻👽👽

This was another tough one at first. I was trying to figure out if there was any significance to the bytes that comprise the alien (UTF-32 1F47D / UTF-16 D8 3D DC 7D / UTF-8 F0 9F 91 BD) and the ghost (UTF-32 1F47B / UTF-16 D8 3D DC 7B / UTF-8 F0 9F 91 BB), but nothing was popping out. After a while, I took a step back and looked at what I had. There is essentially a two character alphabet here: an alien and a ghost, like binary: 0 and 1. I was unsure if the alien was a 0 or 1 (the ghost would be the opposite). Let's see what happens if we say the alien is 0, and the ghost is 1:
        101111001011000010110001101110001010110110111110101010111010110011011110110111111011000111001010101001111011100010110100

Let's try to turn this into bytes:

>>> alien0 = int('0b10111100101100001011000110111000101011011011
1110101010111010110011011110110111111011000111001010101001111011100010110100', 2)
# Using big endian because this is a normal binary string where the left bit is the most signficant
>>> alien0.to_bytes((alien0.bit_length() + 7) // 8, 'big')
b'\xbc\xb0\xb1\xb8\xad\xbe\xab\xac\xde\xdf\xb1\xca\xa7\xb8\xb4'

The resulting byte string is not printable, so maybe the alien is 1 and the ghost is 0:

>>> alien1 = int('0b010000110100111101001110010001110101001001000001
010101000101001100100001001000000100111000110101010110000100011101001011', 2)
>>> alien1.to_bytes((alien1.bit_length() + 7) // 8, 'big')
b'CONGRATS! N5XGK'

Looks like we have our solution: CONGRATS! N5XGK

Riddle 12


         697c611778601371647d12177e7d060572
         3133333731333337313333373133333731

These are two 34 byte strings, so they are not a hash that I am aware of. The lower string looks a bit strange because it is a repeating pattern, so maybe it is some sort of key. One type of cipher is a one time pad where the plaintext is XORed with a byte string that is used only once. To decrypt it, you need to XOR the cipertext with the key. Maybe we should try that with the second line being the key:
        Split into two byte chunks:
        69 7c 61 17 78 60 13 71 64 7d 12 17 7e 7d 06 05 72
        31 33 33 37 31 33 33 37 31 33 33 37 31 33 33 37 31

   XOR: 58 4F 52 20 49 53 20 46 55 4E 21 20 4F 4E 35 32 43

        Translate into ASCII:
        XOR IS FUN! ON52C
Looks like we found our solution: XOR IS FUN! ON52C

Riddle 13


        URER LBH TB: MJX4E

This looks a lot like riddle 7. Let's try a Ceasar cipher on this string:

# The translation tables are the same as Riddle 7
>>> riddle13 = 'URER LBH TB: MJX4E'
>>> for shift, table in enumerate(tables):
...     print('+{}: {}'.format(shift,riddle13.translate(table)))
... 
+0: URER LBH TB: MJX4E
+1: VSFS MCI UC: NKY4F
+2: WTGT NDJ VD: OLZ4G
+3: XUHU OEK WE: PMA4H
+4: YVIV PFL XF: QNB4I
+5: ZWJW QGM YG: ROC4J
+6: AXKX RHN ZH: SPD4K
+7: BYLY SIO AI: TQE4L
+8: CZMZ TJP BJ: URF4M
+9: DANA UKQ CK: VSG4N
+10: EBOB VLR DL: WTH4O
+11: FCPC WMS EM: XUI4P
+12: GDQD XNT FN: YVJ4Q
+13: HERE YOU GO: ZWK4R
+14: IFSF ZPV HP: AXL4S
+15: JGTG AQW IQ: BYM4T
+16: KHUH BRX JR: CZN4U
+17: LIVI CSY KS: DAO4V
+18: MJWJ DTZ LT: EBP4W
+19: NKXK EUA MU: FCQ4X
+20: OLYL FVB NV: GDR4Y
+21: PMZM GWC OW: HES4Z
+22: QNAN HXD PX: IFT4A
+23: ROBO IYE QY: JGU4B
+24: SPCP JZF RZ: KHV4C
+25: TQDQ KAG SA: LIW4D

Looks like this is a Ceasar cipher with a shift of +13: HERE YOU GO: ZWK4R

Riddle 14


        89504E470D0A1A0A0000000D494844520000001D0000000708020000007BBCD1A5000000017352474200
        AECE1CE90000000467414D410000B18F0BFC6105000000097048597300000EC300000EC301C76FA86400
        00001874455874536F667477617265007061696E742E6E657420342E302E36FC8C63DF000001AA494441
        5428534D513DC8416118BD7E4A19180C0665A0582C8C7E22DF20C5480A130629060CF29792C16CB06293
        C82283C2F0C562540693C94F297F2983FB9DEBF9BEDB77A673CE7DEE799FF3BE0CFB81C160904824C409
        8542C1E17098CDE672B90C399D4E9D4EA740208846A39BCD2693C9300CF3F5079A29168B56ABD5E572B5
        DBEDDF5C954A85B9D3E944D2EBF5D66AB5DBEDF67EBF5BADD6EBF51A8D4676BB1D9F7ABD9EDFEF877FBF
        DFE3F1782E97BB5EAFF0B1473A9D063F1C0E1A8D86CB1D8FC7D8CBE3F1341A0DC8C964A2502840FE83CF
        050987C3642693C96AB54A1C558810B8DC4824321C0E178B85CD66836C369BD80244A7D3A194DBED7E3C
        1E8893CBE5E804743A1DEE57964DA552F57A1D64B7DB994C2632095CAE4C260B8542C160502A95AED7EB
        C160100804E05F2E97E3F1882054E6F7DDEFF770CEE73378369BA5DCE7F3899B04E1C174BBDD582CF6FD
        01162F954AD84EA9542E974B9A108BC5FF7301AD564B2F91CFE729174033BC1BF17EBFCFF87CBEF97C4E
        7ABBDDEAF57A90D96C66341A3FA519FE5AD56A35A44824C2D99F71B652A9F0B9ABD5CA62B16040281426
        12891FA2F7838B729D41E80000000049454E44AE426082

This looks like a bunch of bytes, so let's try un-hexing them:

>>> riddle14='89504E470D0A1A0A0000000D494844520000001D000000070802...'
>>> binascii.unhexlify(riddle14)
# Line breaks inserted for formatting.  This would normally be one long string.
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x1d\x00\x00\x00\x07\x08\x02\x00
\x00\x00{\xbc\xd1\xa5\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\x04gAMA
\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00\tpHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01
\xc7o\xa8d\x00\x00\x00\x18tEXtSoftware\x00paint.net 4.0.6\xfc\x8cc\xdf\x00\x00\x01
\xaaIDAT(SMQ=\xc8Aa\x18\xbd~J\x19\x18\x0c\x06e\xa0X,\x8c~"\xdf \xc5H\n\x13\x06)\x06
\x0c\xf2\x97\x92\xc1l\xb0b\x93\xc8"\x83\xc2\xf0\xc5bT\x06\x93\xc9O)\x7f)\x83\xfb\x9d
\xeb\xf9\xbe\xdbw\xa6s\xce}\xeey\x9f\xf3\xbe\x0c\xfb\x81\xc1`\x90H$\xc4\t\x85B\xc1\xe1p
\x98\xcd\xe6r\xb9\x0c9\x9dN\x9dN\xa7@ \x88F\xa3\x9b\xcd&\x93\xc90\x0c\xf3\xf5\x07\x9a)
\x16\x8bV\xab\xd5\xe5r\xb5\xdb\xed\xdf\\\x95J\x85\xb9\xd3\xe9D\xd2\xeb\xf5\xd6j\xb5\xdb
\xed\xf6~\xbf[\xad\xd6\xeb\xf5\x1a\x8dFv\xbb\x1d\x9fz\xbd\x9e\xdf\xef\x87\x7f\xbf\xdf\xe3
\xf1x.\x97\xbb^\xaf\xf0\xb1G:\x9d\x06?\x1c\x0e\x1a\x8d\x86\xcb\x1d\x8f\xc7\xd8\xcb\xe3\xf14
\x1a\r\xc8\xc9d\xa2P(@\xfe\x83\xcf\x05\t\x87\xc3d&\x93\xc9j\xb5J\x1cU\x88\x10\xb8\xdcH$2
\x1c\x0e\x17\x8b\x85\xcdf\x83l6\x9b\xd8\x02D\xa7\xd3\xa1\x94\xdb\xed~<\x1e\x88\x93\xcb
\xe5\xe8\x04t:\x1d\xeeW\x96M\xa5R\xf5z\x1dd\xb7\xdb\x99L&2\t\\\xaeL&\x0b\x85B\xc1`P*\x95
\xae\xd7\xeb\xc1`\x10\x08\x04\xe0_.\x97\xe3\xf1\x88 T\xe6\xf7\xdd\xef\xf7p\xce\xe73x6\x9b
\xa5\xdc\xe7\xf3\x89\x9b\x04\xe1\xc1t\xbb\xddX,\xf6\xfd\x01\x16/\x95J\xd8N\xa9T.\x97K\x9a
\x10\x8b\xc5\xffs\x01\xadVK/\x91\xcf\xe7)\x17@3\xbc\x1b\xf1~\xbf\xcf\xf8|\xbe\xf9|Nz\xbb
\xdd\xea\xf5z\x90\xd9lf4\x1a?\xa5\x19\xfeZ\xd5j5\xa4H$\xc2\xd9\x9fq\xb6R\xa9\xf0\xb9\xab
\xd5\xcab\xb1`@(\x14&\x12\x89\x1f\xa2\xf7\x83\x8br\x9dA\xe8\x00\x00\x00\x00IEND\xaeB`\x82'

There are some interesting ASCII strings in here: PNG, paint.net, IEND. This looks like a PNG file. Let's write it out and see what we get:

>>> with open('riddle14.png', 'wb') as riddle14_output:
...     riddle14_output.write(binascii.unhexlify(riddle14))
... 
569

This is what we get:

There is our solution: AGBTC

Riddle 15


         FRIDAY THE THIRTEENTH, 4:00 PM
         /([FOR]*)([ID]{2})([^N]*)(.)(.*)/g
         $2E$44
The second line looks like a regular expression. The .* and ([ID]{2}) give it away. So we need to apply the regex on the second line to the first line:

>>> import re
>>> riddle15_re = re.compile('([FOR]*)([ID]{2})([^N]*)(.)(.*)')
>>> riddle15_re.match('FRIDAY THE THIRTEENTH, 4:00 PM').groups()
('FR', 'ID', 'AY THE THIRTEE', 'N', 'TH, 4:00 PM')
So we have six different groups. This does not look like a solution, so we are probably not done yet. We still have one more line to take care of. $2 means capture group 2. In this case, that is 'ID'. The E is a literal E, and $4 means the fourth capture group ('N'). The final 4 is a literal 4, so if we put this together, we get our solution: IDEN4

Riddle 16


Okay, the final one. What do we have?
        <~<+oue+DGm>FD,5.CghC,+E)./+Ws0B9h&:~>
That looks like a bunch of junk. By this time, I had become very familiar with the Binary to Text encoding page on Wikipedia that I linked above. I recognized this as Ascii85. Luckily, Python can decode this for us:

>>> riddle16 = '<~<+oue+DGm>FD,5.CghC,+E)./+Ws0B9h&:~>'
# We need to use adobe because the Python docs say:
# "adobe controls whether the input sequence is in Adobe Ascii85 format
# (i.e. is framed with <~ and ~>)."
>>> base64.a85decode(riddle16, adobe=True)
b'This is the last one! DFMFZ'

And we have our final solution: This is the last one! DFMFZ

Wrapping It Up


Even though we have found all of the solutions, we are not done yet. We need to combine them in the right way and derive the final solution. Let's see what we have so far:
        'A2DBM', 'ZXGID', 'XIZLS', 'VYGY6',
        'EBQSA', 'JAOMY', 'NVXSI', 'GY5TF',
        '5DFME', 'A43JN', 'N5XGK', 'ON52C',
        'ZWK4R', 'AGBTC', 'IDEN4', 'DFMFZ'

Because I was familiar with binary encodings, this looks like Base32 because of the alphabet: A-Z and 2-7. Unfortunately, we cannot just ask the computer to try all 16! (16 factorial) permutations of these 16 strings. That would be a ton of operations, and we do not have time for that. However, we can combine as many small chunks as we can to make the problem easier to solve. I wrote the following Python snippet and used it as a base to solve this:

import itertools
import base64
import binascii

strings = ['A2DBM', 'ZXGID', 'XIZLS', 'VYGY6', 'EBQSA', 'JAOMY',
           'NVXSI', 'GY5TF', '5DFME', 'A43JN', 'N5XGK', 'ON52C',
           'ZWK4R', 'AGBTC', 'IDEN4', 'DFMFZ']

for x in itertools.permutations(strings):
    try:
        print('{}: {}'.format(' '.join(x), base64.b32decode(''.join(x)).decode('utf-8')))
    except:
        continue

itertools.permutations allows you to specify how large you want the permutations to be. I started with 2, and that did not yield anything interesting, so I increased it to 3, and something fell out:
        N5XGKIDEN4ZXGID: one do3s

For the smaller sets of strings, I had to pad the base32 decode with equal signs because base32 strings must be a multiple of 40 bits or be padded to a multiple of 40 bits. For permutations of 3, I needed to pad the base32 string with one =:

import itertools
import base64
import binascii

strings = ['A2DBM', 'ZXGID', 'XIZLS', 'VYGY6', 'EBQSA', 'JAOMY', 'NVXSI',
           'GY5TF', '5DFME', 'A43JN', 'N5XGK', 'ON52C', 'ZWK4R', 'AGBTC', 'IDEN4', 'DFMFZ']

for x in itertools.permutations(strings, 3):
    try:
        print('{}: {}'.format(' '.join(x), base64.b32decode(''.join(x) + 1 * '=').decode('utf-8')))
    except:
        continue
I know I did not have to put 1 * '=', but it was easier because I was trying to different paddings with different lengths of permutations. I also tried with permutations of length 4, and another string fell out:
        EBQSA5DFMEZWK4RAGBTC:  a tea3er 0f

So, instead of 16 strings, we only have 11. Here is the final code:

import itertools
import base64
import binascii

strings = ['A2DBM', 'XIZLS', 'VYGY6', 'JAOMY', 'GY5TF', 'A43JN', 'ON52C',
           'DFMFZ', 'N5XGKIDEN4ZXGID', 'NVXSI', 'EBQSA5DFMEZWK4RAGBTC']

for x in itertools.permutations(strings):
    try:
        print('{}: {}'.format(' '.join(x), base64.b32decode(''.join(x)).decode('utf-8')))
    except:
        continue


This ran for an hour or so and eventually popped out the answer:

N5XGKIDEN4ZXGIDON52CA43JNVYGY6JAOMYGY5TFEBQSA5DFMEZWK4RAGBTCA2DBMNVXSIDFMFZXIZLS: one do3s not simply s0lve a tea3er 0f hacky easter

And that is it! This teaser was a lot of fun, and I learned a lot along the way. Until next time!

1 comment: