-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
26 changed files
with
3,796 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.