某手游协议分析2
本帖最后由 salala159 于 2020-2-27 22:04 编辑申明:
1. 本帖不提供手游APP及名称;
2. 本帖不提供任何手游漏洞及细节;
3. 仅提供分析游戏的思路以及一些通用手法。
4. 本帖使用工具:DnSpy,frida
一:开启手游,抓包(以搜索好友的包为例)
疑问1:为啥不以喊话包,或者其它包为例?(答案见文章末尾)
: 0xba74d010 length: 0xc
包内容:ww
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0c 46 ee 41 ca d7 c9 fc 07 ....F.A.....
: 0xba74d010 length: 0xc
包内容:ww
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0c 37 0b dd 41 26 8a 3a 0d ....7..A&.:.
: 0xba74d010 length: 0xc
包内容:ww
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0c 75 5c a7 91 18 2f 9e 74 ....u\.../.t
: 0xba74d010 length: 0xd
包内容:www
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0d 02 d6 e2 01 00 c7 7a e7 16 ..........z..
: 0xba74d010 length: 0xd
包内容:www
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0d 79 45 d4 8e f4 1b c3 38 8f ....yE.....8.
我们从纵横两个方向对比下这些包都有些什么特征:
【横向对比:单独每个包】
1. 包前四个字节为包长(大端序)
2. 除前四字节外的其余字节均为加密状态(并未包含’w’字眼的明文ascii码)
【纵向对比:多个包】
对比第一,第二,第三个包(or 第四,第五个包)
1. 相同的包内容产生的密文均不同
2. 相同的包内容,包长度一致
对比第一,第四个包
1. 明文内容由‘ww‘变为‘www’,包长增加一个字节
总结,从以上分析我们得出:
1. 协议采用流式对称加密方式,根据经验猜测可能是RC4或者AES。密钥key客户端和服务端采用相同的伪随机算法生成,保证包随机化同时的前提密钥key始终同步。
2. 初始密钥key由以下两种方式生成:
1. 在游戏进行联网的伊始,key由服务端发送过来,可能明文发送,可能经由RSA加密发送。
2. 客户端和服务端约定初始密钥key为一个定值。
二: 验证以上猜测:
解压APK包,可在lib目录下发现libmono.so和libtolua.so,可知游戏采用Unity3D的mono编译框架,并且支持实时热更新,也就是说游戏的逻辑可能不在Assembly-CSharp.dll中,极有可能在lua、luac、luajit文件中。这里我们不关注游戏逻辑,所以无所谓脚本是否加密。但游戏的发包的逻辑绝不可能在脚本文件中。游戏的发包逻辑可能在某个so或者Assembly-CSharp.dll中。
疑问2:游戏的发包的逻辑为啥不在脚本文件中?(答案见文章末尾)
接下来我们尝试hook mono_jit_compile_method编译函数来追踪下游戏发包逻辑
Microsoft Windows [版本 6.1.7601]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。
C:\Users\mars>python E:\work\main.py
[+] hook_mono_jit_compile_method @ 0xd213467c
0xce6fc5b8 UIPlaySound:UnityEngine.EventSystems.IPointerClickHandler.OnPointerClick (UnityEngine.EventSystems.PointerEventData)// 点击登录
0xce6fc5f0 UIPlaySound:PlaySound ()
0xce6fc778 UIPlaySound:PlayInGame ()
0xce6fccb0 GOGUI.EventTriggerListener:OnPointerClick (UnityEngine.EventSystems.PointerEventData)
0xce6fce58 GOGUI.EventTriggerListener:CheckClick (single)
0xce6fceb8 DelegateFactory/GOGUI_EventTriggerListener_VoidDelegate_Event:Call (UnityEngine.GameObject)
0xce7e5e48 LuaInterface.LuaFunction:PushSealed<UnityEngine.GameObject> (UnityEngine.GameObject)
0xcf5c6d80 LuaInterface.LuaFunction:PCall () // 调用lua接口
0xce6fcf40 MUGame_LuaMsgHandlerWrap:Connect (intptr)
0xcf5c6f90 LuaInterface.ToLua:CheckArgsCount (intptr,int)
0xce68ca50 LuaInterface.ToLua:CheckObject<MUGame.LuaMsgHandler> (intptr,int)
0xcf5d48a8 LuaInterface.ToLua:CheckString (intptr,int)
0xce68d520 LuaInterface.LuaDLL:luaL_checknumber (intptr,int)
0xce6fd070 MUGame.LuaMsgHandler:Connect (string,int)
0xce6fd0c0 Utils.SafeAction`2<string, int>:SafeInvoke (string,int)
0xce6fd110 Utils.EventsExtension:SafeInvoke<string, int> (System.Action`2<string, int>,string,int)
0xce6fd150 MUGame.NetMod:connect (string,int)
0xce6fd1d8 Utils.TcpClient:Connect (string,int) // 建立网络连接
0xcad0de18 Utils.TcpClient:Close ()
0xce6fd378 Utils.Ipv6:getIPType (string,string,string&,System.Net.Sockets.AddressFamily&)
0xce6fd4d0 Utils.Ipv6:GetIPv6 (string,string)
0xcad0f200 LuaInterface.LuaFunction:EndPCall ()
0xce701b70 GOGUI.EventTriggerListener:OnPointerExit (UnityEngine.EventSystems.PointerEventData)
0xce7020b0 Utils.TcpClient:onConnectProc ()
0xce7021d8 Utils.TcpClient:popPackage ()
0xce7022c8 Utils.ThreadSafeQueue`1<Utils.SMsgData>:Dequeue (Utils.SMsgData&)
0xce703058 Utils.TcpClient:onConnected (System.IAsyncResult)// 确认网络连接完毕
0xce7034b8 Utils.TcpClient:ReceiveLoop (object)// 线程收包循环
0xce703510 Utils.TcpClient:DoSyncRecv ()
0xce7039a0 Utils.TcpClient:ReceivePayload (byte[],int)// 接收到第一个包,初始key
0xc34b2500 Utils.TcpClient:readInt32FromNetwork (System.IO.BinaryReader)
0xc34b26a8 Utils.TcpClient:getBufferInt (byte[])
0xc34b2828 Utils.RC4:.ctor (byte[])// 密钥key初始化的地方
0xc34b2bc0 MUGame.NetMod:onConnect ()
0xcf5c71b0 MUGame.LuaMsgHandler:GetInstance ()
0xc34b2c08 MUGame.LuaMsgHandler:OnConnect ()
0xcf5d71c0 LuaInterface.LuaBaseRef:op_Inequality (LuaInterface.LuaBaseRef,LuaInterface.LuaBaseRef)
0xc34b2c78 MUGame.LuaMsgHandler:callLuaFunction (LuaInterface.LuaFunction,object[])
0xc7697f90 LuaInterface.LuaFunction:Push (LuaInterface.LuaBaseRef)
0xcf5c6d80 LuaInterface.LuaFunction:PCall ()
0xc34b2d40 MUGame_PlatformSDKWrap:LoginGameServerMonitoring (intptr)
0xcf5c6f90 LuaInterface.ToLua:CheckArgsCount (intptr,int)
0xce68ca50 LuaInterface.ToLua:CheckObject<MUGame.PlatformSDK> (intptr,int)
0xcf5d48a8 LuaInterface.ToLua:CheckString (intptr,int)
0xcf5d48a8 LuaInterface.ToLua:CheckString (intptr,int)
0xce68dae8 LuaInterface.LuaDLL:luaL_checkboolean (intptr,int)
0xc34b2e78 MUGame.PlatformSDK:LoginGameServerMonitoring (string,string,bool)
0xce6fd378 Utils.Ipv6:getIPType (string,string,string&,System.Net.Sockets.AddressFamily&)
0xcdd0bf48 MUGame.PlatformSDK:get_Os ()
0xc34b3198 MUGame.AgentAndorid:OnesdkUploadGameLoginCorrect (string)
0xc34b3208 MUGame_PlatformSDKWrap:LogEventOnlyName (intptr)
0xcf5c6f90 LuaInterface.ToLua:CheckArgsCount (intptr,int)
0xce68ca50 LuaInterface.ToLua:CheckObject<MUGame.PlatformSDK> (intptr,int)
0xcf5d48a8 LuaInterface.ToLua:CheckString (intptr,int)
0xce68d520 LuaInterface.LuaDLL:luaL_checknumber (intptr,int)
0xe46924e0 MUGame.PlatformSDK:LogEventOnlyName (string,int) // 游戏登录事件,功能函数,待组包
0xc34b3510 Utils.TcpClient:setBufferInt (byte[],int,int)// 序列化组包
0xc34b3510 Utils.TcpClient:setBufferInt (byte[],int,int)// 序列化组包
0xc34b3598 Utils.TcpClient:rawSend (System.Net.Sockets.Socket,byte[],int,int)// 组包完成待加密发送
0xc34b3688 Utils.RC4:Crypt (byte[],int,int)// 明文加密函数
0xcad0f200 LuaInterface.LuaFunction:EndPCall ()
0xc34b2500 Utils.TcpClient:readInt32FromNetwork (System.IO.BinaryReader)
0xc34b3b50 Utils.TcpClient:pushPackage (Utils.SMsgData)
0xc34b3ba8 Utils.ThreadSafeQueue`1<Utils.SMsgData>:Enqueue (Utils.SMsgData)
0xcf5c71b0 MUGame.LuaMsgHandler:GetInstance ()
0xc34b3f88 MUGame.LuaMsgHandler:CallMsg (uint,byte[])
0xe468ca18 LuaClient:get_Instance ()
0xc34b3fe8 LuaClient:FetchMessage (byte[],uint,LuaInterface.LuaFunction)
0xcf5d40d8 LuaInterface.LuaBaseRef:op_Equality (LuaInterface.LuaBaseRef,LuaInterface.LuaBaseRef)
0xc34b4100 LuaInterface.LuaFunction:Push (uint)
。。。
点击登录游戏,触发以上函数调用,一些主要的调用逻辑我均已注释。整个发包逻辑可以概要为:操作点击游戏--> 调用游戏功能函数-->提取核心逻辑参数--> 序列化组包-->加密序列化后的封包--> send出去。
我们定位到
0xc34b2828 Utils.RC4:.ctor (byte[])// 密钥key初始化的地方
0xc34b3688 Utils.RC4:Crypt (byte[],int,int)// 明文加密函数,
可知函数在Assembly-CSharp.dll模块中,采用的是RC4加密。接下来分析密钥key以及Crypt 函数附近的调用链。
初始密钥key的调用链:
密钥key的生成函数:
public RC4(byte[] key)
{
int num = key.Length;
byte[] array = new byte;
for (int i = 0; i < 256; i++)
{
this.S = (byte)i;
array = key;
}
int num2 = 0;
for (int j = 0; j < 256; j++)
{
num2 = ((num2 + (int)this.S + (int)array) % 256 & 255);
byte b = this.S;
this.S = this.S;
this.S = b;
}
}
初始密钥key通过ReceivePayload函数接收回来,是一个32字节的字节流
private void ReceivePayload(byte[] data, int length)
{
if (this._socket == null)
{
return;
}
if (!this._socket.Connected)
{
this.Close();
return;
}
this.recvBuffer.Position = this.recvBuffer.Length;
this.recvBuffer.Write(data, 0, length);
if (this.lastMsgLength < 0 && this.recvBuffer.Length < 4L)
{
return;
}
this.recvBuffer.Position = 0L;
BinaryReader binaryReader = new BinaryReader(this.recvBuffer);
if (this.lastMsgLength < 0)
{
this.lastMsgLength = this.readInt32FromNetwork(binaryReader) - 4;
if (this.lastMsgLength > 262144)
{
this.Close();
throw new Exception("Too long package length!");
}
}
int num = (int)(this.recvBuffer.Length - this.recvBuffer.Position);
while (num >= this.lastMsgLength && this.lastMsgLength > 0)
{
if (!this._readKey)
{
byte[] key = binaryReader.ReadBytes(this.lastMsgLength);
this._rc4 = new RC4(key);
this._readKey = true;
}
else
{
int msgID = this.readInt32FromNetwork(binaryReader);
byte[] msgData = binaryReader.ReadBytes(this.lastMsgLength - 4);
int num2 = this.lastMsgLength - 4;
if (TcpClient.debugMode)
{
}
this.pushPackage(new SMsgData
{
msgID = (uint)msgID,
msgData = msgData
});
}
this.lastMsgLength = -1;
num = (int)(this.recvBuffer.Length - this.recvBuffer.Position);
if (num >= 4)
{
this.lastMsgLength = this.readInt32FromNetwork(binaryReader) - 4;
num -= 4;
if (this.lastMsgLength > 262144)
{
this.Close();
throw new Exception("Too long package length!");
}
}
}
num = (int)(this.recvBuffer.Length - this.recvBuffer.Position);
if (num > 0)
{
byte[] buffer = this.recvBuffer.GetBuffer();
Array.Copy(buffer, this.recvBuffer.Position, buffer, 0L, (long)num);
}
this.recvBuffer.Position = 0L;
this.recvBuffer.SetLength((long)num);
}
初始密钥key的生成方式我们已经知晓。
加密函数就是一个简单的RC4函数,如下:
public void Crypt(byte[] pt, int start, int end)
{
byte[] s = this.S;
if (end < 0)
{
end = pt.Length;
}
for (int i = start; i < end; i++)
{
this._i = ((this._i + 1) % 256 & 255);
this._j = ((this._j + (int)s) % 256 & 255);
byte b = s;
s = s;
s = b;
int num = (int)(s + s) % 256 & 255;
byte b2 = s;
pt = (b2 ^ pt);
}
}
三:python代码实现简述:
实际的python代码我就不提供了,说下写python代码的思路
1. 使用scapy模块抓取游戏的tcp流,处理并转发
2. 接收服务器发来的第一条包,RSA解密,私钥以及其它必要参数在Assembly-CSharp.dll中
3. 仿照C#密钥key的生成函数写出python版本
4. 进行RC4解密
协议解密效果前后对比:
搜索好友协议:搜索ww
解密后:MessageID: 3506
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0c 00 00 0d b2 0a 02 77 77 ..........ww
解密前:: 0xba615010 length: 0xc
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0c 87 5f db 88 8f 45 82 f3 ....._...E..
搜索好友协议:搜索www
解密后:MessageID: 3506
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0d 00 00 0d b2 0a 03 77 77 77 ..........www
解密前:: 0xba615010 length: 0xd
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 0d 1e 80 42 ce 69 a7 35 63 cb ......B.i.5c.
走路协议
MessageID: 1209
解密后:raw: 0xba615010 length: 0x1b
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 1b 00 00 04 b9 12 0f 0d 88 e5 93 42 15..............B.
00000010e3 a7 98 42 1d 25 e2 9f 43 18 02 ...B.%..C..
解密前:: 0xba615010 length: 0x1b
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 1b 44 e8 67 0f a0 c8 1e c7 a8 2b 1c 5e....D.g......+.^
0000001021 fe 69 03 8e 88 e3 e0 8d 0d 31 !.i.......1
至此,协议已经解密,协议的结构也很清晰明了,
0000000000 00 00 0d 00 00 0d b2 0a 03 77 77 77 ..........www
协议长度 协议id 协议内容
其它协议以此类推。
协议内容中,77 77 77代表www,那前面的0a 03代表什么呢?
如果你有足够的经验,相信聪明的你一看就知道是什么序列化框架,没错,就是protobuf序列化框架,tag = 0a 代表定长数据, size = 03 代表字符串www的长度,符合protobuf的编码格式,我们拿走路协议来验证是否真为protobuf的编码:
0000000000 00 00 1b 00 00 04 b9 12 0f 0d 88 e5 93 42 15..............B.
00000010e3 a7 98 42 1d 25 e2 9f 43 18 02 ...B.%..C..
00 00 00 1b:协议长度(大端序)
00 00 04 b9: msgid(大端序)
12 0f 0d 88 e5 93 42 15e3 a7 98 42 1d 25 e2 9f 43 18 02:协议内容
12: 0x12 = 2 << 3 | 2,index = 2, wire_type = 2代表嵌套结构
0f:嵌套结构的长度
0d:0x0d = 1 << 3 | 5, index = 1, wire_type = 5代表浮点数
88 e5 93 42:转换浮点数73.94830322265625
15:0x15 = 2 << 3 | 5, index = 2, wire_type = 5代表浮点数
e3 a7 98 42:转换浮点数76.3279037475586
1d:0x1d = 3 << 3 | 5, index = 3, wire_type = 5代表浮点数
25 e2 9f 43:转换浮点数319.7667541503906
18:0x18 = 3 << 3 | 0, index = 3, wire_type = 0代表Varint
02:代表数字2
因此,协议内容12 0f 0d 88 e5 93 42 15e3 a7 98 42 1d 25 e2 9f 43 18 02反序列化的样子为:
{
{
73.94830322265625,// x坐标
76.3279037475586,// y坐标
319.7667541503906// z坐标
},
2 // 频道
}
当然,每条数据都这么手工搞会死人的,所以python脚本解密后,再加入反序列化的代码,协议就完美的被还原出来了,还原出来的效果如下:
MessageID: 1502
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 10 00 00 05 de 08 02 12 04 64 64 64 64............dddd
{
"1:0:Varint": 2,
"2:1:string": "dddd"
}
MessageID: 1740
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 2a 00 00 06 cc 0a 0f 0d 15 24 84 42 15...*........$.B.
0000001057 b1 9b 42 1d c9 d7 9f 43 12 0f 0d 4c c9 8f 42W..B....C...L..B
0000002015 15 fe 97 42 1d a0 e1 9f 43 ....B....C
{
"1:0:embedded message": {
"1:0:32-bit": 66.07047271728516,
"2:1:32-bit": 77.84636688232422,
"3:2:32-bit": 319.6858215332031
},
"2:1:embedded message": {
"1:0:32-bit": 71.89315795898438,
"2:1:32-bit": 75.99625396728516,
"3:2:32-bit": 319.7626953125
}
}
MessageID: 1742
0123456789ABCDEF0123456789ABCDEF
0000000000 00 00 19 00 00 06 ce 0a 0f 0d 3c 6a 8f 42 15...........<j.B.
000000104a 1c 98 42 1d ff 01 a0 43 J..B....C
{
"1:0:embedded message": {
"1:0:32-bit": 71.70748901367188,
"2:1:32-bit": 76.05525207519531,
"3:2:32-bit": 320.0155944824219
}
}
....
疑问1:
原因1:喊话包不通用,现在许多游戏的喊话包和正常的游戏逻辑不是一套通讯系统,可能会误判,
原因2:现在好多游戏喊话功能要10级甚至15级开启,我比较懒,不想打游戏
原因3:搜索好友的包,包含特定的明文,可以定制包数据,比较好分析定位,而且一般不受游戏等级限制,其他大部分逻辑包没有太明显的特征
疑问2:
原因:游戏的发包逻辑一般不太可能在lua脚本中,lua本来就是一门动态解释型语言,执行效率低,而发包逻辑几乎一直再调用,严重影响效率。虽说mono中的C#也是解释执行,但其支持jit模式,一些调用频繁的函数只会解释一次。
后话:
话说现在都0202年了,不管哪款手游都会存在一些漏洞,少则四五个,多则一大堆。每次测试下来都挺无奈,难道游戏就做不到无bug吗?还是说故意而为之,真希望厂商多多注意安全这方面的问题。我本人不太喜欢玩游戏,一直只玩一款游戏LOL,而且只用一个英雄,关键技术还菜,万年青铜狗。昨天玩LOL遇到一把瞎子,玩的那是绝壁溜啊,都怀疑是不是带辅助了,全场被虐哭了:lol 学习一下,数据合法性等服务器都效验,该判断的都判断。楼主这个分析出来能发挥什么作用呢?除非遇到服务器没效验和判断,就是bug咯 问下,这款游戏如果,你破解后,会有哪些功能,谢谢 虽然看不懂但是感觉听牛皮
。谢谢楼主分析 看不懂 但是还是要顶下帖{:17_1063:} 小白默默收藏一下 谢谢分享,感谢了 论坛有你更精彩 小白默默收藏一下 学习了收藏!!