Hacking Board Games With Python

First, a disclaimer: I in no way undermined the security or broke into any accounts. I got access to 0 privileged information, merely recorded information that was already presented by the website.

Recently, I’ve become re-enamored with an old school board game: Diplomacy). This board game, set in Europe around the turn of the century (19th-20th century, that is), features gameplay similar to Risk) or Axis and Allies. The key difference in Diplomacy is that the gameplay is entirely free of luck. There are no dice rolls, no random chance, your strength comes entirely from the other nations you can con into helping you. It is a game of wicked social engineering), where every player is trying to get good intelligence on every other player. It is not uncommon for players to ‘release’ a tidbit of information to one player, and see how long it takes to find it’s way back to them from a different player. The math nerd in my head visualizes the connections between these nations as a directed graph – but that’s another post.

I’ve been playing the game online with a friend from the computer science department here, and he made an interesting suggestion – the game may be vulnerable to something called a ‘side channel attack’. The basic concept behind a side channel attack is that there are trends and data that exist alongside the actual data you want access to. In Diplomacy, what I’d really like to have access to is the messages between other players, but that data is not available (and that would be cheating!) Web diplomacy, however, makes a certain amount of other data available. The player’s name, readiness, and online status are all displayed on every page load. With a little help from Python, it’s trivial to capture this data and use it to craft a side channel attack.

Example Status

Example Status in Diplomacy

Allow me to explain the image real quick. On the left, we have the status of the country. Western Canada has not readied any orders, and it’s likely he hasn’t logged in since the last turn. The Near East has orders plugged in, but they are provisional, he hasn’t indicated he’s ready for the turn to progress. India has orders in, and he’s sure about them. To the right of the player’s name is a green dot that indicates whether the player is currently online. In the image above, India is online, the other two players are not. The final piece of relevant data is the last seen time, indicating (we believe) the last time webdiplomacy served a page to that player.

I’m a big fan of making data open and easy to access, but sadly we’re not at the point where all websites have an easy to access data API. So we cannot just write a quick script to pull down a bunch of nicely formatted data, instead we have to scrape the website ourselves and grab what data we can. That’s where the beauty of Python comes into play. There are two simple and easy to use Python libraries that make this possible: Mechanize and Beautiful Soup. Mechanize emulates a fully featured web browser, allowing you to programmatically fill out forms, send mouse clicks, and navigate web pages. We will use it to log into Web Diplomacy and navigate to the game we want to watch. Beautiful Soup is a library that makes parsing html incredibly easy by allowing you to navigate the html tree rather than just the text of the html. Let’s look at the code

# Our imports. Nothing new or interesting here
    import sys, time, os
    from mechanize import Browser
    from BeautifulSoup import BeautifulSoup
     
    ## The url for the logon page
    LOGON_URL = 'http://webdiplomacy.net/logon.php'
     
    ## The url for the board you want to watch. You'll need to put in the game ID
    BOARD_URL = 'http://webdiplomacy.net/board.php?gameID='
     
    ## Webdiplomacy login information
    USERNAME= ''
    PASSWORD= ''
     
    ## Define a simple class to hold the status of a country
    class country:
        def __init__(self, cid, status, online):
            self.countryID = cid
            self.status = status
            self.online = online
     
    ## Our main function
    def fetch():
        # Create a browser object. This is a Mechanize class that simulates
        # a fully featured browser
        br = Browser()
     
        # Tell our browser to open a URL. In this case, navigate to the logon page
        br.open(LOGON_URL)
     
        # Tell our browser to navigate to the first (and only) form on the page
        br.select_form(nr=0)
     
        # This form has two input boxes, populate them with our username and password
        br['loginuser'] = USERNAME
        br['loginpass'] = PASSWORD
     
        # Click the submit button, and ignore the response
        br.submit()
     
        # Now that we are cookied (we've logged in) navigate to the board to watch
        br.open(BOARD_URL)
     
        # And tell our browser to load the page, storing the response we download in resp
        resp = br.reload()
     
        # Response is the full web response, we just care about the HTML portion, so save it off
        html = resp.read();
     
        # Now, we need to create a BeautifulSoup object. A BeautifulSoup object is a
        # tree based way to navigate an html page. So we can get the  tag
        # and navigate through its children  and  tags. (For example)
        # We will be using it to search for specific things on the page, like the ready icon.
        soup = BeautifulSoup(''.join(html))
     
        # We begin by looking for a div, whose id is chatboxtabs. soup.find() returns the first
        # result
        chatboxtabs = soup.find('div', {'id':'chatboxtabs'})
     
        # Set up an empty dictionary to store the status of each country using the ID as the key
        onlineDict = {}
     
        # The empty list of countries to return
        returnList = []
        # The country doing the checking is not in the list of chatboxes, so we have to record
        # its status separately
        missingID = 1
     
        # For each anchor (a) tag inside our div
        for chatbox in chatboxtabs('a'):
            # If the anchor tag has a span within it
            if(chatbox('span') != []):
                # Then prune out the country id. These spans have a class that looks like:
                # "country 3 memberstatusplaying"
                # All we care about is the integer in the middle
                theID = chatbox('span')[0]['class'].replace("country", "").split(" ")[0]
     
                # If the ID we read out is not the next id in order,
                # increment it. (This means missingID will stop at the scipter's country ID)
                if (theID == str(missingID)):
                    missingID += 1
     
                # If the country is online, then there is an image next to their name
                # Store the status for that country
                if (len(chatbox('img')) > 0):
                    onlineDict[theID] = True
                else:
                    onlineDict[theID] = False
     
        # Store the status of the scripting country
        onlineDict[str(missingID)] = True
     
        # Now we look for their readiness status. Each country has a td with a class
        # 'memberLeftSide'. We grab them all with soup.findAll
        details = soup.findAll('td', {'class':'memberLeftSide'})
     
        # Iterate through all the td elements
        for status in details:
            # There are a few we can skip because they have no images
            if (status.img != None):
                # The country ID is grabbed, just like the anchor tags above
                countryID = status.span.contents[2]['class']
                countryID = countryID.replace("country", "")
                countryID = countryID.split(" ")[0]
     
                # Get the alternate text for the image
                statStr = status.img['alt']
                statInt = 2 # Assume 'bad' status
     
                # Set our status integer based on the alt text
                if (statStr == "Not received"):
                    statInt = 1
                elif (statStr == "Ready"):
                    statInt = 4
                elif (statStr == "Completed"):
                    statInt = 3
     
                # Create a country object and store it in our return list
                returnList.append(country(countryID, statInt, onlineDict[countryID]))
     
        # Return our country list
        return returnList

Pretty neat, pretty straightforward, and easy to fix up for your needs. What’d we do with it? I’d recommend you read Matt’s blog entry for the full details. The concept in brief, is that Matt took the above function, tweaked it a little, and stuck it in a loop. Every 2 minutes, he polls the server, and records the results in a SQL database. Over the next few days, Matt gathered a whole pile of data about when players logged in, logged out, and when they changed their status. With a little data visualization wizardry, Matt pieced together that there were two countries who were logging in and changing their statuses together. This was enough to suggest that the players were communicating and coordinating their actions together, which turned out to be the case.