Skip to content

Instantly share code, notes, and snippets.

@MarioMasta64
Forked from khang06/0-SD-GUIDE.md
Created July 4, 2018 15:41
Show Gist options
  • Save MarioMasta64/24f31b36597cf49cfc4cabb45617e50a to your computer and use it in GitHub Desktop.
Save MarioMasta64/24f31b36597cf49cfc4cabb45617e50a to your computer and use it in GitHub Desktop.
Switch SD Dumping 101

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.

Obtaining Your SD Seed

  1. Open sd:/Nintendo/contents/private in a hex editor.
  2. Copy the hex representation of it and put it somewhere for later.
  3. Mount your NAND's SYSTEM partition.
  4. Open /save/8000000000000043 in a hex editor.
  5. Search for the contents of private.
  6. 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. Copy /save/80000000000000e1 and /save/80000000000000e2 to your computer.
  3. Run both files using through get_ticketbins.py. This should give you a personal_ticketblob.bin and common_ticketblob.bin.
python get_ticketbins.py 80000000000000e1
python get_ticketbins.py 80000000000000e2
  1. Run get_titlekeys.py with the first argument being a raw decrypted backup of your PRODINFO.bin and the second being a ticketblob.
python get_titlekeys.py /path/to/PRODINFO.bin personal_ticketblob.bin
python get_titlekeys.py /path/to/PRODINFO.bin common_ticketblob.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.

On certain titles, hactool will complain about sectors as of version 1.1.0. A patch has been merged into the repo, but a release has yet to be made as of this guide. Also, one of the post-1.1.0 commits break some decryption on Windows.

My build has both of these patched out. If a build after 1.1.0 is released, please tell me. https://drive.google.com/file/d/1bfbRQujuGDQrgo3tF-jsEf4Gv1NK9r2o/view?usp=sharing

  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.

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.

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

# SocraticBliss (R)
import os, sys
def read_at(fp, off, len):
fp.seek(off)
return fp.read(len)
def main(argc, argv):
if argc != 2:
print('Usage: %s 80000000000000XX' % argv[0])
return 1
try:
with open(sys.argv[1], "rb") as file:
# Determine if this is a Common or Personal Ticket Blob
if sys.argv[1].upper() == ('80000000000000E1'):
ticketType = 'common'
elif sys.argv[1].upper() == ('80000000000000E2'):
ticketType = 'personal'
else:
ticketType = 'unknown'
# Remove previous entries
try:
os.remove('%s_ticketblob.bin' % (ticketType))
except OSError:
pass
count = 0
fileSize = os.path.getsize(argv[1])
# Find first occurance of a Ticket
for x in xrange(0, fileSize, 0x100):
if read_at(file, x, 4) == b"\x04\x00\x01\x00":
ticketStart = x
break
# Iterate through the Ticket Blob
for i in xrange(ticketStart, fileSize, 0x400):
if read_at(file, i, 4) == b"\x04\x00\x01\x00":
count += 1
tik_block = read_at(file, i, 0x400)
with open('%s_ticketblob.bin' % (ticketType), 'a+b') as outfile:
outfile.write(tik_block)
except:
print('Failed to open %s!' % argv[1])
return 1
print('Saved all %d tickets to %s_ticketblob.bin' % (count, ticketType))
return 0
if __name__=='__main__':
sys.exit(main(len(sys.argv), sys.argv))
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))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment