Exemple #1
0
    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
Exemple #2
0
    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()))
Exemple #3
0
    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
Exemple #4
0
    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] = {}
Exemple #5
0
    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
Exemple #6
0
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)
Exemple #7
0
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)
Exemple #8
0
    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}'
                )
Exemple #9
0
    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
Exemple #10
0
    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
Exemple #12
0
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
Exemple #13
0
    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}