Update 1: See the last section below (The Code) for my Ruby code.
Update 2: Yes, I've written a server that acts just like the real one and will post the code soon.
Update 3: Decompiled RankMe.class is here: RankMe.java
Update 4: The official t400 server source has been released.
Overview
The pt400 challenge was a game in which you had to correctly rank a set of 71 famous people (including hackers in both senses of the word [e.g., Kevin Mitnick, Richard Stallman], famous coders [e.g., Knuth], cryptographers [e.g., Bruce Schneier], etc.). I'll call them all hackers for simplicity.
The client
The game client was a simple Java Swing app called RankMe. You run it from the command line and it connects to the game server. Then, it presents two hackers and you have to pick the better one. If you pick the wrong hacker, the message "Better luck next time!" pops up and the game exits. The ranking of hackers was chosen by DDTek, I think somewhat arbitrarily, and is consistent from game to game.
Analysis
The first thing I tried was to analyze the game traffic with Wireshark. This was pretty fail. There was no JPEG/PNG/GIF/BMP data in the stream, and no understanding of the protocol immediately came to me. I could have spent more time analyzing it, but instead I decided to decompile the client with JD-GUI (http://java.decompiler.free.fr/). This worked perfectly and gave me RankMe.java, which was easy to read and understand. I discovered how the game protocol worked by reading this code.
The game protocol
The protocol is simple. Basically, the server sends two images to the client at a time, and after receiving each pair of images and displaying them to the user, the client sends back the user's guess. If the user is right, the client can download two new images. If the user is wrong, the server sends an error code and disconnects. If the user has guessed enough matches correctly, the server asks for the combination of guesses that won (a string of LRLR..). If the user correctly inputs that combination, the server sends a secret message which is the key for pt400.
Protocol in outline form:
- on connect, client sends string "illogical\n"
- client reads two JPEG images
- to read each image from the server:
- first, read four bytes and convert them to an integer datalen which is the number of bytes in the incoming picture (datalen is xor-encrypted with 0xc932f768)
- read datalen bytes of picture data (xor-encrypted by lower 8 bits of datalen)
- to read each image from the server:
- after user clicks a picture, client sends the user's guess:
- left picture: send 0x0 byte to server
- right picture: send 0x1 byte to server
- goto step 2, but sometimes step 2 will fail with an error code which indicates information about the user's most recent guess:
- if guess was wrong, the integer -1 or -2 will be read from server when attempting to get image length
- if game has been won (64 correct guesses in a row), -3 will be read, and client should send sequence of LRLR used to win game
- if sequence guess is correct, server responds with a secret phrase which is the key to pt400
How to win
My strategy for winning was simple. First, I needed to get the outcome of a bunch of matches so that I'd be able to predict future matches. To do that, I implemented the protocol in Ruby in a class called Game which could connect to the game server and play games forever, collecting match results in a file called gamelog. (The Game class played stupidly, always picking the left picture.) The server was able to run about one round a second, and I had about 3,000 match results by the time I was ready for the next step. The match results (gamelog) looked like this:
9032d9fc7f0eec084636a35a0a623e84b2ee91a0 beat 895de4c13c326b60730786288465f91b5e5ba982
61ed2a78cf49d8c7eaa598fcbd9805825fae291c beat b94386c69bb0f04efe15863c480a2ba73bb7895d
2aea1a34c5cea68d5dd0678f04749e956935a11f beat 7b2fc387378a45a70c9db5cd384d3729ece8b3e7
50232549de83970517f50a6956e905004534919b beat f2f84e9514243c187cda82828569994a49ca481b
6e29bc1551abebad76297c5e3028a14188298d77 beat 815fc7e20b430cb34e98b632e081c8cfc0cbf9fa
(I uniquely identified each hacker by the SHA1 hash of his picture. For example, ac1dburn was ab74176394caeb1158df9c4d9fac004fff27377a, and Tsutomu Shimomura was 39ddc6e687b0cd8255ea44a0a9a7b2cb88601454.)
The next step was to write an extension of the Game class called SmartGame which knew the results of all the matches and, given two hackers, could predict which one would win the match. Here's the prediction logic:
- given hacker1 and hacker2 and previous match data, predict a winner:
- when will hacker1 defeat hacker2?
- when hacker1 has beaten hacker2, or
- when any hacker that hacker1 has beaten, has beaten hacker2, or
- when any hacker that hacker1 has beaten, has beaten any hacker that has beaten hacker2
- etc. (recursion)
- when will hacker1 defeat hacker2?
- if the match data doesn't provide a conclusive answer, then predict based on statistics (wins/loss ratio for hacker1 and hacker2)
Pwnsauce
My original plan for SmartGame was to have it learn from the game as it played; however, it turned out that having a gamelog of 3,000 matches made this unnecessary. SmartGame was able to pick the correct hacker 100% of the time. It could connect to the server and win in less than 30 seconds (or maybe a little longer, but it was fast).
Update: I updated my code to learn from the game as it's played, just because it was an easy change.
The key
The secret key was None hold a candle to Bessie the sh33p!
The code: Try it out
My pt400 solver is written in Ruby. It's messy. Sorry for the delay in putting it up. I meant to do it right after quals.
http://github.com/cvonkleist/defcon18quals/tree/master/pt400/
Check it out of Github (or just download protocol.rb and smart.rb). To run it, do this: