As part of it’s “Summer Games” update, Overwatch put an easter-egg into their video, embedding some base64-encoded text into a scene.
Unfortunately there may be errors since the font doesn’t clearly distinguish between certain characters such as 1/l/I, but one interpretation comes out to binary data like:
1 2 | >>> binascii.b2a_qp(binascii.a2b_base64("U2FsdGVkX1+vupppZksvRf5pq5g5XjFRlipRkwB0K1Y96Qsv2L m+31cmzaAILwytX/z66ZVWEQM/ccf1g+9m5Ubu1+sit+A9cenD xxqklaxbm4cMeh2oKhqlHhdaBKOi6XX2XDWpa6+P5o9MQw==")) 'Salted__=AF=BA=9AifK/E=FEi=AB=989^1Q=96*Q=93=00t+V=3D=E9=0B/=D8=B9=BE=DFW&=\n=CD=A0=08/=0C=AD_=FC=FA=E9=95V=11=03?q=C7=F5=83=EFf=E5F=EE=D7=EB"=B7=E0=3Dq=\n=E9=C3=C7=1A=A4=95=AC[=9B=87=0Cz=1D=A8*=1A=A5=1E=17Z=04=A3=A2=E9u=F6\\5=A9k=\n=AF=8F=E6=8FLC' |
This seems to be from an OpenSSL command-line encryption utility, since it begins with the bytes for “Salted__”. I’m really no crypto expert, but this is interesting. Maybe I can at least run a dictionary-attack against it?
The first thing to note about the puzzle is that it has an odd number of bytes, implying that that a “stream” cipher was used rather than a “block” cipher, or at least a block cipher being used in a streaming mode. Our chances of guessing the right one are somewhat slim anyway, so why not start with a simple stream-cipher, RC4? First we need to figure out how it’s implemented in the OpenSSL command-line tools. Let’s try a simple example using “x” (one byte) as our secret message, and “test” as our key:
1 2 3 4 5 6 7 8 | $ hexdump temp.txt 0000000 0078 0000001 $ openssl RC4 -S FFFFFFFFFFFFFFFF -k "test" -p -in temp.txt -a salt=FFFFFFFFFFFFFFFF key=D7BA581CCB7DBAFD5BD1C7DF8BDDE4E3 U2FsdGVkX1///////////5Y= |
By forcing the salt to a known 8-byte pattern (-S) and by telling OpenSSL to show us the key it created (-p) we know that somehow all those FF’s and “test” combined to make D7BA581CCB7DBAFD5BD1C7DF8BDDE4E3. This is useful, because with a small amount of trial-and-error I can figure out how it is generated, leading to this Python script which gives the same key-output:
1 2 3 4 5 6 7 | import binascii from Crypto. Hash import MD5 password = "test" salt = binascii.a2b_hex( "FFFFFFFFFFFFFFFF" ) key = MD5.new(password + salt).digest() print binascii.b2a_hex(key) # Output: d7ba581ccb7dbafd5bd1c7df8bdde4e3 |
Now we’re one step on the way to scripting up a compatible RC4 encoding routine, which ends up looking like:
01 02 03 04 05 06 07 08 09 10 11 | import binascii from Crypto. Hash import MD5 from Crypto.Cipher import ARC4 def encryptString( self , in_str, password): salt = binascii.a2b_hex( 'FF' * 8 ) tempkey = MD5.new(password + salt).digest() print binascii.b2a_hex(tempkey) cipher = ARC4.new(tempkey) enc = cipher.encrypt(in_str) return 'Salted__' + salt + enc |
Great! Now we can implement decoding, and after a little more tinkering, and we’ve got a little Python class which can encrypt/decrypt the same as the command-line tool:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | import binascii from Crypto. Hash import MD5 from Crypto.Cipher import ARC4 from Crypto import Random class SimpleRc4: def __init__( self ): self .random = Random.new() self .header = "Salted__" self .saltLen = 8 def encryptString( self , in_str, password): salt = self .random.read( self .saltLen) tempkey = MD5.new(password + salt).digest() cipher = ARC4.new(tempkey) enc = cipher.encrypt(in_str) return self .header + salt + enc def decryptString( self , in_str, password): salt = in_str[ len ( self .header) : len ( self .header) + self .saltLen] body = in_str[ len ( self .header) + self .saltLen:] tempkey = MD5.new(password + salt).digest() cipher = ARC4.new(tempkey) dec = cipher.decrypt(body) return dec def selftest( self ): password = "selftest" a = "Content" b = self .encryptString(a,password) c = self .decryptString(b,password) assert (a = = c) |
So what’s this kind of thing useful for? Quickly trying lots and lots of decodings. Here, let’s build up a brute-forcing framework, and prove that we can crack a simple RC4-encoded message… (Github Gist)
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | #!/usr/bin/python import sys import itertools import binascii import StringIO from Crypto. Hash import SHA, MD5 from Crypto.Cipher import AES, ARC4 from Crypto import Random class Breaker: def __init__( self ,e,puzzle): self .e = e self .puzzle = puzzle self .last = None def attempt( self , password): self .last = password result = self .e.decryptString( self .puzzle, password) return result def comboAttack( self , sequence, tester): for pw in sequence: result = b.attempt(pw) if tester(result): yield (pw, result) @staticmethod def CheckBoringAscii(result): for c in result: d = ord (c) if d > 127 : return False elif d < 32 : return False return True @staticmethod def GenPasswordList(passwordFile): with open (passwordFile, 'rb' ) as pwdict: for line in pwdict: pw = line.strip() yield pw @staticmethod def GenBrute(charset, maxlength): for i in range ( 1 , maxlength + 1 ): for c in itertools.product(charset,repeat = i): yield ''.join(c) class SimpleRc4: def __init__( self ): self .random = Random.new() self .header = "Salted__" self .saltLen = 8 def encryptString( self , in_str, password): salt = self .random.read( self .saltLen) tempkey = MD5.new(password + salt).digest() cipher = ARC4.new(tempkey) enc = cipher.encrypt(in_str) return self .header + salt + enc def decryptString( self , in_str, password): salt = in_str[ len ( self .header) : len ( self .header) + self .saltLen] body = in_str[ len ( self .header) + self .saltLen:] tempkey = MD5.new(password + salt).digest() cipher = ARC4.new(tempkey) dec = cipher.decrypt(body) return dec def selftest( self ): password = "selftest" a = "Content" b = self .encryptString(a,password) c = self .decryptString(b,password) assert (a = = c) if __name__ = = "__main__" : e = SimpleRc4() e.selftest() sample = e.encryptString( "brute decode challenge" , "test" ) b = Breaker(e, sample) source = Breaker.GenBrute( 'abcdefghijklmnopqrstuvwxyz' , 4 ) tester = Breaker.CheckBoringAscii for pw,result in b.comboAttack(source, tester): print pw, result #Output: test brute decode challenge |
And… Yes! It manages to crack the sample.
I’ll explore applying this framework to the actual Overwatch puzzle in a follow-up post. I’ll need to put in some fuzzier logic to handle the fact that the source-data — the ciphertext — may be subtly corrupted by errors by humans who have to write it down from inside a video-frame.