Code Poetry
and Text Adventures

by catid posted (>30 days ago) 4:38pm Sat. Jan 12th 2013 PST
Say you're writing an online multiplayer game, where players report their scores to the server after each game.  You want to make this more tamper-proof to help prevent cheating.  Here are some ideas:

Problems With Requiring Unanimity

The problem I see with all players having to upload the same scores is that if one player changes the score, then all of the scores are invalidated.  This can be exploited to deny another player a good positive score.

Solution: Relax It, Only Require Majority

A majority system would be worthwhile however, where as long as an equal majority of the players agree on the score, then it is accepted.  So for two players, the voting does nothing.  For three players, two players need to agree on the score.  For four players, at least two need to agree, etc.  That's the general idea.  Different types of scores would need special handling.

Majority method can only improve the situation in a multiplayer game.  Without it: One player can hack the game client and post a super-high score.  With it: You'd need multiple players who have all done this.  It raises the bar a bit.

Encryption

People looking at the network communications with the server could be able to tell which fields are being used for the score, which gives them insight into how the score is represented internally by the game.  To avoid giving much away, try encrypting all the score-related data into a base64 field in the HTTP request.  I don't mean to say that base64 is a type of encryption, just that you should encrypt it first into a binary blob, say with AES or something, then encode it with base64 for transmission.

You can use a fixed key or one that is already shared somehow with the server.  It doesn't make much difference in the long run because either system can be broken by reverse-engineering the client.  The goal is to just raise the bar of security above a casual observer.  For open-source libraries it does make a difference and the key should be provided to the library during initialization, because the library code is available, only the key would be hidden in the game code after it gets compiled.

Validation

Embed a checksum or hash of the score inside the encrypted base 64 message.  This would be a secret algorithm that does not need to be keyed.  Just pick a math function that is hard to figure out without reverse-engineering the client.  This is important so that someone cannot just flip bits in the encrypted field to choose a new, random, probably huge, score.  You could use JSON or tabs or something to serialize the different parts of the encrypted message payload.

Obfuscation: Hide the Score in Other Stuff

Give the field containing the score data a generic name.  Ideally use this same communication method for multiple message types aside from the score, so it would be harder to know which message contains the score, or even if a score is being sent.

Preventing Replay Attacks

A player can repost the same score over and over again unless there's some limit.  This needs to be written into the game logic: The server should only accept one score result until the next game starts.  But also a player should not be able to attain a good score once, and then replay the same response for all subsequent games.  We can prevent that in a generic and reusable way.

To avoid this, the validation step should also accept a nonce (number only used once) that is unique to each game, and is worked into the validation.  The server would provide the nonce.  If the client generates it then it won't help.  See the example protocol at the end of this post for how this can be accomplished.

For Open-Source Libraries

When the adversary can see the source code of this security code, your best bet is to add some kind of secret key specific to the game.  That way they still need to dig through the actual game code to find out how it works.  The user of your library can make the secret key different for each game that is played.  For instance, you could use the nonce discussed previously as input to a secret hash function that is embedded into the game code, and use the output of that as the key.  This way the adversary would need to reverse-engineer client code in addition to figuring out the key used.

Why Not Just Give Up?

It is always going to be the case that you cannot trust the client.  The crucial insight is to realize that while it cannot be PERFECT, it can be GOOD ENOUGH.

If it's too hard for a neophyte to hack with a web browser, then you reduce the chances of it being tampered with by a LOT.


Tying it All Together: An Example

So here's an example protocol that brings all this together:

When the game client code is compiled, a secret key is embedded into it called SECRET.  The server also knows this secret.  For a game SDK this could be provided to the SDK code by passing in a password string like Initialize("MyGame'sSecretPassword"); that the server also knows.

Before the game starts, the server generates a GAMEID, which is a number that uniquely identifies the play session.  The server generates a random number called NONCE that it associates with the GAMEID in a database.  The server sends GAMEID and NONCE to the client.

The client plays the game.  A game client calculates the score for the game, call it SCORE.

Send over HTTP PUT: http://gameserver.com/sss/docommand?data=STRING

Choose an encryption function ENCRYPT.  Examples are AES, ChaCha, etc.

Where STRING is produced this way:

BASE64(
  ENCRYPT(
   DATA = JSON
   KEY = SECRET
  )
)

And JSON is a JSON string that looks like this:
{
  "command" = "set score",
  "game id" = 7456,
  "score" = 5613412,
  "verification" = CHECKCODE
}

Choose a hash function HASH.  Examples are MD5, SHA1, etc.  It would be okay to truncate the result down to 8 bytes or so.
And CHECKCODE is calculated as CHECKCODE = BASE64(HASH(SECRET || SCORE || NONCE))

The server unpacks the BASE64 encoding, uses SECRET to decrypt the JSON DATA.  It checks that the command is "set score" and then calculates the CHECKCODE from SECRET, SCORE, and NONCE.  If it matches the "verification" field, then "score" is accepted.

The server also needs to make sure that the "game id" corresponds to a game that is still open.  The "game id" would be used to tell the server which play session the score corresponds to, which allows the server to lookup which NONCE it should use to validate the score.  After a "game id" is used, then it should be removed from the database to avoid it being reused.

This is just one example and it doesn't touch on multiplayer majority voting or some other ideas discussed above.


Reacting to Cheaters

The system above allows you to detect an attempt to tamper with the scores.  How to react to cheaters is tricky.  I wouldn't auto-ban someone for cheating because the infraction was deterred and they won't be able to cheat if they keep hammering at the server with bad data.

What could happen if you auto-ban someone is they could send someone else's CD key or other user id, which then would actually enable them to exploit the system to their benefit to hurt another player.  Another version of this exploit: If the server auto-bans the IP address of the foiled cheater, then they could intentionally send bad data at a public coffee house to ban everyone else playing there.

Other Validation Approaches

Having the server recompute the score based on all the inputs is not possible to do in a generic way as far as I can tell.  And even if you do that it's not going to stop people from generating perfect games repeatedly or exploiting it in some other way.  Past some point you just have to let the cheaters win.  The approaches I've outlined above are easy to implement and work for any type of data, so it would work well for a generic online game framework.
last edit by catid edited (>30 days ago) 8:18pm Sat. Jan 12th 2013 PST