Skip to content

Commit

Permalink
2020 update
Browse files Browse the repository at this point in the history
- Replace "cryptography" dependency by "pycryptodome", as the former relied on OpenSSL, causing issues on Mac OS
- Fix a bug where firmtool build -t didn't alias -S
- Fix a bug in the handling of section0 system modules with a 8-character long name
  • Loading branch information
TuxSH committed Dec 26, 2020
1 parent 22a69da commit fdc7085
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 63 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
BSD 3-Clause License

Copyright (c) 2017,
Copyright (c) TuxSH 2017-2020
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ Compatible with Python >= 3.2 and Python >= 2.7.

## Installation

On Windows, install Python >= 3.4 using the installer provided by the official Python website. Make sure that `pip` is in `PATH` then run `pip install cryptography` as administrator.
On Windows, install Python >= 3.4 using the installer provided by the official Python website. Make sure that `pip` is in `PATH`.

On *ix, install the corresponding packages, they should be named `python`, `python-setuptools`, `python-pip` or similar. If your distribution provides it, install `python-cryptography` as well, otherwise run `pip install cryptography` as `root`.
On *ix, install the corresponding packages, they should be named `python`, `python-setuptools`, `python-pip` or similar. You may need to upgrade `pip`.

On Linux distribution having severly outdated packages such as Debian, run `pip install --upgrade pip setuptools pyparsing`.
The preferred way to install and update firmtool is to run `pip install -U git+https://github.com/TuxSH/firmtool.git` directly (with the appropriate permissions), although `python setup.py install` should work as well.

The preferred way to install firmtool is to run `pip install git+https://github.com/TuxSH/firmtool.git` directly (with the appropriate permissions), although `python setup.py install` should work as well.
`firmtool` depends on `pycryptodome` (either as `Crypto` or `Cryptodome`), old `pycrypto` will not work.

## Usage
Showing information about a firmware binary:
Expand All @@ -33,28 +33,28 @@ cd modules
for f in *.cxi
do
ctrtool -p --exefs=exefs.bin $f

if [ $f = "Process9.cxi" ]
then
ctrtool -t exefs --exefsdir=exefs exefs.bin > /dev/null
else
ctrtool -t exefs --exefsdir=exefs --decompresscode exefs.bin > /dev/null
fi

cp exefs/code.bin $(basename -s .cxi $f).bin
rm -rf exefs
done
cd ..
```


Building a firmware binary (for example with two sections, an ARM9 and and ARM11 one, with the entrypoints at the start of the respective sections):
Building a firmware binary (for example with two sections, an Arm9 and and Arm11 one, with the entrypoints at the start of the respective sections):

```bash
firmtool build test.firm -n 0x08006800 -e 0x1FF80000 -D arm9.bin arm11.bin -A 0x08006800 0x1FF80000 -C NDMA XDMA
```

Building a firmware binary from an arm9loaderhax.bin payload which doesn't use the ARM11, with a loader supporting the ARM11 entrypoint being 0:
Building a firmware binary from an arm9loaderhax.bin payload which doesn't use the Arm11, with a loader supporting the Arm11 entrypoint being 0:

```bash
firmtool build test.firm -n 0x23F00000 -e 0 -D arm9loaderhax.bin -A 0x23F00000 -C NDMA
Expand Down
89 changes: 38 additions & 51 deletions firmtool/__main__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python

__author__ = "TuxSH"
__copyright__ = "Copyright (c) 2017 TuxSH"
__copyright__ = "Copyright (c) 2017-2020 TuxSH"
__license__ = "BSD"
__version__ = "1.3"
__version__ = "1.4"

