Hacking 4 Yeezys

This week, Slam Jam Socialism ran an online arcade for a chance to win some Yeezy Boost 350s. My friend Marc sent me the link and asked if I could hack the leaderboards and cop some shoes. Here’s how I did it.

The game was a single-player version of Pong, written in plain JavaScript using <canvas>. Each time the ball hits a paddle, you get a point and it speeds up until you inevitably lose. While playing the game, I had Chrome’s Network tab open to see how it reported actions to the server.

So: every time you lose, the game sends a POST request to the site at /callback.php along with some parameters. Most notably, pointz. So you can just send a new request and set pointz to 99999, right? As I soon discovered, it’s not that easy. When I tried it, I got an error message: "Sorry, please try again". Hmm, my request was almost identical to the legitimate one, though. But what’s sec? It looks like a random hash…

Turns out that sec stands for secret, and the score is one of the inputs to the hash. If the server’s hash doesn’t match yours, then your score gets rejected. The hash must be generated client-side, so I just need to look through the source to find out where the POST request is sent:

    var _0x4d74=["...","\x73\x70\x6C\x69\x74","...","\x66\x72\x6F\x6D\x43\x68\x61\x72\x43\x6F\x64\x65","\x72\x65\x70\x6C\x61\x63\x65","\x5C\x77\x2B","\x5C\x62","\x67"];eval(function(_0x3e69x1,_0x3e69x2,_0x3e69x3,_0x3e69x4,_0x3e69x5,_0x3e69x6){_0x3e69x5=function(_0x3e69x3){return (_0x3e69x3<_0x3e69x2?_0x4d74[4]:_0x3e69x5(parseInt(_0x3e69x3/_0x3e69x2)))+((_0x3e69x3=_0x3e69x3%_0x3e69x2)>35?String[_0x4d74[5]](_0x3e69x3+29):_0x3e69x3.toString(36))};if(!_0x4d74[4][_0x4d74[6]](/^/,String)){while(_0x3e69x3--){_0x3e69x6[_0x3e69x5(_0x3e69x3)]=_0x3e69x4[_0x3e69x3]||_0x3e69x5(_0x3e69x3)};_0x3e69x4=[function(_0x3e69x5){return _0x3e69x6[_0x3e69x5]}];_0x3e69x5=function(){return _0x4d74[7]};_0x3e69x3=1};while(_0x3e69x3--){if(_0x3e69x4[_0x3e69x3]){_0x3e69x1=_0x3e69x1[_0x4d74[6]]( new RegExp(_0x4d74[8]+_0x3e69x5(_0x3e69x3)+_0x4d74[8],_0x4d74[9]),_0x3e69x4[_0x3e69x3])}};return _0x3e69x1}(_0x4d74[0],62,436,_0x4d74[3][_0x4d74[2]](_0x4d74[1]),0,{}))

The game is somehow in hex. No, wait—they’re variables but they just look like memory addresses. That eval is the code that gets executed. We can just console.log what is getting evaled:

