From 29a2e305d92260ab43db762f9ea5e9571d84705f Mon Sep 17 00:00:00 2001 From: mkwcat Date: Fri, 15 Dec 2023 08:05:36 -0500 Subject: [PATCH] Allow extra user data in match commands --- common/match_command.go | 80 +++++++++++++++++++++++++++++++---------- gpcm/friend.go | 58 +++++++++++++++++++++++++----- qr2/message.go | 14 +++----- qr2/session.go | 13 ++++++- 4 files changed, 127 insertions(+), 38 deletions(-) diff --git a/common/match_command.go b/common/match_command.go index 979a4b6..aa1f38b 100644 --- a/common/match_command.go +++ b/common/match_command.go @@ -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: diff --git a/gpcm/friend.go b/gpcm/friend.go index c541e7a..0d5f35f 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -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) } diff --git a/qr2/message.go b/qr2/message.go index 3ca3dd6..4f96600 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -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 } diff --git a/qr2/session.go b/qr2/session.go index 9a63b26..0b4b89f 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -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 +}