diff --git a/MSET9_installer_script/mset9.bat b/MSET9_installer_script/MSET9-Windows.bat similarity index 100% rename from MSET9_installer_script/mset9.bat rename to MSET9_installer_script/MSET9-Windows.bat diff --git a/MSET9_installer_script/mset9.command b/MSET9_installer_script/MSET9-macOS.command similarity index 100% rename from MSET9_installer_script/mset9.command rename to MSET9_installer_script/MSET9-macOS.command diff --git a/MSET9_installer_script/errors.txt b/MSET9_installer_script/errors.txt index cfbfafe..021d401 100644 --- a/MSET9_installer_script/errors.txt +++ b/MSET9_installer_script/errors.txt @@ -1,18 +1,17 @@ Error 01: Not running on the SD Card root. /Nintendo 3DS/ not found. Error 02: Write protected SD Card. Ensure switch is facing up. Error 03: Attempting to change target console version during exploit. -Error 04: No Home Menu Extdata. SD needs reformatting, follow MSET9 Troubleshooting page. -Error 05: No Mii Maker Data, follow MSET9 Troubleshooting page. +Error 04: Multiple ID0s, follow MSET9 Troubleshooting page. +Error 05: Multiple ID1s, follow MSET9 Troubleshooting page. Error 06: Not enough free space on disk. Clean out unused files. -Error 07: Multiple ID0s, follow MSET9 Troubleshooting page. -Error 08: Missing/Malformed file. Reextract MSET9 zip file. -Error 09: Could not change back into SD directory. Ensure the SD Card has been reinserted. -Error 10: Database problem. Follow the troubleshooting to reset the DBs. -Error 12: Multiple ID1s, follow MSET9 Troubleshooting page. +Error 07: Missing/Malformed file. Reextract MSET9 zip file. +Error 08: Could not change back into SD directory. Ensure the SD Card has been reinserted. + Error 13: Device doesn't exist. Error 14: Can't open device. Error 15: Not FAT32 formatted or corrupted filesystem. Error 16: Unable to umount SD card. Error 17: Root privilege is required. +Error 18: Winerror 234, Windows UTF Settings needs adjustment, follow MSET9 Troubleshooting page MSET9 Troubleshooting Page: https://3ds.hacks.guide/troubleshooting#installing-boot9strap-mset9 diff --git a/MSET9_installer_script/mset9.py b/MSET9_installer_script/mset9.py index 4507efa..2ce8975 100644 --- a/MSET9_installer_script/mset9.py +++ b/MSET9_installer_script/mset9.py @@ -1,23 +1,25 @@ #!/usr/bin/python3 -import abc, os, platform, time, binascii +import abc, sys, os, platform, time, pathlib, binascii VERSION = "v1.2c" def prgood(content): - print(f"[\033[0;32m✓\033[0m] {content}") + # print(f"[\033[0;32m✓\033[0m] {content}") + # so that people aren't confused by the [?]. stupid Windows. + print(f"[\033[0;32mOK\033[0m] {content}") def prbad(content): - print(f"[\033[0;91mX\033[0m] {content}") + print(f"[\033[0;91mXX\033[0m] {content}") def prinfo(content): - print(f"[*] {content}") + print(f"[--] {content}") def cleanup(remount=False): pass def exitOnEnter(errCode = 0, remount=False): cleanup(remount) - input("[*] Press Enter to exit...") + input("[--] Press Enter to exit...") exit(errCode) # wrapper for fs operations. can use pyfilesystem2 directly, @@ -48,9 +50,6 @@ def rmtree(self, path): def copytree(self, src, dst): pass @abc.abstractmethod - def walk(self, path, topdown=False): - pass - @abc.abstractmethod def is_writable(self): pass @abc.abstractmethod @@ -71,11 +70,48 @@ def remove_extra(): osver = platform.system() thisfile = os.path.abspath(__file__) +scriptroot = os.path.dirname(thisfile) systmp = None +systemroot = pathlib.Path(sys.executable).anchor # Never hardcode C:. My Windows drive letter is E:, my SD card or USB drive is often C:. +if os.stat(scriptroot).st_dev == os.stat(systemroot).st_dev: + prbad("Error 01: Script is not running on your SD card!") + prinfo(f"Current location: {scriptroot}") + exitOnEnter() + +def dig_for_root(): + import shutil + global thisfile, scriptroot + + if not os.path.ismount(scriptroot): + root = scriptroot + while not os.path.ismount(root) and root != os.path.dirname(root): + root = os.path.dirname(root) + + for f in ["SafeB9S.bin", "b9", "boot.firm", "boot.3dsx", "boot9strap/", "mset9.py", "mset9.bat", "mset9.command", "_INSTRUCTIONS.txt", "errors.txt"]: + try: + shutil.move(os.path.join(scriptroot, f), os.path.join(root, f)) + except: + pass # The sanity checks will deal with that. I just don't want the exception to terminate the script. + + with open(os.path.join(scriptroot, "Note from MSET9.txt"), "w") as f: + f.write("Hey!\n") + f.write("All the MSET9 files have been moved to the root of your SD card.\n\n") + + f.write("\"What is the 'root of my SD card'...?\"\n") + f.write("The root is 'not inside any folder'.\n") + f.write("This is where you can see your 'Nintendo 3DS' folder. (It is not inside the Nintendo 3DS folder itself!)\n\n") + + f.write("Reference image: https://3ds.hacks.guide/images/screenshots/onboarding/sdroot.png\n\n") + + f.write(f"At the time of writing, the root of your SD card is at: '{root}'. Check it out!\n") + f.close() + + scriptroot = root + thisfile = os.path.join(scriptroot, "mset9.py") + if osver == "Darwin": # ======== macOS / iOS? ======== - import sys tmpprefix = "mset9-macos-run-" @@ -101,13 +137,15 @@ def run_diskutil_and_wait(command, dev): return subprocess.run(["diskutil", *command, dev], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode if len(sys.argv) < 2: - if not thisfile.startswith("/Volumes/"): + if not scriptroot.startswith("/Volumes/"): prbad("Error 01: Couldn't find Nintendo 3DS folder! Ensure that you are running this script from the root of the SD card.") # should we add some macos specific message? exitOnEnter() + + dig_for_root() prinfo("Resolving device...") device = None - devid = os.stat(thisfile).st_dev + devid = os.stat(scriptroot).st_dev for devname in os.listdir("/dev"): if not devname.startswith("disk"): continue @@ -394,6 +432,8 @@ def __init__(self, device): self.reload() def exists(self, path): return self.fs.exists(path) + def isdir(self, path): + return self.fs.getinfo(path).is_dir def mkdir(self, path): self.fs.makedir(path) def open(self, path, mode='r'): @@ -421,9 +461,8 @@ def rmtree(self, path): self.fs.removetree(path) def copytree(self, src, dst): self.fs.copydir(src, dst, create=True) - def walk(self, path, topdown=False): # topdown is ignored - for dir_path, dirs, files in self.fs.walk(path): - yield dir_path, list(map(lambda x: x.name, dirs)), list(map(lambda x: x.name, files)) + def listdir(self, path): + return self.fs.listdir(path) def is_writable(self): try: with self.open("test.txt", "w") as f: @@ -493,6 +532,8 @@ def abs(self, path): return os.path.join(self.root, path) def exists(self, path): return os.path.exists(self.abs(path)) + def isdir(self, path): + return os.path.isdir(self.abs(path)) def mkdir(self, path): os.mkdir(self.abs(path)) def open(self, path, mode='r'): @@ -507,8 +548,8 @@ def rmtree(self, path): shutil.rmtree(self.abs(path)) def copytree(self, src, dst): shutil.copytree(self.abs(src), self.abs(dst)) - def walk(self, path, topdown=False): - return os.walk(self.abs(path), topdown=topdown) + def listdir(self, path): + return os.listdir(path) def is_writable(self): writable = os.access(self.root, os.W_OK) try: # Bodge for windows @@ -527,13 +568,13 @@ def reload(self): try: os.chdir(self.root) except Exception: - prbad("Error 09: Couldn't reapply working directory, is SD card reinserted?") + prbad("Error 08: Couldn't reapply working directory, is SD card reinserted?") exitOnEnter() def print_root(self): prinfo(f"Current dir: {self.root}") - - fs = OSFS(os.path.dirname(thisfile)) + dig_for_root() + fs = OSFS(scriptroot) def clearScreen(): if osver == "Windows": @@ -541,6 +582,29 @@ def clearScreen(): else: os.system("clear") +# -1: Cancelled +def getInput(options): + if type(options) == range: + options = [*options, (options[-1] + 1)] + + while 1: + try: + opt = int(input(">>> ")) + except KeyboardInterrupt: + print() + return -1 + except EOFError: + print() + return -1 + except ValueError: + opt = 0xFFFFFFFF + + if opt not in options: + prbad(f"Invalid input, try again. Valid inputs: {str.join(', ', (str(i) for i in options))}") + continue + + return opt + # Section: insureRoot if not fs.exists("Nintendo 3DS/"): prbad("Error 01: Couldn't find Nintendo 3DS folder! Ensure that you are running this script from the root of the SD card.") @@ -561,355 +625,439 @@ def writeProtectCheck(): # Section: SD card free space # ensure 16MB free space -if not fs.ensurespace(16777216): +if not fs.ensurespace(16 * 1024 * 1024): #prbad(f"Error 06: You need at least 16MB free space on your SD card, you have {(freeSpace / 1000000):.2f} bytes!") - prbad(f"Error 06: You need at least 16MB free space on your SD card!") + prbad("Error 06: You need at least 16MB free space on your SD card!") prinfo("Please free up some space and try again.") exitOnEnter() clearScreen() -print(f"MSET9 {VERSION} SETUP by zoogie, Aven and DannyAAM") +print(f"MSET9 {VERSION} SETUP by zoogie, Aven, DannyAAM and thepikachugamer") print("What is your console model and version?") print("Old 3DS has two shoulder buttons (L and R)") print("New 3DS has four shoulder buttons (L, R, ZL, ZR)") + print("\n-- Please type in a number then hit return --\n") + +consoleNames = { + 1: "Old 3DS/2DS, 11.8.0 to 11.17.0", + 2: "New 3DS/2DS, 11.8.0 to 11.17.0", + 3: "Old 3DS/2DS, 11.4.0 to 11.7.0", + 4: "New 3DS/2DS, 11.4.0 to 11.7.0" +} + print("Enter one of these four numbers!") -print("Enter 1 for: Old 3DS/2DS, 11.8.0 to 11.17.0") -print("Enter 2 for: New 3DS/2DS, 11.8.0 to 11.17.0") -print("Enter 3 for: Old 3DS/2DS, 11.4.0 to 11.7.0") -print("Enter 4 for: New 3DS/2DS, 11.4.0 to 11.7.0") +for i in consoleNames: + print(f"Enter {i} for: {consoleNames[i]}") -encodedId1s = { +# print("Enter 1 for: Old 3DS/2DS, 11.8.0 to 11.17.0") +# print("Enter 2 for: New 3DS/2DS, 11.8.0 to 11.17.0") +# print("Enter 3 for: Old 3DS/2DS, 11.4.0 to 11.7.0") +# print("Enter 4 for: New 3DS/2DS, 11.4.0 to 11.7.0") + +encodedID1s = { 1: "FFFFFFFA119907488546696508A10122054B984768465946C0AA171C4346034CA047B84700900A0871A0050899CE0408730064006D00630000900A0862003900", 2: "FFFFFFFA119907488546696508A10122054B984768465946C0AA171C4346034CA047B84700900A0871A005085DCE0408730064006D00630000900A0862003900", 3: "FFFFFFFA119907488546696508A10122054B984768465946C0AA171C4346034CA047B84700900A08499E050899CC0408730064006D00630000900A0862003900", 4: "FFFFFFFA119907488546696508A10122054B984768465946C0AA171C4346034CA047B84700900A08459E050881CC0408730064006D00630000900A0862003900" } -hackedId1Encoded, consoleModel, consoleFirmware = "", "", "" -while 1: - try: - sysModelVerSelect = input(">>> ") - if sysModelVerSelect.startswith("11"): - prbad("Don't type the firmware version, just the selection number!") - sysModelVerSelect = 42 - sysModelVerSelect = int(sysModelVerSelect) - except KeyboardInterrupt: - print() - prgood("Goodbye!") - exitOnEnter(remount=True) - except: - sysModelVerSelect = 42 - if sysModelVerSelect == 1: - hackedId1Encoded = encodedId1s[1] - consoleModel = "OLD3DS" - consoleFirmware = "11.8-11.17" - break - - if sysModelVerSelect == 2: - hackedId1Encoded = encodedId1s[2] - consoleModel = "NEW3DS" - consoleFirmware = "11.8-11.17" - break - - if sysModelVerSelect == 3: - hackedId1Encoded = encodedId1s[3] - consoleModel = "OLD3DS" - consoleFirmware = "11.4-11.7" - break - - if sysModelVerSelect == 4: - hackedId1Encoded = encodedId1s[4] - consoleModel = "NEW3DS" - consoleFirmware = "11.4-11.7" - break - else: - prbad("Invalid input, try again. Valid inputs: 1, 2, 3, 4") +consoleIndex = getInput(range(1, 4)) +if consoleIndex < 0: + prgood("Goodbye!") + exitOnEnter(remount=True) + +ID0, ID0Count, ID1, ID1Count = "", 0, "", 0 + +haxStates = ["\033[30;1mID1 not created\033[0m", "\033[33;1mNot ready - check MSET9 status for more details\033[0m", "\033[32mReady\033[0m", "\033[32;1mInjected\033[0m", "\033[32mRemoved trigger file\033[0m"] +haxState = 0 -trigger = "002F003A.txt" # all 3ds ":/" in hex -hackedId1 = bytes.fromhex(hackedId1Encoded).decode("utf-16le") # ID1 - arm injected payload in readable format -id1 = "" -id0 = "" -realId1Path = "" +realID1Path = "" +realID1BackupTag = "_user-id1" -extdataRoot = "" -realId1BackupTag = "_user-id1" -id0Count = 0 -id1Count = 0 -id0List = [] +hackedID1 = bytes.fromhex(encodedID1s[consoleIndex]).decode("utf-16le") # ID1 - arm injected payload in readable format +hackedID1Path = "" -homeMenuExtdata = [0x8F, 0x98, 0x82, 0xA1, 0xA9, 0xB1] # us,eu,jp,ch,kr,tw +homeMenuExtdata = [0x8F, 0x98, 0x82, 0xA1, 0xA9, 0xB1] # us,eu,jp,ch,kr,tw miiMakerExtdata = [0x217, 0x227, 0x207, 0x267, 0x277, 0x287] # us,eu,jp,ch,kr,tw +trigger = "002F003A.txt" # all 3ds ":/" in hex format +triggerFilePath = "" -# make a table so we can print regions based on what hex code from the above is found -regionTable = { - 0x8F: "USA Region", - 0x98: "EUR Region", - 0x82: "JPN Region", - 0xA1: "CHN Region", - 0xA9: "KOR Region", - 0xB1: "TWN Region" -} - -homeDataPath, miiDataPath, homeHex, miiHex = "", "", 0x0, 0x0 -def sanity(): - global fs, haxState, realId1Path, id0, id1, homeDataPath, miiDataPath, homeHex, miiHex - menuExtdataGood = False - miiExtdataGood = False +def createHaxID1(): + global fs, ID0, hackedID1Path, realID1Path, realID1BackupTag + print("\033[0;33m=== DISCLAIMER ===\033[0m") # 5;33m? The blinking is awesome but I also don't want to frighten users lol + print() + print("This process will temporarily reset all your 3DS data.") + print("All your applications and themes will disappear.") + print("This is perfectly normal, and if everything goes right, it will re-appear") + print("at the end of the process.") print() - prinfo("Performing sanity checks...") - - writeProtectCheck() - - prinfo("Ensuring extracted files exist...") - fileSanity = 0 - fileSanity += softcheck("boot9strap/boot9strap.firm", 0, 0x08129C1F, 1) - fileSanity += softcheck("boot.firm", retval = 1) - fileSanity += softcheck("boot.3dsx", retval = 1) - fileSanity += softcheck("b9", retval = 1) - fileSanity += softcheck("SafeB9S.bin", retval = 1) - if fileSanity > 0: - prbad("Error 08: One or more files are missing or malformed!") - prinfo("Please re-extract the MSET9 zip file, overwriting any existing files when prompted.") + print("In any case, it is highly recommended to make a backup of your SD card's contents to a folder on your PC.") + print("(Especially the 'Nintendo 3DS' folder.)") + print() + + if osver == "Linux": # ... + print("(on Linux, things like to not go right - please ensure that your SD card is mounted with the 'utf8' option.)") + print() + + print("Input '1' again to confirm.") + print("Input '2' to cancel.") + time.sleep(3) + if getInput(range(1, 2)) != 1: + print() + prinfo("Cancelled.") + exitOnEnter(remount=True) + + hackedID1Path = ID0 + "/" + hackedID1 + + try: + prinfo("Creating hacked ID1...") + fs.mkdir(hackedID1Path) + prinfo("Creating dummy databases...") + fs.mkdir(hackedID1Path + "/dbs") + fs.open (hackedID1Path + "/dbs/title.db", "w").close() + fs.open (hackedID1Path + "/dbs/import.db", "w").close() + except Exception as exc: + if isinstance(exc, OSError) and osver == "Windows" and exc.winerror == 234: # WinError 234 my love + prbad("Error 18: Windows locale settings are broken!") + prinfo("Consult https://3ds.hacks.guide/troubleshooting#installing-boot9strap-mset9 for instructions.") + prinfo("If you need help, join Nintendo Homebrew on Discord: https://discord.gg/nintendohomebrew") + elif isinstance(exc, OSError) and osver == "Linux" and exc.errno == 22: # Don't want this message to display on Windows if it ever manages to + prbad("Failed to create hacked ID1!") # Give this an error number? + prbad(f"Error details: {str(exc)}") + prinfo("Please unmount your SD card and remount it with the 'utf8' option.") # Should we do this ourself? Like look at macOS + else: + prbad("An unknown error occured!") + prbad(f"Error details: {str(exc)}") + prinfo("Join Nintendo Homebrew on Discord for help: https://discord.gg/nintendohomebrew") + exitOnEnter() - prgood("All files look good!") + + if not realID1Path.endswith(realID1BackupTag): + prinfo("Backing up original ID1...") + fs.rename(realID1Path, realID1Path + realID1BackupTag) + + prgood("Created hacked ID1.") + exitOnEnter() + +titleDatabasesGood = False +menuExtdataGood = False +miiExtdataGood = False + +def sanity(): + global fs, hackedID1Path, titleDatabasesGood, menuExtdataGood, miiExtdataGood prinfo("Checking databases...") - checkTitledb = softcheck(realId1Path + "/dbs/title.db", 0x31E400, 0, 1) - checkImportdb = softcheck(realId1Path + "/dbs/import.db", 0x31E400, 0, 1) - if checkTitledb or checkImportdb: - prbad("Error 10: Database(s) malformed or missing!") - if not ( - fs.exists(realId1Path + "/dbs/import.db") - or fs.exists(realId1Path + "/dbs/title.db") - ): - if not fs.exists(realId1Path + "/dbs"): - fs.mkdir(realId1Path + "/dbs") - if checkTitledb: - fs.open(realId1Path + "/dbs/title.db", "x").close() - if checkImportdb: - fs.open(realId1Path + "/dbs/import.db", "x").close() - - prinfo("Created empty databases.") - prinfo("Please initialize the title database by inserting the SD into your console, powering it on, then navigating to System Settings -> Data Management -> Nintendo 3DS -> Software -> Reset, then rerun this script.") - prinfo("Visual guide: https://3ds.hacks.guide/images/screenshots/database-reset.jpg") - exitOnEnter() - else: - prgood("Databases look good!") - - if fs.exists(realId1Path + "/extdata/" + trigger): - prinfo("Removing stale trigger...") - fs.remove(realId1Path + "/extdata/" + trigger) - - extdataRoot = realId1Path + "/extdata/00000000" + checkTitledb = softcheck(hackedID1Path + "/dbs/title.db", 0x31E400) + checkImportdb = softcheck(hackedID1Path + "/dbs/import.db", 0x31E400) + titleDatabasesGood = not (checkTitledb or checkImportdb) + if not titleDatabasesGood: + if not fs.exists(hackedID1Path + "/dbs"): + fs.mkdir(hackedID1Path + "/dbs") + # Stub them both. I'm not sure how the console acts if title.db is fine but not import. Someone had that happen, once + fs.open(hackedID1Path + "/dbs/title.db", "w").close() + fs.open(hackedID1Path + "/dbs/import.db", "w").close() prinfo("Checking for HOME Menu extdata...") for i in homeMenuExtdata: - extdataRegionCheck = extdataRoot + f"/{i:08X}" + extdataRegionCheck = hackedID1Path + f"/extdata/00000000/{i:08X}" if fs.exists(extdataRegionCheck): - prgood(f"Detected {regionTable[i]} HOME Menu data!") - homeHex = i - homeDataPath = extdataRegionCheck menuExtdataGood = True break - if not menuExtdataGood: - prbad("Error 04: No HOME Menu data!") - prinfo("Your SD is not formatted properly, or isn't being read by the console.") - prinfo("Please go to https://3ds.hacks.guide/troubleshooting#installing-boot9strap-mset9 for instructions.") - prinfo("If you need help, join Nintendo Homebrew on Discord: https://discord.gg/nintendohomebrew") - exitOnEnter() - prinfo("Checking for Mii Maker extdata...") for i in miiMakerExtdata: - extdataRegionCheck = extdataRoot + f"/{i:08X}" + extdataRegionCheck = hackedID1Path + f"/extdata/00000000/{i:08X}" if fs.exists(extdataRegionCheck): - prgood("Found Mii Maker data!") - miiHex = i - miiDataPath = extdataRegionCheck miiExtdataGood = True break - - if not miiExtdataGood: - prbad("Error 05: No Mii Maker data!") - prinfo("Please go to https://3ds.hacks.guide/troubleshooting#installing-boot9strap-mset9 for instructions.") - exitOnEnter() -def injection(): - global fs, realId1Path, id1 + return menuExtdataGood and miiExtdataGood and titleDatabasesGood - if not fs.exists(id0 + "/" + hackedId1): - prinfo("Creating hacked ID1...") - hackedId1Path = id0 + "/" + hackedId1 - fs.mkdir(hackedId1Path) - fs.mkdir(hackedId1Path + "/extdata") - fs.mkdir(hackedId1Path + "/extdata/00000000") +def sanityReport(): + fs.print_root() + + if not menuExtdataGood: + prbad("HOME menu extdata: Missing!") + prinfo("Please power on your console with your SD inserted, then check again.") + prinfo("If this does not work, your SD card may need to be reformatted.") else: - prinfo("Reusing existing hacked ID1...") - hackedId1Path = id0 + "/" + hackedId1 + prgood("HOME menu extdata: OK!") - if not fs.exists(hackedId1Path + "/dbs"): - prinfo("Copying databases to hacked ID1...") - fs.copytree(realId1Path + "/dbs", hackedId1Path + "/dbs") + print() - prinfo("Copying extdata to hacked ID1...") - if not fs.exists(hackedId1Path + f"/extdata/00000000/{homeHex:08X}"): - fs.copytree(homeDataPath, hackedId1Path + f"/extdata/00000000/{homeHex:08X}") - if not fs.exists(hackedId1Path + f"/extdata/00000000/{miiHex:08X}"): - fs.copytree(miiDataPath, hackedId1Path + f"/extdata/00000000/{miiHex:08X}") + if not miiExtdataGood: + prbad("Mii Maker extdata: Missing!") + prinfo("Please power on your console with your SD inserted, then launch Mii Maker.") + else: + prgood("Mii Maker extdata: OK!") - prinfo("Injecting trigger file...") - triggerFilePath = id0 + "/" + hackedId1 + "/extdata/" + trigger - if not fs.exists(triggerFilePath): - with fs.open(triggerFilePath, "w") as f: - f.write("plz be haxxed mister arm9, thx") - f.close() - - if fs.exists(realId1Path) and realId1BackupTag not in realId1Path: - prinfo("Backing up real ID1...") - fs.rename(realId1Path, realId1Path + realId1BackupTag) - id1 += realId1BackupTag - realId1Path = f"{id0}/{id1}" + print() + + if not titleDatabasesGood: + prbad("Title database: Not initialized!") + prinfo("Please power on your console with your SD inserted, open System Setttings,") + prinfo("navigate to Data Management -> Nintendo 3DS -> Software, then select Reset.") else: - prinfo("Skipping backup because a backup already exists!") + prgood("Title database: OK!") + + print() +def injection(create=True): + global fs, haxState, hackedID1Path, trigger + + triggerFilePath = hackedID1Path + "/extdata/" + trigger + + if not fs.exists(triggerFilePath) ^ create: + prbad(f"Trigger file already {'injected' if create else 'removed'}!") + return + + if fs.exists(triggerFilePath): + fs.remove(triggerFilePath) + haxState = 4 + prgood("Removed trigger file.") + return + + prinfo("Injecting trigger file...") + with fs.open(triggerFilePath, 'w') as f: + f.write("pls be haxxed mister arm9, thx") + f.close() prgood("MSET9 successfully injected!") + exitOnEnter() def remove(): - global fs, realId1Path, id0, id1 + global fs, ID0, ID1, hackedID1Path, realID1Path, realID1BackupTag, titleDatabasesGood + prinfo("Removing MSET9...") - if fs.exists(realId1Path) and realId1BackupTag in realId1Path: - prinfo("Renaming original Id1...") - fs.rename(realId1Path, id0 + "/" + id1[:32]) - else: - prgood("Nothing to remove!") - return - - # print(id1_path, id1_root+"/"+id1[:32]) - for id1Index in range(1,5): # Attempt to remove *all* hacked id1s - maybeHackedId = bytes.fromhex(encodedId1s[id1Index]).decode("utf-16le") - if fs.exists(id0 + "/" + maybeHackedId): - prinfo("Deleting hacked ID1...") - fs.rmtree(id0 + "/" + maybeHackedId) - id1 = id1[:32] - realId1Path = id0 + "/" + id1 + if hackedID1Path and fs.exists(hackedID1Path): + if not fs.exists(realID1Path + "/dbs") and titleDatabasesGood: + prinfo("Moving databases to user ID1...") + fs.rename(hackedID1Path + "/dbs", realID1Path + "/dbs") + + prinfo("Deleting hacked ID1...") + fs.rmtree(hackedID1Path) + + if fs.exists(realID1Path) and realID1Path.endswith(realID1BackupTag): + prinfo("Renaming original ID1...") + fs.rename(realID1Path, ID0 + "/" + ID1[:32]) + ID1 = ID1[:32] + realID1Path = ID0 + "/" + ID1 + + haxState = 0 prgood("Successfully removed MSET9!") -def softcheck(keyfile, expectedSize = None, crc32 = None, retval = 0): +def softcheck(keyfile, expectedSize = None, crc32 = None): global fs filename = keyfile.rsplit("/")[-1] + if not fs.exists(keyfile): prbad(f"{filename} does not exist on SD card!") - return retval - if expectedSize: - fileSize = fs.getsize(keyfile) - if expectedSize != fileSize: - prbad(f"{filename} is size {fileSize:,} bytes, not expected {expectedSize:,} bytes") - return retval - elif crc32: + return 1 + + fileSize = fs.getsize(keyfile) + if not fileSize: + prbad(f"{filename} is an empty file!") + return 1 + elif expectedSize and fileSize != expectedSize: + prbad(f"{filename} is size {fileSize:,} bytes, not expected {expectedSize:,} bytes") + return 1 + + if crc32: with fs.open(keyfile, "rb") as f: checksum = binascii.crc32(f.read()) + f.close() if crc32 != checksum: prbad(f"{filename} was not recognized as the correct file") - f.close() - return retval - f.close() + return 1 + prgood(f"{filename} looks good!") return 0 +def is3DSID(name): + if not len(name) == 32: + return False + + try: + hex_test = int(name, 0x10) + except: + return False + + return True + + +# Section: Sanity checks A (global files required for exploit) +writeProtectCheck() + +prinfo("Ensuring extracted files exist...") + +fileSanity = 0 +fileSanity += softcheck("boot9strap/boot9strap.firm", crc32=0x08129C1F) +fileSanity += softcheck("boot.firm") +fileSanity += softcheck("boot.3dsx") +fileSanity += softcheck("b9") +fileSanity += softcheck("SafeB9S.bin") + +if fileSanity > 0: + prbad("Error 07: One or more files are missing or malformed!") + prinfo("Please re-extract the MSET9 zip file, overwriting any existing files when prompted.") + exitOnEnter() + +# prgood("All files look good!") + # Section: sdwalk -for root, dirs, files in fs.walk("Nintendo 3DS/", topdown=True): +for dirname in fs.listdir("Nintendo 3DS/"): + fullpath = "Nintendo 3DS/" + dirname - for name in dirs: - # If the name doesn't contain sdmc (Ignores MSET9 exploit folder) - if "sdmc" not in name and len(name[:32]) == 32: - try: - # Check to see if the file name encodes as an int (is hex only) - hexVerify = int(name[:32], 16) - except: - continue - if type(hexVerify) is int: - # Check if the folder (which is either id1 or id0) has the extdata folder - # if it does, it's an id1 folder - if fs.exists(os.path.join(root, name) + "/extdata"): - id1Count += 1 - id1 = name - id0 = root - realId1Path = os.path.join(root, name) - - # Otherwise, add it to the id0 list because we need to make sure we only have one id0 - else: - if len(name) == 32: - id0Count += 1 - id0List.append(os.path.join(root, name)) - - for name in dirs: # Run the check for existing install after figuring out the structure - # Check if we have an MSET9 Hacked id1 folder - if "sdmc" in name and len(name) == 32: - # If the MSET9 folder doesn't match the proper haxid1 for the selected console version - if hackedId1 != name: - prbad("Error 03: Don't change console model/version in the middle of MSET9!") - prbad("Please restart the setup.") - remove() - exitOnEnter() + if not fs.isdir(fullpath): + prinfo(f"Found file in Nintendo 3DS folder? '{dirname}'") + continue + if not is3DSID(dirname): + continue -prinfo("Detected ID0(s):") -for i in id0List: - prinfo(i) -print() -if id0Count != 1: - prbad(f"Error 07: You don't have 1 ID0 in your Nintendo 3DS folder, you have {id0Count}!") + prinfo(f"Detected ID0: {dirname}") + ID0 = fullpath + ID0Count += 1 + +if ID0Count != 1: + prbad(f"Error 04: You don't have 1 ID0 in your Nintendo 3DS folder, you have {ID0Count}!") prinfo("Consult: https://3ds.hacks.guide/troubleshooting#installing-boot9strap-mset9 for help!") exitOnEnter() -if id1Count != 1: - prbad(f"Error 12: You don't have 1 ID1 in your Nintendo 3DS folder, you have {id1Count}!") +for dirname in fs.listdir(ID0): + fullpath = ID0 + "/" + dirname + + if not fs.isdir(fullpath): + prinfo(f"Found file in ID0 folder? '{dirname}'") + continue + + if is3DSID(dirname) or (dirname[32:] == realID1BackupTag and is3DSID(dirname[:32])): + prinfo(f"Detected ID1: {dirname}") + ID1 = dirname + realID1Path = ID0 + "/" + ID1 + ID1Count += 1 + elif "sdmc" in dirname and len(dirname) == 32: + currentHaxID1enc = dirname.encode("utf-16le").hex().upper() + currentHaxID1index = 0 + + for haxID1index in encodedID1s: + if currentHaxID1enc == encodedID1s[haxID1index]: + currentHaxID1index = haxID1index + break + + if currentHaxID1index == 0 or (hackedID1Path and fs.exists(hackedID1Path)): # shouldn't happen + prbad("Unrecognized/duplicate hacked ID1 in ID0 folder, removing!") + fs.rmtree(fullpath) + elif currentHaxID1index != consoleIndex: + prbad("Error 03: Don't change console model/version in the middle of MSET9!") + print(f"Earlier, you selected: '[{currentHaxID1index}.] {consoleNames[currentHaxID1index]}'") + print(f"Now, you selected: '[{consoleIndex}.] {consoleNames[consoleIndex]}'") + print() + print("Please re-enter the number for your console model and version.") + + choice = getInput([consoleIndex, currentHaxID1index]) + if choice < 0: + prinfo("Cancelled.") + hackedID1Path = fullpath + remove() + exitOnEnter() + + elif choice == currentHaxID1index: + consoleIndex = currentHaxID1index + hackedID1 = dirname + + elif choice == consoleIndex: + fs.rename(fullpath, ID0 + "/" + hackedID1) + + hackedID1Path = ID0 + "/" + hackedID1 + sanityOK = sanity() + + if fs.exists(hackedID1Path + "/extdata/" + trigger): + triggerFilePath = hackedID1Path + "/extdata/" + trigger + haxState = 3 # Injected. + elif sanityOK: + haxState = 2 # Ready! + else: + haxState = 1 # Not ready... + +if ID1Count != 1: + prbad(f"Error 05: You don't have 1 ID1 in your Nintendo 3DS folder, you have {ID1Count}!") prinfo("Consult: https://3ds.hacks.guide/troubleshooting#installing-boot9strap-mset9 for help!") exitOnEnter() -clearScreen() -print(f"MSET9 {VERSION} SETUP by zoogie, Aven and DannyAAM") -print(f"Using {consoleModel} {consoleFirmware}") +def mainMenu(): + clearScreen() + print(f"MSET9 {VERSION} SETUP by zoogie, Aven, DannyAAM and thepikachugamer") + print(f"Using {consoleNames[consoleIndex]}") + print() + print(f"Current MSET9 state: {haxStates[haxState]}") -print("\n-- Please type in a number then hit return --\n") -print("↓ Input one of these numbers!") -print("1. Perform sanity checks") -print("2. Inject MSET9 payload") -print("3. Remove MSET9") -print("4. Exit") + print("\n-- Please type in a number then hit return --\n") -while 1: - try: - sysModelVerSelect = int(input(">>> ")) - except KeyboardInterrupt: - sysModelVerSelect = 4 # exit on Ctrl+C - print() - except: - sysModelVerSelect = 42 + print("↓ Input one of these numbers!") - fs.reload() + print("1. Create MSET9 ID1") + print("2. Check MSET9 status") + print("3. Inject trigger file") + print("4. Remove trigger file") - if sysModelVerSelect == 1: - sanity() - prgood("Everything appears to be functional!\n") - exitOnEnter() - elif sysModelVerSelect == 2: - sanity() - injection() - exitOnEnter() - elif sysModelVerSelect == 3: - remove() - remove_extra() - exitOnEnter(remount=True) - elif sysModelVerSelect == 4 or "exit": - break - else: - prinfo("Invalid input, try again. Valid inputs: 1, 2, 3, 4") + if haxState != 3: + print("5. Remove MSET9") + + print("\n0. Exit") + + while 1: + optSelect = getInput(range(0, 5)) + + fs.reload() # (?) + + if optSelect <= 0: + break + + elif optSelect == 1: # Create hacked ID1 + if haxState > 0: + prinfo("Hacked ID1 already exists.") + continue + createHaxID1() + exitOnEnter() + + elif optSelect == 2: # Check status + if haxState == 0: # MSET9 ID1 not present + prbad("Can't do that now!") + continue + sanityReport() + exitOnEnter() + + elif optSelect == 3: # Inject trigger file + if haxState != 2: # Ready to inject + prbad("Can't do that now!") + continue + injection(create=True) + # exitOnEnter() # has it's own + + elif optSelect == 4: # Remove trigger file + if haxState < 2: + prbad("Can't do that now!") + injection(create=False) + time.sleep(3) + return mainMenu() + + elif optSelect == 5: # Remove MSET9 + if haxState <= 0: + prinfo("Nothing to do.") + continue + if haxState == 3: + prbad("Can't do that now!") + continue + + remove() + remove_extra() # (?) + exitOnEnter(remount=True) +mainMenu() cleanup(remount=True) prgood("Goodbye!") time.sleep(2)