$(document).ready(function(){window.addEventListener("load",function(){window.scrollTo(0,0)});var fbb=navigator.appVersion.indexOf("FBAN");if(fbb>-1){var fbbh=44}else{var fbbh=0}var canvas=document.getElementById("canvas"),ctx=canvas.getContext("2d"),W=$(window).width(),H=$(window).height()-fbbh,particles=[],ball={},paddles=[2],mouse={},points=0,fps=60,particlesCount=3,flag=0,particlePos={},multipler=1,startBtn={},restartBtn={},over=0,init,paddleHit;var mq=window.matchMedia("(min-width: 768px)");if(mq.matches){var offset=0}else{var offset=50}function player(){window.requestAnimFrame=(function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(callback){return window.setTimeout(callback,1000/60)}})();window.cancelRequestAnimFrame=(function(){return window.cancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.mozCancelRequestAnimationFrame||window.oCancelRequestAnimationFrame||window.msCancelRequestAnimationFrame||clearTimeout})();canvas.addEventListener("mousemove",trackPosition,true);canvas.addEventListener("mousedown",btnClick,true);canvas.addEventListener("touchmove",trackPositionT,true);canvas.addEventListener("touchend",btnClick,true);collision=document.getElementById("collide");collisionWall=document.getElementById("collide_wall");canv...

This is a lot better, but it’s still minified. Luckily, there’s jsnice to prettify it for us:

  // some code above...
  function start() {
  // some code here...
    over = 1;
    $.ajax({
      method : "POST",
      url : "callback-ism.php",
      data : {
        action : "setpoint",
        email : email,
        pointz : y,
        sec : s(email, y, token)
      }
    }).done(function(opt_classNames) {
	// more code below...

Well that worked way better than I expected. So sec is generated from a function s, which takes in email, y (which is pointz), and token. What’s s?

function s(email, val, v) {
  md5_iterations = parseInt(v.substring(0, 1));
  multiplier = parseInt(v.substring(4, 5));
  divider = parseInt(v.substring(17, 18));
  hash_calc = Math.ceil(val * multiplier / divider) + v + email;
  md5_iteration = 1;
  for (;md5_iteration <= md5_iterations;md5_iteration++) {
    hash_calc = m(hash_calc);
  }
  return hash_calc;
}

The hash is produced from iteratively hashing substrings of my email, the points I scored, and a token generated on each session that’s set server-side. Now all that’s left to do is calculate the hash of the number of points I want, and replace the parameters in the curl request with my new score and hash.

rekt

I still didn’t win the Yeezys though

Finding the edit distance between two strings

The edit distance between two strings refers to the minimum number of character insertions, deletions, and substitutions required to change one string to the other. For example, the edit distance between “kitten” and “sitting” is three: substitute the “k” for “s”, substitute the “e” for “i”, and append a “g”. Our particular metric, which allows insertions, deletions, and substitutions is called the Levenshtein distance (other variants exist, such as the LCS (longest common subsequence) distance, which disallows substitution). Edit distance is useful for measuring similarity between strings. Superficial similarity is implied here, as in: the content and meaning of the words don’t matter; for semantic similarity, look at Jaccard’s similarity coefficient. Let’s implement the Wagner-Fischer algorithm, which computes the edit distance between two strings.

The key insight required to understand this algorithm is – as usual – recursive. Notice that if we have the edit distance d between two strings minus the first character, then the actual edit distance will be d + 1 only if the first characters of both strings are not the same. Take the words “jewels” and “mogwai”: assuming we have the edit distance between “ewels” and “ogwai” (5), then the actual distance will be 6, since “j” and “m” are different. We can simply apply this rule until we hit the end of one (or both) of the strings. Then the edit distance will just be the length of the remaining string, as we just can add the requisite characters. The astute reader will notice that there is a mistake in our analysis, however: we’ve only covered the substitution rule. Evaluating “godspeed” and “speed” using this rule would result in an edit distance of 8, which is clearly wrong, since we should be able to do it in 3 moves by deleting “god” from “godspeed”. We can support suffix alignment by computing our first rule, then the edit distance between each string and the last n-1 characters of the other plus one, and then taking the minimum between all of these computations. This essentially ends up trying each path to see whether we can find a common fragment in both strings that isn’t necessarily aligned.

Here’s my implementation of our rule in Python:

1
2
3
4
5
6
7
8
def levenshtein(s1, s2):
    if len(s1) == 0 or len(s2) == 0:
        return max(len(s1), len(s2))

    return min(levenshtein(s1[1:], s2) + 1,
               levenshtein(s1, s2[1:]) + 1,
               levenshtein(s1[1:], s2[1:]) if s1[0] == s2[0]
               else levenshtein(s1[1:], s2[1:]) + 1)

However, this is really inefficient because of all the repeated subcomputations. Running this on the first two sentences of lorem ipsum doesn’t even terminate in a reasonable amount of time. We can memoize, essentially caching our subcomputations, which is a top-down dynamic programming strategy:

1
2
3
4
5
6
7
8
9
10
11
12
memo = {}
def levenshtein(s1, s2):
    if len(s1) == 0 or len(s2) == 0:
        return max(len(s1), len(s2))

    if (s1, s2) not in memo:
        memo[(s1, s2)] = min(levenshtein(s1[1:], s2) + 1,
                levenshtein(s1, s2[1:]) + 1,
                levenshtein(s1[1:], s2[1:]) if s1[0] == s2[0]
                else levenshtein(s1[1:], s2[1:]) + 1)

    return memo[(s1, s2)]

That’s easy, but boring. Plus, there’s still a lot of memory overhead from the stack frames generated from all the recursion. Let’s take a bottom-up approach to the problem instead. The actual Wagner-Fischer algorithm uses a two-dimensional array (or matrix) to hold the edit distances between prefixes of each string. The array (let’s call it A) has size m by n, where m and n are one plus the lengths of the first and second strings s1 and s2, respectively. What is stored at each index i, j is the edit distance between s1[:i] and s2[:j]. Then, from our work above, we have the following relation:

\[A[i, j] = \cases{ i & \text{if } j = 0\cr j & \text{if } i= 0\cr A[i - 1, j - 1] & \text{if } s1[i] = s2[j]\cr min(A[i-1, j], A[i, j-1], A[i - 1, j - 1]) + 1 & \text{if } s1[i] \neq s2[j] }\]

Notice that the recursive part of the relation depends on the values directly above, left, and upper left. This means we have to fill in the matrix in a certain order. My solution works like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def levenshtein(s1, s2):
    x = len(s1) + 1 # the length of the x-coordinate
    y = len(s2) + 1 # the length of the y-coordinate

    A = [[-1 for i in range(x)] for j in range(y)]
    for i in range(x):
        A[0][i] = i

    for j in range(y):
        A[j][0] = j

    for i in range(1, y):
        for j in range(1, x):
            if s1[j- 1] == s2[i - 1]:
                A[i][j] = A[i - 1][j - 1]
            else:
                A[i][j] = min(
                        A[i - 1][j] + 1,
                        A[i][j - 1] + 1,
                        A[i - 1][j - 1] + 1
                        )
    return A[y - 1][x - 1] # return the edit distance between the two strings

And now you know how to compute the Levenshtein distance! As an exercise, try reconstructing the actual transformations to go from s1 to s2.

Yelp

I’m super excited to announce that I will be interning at Yelp in San Francisco for the upcoming summer! I’ll be joining the Consumer Team and working all over the stack.

Finding the most uncommon words

Did you know that the URLs generated in Berri aren’t short hashes but actually just really uncommon English words? If you thought your room name was randomly generated because it’s unrecognizable, it might be interesting to look up what the definition is. Anyway, getting a list of uncommon words isn’t too complicated, but it is fun and I wanted to write a blog post so I’ll show you the nitty gritty.

If we remove all the common words from the set of all words, then it follows that we are left with only the uncommon ones. An easy source for the set of all words is /usr/share/dict/words, which is just a pre-installed file with newline-separated words. I say easy because everyone already has a copy and because it’s in a nice, parseable format. Note, however, that it isn’t necessarily a good dictionary: there are ~20000 words compared to the Oxford English Dictionary’s ~600000. But I think it’s a worthwhile tradeoff to not having to scrape the OED. What about the set of common words? Well, we can scrape some text from the web, and, if we find a word that’s being used, we can consider it common enough to throw away. Let’s begin!

It turns out that we don’t need to scrape anything. Data dumps of all of Wikipedia’s articles are readily available, so let’s grab the latest one from the Simple Wikipedia data dump. Why Simple Wikipedia? The Simple English Wikipedia dump is 473 MB uncompressed, which when compared to the regular English Wikipedia’s 44 GB uncompressed and considering the amount of parsing and checking we are going to have to do suddenly seems very reasonable. It also should contain very commonly-used words. Anyways:

wget http://dumps.wikimedia.org/simplewiki/latest/simplewiki-latest-pages-articles.xml.bz2

bzip2 simplewiki-latest-pages-articles.xml.bz2

Let’s take a look at the structure of the data:

<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.9/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.9/ http://www.mediawiki.org/xml/export-0.9.xsd" version="0.9" xml:lang="en">
  <siteinfo>
    ...
  </siteinfo>
    <page>
    <title>April</title>
    <ns>0</ns>
    <id>1</id>
    <revision>
      <id>4784983</id>
      <parentid>4657771</parentid>
      <timestamp>2014-04-18T02:15:54Z</timestamp>
      <contributor>
        <ip>61.199.127.79</ip>
      </contributor>
      <comment>made the grammar better</comment>
      <text xml:space="preserve">
'''April''' is the fourth [[month]] of the [[year]], and comes between [[March]] and [[May]]. It has 30 [[day]]s. April begins on the same day of week as [[July]] in all years and also [[January]] in leap years.
...
</text>
      <sha1>jk9e5is1yxp1resnscpooes74s5fnc1</sha1>
      <model>wikitext</model>
      <format>text/x-wiki</format>
    </revision>
  </page>

So all the data is in the <text> tag under <revision> in each <page>. We can use xsltproc to parse the XML using the following stylesheet:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>
  <xsl:template match="mediawiki">
    <xsl:for-each select="page/revision">
      <xsl:value-of select="text"/>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Save the stylesheet as stylesheet.xsl and run xsltproc stylesheet.xsl simplewiki-latest-pages-articles.xml > wiki-raw-text. You can take a look at the output with less wiki-raw-text. It looks like there’s still some scrubbing to do: we have to remove whitespace and other non-alphabetic characters and put each word in its own line so we can parse it the same way as /usr/share/dict/words. Let’s break out some good old UNIX tools.

First, we replace all non-alphabetic characters with spaces:

tr -c "[:alpha:]" " " < wiki-raw-text > wiki-alpha-text

Next, slam everything down to lowercase (oh yeah, do this for /words too):

tr '[:upper:]' '[:lower:]' < wiki-alpha-text > wiki-lower-text

Finally, we can use sed to strip all whitespace and replace it with a newline, piping the result to sort and then uniq to only get unique words:

sed -e "s/[[:space:]] */\n/g" wiki-lower-text | sort | uniq > wiki-parsed-text

(If you are on OS X, you will have to download gnu-sed using brew install gnu-sed for sed to read the newline character.)

Now we’ve got a complete set of words and a set of common words. The running time for removing the common words from the complete set naively can get as bad as O(m * n) where m and n are the sizes of the sets. We can do a lot better using a Trie, which is a super hip data structure optimized for looking up and deleting strings. Looking up a word in a trie is only O(len(word)), which doesn’t vary too widely, so removing a set M from the complete set should only take O(M) (after inserting all words, of course). Here’s my Python implementation of a Trie:

1
2
3
4
5
6
7
8
9
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
class Trie:
    def __init__(self):
        self._trie = {"word": ""}

    def insert(self, x):
        trie = self._trie

        for index, char in enumerate(x):
            if char not in trie:
                trie[char] = {}
            trie = trie[char]
            if index == (len(x) - 1) :
                trie["word?"] = True
        return None

    def delete(self, x):
        trie = self._trie
        try:
            for index, char in enumerate(x):
                trie = trie[char]
                if index == (len(x) - 1):
                    trie["word?"] = False
        except KeyError:
            return False
        return None

    def get_all_strings(self):
        trie = self._trie
        queue = [trie]
        lst = []
        while queue:
            d = queue[0]
            current_word = d["word"]
            if d.get("word?"):
                lst.append(current_word)
            d.pop("word?", None)
            d.pop("word")

            keys = list(d.keys())
            for key in keys:
                d[key]["word"] = current_word + key
                queue.append(d[key])
            queue.pop(0)
        return lst

And my Python script that adds all the words to the trie, removes the ones we’ve parsed, and prints out the remaining ones:

1
2
3
4
5
6
7
8
9
10
11
12
from trie import Trie
dicttrie = Trie()

with open("dict-parsed") as dictfile, open("wiki-parsed-text") as wikifile:
    for line in dictfile:
        word = line.rstrip("\n")
        dicttrie.insert(word)
    for line in wikifile:
        word = line.rstrip("\n")
        dicttrie.delete(word)
    for word in dicttrie.get_all_strings():
        print(word)

Now:

python3 words.py > uncommon-words

And there you have it. uncommon-words now contains the most uncommon words in the dictionary, such as

  • joskin
  • kendir
  • glazily
  • rubelet
  • suiform
  • bursicle

Cool beans.

Edit: My friend Jay pointed out that using the built-in set type was much faster than using a trie:

1
2
3
4
5
6
7
8
9
10
with open("dict-parsed") as dictfile, open("wiki-parsed-text") as wikifile:
    all_words = set()
    for line in dictfile:
        word = line.rstrip("\n")
        all_words.add(word)
    common_words = set()
    for line in wikifile:
        word = line.rstrip("\n")
        common_words.add(word)
    print(all_words - common_words)

takes about 1.5 seconds versus my trie-based implementation, which took 37 seconds. But that is lame and boring and I implemented a really cool data structure for a real-world application so who’s really the winner here Jay?

It’s me. I am the winner.

Hello, world!

Hi, I’m Lawrence. I’m a Computer Science student at the University of Toronto. I’m interested in startups, software development, programming languages, and systems design.

I hack with vim, git, and my Mac.