Skip to content

Instantly share code, notes, and snippets.

@jakibaki
Forked from khang06/0-SD-GUIDE.md
Created June 18, 2018 14:44
Show Gist options
  • Save jakibaki/af69eaa8f1920f954f54a98151215320 to your computer and use it in GitHub Desktop.
Save jakibaki/af69eaa8f1920f954f54a98151215320 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 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/your.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.

  1. It's not over yet! The dumped NCA is still title key encrypted. Run hactool -k path/to/your.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/your.keys game.nca --plaintext=game_decrypted.nca --titlekey=put_your_title_key_here
  1. You're done! Now you can do whatever you want with that decrypted NCA.

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

# 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