Send SBCM exploit to client + support dev auth after login

This commit is contained in:
mkwcat 2023-12-24 14:44:47 -05:00
parent 978a7d3956
commit 5710b20b28
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
11 changed files with 218 additions and 33 deletions

View File

@ -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)))

View File

@ -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))

View File

@ -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 {

View File

@ -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()

View File

@ -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 {

View File

@ -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

View File

@ -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
}
}

View File

@ -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,

View File

@ -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:

View File

@ -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")
}
}()
}

View File

@ -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 (