Allow extra user data in match commands

This commit is contained in:
mkwcat 2023-12-15 08:05:36 -05:00
parent adaefd1cbb
commit 29a2e305d9
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
4 changed files with 127 additions and 38 deletions

View File

@ -58,6 +58,8 @@ type MatchCommandDataReservation struct {
IsFriend bool
LocalPlayerCount uint32
ResvCheckValue uint32
UserData []byte
}
type MatchCommandDataResvOK struct {
@ -75,17 +77,20 @@ type MatchCommandDataResvOK struct {
ClientCount uint32
ResvCheckValue uint32
// Only exists in version 3 and 11
// Version 3 and 11
ProfileIDs []uint32
// Version 11
IsFriend bool
UserData uint32
UserData []byte
}
type MatchCommandDataResvDeny struct {
Reason uint32
ReasonString string
UserData []byte
}
type MatchCommandDataTellAddr struct {
@ -173,13 +178,18 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
return MatchCommandData{}, false
}
// Match commands must be 4 byte aligned
if (len(buffer) & 3) != 0 {
return MatchCommandData{}, false
}
switch command {
case MatchReservation:
if version == 3 && (len(buffer) != 0x04 && len(buffer) != 0x0C) {
if version == 3 && len(buffer) < 0x0C {
break
}
if (version == 11 && len(buffer) != 0x14) || (version == 90 && len(buffer) != 0x24) {
if (version == 11 && len(buffer) < 0x14) || (version == 90 && len(buffer) < 0x24) {
break
}
@ -188,7 +198,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
break
}
if version == 3 && len(buffer) == 0x04 {
if version == 3 && len(buffer) < 0x0C {
return MatchCommandData{Reservation: &MatchCommandDataReservation{
MatchType: byte(matchType),
HasPublicIP: false,
@ -207,6 +217,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
HasPublicIP: true,
PublicIP: binary.BigEndian.Uint32(buffer[0x04:0x08]),
PublicPort: uint16(publicPort),
UserData: buffer[0x0C:],
}}, true
case 11:
@ -223,6 +234,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
PublicPort: uint16(publicPort),
IsFriend: isFriend,
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x10:0x14]),
UserData: buffer[0x14:],
}}, true
case 90:
@ -248,6 +260,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
IsFriend: isFriend,
LocalPlayerCount: binary.LittleEndian.Uint32(buffer[0x1C:0x20]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x20:0x24]),
UserData: buffer[0x24:],
}}, true
}
@ -258,49 +271,50 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
}
clientCount := binary.LittleEndian.Uint32(buffer[0x00:0x04])
if version == 3 && (clientCount > 29 || len(buffer) != int(0xC+clientCount*0x4)) {
if version == 3 && (clientCount > 29 || len(buffer) < int(0x0C+clientCount*0x4)) {
break
}
if version == 11 && (clientCount > 24 || len(buffer) != int(0x20+clientCount*0x4)) {
if version == 11 && (clientCount > 24 || len(buffer) < int(0x20+clientCount*0x4)) {
break
}
var profileIDs []uint32
for i := uint32(0); i < clientCount; i++ {
profileIDs = append(profileIDs, binary.LittleEndian.Uint32(buffer[0x4+i*4:0x4+i*4+4]))
profileIDs = append(profileIDs, binary.LittleEndian.Uint32(buffer[0x04+i*4:0x04+i*4+4]))
}
index := 0x4 + clientCount*4
index := 0x04 + clientCount*4
publicPort := binary.LittleEndian.Uint32(buffer[index+0x4 : index+0x8])
publicPort := binary.LittleEndian.Uint32(buffer[index+0x04 : index+0x08])
if publicPort > 0xffff {
break
}
if version == 3 {
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x4]),
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x04]),
PublicPort: uint16(publicPort),
ClientCount: clientCount,
ProfileIDs: profileIDs,
UserData: buffer[index+0x8:],
}}, true
} else if version == 11 {
isFriendValue := binary.LittleEndian.Uint32(buffer[index+0x8 : index+0xC])
isFriendValue := binary.LittleEndian.Uint32(buffer[index+0x08 : index+0x0C])
if isFriendValue > 1 {
break
}
isFriend := isFriendValue != 0
return MatchCommandData{ResvOK: &MatchCommandDataResvOK{
MaxPlayers: binary.LittleEndian.Uint32(buffer[0x14:0x18]),
SenderAID: binary.LittleEndian.Uint32(buffer[index+0xC : index+0x10]),
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x4]),
MaxPlayers: binary.LittleEndian.Uint32(buffer[index+0x14 : index+0x18]),
SenderAID: binary.LittleEndian.Uint32(buffer[index+0x0C : index+0x10]),
PublicIP: binary.BigEndian.Uint32(buffer[index : index+0x04]),
PublicPort: uint16(publicPort),
GroupID: binary.LittleEndian.Uint32(buffer[0x10:0x14]),
GroupID: binary.LittleEndian.Uint32(buffer[index+0x10 : index+0x14]),
ClientCount: clientCount,
ProfileIDs: profileIDs,
IsFriend: isFriend,
UserData: binary.LittleEndian.Uint32(buffer[0x18:0x1C]),
UserData: buffer[index+0x18:],
}}, true
}
break
@ -335,6 +349,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
ReceiverNewAID: binary.LittleEndian.Uint32(buffer[0x28:0x2C]),
ClientCount: binary.LittleEndian.Uint32(buffer[0x2C:0x30]),
ResvCheckValue: binary.LittleEndian.Uint32(buffer[0x30:0x34]),
UserData: buffer[0x34:],
}}, true
case MatchResvDeny:
@ -359,6 +374,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD
return MatchCommandData{ResvDeny: &MatchCommandDataResvDeny{
Reason: reason,
ReasonString: reasonString,
UserData: buffer[0x4:],
}}, true
case MatchResvWait:
@ -468,6 +484,12 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, data.Reservation.LocalPlayerCount)
message = binary.LittleEndian.AppendUint32(message, data.Reservation.ResvCheckValue)
}
message = append(message, data.Reservation.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
case MatchResvOK:
@ -483,6 +505,11 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, uint32(data.ResvOK.PublicPort))
if version == 3 {
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
}
@ -496,7 +523,12 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.SenderAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.GroupID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.MaxPlayers)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.UserData)
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
}
@ -514,10 +546,22 @@ func EncodeMatchCommand(command byte, data MatchCommandData, version int) ([]byt
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ReceiverNewAID)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ClientCount)
message = binary.LittleEndian.AppendUint32(message, data.ResvOK.ResvCheckValue)
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
case MatchResvDeny:
message := binary.LittleEndian.AppendUint32([]byte{}, data.ResvDeny.Reason)
message = append(message, data.ResvOK.UserData...)
if (len(message) & 3) != 0 {
return []byte{}, false
}
return message, true
case MatchResvWait:

