diff --git a/database/login.go b/database/login.go index d36117a..a2b92ec 100644 --- a/database/login.go +++ b/database/login.go @@ -3,10 +3,11 @@ package database import ( "context" "fmt" - "github.com/jackc/pgx/v4/pgxpool" - "github.com/logrusorgru/aurora/v3" "wwfc/common" "wwfc/logging" + + "github.com/jackc/pgx/v4/pgxpool" + "github.com/logrusorgru/aurora/v3" ) func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsbrcd string, profileId uint32, ngDeviceId uint32) (User, bool) { @@ -52,7 +53,7 @@ func LoginUserToGPCM(pool *pgxpool.Pool, ctx context.Context, userId uint64, gsb user.LastName = *lastName } - if expectedNgId != nil { + if expectedNgId != nil && user.NgDeviceId != 0 { user.NgDeviceId = *expectedNgId if ngDeviceId != 0 && user.NgDeviceId != ngDeviceId { logging.Error("DATABASE", "NG device ID mismatch for profile", aurora.Cyan(user.ProfileId), "- expected", aurora.Cyan(fmt.Sprintf("%08x", user.NgDeviceId)), "but got", aurora.Cyan(fmt.Sprintf("%08x", ngDeviceId))) diff --git a/database/user.go b/database/user.go index d8e62c9..7736745 100644 --- a/database/user.go +++ b/database/user.go @@ -3,8 +3,9 @@ package database import ( "context" "errors" - "github.com/jackc/pgx/v4/pgxpool" "math/rand" + + "github.com/jackc/pgx/v4/pgxpool" ) const ( @@ -85,6 +86,15 @@ func (user *User) UpdateProfileID(pool *pgxpool.Pool, ctx context.Context, newPr return err } +func (user *User) UpdateDeviceID(pool *pgxpool.Pool, ctx context.Context, newDeviceId uint32) error { + _, err := pool.Exec(ctx, UpdateUserNGDeviceID, user.ProfileId, newDeviceId) + if err == nil { + user.NgDeviceId = newDeviceId + } + + return err +} + func GetUniqueUserID() uint64 { // Not guaranteed unique but doesn't matter in practice if multiple people have the same user ID. return uint64(rand.Int63n(0x80000000000)) diff --git a/gpcm/friend.go b/gpcm/friend.go index dfec279..7edfc76 100644 --- a/gpcm/friend.go +++ b/gpcm/friend.go @@ -169,9 +169,7 @@ func (g *GameSpySession) authAddFriend(command common.GameSpyCommand) { func (g *GameSpySession) setStatus(command common.GameSpyCommand) { status := command.CommandValue - if g.QR2IP != 0 { - qr2.ProcessGPStatusUpdate(g.QR2IP, status) - } + qr2.ProcessGPStatusUpdate(g.User.ProfileId, g.QR2IP, status) statstring, ok := command.OtherValues["statstring"] if !ok { diff --git a/gpcm/login.go b/gpcm/login.go index 80d1786..85eb81c 100644 --- a/gpcm/login.go +++ b/gpcm/login.go @@ -213,6 +213,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { sessions[g.User.ProfileId] = g mutex.Unlock() + g.AuthToken = authToken g.LoginTicket = common.MarshalGPCMLoginTicket(g.User.ProfileId) g.SessionKey = rand.Int31n(290000000) + 10000000 g.GameCode = gamecd @@ -224,7 +225,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, "RMCJ") // Notify QR2 of the login - qr2.Login(g.User.ProfileId, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated) + qr2.Login(g.User.ProfileId, gamecd, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated) payload := common.CreateGameSpyMessage(common.GameSpyCommand{ Command: "lc", @@ -245,6 +246,42 @@ func (g *GameSpySession) login(command common.GameSpyCommand) { g.sendFriendRequests() } +func (g *GameSpySession) exLogin(command common.GameSpyCommand) { + payloadVer, payloadVerExists := command.OtherValues["payload_ver"] + signature, signatureExists := command.OtherValues["wwfc_sig"] + deviceId := uint32(0) + + if !payloadVerExists || payloadVer != "1" { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "The payload version is invalid.", + Fatal: true, + }) + return + } + + if !signatureExists { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "Missing authentication signature.", + Fatal: true, + }) + return + } + + if deviceId = verifySignature(g.AuthToken, signature); deviceId == 0 { + g.replyError(GPError{ + ErrorCode: ErrLogin.ErrorCode, + ErrorString: "The authentication signature is invalid.", + Fatal: true, + }) + return + } + + g.DeviceAuthenticated = true + qr2.SetDeviceAuthenticated(g.User.ProfileId) +} + func IsLoggedIn(profileID uint32) bool { mutex.Lock() defer mutex.Unlock() diff --git a/gpcm/main.go b/gpcm/main.go index 463baec..640be7a 100644 --- a/gpcm/main.go +++ b/gpcm/main.go @@ -24,6 +24,7 @@ type GameSpySession struct { LoggedIn bool DeviceAuthenticated bool Challenge string + AuthToken string LoginTicket string SessionKey int32 GameCode string @@ -89,7 +90,7 @@ func (g *GameSpySession) closeSession() { if g.LoggedIn { qr2.Logout(g.User.ProfileId) if g.QR2IP != 0 { - qr2.ProcessGPStatusUpdate(g.QR2IP, "0") + qr2.ProcessGPStatusUpdate(g.User.ProfileId, g.QR2IP, "0") } g.sendLogoutStatus() } @@ -105,7 +106,7 @@ func (g *GameSpySession) closeSession() { // Handles incoming requests. func handleRequest(conn net.Conn) { - session := GameSpySession{ + session := &GameSpySession{ Conn: conn, User: database.User{}, ModuleName: "GPCM", @@ -172,6 +173,7 @@ func handleRequest(conn net.Conn) { session.Conn.Write([]byte(`\ka\\final\`)) }) commands = session.handleCommand("login", commands, session.login) + commands = session.handleCommand("wwfc_exlogin", commands, session.exLogin) commands = session.ignoreCommand("logout", commands) if len(commands) != 0 && session.LoggedIn == false { diff --git a/qr2/group.go b/qr2/group.go index 88b2b5a..1e4d736 100644 --- a/qr2/group.go +++ b/qr2/group.go @@ -2,11 +2,12 @@ package qr2 import ( "fmt" - "github.com/logrusorgru/aurora/v3" "strconv" "strings" "wwfc/common" "wwfc/logging" + + "github.com/logrusorgru/aurora/v3" ) type Group struct { @@ -112,11 +113,48 @@ func ProcessGPResvOK(cmd common.MatchCommandDataResvOK, senderIP uint64, senderP return processResvOK(moduleName, cmd, from, to) } -func ProcessGPStatusUpdate(senderIP uint64, status string) { - if status == "0" || status == "1" || status == "3" || status == "4" { - mutex.Lock() - defer mutex.Unlock() +func ProcessGPStatusUpdate(profileID uint32, senderIP uint64, status string) { + moduleName := "QR2/GPStatus:" + strconv.FormatUint(uint64(profileID), 10) + mutex.Lock() + defer mutex.Unlock() + + login, exists := logins[profileID] + if !exists || login == nil { + logging.Error(moduleName, "Received status update for non-existent profile ID", aurora.Cyan(profileID)) + return + } + + session := login.Session + if session == nil { + if senderIP == 0 { + logging.Info(moduleName, "Received status update for profile ID", aurora.Cyan(profileID), "but no session exists") + return + } + + // Login with this profile ID + session, exists = sessions[senderIP] + if !exists || session == nil { + logging.Info(moduleName, "Received status update for profile ID", aurora.Cyan(profileID), "but no session exists") + return + } + + if !session.setProfileID(moduleName, strconv.FormatUint(uint64(profileID), 10)) { + return + } + } + + // Send the client message exploit if not received yet + if status != "0" && status != "1" && !session.ExploitReceived && session.Login != nil && session.Login.NeedsExploit { + sessionCopy := *session + + mutex.Unlock() + logging.Notice(moduleName, "Sending SBCM exploit to DNS patcher client") + sendClientExploit(moduleName, sessionCopy) + mutex.Lock() + } + + if status == "0" || status == "1" || status == "3" || status == "4" { session := sessions[senderIP] if session == nil || session.GroupPointer == nil { return diff --git a/qr2/heartbeat.go b/qr2/heartbeat.go index afe7bc5..d94af35 100644 --- a/qr2/heartbeat.go +++ b/qr2/heartbeat.go @@ -2,11 +2,12 @@ package qr2 import ( "encoding/binary" - "github.com/logrusorgru/aurora/v3" "net" "strings" "wwfc/common" "wwfc/logging" + + "github.com/logrusorgru/aurora/v3" ) func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []byte) { @@ -16,13 +17,20 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b values := strings.Split(string(buffer[5:]), "\u0000") payload := map[string]string{} + unknowns := []string{} for i := 0; i < len(values); i += 2 { if len(values[i]) == 0 || values[i][0] == '+' { continue } - payload[values[i]] = values[i+1] logging.Info(moduleName, aurora.Cyan(values[i]).String()+":", aurora.Cyan(values[i+1])) + + if values[i] == "unknown" { + unknowns = append(unknowns, values[i+1]) + continue + } + + payload[values[i]] = values[i+1] } realIP, realPort := common.IPFormatToString(addr.String()) @@ -41,7 +49,8 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b lookupAddr := makeLookupAddr(addr.String()) - if statechanged, ok := payload["statechanged"]; ok { + statechanged, ok := payload["statechanged"] + if ok { if statechanged == "1" { // TODO: This would be a good place to run the server->client message exploit // for DNS patcher games that require code patches. The status code should be @@ -62,9 +71,29 @@ func heartbeat(moduleName string, conn net.PacketConn, addr net.Addr, buffer []b return } + if payload["gamename"] == "mariokartwii" && len(unknowns) > 0 { + // Try to login using the first unknown as a profile ID + // This makes it possible to execute the exploit on the client sooner + profileId := unknowns[0] + logging.Notice(moduleName, "Attempting to use unknown as profile ID", aurora.Cyan(profileId)) + + mutex.Lock() + session, sessionExists := sessions[lookupAddr] + if !sessionExists { + logging.Error(moduleName, "Session not found") + } else { + session.setProfileID(moduleName, profileId) + } + mutex.Unlock() + } + if !session.Authenticated { logging.Notice(moduleName, "Sending challenge") sendChallenge(conn, addr, session, lookupAddr) return + } else if !session.ExploitReceived && session.Login != nil && session.Login.NeedsExploit && statechanged == "1" { + logging.Notice(moduleName, "Sending SBCM exploit to DNS patcher client") + sendClientExploit(moduleName, session) + return } } diff --git a/qr2/logins.go b/qr2/logins.go index 290b9ef..3d1f2ca 100644 --- a/qr2/logins.go +++ b/qr2/logins.go @@ -2,6 +2,7 @@ package qr2 type LoginInfo struct { ProfileID uint32 + GameCode string InGameName string ConsoleFriendCode uint64 GPPublicIP string @@ -12,11 +13,12 @@ type LoginInfo struct { var logins = map[uint32]*LoginInfo{} -func Login(profileID uint32, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool) { +func Login(profileID uint32, gameCode string, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool) { mutex.Lock() logins[profileID] = &LoginInfo{ ProfileID: profileID, + GameCode: gameCode, InGameName: inGameName, ConsoleFriendCode: consoleFriendCode, GPPublicIP: publicIP, diff --git a/qr2/main.go b/qr2/main.go index 3358cf6..385adba 100644 --- a/qr2/main.go +++ b/qr2/main.go @@ -2,11 +2,12 @@ package qr2 import ( "encoding/binary" - "github.com/logrusorgru/aurora/v3" "net" "time" "wwfc/common" "wwfc/logging" + + "github.com/logrusorgru/aurora/v3" ) const ( @@ -21,6 +22,8 @@ const ( KeepAliveRequest = 0x08 AvailableRequest = 0x09 ClientRegisteredReply = 0x0A + + ClientExploitReply = 0x10 ) var masterConn net.PacketConn @@ -116,6 +119,10 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { case ClientMessageAckRequest: logging.Notice(moduleName, "Command:", aurora.Yellow("CLIENT_MESSAGE_ACK")) + + // In case ClientExploitReply is lost, this can be checked as well + // This would be sent either after the payload is downloaded, or the client is already patched + session.ExploitReceived = true return case KeepAliveRequest: @@ -131,7 +138,13 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) { return case ClientRegisteredReply: - logging.Notice(moduleName, "Command:", aurora.Cyan("CLIENT_REGISTERED")) + logging.Notice(moduleName, "Command:", aurora.Yellow("CLIENT_REGISTERED")) + break + + case ClientExploitReply: + logging.Notice(moduleName, "Command:", aurora.Yellow("CLIENT_EXPLOIT_ACK")) + + session.ExploitReceived = true break default: diff --git a/qr2/message.go b/qr2/message.go index ca9cd09..d1c2141 100644 --- a/qr2/message.go +++ b/qr2/message.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/binary" "fmt" + "os" "strconv" + "time" "wwfc/common" "wwfc/logging" @@ -271,3 +273,55 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) { logging.Error(moduleName, "Error sending message:", err.Error()) } } + +func sendClientExploit(moduleName string, sessionCopy Session) { + if len(sessionCopy.Login.GameCode) != 4 || !common.IsUppercaseAlphanumeric(sessionCopy.Login.GameCode) { + logging.Error(moduleName, "Invalid game code:", aurora.Cyan(sessionCopy.Login.GameCode)) + return + } + + exploit, err := os.ReadFile("payload/sbcm/" + "payload." + sessionCopy.Login.GameCode + ".bin") + if err != nil { + logging.Error(moduleName, "Error reading exploit file", aurora.Cyan(sessionCopy.Login.GameCode), "-", err.Error()) + return + } + + mutex.Lock() + session, sessionExists := sessions[makeLookupAddr(sessionCopy.Addr.String())] + if !sessionExists { + logging.Error(moduleName, "Session not found") + return + } + + packetCount := session.PacketCount + 1 + session.PacketCount = packetCount + mutex.Unlock() + + // Now send the exploit + payload := createResponseHeader(ClientMessageRequest, sessionCopy.SessionID) + payload = append(payload, []byte{0, 0, 0, 0}...) + binary.BigEndian.PutUint32(payload[len(payload)-4:], packetCount) + payload = append(payload, exploit[0xB:]...) + + go func() { + for { + _, err = masterConn.WriteTo(payload, sessionCopy.Addr) + if err != nil { + logging.Error(moduleName, "Error sending message:", err.Error()) + } + + // Resend the message if no ack after 2 seconds + time.Sleep(2 * time.Second) + + mutex.Lock() + session, sessionExists := sessions[makeLookupAddr(sessionCopy.Addr.String())] + if !sessionExists || session.ExploitReceived || session.Login == nil || !session.Login.NeedsExploit { + mutex.Unlock() + return + } + + mutex.Unlock() + logging.Notice(moduleName, "Resending SBCM exploit to DNS patcher client") + } + }() +} diff --git a/qr2/session.go b/qr2/session.go index 152ed3a..26495cc 100644 --- a/qr2/session.go +++ b/qr2/session.go @@ -20,18 +20,19 @@ const ( ) type Session struct { - SessionID uint32 - SearchID uint64 - Addr net.Addr - Challenge string - Authenticated bool - Login *LoginInfo - LastKeepAlive int64 - Endianness byte // Some fields depend on the client's endianness - Data map[string]string - PacketCount uint32 - ReservationID uint64 - GroupPointer *Group + SessionID uint32 + SearchID uint64 + Addr net.Addr + Challenge string + Authenticated bool + Login *LoginInfo + ExploitReceived bool + LastKeepAlive int64 + Endianness byte // Some fields depend on the client's endianness + Data map[string]string + PacketCount uint32 + ReservationID uint64 + GroupPointer *Group } var (