#USAGE:
#python personaq.py --US3ds "US.3ds" --JP3ds "JP.3ds"
# --USsize 1856598016 --USoffset 0x2f5000
import sys, getopt, os
import struct #nhcc
ncch_title = ''
ncch_offset = 0
romfs_offset= 0
romfs_size = 0
s_rom__byte = []
def main(argv):
global s_rom__byte
s_param, s_arg = getopt.getopt( argv , '', [\
'US3ds=', 'USsize=', 'USoffset=',\
'JP3ds=', 'JPsize=', 'JPoffset=', 'skip-md5', 'xorpad', 'bake' ] )
#s_param long and short keys
#s_arg values without keys
#xorpad for xoring and wait for modification
#bake is re-encrypting the 3ds file
if filter( lambda x: x[ 0 ] == '--bake', s_param ):
xorpad( s_param )
print "ROM stats for dev"
for i in range(0,2):
print s_rom__byte[ i * 4 ]
print hex( s_rom__byte[ i * 4 + 1 ] + s_rom__byte[ i * 4 + 2 ] )
print str( s_rom__byte[ i * 4 + 3 ] )
print ''
the_offset = 0
for the_region in ["US", "JP"]:
print "Decrypting " + the_region + " rom"
xorbin = open( s_rom__byte[ the_offset * 4 + 0 ] + ".Main.romfs.xorpad", 'rb' )
romfs( the_region, s_param,\
s_rom__byte[ the_offset * 4 + 3 ],\
s_rom__byte[ the_offset * 4 + 1 ] + s_rom__byte[ the_offset * 4 + 2 ],\
xorbin )
xorbin.close()
os.system( "ctrtool -x EncryptedRomFS.bin" );
os.system( "ctrtool -t =romfs --romfsdir=" + the_region +"ROM EncryptedRomFS.bin" );
os.system( "move " + the_region + "ROM\\data.cpk " + the_region + "data.cpk" );
os.system( "cpk_unpack " + the_region + "data.cpk" );
the_offset += 1
print ''
print 'Copy sound files from JP ROM'
os.system( "xcopy USdata.cpk_unpacked UDdata.cpk_unpacked\\ /S /E")
os.system( "xcopy JPdata.cpk_unpacked\\sound\\*.awb UDdata.cpk_unpacked\\sound\\ /Y")
os.system( "move \"Persona Q Filelist.csv\" UDdata.cpk_unpacked\\")
os.system( "cpkmakec \"Persona Q Filelist.csv\" dudata.cpk -align=2048 -mode=FILENAME -dir=UDdata.cpk_unpacked")
os.system( "move UDdata.cpk_unpacked\\dudata.cpk ." )
print 'This is the point where I refused to use any ddl or exe from the SDK tool.'
print 'Now we have a useless cpk file. Enjoy :# ... these steps took 150 minutes on an average computer'
else:
if filter( lambda x: x[ 0 ] == '--skip-md5', s_param ):
pass
else:
print "US ROM checksum"
os.system( 'md5 \"' +\
filter( lambda x: x[ 0 ] == '--US3ds' , s_param )[ 0 ][ 1 ] +\
'\"')
print "JP ROM checksum"
os.system( 'md5 \"' +\
filter( lambda x: x[ 0 ] == '--JP3ds' , s_param )[ 0 ][ 1 ] +\
'\"')
xorpad( s_param )
def inline_xor_nocopy(aa, bb):
real_size = len(aa)
a = numpy.frombuffer(aa, dtype=numpy.uint64)
b = numpy.frombuffer(bb, dtype=numpy.uint64)
return weave.inline(code2, ["a", "b", "real_size"],
headers = ['"emmintrin.h"'],
support_code = support)
def xor(data, key):
index = len(data) % 4
size = (4, 1, 2, 1)[index]
type = ('L', 'B', 'H', 'B')[index]
key_len = len(key)/size
data_len = len(data)/size
key_fmt = "<" + str(key_len) + type;
data_fmt = "<" + str(data_len) + type;
key_list = struct.unpack(key_fmt, key)
data_list = struct.unpack(data_fmt, data)
result = []
for i in range(data_len):
result.append (key_list[i % key_len] ^ data_list[i])
return struct.pack(data_fmt, *result)
def romfs( a_region, s_param, a_size, an_offset, xorbin ):
with open(\
filter( lambda x: x[ 0 ] == '--' + a_region + '3ds' , s_param )[ 0 ][ 1 ],\
'rb' ) as rom__us:
with open( 'EncryptedRomFS.bin', 'wb' ) as ebin:
ebin__size = a_size
ebin__offset= an_offset
rom__us.seek( ebin__offset )
#Don't do that: #xorbin.seek( ebin__offset )
every = 128 / 8 * 1
progress = 0
while True:
if progress == 0:
print "%d bytes" % ( ebin__size )
progress = ( progress + 1 ) % every
s_byte = None
if ebin__size > 8 * 1048576:
s_byte = rom__us.read( 8 * 1048576 )
s_xor = xorbin.read( 8 * 1048576 )
ebin__size -= 8 * 1048576
else:
s_byte = rom__us.read( ebin__size )
s_xor = xorbin.read( ebin__size )
ebin__size = 0
if s_byte and s_xor:
ebin.write( xor( s_byte, s_xor ) )
else:
print "Error creating EncryptedRomFS.bin"
if ebin__size == 0:
break
rom__us.close()
ebin.close()
print "(" + a_region + ") EncryptedRomFS.bin created"
def xorpad( s_param ):
global s_rom__byte
s_file = [\
filter( lambda x: x[ 0 ] == '--US3ds' , s_param )[ 0 ][ 1 ],\
filter( lambda x: x[ 0 ] == '--JP3ds' , s_param )[ 0 ][ 1 ]\
]
s_region = [\
'US', 'JP'\
]
entries = 0
data = ''
i = 0
for file in s_file:
with open( file ,'rb') as the_rom:
result = []
result = parseNCSD( the_rom )
s_rom__byte.append( ncch_title )
s_rom__byte.append( ncch_offset )
s_rom__byte.append( romfs_offset )
s_rom__byte.append( romfs_size )
if result:
entries += result[ 0 ]
data = data + result[ 1 ]
dndFix = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),\
'ncchinfo.bin' )
# s_region[ i ] + '_ncchinfo.bin') #Fix drag'n'drop
with open(dndFix, 'wb') as binfile:
binfile.write(struct.pack('<IIII',0xFFFFFFFF, 0xF0000003, entries, 0))
binfile.write(data)
binfile.close()
i += 1
# TODO rewrite
from ctypes import *
from binascii import hexlify
mediaUnitSize = 0x200
class ncchHdr(Structure):
_fields_ = [
('signature', c_uint8 * 0x100),
('magic', c_char * 4),
('ncchSize', c_uint32),
('titleId', c_uint8 * 0x8),
('makerCode', c_uint16),
('formatVersion', c_uint8),
('formatVersion2', c_uint8),
('padding0', c_uint32),
('programId', c_uint8 * 0x8),
('padding1', c_uint8 * 0x10),
('logoHash', c_uint8 * 0x20),
('productCode', c_uint8 * 0x10),
('exhdrHash', c_uint8 * 0x20),
('exhdrSize', c_uint32),
('padding2', c_uint32),
('flags', c_uint8 * 0x8),
('plainRegionOffset', c_uint32),
('plainRegionSize', c_uint32),
('logoOffset', c_uint32),
('logoSize', c_uint32),
('exefsOffset', c_uint32),
('exefsSize', c_uint32),
('exefsHashSize', c_uint32),
('padding4', c_uint32),
('romfsOffset', c_uint32),
('romfsSize', c_uint32),
('romfsHashSize', c_uint32),
('padding5', c_uint32),
('exefsHash', c_uint8 * 0x20),
('romfsHash', c_uint8 * 0x20),
]
class ncchSection:
exheader = 1
exefs = 2
romfs = 3
class ncch_offsetsize(Structure):
_fields_ = [
('offset', c_uint32),
('size', c_uint32),
]
class ncsdHdr(Structure):
_fields_ = [
('signature', c_uint8 * 0x100),
('magic', c_char * 4),
('mediaSize', c_uint32),
('titleId', c_uint8 * 0x8),
('padding0', c_uint8 * 0x10),
('offset_sizeTable', ncch_offsetsize * 0x8),
('padding1', c_uint8 * 0x28),
('flags', c_uint8 * 0x8),
('ncchIdTable', c_uint8 * 0x40),
('padding2', c_uint8 * 0x30),
]
ncsdPartitions = [b'Main', b'Manual', b'DownloadPlay', b'Partition4', b'Partition5', b'Partition6', b'Partition7', b'UpdateData']
def roundUp(numToRound, multiple): #From http://stackoverflow.com/a/3407254
if (multiple == 0):
return numToRound
remainder = abs(numToRound) % multiple
if (remainder == 0):
return numToRound
if (numToRound < 0):
return -(abs(numToRound) - remainder)
return numToRound + multiple - remainder
def reverseCtypeArray(ctypeArray): #Reverses a ctype array and converts it to a hex string.
return ''.join('%02X' % x for x in ctypeArray[::-1])
#Is there a better way to do this?
def getNcchAesCounter(header, type): #Function based on code from ctrtool's source: https://github.com/3DSGuy/Project_CTR
counter = bytearray(b'\x00' * 16)
if header.formatVersion == 2 or header.formatVersion == 0:
counter[:8] = bytearray(header.titleId[::-1])
counter[8:9] = chr(type)
elif header.formatVersion == 1:
x = 0
if type == ncchSection.exheader:
x = 0x200 #ExHeader is always 0x200 bytes into the NCCH
if type == ncchSection.exefs:
x = header.exefsOffset * mediaUnitSize
if type == ncchSection.romfs:
x = header.romfsOffset * mediaUnitSize
counter[:8] = bytearray(header.titleId)
for i in xrange(4):
counter[12+i] = chr((x>>((3-i)*8)) & 0xFF)
return bytes(counter)
def parseNCSD(fh):
print 'Parsing NCSD in file "%s":' % os.path.basename(fh.name)
entries = 0
data = ''
fh.seek(0)
header = ncsdHdr()
fh.readinto(header) #Reads header into structure
for i in xrange(len(header.offset_sizeTable)):
if header.offset_sizeTable[i].offset:
result = parseNCCH(fh, header.offset_sizeTable[i].offset * mediaUnitSize, i, reverseCtypeArray(header.titleId), 0)
entries += result[0]
data = data + result[1]
return [entries, data]
def parseNCCH(fh, offs=0, idx=0, titleId='', standAlone=1):
global ncch_title, ncch_offset
tab = ' ' if not standAlone else ' '
if not standAlone and ncsdPartitions[idx] == "Main":
print ' Parsing %s NCCH' % ncsdPartitions[idx]
else:
pass #print 'Parsing NCCH in file "%s":' % os.path.basename(fh.name)
entries = 0
data = ''
fh.seek(offs)
header = ncchHdr()
fh.readinto(header) #Reads header into structure
if titleId == '':
titleId = reverseCtypeArray(header.titleId)
keyY = bytearray(header.signature[:16])
if not standAlone and ncsdPartitions[idx] == "Main":
print tab + 'NCCH Offset: %08X' % offs
print tab + 'Product code: ' + str(bytearray(header.productCode)).rstrip('\x00')
ncch_offset = offs
if not standAlone and ncsdPartitions[idx] == "Main":
print tab + 'Partition number: %d' % idx
print tab + 'KeyY: %s' % hexlify(keyY).upper()
print tab + 'Title ID: %s' % reverseCtypeArray(header.titleId)
ncch_title = reverseCtypeArray(header.titleId)
print tab + 'Format version: %d' % header.formatVersion
uses7xCrypto = bytearray(header.flags)[3]
if uses7xCrypto:
print tab + 'Uses 7.x NCCH crypto'
print ''
if header.exhdrSize:
data = data + parseNCCHSection(header, ncchSection.exheader, 0, 1, tab, ncsdPartitions[idx] )
data = data + genOutName(titleId, ncsdPartitions[idx], b'exheader')
entries += 1
print ''
if header.exefsSize: #We need generate two xorpads for exefs if it uses 7.x crypto, since only a part of it uses the new crypto.
data = data + parseNCCHSection(header, ncchSection.exefs, 0, 1, tab, ncsdPartitions[idx] )
data = data + genOutName(titleId, ncsdPartitions[idx], b'exefs_norm')
entries += 1
if uses7xCrypto:
data = data + parseNCCHSection(header, ncchSection.exefs, uses7xCrypto, 0, tab, ncsdPartitions[idx] )
data = data + genOutName(titleId, ncsdPartitions[idx], b'exefs_7x')
entries += 1
print ''
if header.romfsSize:
data = data + parseNCCHSection(header, ncchSection.romfs, uses7xCrypto, 1, tab, ncsdPartitions[idx] )
data = data + genOutName(titleId, ncsdPartitions[idx], b'romfs')
entries += 1
print ''
print ''
return [entries, data]
def parseNCCHSection(header, type, uses7xCrypto, doPrint, tab, partition_id):
global romfs_offset, romfs_size
if type == ncchSection.exheader:
sectionName = 'ExHeader'
offset = 0x200 #Always 0x200
sectionSize = header.exhdrSize * mediaUnitSize
elif type == ncchSection.exefs:
sectionName = 'ExeFS'
offset = header.exefsOffset * mediaUnitSize
sectionSize = header.exefsSize * mediaUnitSize
elif type == ncchSection.romfs:
sectionName = 'RomFS'
offset = header.romfsOffset * mediaUnitSize
sectionSize = header.romfsSize * mediaUnitSize
else:
print 'Invalid NCCH section type was somehow passed in. :/'
sys.exit()
counter = getNcchAesCounter(header, type)
keyY = bytearray(header.signature[:16])
sectionMb = roundUp(sectionSize, 1024*1024) / (1024*1024)
if sectionMb == 0:
sectionMb = 1 #Should never happen, but meh.
if doPrint and partition_id == "Main":
if sectionName == "RomFS":
romfs_offset = offset
romfs_size = sectionSize
print tab + '%s offset: %08X' % (sectionName, offset)
print tab + '%s counter: %s' % (sectionName, hexlify(counter))
print tab + '%s Megabytes(rounded up): %d' % (sectionName, sectionMb)
return struct.pack('<16s16sIIII', str(counter), str(keyY), sectionMb, 0, 0, uses7xCrypto)
def genOutName(titleId, partitionName, sectionName):
outName = b'sdmc:/%s.%s.%s.xorpad' % (titleId, partitionName, sectionName)
outName = outName.encode('utf-16le') + b'\x00\x00'
if len(outName) > 112:
print "Output file name too large. This shouldn't happen."
sys.exit()
return outName + (b'\x00'*(112-len(outName))) #Pad out so whole entry is 160 bytes (48 bytes are set before filename)
if __name__ == "__main__":
main(sys.argv[1:])