View File

@ -3,6 +3,7 @@ package gpcm
import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"strconv"
"strings"
@ -263,6 +264,12 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
break
}
if len(msgData) > 0x200 || (len(msgData)&3) != 0 {
logging.Error(g.ModuleName, "Invalid length message data; message:", msg)
g.replyError(ErrMessage)
return
}
msgMatchData, ok := common.DecodeMatchCommand(cmd, msgData, version)
common.LogMatchCommand(g.ModuleName, strconv.FormatInt(int64(toProfileId), 10), cmd, msgMatchData)
if !ok {
@ -272,16 +279,23 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
}
if cmd == common.MatchReservation {
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) == int32(msgMatchData.Reservation.PublicIP) {
g.QR2IP = uint64(msgMatchData.Reservation.PublicIP) | (uint64(msgMatchData.Reservation.PublicPort) << 32)
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) != int32(msgMatchData.Reservation.PublicIP) {
logging.Error(g.ModuleName, "RESERVATION: Public IP mismatch")
g.replyError(ErrMessage)
return
}
} else if cmd == common.MatchResvOK {
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) == int32(msgMatchData.ResvOK.PublicIP) {
g.QR2IP = uint64(msgMatchData.ResvOK.PublicIP) | (uint64(msgMatchData.ResvOK.PublicPort) << 32)
}
}
// TODO: Replace public IP with QR2 search ID
g.QR2IP = uint64(msgMatchData.Reservation.PublicIP) | (uint64(msgMatchData.Reservation.PublicPort) << 32)
} else if cmd == common.MatchResvOK {
if common.IPFormatNoPortToInt(g.Conn.RemoteAddr().String()) != int32(msgMatchData.ResvOK.PublicIP) {
logging.Error(g.ModuleName, "RESV_OK: Public IP mismatch")
g.replyError(ErrMessage)
return
}
g.QR2IP = uint64(msgMatchData.ResvOK.PublicIP) | (uint64(msgMatchData.ResvOK.PublicPort) << 32)
}
mutex.Lock()
defer mutex.Unlock()
@ -300,7 +314,10 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
}
if cmd == common.MatchReservation {
g.ReservationPID = uint32(toProfileId)
msgMatchData.Reservation.PublicIP = 0
msgMatchData.Reservation.PublicPort = 0
msgMatchData.Reservation.LocalIP = 0
msgMatchData.Reservation.LocalPort = 0
} else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait {
if toSession.ReservationPID != g.User.ProfileId {
logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "has no reservation with the sender")
@ -319,9 +336,32 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
g.replyError(ErrMessage)
return
}
if g.QR2IP&0xffffffff != toSession.QR2IP&0xffffffff {
searchId := qr2.GetSearchID(g.QR2IP)
if searchId == 0 {
logging.Error(g.ModuleName, "Could not get QR2 search ID for IP", aurora.Cyan(fmt.Sprintf("%016x", g.QR2IP)))
g.replyError(ErrMessage)
return
}
msgMatchData.ResvOK.PublicIP = uint32(searchId & 0xffffffff)
msgMatchData.ResvOK.PublicPort = uint16(searchId >> 32)
}
}
}
newMsg, ok := common.EncodeMatchCommand(cmd, msgMatchData, version)
if !ok || len(newMsg) > 0x200 {
logging.Error(g.ModuleName, "Failed to encode match command; message:", msg)
g.replyError(ErrMessage)
return
}
if cmd == common.MatchReservation {
g.ReservationPID = uint32(toProfileId)
}
sendMessageToSession("1", g.User.ProfileId, toSession, msg)
}

