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:
>>> 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:
$ 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:
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:
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:
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)
#!/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.