"""
Parses, extracts, and builds 3DS firmware files
Expand All @@ -16,6 +16,15 @@
import sys
import os

# Try to import PyCryptodome
try:
import Crypto # type: ignore
except ImportError:
import Cryptodome as Crypto # type: ignore

from Crypto.Cipher import AES
from Crypto.Hash import SHA256

# lenny
perfectSignatures = {
"firm-nand-retail": (
Expand Down Expand Up @@ -141,7 +150,7 @@ def extractElf(elfFile):
phentsize, phnum, shentsize, shnum, shstrndx = unpack("<16s2H5I6H", hdr)

if machine != 40:
raise ValueError("machine type not ARM")
raise ValueError("machine type not Arm")

if version != 1:
raise ValueError("invalid ELF version")
Expand Down Expand Up @@ -182,10 +191,6 @@ def extractElf(elfFile):

class FirmSectionHeader(object):
def check(self):
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

off = self.offset
if self.copyMethod == 0 and (1 << 20) > self.size >= 0x800+0xA00 and self.address == 0x08006000:
if self.sectionData[0x50 : 0x53] == b"K9L":
self.guessedType = self.sectionData[0x50 : 0x54].decode("ascii")
Expand All @@ -197,19 +202,14 @@ def check(self):
if self.sectionData[0x100 : 0x104] == b"NCCH":
self.guessedType = "Kernel11 modules"

H = hashes.Hash(hashes.SHA256(), backend=default_backend())
H.update(self.sectionData)
self.hashIsValid = self.hash == H.finalize()
hash = SHA256.new(self.sectionData).digest()
self.hashIsValid = self.hash == hash

def doNtrCrypto(self, encrypt = True):
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

iv = pack("<4I", self.offset, self.address, self.size, self.size)
key = spiCryptoKey.get(self.kind.split('-')[-1], "retail")
cipher = Cipher(algorithms.AES(unhexlify(key)), modes.CBC(iv), backend=default_backend())
obj = cipher.encryptor() if encrypt else cipher.decryptor()
return obj.update(self.sectionData) + obj.finalize()
key = unhexlify(spiCryptoKey.get(self.kind.split('-')[-1], "retail"))
cipher = AES.new(key, AES.MODE_CBC, iv)
return cipher.encrypt(self.sectionData) if encrypt else cipher.decrypt(self.sectionData)

def __init__(self, n, kind = "nand-retail", data = None):
self.num = n
Expand All @@ -225,17 +225,12 @@ def __init__(self, n, kind = "nand-retail", data = None):
self.check()

def setData(self, data):
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

self.sectionData = data + b'\xFF' * ((512 - (len(data) % 512)) % 512)
self.size = len(self.sectionData)
self.guessedType = ''
self.hashIsValid = True

H = hashes.Hash(hashes.SHA256(), backend=default_backend())
H.update(self.sectionData)
self.hash = H.finalize()
self.hash = SHA256.new(self.sectionData).digest()

def buildHeader(self):
return pack("<4I32s", self.offset, self.address, self.size, self.copyMethod, self.hash)
Expand All @@ -249,7 +244,9 @@ def export(self, basePath, extractModules = False, secretSector = None):
while pos < self.size:
size = unpack_from("<I", self.sectionData, pos + 0x104)[0] * 0x200
name = self.sectionData[pos + 0x200: pos + 0x208].decode("ascii")
name = "{0}.cxi".format(name[:name.find('\x00')])
nullBytePos = name.find('\x00')
name = name if nullBytePos == -1 else name[:nullBytePos]
name = "{0}.cxi".format(name)
with open(os.path.join(basePath, "modules", name), "wb+") as f:
f.write(self.sectionData[pos : pos + size])
pos += size
Expand All @@ -258,27 +255,23 @@ def export(self, basePath, extractModules = False, secretSector = None):
f.write(self.sectionData)

elif self.guessedType.startswith("K9L") and secretSector is not None:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

# kek is in keyslot 0x11, as "normal key"
encKeyX = self.sectionData[:0x10] if self.guessedType[3] == '0' else self.sectionData[0x60 : 0x70]
key0x11 = secretSector[:0x10] if self.guessedType[3] != '2' else secretSector[0x10 : 0x20]

de = Cipher(algorithms.AES(key0x11), modes.ECB(), backend=default_backend()).decryptor()
keyX = de.update(encKeyX) + de.finalize()
kek = secretSector[:0x10] if self.guessedType[3] != '2' else secretSector[0x10 : 0x20]

keyX = AES.new(kek, AES.MODE_ECB).decrypt(encKeyX)
keyY = self.sectionData[0x10 : 0x20]

ctr = self.sectionData[0x20 : 0x30]
key = unhexlify("{0:032X}".format(keyscrambler(int(hexlify(keyX), 16), int(hexlify(keyY), 16))))

ctr = self.sectionData[0x20 : 0x30]
sizeDec = self.sectionData[0x30 : 0x38].decode("ascii")
size = int(sizeDec[:sizeDec.find('\x00')])
size = int(sizeDec[:sizeDec.find('\x00')], 10)

data = self.sectionData
if 0x800 + size <= self.size:
de = Cipher(algorithms.AES(key), modes.CTR(ctr), backend=default_backend()).decryptor()
data = b''.join((self.sectionData[:0x800], de.update(self.sectionData[0x800 : 0x800 + size]), de.finalize(), self.sectionData[0x800+size:]))
cipher = AES.new(key, AES.MODE_CTR, initial_value=ctr, nonce=b'')
decData = cipher.decrypt(self.sectionData[0x800 : 0x800 + size])
data = b''.join((self.sectionData[:0x800], decData, self.sectionData[0x800+size:]))
if extractModules:
exportP9(basePath, data)

Expand Down Expand Up @@ -356,8 +349,8 @@ def build(self):
def __str__(self):
hdr = """Priority:\t\t{0}
ARM9 entrypoint:\t0x{1:08X}{2}
ARM11 entrypoint:\t0x{3:08X}{4}
Arm9 entrypoint:\t0x{1:08X}{2}
Arm11 entrypoint:\t0x{3:08X}{4}
RSA-2048 signature:\t{5:0256X}
Expand Down Expand Up @@ -423,23 +416,18 @@ def buildFirm(args):

