Created
August 8, 2016 06:08
-
-
Save ihaveamac/0c546ede6994968cf4567f727dd5ddea to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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