{"id":1054,"date":"2016-08-03T17:45:24","date_gmt":"2016-08-04T00:45:24","guid":{"rendered":"http:\/\/technofovea.com\/blog\/?p=1054"},"modified":"2016-08-03T18:22:08","modified_gmt":"2016-08-04T01:22:08","slug":"experiments-in-decryption-overwatch-puzzle","status":"publish","type":"post","link":"http:\/\/technofovea.com\/blog\/archives\/1054","title":{"rendered":"Experiments in decryption &#8211; Overwatch puzzle"},"content":{"rendered":"<p>As part of it&#8217;s &#8220;Summer Games&#8221; update, Overwatch put an easter-egg into their video, embedding some base64-encoded text into a scene. <a href=\"http:\/\/technofovea.com\/blog\/wp-content\/uploads\/2016\/08\/ow_b64_hint.png\"><img loading=\"lazy\" class=\"size-medium wp-image-1080 aligncenter\" src=\"http:\/\/technofovea.com\/blog\/wp-content\/uploads\/2016\/08\/ow_b64_hint-300x75.png\" alt=\"Overwatch Base64 hint\" width=\"300\" height=\"75\" srcset=\"http:\/\/technofovea.com\/blog\/wp-content\/uploads\/2016\/08\/ow_b64_hint-300x75.png 300w, http:\/\/technofovea.com\/blog\/wp-content\/uploads\/2016\/08\/ow_b64_hint.png 753w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/a><\/p>\n<p>&nbsp;<\/p>\n<p>Unfortunately there may be errors since the font doesn&#8217;t clearly distinguish between certain characters such as 1\/l\/I, but one interpretation comes out to binary data like:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n&gt;&gt;&gt; binascii.b2a_qp(binascii.a2b_base64(&quot;U2FsdGVkX1+vupppZksvRf5pq5g5XjFRlipRkwB0K1Y96Qsv2L m+31cmzaAILwytX\/z66ZVWEQM\/ccf1g+9m5Ubu1+sit+A9cenD xxqklaxbm4cMeh2oKhqlHhdaBKOi6XX2XDWpa6+P5o9MQw==&quot;))\r\n'Salted__=AF=BA=9AifK\/E=FEi=AB=989^1Q=96*Q=93=00t+V=3D=E9=0B\/=D8=B9=BE=DFW&amp;=\\n=CD=A0=08\/=0C=AD_=FC=FA=E9=95V=11=03?q=C7=F5=83=EFf=E5F=EE=D7=EB&quot;=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'\r\n\r\n<\/pre>\n<p>This seems to be from an OpenSSL command-line encryption utility, since it begins with the bytes for &#8220;Salted__&#8221;. I&#8217;m really no crypto expert, but this is interesting. Maybe I can at least run a dictionary-attack against it?<\/p>\n<p>The first thing to note about the puzzle is that it has an odd number of bytes, implying that that a &#8220;stream&#8221; cipher was used rather than a &#8220;block&#8221; 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&#8217;s implemented in the OpenSSL command-line tools. Let&#8217;s try a simple example using &#8220;x&#8221; (one byte) as our secret message, and &#8220;test&#8221; as our key:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n$ hexdump temp.txt\r\n0000000 0078\r\n0000001\r\n\r\n$ openssl RC4 -S FFFFFFFFFFFFFFFF -k &quot;test&quot; -p -in temp.txt -a\r\nsalt=FFFFFFFFFFFFFFFF\r\nkey=D7BA581CCB7DBAFD5BD1C7DF8BDDE4E3\r\nU2FsdGVkX1\/\/\/\/\/\/\/\/\/\/\/5Y=\r\n<\/pre>\n<p>&nbsp;<\/p>\n<p>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&#8217;s and &#8220;test&#8221; 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:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nimport binascii\r\nfrom Crypto.Hash import MD5\r\n\r\npassword = &quot;test&quot;\r\nsalt = binascii.a2b_hex(&quot;FFFFFFFFFFFFFFFF&quot;)\r\nkey = MD5.new(password+salt).digest()\r\nprint binascii.b2a_hex(key) # Output: d7ba581ccb7dbafd5bd1c7df8bdde4e3\r\n<\/pre>\n<p>Now we&#8217;re one step on the way to scripting up a compatible RC4 encoding routine, which ends up looking like:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nimport binascii\r\nfrom Crypto.Hash import MD5\r\nfrom Crypto.Cipher import ARC4\r\n\r\ndef encryptString(self, in_str, password):\r\n    salt =  binascii.a2b_hex('FF' * 8)\r\n    tempkey = MD5.new(password+salt).digest()\r\n    print binascii.b2a_hex(tempkey)\r\n    cipher = ARC4.new(tempkey)\r\n    enc = cipher.encrypt(in_str)\r\n    return 'Salted__' + salt + enc\r\n<\/pre>\n<p>Great! Now we can implement decoding, and after a little more tinkering, and we&#8217;ve got a little Python class which can encrypt\/decrypt the same as the command-line tool:<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\nimport binascii\r\nfrom Crypto.Hash import MD5\r\nfrom Crypto.Cipher import ARC4\r\nfrom Crypto import Random\r\n\r\nclass SimpleRc4:\r\n    def __init__(self):        \r\n        self.random = Random.new()       \r\n        self.header = &quot;Salted__&quot;\r\n        self.saltLen = 8\r\n\r\n    def encryptString(self, in_str, password):        \r\n        salt =  self.random.read(self.saltLen)\r\n        tempkey = MD5.new(password+salt).digest()\r\n        cipher = ARC4.new(tempkey)\r\n        enc = cipher.encrypt(in_str)\r\n        return self.header + salt + enc\r\n    \r\n    def decryptString(self, in_str, password):        \r\n        salt = in_str[len(self.header) : len(self.header)+self.saltLen]\r\n        body = in_str[len(self.header)+self.saltLen:]\r\n        tempkey = MD5.new(password+salt).digest()        \r\n        cipher = ARC4.new(tempkey)\r\n        dec = cipher.decrypt(body)\r\n        return dec\r\n\r\n    def selftest(self):\r\n        password = &quot;selftest&quot;\r\n        a = &quot;Content&quot;                    \r\n        b = self.encryptString(a,password)                                        \r\n        c = self.decryptString(b,password)        \r\n        assert(a == c)\r\n<\/pre>\n<p>So what&#8217;s this kind of thing useful for? Quickly trying lots and lots of decodings. Here, let&#8217;s build up a brute-forcing framework, and prove that we can crack a simple RC4-encoded message&#8230; (<a href=\"https:\/\/gist.github.com\/DHager\/b53bc8d17dec0b957c537d3cfe0c1bc7\" target=\"_blank\">Github Gist<\/a>)<\/p>\n<pre class=\"brush: python; title: ; notranslate\" title=\"\">\r\n#!\/usr\/bin\/python\r\nimport sys\r\nimport itertools\r\nimport binascii\r\nimport StringIO\r\n\r\nfrom Crypto.Hash import SHA, MD5\r\nfrom Crypto.Cipher import AES, ARC4\r\nfrom Crypto import Random\r\n    \r\n\r\nclass Breaker:\r\n    def __init__(self,e,puzzle):\r\n        self.e = e\r\n        self.puzzle = puzzle\r\n        self.last = None\r\n            \r\n    def attempt(self, password):\r\n        self.last = password\r\n        result = self.e.decryptString(self.puzzle, password)                \r\n        return result\r\n    \r\n    def comboAttack(self, sequence, tester):\r\n        for pw in sequence:            \r\n            result = b.attempt(pw)\r\n            if tester(result):\r\n                yield (pw, result)\r\n                \r\n    @staticmethod\r\n    def CheckBoringAscii(result):\r\n        for c in result:\r\n            d = ord(c)\r\n            if d &gt; 127:\r\n                return False\r\n            elif d &lt; 32:\r\n                return False\r\n            \r\n        return True\r\n\r\n    @staticmethod\r\n    def GenPasswordList(passwordFile):        \r\n        with open(passwordFile,'rb') as pwdict:\r\n            for line in pwdict:            \r\n                pw = line.strip()\r\n                yield pw\r\n\r\n    @staticmethod\r\n    def GenBrute(charset, maxlength):        \r\n        for i in range(1, maxlength + 1):            \r\n            for c in itertools.product(charset,repeat=i):\r\n                yield ''.join(c)       \r\n\r\nclass SimpleRc4:\r\n    def __init__(self):        \r\n        self.random = Random.new()       \r\n        self.header = &quot;Salted__&quot;\r\n        self.saltLen = 8\r\n\r\n    def encryptString(self, in_str, password):        \r\n        salt =  self.random.read(self.saltLen)\r\n        tempkey = MD5.new(password+salt).digest()\r\n        cipher = ARC4.new(tempkey)\r\n        enc = cipher.encrypt(in_str)\r\n        return self.header + salt + enc\r\n    \r\n    def decryptString(self, in_str, password):        \r\n        salt = in_str[len(self.header) : len(self.header)+self.saltLen]\r\n        body = in_str[len(self.header)+self.saltLen:]\r\n        tempkey = MD5.new(password+salt).digest()        \r\n        cipher = ARC4.new(tempkey)\r\n        dec = cipher.decrypt(body)\r\n        return dec\r\n\r\n    def selftest(self):\r\n        password = &quot;selftest&quot;\r\n        a = &quot;Content&quot;                    \r\n        b = self.encryptString(a,password)                                        \r\n        c = self.decryptString(b,password)        \r\n        assert(a == c)\r\n\r\n\r\nif __name__ == &quot;__main__&quot;:\r\n   \r\n    e = SimpleRc4()    \r\n    e.selftest()\r\n    sample = e.encryptString(&quot;brute decode challenge&quot;, &quot;test&quot;)\r\n    \r\n    b = Breaker(e, sample)\r\n    \r\n    source = Breaker.GenBrute('abcdefghijklmnopqrstuvwxyz',4)\r\n    tester = Breaker.CheckBoringAscii\r\n    for pw,result in b.comboAttack(source, tester):\r\n        print pw, result \r\n    #Output: test brute decode challenge\r\n<\/pre>\n<p>And&#8230; Yes! It manages to crack the sample.<\/p>\n<p>I&#8217;ll explore applying this framework to the actual Overwatch puzzle in a follow-up post. I&#8217;ll need to put in some fuzzier logic to handle the fact that the source-data &#8212; the ciphertext &#8212; may be subtly corrupted by errors by humans who have to write it down from inside a video-frame.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>As part of it&#8217;s &#8220;Summer Games&#8221; update, Overwatch put an easter-egg into their video, embedding some base64-encoded text into a scene. &nbsp; Unfortunately there may be errors since the font doesn&#8217;t clearly distinguish between certain characters such as 1\/l\/I, but one interpretation comes out to binary data like: This seems to be from an OpenSSL [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[10,4],"tags":[30,31],"_links":{"self":[{"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/posts\/1054"}],"collection":[{"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/comments?post=1054"}],"version-history":[{"count":22,"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/posts\/1054\/revisions"}],"predecessor-version":[{"id":1084,"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/posts\/1054\/revisions\/1084"}],"wp:attachment":[{"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/media?parent=1054"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/categories?post=1054"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/technofovea.com\/blog\/wp-json\/wp\/v2\/tags?post=1054"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}