Sake: Sanitize Mii info from Mario kart Wii ghost data

This commit is contained in:
Palapeli 2026-04-04 01:21:26 -04:00
parent 865d8a8df2
commit 4dbe2a7678
No known key found for this signature in database
GPG Key ID: 1FFE8F556A474925
5 changed files with 115 additions and 29 deletions

View File

@ -341,8 +341,12 @@ func (rkgd RKGhostData) GetLocationCode() uint16 {
return uint16(rkgd.GetBits(0x36, 0, 16))
}
func (rkgd RKGhostData) GetMiiData() Mii {
return Mii(rkgd[0x3C : 0x3C+0x4C])
func (rkgd RKGhostData) GetMiiData() RawMii {
return RawMiiFromBytes(rkgd[0x3C : 0x3C+0x4C])
}
func (rkgd RKGhostData) SetMiiData(miiData RawMii) {
copy(rkgd[0x3C:0x3C+0x4C], miiData.Data[:])
}
func (rkgd RKGhostData) GetCompressedSize() uint32 {
@ -353,6 +357,11 @@ func (rkgd RKGhostData) GetCompressedData() []byte {
return []byte(rkgd[0x8C : len(rkgd)-4])
}
func (rkgd RKGhostData) RecalculateCRC() {
crc := crc32.ChecksumIEEE(rkgd[:len(rkgd)-4])
binary.BigEndian.PutUint32(rkgd[len(rkgd)-4:], crc)
}
func (rkgd RKGhostData) IsRKGDFileValid(moduleName string, expectedCourse MarioKartWiiCourseId, expectedScore int) bool {
rkgdFileMagic := []byte{'R', 'K', 'G', 'D'}
rkgdFileLength := len(rkgd)
@ -443,7 +452,7 @@ func (rkgd RKGhostData) IsRKGDFileValid(moduleName string, expectedCourse MarioK
return false
}
if rkgd.GetMiiData().RFLCalculateCRC() != 0x0000 {
if rkgd.GetMiiData().CalculateMiiCRC() != 0x0000 {
logging.Error(moduleName, "Invalid RKGD Mii data CRC")
return false
}

View File

@ -1,15 +1,25 @@
package common
import "encoding/binary"
// References:
// https://wiibrew.org/wiki/Mii_Data
// https://github.com/kiwi515/ogws/tree/master/src/RVLFaceLib
type Mii [0x4C]byte
type RawMii struct {
Data [0x4C]byte
}
func (data Mii) RFLCalculateCRC() uint16 {
func RawMiiFromBytes(data []byte) RawMii {
var miiData [0x4C]byte
copy(miiData[:], data[:0x4C])
return RawMii{Data: miiData}
}
func (data RawMii) CalculateMiiCRC() uint16 {
crc := uint16(0)
for _, val := range data {
for _, val := range data.Data {
for j := 0; j < 8; j++ {
if crc&0x8000 != 0 {
crc <<= 1
@ -38,7 +48,7 @@ var officialMiiList = []uint64{
0x80000005ECFF82D2,
}
func RFLSearchOfficialData(id uint64) (bool, int) {
func SearchOfficialMiiData(id uint64) (bool, int) {
for i, v := range officialMiiList {
if v == id {
return true, i
@ -47,3 +57,39 @@ func RFLSearchOfficialData(id uint64) (bool, int) {
return false, -1
}
// ClearMiiInfo clears any Mii information that generally isn't or shouldn't be shared publicly,
// mainly the "console ID", which can be used to determine the Mii creator's MAC address
func (mii RawMii) ClearMiiInfo() RawMii {
// Clear the create ID, which the MAC address can be derived from
binary.BigEndian.PutUint32(mii.Data[0x18:0x1C], 0x80000000)
binary.BigEndian.PutUint32(mii.Data[0x1C:0x20], 0)
// Clear all characters in the Mii name succeeding the null terminator
hitNullTerminator := false
for i := 0; i < 20; i += 2 {
if hitNullTerminator {
mii.Data[0x2+i] = 0
mii.Data[0x2+i+1] = 0
} else if mii.Data[0x2+i] == 0 && mii.Data[0x2+i+1] == 0 {
hitNullTerminator = true
}
}
// Clear the creator name
for i := 0; i < 20; i++ {
mii.Data[0x36+i] = 0
}
// Clear the birthday
mii.Data[0] &= ^byte(0x3F)
mii.Data[1] &= ^byte(0xE0)
// Clear checksum and recalculate
mii.Data[0x4A] = 0
mii.Data[0x4B] = 0
crc := mii.CalculateMiiCRC()
mii.Data[0x4A] = byte(crc >> 8)
mii.Data[0x4B] = byte(crc & 0xFF)
return mii
}

View File

@ -393,15 +393,15 @@ func ProcessUSER(senderPid uint32, senderIP uint64, packet []byte) {
}
index := 0x08 + i*0x4C
mii := common.Mii(packet[index : index+0x4C])
if mii.RFLCalculateCRC() != 0x0000 {
mii := common.RawMiiFromBytes(packet[index : index+0x4C])
if mii.CalculateMiiCRC() != 0x0000 {
logging.Error(moduleName, "Received USER packet with invalid Mii data CRC")
gpErrorCallback(senderPid, "bad_packet")
return
}
createId := binary.BigEndian.Uint64(packet[index+0x18 : index+0x20])
official, _ := common.RFLSearchOfficialData(createId)
official, _ := common.SearchOfficialMiiData(createId)
if official {
miiName = append(miiName, "Player")
} else {

View File

@ -1,6 +1,7 @@
package race
import (
"encoding/base64"
"encoding/xml"
"io"
"net/http"
@ -153,11 +154,19 @@ func handleGetTopTenRankingsRequest(moduleName string, responseWriter http.Respo
numberOfRankings := len(topTenRankings)
data := make([]rankingsResponseData, 0, numberOfRankings)
for i, topTenRanking := range topTenRankings {
// Filter player info just in case
playerInfo, err := base64.StdEncoding.DecodeString(topTenRanking.PlayerInfo)
if err != nil {
panic(err)
}
miiData := common.RawMiiFromBytes(playerInfo).ClearMiiInfo().Data
playerInfo = append(miiData[:], playerInfo[len(miiData):]...)
rankingData := rankingsResponseRankingData{
OwnerID: topTenRanking.PID,
Rank: i + 1,
Time: topTenRanking.Score,
UserData: topTenRanking.PlayerInfo,
UserData: base64.StdEncoding.EncodeToString(playerInfo),
}
responseData := rankingsResponseData{

View File

@ -18,11 +18,11 @@ import (
)
type playerInfo struct {
MiiData common.Mii // 0x00
ControllerId byte // 0x4C
Unknown byte // 0x4D
StateCode byte // 0x4E
CountryCode byte // 0x4F
MiiData common.RawMii // 0x00
ControllerId byte // 0x4C
Unknown byte // 0x4D
StateCode byte // 0x4E
CountryCode byte // 0x4F
}
const (
@ -266,7 +266,11 @@ func handleMarioKartWiiGhostDownloadRequest(moduleName string, responseWriter ht
return
}
responseBody := append(downloadedGhostFileHeader(), ghost...)
ghostData := common.RKGhostData(ghost)
ghostData.SetMiiData(ghostData.GetMiiData().ClearMiiInfo())
ghostData.RecalculateCRC()
responseBody := append(downloadedGhostFileHeader(), []byte(ghostData)...)
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultSuccess))
responseWriter.Header().Set("Content-Length", strconv.Itoa(len(responseBody)))
@ -330,13 +334,13 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
return
}
if !isPlayerInfoValid(playerInfo) {
fixedPlayerInfo, ok := isPlayerInfoValid(playerInfo)
if !ok {
logging.Error(moduleName, "Invalid player info:", aurora.Cyan(playerInfo))
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultMissingParameter))
return
}
// Mario Kart Wii expects player information to be in this form
playerInfo, _ = common.Base64Convert(playerInfo, base64.URLEncoding, base64.StdEncoding)
playerInfo = fixedPlayerInfo
// The multipart boundary utilized by GameSpy does not conform to RFC 2045. To ensure compliance,
// we need to surround it with double quotation marks.
@ -377,17 +381,21 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http
return
}
if !common.RKGhostData(ghostFile).IsRKGDFileValid(moduleName, courseId, score) {
ghostData := common.RKGhostData(ghostFile)
if !ghostData.IsRKGDFileValid(moduleName, courseId, score) {
logging.Error(moduleName, "Received an invalid ghost file")
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultFileTooLarge))
return
}
ghostData.SetMiiData(ghostData.GetMiiData().ClearMiiInfo())
ghostData.RecalculateCRC()
if isContest {
ghostFile = nil
}
err = database.InsertMarioKartWiiGhostFile(pool, ctx, regionId, courseId, score, pid, playerInfo, ghostFile)
err = database.InsertMarioKartWiiGhostFile(pool, ctx, regionId, courseId, score, pid, playerInfo, []byte(ghostData))
if err != nil {
logging.Error(moduleName, "Failed to insert the ghost file into the database:", err)
responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultServerError))
@ -405,30 +413,44 @@ func downloadedGhostFileHeader() []byte {
return downloadedGhostFileHeader[:]
}
func isPlayerInfoValid(playerInfoString string) bool {
// isPlayerInfoValid checks if the player info (base64.URLEncoding) string is valid, and if so, returns a "fixed" version of it with base64.StdEncoding
func isPlayerInfoValid(playerInfoString string) (string, bool) {
playerInfoByteArray, err := base64.URLEncoding.DecodeString(playerInfoString)
if err != nil {
return false
return "", false
}
if len(playerInfoByteArray) != playerInfoSize {
return false
return "", false
}
var playerInfo playerInfo
reader := bytes.NewReader(playerInfoByteArray)
err = binary.Read(reader, binary.BigEndian, &playerInfo)
if err != nil {
return false
return "", false
}
if playerInfo.MiiData.RFLCalculateCRC() != 0x0000 {
return false
if playerInfo.MiiData.CalculateMiiCRC() != 0x0000 {
return "", false
}
controllerId := common.MarioKartWiiControllerId(playerInfo.ControllerId)
return controllerId.IsValid()
if !controllerId.IsValid() {
return "", false
}
playerInfo.MiiData.ClearMiiInfo()
fixedPlayerInfoByteArray := new(bytes.Buffer)
err = binary.Write(fixedPlayerInfoByteArray, binary.BigEndian, playerInfo)
if err != nil {
return "", false
}
fixedPlayerInfoString := base64.StdEncoding.EncodeToString(fixedPlayerInfoByteArray.Bytes())
return fixedPlayerInfoString, true
}
func getMultipartBoundary(contentType string) string {