Skip to content

Instantly share code, notes, and snippets.

@ihaveamac
Created August 8, 2016 06:08
Show Gist options
  • Save ihaveamac/0c546ede6994968cf4567f727dd5ddea to your computer and use it in GitHub Desktop.
Save ihaveamac/0c546ede6994968cf4567f727dd5ddea to your computer and use it in GitHub Desktop.
#!/usr/bin/env python2
# THIS CREATES BAD CIAS - DON'T BOTHER
import binascii
import itertools
import hashlib
import math
import os
import struct
import sys
import zlib
if len(sys.argv) < 2:
print("3dsconv-destroy.py <cci>")
print("this overwrites the original file, also you shouldn't use this. if you don't want that, use https://github.com/ihaveamac/3dsconv")
sys.exit(1)
mu = 0x200 # media unit
readsize = 8 * 1024 * 1024 # used from padxorer
# includes certificate chain, ticket (blank titlekey and title id), and tmd signature + blank header (blank title id)
# + blank content info records
# compressed using zlib then encoded with base64
# yes yes this is probably a bad idea, but i'm too lazy pls
ciainfo = """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"""
# add 0x10A8 (4264) 00 to the end of this...
# cert chain size: 0xA00
# ticket size: 0x350
# tmd size: 0x1240
# used from http://www.gossamer-threads.com/lists/python/python/163938
def bytes2int(string):
i_s = 0
for ch in string:
i_s = 256 * i_s + ord(ch)
return i_s
def showprogress(val, max):
# crappy workaround I bet, but print() didn't do what I wanted
minval = min(val, max)
sys.stdout.write("\r %5.1f%% %10i / %i" % ((minval / max) * 100, minval, max))
sys.stdout.flush()
with open(sys.argv[1], "rb+") as romf:
romf.seek(0x100)
if romf.read(4) != "NCSD":
print("probably not cci (can't find NCSD)")
sys.exit(1)
romf.seek(0x108)
tid_bin = romf.read(8)[::-1]
# find Game Executable CXI
romf.seek(0x120)
# I should use struct for these. too lazy at the moment though, I just copied these over from 3dsconv
gamecxi_offset = bytes2int(romf.read(0x4)[::-1]) * mu
gamecxi_size = bytes2int(romf.read(0x4)[::-1]) * mu
# find Manual CFA
romf.seek(0x128)
manualcfa_offset = bytes2int(romf.read(0x4)[::-1]) * mu
manualcfa_size = bytes2int(romf.read(0x4)[::-1]) * mu
# find Download Play child container CFA
romf.seek(0x130)
dlpchildcfa_offset = bytes2int(romf.read(0x4)[::-1]) * mu
dlpchildcfa_size = bytes2int(romf.read(0x4)[::-1]) * mu
romf.seek(gamecxi_offset + 0x18F)
decrypted = int(binascii.hexlify(romf.read(1))) & 0x04
if not decrypted:
print("only supports decrypted (if you want encrypted use the original 3dsconv)")
sys.exit(1)
# probably should calculate these but I'm lazy this is easier
tmdpadding = "\0" * 0x70C # padding to add at the end of the tmd
contentcount = "\x01" # for convenience later since this has to be written a few times
contentindex = "\x80" # some weird thing in the CIA header
if manualcfa_offset != 0:
tmdpadding = "\0" * 0x6DC
contentcount = "\x02"
contentindex = "\xC0"
if dlpchildcfa_offset != 0:
tmdpadding = "\0" * 0x6AC
contentcount = "\x03"
contentindex = "\xE0"
chunkrecords = "\0" * 0xC # 1st content: ID 0x00000000, Index 0x0000
chunkrecords += struct.pack(">I", gamecxi_size)
chunkrecords += "\0" * 0x20 # sha256 hash to be filled in later
if manualcfa_offset != 0:
chunkrecords += binascii.unhexlify("000000010001000000000000") # 2nd content: ID 0x1, Index 0x1
chunkrecords += struct.pack(">I", manualcfa_size)
chunkrecords += "\0" * 0x20 # sha256 hash to be filled in later
if dlpchildcfa_offset != 0:
chunkrecords += binascii.unhexlify("000000020002000000000000") # 3nd content: ID 0x2, Index 0x2
chunkrecords += struct.pack(">I", dlpchildcfa_size)
chunkrecords += "\0" * 0x20 # sha256 hash to be filled in later
romf.seek(0)
romf.write(
binascii.unhexlify("2020000000000000000A0000500300004012000000000000") +
struct.pack("<I", gamecxi_size + manualcfa_size + dlpchildcfa_size) +
("\0" * 4) + contentindex + ("\0" * 0x201F) +
zlib.decompress(ciainfo.decode('base64')) + ("\0" * 2412) +
chunkrecords + tmdpadding
)
chunkrecords = list(chunkrecords) # to update hashes in it, then calculate the hash over the entire thing
romf.seek(0x2F9F)
romf.write(contentcount)
romf.seek(0x2C1C)
romf.write(tid_bin)
romf.seek(0x2F4C)
romf.write(tid_bin)
romf.seek(gamecxi_offset + 0x20D)
sdbyte = chr(ord(romf.read(1)) | 2)
romf.seek(gamecxi_offset + 0x20D)
romf.write(sdbyte)
romf.seek(gamecxi_offset + 0x200)
exh_hash = hashlib.sha256(romf.read(0x400)).digest()
romf.seek(gamecxi_offset + 0x160)
romf.write(exh_hash)
romf.seek(gamecxi_offset + 0x3C0)
savesize = romf.read(4)
romf.seek(0x2F5A)
romf.write(savesize)
# Game Executable CXI second-half ExHeader + contents
print("- reading Game Executable CXI")
gamecxi_hash = hashlib.sha256()
romf.seek(gamecxi_offset)
left = gamecxi_size
tmpread = ""
for __ in itertools.repeat(0, int(math.floor((gamecxi_size / readsize)) + 1)):
toread = min(readsize, left)
tmpread = romf.read(toread)
gamecxi_hash.update(tmpread)
left -= readsize
showprogress(gamecxi_size - left, gamecxi_size)
if left <= 0:
print("")
break
romf.seek(0x38D4)
romf.write(gamecxi_hash.digest())
chunkrecords[0x10:0x30] = list(gamecxi_hash.digest())
# Manual CFA
if manualcfa_offset != 0:
print("- reading Manual CFA")
manualcfa_hash = hashlib.sha256()
romf.seek(manualcfa_offset)
left = manualcfa_size
for __ in itertools.repeat(0, int(math.floor((manualcfa_size / readsize)) + 1)):
toread = min(readsize, left)
tmpread = romf.read(toread)
manualcfa_hash.update(tmpread)
left -= readsize
showprogress(manualcfa_size - left, manualcfa_size)
if left <= 0:
print("")
break
romf.seek(0x3904)
romf.write(manualcfa_hash.digest())
chunkrecords[0x40:0x60] = list(manualcfa_hash.digest())
# Download Play child container CFA
if dlpchildcfa_offset != 0:
print("- reading Download Play child container CFA")
dlpchildcfa_hash = hashlib.sha256()
romf.seek(dlpchildcfa_offset)
left = dlpchildcfa_size
for __ in itertools.repeat(0, int(math.floor((dlpchildcfa_size / readsize)) + 1)):
toread = min(readsize, left)
tmpread = romf.read(toread)
dlpchildcfa_hash.update(tmpread)
left -= readsize
showprogress(dlpchildcfa_size - left, dlpchildcfa_size)
if left <= 0:
print("")
break
romf.seek(0x3934)
romf.write(dlpchildcfa_hash.digest())
chunkrecords[0x70:0x90] = list(dlpchildcfa_hash.digest())
print(romf.tell())
romf.seek(0x4000 + gamecxi_size + manualcfa_size + dlpchildcfa_size)
print(romf.tell())
romf.truncate()
chunkrecords_hash = hashlib.sha256("".join(chunkrecords))
romf.seek(0x2FC7)
romf.write(contentcount + chunkrecords_hash.digest())
romf.seek(0x2FA4)
inforecords_hash = hashlib.sha256("\x00\x00\x00" + contentcount + chunkrecords_hash.digest() + ("\x00"*0x8DC))
romf.write(inforecords_hash.digest())
os.rename(sys.argv[1], sys.argv[1] + ".cia")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment