borntohonk / Switch-Ghidra-Guides

Various patches for the Nintendo Switch, and how to make them.
MIT License
112 stars 12 forks source link

Mod - Mariko_keygen.py - don't overwrite custom keys #12

Closed mrdude2478 closed 3 months ago

mrdude2478 commented 4 months ago

Mod info, currently with the -k flag we can use our own keyset to generate keys.

Problem - ourr keyset gets overwritten by new keys.

Solution: Under - prod_keys = "%s" % args.keys add this line: new_keys = "prod.keys"

Next: comment out line: #os.rename(prod_keys, 'temp.keys') Under add this: os.popen('cp ' + prod_keys + ' temp.keys')

Nex replace line : with open(prod_keys, 'w') as new_prod_keys: to this: with open(new_keys, 'w') as new_prod_keys:

This we we can keep our custom keyset and still write a new prod.keys file.

borntohonk commented 4 months ago

Mod info, currently with the -k flag we can use our own keyset to generate keys.

Problem - ourr keyset gets overwritten by new keys.

Solution: Under - prod_keys = "%s" % args.keys add this line: new_keys = "prod.keys"

Next: comment out line: #os.rename(prod_keys, 'temp.keys') Under add this: os.popen('cp ' + prod_keys + ' temp.keys')

Nex replace line : with open(prod_keys, 'w') as new_prod_keys: to this: with open(new_keys, 'w') as new_prod_keys:

This we we can keep our custom keyset and still write a new prod.keys file.

edit: the solution below is more appropriate for a less taxing end-user experience.

borntohonk commented 4 months ago

a rewritten keygen that has keys embedded, and requires nothing of the user but firmware files would be this.

this scenario contains all that is required, assuming the user has NO KEYS AT ALL AT ~/.switch/prod.keys which is fairly important to remove when testing, since hactoolnet will pull from it even if you use -k or --keyset!

in this new version -k and --keys instead designate where the keys should be saved.

(you need master_key_00 to open the fs nca 0100000000000819 as bare minimum, and mariko_bek as bare minimum to open Package1, and mariko_kek as bare minimum to transform master_mariko_kek_source into master_kek)

the complete required keylist to avoid running keygen several times would be:

master_key_00 mariko_kek mariko_bek

