Skip to content

Instantly share code, notes, and snippets.

@khang06
Last active April 19, 2024 07:41
Show Gist options
  • Star 37 You must be signed in to star a gist
  • Fork 7 You must be signed in to fork a gist
  • Save khang06/84aabeac507fa99a676d22bb6120cea8 to your computer and use it in GitHub Desktop.
Save khang06/84aabeac507fa99a676d22bb6120cea8 to your computer and use it in GitHub Desktop.
Switch SD Dumping 101

https://gbatemp.net/threads/nintendo-switch-sd-to-nsp-dumper.514816/ for a more automated and easier way to do this

This guide assumes you have previous experience with hactool and messing with your NAND. You aren't supposed to blindly copy commands in this, so read before pasting!

Also, the Python sections require Python 2.7 and pycrypto. Make sure your hactool is v1.2 or above.

Obtaining Your SD Seed

  1. Run https://cdn.discordapp.com/attachments/432400335235973120/478053328857726976/Compelled-Disclosure.nro (source at https://github.com/shadowninja108/Compelled-Disclosure, thx Shadów#6239)
  2. Rename the bin files in sd:/switch/whateverfolderyouputthedumperin/80000000000000e1 and 80000000000000e2 to whatever you want and copy them to your computer. Also copy the private file in 8000000000000043 to your computer.
  3. Open sd:/Nintendo/contents/private in a hex editor.
  4. Copy the hex representation of it.
  5. Open the private from 8000000000000043 in a hex editor.
  6. Search for the contents of the private from the contents folder.
  7. Copy the 16 bytes after that. This is your SD seed. Don't lose it!

Obtaining Your Title Keys

  1. Replace put_eticket_rsa_kek_here in get_titlekeys.py with the actual eticket_rsa_kek.
  2. Run get_titlekeys.py with the first argument being a raw decrypted backup of your PRODINFO.bin and the second being a ticketblob (those bin files in the e1 and e2 folders). Make sure to run it on both to get all keys!
python get_titlekeys.py /path/to/PRODINFO.bin ticket.bin
  1. Save the outputs somewhere safe. These are your title keys! If you buy another title and want to dump it, you'll have to do these steps again.

Decrypting (the hard part)

  1. Open sd:/Nintendo/Contents/registered. There should be a lot of folders with hexadecimal names. (e.g. 0000004C)
  2. Use a tool like WizTree to find the sizes of each folder. This can help pinpoint what title you should dump. Taking a look at the creation dates can help, too.
  3. Time for the part everyone messes up:

Let's say the title you want to dump is at F:/Nintendo/Contents/registered/00001337/cafebebecafebebecafebebecafebebe.nca/00.

The command you would write would look something like this:

hactool -k path/to/prod.keys -t nax0 --sdseed=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX --sdpath="/registered/00001337/cafebebecafebebecafebebecafebebe.nca" --plaintext=game.nca "F:/Nintendo/Contents/registered/00001337/cafebebecafebebecafebebecafebebe.nca/00"

If it works, great! If you get Error: NAX0 key derivation failed. Check SD card seed and relative path?, you probably messed up typing the command.

  1. It's not over yet! The dumped NCA is still title key encrypted. Run hactool -k path/to/prod.keys your.nca. Since it's encrypted, hactool will complain about it being corrupted.
  2. Check the output for the Rights ID. For example, Splatoon 2 USA would say Rights ID: 01003BC0000A00000000000000000000.
  3. Look for the corresponding title key in your title key dump.
  4. Finally, run this command:
hactool -k path/to/prod.keys game.nca --titlekey=put_your_title_key_here
  1. If it doesn't complain about corruption, you're done! You can extract exefs and romfs the usual ways, but you need to put in the titlekey flag.

Shoutouts to Simpleflips whoever writes those python scripts. You guys are the best!

Bonuses

That hactool build was cloned from commit 9d9b781 with commit 94d55a9 cherry-picked and was compiled on MSYS2 with the config.mk.template. (or at least until I removed it because hactool 1.2 launched with that regression fixed)

Game updates can be extracted with hactool -k path/to/prod.keys game_update.nca --titlekey=update_titlekey --basenca=game.nca --romfsdir=updatedgameromfs --exefsdir=updatedgameexefs where game_update is the update NCA, update_titlekey is the title key for the update, and game.nca is the original, unpatched NCA for the game. The dump will be the update combined with the base.

You can dump titles from System Memory by mounting your USER partition. The layout is similar, but the files aren't NAX0 files. Instead, they're just normal NCAs. They can be decrypted with the correct master key and you still need the title key.

Changelog

v2: add jakibaki's modified get_ticketbins

v3: hactool doesn't seem to support dumping plaintext from titlekeys and i put in my own hactool build

v4: add some bonuses

v5: clarify keys

v6: stuff

v7: add a better method of getting the ticketbins

v8: nobody reads this changelog just go look in revisions

