游戏服务器防外挂-通信安全
在游戏行业有太多因为外挂程序最终导致游戏产品快速死亡的血淋淋的教训了。这些外挂程序就是利用篡改/模拟客户端/服务器之间的通信数据来进行作弊牟得额外收益。因此在网络游戏中客户端/服务器之间的通信安全是游戏服务器技术中至关重要的一环。我们这篇文章讲一讲关于如何使用加密/校验的方式进行安全通信。
外挂利用非安全通信作弊溯源!
很多时候程序员修复bug/漏洞和医生治病差不多都要对症下药。一个可以被外挂作弊的服务器程序提供的服务可以说是一个病态的服务,那么为了解决这个问题我们需要找到症结所在。
我们先从对于客户端/服务器通信数据不采用任何安全处理方式开始分析。二者之间的通信数据虽然被转换成了二进制字节流的方式进行通信,但是以目前的技术手段这些数据几乎可以理解为就是明文传输的。那么外挂程序可以利用多种手段对通信数据进行二次加工。
- 通过修改客户端内存的方式篡改通信数据
- 通过拦截客户端/服务器收到/发出的协议包进行二次篡改
- 通过其他脚本/程序模拟客户端和服务器进行通信
- ......
我们刚刚说到既然是明文的传输,那么以上三种手段就可以轻而易举的作弊了。举个例子:某个玩家在游戏中可以领取某个礼包奖励,通信数据可能包含一个礼包ID,那么如果外挂程序可以将礼包ID这个字段篡改为任意他想要修改的值发送给服务器,对于这种篡改如果服务程序存在漏洞不能正确的应对那么极有可能导致可以利用外挂获得额外的收益。通过上述分析应该很容易找到核心症结在于通信的数据是一种暴露的状态,并且处于没有任何校验或者合法性检验的状态,这是一种极不安全的状态。因此我们要做得就是找到一种方式方法去规避这种风险,用技术的手段将通信数据给保护起来,就像你去银行取钱需要使用银行卡以及账号密码进行验证类似的道理。
如何保证通信数据的安全!
一切来源于客户端的数据皆不可靠!
对于有过丰富的游戏服务器开发经验的程序员一定十分认同这句话,甚至会把它当做编程时的座右铭。这句话的含义就是上述我们所有的通信安全的症结所在。来自于客户端的数据有可能是经过篡改的数据,因此在服务器接收到这些数据后第一步便是先想方法验证一下这些数据是否是可信的。这个验证过程就需要使用到两个技术点"加密&校验"。一般情况下加密的过程主要是使用"AES"和"DES"这种对称加密算法,校验方式通常会使用HamcMd5摘要校验算法进行验证。
"AES" & "DES"加密/解密:
- 加密模式:"ECB", "CBC", "CTR", "OFB", "CFB"
- 填充方式:"PKCS5", "PKCS7", "ZERO"
- 输出格式:"BASE64", "HEX"
使用"AES"或者"DES"进行加密/解密的目的是为了将数据转为密文而不再使用暴露的明文,使用HamcMd5摘要校验算法的目的是为了校验数据是否被篡改过了。AES加密+HamcMd5校验能够保证只有双方持有同样的加密校验的key(类似于密码)以及相同的计算算法才是可信的通信数据,否则认为数据是不安全的可以丢弃这种数据。
在这个基础上我们再来分一下外挂程序如果想要突破这一层的防守需要做的事情。首先外挂程序需要获取客户端/服务器通信加密校验的aesKey和hamcMd5Key,然后需要获取客户端/服务器双方约定的加密校验的字段以及原文到密文的计算算法才能进行数据的篡改,这在很大程度上提升了外挂程序开发的难度,其实防外挂终究是一场拉锯战,也就是只能不断的提升外挂的难度从理论上并不能真正的杜绝外挂。
加密-解密/校验流程:
代码示例
safe_comm_util.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
func TestEncryptionVerificationEncode(t *testing.T) { src := []byte("this is a safe communication port") cipher, err := SafeCommEncode(src) if err != nil { t.Log(err) return } origin, err := SafeCommDecode(cipher) if err != nil { t.Log(err) return } t.Log(string(origin)) } |
safe_comm_util.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
var ( md5Bytes = [2]int{0, 24} codeBytes = [2]int{24, 26} vBytes = [2]int{26, 30} ivBytes = [2]int{30, 54} extLen = 54 ) var ( versionStr = "v1.0" version = []byte(versionStr) okCode = uint16(1) aesKey = []byte("MaPEZQleQYhYzRyW") hamcMd5Key = []byte("etHsbZRjxAwnwekrBEmfdzdcEkXBAkjQ") ) func HamcMd5(data []byte, key []byte) []byte { h := hmac.New(md5.New, key) h.Write(data) return h.Sum(nil) } // cipher-密文 密文结构:c+v+iv+md5+cipher 返回原文:decode(cipher) func SafeCommDecode(cipher []byte) ([]byte, error) { if len(cipher) < extLen { return nil, errors.New("input cipher text is invalid: length too short") } if len(cipher) == extLen { return []byte{}, nil } c := binary.LittleEndian.Uint16(cipher[codeBytes[0]:codeBytes[1]]) if c != okCode { return nil, errors.New("input cipher text is invalid: code error") } var strb strings.Builder _, err := strb.Write(cipher[vBytes[0]:vBytes[1]]) if err != nil { return nil, err } if strb.String() != versionStr { return nil, errors.New("input cipher text is invalid: version error") } base64Fmt := &Base64Format{} recvMd5Sign := make([]byte, md5Bytes[1]-md5Bytes[0]) copy(recvMd5Sign, cipher[md5Bytes[0]:md5Bytes[1]]) md5Code := base64Fmt.Encode(HamcMd5(cipher[md5Bytes[1]:], hamcMd5Key)) if string(recvMd5Sign) != string(md5Code) { return nil, errors.New("data is modified") } aes := NewAESCrypto(&CryptoData{ Mode: &CBCMode{}, Padding: &PKCS5Padding{}, Format: &HexFormat{}, }) ivbase64 := make([]byte, ivBytes[1]-ivBytes[0]) copy(ivbase64, cipher[ivBytes[0]:ivBytes[1]]) iv, err := base64Fmt.Decode(ivbase64) if err != nil { return nil, err } origin, err := aes.Decrypt(cipher[extLen:], aesKey, HamcMd5(iv, hamcMd5Key)) if err != nil { return nil, err } return origin, nil } // origin-原文 ciphertext-密文 密文结构:c+v+iv+md5+cipher // c-code:状态码 // c-version:版本号 // iv-aes iv:Aes cbc 模式的iv,base64格式,固定为24字节 // md5-md5校验码 HmacMd5((c+v+iv+cipher), key) func SafeCommEncode(origin []byte) (ciphertext []byte, err error) { iv := randutil.RandBytes(16) aes := NewAESCrypto(&CryptoData{ Mode: &CBCMode{}, Padding: &PKCS5Padding{}, Format: &HexFormat{}, }) cipher, err := aes.Encrypt(origin, aesKey, HamcMd5(iv, hamcMd5Key)) if err != nil { return ciphertext, err } ciphertext = make([]byte, extLen+len(cipher)) binary.LittleEndian.PutUint16(ciphertext[codeBytes[0]:codeBytes[1]], okCode) copy(ciphertext[vBytes[0]:vBytes[1]], version) base64Fmt := &Base64Format{} ivbase64 := base64Fmt.Encode(iv) if len(ivbase64) != 24 { return ciphertext, errors.New("input param is invalid, iv error") } copy(ciphertext[ivBytes[0]:ivBytes[1]], ivbase64) copy(ciphertext[extLen:], cipher) md5Sign := base64Fmt.Encode(HamcMd5(ciphertext[md5Bytes[1]:], hamcMd5Key)) copy(ciphertext[md5Bytes[0]:md5Bytes[1]], md5Sign) return ciphertext, nil } |