魔兽世界私服网可以说是现在市面上最红火的一个网络游戏了,不过从游戏性还是耐玩性,都强过不少其他网游。现在的网游,很多一部分都存在着私服,但是这些大多是泄漏出来的官方服务器端,所以只需稍加修改就可以架设一个完美的私服。不过魔兽世界一直以来只有模拟器,这些模拟器都是不少编程和破解高手费尽心血破解魔兽客户端,研究封包慢慢写出来的服务器端。但是就算市面上最好的模拟器(要收费的)WoWEmu,模拟度也只能达到大概60%左右,存在不少bug。还有更重要的是,由于WoWEmu使用的是文本文件数据库,还有也许服务器端框架上的问题,即便是非常强劲的服务器,都只能带起很少的在线人数,还有就是这个模拟器还是收费的。现在还有另外几款WoW的模拟器,例如RunWoW,WoWWoW,等,有些甚至还是SQL版,不过这些服务器端的模拟度都不如WoWEmu,所以使用的人也很少。
国内现在应该有很多人有兴趣编写一个自己的魔兽服务器端,但是碍于缺乏资料,不知道该从何下手,网上的资料却又非常不全和不详细,能找得到的唯一能编译的代码是非常古老的WoWDaemon,但是这东西是针对WoW Beta的,所以对现在的WoW来说,没多少价值。网上还有流传着一些反编译WoWWoW出来的代码,但是这个代码无法编译,而且因为用的代码混淆器,要想从中得到一些自己想要的东西是非常费力的。所以本人现在想把我近段时间对魔兽世界的研究结果发布出来,希望能对大家有所帮助。
本篇文章中的所有例子和讲解都是以vb.net为基础的,对c#的读者问题应该也不大。使用c++的估计也只是关心相关算法,所以也没有太大问题。适用客户端为1.80-1.92
首先,我会跟大家讲解一下魔兽世界在登陆过程中的流程。首先,客户端会发送一个包含用户名和客户端版本,语言等信息的包给服务器端,然后服务器端验证版本是否符合,用户名是否存在,然后将用户名和密码通过一个算法转换成一串16进制数据,发给客户端当验证码。客户端这时会使用这个验证码,跟客户输入的用户名和密码通过运算得到另外一个验证数据,再连同随机产生的通讯密码一同发给服务器端,服务器端再通过通讯密码,和第一次发送的验证码再通过一番计算,如果跟这次客户端发过来的验证数据一模一样,则验证通过,然后把验证码记录下来,供游戏服务器验证客户端是否经过帐号验证使用。
下面我会跟大家讲各个过程的封包结构和算法
1、 第一个封包(客户端发送用户名和版本信息)
这个封包的结构如下:
Public Structure ClientLogonChallenge
Dim cmd As MyWoW.LoginServer.Packet.LogonOpcode ' // OP code = CMD_AUTH_LOGON_CHALLENGE
Dim [error] As MyWoW.LoginServer.Packet.LogonErrorCode ' // 0x02
Dim size As UInt16 ' // size of the rest of packet, without this part
Dim gamename() As Char ' // "WoW"
Dim version() As Byte ' // 0x01 08 00 -> (1.8.0)
Dim build As UInt16 ' // 0x7F12 -> 4735
Dim platform() As Char ' // 68x\0 -> "x86"
Dim os() As Char ' // niW\0 -> "Win"
Dim localization() As Char '// BGne -> 'enGB'
Dim timezone_bias As UInt32 ' // 0x78 00 00 00
Dim ip() As Byte ' // client ip: 0x7F 00 00 01
Dim acclen As Byte ' // length of account name (without zero-char)
Dim acc() As Char ' // upcased account name (without zero-char)
End Structure
其中cmd是登陆过程使用的Opcode(即命令代码),登陆过程的Opcode列表如下:
Public Enum LogonOpcode As Byte
CMD_AUTH_LOGON_CHALLENGE = &H0 ' // client
CMD_AUTH_LOGON_PROOF = &H1 ' // client
CMD_AUTH_RECONNECT_CHALLENGE = &H2 ' // client
CMD_AUTH_RECONNECT_PROOF = &H3 ' // client
CMD_REALM_LIST = &H10 ' // client
CMD_XFER_INITIATE = &H30 ' // client? from server
CMD_XFER_DATA = &H31 ' // client? from server
CMD_XFER_ACCEPT = &H32 ' // not official name, from client
CMD_XFER_RESUME = &H33 ' // not official name, from client
CMD_XFER_CANCEL = &H34 ' // not official name, from client
'// unknown:
CMD_GRUNT_AUTH_CHALLENGE = &H0 ' // server
CMD_GRUNT_AUTH_VERIFY = &H2 ' // server
CMD_GRUNT_CONN_PING = &H10 ' // server
CMD_GRUNT_CONN_PONG = &H11 ' // server
CMD_GRUNT_HELLO = &H20 ' // server
CMD_GRUNT_PROVESESSION = &H21 ' // server
CMD_GRUNT_KICK = &H24 ' // server
End Enum
error为错误代码,列表如下:
Public Enum LogonErrorCode As Byte
'// LOGIN_STATE_FAILED:
LOGIN_FAILED = 1 ' // 2, B, C, D // "Unable to connect"
LOGIN_BANNED = 3 ' // "This World of Warcraft account has been closed and is no longer in service -- Please check the registered email address of this account for further information."; -- This is the error message players get when trying to log in with a banned account.
LOGIN_UNKNOWN_ACCOUNT = 4 ' // 5 // "The information you have entered is not valid. Please check the spelling of the account name and password. If you need help in retrieving a lost or stolen password and account, see www.worldofwarcraft.com for more information.";
LOGIN_ALREADYONLINE = 6 ' // "This account is already logged into World of Warcraft. Please check the spelling and try again.";
LOGIN_NOTIME = 7 ' // "You have used up your prepaid time for this account. Please purchase more to continue playing";
LOGIN_DBBUSY = 8 ' // "Could not log in to World of Warcraft at this time. Please try again later.";
LOGIN_BADVERSION = 9 ' // "Unable to validate game version. This may be caused by file corruption or the interference of another program. Please visitwww.blizzard.com/support/wow/ for more information and possible solutions to this issue.";
LOGIN_PARENTALCONTROL = &HF ' // "17"="LOGIN_PARENTALCONTROL" // "Access to this account has been blocked by parental controls. Your settings may be changed in your account preferences at http://www.worldofwarcraft.com.";
'// LOGIN_STATE_AUTHENTICATED:
LOGIN_OK = 0 ' // E
'// LOGIN_STATE_DOWNLOADFILE, LOGIN_OK
LOGIN_DOWNLOADFILE = &HA ' // not official name
End Enum
size为无符号16位整数型,表示除了封包头部(命令码,错误码,和封包大小)之外的剩余封包大小
gamename一直都为WoW,不过4个字节长度,第一个字节为0
version表示客户端版本号,3字节,一个字节表示一个小节,比如1.8.0则为01 08 00
build是客户端的build号,16位无符号整数,如:7F12就是4735。
platform是系统信息,一般为“68x”即x86,4字节
os是操作系统,如niW则为Win,4字节
localization是客户端语言和区域信息,英语英国则为BGne,即enGB,4字节
timezone就是时区,没什么好解释,4字节
ip是客户端ip地址,4字节,如7F 00 00 1为127.0.0.001
acclen为帐号名长度,8位无符号整数,1字节
acc为用户输入的用户名,全部大写
其实这么多信息,真正重要的是版本号和用户名,其他处不处理看个人喜好。
服务器的响应封包结构如下:
Public Structure SServerLogonChallenge_Ok
Dim cmd As Packet.LogonOpcode '; // 0x00 -> OPcode = CMD_AUTH_LOGON_CHALLENGE ? CMD_GRUNT_AUTH_CHALLENGE
Dim [error] As Packet.LogonErrorCode ' ; // 0 -> ok = LOGIN_OK
Dim unk1 As Byte ' // 0x00
Dim B() As Byte ' // calculated by server
Dim g_len As Byte ' // 0x01
Dim g As Byte ' // 0x07 (const)
Dim N_len As Byte ' // 0x20
Dim N As BigInteger ' // const
Dim salt() As Byte ' // random
Dim unk2() As Byte ' //1: BA 79 D4 8D - BF FC BF AD - 8C B4 EC B3 - 75 C5 96 05
End Structure
其中cmd还是CMD_AUTH_LOGON_CHALLENGE,没变。error不管是否版本错误或者帐号不存在,都通通回复LOGIN_OK,unk1不知道有什么用,0即可。
下面讲解B的算法:
要算B,首先得生成几个其他得值,首先是N,这个是一个不变的常数,也许大家已经注意到了,N的类型是BigInteger,B和SALT都是转换成16进制的BigInteger,vb.net或者c#本身不支持这个数据类型,要实现这个就要用到一个附加的类。这个类我稍微修改了一下,这样可以适用于WoW的运算,点击这里可以下载到。N用16进制表示出来为:"894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7",长度是32字节。然后是salt,是一个随机生成的一个32字节长的整数。g为常数7。
现在开始计算部分:
首先,我们需要一个临时字符串,用来保存从帐号数据库里取得的正确的用户名和密码(例如,用户名abc,密码def,那么这个字符串为:”ABC:DEF”)。此外我们还需要另外一个Byte()类变量,比如Dim TempHash() As Byte,TempHash等于刚才那个临时字符串的sha1值。现在将tempHash反转(reserve)然后用他创建一个新的临时BigInteger变量x。
现在我们需要另外一些临时变量:tmp As BigInteger, v As BigInteger, k As New BigInteger,其中tmp等于N反转后生成的biginteger,k等于3,v=(x^g) mod tmp
(运算符都为vb运算符,^是指数,mod是求余数,c#或者c++请自行修改)
现在需要随机生成另外一个20字节长度的大整数:Dim b As BigInteger。注意,这里的b是个临时变量,并不是最后计算结果B。
现在从新给tmp赋值:tmp=(k * v + (b ^ g) mod tmp) mod tmp
计算结果B等于tmp的16进制表达方式然后反转。
封包最后一个变量,unk2,随机生成一个16字节长度大整数转换成16进制即可
下面是我写的处理第一个封包的代码,没优化,所以有些地方有点不合理,不过不影响使用:
Private Shared Function ProcessClientLogonChallenge(ByVal sock As MyWoW.LoginServer.Client, ByVal data As ClientLogonChallenge) As Boolean
Dim senddata As Packet.PacketSendManager.Packet
Dim sendpacket As SServerLogonChallenge_Ok
Dim tick As Integer = GetTickCount
Dim x As BigInteger, tmp As New BigInteger, temphashb() As Byte
sock.Challenged = True
sendpacket.error = LogonErrorCode.LOGIN_OK
sock.PendingError = LogonErrorCode.LOGIN_OK
If data.acclen >= 30 Then
sock.PendingError = LogonErrorCode.LOGIN_UNKNOWN_ACCOUNT
'Return True
End If
If Convert.ToInt16(data.build) < m_minbuild Or Convert.ToInt16(data.build) > m_maxbuild Then
sock.PendingError = LogonErrorCode.LOGIN_BADVERSION
End If
Dim _id As String = data.acc
Dim _pass As String = m_Database.GetItem("id='" & _id & "'", "password")
If _pass Is Nothing Then sock.PendingError = LogonErrorCode.LOGIN_UNKNOWN_ACCOUNT
Dim TempHash As SHA1
sendpacket.cmd = LogonOpcode.CMD_AUTH_LOGON_CHALLENGE
'If sock.PendingError <> LogonErrorCode.LOGIN_OK Then sock.PendingError = LogonErrorCode.LOGIN_OK
'If sendpacket.error <> LogonErrorCode.LOGIN_OK Then sendpacket.error = LogonErrorCode.LOGIN_OK
sendpacket.unk1 = 0
sendpacket.N_len = 32
sendpacket.N = New BigInteger("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7")
sock.N = New BigInteger(sendpacket.N)
sendpacket.salt = tmp.genRandom(8 * 32).getBytes
sock.Salt = New BigInteger(sendpacket.salt)
sendpacket.g_len = 1
sendpacket.g = 7
TempHash = SHA1.Create
Dim temp As String = _id & ":" & _pass
sock.Username = _id.ToUpper
TempHash.TransformFinalBlock(Helper.strtobytes(temp.ToUpper), 0, temp.Length)
temphashb = TempHash.Hash
temphashb = Helper.CombinBytes(sendpacket.salt, temphashb)
TempHash = SHA1.Create
TempHash.TransformFinalBlock(temphashb, 0, temphashb.Length)
temphashb = TempHash.Hash
Array.Reverse(temphashb)
x = New BigInteger(temphashb)
temphashb = sendpacket.N.getBytes
Array.Reverse(temphashb)
tmp = New BigInteger(temphashb)
Dim g As New BigInteger(sendpacket.g), v As BigInteger, k As New BigInteger(3)
v = g.modPow(x, tmp)
sock.v = New BigInteger(v)
Dim tmp2 As BigInteger, b As BigInteger
b = BigInteger.genRandom(20 * 8)
sock.pb = New BigInteger(b)
Dim bb() As Byte = b.getBytes
Array.Reverse(bb)
b = New BigInteger(bb)
tmp2 = BigInteger.op_Multiply(k, v)
tmp = g.modPow(b, tmp)
tmp = BigInteger.op_Addition(tmp2, tmp)
tmp = BigInteger.op_Modulus(tmp, New BigInteger(temphashb))
temphashb = tmp.getBytes
Array.Reverse(temphashb)
sock.B = New BigInteger(temphashb)
sendpacket.B = temphashb
sendpacket.unk2 = BigInteger.genRandom(8 * 16).getBytes
Dim datas() As Byte, br As New BinReader
ReDim temphashb(0)
temphashb(0) = sendpacket.cmd
datas = temphashb
temphashb(0) = sendpacket.error
datas = Helper.CombinBytes(datas, temphashb)
temphashb(0) = sendpacket.unk1
datas = Helper.CombinBytes(datas, temphashb)
datas = Helper.CombinBytes(datas, sendpacket.B)
temphashb(0) = sendpacket.g_len
datas = Helper.CombinBytes(datas, temphashb)
temphashb(0) = sendpacket.g
datas = Helper.CombinBytes(datas, temphashb)
temphashb(0) = sendpacket.N_len
datas = Helper.CombinBytes(datas, temphashb)
datas = Helper.CombinBytes(datas, sendpacket.N.getBytes)
datas = Helper.CombinBytes(datas, sendpacket.salt)
datas = Helper.CombinBytes(datas, sendpacket.unk2)
Dim packets As New Packet.PacketSendManager.Packet
packets.sock = sock.CSocket
packets.data = datas
ConMsg("Challenge运算时间:" & GetTickCount - tick)
m_SendMgr.AddPacket(packets)
Return True
|