def __init__(self, sd_dir: str, movable: str, dev: bool = False, readonly: bool = False): self.crypto = CryptoEngine(dev=dev) with open(movable, 'rb') as mv: mv.seek(0x110) key_y = mv.read(0x10) key_hash = sha256(key_y).digest() hash_parts = unpack('<IIII', key_hash[0:16]) self.root_dir = f'{hash_parts[0]:08x}{hash_parts[1]:08x}{hash_parts[2]:08x}{hash_parts[3]:08x}' if not os.path.isdir(sd_dir + '/' + self.root_dir): exit(f'Failed to find {self.root_dir} in the SD dir.') self.fds: Dict[int, BinaryIO] = {} print('Root dir: ' + self.root_dir) self.crypto.set_keyslot('y', 0x34, readbe(key_y)) print('Key: ' + self.crypto.key_normal[0x34].hex()) self.root = os.path.realpath(sd_dir + '/' + self.root_dir) self.root_len = len(self.root) self.readonly = readonly
def __init__(self, ncch_fp: BinaryIO, g_stat: os.stat_result, decompress_code: bool = True, dev: bool = False, seeddb: str = None): self.crypto = CryptoEngine(dev=dev) self.decompress_code = decompress_code self.seeddb = seeddb self.files: Dict[str, Dict] = {} # get status change, modify, and file access times self._g_stat = g_stat self.g_stat = {'st_ctime': int(g_stat.st_ctime), 'st_mtime': int(g_stat.st_mtime), 'st_atime': int(g_stat.st_atime)} ncch_header = ncch_fp.read(0x200) self.reader = NCCHReader.from_header(ncch_header) self.f = ncch_fp if not self.reader.flags.no_crypto: # I should figure out what happens if fixed-key crypto is # used along with seed. even though this will never # happen in practice, I would still like to see what # happens if it happens. if self.reader.flags.fixed_crypto_key: normal_key = FIXED_SYSTEM_KEY if self.reader.program_id & (0x10 << 32) else 0x0 self.crypto.set_normal_key(0x2C, normal_key.to_bytes(0x10, 'big')) else: if self.reader.flags.uses_seed: self.reader.load_seed_from_seeddb() self.crypto.set_keyslot('y', 0x2C, readbe(self.reader.get_key_y(original=True))) self.crypto.set_keyslot('y', self.reader.extra_keyslot, readbe(self.reader.get_key_y()))
def __init__(self, boot9, seeddb, movable, cias, sd, skip_contents=False): self.event = Events() self.log_lines = [] # Stores all info messages for user to view self.crypto = CryptoEngine(boot9=boot9) self.crypto.setup_sd_key_from_file(movable) self.seeddb = seeddb self.cias = cias self.sd = sd self.skip_contents = skip_contents self.movable = movable
def __init__(self, tmd_file: str = None, cdn_dir: str = None, dec_key: str = None, dev: bool = False, seeddb: str = None, boot9: str = None): if tmd_file: self.cdn_dir = os.path.dirname(tmd_file) else: self.cdn_dir = cdn_dir tmd_file = os.path.join(cdn_dir, 'tmd') self.crypto = CryptoEngine(boot9=boot9, dev=dev) self.cdn_content_size = 0 self.dev = dev self.seeddb = seeddb # get status change, modify, and file access times try: self.g_stat = get_time(tmd_file) except FileNotFoundError: exit('Could not find "tmd" in directory') self.tmd = TitleMetadataReader.from_file(tmd_file) # noinspection PyUnboundLocalVariable self.title_id = self.tmd.title_id if not os.path.isfile(self.rp('cetk')): if not dec_key: exit('cetk not found. Provide the ticket or decrypted titlekey with --dec-key.') if dec_key: try: titlekey = bytes.fromhex(dec_key) if len(titlekey) != 16: exit('--dec-key input is not 32 hex characters.') except ValueError: exit('Failed to convert --dec-key input to bytes. Non-hex character likely found, or is not ' '32 hex characters.') # noinspection PyUnboundLocalVariable self.crypto.set_normal_key(Keyslot.DecryptedTitlekey, titlekey) else: with open(self.rp('cetk'), 'rb') as tik: # load ticket self.crypto.load_from_ticket(tik.read(0x350)) # create virtual files self.files = {'/ticket.bin': {'size': 0x350, 'type': 'raw', 'real_filepath': self.rp('cetk')}, '/tmd.bin': {'size': 0xB04 + self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0, 'type': 'raw', 'real_filepath': self.rp('tmd')}, '/tmdchunks.bin': {'size': self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': 0xB04, 'type': 'raw', 'real_filepath': self.rp('tmd')}} self.dirs: Dict[str, NCCHContainerMount] = {}
def __init__(self, sd_dir: str, movable: bytes, dev: bool = False, readonly: bool = False, boot9: str = None): self.crypto = CryptoEngine(boot9=boot9, dev=dev) self.crypto.setup_sd_key(movable) self.root_dir = self.crypto.id0.hex() if not os.path.isdir(sd_dir + '/' + self.root_dir): exit(f'Failed to find {self.root_dir} in the SD dir.') print('Root dir: ' + self.root_dir) print('Key: ' + self.crypto.keygen(Keyslot.SD).hex()) self.root = os.path.realpath(sd_dir + '/' + self.root_dir) self.root_len = len(self.root) self.readonly = readonly
def load_custom_boot9(path: str, dev: bool = False): """Load keys from a custom ARM9 bootROM path.""" if path: from pyctr.crypto import CryptoEngine # doing this will set up the keys for all future CryptoEngine objects CryptoEngine(boot9=path, dev=dev)
parser = ArgumentParser( description= 'Manually install a CIA to the SD card for a Nintendo 3DS system.') parser.add_argument('cia', help='CIA files', nargs='+') parser.add_argument('-m', '--movable', help='movable.sed file', required=True) parser.add_argument('-b', '--boot9', help='boot9 file') parser.add_argument('--sd', help='path to SD root') parser.add_argument('--skip-contents', help="don't add contents, only add title info entry", action='store_true') args = parser.parse_args() # set up the crypto engine to encrypt contents as they are written to the SD crypto = CryptoEngine(boot9=args.boot9) crypto.setup_sd_key_from_file(args.movable) # try to find the path to the SD card contents print('Finding path to install to...') sd_path = join(args.sd, 'Nintendo 3DS', crypto.id0.hex()) id1s = [] for d in scandir(sd_path): if d.is_dir() and len(d.name) == 32: try: id1_tmp = bytes.fromhex(d.name) except ValueError: continue else: id1s.append(d.name)
def __init__(self, nand_fp: BinaryIO, g_stat: dict, dev: bool = False, readonly: bool = False, otp: bytes = None, cid: AnyStr = None, boot9: str = None): self.crypto = CryptoEngine(boot9=boot9, dev=dev) self.g_stat = g_stat nand_fp.seek(0x100) # screw the signature ncsd_header = nand_fp.read(0x100) if ncsd_header[0:4] != b'NCSD': exit( 'NCSD magic not found, is this a real Nintendo 3DS NAND image?' ) media_id = ncsd_header[0x8:0x10] if media_id != b'\0' * 8: exit( 'Media ID not all-zero, is this a real Nintendo 3DS NAND image?' ) # check for essential.exefs nand_fp.seek(0x200) try: exefs = ExeFSReader(nand_fp) except InvalidExeFSError: exefs = None otp_data = None if otp: try: with open(otp, 'rb') as f: otp_data = f.read(0x200) except Exception: print(f'Failed to open and read given OTP ({otp}).\n') print_exc() exit(1) else: if exefs is None: exit( 'OTP not found, provide with --otp or embed essentials backup with GodMode9' ) else: try: with exefs.open('otp') as otp: otp_data = otp.read(0x200) except ExeFSFileNotFoundError: exit( '"otp" not found in essentials backup, update with GodMode9 or provide with --otp' ) self.crypto.setup_keys_from_otp(otp_data) def generate_ctr(): print( 'Attempting to generate Counter for CTR/TWL areas. If errors occur, provide the CID manually.' ) # -------------------------------------------------- # # attempt to generate CTR Counter nand_fp.seek(0xB9301D0) # these blocks are assumed to be entirely 00, so no need to xor anything ctrn_block_0x1d = nand_fp.read(0x10) ctrn_block_0x1e = nand_fp.read(0x10) for ks in (Keyslot.CTRNANDOld, Keyslot.CTRNANDNew): ctr_counter_offs = self.crypto.create_ecb_cipher(ks).decrypt( ctrn_block_0x1d) ctr_counter = int.from_bytes(ctr_counter_offs, 'big') - 0xB9301D # try the counter out = self.crypto.create_ctr_cipher( ks, ctr_counter + 0xB9301E).decrypt(ctrn_block_0x1e) if out == b'\0' * 16: print('Counter for CTR area automatically generated.') self.ctr = ctr_counter break else: print( 'Counter could not be generated for CTR area. Related virtual files will not appear.' ) self.ctr = None # -------------------------------------------------- # # attempt to generate TWL Counter nand_fp.seek(0x1C0) twln_block_0x1c = readbe(nand_fp.read(0x10)) twl_blk_xored = twln_block_0x1c ^ 0x18000601A03F97000000A97D04000004 twl_counter_offs = self.crypto.create_ecb_cipher(0x03).decrypt( twl_blk_xored.to_bytes(0x10, 'little')) twl_counter = int.from_bytes(twl_counter_offs, 'big') - 0x1C # try the counter twln_block_0x1d = nand_fp.read(0x10) out = self.crypto.create_ctr_cipher(0x03, twl_counter + 0x1D).decrypt(twln_block_0x1d) if out == b'\x8e@\x06\x01\xa0\xc3\x8d\x80\x04\x00\xb3\x05\x01\x00\x00\x00': print('Counter for TWL area automatically generated.') self.ctr_twl = twl_counter else: print( 'Counter could not be generated for TWL area. Related virtual files will not appear.' ) self.ctr_twl = None cid_data = None if cid: try: with open(cid, 'rb') as f: cid_data = f.read(0x200) except Exception: print(f'Failed to open and read given CID ({cid}).') print( 'If you want to attempt Counter generation, do not provide a CID path.\n' ) print_exc() exit(1) else: if exefs is None: generate_ctr() else: try: with exefs.open('nand_cid') as cid: cid_data = cid.read(0x10) except ExeFSFileNotFoundError: print( '"nand_cid" not found in essentials backup, update with GodMode9 or provide with --cid' ) generate_ctr() if cid_data: self.ctr = readbe(sha256(cid_data).digest()[0:16]) self.ctr_twl = readle(sha1(cid_data).digest()[0:16]) if not (self.ctr or self.ctr_twl): exit("Couldn't generate Counter for both CTR/TWL. " "Make sure the OTP is correct, or provide the CID manually.") nand_fp.seek(0, 2) raw_nand_size = nand_fp.tell() self.real_nand_size = nand_size[readle(ncsd_header[4:8])] self.files = { '/nand_hdr.bin': { 'size': 0x200, 'offset': 0, 'keyslot': 0xFF, 'type': 'raw' }, '/nand.bin': { 'size': raw_nand_size, 'offset': 0, 'keyslot': 0xFF, 'type': 'raw' }, '/nand_minsize.bin': { 'size': self.real_nand_size, 'offset': 0, 'keyslot': 0xFF, 'type': 'raw' } } nand_fp.seek(0x12C00) keysect_enc = nand_fp.read(0x200) if len(set(keysect_enc)) != 1: keysect_dec = self.crypto.create_ecb_cipher(0x11).decrypt( keysect_enc) # i'm cheating here by putting the decrypted version in memory and # not reading from the image every time. but it's not AES-CTR so # f**k that. self.files['/sector0x96.bin'] = { 'size': 0x200, 'offset': 0x12C00, 'keyslot': 0x11, 'type': 'keysect', 'content': keysect_dec } ncsd_part_fstype = ncsd_header[0x10:0x18] ncsd_part_crypttype = ncsd_header[0x18:0x20] ncsd_part_raw = ncsd_header[0x20:0x60] ncsd_partitions = [[ readle(ncsd_part_raw[i:i + 4]) * 0x200, readle(ncsd_part_raw[i + 4:i + 8]) * 0x200 ] for i in range(0, 0x40, 0x8)] # including padding for crypto if self.ctr_twl: twl_mbr = self.crypto.create_ctr_cipher( Keyslot.TWLNAND, self.ctr_twl + 0x1B).decrypt(ncsd_header[0xB0:0x100])[0xE:0x50] if twl_mbr[0x40:0x42] == b'\x55\xaa': twl_partitions = [[ readle(twl_mbr[i + 8:i + 12]) * 0x200, readle(twl_mbr[i + 12:i + 16]) * 0x200 ] for i in range(0, 0x40, 0x10)] else: twl_partitions = None self.files['/twlmbr.bin'] = { 'size': 0x42, 'offset': 0x1BE, 'keyslot': Keyslot.TWLNAND, 'type': 'twlmbr', 'content': twl_mbr } else: twl_partitions = None # then actually parse the partitions to create files firm_idx = 0 for idx, part in enumerate(ncsd_partitions): if ncsd_part_fstype[idx] == 0: continue print( f'ncsd idx:{idx} fstype:{ncsd_part_fstype[idx]} crypttype:{ncsd_part_crypttype[idx]} ' f'offset:{part[0]:08x} size:{part[1]:08x} ', end='') if idx == 0: if self.ctr_twl: self.files['/twl_full.img'] = { 'size': part[1], 'offset': part[0], 'keyslot': Keyslot.TWLNAND, 'type': 'enc' } print('/twl_full.img') if twl_partitions: twl_part_fstype = 0 for t_idx, t_part in enumerate(twl_partitions): if t_part[0] != 0: print( f'twl idx:{t_idx} ' f'offset:{t_part[0]:08x} size:{t_part[1]:08x} ', end='') if twl_part_fstype == 0: self.files['/twln.img'] = { 'size': t_part[1], 'offset': t_part[0], 'keyslot': Keyslot.TWLNAND, 'type': 'enc' } print('/twln.img') twl_part_fstype += 1 elif twl_part_fstype == 1: self.files['/twlp.img'] = { 'size': t_part[1], 'offset': t_part[0], 'keyslot': Keyslot.TWLNAND, 'type': 'enc' } print('/twlp.img') twl_part_fstype += 1 else: self.files[ f'/twl_unk{twl_part_fstype}.img'] = { 'size': t_part[1], 'offset': t_part[0], 'keyslot': Keyslot.TWLNAND, 'type': 'enc' } print(f'/twl_unk{twl_part_fstype}.img') twl_part_fstype += 1 else: print('<ctr_twl not set>') elif self.ctr: if ncsd_part_fstype[idx] == 3: # boot9 hardcoded this keyslot, i'll do this properly later self.files[f'/firm{firm_idx}.bin'] = { 'size': part[1], 'offset': part[0], 'keyslot': Keyslot.FIRM, 'type': 'enc' } print(f'/firm{firm_idx}.bin') firm_idx += 1 elif ncsd_part_fstype[ idx] == 1 and ncsd_part_crypttype[idx] >= 2: ctrnand_keyslot = Keyslot.CTRNANDOld if ncsd_part_crypttype[ idx] == 2 else Keyslot.CTRNANDNew self.files['/ctrnand_full.img'] = { 'size': part[1], 'offset': part[0], 'keyslot': ctrnand_keyslot, 'type': 'enc' } print('/ctrnand_full.img') nand_fp.seek(part[0]) iv = self.ctr + (part[0] >> 4) ctr_mbr = self.crypto.create_ctr_cipher( ctrnand_keyslot, iv).decrypt(nand_fp.read(0x200))[0x1BE:0x200] if ctr_mbr[0x40:0x42] == b'\x55\xaa': ctr_partitions = [[ readle(ctr_mbr[i + 8:i + 12]) * 0x200, readle(ctr_mbr[i + 12:i + 16]) * 0x200 ] for i in range(0, 0x40, 0x10)] ctr_part_fstype = 0 for c_idx, c_part in enumerate(ctr_partitions): if c_part[0] != 0: print( f'ctr idx:{c_idx} offset:{part[0] + c_part[0]:08x} ' f'size:{c_part[1]:08x} ', end='') if ctr_part_fstype == 0: self.files['/ctrnand_fat.img'] = { 'size': c_part[1], 'offset': part[0] + c_part[0], 'keyslot': ctrnand_keyslot, 'type': 'enc' } print('/ctrnand_fat.img') ctr_part_fstype += 1 else: self.files[ f'/ctr_unk{ctr_part_fstype}.img'] = { 'size': c_part[1], 'offset': part[0] + c_part[0], 'keyslot': ctrnand_keyslot, 'type': 'enc' } print(f'/ctr_unk{ctr_part_fstype}.img') ctr_part_fstype += 1 elif ncsd_part_fstype[idx] == 4: self.files['/agbsave.bin'] = { 'size': part[1], 'offset': part[0], 'keyslot': Keyslot.AGB, 'type': 'enc' } print('/agbsave.bin') else: print('<ctr not set>') self.readonly = readonly # GM9 bonus drive if raw_nand_size != self.real_nand_size: nand_fp.seek(self.real_nand_size) bonus_drive_header = nand_fp.read(0x200) if bonus_drive_header[0x1FE:0x200] == b'\x55\xAA': self.files['/bonus.img'] = { 'size': raw_nand_size - self.real_nand_size, 'offset': self.real_nand_size, 'keyslot': 0xFF, 'type': 'raw' } self.f = nand_fp if exefs is not None: exefs_size = sum( roundup(x.size, 0x200) for x in exefs.entries.values()) + 0x200 self.files['/essential.exefs'] = { 'size': exefs_size, 'offset': 0x200, 'keyslot': 0xFF, 'type': 'raw' } try: self.exefs_fuse = ExeFSMount(exefs, g_stat=g_stat) self.exefs_fuse.init('/') self._essentials_mounted = True except Exception as e: print( f'Failed to mount essential.exefs: {type(e).__name__}: {e}' )
def __init__(self, nand_fp: BinaryIO, g_stat: os.stat, consoleid: str, cid: str = None, readonly: bool = False): self.crypto = CryptoEngine(setup_b9_keys=False) self.readonly = readonly self.g_stat = { 'st_ctime': int(g_stat.st_ctime), 'st_mtime': int(g_stat.st_mtime), 'st_atime': int(g_stat.st_atime) } self.files = {} res = nand_fp.seek(0, 2) if res == 0xF000200: self.files['/nocash_blk.bin'] = { 'offset': 0xF000000, 'size': 0x200, 'type': 'dec' } elif res != 0xF000000: exit( f'Unknown NAND size (expected 0xF000000 or 0xF000200, got {res:#09X}' ) nand_fp.seek(0) try: consoleid = bytes.fromhex(consoleid) except (ValueError, TypeError): try: with open(consoleid, 'rb') as f: consoleid = f.read(0x10) except (FileNotFoundError, TypeError): exit( 'Failed to convert Console ID to bytes, or file did not exist.' ) twl_consoleid_list = (readbe(consoleid[4:8]), readbe(consoleid[0:4])) key_x_list = [ twl_consoleid_list[0], twl_consoleid_list[0] ^ 0x24EE6906, twl_consoleid_list[1] ^ 0xE65B601D, twl_consoleid_list[1] ] self.crypto.set_keyslot('x', 0x03, pack('<4I', *key_x_list)) header_enc = nand_fp.read(0x200) if cid: try: cid = bytes.fromhex(cid) except ValueError: try: with open(cid, 'rb') as f: cid = f.read(0x10) except FileNotFoundError: exit( 'Failed to convert CID to bytes, or file did not exist.' ) self.ctr = readle(sha1(cid).digest()[0:16]) else: # attempt to generate counter block_0x1c = readbe(header_enc[0x1C0:0x1D0]) blk_xored = block_0x1c ^ 0x1804060FE03B77080000896F06000002 ctr_offs = self.crypto.create_ecb_cipher(0x03).decrypt( blk_xored.to_bytes(0x10, 'little')) self.ctr = int.from_bytes(ctr_offs, 'big') - 0x1C # try the counter block_0x1d = header_enc[0x1D0:0x1E0] out = self.crypto.create_ctr_cipher(0x03, self.ctr + 0x1D).decrypt(block_0x1d) if out != b'\xce<\x06\x0f\xe0\xbeMx\x06\x00\xb3\x05\x01\x00\x00\x02': exit( 'Counter could not be automatically generated. Please provide the CID, ' 'or ensure the provided Console ID is correct..') print('Counter automatically generated.') self.files['/stage2_infoblk1.bin'] = { 'offset': 0x200, 'size': 0x200, 'type': 'dec' } self.files['/stage2_infoblk2.bin'] = { 'offset': 0x400, 'size': 0x200, 'type': 'dec' } self.files['/stage2_infoblk3.bin'] = { 'offset': 0x600, 'size': 0x200, 'type': 'dec' } self.files['/stage2_bootldr.bin'] = { 'offset': 0x800, 'size': 0x4DC00, 'type': 'dec' } self.files['/stage2_footer.bin'] = { 'offset': 0x4E400, 'size': 0x400, 'type': 'dec' } self.files['/diag_area.bin'] = { 'offset': 0xFFA00, 'size': 0x400, 'type': 'dec' } header = self.crypto.create_ctr_cipher(0x03, self.ctr).decrypt(header_enc) mbr = header[0x1BE:0x200] if mbr[0x40:0x42] != b'\x55\xaa': exit( 'MBR signature not found. Make sure the provided Console ID and CID are correct.' ) partitions = [[ readle(mbr[i + 8:i + 12]) * 0x200, readle(mbr[i + 12:i + 16]) * 0x200 ] for i in range(0, 0x40, 0x10)] for idx, part in enumerate(partitions): if part[0]: ptype = 'enc' if idx < 2 else 'dec' pname = ('twl_main', 'twl_photo', 'twl_unk1', 'twk_unk2')[idx] self.files[f'/{pname}.img'] = { 'offset': part[0], 'size': part[1], 'type': ptype } self.f = nand_fp
def __init__(self, cia_fp: BinaryIO, g_stat: os.stat_result, dev: bool = False, seeddb: bool = None): self.crypto = CryptoEngine(dev=dev) self.dev = dev self.seeddb = seeddb self._g_stat = g_stat # get status change, modify, and file access times self.g_stat = { 'st_ctime': int(g_stat.st_ctime), 'st_mtime': int(g_stat.st_mtime), 'st_atime': int(g_stat.st_atime) } # open cia and get section sizes archive_header_size, cia_type, cia_version, cert_chain_size, \ ticket_size, tmd_size, meta_size, content_size = unpack('<IHHIIIIQ', cia_fp.read(0x20)) self.cia_size = new_offset(archive_header_size) + new_offset(cert_chain_size) + new_offset(ticket_size)\ + new_offset(tmd_size) + new_offset(meta_size) + new_offset(content_size) # get offsets for sections of the CIA # each section is aligned to 64-byte blocks cert_chain_offset = new_offset(archive_header_size) ticket_offset = cert_chain_offset + new_offset(cert_chain_size) tmd_offset = ticket_offset + new_offset(ticket_size) self.content_offset = tmd_offset + new_offset(tmd_size) meta_offset = self.content_offset + new_offset(content_size) # load tmd cia_fp.seek(tmd_offset) self.tmd = TitleMetadataReader.load(cia_fp) self.title_id = self.tmd.title_id # load titlekey cia_fp.seek(ticket_offset) self.crypto.load_from_ticket(cia_fp.read(ticket_size)) # create virtual files self.files = { '/header.bin': { 'size': archive_header_size, 'offset': 0, 'type': 'raw' }, '/cert.bin': { 'size': cert_chain_size, 'offset': cert_chain_offset, 'type': 'raw' }, '/ticket.bin': { 'size': ticket_size, 'offset': ticket_offset, 'type': 'raw' }, '/tmd.bin': { 'size': tmd_size, 'offset': tmd_offset, 'type': 'raw' }, '/tmdchunks.bin': { 'size': self.tmd.content_count * CHUNK_RECORD_SIZE, 'offset': tmd_offset + 0xB04, 'type': 'raw' } } if meta_size: self.files['/meta.bin'] = { 'size': meta_size, 'offset': meta_offset, 'type': 'raw' } # show icon.bin if meta size is the expected size # in practice this never changes, but better to be safe if meta_size == 0x3AC0: self.files['/icon.bin'] = { 'size': 0x36C0, 'offset': meta_offset + 0x400, 'type': 'raw' } self.dirs: Dict[str, NCCHContainerMount] = {} self.f = cia_fp
def main(): print(len(sys.argv)) if len(sys.argv) < 9: print("""Arguments missing. Required arguments are: Nintendo 3DS folder --inputFolder folderPath Movable.sed path: --movable movablePath Output folder: --outputFolder folderPath Path to disa-extract.py: --disa disaPath """) return None inputFolder = None outputFolder = None movablePath = None disaPath = None i = 1 while i < len(sys.argv): print(i, sys.argv[i]) if sys.argv[i] == "--inputFolder" or sys.argv[i] == "-i": i += 1 inputFolder = sys.argv[i] elif sys.argv[i] == "--outputFolder" or sys.argv[i] == "-o": i += 1 outputFolder = sys.argv[i] elif sys.argv[i] == "--movable" or sys.argv[i] == "-m": i += 1 movablePath = sys.argv[i] elif sys.argv[i] == "--disa" or sys.argv[i] == "-e": i += 1 disaPath = sys.argv[i] releasesPath = sys.argv[i] else: print("Unable to parse {}".format(sys.argv[i])) i += 1 if inputFolder is None: print("Input folder is not present") if outputFolder is None: print("Output folder is not present") if movablePath is None: print("Movable path is not present") if disaPath is None: print("Path to disa-extract.py is not present") if inputFolder is None or outputFolder is None or movablePath is None or disaPath is None: return None print("All paths parsed") if not (pathlib.Path(inputFolder).is_dir()): print( "Input folder directory does not exist. Inputted path: {}".format( inputFolder)) if not (pathlib.Path(outputFolder).is_dir()): print( "Output folder directory does not exist. Inputted path: {}".format( outputFolder)) if not (os.path.isfile(movablePath)): print("movable does not exist. Inputted path: {}".format(movablePath)) if not (os.path.isfile(disaPath)): print("disa_extract.py does not exist. Inputted path: {}".format( disaPath)) print( "Creating crypto object, if this creates an error then boot9.bin is not found" ) crypto = CryptoEngine() print("Setting movable.sed for decryption") crypto.setup_sd_key_from_file(movablePath) print("Getting directories in Nintendo 3DS") #id0 = selectDirectory32char(inputFolder) id0 = crypto.id0 id0 = id0.hex() print("Getting directories in {}".format(id0)) id1 = selectDirectory32char(inputFolder + "\\" + id0) print("Starting dump and decrypt.") for titleHigh in os.listdir(inputFolder + "\\" + id0 + "\\" + id1 + "\\title\\"): print("Title High: {}".format(titleHigh)) for titleLow in os.listdir(inputFolder + "\\" + id0 + "\\" + id1 + "\\title\\" + titleHigh + "\\"): print("Title Low: {}".format(titleLow)) appFile = selectAppFile( crypto, inputFolder + "\\" + id0 + "\\" + id1 + "\\title\\" + titleHigh + "\\" + titleLow + "\\" + "content" + "\\") return None
parser = argparse.ArgumentParser( description='Generate Nintendo 3DS CMD files.') parser.add_argument('-t', '--tmd', help='tmd file', required=True) parser.add_argument('-m', '--movable', help='movable.sed file', required=True) parser.add_argument('-o', '--otp', help='otp.bin file, for TWLNAND contents') parser.add_argument('-b', '--boot9', help='boot9 file') parser.add_argument('--output-id', help='CMD content ID, default 00000001', default='00000001') a = parser.parse_args() MISSING = b'\xff\xff\xff\xff' crypto = CryptoEngine() crypto.setup_sd_key_from_file(a.movable) try: crypto.setup_keys_from_otp_file(a.otp) except FileNotFoundError: pass tmd = TitleMetadataReader.from_file(a.tmd) dirname = os.path.dirname(a.tmd) if tmd.title_id.startswith('0004008c'): content_dir = os.path.join(dirname, '00000000') else: content_dir = dirname # TODO: check Download Play if tmd.title_id.startswith('00048'): keyslot = Keyslot.CMACNANDDB
def __init__(self, nand_fp: BinaryIO, g_stat: dict, consoleid: str = None, cid: str = None, readonly: bool = False): self.crypto = CryptoEngine(setup_b9_keys=False) self.readonly = readonly self.g_stat = g_stat self.files = {} self.f = nand_fp nand_size = nand_fp.seek(0, 2) if nand_size < 0xF000000: exit(f'NAND is too small (expected >= 0xF000000, got {nand_size:#X}') if nand_size & 0x40 == 0x40: self.files['/nocash_blk.bin'] = {'offset': nand_size - 0x40, 'size': 0x40, 'type': 'dec'} nand_fp.seek(0) try: consoleid = bytes.fromhex(consoleid) except (ValueError, TypeError): try: with open(consoleid, 'rb') as f: consoleid = f.read(0x10) except (FileNotFoundError, TypeError): # read Console ID and CID from footer try: nocash_blk: bytes = self.read('/nocash_blk.bin', 0x40, 0, 0) except KeyError: if consoleid is None: exit('Nocash block not found, and Console ID not provided.') else: exit('Failed to convert Console ID to bytes, or file did not exist.') else: if len(nocash_blk) != 0x40: exit('Failed to read 0x40 of footer (this should never happen)') if nocash_blk[0:0x10] != b'DSi eMMC CID/CPU': exit('Failed to find footer magic "DSi eMMC CID/CPU"') if len(set(nocash_blk[0x10:0x40])) == 1: exit('Nocash block is entirely empty. Maybe re-dump NAND with another exploit, or manually ' 'get Console ID with some other method.') cid = nocash_blk[0x10:0x20] consoleid = nocash_blk[0x20:0x28][::-1] print('Console ID and CID read from nocash block.') twl_consoleid_list = (readbe(consoleid[4:8]), readbe(consoleid[0:4])) key_x_list = [twl_consoleid_list[0], twl_consoleid_list[0] ^ 0x24EE6906, twl_consoleid_list[1] ^ 0xE65B601D, twl_consoleid_list[1]] self.crypto.set_keyslot('x', Keyslot.TWLNAND, pack('<4I', *key_x_list)) nand_fp.seek(0) header_enc = nand_fp.read(0x200) if cid: if not isinstance(cid, bytes): # if cid was already read above try: cid = bytes.fromhex(cid) except ValueError: try: with open(cid, 'rb') as f: cid = f.read(0x10) except FileNotFoundError: exit('Failed to convert CID to bytes, or file did not exist.') self.ctr = readle(sha1(cid).digest()[0:16]) else: # attempt to generate counter block_0x1c = readbe(header_enc[0x1C0:0x1D0]) blk_xored = block_0x1c ^ 0x1804060FE03B77080000896F06000002 ctr_offs = self.crypto.create_ecb_cipher(Keyslot.TWLNAND).decrypt(blk_xored.to_bytes(0x10, 'little')) self.ctr = int.from_bytes(ctr_offs, 'big') - 0x1C # try the counter block_0x1d = header_enc[0x1D0:0x1E0] out = self.crypto.create_ctr_cipher(Keyslot.TWLNAND, self.ctr + 0x1D).decrypt(block_0x1d) if out != b'\xce<\x06\x0f\xe0\xbeMx\x06\x00\xb3\x05\x01\x00\x00\x02': exit('Counter could not be automatically generated. Please provide the CID, ' 'or ensure the provided Console ID is correct.') print('Counter automatically generated.') self.files['/stage2_infoblk1.bin'] = {'offset': 0x200, 'size': 0x200, 'type': 'dec'} self.files['/stage2_infoblk2.bin'] = {'offset': 0x400, 'size': 0x200, 'type': 'dec'} self.files['/stage2_infoblk3.bin'] = {'offset': 0x600, 'size': 0x200, 'type': 'dec'} self.files['/stage2_bootldr.bin'] = {'offset': 0x800, 'size': 0x4DC00, 'type': 'dec'} self.files['/stage2_footer.bin'] = {'offset': 0x4E400, 'size': 0x400, 'type': 'dec'} self.files['/diag_area.bin'] = {'offset': 0xFFA00, 'size': 0x400, 'type': 'dec'} header = self.crypto.create_ctr_cipher(Keyslot.TWLNAND, self.ctr).decrypt(header_enc) mbr = header[0x1BE:0x200] mbr_sig = mbr[0x40:0x42] if mbr_sig != b'\x55\xaa': exit(f'MBR signature not found (expected "55aa", got "{mbr_sig.hex()}"). ' f'Make sure the provided Console ID and CID are correct.') partitions = [[readle(mbr[i + 8:i + 12]) * 0x200, readle(mbr[i + 12:i + 16]) * 0x200] for i in range(0, 0x40, 0x10)] for idx, part in enumerate(partitions): if part[0]: ptype = 'enc' if idx < 2 else 'dec' pname = ('twl_main', 'twl_photo', 'twl_unk1', 'twk_unk2')[idx] self.files[f'/{pname}.img'] = {'offset': part[0], 'size': part[1], 'type': ptype}