import os, sys
from struct import unpack as up, pack as pk
from binascii import unhexlify as uhx, hexlify as hx
from Crypto.Cipher import AES
from Crypto.Util import Counter
import hashlib
# Note: Insert correct RSA kek here, or disable correctness enforcement.
enforce_rsa_kek_correctneess = True
rsa_kek = uhx('put_eticket_rsa_kek_here')
def safe_open(path, mode):
import os
dn = os.path.split(path)[0]
try:
os.makedirs(dn)
except OSError:
if not os.path.isdir(dn):
raise
except WindowsError:
if not os.path.isdir(dn):
raise
return open(path, mode)
def hex2ctr(x):
return Counter.new(128, initial_value=int(x, 16))
def b2ctr(x):
return Counter.new(128, initial_value=int(hx(x), 16))
def read_at(fp, off, len):
fp.seek(off)
return fp.read(len)
def read_u8(fp, off):
return up('<B', read_at(fp, off, 1))[0]
def read_u16(fp, off):
return up('<H', read_at(fp, off, 2))[0]
def read_u32(fp, off):
return up('<I', read_at(fp, off, 4))[0]
def read_u64(fp, off):
return up('<Q', read_at(fp, off, 8))[0]
def read_str(fp, off, l):
if l == 0:
return ''
s = read_at(fp, off, l)
if '\0' in s:
s = s[:s.index('\0')]
return s
def sxor(x, y):
return ''.join([chr(ord(a) ^ ord(b)) for a,b in zip(x,y)])
def MGF1(seed, mask_len, hash=hashlib.sha256):
mask = ''
i = 0
while len(mask) < mask_len:
mask += hash(seed + pk('>I', i)).digest()
i += 1
return mask[:mask_len]
def get_rsa_keypair(cal0):
if read_at(cal0, 0, 4) != 'CAL0':
print 'Invalid CAL0 magic!'
sys.exit(1)
if read_at(cal0, 0x20, 0x20) != hashlib.sha256(read_at(cal0, 0x40, read_u32(cal0, 0x8))).digest():
print 'Invalid CAL0 hash!'
sys.exit(1)
dec = AES.new(rsa_kek, AES.MODE_CTR, counter=b2ctr(read_at(cal0, 0x3890, 0x10))).decrypt(read_at(cal0, 0x38A0, 0x230))
D = int(hx(dec[:0x100]), 0x10)
N = int(hx(dec[0x100:0x200]), 0x10)
E = int(hx(dec[0x200:0x204]), 0x10)
if E != 0x10001:
print '[WARN]: Public Exponent is not 65537. rsa_kek is probably wrong.'
if pow(pow(0xCAFEBABE, D, N), E, N) != 0xCAFEBABE:
print 'Failed to verify ETicket RSA keypair!'
print 'Decrypted key was %s' % hx(dec)
sys.exit(1)
return (E, D, N)
def extract_titlekey(S, kp):
E, D, N = kp
M = uhx('%0512X' % pow(S, D, N))
M = M[0] + sxor(M[1:0x21], MGF1(M[0x21:], 0x20)) + sxor(M[0x21:], MGF1(sxor(M[1:0x21], MGF1(M[0x21:], 0x20)), 0xDF))
pref, salt, DB = M[0], M[1:0x21], M[0x21:]
if pref != '\x00':
return None
label_hash, DB = DB[:0x20], DB[0x20:]
if label_hash != uhx('E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855'):
return None
for i in xrange(1, len(DB)):
if DB.startswith('\x00'*i + '\x01'):
return DB[i+1:]
return None
def get_titlekeys(tik, tik_size, kp):
if tik_size & 0x3FF:
print 'Invalid ticket binary!'
sys.exit(1)
num_tiks = tik_size >> 10
for i in xrange(num_tiks):
ofs = i << 10
CA = read_at(tik, ofs + 0x140, 4)
if CA == '\x00'*4:
continue
if CA != 'Root':
print 'Unknown Ticket verifier: %s' % read_str(tik, ofs + 0x140, 0x40)
tkey_block = read_at(tik, ofs + 0x180, 0x100)
if tkey_block[0x10:] == '\x00'*0xF0:
# Common Ticket
titlekey = tkey_block[:0x10]
else:
# Personalized Ticket
titlekey = extract_titlekey(int(hx(tkey_block), 16), kp)
if titlekey is not None:
print 'Ticket %d:' % i
print ' Rights ID: %s' % hx(read_at(tik, ofs + 0x2A0, 0x10))
print ' Title ID: %s' % hx(read_at(tik, ofs + 0x2A0, 8))
print ' Titlekey: %s' % hx(titlekey)
return
def main(argc, argv):
if argc != 3:
print 'Usage: %s CAL0 ticket.bin' % argv[0]
return 1
if enforce_rsa_kek_correctneess and hashlib.sha256(rsa_kek).hexdigest().upper() != '46CCCF288286E31C931379DE9EFA288C95C9A15E40B00A4C563A8BE244ECE515':
print 'Error: rsa_kek is incorrect (hash mismatch detected)'
print 'Please insert the correct rsa_kek at the top of the script.'
return 1
try:
cal0 = open(argv[1], 'rb')
kp = get_rsa_keypair(cal0)
cal0.close()
except:
print 'Failed to open %s!' % argv[1]
return 1
try:
tik = open(argv[2], 'rb')
get_titlekeys(tik, os.path.getsize(argv[2]), kp)
tik.close()
except:
print 'Failed to open %s!' % argv[2]
return 1
print 'Done!'
return 0
if __name__=='__main__':
sys.exit(main(len(sys.argv), sys.argv))
@dm4uz3
Copy link

dm4uz3 commented Jun 19, 2018

damn

Copy link

ghost commented Jun 30, 2018

lolwut
Unfortunatley, Spai is dumb...

@homebrewGT
Copy link

homebrewGT commented Jul 2, 2018

If the update .NCA is divided into 3 files... how to merge them into one? also doesn't have only one .NCA folder, it has two.

Help.

@lallousx86
Copy link

@davisrayler, use some Batchography tricks:

copy /b 00+01+02+03 single.nca

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment