mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Prevent savedata corruption and denial-of-service by adding four layers of protection to the save pipeline: - Bounded decompression (nullcomp.DecompressWithLimit): caps output size to prevent OOM from crafted payloads that expand to exhaust memory - Bounds-checked delta patching (deltacomp.ApplyDataDiffWithLimit): validates offsets before writing, returns errors for negative offsets, truncated patches, and oversized output; ApplyDataDiff now returns original data on error instead of partial corruption - Size limits on save handlers: rejects compressed payloads >512KB and decompressed data >1MB before processing; applied to main savedata, platedata, and platebox diff paths - Rotating savedata backups: 3 slots per character with 30-minute interval, snapshots the previous state before overwriting, backed by new savedata_backups table (migration 0007)
229 lines
6.9 KiB
Go
229 lines
6.9 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"encoding/binary"
|
|
|
|
"erupe-ce/common/bfutil"
|
|
"erupe-ce/common/stringsupport"
|
|
cfg "erupe-ce/config"
|
|
"erupe-ce/server/channelserver/compression/nullcomp"
|
|
)
|
|
|
|
// SavePointer identifies a section within the character save data blob.
|
|
type SavePointer int
|
|
|
|
const (
|
|
pGender = iota
|
|
pRP
|
|
pHouseTier
|
|
pHouseData
|
|
pBookshelfData
|
|
pGalleryData
|
|
pToreData
|
|
pGardenData
|
|
pPlaytime
|
|
pWeaponType
|
|
pWeaponID
|
|
pHR
|
|
pGRP
|
|
pKQF
|
|
lBookshelfData
|
|
)
|
|
|
|
// CharacterSaveData holds a character's save data and its parsed fields.
|
|
type CharacterSaveData struct {
|
|
CharID uint32
|
|
Name string
|
|
IsNewCharacter bool
|
|
Mode cfg.Mode
|
|
Pointers map[SavePointer]int
|
|
|
|
Gender bool
|
|
RP uint16
|
|
HouseTier []byte
|
|
HouseData []byte
|
|
BookshelfData []byte
|
|
GalleryData []byte
|
|
ToreData []byte
|
|
GardenData []byte
|
|
Playtime uint32
|
|
WeaponType uint8
|
|
WeaponID uint16
|
|
HR uint16
|
|
GR uint16
|
|
KQF []byte
|
|
|
|
compSave []byte
|
|
decompSave []byte
|
|
}
|
|
|
|
func getPointers(mode cfg.Mode) map[SavePointer]int {
|
|
pointers := map[SavePointer]int{pGender: 81, lBookshelfData: 5576}
|
|
switch mode {
|
|
case cfg.ZZ:
|
|
pointers[pPlaytime] = 128356
|
|
pointers[pWeaponID] = 128522
|
|
pointers[pWeaponType] = 128789
|
|
pointers[pHouseTier] = 129900
|
|
pointers[pToreData] = 130228
|
|
pointers[pHR] = 130550
|
|
pointers[pGRP] = 130556
|
|
pointers[pHouseData] = 130561
|
|
pointers[pBookshelfData] = 139928
|
|
pointers[pGalleryData] = 140064
|
|
pointers[pGardenData] = 142424
|
|
pointers[pRP] = 142614
|
|
pointers[pKQF] = 146720
|
|
case cfg.Z2, cfg.Z1, cfg.G101, cfg.G10, cfg.G91, cfg.G9, cfg.G81, cfg.G8,
|
|
cfg.G7, cfg.G61, cfg.G6, cfg.G52, cfg.G51, cfg.G5, cfg.GG, cfg.G32, cfg.G31,
|
|
cfg.G3, cfg.G2, cfg.G1:
|
|
pointers[pPlaytime] = 92356
|
|
pointers[pWeaponID] = 92522
|
|
pointers[pWeaponType] = 92789
|
|
pointers[pHouseTier] = 93900
|
|
pointers[pToreData] = 94228
|
|
pointers[pHR] = 94550
|
|
pointers[pGRP] = 94556
|
|
pointers[pHouseData] = 94561
|
|
pointers[pBookshelfData] = 103928
|
|
pointers[pGalleryData] = 104064
|
|
pointers[pGardenData] = 106424
|
|
pointers[pRP] = 106614
|
|
pointers[pKQF] = 110720
|
|
case cfg.F5, cfg.F4:
|
|
pointers[pPlaytime] = 60356
|
|
pointers[pWeaponID] = 60522
|
|
pointers[pWeaponType] = 60789
|
|
pointers[pHouseTier] = 61900
|
|
pointers[pToreData] = 62228
|
|
pointers[pHR] = 62550
|
|
pointers[pHouseData] = 62561
|
|
pointers[pBookshelfData] = 71928
|
|
pointers[pGalleryData] = 72064
|
|
pointers[pGardenData] = 74424
|
|
pointers[pRP] = 74614
|
|
case cfg.S6:
|
|
pointers[pPlaytime] = 12356
|
|
pointers[pWeaponID] = 12522
|
|
pointers[pWeaponType] = 12789
|
|
pointers[pHouseTier] = 13900
|
|
pointers[pToreData] = 14228
|
|
pointers[pHR] = 14550
|
|
pointers[pHouseData] = 14561
|
|
pointers[pBookshelfData] = 23928
|
|
pointers[pGalleryData] = 24064
|
|
pointers[pGardenData] = 26424
|
|
pointers[pRP] = 26614
|
|
}
|
|
if mode == cfg.G5 {
|
|
pointers[lBookshelfData] = 5548
|
|
} else if mode <= cfg.GG {
|
|
pointers[lBookshelfData] = 4520
|
|
}
|
|
return pointers
|
|
}
|
|
|
|
func (save *CharacterSaveData) Compress() error {
|
|
var err error
|
|
save.compSave, err = nullcomp.Compress(save.decompSave)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (save *CharacterSaveData) Decompress() error {
|
|
var err error
|
|
save.decompSave, err = nullcomp.DecompressWithLimit(save.compSave, saveDataMaxDecompressedPayload)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// This will update the character save with the values stored in the save struct
|
|
func (save *CharacterSaveData) updateSaveDataWithStruct() {
|
|
rpBytes := make([]byte, 2)
|
|
binary.LittleEndian.PutUint16(rpBytes, save.RP)
|
|
if save.Mode >= cfg.F4 {
|
|
copy(save.decompSave[save.Pointers[pRP]:save.Pointers[pRP]+saveFieldRP], rpBytes)
|
|
}
|
|
if save.Mode >= cfg.G10 {
|
|
copy(save.decompSave[save.Pointers[pKQF]:save.Pointers[pKQF]+saveFieldKQF], save.KQF)
|
|
}
|
|
}
|
|
|
|
// This will update the save struct with the values stored in the character save
|
|
// Save data field sizes
|
|
const (
|
|
saveFieldRP = 2
|
|
saveFieldHouseTier = 5
|
|
saveFieldHouseData = 195
|
|
saveFieldGallery = 1748
|
|
saveFieldTore = 240
|
|
saveFieldGarden = 68
|
|
saveFieldPlaytime = 4
|
|
saveFieldWeaponID = 2
|
|
saveFieldHR = 2
|
|
saveFieldGRP = 4
|
|
saveFieldKQF = 8
|
|
saveFieldNameOffset = 88
|
|
saveFieldNameLen = 12
|
|
)
|
|
|
|
func (save *CharacterSaveData) updateStructWithSaveData() {
|
|
save.Name = stringsupport.SJISToUTF8Lossy(bfutil.UpToNull(save.decompSave[saveFieldNameOffset : saveFieldNameOffset+saveFieldNameLen]))
|
|
if save.decompSave[save.Pointers[pGender]] == 1 {
|
|
save.Gender = true
|
|
} else {
|
|
save.Gender = false
|
|
}
|
|
if !save.IsNewCharacter {
|
|
if save.Mode >= cfg.S6 {
|
|
save.RP = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pRP] : save.Pointers[pRP]+saveFieldRP])
|
|
save.HouseTier = save.decompSave[save.Pointers[pHouseTier] : save.Pointers[pHouseTier]+saveFieldHouseTier]
|
|
save.HouseData = save.decompSave[save.Pointers[pHouseData] : save.Pointers[pHouseData]+saveFieldHouseData]
|
|
save.BookshelfData = save.decompSave[save.Pointers[pBookshelfData] : save.Pointers[pBookshelfData]+save.Pointers[lBookshelfData]]
|
|
save.GalleryData = save.decompSave[save.Pointers[pGalleryData] : save.Pointers[pGalleryData]+saveFieldGallery]
|
|
save.ToreData = save.decompSave[save.Pointers[pToreData] : save.Pointers[pToreData]+saveFieldTore]
|
|
save.GardenData = save.decompSave[save.Pointers[pGardenData] : save.Pointers[pGardenData]+saveFieldGarden]
|
|
save.Playtime = binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pPlaytime] : save.Pointers[pPlaytime]+saveFieldPlaytime])
|
|
save.WeaponType = save.decompSave[save.Pointers[pWeaponType]]
|
|
save.WeaponID = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pWeaponID] : save.Pointers[pWeaponID]+saveFieldWeaponID])
|
|
save.HR = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pHR] : save.Pointers[pHR]+saveFieldHR])
|
|
if save.Mode >= cfg.G1 {
|
|
if save.HR == uint16(999) {
|
|
save.GR = grpToGR(int(binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pGRP] : save.Pointers[pGRP]+saveFieldGRP])))
|
|
}
|
|
}
|
|
if save.Mode >= cfg.G10 {
|
|
save.KQF = save.decompSave[save.Pointers[pKQF] : save.Pointers[pKQF]+saveFieldKQF]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// isHouseTierCorrupted checks whether the house tier field contains 0xFF
|
|
// bytes, which indicates an uninitialized or -1 value from the game client.
|
|
// The game uses small positive integers for theme IDs; 0xFF is never valid.
|
|
func (save *CharacterSaveData) isHouseTierCorrupted() bool {
|
|
for _, b := range save.HouseTier {
|
|
if b == 0xFF {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// restoreHouseTier replaces the current house tier with the given value in
|
|
// both the struct field and the underlying decompressed save blob, keeping
|
|
// them consistent for Save().
|
|
func (save *CharacterSaveData) restoreHouseTier(valid []byte) {
|
|
save.HouseTier = make([]byte, len(valid))
|
|
copy(save.HouseTier, valid)
|
|
offset, ok := save.Pointers[pHouseTier]
|
|
if ok && offset+len(valid) <= len(save.decompSave) {
|
|
copy(save.decompSave[offset:offset+len(valid)], valid)
|
|
}
|
|
}
|