Accept players connecting via DNS exploit

This commit is contained in:
mkwcat 2023-12-19 11:25:34 -05:00
parent e130fdc60f
commit 357a3d5b14
No known key found for this signature in database
GPG Key ID: 7A505679CE9E7AA9
10 changed files with 227 additions and 108 deletions

View File

@ -29,7 +29,7 @@ func generateRandom(n int) []byte {
var (
authTokenKey = generateRandom(16)
authTokenIV = generateRandom(16)
authTokenMagic = generateRandom(16)
authTokenMagic = generateRandom(15)
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) (string, string) {
func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, isLocalhost bool) (string, string) {
blob := binary.LittleEndian.AppendUint64([]byte{}, uint64(time.Now().Unix()))
blob = appendString(blob, gamecd, 4)
@ -64,6 +64,13 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
challenge := RandomString(8)
blob = append(blob, []byte(challenge)...)
if isLocalhost {
blob = append(blob, 0x01)
} else {
blob = append(blob, 0x00)
}
blob = append(blob, authTokenMagic...)
block, err := aes.NewCipher(authTokenKey)
@ -75,7 +82,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) {
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) {
if !strings.HasPrefix(token, "NDS") {
err = errors.New("invalid auth token prefix")
return
@ -98,7 +105,7 @@ func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime ti
cipher.NewCBCDecrypter(block, authTokenIV).CryptBlocks(blob, blob)
if !bytes.Equal(blob[0x80:0x90], authTokenMagic) {
if !bytes.Equal(blob[0x90-len(authTokenMagic):0x90], authTokenMagic) {
err = errors.New("invalid auth token magic")
return
}
@ -112,6 +119,7 @@ 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
return
}

View File

@ -4,12 +4,13 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"strconv"
"strings"
"wwfc/common"
"wwfc/logging"
"wwfc/qr2"
"github.com/logrusorgru/aurora/v3"
)
func (g *GameSpySession) isFriendAdded(profileId uint32) bool {
@ -198,7 +199,18 @@ func (g *GameSpySession) setStatus(command common.GameSpyCommand) {
mutex.Unlock()
}
const (
resvDenyVer3 = "GPCM3vMAT\x0316"
resvDenyVer11 = "GPCM11vMAT\x0300000010"
resvDenyVer90 = "GPCM90vMAT\x03EAAAAA**"
resvWaitVer3 = "GPCM3vMAT\x04"
resvWaitVer11 = "GPCM11vMAT\x04"
resvWaitVer90 = "GPCM90vMAT\x04"
)
func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
// TODO: There are other command values that mean the same thing
if command.CommandValue != "1" {
logging.Notice(g.ModuleName, "Received unknown bestie message type:", aurora.Cyan(command.CommandValue))
return
@ -228,16 +240,24 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
// Parse message for security and room tracking purposes
var version int
var msgDataIndex int
var resvDenyMsg string
var resvWaitMsg string
if strings.HasPrefix(msg, "GPCM3vMAT") {
version = 3
resvDenyMsg = resvDenyVer3
resvWaitMsg = resvWaitVer3
msgDataIndex = 9
} else if strings.HasPrefix(msg, "GPCM11vMAT") {
// Only used for Brawl
version = 11
resvDenyMsg = resvDenyVer11
resvWaitMsg = resvWaitVer11
msgDataIndex = 10
} else if strings.HasPrefix(msg, "GPCM90vMAT") {
version = 90
resvDenyMsg = resvDenyVer90
resvWaitMsg = resvWaitVer90
msgDataIndex = 10
} else {
logging.Error(g.ModuleName, "Invalid message prefix; message:", msg)
@ -245,6 +265,13 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
return
}
if !g.DeviceAuthenticated {
logging.Notice(g.ModuleName, "Sender is not device authenticated yet")
// g.replyError(ErrMessage)
sendMessageToSession("1", uint32(toProfileId), g, resvWaitMsg)
return
}
if len(msg) < msgDataIndex+1 {
logging.Error(g.ModuleName, "Invalid message length; message:", msg)
g.replyError(ErrMessage)
@ -268,7 +295,6 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
msgData = binary.LittleEndian.AppendUint32(msgData, uint32(intValue))
}
break
case 11:
for _, stringValue := range strings.Split(msg[msgDataIndex:], "/") {
@ -281,7 +307,6 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
msgData = append(msgData, byteValue...)
}
break
case 90:
msgData, err = common.Base64DwcEncoding.DecodeString(msg[msgDataIndex:])
@ -290,7 +315,11 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
g.replyError(ErrMessage)
return
}
break
default:
logging.Error(g.ModuleName, "Invalid message version; message:", msg)
g.replyError(ErrMessage)
return
}
if len(msgData) > 0x200 || (len(msgData)&3) != 0 {
@ -332,7 +361,8 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
var toSession *GameSpySession
if toSession, ok = sessions[uint32(toProfileId)]; !ok || !toSession.LoggedIn {
logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "is not online")
g.replyError(ErrMessageFriendOffline)
// g.replyError(ErrMessageFriendOffline)
sendMessageToSession("1", toSession.User.ProfileId, g, resvDenyMsg)
return
}
@ -342,6 +372,12 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
return
}
if !toSession.DeviceAuthenticated {
logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "is not device authenticated")
sendMessageToSession("1", toSession.User.ProfileId, g, resvDenyMsg)
return
}
if cmd == common.MatchReservation {
msgMatchData.Reservation.PublicIP = 0
msgMatchData.Reservation.PublicPort = 0
@ -419,6 +455,12 @@ func sendMessageToProfileId(msgType string, from uint32, to uint32, msg string)
func (g *GameSpySession) sendFriendStatus(profileId uint32) {
if g.isFriendAdded(profileId) {
if session, ok := sessions[profileId]; ok && session.LoggedIn && session.isFriendAdded(g.User.ProfileId) {
// Prevent players abusing a stack overflow exploit with the locstring in Mario Kart Wii
if session.NeedsExploit && strings.HasPrefix(session.GameCode, "RMC") && len(g.LocString) > 0x14 {
logging.Warn("GPCM", "Blocked message from", aurora.Cyan(g.User.ProfileId), "to", aurora.Cyan(session.User.ProfileId), "due to a stack overflow exploit")
return
}
sendMessageToSession("100", g.User.ProfileId, session, g.Status)
}
}

View File

@ -6,7 +6,6 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"math/rand"
"strconv"
"strings"
@ -15,6 +14,8 @@ import (
"wwfc/database"
"wwfc/logging"
"wwfc/qr2"
"github.com/logrusorgru/aurora/v3"
)
func generateResponse(gpcmChallenge, nasChallenge, authToken, clientChallenge string) string {
@ -110,42 +111,13 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
return
}
if command.OtherValues["payload_ver"] != "1" {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The payload version is invalid.",
Fatal: true,
})
return
}
authToken := command.OtherValues["authtoken"]
if authToken == "" {
g.replyError(ErrLogin)
return
}
signature, exists := command.OtherValues["wwfc_sig"]
if !exists {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "Missing authentication signature.",
Fatal: true,
})
return
}
var deviceId uint32
if deviceId = verifySignature(authToken, signature); deviceId == 0 {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The authentication signature is invalid.",
Fatal: true,
})
return
}
err, _, issueTime, userId, gsbrcd, cfc, _, _, ingamesn, challenge := common.UnmarshalNASAuthToken(authToken)
err, gamecd, issueTime, userId, gsbrcd, cfc, _, _, ingamesn, challenge, isLocalhost := common.UnmarshalNASAuthToken(authToken)
if err != nil {
g.replyError(ErrLogin)
return
@ -157,6 +129,43 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
return
}
payloadVer, payloadVerExists := command.OtherValues["payload_ver"]
signature, signatureExists := command.OtherValues["wwfc_sig"]
deviceId := uint32(0)
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 {
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(authToken, signature); deviceId == 0 {
g.replyError(GPError{
ErrorCode: ErrLogin.ErrorCode,
ErrorString: "The authentication signature is invalid.",
Fatal: true,
})
return
}
}
response := generateResponse(g.Challenge, challenge, authToken, command.OtherValues["challenge"])
if response != command.OtherValues["response"] {
g.replyError(ErrLogin)
@ -194,11 +203,10 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
// Check to see if a session is already open with this profile ID
mutex.Lock()
_, exists = sessions[g.User.ProfileId]
_, exists := sessions[g.User.ProfileId]
if exists {
mutex.Unlock()
// Original GPCM would've force kicked the other logged in client,
// but we just kick this client
// TODO: Kick the other client, not this one
g.replyError(ErrForcedDisconnect)
return
}
@ -207,15 +215,16 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
g.LoginTicket = common.MarshalGPCMLoginTicket(g.User.ProfileId)
g.SessionKey = rand.Int31n(290000000) + 10000000
g.GameCode = gamecd
g.InGameName = ingamesn
g.DeviceAuthenticated = !g.NeedsExploit
g.LoggedIn = true
g.ModuleName = "GPCM:" + strconv.FormatInt(int64(g.User.ProfileId), 10)
g.ModuleName += "/" + common.CalcFriendCodeString(g.User.ProfileId, "RMCJ")
// Notify QR2 of the login
// TODO: Get ingamesn and cfc from NAS
qr2.Login(g.User.ProfileId, ingamesn, cfc, g.Conn.RemoteAddr().String())
qr2.Login(g.User.ProfileId, ingamesn, cfc, g.Conn.RemoteAddr().String(), g.NeedsExploit, g.DeviceAuthenticated)
payload := common.CreateGameSpyMessage(common.GameSpyCommand{
Command: "lc",

View File

@ -5,8 +5,6 @@ import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
"io"
"net"
"sync"
@ -14,24 +12,31 @@ import (
"wwfc/database"
"wwfc/logging"
"wwfc/qr2"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/logrusorgru/aurora/v3"
)
type GameSpySession struct {
Conn net.Conn
User database.User
ModuleName string
LoggedIn bool
Challenge string
LoginTicket string
SessionKey int32
InGameName string
Status string
LocString string
FriendList []uint32
AuthFriendList []uint32
Conn net.Conn
User database.User
ModuleName string
LoggedIn bool
DeviceAuthenticated bool
Challenge string
LoginTicket string
SessionKey int32
GameCode string
InGameName string
Status string
LocString string
FriendList []uint32
AuthFriendList []uint32
QR2IP uint64
ReservationPID uint32
NeedsExploit bool
}
var (

View File

@ -4,7 +4,6 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/logrusorgru/aurora/v3"
"net/http"
"net/url"
"os"
@ -16,6 +15,8 @@ import (
"wwfc/common"
"wwfc/database"
"wwfc/logging"
"github.com/logrusorgru/aurora/v3"
)
var (
@ -76,7 +77,8 @@ func handleAuthRequest(moduleName string, w http.ResponseWriter, r *http.Request
break
case "login":
reply = login(moduleName, fields)
isLocalhost := strings.HasPrefix(r.RemoteAddr, "127.0.0.1:") || strings.HasPrefix(r.RemoteAddr, "[::1]:")
reply = login(moduleName, fields, isLocalhost)
break
case "svcloc":
@ -158,7 +160,7 @@ func acctcreate() map[string]string {
}
}
func login(moduleName string, fields map[string]string) map[string]string {
func login(moduleName string, fields map[string]string, isLocalhost bool) map[string]string {
param := map[string]string{
"retry": "0",
"datetime": getDateTime(),
@ -231,7 +233,7 @@ func login(moduleName string, fields map[string]string) map[string]string {
return param
}
authToken, challenge := common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"])
authToken, challenge := common.MarshalNASAuthToken(gamecd, userId, gsbrcd, cfcInt, regionByte[0], langByte[0], fields["ingamesn"], isLocalhost)
param["returncd"] = "001"
param["challenge"] = challenge

View File

@ -315,12 +315,11 @@ func startHTTPSProxy(address string, nasAddr string) {
// Read bytes from the HTTP server and forward them through the TLS connection
go func() {
buf := make([]byte, 0x1000)
recvBuf := make([]byte, 0x100)
seq := uint64(1)
index := 0
for {
n, err := newConn.Read(buf[index:])
n, err := newConn.Read(recvBuf)
if err != nil {
if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") {
return
@ -330,9 +329,9 @@ func startHTTPSProxy(address string, nasAddr string) {
return
}
// fmt.Printf("Sent:\n% X ", buf[index:index+n])
// fmt.Printf("Sent:\n% X ", recvBuf[:n])
var record []byte
record, seq = encryptTLS(macFn, cipher, buf[index:index+n], seq, []byte{0x17, 0x03, 0x01, byte(n >> 8), byte(n)})
record, seq = encryptTLS(macFn, cipher, recvBuf[:n], seq, []byte{0x17, 0x03, 0x01, byte(n >> 8), byte(n)})
_, err = conn.Write(record)
if err != nil {
@ -349,6 +348,11 @@ func startHTTPSProxy(address string, nasAddr string) {
for {
n, err := conn.Read(buf[index:])
if err != nil {
if errors.Is(err, io.EOF) || strings.Contains(err.Error(), "use of closed network connection") {
logging.Info(moduleName, "Connection closed by client after", aurora.BrightCyan(total), "bytes")
return
}
logging.Error(moduleName, "Failed to read from client:", err)
return
}
@ -387,18 +391,20 @@ func startHTTPSProxy(address string, nasAddr string) {
// fmt.Printf("\nDecrypted content:\n% X \n", buf[5:5+recordLength])
if buf[0] != 0x17 {
if buf[0] == 0x15 && buf[5] == 0x01 && buf[6] == 0x00 {
if buf[0] == 0x15 || buf[5] == 0x01 || buf[6] == 0x00 {
logging.Info(moduleName, "Alert connection close by client after", aurora.BrightCyan(total), "bytes")
return
}
logging.Error(moduleName, "Non-application data received")
return
}
// Send the decrypted content to the HTTP server
_, err = newConn.Write(buf[5 : 5+recordLength-16])
if err != nil {
logging.Error(moduleName, "Failed to write to HTTP server:", err)
logging.Error(moduleName, "Non-application data received:", aurora.Cyan(fmt.Sprintf("% X ", buf[:5+recordLength])))
return
} else {
// Send the decrypted content to the HTTP server
_, err = newConn.Write(buf[5 : 5+recordLength-16])
if err != nil {
logging.Error(moduleName, "Failed to write to HTTP server:", err)
return
}
}
buf = buf[5+recordLength:]

View File

@ -1,27 +1,57 @@
package qr2
type LoginInfo struct {
ProfileID uint32
InGameName string
ConsoleFriendCode uint64
GPPublicIP string
ProfileID uint32
InGameName string
ConsoleFriendCode uint64
GPPublicIP string
NeedsExploit bool
DeviceAuthenticated bool
Session *Session
}
var logins = map[uint32]LoginInfo{}
var logins = map[uint32]*LoginInfo{}
func Login(profileID uint32, inGameName string, consoleFriendCode uint64, publicIP string) {
func Login(profileID uint32, inGameName string, consoleFriendCode uint64, publicIP string, needsExploit bool, deviceAuthenticated bool) {
mutex.Lock()
logins[profileID] = LoginInfo{
ProfileID: profileID,
InGameName: inGameName,
ConsoleFriendCode: consoleFriendCode,
GPPublicIP: publicIP,
logins[profileID] = &LoginInfo{
ProfileID: profileID,
InGameName: inGameName,
ConsoleFriendCode: consoleFriendCode,
GPPublicIP: publicIP,
NeedsExploit: needsExploit,
DeviceAuthenticated: deviceAuthenticated,
Session: nil,
}
mutex.Unlock()
}
func SetDeviceAuthenticated(profileID uint32) {
mutex.Lock()
if login, exists := logins[profileID]; exists {
login.DeviceAuthenticated = true
if login.Session != nil {
login.Session.Data["+deviceauth"] = "1"
}
}
mutex.Unlock()
}
func Logout(profileID uint32) {
mutex.Lock()
// Delete login's session
if login, exists := logins[profileID]; exists {
if login.Session != nil {
removeSession(makeLookupAddr(login.Session.Addr.String()))
}
}
delete(logins, profileID)
mutex.Unlock()
}

View File

@ -4,10 +4,11 @@ import (
"bytes"
"encoding/binary"
"fmt"
"github.com/logrusorgru/aurora/v3"
"strconv"
"wwfc/common"
"wwfc/logging"
"github.com/logrusorgru/aurora/v3"
)
func printHex(data []byte) string {
@ -48,6 +49,10 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
return
}
if receiver.Login == nil || !receiver.Login.DeviceAuthenticated {
logging.Error(moduleName, "Destination", aurora.Cyan(destSearchID), "is not device authenticated")
}
// Decode and validate the message
isNatnegPacket := false
if bytes.Equal(message[:2], []byte{0xfd, 0xfc}) {
@ -100,6 +105,11 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
return
}
if sender.Login == nil || !sender.Login.DeviceAuthenticated {
logging.Error(moduleName, "Sender is not device authenticated")
return
}
var ok bool
matchData, ok = common.DecodeMatchCommand(message[8], message[0x14:], version)
if !ok {

View File

@ -1,7 +1,6 @@
package qr2
import (
"github.com/logrusorgru/aurora/v3"
"math/rand"
"net"
"strconv"
@ -10,6 +9,8 @@ import (
"time"
"wwfc/common"
"wwfc/logging"
"github.com/logrusorgru/aurora/v3"
)
const (
@ -24,7 +25,7 @@ type Session struct {
Addr net.Addr
Challenge string
Authenticated bool
Login LoginInfo
Login *LoginInfo
LastKeepAlive int64
Endianness byte // Some fields depend on the client's endianness
Data map[string]string
@ -60,6 +61,13 @@ func removeSession(addr uint64) {
session.GroupPointer.Server = nil
// TODO: Search for new host via dwc_hoststate
}
session.GroupPointer = nil
}
if session.Login != nil {
session.Login.Session = nil
session.Login = nil
}
// Delete search ID lookup
@ -156,7 +164,7 @@ func (session *Session) setProfileID(moduleName string, newPID string) bool {
// Check if the public IP matches the one used for the GPCM session
var gpPublicIP string
var loginInfo LoginInfo
var loginInfo *LoginInfo
var ok bool
if loginInfo, ok = logins[uint32(profileID)]; ok {
gpPublicIP = loginInfo.GPPublicIP
@ -172,24 +180,18 @@ func (session *Session) setProfileID(moduleName string, newPID string) bool {
session.Login = loginInfo
// Constraint: only one session can exist with a profile ID
var outdated []uint64
for sessionAddr, otherSession := range sessions {
if otherSession == session {
continue
}
if otherPID, ok := otherSession.Data["dwc_pid"]; !ok || otherPID != newPID {
continue
}
// Remove old sessions with the PID
outdated = append(outdated, sessionAddr)
// Constraint: only one session can exist with a given profile ID
if loginInfo.Session != nil {
logging.Notice(moduleName, "Removing outdated session", aurora.BrightCyan(loginInfo.Session.Addr.String()), "with PID", aurora.Cyan(newPID))
removeSession(makeLookupAddr(loginInfo.Session.Addr.String()))
}
for _, sessionAddr := range outdated {
logging.Notice(moduleName, "Removing outdated session", aurora.BrightCyan(sessions[sessionAddr].Addr.String()), "with PID", aurora.Cyan(newPID))
removeSession(sessionAddr)
loginInfo.Session = session
if loginInfo.DeviceAuthenticated {
session.Data["+deviceauth"] = "1"
} else {
session.Data["+deviceauth"] = "0"
}
session.Data["dwc_pid"] = newPID

View File

@ -1,9 +1,10 @@
package serverbrowser
import (
"github.com/logrusorgru/aurora/v3"
"wwfc/logging"
"wwfc/serverbrowser/filter"
"github.com/logrusorgru/aurora/v3"
)
// DWC makes requests in the following formats:
@ -29,6 +30,10 @@ func filterServers(servers []map[string]string, queryGame string, expression str
continue
}
if server["+deviceauth"] != "1" {
continue
}
if server["dwc_mver"] == "90" && (server["dwc_hoststate"] != "0" && server["dwc_hoststate"] != "2") {
continue
}