前言
大多数加密方案都假定可信的发送者和接收者会通过一个不可信的通道通信. 虽然假设发送者会故意尝试愚弄接收者有点荒谬,但这确实是摆在开发者面前的问题。有些玩家是不可信的, 更糟的是, 他们能够通过客户端执行文件获取对加密算法和所有通信的了解。在这样的情况下,我们不可能提供完全安全的通信,但是我们可以为攻击者制造麻烦。本文着重介绍一些实用的技术来为网络游戏建立一个应用程序级通讯协议。
Definitions
协议设计在基于客户端/服务器的游戏中是最有趣的,这种游戏由若干不可信的客户端和一个可信的中央服务器通信。(Cheating在点对点的游戏中当然也是个问题,不过因为这样的游戏中没有节点是可信的,所以这种情况是没希望的)在客户端/服务器游戏中cheat的后果是非常严重的,因为服务器作为唯一的可信实体,维护游戏状态并且验证所有客户端命令。当游戏状态稳定后, 一次成功的cheat能够使包括上千个玩家的游戏变得不稳定。
考虑在客户端/服务器系统中的协议安全性。客户端和服务器在网络通道中发送封包通信,网络通道或许可信(TCP),或许不可信(UDP).尽管客户端之间也能够直接通信,比如文字聊天或者声音,我们假定任何需要安全保证的数据只在客户端和服务器之间发送。
封包篡改(Packet Tampering)
大多数协议hackers是偶然的:他们改变封包中的数个bytes,看看会发生什么。抵御此类攻击的第一道防线是简单的校验和。一个校验和是一个由封包所有的byte组合而成的一个短整数。发送者计算出封包的校验和并连同封包一起发送给接收者。接收者取得封包并重新计算校验和;如果和发送者的校验和不匹配,就认为封包被破坏了并抛弃之。在计算校验和时需要包含整个封包,包括header,这样接收者才能检测出header是否被破坏了。
一个完美的校验和算法应该能够对封包中的任何字节改动产生不同的值。当然完美的校验和应该too long to be useful,hash函数有着相同的设计目标从而能够产生完美的校验和。单向的hash函数尤其有用,能够把输入变为混乱值,并且逆向重建输入是不可能的。MD5算法经过广泛的检测,被认为是足够快速的并能够胜任游戏使用的算法。其源代码可在网上找到。
这种简单的校验和机制有两个缺点。第一,因为客户端执行文件包含校验和算法代码,攻击者能够逆向工程出此算法,从而可以为任何消息计算校验和。第二,攻击者能够截获封包并在稍后重发,这也被称为封包重发。
封包重发(Packet Replay)
所谓封包重发,指的是那些不怀好意的玩家截获客户端发出的封包(通常使用某种封包嗅探器),然后多次发送此包。
虽然客户端运用定时器检测机制能够阻止正常客户端过快发送命令,比如定时器没每秒最多发出一个指令,那么无论客户端多么频繁地截获玩家的命令(比如玩家疯狂地按键),这些命令仍会每秒只发送一次。使用封包重发则是非正规渠道,他是在客户端控制之外重新发送封包,所以能够在一秒内发送相同的指令数百次。系统设计者也许会在服务器端使用同样每秒一次的定时器检测机制防御这种攻击。但是由于有网络延迟,这样做显得不切实际。因为虽然能够检测出大多数封包重发攻击,但是网络延迟可能会使多个正常封包同时到达服务器,这样就会引起合法命令被抛弃。我们当然不想我们的安全方案把合法玩家视为cheater。
为了防范封包重发,每个封包应该包含一些状态信息,以使数据相同的封包也能有不同的位模式。随着发送封包而增长的数字能够担当此任,但此法太容易被攻击者破解。更好的方案是使用状态机为连续封包产生连续ID数。一个快速可信复杂的方法是使用线性同余的随机数产生器(通常在系统库中)。这种产生器工作如下:
State = ( State + a ) * b
其中a和b都是精心挑选的数字。发送者和接收者都为连接准备一个随机数发生器。发送一个封包时,发送者产生一个随机数并把它加入封包,同时更新随机数产生器。接收者检查到来封包中的随机数是否和自己随机数发生器匹配;如果不匹配,可以认为封包有问题;匹配则更新随机数发生器以为下一个封包做准备。
此法有两个复杂度。
第一是发送者和接收者如何同步状态机,可以用相同的种子开始状态机,但是然后初始的封包流将一直有相同的位模式并且会很容易被破解。取而代之的是,服务器用一个随机的种子值初始化他的状态机并将此值作为第一个消息发给客户端。
第二个复杂度是如何保持状态机在通讯过程中同步。在可信连接上,封包绝不会丢失,可以保证同步。然而当封包会丢失或重组,状况有点复杂。如果一个消息丢失了,发送者状态机将比接收者快一步;接着的封包将被拒绝,尽管他们是合法的。一个简单的解决方案是依赖于随每个封包一起发送的一个真正的序列号(大多数游戏包含此号码在消息中,以在不可信transport上提供可信链接)。给定一个序列号,接收者可以判断他的状态机跳了多少步以接获当前封包。如果程序允许无序delivery,旧的状态机将不得不保存给一个无序封包到达时使用。大多数运行时库提供的rand函数作为一个状态机使用都不够准确因为它的精度太低(许多实现只有15位)并且它显然的选择作为一个源随机数。
其他的技术(Additional Techniques)
理想状况下,两个具有相同payloads的封包应该在bit patterns上尽可能少的暴露相关性,以阻止攻击者对payload进行分析。消除两组数据所有相关性的一个简单方法是用一串随机bits组合他们,使用异或(XOR)操作。想象一下前面所描述的防御封包重发的方案,发送者和接收者已经同步了他们的随机数发生器。因此,发送者能够为每个封包产生一串随机数并把他们异或放进封包payload中;接收者产生同样的一串随机数然后用同样的方法获取原始的payload。
两个相同长度的封包的会留给给攻击者一条线索,那就是封包加密了类似的数据。为了进一步阻挡攻击者,封包可以包含一堆随机的”垃圾”数据,此数据只为改变封包长度。垃圾数据的长度通常由另一个状态机决定。发送者检查他的状态机来决定产生多少垃圾数据并将这些随机的bytes插入到即将发送的封包中。接收者简单的忽略这些垃圾数据。增加的垃圾数据进一步隐藏了payload但是耗费更多的带宽。那些受带宽限制的程序,垃圾数据的长度应该比 payload的平均长度更短。
逆向工程(Reverse Engineering)
address最困难的问题以及最终任何阻止篡改协议的方法的缺点就是客户端包含完全的加密算法并且总能够被逆向工程。下面的步骤能够用来使逆向工程更困难:
- 发布的任何代码都不该包含任何符号和调试信息。
- 不要把对缓冲区加密和解密分别放在他们自己的函数中;应该把它们和一些其他的网络代码放在一起。用可维护性换取安全性这里是值得考虑的。
- 应该在运行时计算”魔法数”(不如初始化种子),而不要直接把他们的值放在执行文件中。
- 在每个版本的客户端中都包含良好的加密方法,就算是早期的测试。如果某个版本客户端没有加密,用户就能够记录未加密的封包流,并用对封包payload的了解来破解下一个加密版本。
- 记住你的目标是让cheating花大代价,而非杜绝cheating。
实现(Implementation)
本文包含一个C++类SecureTransport,此类使用了所有之前描述过的技术。一个SecureTransport对象封装了发送者和接受者间的一个双向连接。对于每个方向,对象维护4个线性同余的随机数发生器作为协议状态机。这些被初始化为static值,服务器把随机数种子作为第一个消息发给客户端。此类像下面这样使用状态机:
- 包头开始异或成员length。(这是不必要的如果底层协议提供封包长度,比如UDP)
- 一个消息序列号被用来防止封包重发。
- 在每个封包中决定垃圾数据的长度。
- 产生随机bits来异或payload(加密).
一个独立的随机数发生器被用来产生实际的垃圾数据。在调试时,把垃圾数据设为一个常量是很有用的。
源码下载地址
写在最后
此文引用至网上某一篇论文,出处不详,在这里再次向作者(Andrew Kirmse)表达敬意以及歉意!
原文如下:
A Network Protocol for Online Games
Andrew Kirmse
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.
Definitions
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.
Packet Tampering
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.
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].
Additional Techniques
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.
Reverse Engineering
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.
Implementation
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.
References
[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