diff --git a/common/auth_token.go b/common/auth_token.go index 0af3abe..e53113f 100644 --- a/common/auth_token.go +++ b/common/auth_token.go @@ -29,7 +29,7 @@ func generateRandom(n int) []byte { var ( authTokenKey = generateRandom(16) authTokenIV = generateRandom(16) - authTokenMagic = generateRandom(15) + authTokenMagic = generateRandom(14) loginTicketKey = generateRandom(16) loginTicketIV = generateRandom(16) @@ -46,7 +46,7 @@ func appendString(blob []byte, value string, maxlen int) []byte { return blob } -func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, isLocalhost bool) (string, string) { +func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, unitcd byte, isLocalhost bool) (string, string) { blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix())) blob = appendString(blob, gamecd, 4) @@ -65,6 +65,8 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64 challenge := RandomString(8) blob = append(blob, []byte(challenge)...) + blob = append(blob, byte(unitcd)) + if isLocalhost { blob = append(blob, 0x01) } else { @@ -82,7 +84,7 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64 return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge } -func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, isLocalhost bool) { +func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, unitcd byte, isLocalhost bool) { if !strings.HasPrefix(token, "NDS") { err = errors.New("invalid auth token prefix") return @@ -119,7 +121,8 @@ func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime ti lang = blob[0x2B] ingamesn = string(blob[0x2D : 0x2D+min(blob[0x2C], 75)]) challenge = string(blob[0x78:0x80]) - isLocalhost = blob[0x80] == 0x01 + unitcd = blob[0x80] + isLocalhost = blob[0x81] == 0x01 return } diff --git a/common/game_list.go b/common/game_list.go index 1c72f6b..2907522 100644 --- a/common/game_list.go +++ b/common/game_list.go @@ -4,6 +4,7 @@ import ( "encoding/csv" "os" "strconv" + "strings" "sync" ) @@ -84,3 +85,20 @@ func ReadGameList() { gameListNameLookup[entry[1]] = index } } + +func GetExpectedUnitCode(gameName string) byte { + if strings.HasSuffix(gameName, "wii") || strings.HasSuffix(gameName, "wiiam") { + return 1 + } + + if gameName == "sneezieswiiw" || gameName == "wormswiiware" || gameName == "wormswiiwaream" { + return 1 + } + + // Games with weird other regions + if gameName == "jockracerna" || gameName == "jockracereu" || gameName == "sengo3wiijp" { + return 1 + } + + return 0 +} diff --git a/common/ip_address.go b/common/ip_address.go index 9fe8e16..965e4fb 100644 --- a/common/ip_address.go +++ b/common/ip_address.go @@ -45,6 +45,14 @@ func IPFormatToString(ip string) (string, string) { return strconv.FormatInt(int64(intIP), 10), strconv.FormatUint(uint64(intPort), 10) } +func IPFormatToStringLE(ip string) (string, string) { + intIP, intPort := IPFormatToInt(ip) + + // Convert to little endian and print as big endian int + intIP = int32((uint32(intIP) >> 24) | ((uint32(intIP) & 0x00FF0000) >> 8) | ((uint32(intIP) & 0x0000FF00) << 8) | ((uint32(intIP) & 0x000000FF) << 24)) + return strconv.FormatInt(int64(intIP), 10), strconv.FormatUint(uint64(intPort), 10) +} + func IPFormatBytes(ip string) []byte { if strings.Contains(ip, ":") { ip = strings.Split(ip, ":")[0] diff --git a/common/match_command.go b/common/match_command.go index 62af7ad..dcf5fa1 100644 --- a/common/match_command.go +++ b/common/match_command.go @@ -49,6 +49,7 @@ type MatchCommandData struct { TellAddr *MatchCommandDataTellAddr ServerCloseClient *MatchCommandDataServerCloseClient SuspendMatch *MatchCommandDataSuspendMatch + Other []byte } type MatchCommandDataReservation struct { @@ -189,7 +190,7 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD switch command { case MatchReservation: - if version == 3 && len(buffer) < 0x0C { + if version == 3 && len(buffer) < 0x04 { break } @@ -473,6 +474,25 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD }, }, true + case MatchPollTimeout: + if len(buffer) != 0x00 { + break + } + return MatchCommandData{ + Version: version, + Command: command, + }, true + + case MatchPollToAck: + if len(buffer) != 0x04 { + break + } + return MatchCommandData{ + Version: version, + Command: command, + Other: buffer, + }, true + case MatchSuspendMatch: if len(buffer) == 0x08 { return MatchCommandData{ @@ -497,6 +517,13 @@ func DecodeMatchCommand(command byte, buffer []byte, version int) (MatchCommandD }, }, true } + + default: + return MatchCommandData{ + Version: version, + Command: command, + Other: buffer, + }, true } return MatchCommandData{}, false @@ -640,6 +667,12 @@ func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) { } return message, true + case MatchPollTimeout: + return []byte{}, true + + case MatchPollToAck: + return data.Other, true + case MatchSuspendMatch: message := binary.LittleEndian.AppendUint32([]byte{}, data.SuspendMatch.HostProfileID) message = binary.LittleEndian.AppendUint32(message, data.SuspendMatch.IsHostFlag) @@ -649,6 +682,10 @@ func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) { message = binary.LittleEndian.AppendUint32(message, data.SuspendMatch.ClientAIDValue) } return message, true + + default: + logging.Info("Common", "Unknown match command:", aurora.Cyan(command), "data:", data.Other) + return data.Other, true } return []byte{}, false diff --git a/game_list.tsv b/game_list.tsv index 42310d2..7c06182 100644 --- a/game_list.tsv +++ b/game_list.tsv @@ -660,7 +660,9 @@ Celtic Kings Demo celtickingsdemo 667 TCQMZI Celtic Lore: Sidhe Hills celticloresidhehil Ceville ceville Chainz 2: Relinked chainz2relinked -Champion Jockey - G1 Jockey & Gallop Racer (Wii) jockracerna yNVo3W +Champion Jockey - G1 Jockey & Gallop Racer (EU) (Wii) jockracereu eG1kq5 +Champion Jockey - G1 Jockey & Gallop Racer (JP) (Wii) jockracerwii haVAVM +Champion Jockey - G1 Jockey & Gallop Racer (NA) (Wii) jockracerna yNVo3W Champions Online champon Championship Euchre cheuchre 317 Yw7fc9 Championship Hearts chhearts 251 Yw7fc9 diff --git a/gpcm/login.go b/gpcm/login.go index 2549024..0209922 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -18,6 +18,11 @@ import ( "github.com/logrusorgru/aurora/v3" ) +const ( + UnitCodeDS = 0 + UnitCodeWii = 1 +) + func generateResponse(gpcmChallenge, nasChallenge, authToken, clientChallenge string) string { hasher := md5.New() hasher.Write([]byte(nasChallenge)) @@ -137,7 +142,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { return } - err, gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, isLocalhost := common.UnmarshalNASAuthToken(authToken) + err, gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, unitcd, isLocalhost := common.UnmarshalNASAuthToken(authToken) if err != nil { g.replyError(ErrLogin) return @@ -149,10 +154,6 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { return } - _, payloadVerExists := command.OtherValues["payload_ver"] - _, signatureExists := command.OtherValues["wwfc_sig"] - deviceId := uint32(0) - g.GameName = command.OtherValues["gamename"] logging.Info(g.ModuleName, "Game name:", aurora.Cyan(g.GameName)) g.GameCode = gamecd @@ -160,6 +161,11 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { g.Language = lang g.ConsoleFriendCode = cfc g.InGameName = ingamesn + g.UnitCode = unitcd + + _, payloadVerExists := command.OtherValues["payload_ver"] + _, signatureExists := command.OtherValues["wwfc_sig"] + deviceId := uint32(0) if hostPlatform, exists := command.OtherValues["wwfc_host"]; exists { g.HostPlatform = hostPlatform @@ -169,14 +175,22 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { g.LoginInfoSet = true - if isLocalhost && !payloadVerExists && !signatureExists { - // Players using the DNS exploit, need patching using a QR2 exploit - // TODO: Check that the game is compatible with the DNS - g.NeedsExploit = true - } else { - deviceId = g.verifyExLoginInfo(command, authToken) - if deviceId == 0 { - return + if g.GameName != "mahjongkcds" && common.GetExpectedUnitCode(g.GameName) != unitcd { + logging.Error(g.ModuleName, "Incorrect unit code specified:", aurora.Cyan(unitcd)) + g.replyError(ErrLogin) + return + } + + if g.UnitCode == UnitCodeWii { + if isLocalhost && !payloadVerExists && !signatureExists { + // Players using the DNS exploit, need patching using a QR2 exploit + // TODO: Check that the game is compatible with the DNS + g.NeedsExploit = true + } else { + deviceId = g.verifyExLoginInfo(command, authToken) + if deviceId == 0 { + return + } } } @@ -251,13 +265,19 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { // Notify QR2 of the login qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated, g.User.Restricted, KickPlayer) + replyUserId := g.User.UserId + if g.UnitCode == UnitCodeDS { + // Workaround for SDK bug + replyUserId = 0 + } + payload := common.CreateGameSpyMessage(common.GameSpyCommand{ Command: "lc", CommandValue: "2", OtherValues: map[string]string{ "sesskey": strconv.FormatInt(int64(g.SessionKey), 10), "proof": proof, - "userid": strconv.FormatUint(g.User.UserId, 10), + "userid": strconv.FormatUint(replyUserId, 10), "profileid": strconv.FormatUint(uint64(g.User.ProfileId), 10), "uniquenick": g.User.UniqueNick, "lt": g.LoginTicket, diff --git a/gpcm/main.go b/gpcm/main.go index d5b4b96..9ac2c81 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -38,6 +38,7 @@ type GameSpySession struct { ConsoleFriendCode uint64 DeviceId uint32 HostPlatform string + UnitCode byte Status string LocString string diff --git a/gpsp/main.go b/gpsp/main.go index 692aba9..2c18410 100644 --- a/gpsp/main.go +++ b/gpsp/main.go @@ -75,6 +75,9 @@ func handleRequest(conn net.Conn) { switch command.Command { default: logging.Error(moduleName, "Unknown command:", command.Command) + logging.Error(moduleName, "Raw data:", string(buffer)) + replyError(moduleName, conn, gpcm.ErrParse) + break case "ka": conn.Write([]byte(`\ka\\final\`)) @@ -83,6 +86,10 @@ func handleRequest(conn net.Conn) { case "otherslist": conn.Write([]byte(handleOthersList(command))) break + + case "search": + conn.Write([]byte(handleSearch(command))) + break } } } diff --git a/gpsp/search.go b/gpsp/search.go new file mode 100644 index 0000000..59f7b5f --- /dev/null +++ b/gpsp/search.go @@ -0,0 +1,33 @@ +package gpsp + +import ( + "strconv" + "wwfc/common" + "wwfc/gpcm" + "wwfc/logging" + + "github.com/logrusorgru/aurora/v3" +) + +func handleSearch(command common.GameSpyCommand) string { + moduleName := "GPSP" + + strProfileId, ok := command.OtherValues["profileid"] + if !ok { + logging.Error(moduleName, "Missing profileid in search") + return gpcm.ErrSearch.GetMessage() + } + + profileId, err := strconv.ParseUint(strProfileId, 10, 32) + if err != nil { + logging.Error(moduleName, "Invalid profileid:", strProfileId) + return gpcm.ErrSearch.GetMessage() + } + + moduleName = "GPSP:" + strconv.FormatUint(profileId, 10) + logging.Info(moduleName, "Search for", aurora.Cyan(profileId)) + + return common.CreateGameSpyMessage(common.GameSpyCommand{ + Command: "bsrdone", + }) +} diff --git a/nas/auth.go b/nas/auth.go index e0b7e85..042bb8f 100644 --- a/nas/auth.go +++ b/nas/auth.go @@ -30,7 +30,7 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request replyHTTPError(w, 400, "400 Bad Request") return } - + // Need to know this here to determine UTF-16 endianness (LE for DS, BE for Wii) // unitcd 0 = DS, 1 = Wii unitcdValues, ok := r.PostForm["unitcd"] @@ -264,10 +264,10 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st // Only later DS games send this ingamesn, ok := fields["ingamesn"] if ok { - authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, isLocalhost) + authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], ingamesn, 0, isLocalhost) logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname), "ingamesn:", aurora.Cyan(ingamesn)) } else { - authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", isLocalhost) + authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, 0, 0, langByte[0], "", 0, isLocalhost) logging.Notice(moduleName, "Login (DS)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "devname:", aurora.Cyan(devname)) } @@ -299,7 +299,7 @@ func login(moduleName string, fields map[string]string, isLocalhost bool) map[st return param } - authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], isLocalhost) + authToken, challenge = common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], 1, isLocalhost) logging.Notice(moduleName, "Login (Wii)", aurora.Cyan(strconv.FormatUint(userId, 10)), aurora.Cyan(gsbrcd), "ingamesn:", aurora.Cyan(fields["ingamesn"])) } diff --git a/qr2/heartbeat.go b/qr2/heartbeat.go index 00d6d20..cdf7ae1 100644 --- a/qr2/heartbeat.go +++ b/qr2/heartbeat.go @@ -40,18 +40,30 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b realIP, realPort := common.IPFormatToString(addr.String()) + noIP := false if ip, ok := payload["publicip"]; !ok || ip == "0" { - // Set the public IP key to the real IP - payload["publicip"] = realIP - payload["publicport"] = realPort + noIP = true } - // Client is mistaken about its public IP - if payload["publicip"] != realIP || payload["publicport"] != realPort { - logging.Error(moduleName, "Public IP mismatch") - return + clientEndianness := common.GetExpectedUnitCode(payload["gamename"]) + if !noIP && clientEndianness == ClientBigEndian { + if payload["publicip"] != realIP || payload["publicport"] != realPort { + // Client is mistaken about its public IP + logging.Error(moduleName, "Public IP mismatch") + return + } + } else if !noIP && clientEndianness == ClientLittleEndian { + realIPLE, realPortLE := common.IPFormatToStringLE(addr.String()) + if payload["publicip"] != realIPLE || payload["publicport"] != realPortLE { + // Client is mistaken about its public IP + logging.Error(moduleName, "Public IP mismatch") + return + } } + payload["publicip"] = realIP + payload["publicport"] = realPort + lookupAddr := makeLookupAddr(addr.String()) statechanged, ok := payload["statechanged"] @@ -100,7 +112,7 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b mutex.Unlock() } - if !session.Authenticated { + if !session.Authenticated || noIP { sendChallenge(conn, addr, session, lookupAddr) } else if !session.ExploitReceived && session.Login != nil && session.Login.NeedsExploit && statechanged == "1" { logging.Notice(moduleName, "Sending SBCM exploit to DNS patcher client") diff --git a/qr2/message.go b/qr2/message.go index 89d7792..348946f 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -241,7 +241,7 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { } s := sleep.Sleeper{} - s.AddWaker(&receiver.MessageAckWaker) + s.AddWaker(receiver.MessageAckWaker) timeWaker := sleep.Waker{} s.AddWaker(&timeWaker) diff --git a/qr2/session.go b/qr2/session.go index 0e92a67..1921a98 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -15,9 +15,9 @@ import ( ) const ( - ClientNoEndian = iota - ClientBigEndian - ClientLittleEndian + ClientLittleEndian = 0 + ClientBigEndian = 1 + ClientNoEndian = 2 ) type Session struct { @@ -34,8 +34,8 @@ type Session struct { PacketCount uint32 Reservation common.MatchCommandData ReservationID uint64 - MessageMutex deadlock.Mutex - MessageAckWaker sleep.Waker + MessageMutex *deadlock.Mutex + MessageAckWaker *sleep.Waker GroupPointer *Group } @@ -116,8 +116,8 @@ func setSessionData(moduleName string, addr net.Addr, sessionId uint32, payload PacketCount: 0, Reservation: common.MatchCommandData{}, ReservationID: 0, - MessageMutex: deadlock.Mutex{}, - MessageAckWaker: sleep.Waker{}, + MessageMutex: &deadlock.Mutex{}, + MessageAckWaker: &sleep.Waker{}, } }