(an alternative approach also would be to overwrite the inputs with an externally pre-existing key file containing those 3 keys, but that's outside scope of this repository)

import re
import subprocess
import argparse
import os
import shutil
import hashlib

argParser = argparse.ArgumentParser()
argParser.add_argument("-f", "--firmware", help="firmware folder")
argParser.add_argument("-k", "--keys", help="Where you want the keys to be saved")
args = argParser.parse_args()
firmware = "%s" % args.firmware
prod_keys = "%s" % args.keys

if firmware == "None":
    firmware = "firmware"

if prod_keys == "None":
    prod_keys = "prod.keys"

mariko_bek_key = '6A5DXXXXXXXXXXXXXXXXXXXXXXXXXX' # fill in mariko_bek here
mariko_kek_key = '4130XXXXXXXXXXXXXXXXXXXXXXXXXX' # fill in mariko_kek here
master_key_00_key = 'C2CAXXXXXXXXXXXXXXXXXXXXXXXXXX' # fill in master_key_00 here
mariko_bek = f'mariko_bek                        = {mariko_bek_key}'
mariko_kek = f'mariko_kek                        = {mariko_kek_key}'
master_key_00 = f'master_key_00                     = {master_key_00_key}'
bek_hash = hashlib.sha256(mariko_bek_key.encode('utf-8')).hexdigest()
kek_hash = hashlib.sha256(mariko_kek_key.encode('utf-8')).hexdigest()
key_hash = hashlib.sha256(master_key_00_key.encode('utf-8')).hexdigest()

if bek_hash != "ca0fabfd30a3f567aec3e5432428d1a14a0926f49b1093416f00914bc71373ab":
    print("You have filled in the wrong key for mariko_bek, exiting.")
    exit()

if kek_hash != "53be3a736bdb7ff26868ce73e9e5b8ad3b652039be75dfce89a91d11a4c69866":
    print("You have filled in the wrong key for mariko_kek, exiting.")
    exit()

if key_hash != "a2f21eb6b64c18ca50ef4f3403e917dc5f0b39713445a77beba848876d1b1af7":
    print("You have filled in the wrong key for master_key_00, exiting.")
    exit()

with open('temp.keys', 'w') as temp_keys:
    temp_keys.write(f'{mariko_bek}')
    temp_keys.write(f'\n')
    temp_keys.write(f'{mariko_kek}')
    temp_keys.write(f'\n')
    temp_keys.write(f'{master_key_00}')
    temp_keys.close()
    subprocess.run(f'hactoolnet --keyset temp.keys -t switchfs {firmware} --title 0100000000000819 --romfsdir 0100000000000819/romfs/', stdout = subprocess.DEVNULL)
    subprocess.run(f'hactoolnet --keyset temp.keys -t pk11 0100000000000819/romfs/a/package1 --outdir 0100000000000819/romfs/a/pkg1', stdout = subprocess.DEVNULL)
    with open('0100000000000819/romfs/a/pkg1/Decrypted.bin', 'rb') as decrypted_bin:
        secmon_data = decrypted_bin.read()
        result = re.search(b'\x4F\x59\x41\x53\x55\x4D\x49', secmon_data)
        byte_alignment = decrypted_bin.seek(result.end() + 0x32)
        mariko_master_kek_source_key = decrypted_bin.read(0x10).hex().upper()
        byte_alignment = decrypted_bin.seek(0x150)
        revision = decrypted_bin.read(0x01).hex().upper()
        incremented_revision = int(revision) - 0x1
        mariko_master_kek_source = f'mariko_master_kek_source_{incremented_revision}       = ' + mariko_master_kek_source_key
        decrypted_bin.close()
        with open('temp.keys', 'a') as keygen:
            keygen.write(f'\n')
            keygen.write(f'{mariko_master_kek_source}')
            keygen.close()
        with open(prod_keys, 'w') as new_prod_keys:
            subprocess.run(f'hactoolnet --keyset temp.keys -t keygen', stdout=new_prod_keys)
            new_prod_keys.close()
            os.remove('temp.keys')
            print(f'# Keygen completed and output to {prod_keys}, exiting.')
            shutil.rmtree('0100000000000819')
            exit()
borntohonk commented 4 months ago

(i don't intend to update the keygen in make_patches.py as that is more of an "update existing keyfile and make patches" kind of ordeal.)

mrdude2478 commented 4 months ago

Nice, that new keygen code you posted above works excellent. I didn't need to type in anything. Basically I just had a folder called firmware in the same place as I ran the script and that was me done. I'll do slight mod so I can just drag the firmware folder onto the file and it will get the path automatically. Good job.

mrdude2478 commented 4 months ago

Here you go, menu to run scripts in the directory before scripts.

Menu.py

#Menu By MrDude
import os
import pip
from os import path

python = "python"
package = 'argparse'
package2 = 'shutils'
firmware = str("firmware")
debug = 0
os.getcwd() #get current working directory
os.chdir('scripts') #change dir to scripts

def clear_screen():
    global debug
    if debug == 0:
        os.system('cls' if os.name=='nt' else 'clear')

def firm():
    print("Type the firmware folder address or drag the folder onto this menu:")
    global firmware
    firmware = input().replace('"', '')
    if not path.exists(firmware):
        firmware = str("firmware")     

def install():
    try:
        return __import__(package)

    except ImportError:
        pip.main(['install', package, package2])

def keygen1():
    os.system(python + ' keygen.py -f "' + firmware + '" "-k prod.keys"')

def keygen2():
    os.system(python + ' erista_keygen.py -f "' + firmware + '" "-k prod.keys"')

def keygen3():
    os.system(python + ' mariko_keygen.py -f "' + firmware + '" "-k prod.keys"')

def menu():
    try:
        ans=True
        while ans:
            print("=========================================================================")
            print("Keygens By borntohonk, https://github.com/borntohonk/Switch-Ghidra-Guides")
            print("=========================================================================")
            print("1.Run AutokeyGen")
            print("2.Run Mariko Keygen")
            print("3.Run Erista Keygen")          
            print("4.Exit/Quit\n")
            ans=input("What would you like to do? ")
            if ans=="1":
                print("")
                firm()
                keygen1()
                #clear_screen()
            elif ans=="2":
                print("")
                firm()
                keygen2()
                clear_screen()
            elif ans=="3":
                print("")
                firm()
                keygen3()
                clear_screen()
            elif ans=="4":
                print("\n Goodbye")
                ans = None
            else:
                print("\n Not Valid Choice Try again")
    except OSError as e:
        print("Error: %s : %s" % ("In menu: ", e.strerror))             

install()
menu()

Notice in this menu you can install dependencies - I put argparse as an example.

Next mod keygen script by changing a few lines near the top:

import re
import subprocess
import argparse
import os
import sys
import shutil
import hashlib
import os.path
from pathlib import Path

argParser = argparse.ArgumentParser()
argParser.add_argument("-f", "--firmware", help="firmware folder")
argParser.add_argument("-k", "--keys", help="Where you want the keys to be saved")
args = argParser.parse_args()
firmware = "%s" % args.firmware
prod_keys = "%s" % args.keys

if firmware == "None":
    try:
        firmware = sys.argv[1]
    except:
        firmware = "firmware"

if prod_keys == "None":
    try:
        prod_keys = sys.argv[2] 
    except:
        prod_keys = "prod.keys"

Now when you run the menu.py you can call the scripts and just drag the firmware folder onto the menu to get the path.

borntohonk commented 4 months ago

python = "python" package = 'argparse' package2 = 'shutils'

both argparse (since 3.2) and shutil (3.4) are default binaries in python 3, there's no need for end-users to grab it off of pip if they use common python installs like 3.8+ (i personally use 3.11)

borntohonk commented 4 months ago

(running the script on a fresh python 3.11 install on a machine that has never had python installed nor any pip modules installed, runs the script just fine)

argparase can be removed if one just removes the arguments and forces the end user to put files in folder named firmware, and the output to always be prod.keys, as example

and the checks i added for the repository can also be removed (they are mostly there as guardrails for people who stumble upon the script and fill in incorrect keys, and the keyfile inputs are kind of pointless.)

I also notice you have an erista_keygen option, you should remove that, as it relies on tsec_secret_26, which is not public and methods to obtain it are not simple.

if one completely disregards that people might use incorrect keys and have it be their problem, everything can be extremely simplified.

if simplified like this you should just run your script that runs this script into just renaming prod.keys / moving it after it's done running.

import subprocess, re, os, shutil

with open('temp.keys', 'w') as temp_keys:
    temp_keys.write(f'mariko_bek = 6A5DXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
    temp_keys.write(f'mariko_kek = 4130XXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
    temp_keys.write(f'master_key_00 = C2CAXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
    temp_keys.close()
    subprocess.run(f'hactoolnet --keyset temp.keys -t switchfs firmware --title 0100000000000819 --romfsdir 0100000000000819/romfs/', stdout = subprocess.DEVNULL)
    subprocess.run(f'hactoolnet --keyset temp.keys -t pk11 0100000000000819/romfs/a/package1 --outdir 0100000000000819/romfs/a/pkg1', stdout = subprocess.DEVNULL)
    with open('0100000000000819/romfs/a/pkg1/Decrypted.bin', 'rb') as decrypted_bin:
        secmon_data = decrypted_bin.read()
        result = re.search(b'\x4F\x59\x41\x53\x55\x4D\x49', secmon_data)
        byte_alignment = decrypted_bin.seek(result.end() + 0x32)
        mariko_master_kek_source_key = decrypted_bin.read(0x10).hex().upper()
        byte_alignment = decrypted_bin.seek(0x150)
        revision = decrypted_bin.read(0x01).hex().upper()
        decrypted_bin.close()
        with open('temp.keys', 'a') as temp_keys:
            temp_keys.write(f'\nmariko_master_kek_source_{int(revision) - 0x1} = {mariko_master_kek_source_key}')
            temp_keys.close()
            with open('prod.keys', 'w') as keygen:
                subprocess.run(f'hactoolnet --keyset temp.keys -t keygen', stdout=keygen)
                keygen.close()
                os.remove('temp.keys')
                shutil.rmtree('0100000000000819')
                print(f'# Keygen completed and output to prod.keys, exiting.')
                exit()
borntohonk commented 4 months ago

Similarily, a simplified version of make_patches.py with the simplified keygen built-in, would look like this, with all checks to prevent failure / debug warnings on regexes/ stop duplicates from being attempted made removed, and version is now extracted from https://switchbrew.org/wiki/System_Version_Title, from the files you've put in 'firmware'

(it does a hacky approach for loader_patches.ini and fs_patches.ini where it appends a blank line if the file exists, and creates a new file with blank line if it doesn't, for compatability, those lines can be removed if it's guaranteed the end user will run with those files present)

instructions: python make_patches.py

import re, subprocess, shutil, hashlib, os, pathlib

pathlib.Path('patches/atmosphere/exefs_patches/ams_blanker_fix').mkdir(parents=True, exist_ok=True)
pathlib.Path('patches/atmosphere/exefs_patches/es_patches').mkdir(parents=True, exist_ok=True)
pathlib.Path('patches/atmosphere/exefs_patches/nifm_ctest').mkdir(parents=True, exist_ok=True)
pathlib.Path('patches/bootloader').mkdir(parents=True, exist_ok=True)
pathlib.Path('hekate_patches').mkdir(parents=True, exist_ok=True)

with open('hekate_patches/header.ini', 'w') as header_ini:
    header_ini.write(f'# UTF-8\n')
    header_ini.write(f'# A KIP section is [kip1_name:sha256_hex_8bytes]\n')
    header_ini.write(f'# A patchset is .patch_name=kip_section_dec:offset_hex_0x:length_hex_0x:src_data_hex,dst_data_hex\n')
    header_ini.write(f'# _dec: 1 char decimal | _hex_0x: max u32 prefixed with 0x | _hex: hex array.\n')
    header_ini.write(f'# Kip1 section decimals: TEXT: 0, RODATA: 1, DATA: 2.\n')
    header_ini.close()

with open('hekate_patches/fs_patches.ini', 'a') as fs_patches_ini:
    fs_patches_ini.write('\n')
    fs_patches_ini.close()

with open('hekate_patches/loader_patches.ini', 'a') as loader_patches_ini:
    loader_patches_ini.write('\n')
    loader_patches_ini.close()

print("# initiating keygen")
with open('temp.keys', 'w') as temp_keys:
    temp_keys.write(f'mariko_bek = 6A5DXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
    temp_keys.write(f'mariko_kek = 4130XXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
    temp_keys.write(f'master_key_00 = C2CAXXXXXXXXXXXXXXXXXXXXXXXXXXXX\n')
    temp_keys.close()
    subprocess.run(f'hactoolnet -k temp.keys -t switchfs firmware --title 0100000000000819 --romfsdir 0100000000000819/romfs/', stdout = subprocess.DEVNULL)
    subprocess.run(f'hactoolnet -k temp.keys -t pk11 0100000000000819/romfs/a/package1 --outdir 0100000000000819/romfs/a/pkg1', stdout = subprocess.DEVNULL)
    with open('0100000000000819/romfs/a/pkg1/Decrypted.bin', 'rb') as decrypted_bin:
        secmon_data = decrypted_bin.read()
        result = re.search(b'\x4F\x59\x41\x53\x55\x4D\x49', secmon_data)
        byte_alignment = decrypted_bin.seek(result.end() + 0x32)
        mariko_master_kek_source_key = decrypted_bin.read(0x10).hex().upper()
        byte_alignment = decrypted_bin.seek(0x150)
        revision = decrypted_bin.read(0x01).hex().upper()
        decrypted_bin.close()
        with open('temp.keys', 'a') as temp_keys:
            temp_keys.write(f'\nmariko_master_kek_source_{int(revision) - 0x1} = {mariko_master_kek_source_key}')
            temp_keys.close()
            with open('prod.keys', 'w') as keygen:
                subprocess.run(f'hactoolnet -k temp.keys -t keygen', stdout=keygen)
                keygen.close()
                os.remove('temp.keys')
                shutil.rmtree('0100000000000819')
                print(f'# Keygen completed and output to prod.keys')

escompressed = f'firmware/titleid/0100000000000033/exefs/main'
nifmcompressed = f'firmware/titleid/010000000000000f/exefs/main'
nimcompressed = f'firmware/titleid/0100000000000025/exefs/main'
esuncompressed = f'firmware/titleid/0100000000000033/exefs/u_main'
nifmuncompressed = f'firmware/titleid/010000000000000f/exefs/u_main'
nimuncompressed = f'firmware/titleid/0100000000000025/exefs/u_main'
fat32compressed = f'firmware/titleid/0100000000000819/romfs/nx/ini1/FS.kip1'
exfatcompressed = f'firmware/titleid/010000000000081b/romfs/nx/ini1/FS.kip1'
fat32uncompressed = f'firmware/titleid/0100000000000819/romfs/nx/ini1/u_FS.kip1'
exfatuncompressed = f'firmware/titleid/010000000000081b/romfs/nx/ini1/u_FS.kip1'

subprocess.run(f'hactoolnet -k prod.keys -t switchfs firmware --title 0100000000000809 --romfsdir firmware/titleid/0100000000000809/romfs/', stdout = subprocess.DEVNULL)
with open('firmware/titleid/0100000000000809/romfs/file', 'rb') as get_version:
        byte_alignment = get_version.seek(0x68)
        read_version_number = get_version.read(0x6).hex().upper()
        version = (bytes.fromhex(read_version_number).decode('utf-8'))
        print(f"# Firmware version number: {version}")

print('# extracting components')
subprocess.run(f'hactoolnet -k prod.keys -t switchfs firmware --title 0100000000000033 --exefsdir firmware/titleid/0100000000000033/exefs/', stdout = subprocess.DEVNULL)
subprocess.run(f'hactool --disablekeywarns -k prod.keys -t nso0 {escompressed} --uncompressed={esuncompressed}', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t switchfs firmware --title 010000000000000f --exefsdir firmware/titleid/010000000000000f/exefs/', stdout = subprocess.DEVNULL)
subprocess.run(f'hactool --disablekeywarns -k prod.keys -t nso0 {nifmcompressed} --uncompressed={nifmuncompressed}', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t switchfs firmware --title 0100000000000025 --exefsdir firmware/titleid/0100000000000025/exefs/', stdout = subprocess.DEVNULL)
subprocess.run(f'hactool --disablekeywarns -k prod.keys -t nso0 {nimcompressed} --uncompressed={nimuncompressed}', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t switchfs firmware --title 0100000000000819 --romfsdir firmware/titleid/0100000000000819/romfs/', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t pk21 firmware/titleid/0100000000000819/romfs/nx/package2 --ini1dir firmware/titleid/0100000000000819/romfs/nx/ini1', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t kip1 {fat32compressed} --uncompressed {fat32uncompressed}', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t switchfs firmware --title 010000000000081b --romfsdir firmware/titleid/010000000000081b/romfs/', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t pk21 firmware/titleid/010000000000081b/romfs/nx/package2 --ini1dir firmware/titleid/010000000000081b/romfs/nx/ini1', stdout = subprocess.DEVNULL)
subprocess.run(f'hactoolnet -k prod.keys -t kip1 {exfatcompressed} --uncompressed {exfatuncompressed}', stdout = subprocess.DEVNULL)

def get_es_build_id():
    with open(escompressed, 'rb') as f:
        f.seek(0x40)
        return f.read(0x14).hex().upper()

def get_nifm_build_id():
    with open(nifmcompressed, 'rb') as f:
        f.seek(0x40)
        return f.read(0x14).hex().upper()

def get_nim_build_id():
    with open(nimcompressed, 'rb') as f:
        f.seek(0x40)
        return f.read(0x14).hex().upper()

esbuildid = get_es_build_id()
nifmbuildid = get_nifm_build_id()
nimbuildid = get_nim_build_id()
fat32hash = hashlib.sha256(open(f'{fat32compressed}', 'rb').read()).hexdigest().upper()
exfathash = hashlib.sha256(open(f'{exfatcompressed}', 'rb').read()).hexdigest().upper()

print("# making patches")
with open(f'{esuncompressed}', 'rb') as decompressed_es_nso:
    read_data = decompressed_es_nso.read()
    result = re.search(rb'.\x00\x91.{2}\x00\x94\xa0\x83\x00\xd1.{2}\xff\x97', read_data)
    patch = '%06X%s%s' % (result.end(), '0004', 'E0031FAA')
    offset = '%06X' % (result.end())
    text_file = open('./patches/atmosphere/exefs_patches/es_patches/%s.ips' % esbuildid, 'wb')
    text_file.write(bytes.fromhex(str(f'5041544348{patch}454F46')))
    text_file.close()
decompressed_es_nso.close()

with open(f'{nifmuncompressed}', 'rb') as decompressed_nifm_nso:
    read_data = decompressed_nifm_nso.read()
    result = re.search(rb'.{20}\xf4\x03\x00\xaa.{4}\xf3\x03\x14\xaa\xe0\x03\x14\xaa\x9f\x02\x01\x39\x7f\x8e\x04\xf8', read_data)
    text_file = open('./patches/atmosphere/exefs_patches/nifm_ctest/%s.ips' % nifmbuildid, 'wb')
    patch = '%06X%s%s' % (result.start(), '0014', '00309AD2001EA1F2610100D4E0031FAAC0035FD6')
    offset = '%06X' % (result.start())
    text_file.write(bytes.fromhex(str(f'5041544348{patch}454F46')))
    text_file.close()
decompressed_nifm_nso.close()

with open(f'{nimuncompressed}', 'rb') as decompressed_nim_nso:
    read_data = decompressed_nim_nso.read()
    result = re.search(rb'\x80\x0f\x00\x35\x1f\x20\x03\xd5', read_data)
    text_file = open('./patches/atmosphere/exefs_patches/ams_blanker_fix/%s.ips' % nimbuildid, 'wb')
    patch = '%06X%s%s' % (result.end(), '0004', 'E2031FAA')
    offset = '%06X' % (result.end())
    text_file.write(bytes.fromhex(str(f'5041544348{patch}454F46')))
    text_file.close()
decompressed_nim_nso.close()

with open(f'{fat32uncompressed}', 'rb') as fat32f:
    read_data = fat32f.read()
    result1 = re.search(rb'\x52.{3}\x52.{3}\x52.{3}\x52.{3}\x94', read_data)
    result2 = re.search(rb'\x08\x1C\x00\x12\x1F\x05\x00\x71\x41\x01\x00\x54', read_data)
    patch1 = '%06X%s%s' % (result1.end(), '0004', '1F2003D5')
    patch2 = '%06X%s%s' % (result2.start() - 0x4, '0004', 'E0031F2A')
    fat32_hekate = open('./hekate_patches/fs_patches.ini', 'a')
    fat32_hekate.write(f'\n#FS {version}-fat32\n')
    fat32_hekate.write(f'[FS:{fat32hash[:16]}]\n')
    byte_alignment = fat32f.seek(result1.end())
    fat32_hekate.write('.nosigchk=0:0x' + '%06X' % (result1.end()-0x100) + ':0x4:' + fat32f.read(0x4).hex().upper() + ',1F2003D5\n')
    byte_alignment = fat32f.seek(result2.start() - 0x4)
    fat32_hekate.write('.nosigchk=0:0x' + '%06X' % (result2.start()-0x104) + ':0x4:' + fat32f.read(0x4).hex().upper() + ',E0031F2A\n')
    fat32_hekate.close()
fat32f.close()

with open(f'{exfatuncompressed}', 'rb') as exfatf:
    read_data = exfatf.read()
    result1 = re.search(rb'\x52.{3}\x52.{3}\x52.{3}\x52.{3}\x94', read_data)
    result2 = re.search(rb'\x08\x1C\x00\x12\x1F\x05\x00\x71\x41\x01\x00\x54', read_data)
    patch1 = '%06X%s%s' % (result1.end(), '0004', '1F2003D5')
    patch2 = '%06X%s%s' % (result2.start() - 0x4, '0004', 'E0031F2A')
    exfat_hekate = open('./hekate_patches/fs_patches.ini', 'a')
    exfat_hekate.write(f'\n#FS {version}-exfat\n')
    exfat_hekate.write(f'[FS:{exfathash[:16]}]\n')
    byte_alignment = exfatf.seek(result1.end())
    exfat_hekate.write('.nosigchk=0:0x' + '%06X' % (result1.end()-0x100) + ':0x4:' + exfatf.read(0x4).hex().upper() + ',1F2003D5\n')
    byte_alignment = exfatf.seek(result2.start() - 0x4)
    exfat_hekate.write('.nosigchk=0:0x' + '%06X' % (result2.start()-0x104) + ':0x4:' + exfatf.read(0x4).hex().upper() + ',E0031F2A\n')
    exfat_hekate.close()
exfatf.close()

with open('./patches/bootloader/patches.ini', 'wb') as outfile:
    for filename in ['./hekate_patches/header.ini', './hekate_patches/fs_patches.ini', './hekate_patches/loader_patches.ini']:
        with open(filename, 'rb') as readfile:
            shutil.copyfileobj(readfile, outfile)

shutil.rmtree('firmware/titleid')
print(f'# done, patches created, /patches/bootloader/patches.ini, should now have values for {version}, and .ips patches for {version} are also present')
mrdude2478 commented 4 months ago

Nice, I can see you've been a busy bee today. I was was also updating my program and added in the keygen stuff:

Screenshot

It's working well but now I might add the version info stuff you added, although it can already tell what sdk the firmware was built from and when bootloader version it is from. Thanks for updating your code, i'm learning from it.

mrdude2478 commented 4 months ago

Oppps I'm an idiot, I thought you had an issue with your script but I never had hactool in the folder I was running it from, I see you are using both hactoolnet and hactool now.

borntohonk commented 4 months ago

Oppps I'm an idiot, I thought you had an issue with your script but I never had hactool in the folder I was running it from, I see you are using both hactoolnet and hactool now.

Unfortunately hactoolnet cannot decompress .nso0, (exefs/main), so hactool is used, could always also use nxo64 py import and decompress that way, but I prefer using hactool/hactoolnet

mrdude2478 commented 4 months ago

@borntohonk

Now since you are using hactool and you are already opening TitleID: 0100000000000809, you can kill two birds with one stone and get the master key revision from that:

ContentType: Data
TitleID: 0100000000000809
NCA: 3d2e5985a13bbb3865739ce9dba73d2a.nca

hactool -k prod.keys -i firmware/3d2e5985a13bbb3865739ce9dba73d2a.nca

Content Size:                       0x000000015000
Title ID:                           0100000000000809
SDK Version:                        18.3.0.0
Distribution type:                  Download
Content Type:                       Data
Master Key Revision:                0x11 (18.0.0-18.x.x)
Encryption Type:                    Standard crypto

Maybe that will be more future proof than looking at the bootloader version? I think all hactool does is check what key was needed to decypyt it and that's how you get the master key revision, but I might be wrong.

borntohonk commented 4 months ago

Now since you are using hactool and you are already opening TitleID: 0100000000000809, you can kill two birds with one stone and get the master key revision from that: Maybe that will be more future proof than looking at the bootloader version? I think all hactool does is check what key was needed to decypyt it and that's how you get the master key revision, but I might be wrong.

That's not needed for make_patches.py and for keygen point of view, you need keys you don't have yet (potentially) to open that nca, which makes it redundant.

bootloader version and sdk versions also are misleading.

We've already implemented enough code to determine versions and key revisions that i'm satisfied with, that is extracted directly from persistent nca's at persistent offsets that are hardcoded into package1/ decrypted secmon / the "System Version Title" nca 0100000000000809 which is what feeds the version string to your system settings on your console.

0x68 | 0x18 | Display version (System-version in string form with zeros afterwards, e.g. "2.1.0"). This is what is displayed in System settings

https://switchbrew.org/wiki/System_Version_Title

borntohonk commented 4 months ago

if nxo64.py is readded in the same folder, one can replace the hactool strings with this, but it adds a dependency on lz4 from pip (pip install lz4) (effectively removing the need for hactool, but adding the need for LZ4 from pip)

or alternatively just add the imports and definitions to make_patches.py and remove the nxo64 prefix to commands ( nxo64.write_file -> write_file, nxo64.decompress_nso() - decompress_nso() )

code additions/replacements for hactool lines:

import nxo64

with open(f'{nimcompressed}', 'rb') as compressed_nim_nso:
    nxo64.write_file(f'{nimuncompressed}', nxo64.decompress_nso(compressed_nim_nso))
with open(f'{nifmcompressed}', 'rb') as compressed_nifm_nso:
    nxo64.write_file(f'{nifmuncompressed}', nxo64.decompress_nso(compressed_nifm_nso))
with open(f'{escompressed}', 'rb') as compressed_es_nso:
    nxo64.write_file(f'{esuncompressed}', nxo64.decompress_nso(compressed_es_nso))

nxo64.py to be stored in the same folder as make_patches.py:

#!/usr/bin/env python3

# The following is adapted from https://github.com/reswitched/loaders/blob/master/nxo64.py
#
# ===========================================================================================
#
# Copyright 2017 Reswitched Team
#
# Permission to use, copy, modify, and/or distribute this software for any purpose with or
# without fee is hereby granted, provided that the above copyright notice and this permission
# notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
# SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
# THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE
# OR PERFORMANCE OF THIS SOFTWARE.

import os
from struct import calcsize, unpack, unpack_from
from typing import Union

# pip install lz4
import lz4.block

uncompress = lz4.block.decompress

def write_file(filename: str, data: Union[bytes, bytearray]) -> None:
    with open(filename, 'wb') as f:
        f.write(data)

class BinFile(object):
    def __init__(self, li):
        self._f = li

    def read(self, arg):
        if isinstance(arg, str):
            fmt = '<' + arg
            size = calcsize(fmt)
            raw = self._f.read(size)
            out = unpack(fmt, raw)
            if len(out) == 1:
                return out[0]
            return out
        elif arg is None:
            return self._f.read()
        else:
            out = self._f.read(arg)
            return out

    def read_from(self, arg, offset):
        old = self.tell()
        try:
            self.seek(offset)
            out = self.read(arg)
        finally:
            self.seek(old)
        return out

    def seek(self, off):
        self._f.seek(off)

    def close(self):
        self._f.close()

    def tell(self):
        return self._f.tell()

def decompress_nso(fileobj):
    f = BinFile(fileobj)

    if f.read_from('4s', 0) != b'NSO0':
        raise Exception('Invalid NSO magic')

    toff, tloc, tsize = f.read_from('3I', 0x10)
    roff, rloc, rsize = f.read_from('3I', 0x20)
    doff, dloc, dsize = f.read_from('3I', 0x30)

    tfilesize, rfilesize, dfilesize = f.read_from('3I', 0x60)
    bsssize = f.read_from('I', 0x3C)

    text = uncompress(f.read_from(tfilesize, toff), uncompressed_size=tsize)
    ro = uncompress(f.read_from(rfilesize, roff), uncompressed_size=rsize)
    data = uncompress(f.read_from(dfilesize, doff), uncompressed_size=dsize)

    full = text
    if rloc >= len(full):
        full += b'\0' * (rloc - len(full))
    else:
        full = full[:rloc]
    full += ro
    if dloc >= len(full):
        full += b'\0' * (dloc - len(full))
    else:
        full = full[:dloc]
    full += data

    return full
mrdude2478 commented 3 months ago

Nice, I use that file for IDA and I have lz4 installed already.

On another note I finished up adding the keygen stuff to IPS Patch Creator 1.58 if you want to test it out. Where can I message you with a link to download it. I don't want to post on a public forum as it's got some keys embedded in the program.

borntohonk commented 3 months ago

Nice, I use that file for IDA and I have lz4 installed already.

On another note I finished up adding the keygen stuff to IPS Patch Creator 1.58 if you want to test it out. Where can I message you with a link to download it. I don't want to post on a public forum as it's got some keys embedded in the program.

I normally just run cli command, and latest commit is how I actually use it, I generate patches for an atmosphere fork with embedded patches instead of .ips or hekate patches like this https://github.com/borntohonk/Atmosphere/commit/63f65a4e4210baa3e71504c68adccb50041c7cc1

mrdude2478 commented 3 months ago

Wow, that's excellnt - I didn't know you could do it like that. I've never seen it done that way before. Thanks for the link, I'll definately check that out when I get some time. :-)

mrdude2478 commented 3 months ago

@borntohonk

Can you also make a dev keygen, for example:

mariko_master_kek_dev_source_11 = e445xxxx

Found in: \0100000000000819\romfs\a\pkg1\Decrypted.bin Look for hex: 4F594153554D49 - 0x22 after this find the 0x10 long dev key

So pretty much the same as you have but only 10 bytes before the mariko_master_kek__source_11 key which is 0x32 after 0x4F594153554D49.

Thanks for looking.

borntohonk commented 3 months ago

@borntohonk

Can you also make a dev keygen, for example:

mariko_master_kek_dev_source_11 = e445xxxx

Found in: \0100000000000819\romfs\a\pkg1\Decrypted.bin Look for hex: 4F594153554D49 - 0x22 after this find the 0x10 long dev key

So pretty much the same as you have but only 10 bytes before the mariko_master_kek__source_11 key which is 0x32 after 0x4F594153554D49.

Thanks for looking.

I have added this to just mariko_keygen.py, one now calls a -d or --dev to generate a dev keyset, default to dev.keys, or if both -d/--dev and -k/--keys is provided at same time it will output it elsewhere. and i will be closing this issue because it is becoming a tiny bit cluttered/off-scope.

borntohonk commented 3 months ago

(i accidentally pushed local unfinished code, but I force-pushed the correct code now, which adds devkeyset keygen support)

mrdude2478 commented 3 months ago

Thanks.