firmObj.check()
if not firmObj.arm9EntrypointFound:
raise ValueError("invalid or missing ARM9 entrypoint")
raise ValueError("invalid or missing Arm9 entrypoint")

if not (firmObj.arm11Entrypoint == 0 or firmObj.arm11EntrypointFound): # bootrom / FIRM won't boot firms with a NULL arm11 ep, though
raise ValueError("invalid or missing ARM11 entrypoint")
raise ValueError("invalid or missing Arm11 entrypoint")

if args.signature:
firmObj.signature = unhexlify(perfectSignatures["firm-" + args.signature])
data = firmObj.build()
args.outfile.write(data)
if args.generate_hash:
with open(args.outfile.name + ".sha", "wb+") as f:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes

H = hashes.Hash(hashes.SHA256(), backend=default_backend())
H.update(data)
f.write(H.finalize())
f.write(SHA256.new(data).digest())

def Uint32(s):
N = 0
Expand Down Expand Up @@ -475,16 +463,15 @@ def main(args=None):
parser_build = subparsers.add_parser("build")
parser_build.set_defaults(func=buildFirm)
parser_build.add_argument("outfile", help="Output firmware file", type=argparse.FileType("wb+"))
parser_build.add_argument("-n", "--arm9-entrypoint", help="ARM9 entrypoint (deduced from the first ELF file having an entrypoint and corresponding to a NDMA-copied \
parser_build.add_argument("-n", "--arm9-entrypoint", help="Arm9 entrypoint (deduced from the first ELF file having an entrypoint and corresponding to a NDMA-copied \
section, otherwise required)", type=Uint32, default=0) # "nine"
parser_build.add_argument("-e", "--arm11-entrypoint", help="ARM11 entrypoint (deduced from the first ELF file having and entrypoint and corresponding to a XDMA-copied \
parser_build.add_argument("-e", "--arm11-entrypoint", help="Arm11 entrypoint (deduced from the first ELF file having and entrypoint and corresponding to a XDMA-copied \
section, otherwise required)", type=Uint32, default=0) # "eleven"
parser_build.add_argument("-D", "--section-data", help="Files containing the data of each section (required)", type=argparse.FileType("rb"), nargs='+', required=True)
parser_build.add_argument("-A", "--section-addresses", help="Loading address of each section (inferred from the corresponding ELF file, otherwise required)",
type=Uint32, nargs='+', default=[])
parser_build.add_argument("-C", "--section-copy-methods", help="Copy method of each section (NDMA, XDMA, memcpy) (required)", choices=("NDMA", "XDMA", "memcpy"), nargs='+', required=True)
parser_build.add_argument("-S", "--signature", help="The kind of the perfect signature to include (default: nand-retail)", choices=("nand-retail", "spi-retail", "nand-dev", "spi-dev"), default="nand-retail")
parser_build.add_argument("-t", "--type", help="Same as --signature", choices=("nand-retail", "spi-retail", "nand-dev", "spi-dev"), default="nand-retail")
parser_build.add_argument("-S", "--signature", "-t", "--type", help="The kind of the perfect signature to include (default: nand-retail)", choices=("nand-retail", "spi-retail", "nand-dev", "spi-dev"), default="nand-retail")
parser_build.add_argument("-g", "--generate-hash", help="Generate a .sha file containing the SHA256 digest of the output file", action="store_true", default=False)
parser_build.add_argument("-i", "--suggest-screen-init", help="Suggest that screen init should be done before launching the output file", action="store_true", default=False)
parser_build.add_argument("-b", "--suggest-skipping-bootrom-lockout", help="Suggest skipping bootrom lockout", action="store_true", default=False)
Expand Down
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()

setup(name='firmtool',
version='1.3',
version='1.4',
description='Parses, extracts, and builds 3DS firmware files',
license='BSD',
keywords='3DS firmware parse extract build',
author='TuxSH',
author_email='tuxsh@sfr.fr',
author_email='lumas@phe.re',
long_description=read('README.md'),
classifiers=[
"Topic :: Utilities",
"License :: OSI Approved :: BSD License",
],
install_requires=['cryptography'],
install_requires=['pycryptodome'],
packages=['firmtool'],
entry_points={ "console_scripts": [ "firmtool=firmtool.__main__:main" ] }
)

0 comments on commit fdc7085

Please sign in to comment.