大多数加密方案都假定可信的发送者和接收者会通过一个不可信的通道通信. 虽然假设发送者会故意尝试愚弄接收者有点荒谬,但这确实是摆在开发者面前的问题。有些玩家是不可信的, 更糟的是, 他们能够通过客户端执行文件获取对加密算法和所有通信的了解。在这样的情况下，我们不可能提供完全安全的通信，但是我们可以为攻击者制造麻烦。本文着重介绍一些实用的技术来为网络游戏建立一个应用程序级通讯协议。
一个完美的校验和算法应该能够对封包中的任何字节改动产生不同的值。当然完美的校验和应该too long to be useful，hash函数有着相同的设计目标从而能够产生完美的校验和。单向的hash函数尤其有用，能够把输入变为混乱值，并且逆向重建输入是不可能的。MD5算法经过广泛的检测，被认为是足够快速的并能够胜任游戏使用的算法。其源代码可在网上找到。
State = ( State + a ) * b
A Network Protocol for Online Games
Most encryption schemes assume that a trusted sender and a trusted recipient want to communicate over an untrusted channel.It seems absurd to suggest that the sender could deliberately try to fool the recipient, yet this is exactly the problem facing designers of online games.Some players cannot be trusted, and worse, they have complete access to the encryption algorithm and all communications via the client executable. Under such circumstances we cannot hope to provide completely secure communications, but we can make the attacker’s job more trouble than it’s worth. This article presents some pratical techniques for building and application-level communications protocol for online games.
Protocol design is most interesting in client/server games, where one of more untrusted clients communicate with a trusted central server.(Cleating is certainly also a problem in peer-to-peer games, but because no entity is trusted in such games, the situation is hopeless)The consequences of cheating in a client/server game is high because the server, as the only trusted entity, maintains the game state and verifies all client commands.
We consider protocol security features in a client/server system.The client and server communicate by sending packets over a network channel, which might be reliable(typically TCP) or unreliable(UDP). Although clients can also communicate directly with each other, perhaps for chat or voice, we assume that any data that need to be secured are sent only between a client and the server.
Each packet contains two parts: the header, containing administrative information, and the payload, containing the actual data we want to communicate. The goal of the network protocol is to deliver the sender’s original payload to the recipient.Any modifications to the sender’s sequence of payloads should be detected.We deal only with delivery of the payload, leaving the details of packet ordering and reliablility to lower levels in the protocol stack.
Most protocol hackers are casual: they change bytes in a packet and see what happens. The first line of defense anainst such attacks is a simple checksum. A checksum is a short number produced by combining every byte of the packet.The sender computes the checksum of the packet and sends both the packet and the checksum to the recipient.The recipient takes the packer and recomputes its checksum;if the computed checksum doesn’t match the checksum from the sender, the packet is corrupt and should be rejected.It’s important to include the entire packet, including the header, in the checksum computation, so that the recipient can detect changes to the header as well as the payload.
A perfect checksum computation would produce a different value if any byte of the packet were changed to any other value. A perfect checksum would be too long to be useful, of course, but hash functions have the same design goal and make excellent checksums. Particularly useful are one-way hash functions, which scramble their input to the extent that reconstructing any part of the input from the hash value is impossible for practical purposes. The MD5 algorithm is well tested, publicly available, and fast enough for use in games. A public domain implementation is available online[Plumb93].
There are two weaknesses in this simple checksum mechanism. First, because the client executable contains the checksum computation code, and attacker can reverse engineer the checksum algorithm, and then compute valid checksums for any message. Second, and attacker can capture valid packets and resend them later, and attack known as packet replay.
In a packet replay attack, a malicious user captures a packet from the client (typically using a packet sniffer), and sends it multiple times. A common tactic is to use packet replay to perform commands faster than game allows, even if there are timing checks in the client. For example, a client might use a timer to send a certain command to the server at most once per second, no matter how frequently the player issues the command. Using packet replay, a single user might issue the same command hundreds of times per second.
A system designer might try to stop this particular attack by putting a similar once-per-second timer check on the server as well. In the face of widely variable network latency,howerer,this defense is impractical.Although it detects most packet replay attacks, varying network delays can make packets bunch together by the time they reach the server, causing legal command sequences to be rejected. We certainly do not want our security scheme to mark law-abiding players as cheaters.
To guard against packet replay, each packet should contain some state information, so that even pakcets with identical payloads have different bit pattern. Something as simple as a number that increments with each sent packet would do, although that scheme is too easy for an attacker to figure out. A better answer is to use a state machine to produce successive identifying numbers for successive packets. A fast and resonably complicated method is a liner congruential random number generator of the type typically found in system libraries. Such generators operate as follows:
State = ( State + a ) * b
where a and b are carefully chosen integers. (For a discussion of this generators, see [Knuth98].)
The sender and recipient each keep a liner congruential random number generator for their connections. When sending a packet, the sender produces a random number and adds it to the packet, simultaneously stepping its random number generator. The receiver checks the random number in the incomming packet against its generator; if the numbers don’t match, the packet has been tampered with. If the numbers do match, the receiver steps its random number generator to prepare for the next packet.
There are two complications with this scheme. The first is how the sender and receiver initially synchronize their state machines.They could each start their state machines with same fixed seed, but then the initial stream of packets would always have the same bit patterns and thus would be vulnerable to analysis.Instead, the server can initialize its state machine with randomly generated seed values and send these to the client in its first message.
The second complication is how to keep the state machines synchronized during communication. On a reliable connection, packets are never lost, so synchronization is guaranteed. When packets are dropped or reordered, howerver, the situation becomes more complicated. If a message is lost, the sender’s state machine will have advanced one more step than the receiver’s; subsequent packets will be rejected, even though they are legitimate. A simple solution is to rely on a true sequence number sent with each packet (most games include this number with messages anyway, toprovide a reliable connection over an unreliable trasport).Given a sequence number, the receiver can determine how many times to step its state machine to catch up to the current packet. If the application allows out-of-order delivery, the old state of the state machine will have to be stored for use when an out-of-order packet arrivers later.
The rand function provided with most run-time libraries is inappropriate for use as a state machine because of its low precision (many implementations have only 15 bits) and its obvious choice as a source of random numbers. A fast, high-quality random number implementation is given in [Booth97].
Ideally, two packets with identical payloads should show as little correlation in their bit patterns as possible, to frustrate analysis of the payload. An easy way to remove all correlation betwen two sets of data is to combine them with a sequence of random bits, using the exclusive-or(XOR) operator.Assuming the previous described packet replay defense, the sender and receiver already have synchronized random number generators.Thus,the sender can generate a sequence of random number for each packet and XOR these into the packet payload; the receiver generates the same sequence of numbers and retrieves the original payload in the same way.
Even the fact that two packets have the same length can give an attacker a clue that the packets encode similar data. To further frustrate attacks, each packet can contain a variable amout of random “junk” data, meant only to vary the length of the packet. The length of the junk data is determined by yet another synchronized state machine. The sender checks its state machine to determine how much junk to generate and insert that number of random bytes into an outgoing packet. The receiver simply ignores the junk data.Increasing the amount of junk data helps to further hide the payload but costs additional bandwidth. In typical applications in which bandwidth is limited, the average length of junk data should be made small compared to the average payload size.
The hardest problem to address, and ultimately the downfall of any scheme to stop protocol tampering, is that the client contains the entire encryption algorithm and thus can always be reverse engineered. Some steps you can take to make reverse engineering harder are as follows:
- Remove all symbols and debugging information from any code released to the public.
- Don’t isolate buffer encryption and decryption in their own function; instead, combine there with some other network code. This is one area in which it can be worthwhile to trade maintainability for security.
- Compute “magic numbers” (such as initialization seeds) at run time instead of placing their values directly in the executable.
- Include a good encryption scheme in every version of the client, even early betas. If any client version lacks encryption, a user can record a stream of unencrypted packets from one client and then use knowledge of the packet payload to help break the encryption in a later version.
- Remember that your goal is to make cheating prohibitively expensive, not impossible.
The implementation included with this article includes a C++ class SecureTransport that uses all the previously described techniques. A SecureTransport object encapsulates a two-way connection between a sender and a recipient. For each direction, the object maintains four linear congruential random number generators as protocol state machines. These are initialized to static values, with the understanding that the server would send random seeds in its first message to the client. The class uses the state machines as follows:
- It XORs the length field at the start of the header. (This is unnecessary if the underlying protocol provides a packet length as in UDP.)
- A message sequence number is used to prevent packet replay.
- It determines the length of junk data in each packet.
- It generates random bits to XOR the payload.
A separate random number generator is used to generate the actual junk data. During debugging, it is useful to set the junk data to a known constant value.
[Booth97] Booth,Rick,Inner Loops, Addison-Wesley Developers Press, 1997.
[Knuth98] Kunth,Donald,The Art of Computer Programming,Volume 2: Seminnmcrical Algorithms, third edition. Addision-Wesley Longman, Inc, 1998
[Plumb93] Plumb,Colin,”md5.c” available online at http://src.openresources.com/debian/src/admin/html/s/rpm_2.4.12.orig%20rpm-2.4.12%20lib%20md5.c.html 1993