Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
SeekyCt committed Jul 23, 2021
1 parent 88c4ed7 commit 4cf2298
Show file tree
Hide file tree
Showing 26 changed files with 3,796 additions and 1 deletion.
14 changes: 14 additions & 0 deletions INJECTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Injecting a Mod into a Data.bin Save
* Install [Python 3](https://www.python.org/) if you don't already have it
* Download the latest `SaveBinPatcher.zip` from [here](https://github.com/SeekyCt/spm-save-exploit/releases)
* Extract the zip somewhere on your computer
* Move your data.bin file into your extracted SaveBinPatcher
* Move the rel for the mod into your extracted SaveBinPatcher
* Run inject.py in SaveBinPatcher
* Enter `data.bin` when asked for the path to the input data.bin
* Enter your game version when prompted
* Enter the id of the save file you want to replace with the rel loader when prompted
* Enter the filename of the rel you downloaded when asked for the path to the input rel
* Enter `output.bin` when asked for the path to the output data.bin
* Wait a little while for the save to be repacked (usually takes about 20-30 seconds)
* Your save is now patched, return to the tutorial that linked you here for instructions on what to do next
22 changes: 21 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,21 @@
# spm-save-exploit
# Super Paper Mario Save File ACE Exploit

This is an exploit for all versions of Super Paper Mario to allow arbitrary code to be ran from a save file. See [this video](https://youtu.be/aqeQ21WHMVE) for a demonstration.

## How does it work?

The game calls the function `pausewinSetMessage` with the item id of every item in the inventory to display their descriptions in the pause menu. The id is not checked to be valid here, and is used as an index into a table to read the message's name string from. The game then copies that name message onto the stack without checking the length so that it can append "_ex" to it:

![](https://cdn.discordapp.com/attachments/610974864706371585/868163069606527016/unknown.png)

By editing a save file's inventory, the id of an item can be set high enough that the pointer to its description message name string can be read from the save file, and that pointer can be set to point to somewhere in the save file too to use a custom string. That custom string can then used to overflow the buffer for the string on the stack and overwrite the link register save after it, meaning that the game can be made to branch to any address desired when this function finishes. By setting this address to somewhere within the save file, arbitrary code can then be ran.

## How does this load mods?

Thanks to a lot of help from Zephiles, as well as some code by PistonMiner that was re-used from the TTYD save exploit rel loader, the payload for this exploit can be made to reboot the game and hook in a few places to load and execute a rel from NAND. See `source` for more details.

## Credits
* Seeky - finding the exploit
* Zephiles - writing the majority of the rel loader payload
* PistonMiner - code re-used from the TTYD save file rel loader
* Segher and Dolphin Emulator developers - Wii save unpacking & packing code referenced
197 changes: 197 additions & 0 deletions SaveFileHack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
#!/usr/bin/python3
import binascii
import sys

# stringToInt taken from text_to_bits from here:
# https://stackoverflow.com/questions/7396849/convert-binary-to-ascii-and-vice-versa
def stringToInt(string, encoding="utf-8", errors="surrogatepass"):
bits = bin(int(binascii.hexlify(string.encode(encoding, errors)), 16))[2:]
return int(bits.zfill(8 * ((len(bits) + 7) // 8)), 2)

def verifyVersionString(string):
if string == "eu0":
return True
elif string == "eu1":
return True
elif string == "jp0":
return True
elif string == "jp1":
return True
elif string == "kr0":
return True
elif string == "us0":
return True
elif string == "us1":
return True
elif string == "us2":
return True
else:
return False

# Make sure something was passed in
if len(sys.argv) < 2:
input("You must pass in a proper SPM save file. Press Enter to close this window.")
sys.exit("")

# Check if the version number was passed in
VersionString = ""
if len(sys.argv) < 3:

# Prompt for the version number to use
while (VersionString == ""):
VersionString = input("Enter the version of the game to hack\n(eu0, eu1, jp0, jp1, kr0, us0, us1, us2): ")

# Make sure the input is valid
if (not verifyVersionString(VersionString)):
VersionString = ""
else:
VersionString = sys.argv[2]

# Make sure the input is valid
if (not verifyVersionString(VersionString)):
VersionString = ""

# Prompt for the version number to use
while (VersionString == ""):
VersionString = input("Enter the version of the game to hack\n(eu0, eu1, jp0, jp1, kr0, us0, us1, us2): ")

# Make sure the input is valid
if (not verifyVersionString(VersionString)):
VersionString = ""

# Set version-specific values
if (VersionString == "eu0") or (VersionString == "eu1"):
InitAsmFunctionPointer = 0x80526294
TextBufferPointerOffset = 0x1B0
TextBufferPointer = 0x805256FC
BinVersion = "EU"
ItemId = 0x6E59
elif VersionString == "jp0":
InitAsmFunctionPointer = 0x804B8594
TextBufferPointerOffset = 0x17C
TextBufferPointer = 0x804B79C8
BinVersion = "JP_0"
ItemId = 0x6D0A
elif VersionString == "jp1":
InitAsmFunctionPointer = 0x804B9B94
TextBufferPointerOffset = 0x174
TextBufferPointer = 0x804B8FC0
BinVersion = "JP_1"
ItemId = 0x6D24
elif VersionString == "kr0":
InitAsmFunctionPointer = 0x8055DBF4
TextBufferPointerOffset = 0x170
TextBufferPointer = 0x8055D01C
BinVersion = "KR"
ItemId = 0x70D9
elif VersionString == "us0":
InitAsmFunctionPointer = 0x804E3294
TextBufferPointerOffset = 0x1A4
TextBufferPointer = 0x804E26F0
BinVersion = "US_0"
ItemId = 0x6D08
elif VersionString == "us1":
InitAsmFunctionPointer = 0x804E4B14
TextBufferPointerOffset = 0x1AC
TextBufferPointer = 0x804E3F78
BinVersion = "US_1"
ItemId = 0x6D26
elif VersionString == "us2":
InitAsmFunctionPointer = 0x804E4C94
TextBufferPointerOffset = 0x174
TextBufferPointer = 0x804E40C0
BinVersion = "US_2"
ItemId = 0x6D24

f = open(sys.argv[1], "r+b")

# Clear all of the bytes in the file
f.seek(0, 0)
f.write((0).to_bytes(0x25B0, byteorder="big", signed=False))

# Set the default efb width and height, as they can apparently effect being able to open the item menu
f.seek(0x20, 0)
f.write((0x26001E0).to_bytes(4, byteorder="big", signed=False))

# Write the new file name
FileNameString = "REL Loader\0"
f.seek(0x28, 0)
f.write(stringToInt(FileNameString).to_bytes(len(FileNameString), byteorder="big", signed=False))

# Write the map name
MapNameString = "dos_01\0"
f.seek(0x4C, 0)
f.write(stringToInt(MapNameString).to_bytes(len(MapNameString), byteorder="big", signed=False))

# Set Mario's level to 1, to prevent leveling up immediately
f.seek(0x1B14, 0)
f.write((1).to_bytes(4, byteorder="big", signed=False))

# Set the flip timer to 10 to prevent counting up immediately
f.seek(0x1B24, 0)
f.write((10).to_bytes(4, byteorder="big", signed=False))

# Write the item id
f.seek(0x1B70, 0)
f.write(ItemId.to_bytes(2, byteorder="big", signed=False))

# Write the pointer to text buffer
f.seek(TextBufferPointerOffset, 0)
f.write(TextBufferPointer.to_bytes(4, byteorder="big", signed=False))

# Write the text buffer
f.seek(TextBufferPointerOffset + 0x4, 0)
for i in range(0x94):
f.write((0x33).to_bytes(1, byteorder="big", signed=False))

# Write the pointer to the init asm function
f.seek(TextBufferPointerOffset + 0x98, 0)
f.write(InitAsmFunctionPointer.to_bytes(4, byteorder="big", signed=False))

# Write the init asm function
# The init function is the same for all versions except for Korean
if VersionString == "kr0":
InitAsmFuncBinName = "Init_KR"
else:
InitAsmFuncBinName = "Init_Main"

InitAsmFuncOffset = 0xD4C
g = open("bin/" + InitAsmFuncBinName + ".bin", "rb")

# Perform the write
Func = g.read()

f.seek(InitAsmFuncOffset, 0)
for b in Func:
f.write(b.to_bytes(1, byteorder="big", signed=False))

g.close()

# Write the main asm function
g = open("bin/Main_" + BinVersion + ".bin", "rb")

# Perform the write
Func = g.read()

f.seek(InitAsmFuncOffset + 0x24, 0)
for b in Func:
f.write(b.to_bytes(1, byteorder="big", signed=False))

g.close()

# Get the sum of the bytes for the data field
f.seek(0x8, 0)
DataFieldSum = 0x3FC
DataField = f.read(0x25A8)
for b in DataField:
DataFieldSum += b

# Set the checksum of the bytes for the data field
f.seek(0x25B0, 0)
f.write(DataFieldSum.to_bytes(4, byteorder="big", signed=False))

# Set the inverted checksum of the bytes for the data field
f.seek(0x25B4, 0)
f.write((~DataFieldSum).to_bytes(4, byteorder="big", signed=True))

f.close()
27 changes: 27 additions & 0 deletions Source/Init_KR.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Code created by Zephiles

# Set global function/variable offsets
.set GP_OFFSET,-0x7DA8
.set MAIN_FUNC_OFFSET,0xD70
.set NANDMGR_WORK_PTR_OFFSET,-0x7D70

# Get the index for the file loaded
lwz r4,GP_OFFSET(r13)
lwz r4,0xDC(r4)

# Adjust the index to be in multiples of 0x25B8 bytes
mulli r4,r4,0x25B8

# Add the offset to main function
addi r4,r4,MAIN_FUNC_OFFSET

# Get the NAND File pointer
lwz r5,NANDMGR_WORK_PTR_OFFSET(r13)
lwz r5,0x10(r5)

# Get the start of the main function in the current file
add r12,r5,r4

# Jump to the main function
mtctr r12
bctr
27 changes: 27 additions & 0 deletions Source/Init_Main.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Code created by Zephiles

# Set global function/variable offsets
.set GP_OFFSET,-0x7D88
.set MAIN_FUNC_OFFSET,0xD70
.set NANDMGR_WORK_PTR_OFFSET,-0x7D50

# Get the index for the file loaded
lwz r4,GP_OFFSET(r13)
lwz r4,0xDC(r4)

# Adjust the index to be in multiples of 0x25B8 bytes
mulli r4,r4,0x25B8

# Add the offset to main function
addi r4,r4,MAIN_FUNC_OFFSET

# Get the NAND File pointer
lwz r5,NANDMGR_WORK_PTR_OFFSET(r13)
lwz r5,0x10(r5)

# Get the start of the main function in the current file
add r12,r5,r4

# Jump to the main function
mtctr r12
bctr
Loading

0 comments on commit 4cf2298

Please sign in to comment.