View File

@ -30,7 +30,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
var receiver *Session
senderIPInt, _ := common.IPFormatToInt(senderIP)
useSearchID := destSearchID < (0x400 << 32)
useSearchID := destSearchID < (1 << 24)
if useSearchID {
receiver = sessionBySearchID[destSearchID]
} else {
@ -62,7 +62,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
moduleName = "QR2/MSG:s" + strconv.FormatUint(uint64(natnegID), 10)
} else if bytes.Equal(message[:4], []byte{0xbb, 0x49, 0xcc, 0x4d}) || bytes.Equal(message[:4], []byte("SBCM")) {
// DWC match command
if len(message) < 0x14 {
if len(message) < 0x14 || len(message) > 0x94 {
logging.Error(moduleName, "Received invalid length match command packet")
return
}
@ -76,13 +76,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
senderProfileID := binary.LittleEndian.Uint32(message[0x10:0x14])
moduleName = "QR2/MSG:p" + strconv.FormatUint(uint64(senderProfileID), 10)
dataSize := message[9]
if dataSize > 0x80 {
logging.Error(moduleName, "Received malicious match command packet header")
return
}
if (int(dataSize) + 0x14) != len(message) {
if (int(message[9]) + 0x14) != len(message) {
logging.Error(moduleName, "Received invalid match command packet header")
return
}
@ -204,7 +198,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
var matchMessage []byte
matchMessage, ok = common.EncodeMatchCommand(message[8], matchData, version)
if !ok {
if !ok || len(matchMessage) > 0x80 {
logging.Error(moduleName, "Failed to reencode match command:", aurora.Cyan(printHex(message)))
return
}

View File

@ -108,7 +108,7 @@ func setSessionData(moduleName string, addr net.Addr, sessionId uint32, payload
// Set search ID
for {
searchID := uint64(rand.Int63n((0x400<<32)-1) + 1)
searchID := uint64(rand.Int63n((1<<24)-1) + 1)
if _, exists := sessionBySearchID[searchID]; !exists {
session.SearchID = searchID
session.Data["+searchid"] = strconv.FormatUint(searchID, 10)
@ -236,3 +236,14 @@ func GetSessionServers() []map[string]string {
return servers
}
func GetSearchID(addr uint64) uint64 {
mutex.Lock()
defer mutex.Unlock()
if session := sessions[addr]; session != nil {
return session.SearchID
}
return 0
}