5 Commits

Author SHA1 Message Date
4f96e1b0d9 Add more detailed keys to pyproject.toml 2024-07-17 21:03:08 -04:00
bcd61b8a37 Slightly improve fakesign docstrings 2024-07-17 20:48:16 -04:00
a56fa6e051 Added methods to fakesign a TMD or Ticket 2024-07-17 20:44:04 -04:00
535de7f228 Read/write minor version in tmd module, allows for fakesigning 2024-07-10 20:18:15 +10:00
adac67b158 Change title version handling in tmd module
Now saving the version number (like v513) straight from the TMD and using that to dump the TMD, in case the converted version number (like v2.2) doesn't work right, which mostly applies to the system menu.
2024-07-10 08:11:14 +10:00
4 changed files with 109 additions and 15 deletions

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "libWiiPy" name = "libWiiPy"
version = "0.4.0" version = "0.4.1"
authors = [ authors = [
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" }, { name="NinjaCheetah", email="ninjacheetah@ncxprogramming.com" },
{ name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" } { name="Lillian Skinner", email="lillian@randommeaninglesscharacters.com" }
@@ -9,17 +9,28 @@ description = "A modern Python library for handling files used by the Wii"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
classifiers = [ classifiers = [
"Programming Language :: Python :: 3", # How mature is this project? Common values are
# 3 - Alpha
# 4 - Beta
# 5 - Production/Stable
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
] ]
dependencies = [ dependencies = [
"pycryptodome", "pycryptodome",
"requests" "requests"
] ]
keywords = ["Wii", "wii"]
[project.urls] [project.urls]
Homepage = "https://github.com/NinjaCheetah/libWiiPy" Homepage = "https://github.com/NinjaCheetah/libWiiPy"
Documentation = "https://ninjacheetah.github.io/libWiiPy/"
Repository = "https://github.com/NinjaCheetah/libWiiPy.git"
Issues = "https://github.com/NinjaCheetah/libWiiPy/issues" Issues = "https://github.com/NinjaCheetah/libWiiPy/issues"
[build-system] [build-system]

View File

@@ -5,6 +5,7 @@
import io import io
import binascii import binascii
import hashlib
from dataclasses import dataclass as _dataclass from dataclasses import dataclass as _dataclass
from .crypto import decrypt_title_key from .crypto import decrypt_title_key
from typing import List from typing import List
@@ -162,8 +163,7 @@ class Ticket:
def dump(self) -> bytes: def dump(self) -> bytes:
""" """
Dumps the Ticket object back into bytes. This also sets the raw Ticket attribute of Ticket object to the Dumps the Ticket object back into bytes.
dumped data, and triggers load() again to ensure that the raw data and object match.
Returns Returns
------- -------
@@ -226,6 +226,40 @@ class Ticket:
ticket_data += title_limit_data ticket_data += title_limit_data
return ticket_data return ticket_data
def fakesign(self) -> None:
"""
Fakesigns this Ticket for the trucha bug.
This is done by brute-forcing a Ticket body hash starting with 00, causing it to pass signature verification on
older IOS versions that incorrectly check the hash using strcmp() instead of memcmp(). The signature will also
be erased and replaced with all NULL bytes.
The hash is brute-forced by using the first two bytes of an unused section of the Ticket as a 16-bit integer,
and incrementing that value by 1 until an appropriate hash is found.
This modifies the Ticket object in place. You will need to call this method after any changes, and before
dumping the Ticket object back into bytes.
"""
# Clear the signature, so that the hash derived from it is guaranteed to always be
# '0000000000000000000000000000000000000000'.
self.signature = b'\x00' * 256
current_int = 0
test_hash = ''
while test_hash[:2] != '00':
current_int += 1
# We're using the first 2 bytes of this unused region of the Ticket as a 16-bit integer, and incrementing
# that to brute-force the hash we need.
data_to_edit = self.unknown2
data_to_edit = int.to_bytes(current_int, 2) + data_to_edit[2:]
self.unknown2 = data_to_edit
# Trim off the first 320 bytes, because we're only looking for the hash of the Ticket's body.
# This is a try-except because an OverflowError will be thrown if the number being used to brute-force the
# hash gets too big, as it is only a 16-bit integer. If that happens, then fakesigning has failed.
try:
test_hash = hashlib.sha1(self.dump()[320:]).hexdigest()
except OverflowError:
raise Exception("An error occurred during fakesigning. Ticket could not be fakesigned!")
def get_title_id(self) -> str: def get_title_id(self) -> str:
""" """
Gets the Title ID of the ticket's associated title. Gets the Title ID of the ticket's associated title.

View File

@@ -234,3 +234,18 @@ class Title:
""" """
# Load the decrypted content. # Load the decrypted content.
self.content.load_content(dec_content, index, self.ticket.get_title_key()) self.content.load_content(dec_content, index, self.ticket.get_title_key())
def fakesign(self) -> None:
"""
Fakesigns this Title for the trucha bug.
This is done by brute-forcing a TMD and Ticket body hash starting with 00, causing it to pass signature
verification on older IOS versions that incorrectly check the hash using strcmp() instead of memcmp(). The TMD
and Ticket signatures will also be erased and replaced with all NULL bytes.
This modifies the TMD and Ticket objects that are part of this Title in place. You will need to call this method
after any changes to the TMD or Ticket, and before dumping the Title object into a WAD to ensure that the WAD
is properly fakesigned.
"""
self.tmd.fakesign()
self.ticket.fakesign()

View File

@@ -5,6 +5,7 @@
import io import io
import binascii import binascii
import hashlib
import struct import struct
from typing import List from typing import List
from ..types import _ContentRecord from ..types import _ContentRecord
@@ -50,8 +51,10 @@ class TMD:
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew. self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
self.access_rights: bytes = b'' self.access_rights: bytes = b''
self.title_version: int = 0 # The version of the associated title. self.title_version: int = 0 # The version of the associated title.
self.title_version_converted: int = 0 # The title version in vX.X format.
self.num_contents: int = 0 # The number of contents contained in the associated title. self.num_contents: int = 0 # The number of contents contained in the associated title.
self.boot_index: int = 0 # The content index that contains the bootable executable. self.boot_index: int = 0 # The content index that contains the bootable executable.
self.minor_version: int = 0 # Minor version (unused typically).
self.content_records: List[_ContentRecord] = [] self.content_records: List[_ContentRecord] = []
def load(self, tmd: bytes) -> None: def load(self, tmd: bytes) -> None:
@@ -128,18 +131,23 @@ class TMD:
# Access rights of the title; DVD-video access and AHBPROT. # Access rights of the title; DVD-video access and AHBPROT.
tmd_data.seek(0x1D8) tmd_data.seek(0x1D8)
self.access_rights = tmd_data.read(4) self.access_rights = tmd_data.read(4)
# Calculate the version number by multiplying 0x1DC by 256 and adding 0x1DD. # Version number straight from the TMD.
tmd_data.seek(0x1DC)
self.title_version = int.from_bytes(tmd_data.read(2))
# Calculate the converted version number by multiplying 0x1DC by 256 and adding 0x1DD.
tmd_data.seek(0x1DC) tmd_data.seek(0x1DC)
title_version_high = int.from_bytes(tmd_data.read(1)) * 256 title_version_high = int.from_bytes(tmd_data.read(1)) * 256
tmd_data.seek(0x1DD)
title_version_low = int.from_bytes(tmd_data.read(1)) title_version_low = int.from_bytes(tmd_data.read(1))
self.title_version = title_version_high + title_version_low self.title_version_converted = title_version_high + title_version_low
# The number of contents listed in the TMD. # The number of contents listed in the TMD.
tmd_data.seek(0x1DE) tmd_data.seek(0x1DE)
self.num_contents = int.from_bytes(tmd_data.read(2)) self.num_contents = int.from_bytes(tmd_data.read(2))
# The content index that contains the bootable executable. # The content index that contains the bootable executable.
tmd_data.seek(0x1E0) tmd_data.seek(0x1E0)
self.boot_index = int.from_bytes(tmd_data.read(2)) self.boot_index = int.from_bytes(tmd_data.read(2))
# The minor version of the title (typically unused).
tmd_data.seek(0x1E2)
self.minor_version = int.from_bytes(tmd_data.read(2))
# Get content records for the number of contents in num_contents. # Get content records for the number of contents in num_contents.
self.content_records = [] self.content_records = []
for content in range(0, self.num_contents): for content in range(0, self.num_contents):
@@ -152,8 +160,7 @@ class TMD:
def dump(self) -> bytes: def dump(self) -> bytes:
""" """
Dumps the TMD object back into bytes. This also sets the raw TMD attribute of TMD object to the dumped data, Dumps the TMD object back into bytes.
and triggers load() again to ensure that the raw data and object match.
Returns Returns
------- -------
@@ -200,16 +207,13 @@ class TMD:
# Access rights. # Access rights.
tmd_data += self.access_rights tmd_data += self.access_rights
# Title version. # Title version.
title_version_high = round(self.title_version / 256) tmd_data += int.to_bytes(self.title_version, 2)
tmd_data += int.to_bytes(title_version_high, 1)
title_version_low = self.title_version % 256
tmd_data += int.to_bytes(title_version_low, 1)
# Number of contents. # Number of contents.
tmd_data += int.to_bytes(self.num_contents, 2) tmd_data += int.to_bytes(self.num_contents, 2)
# Boot index. # Boot index.
tmd_data += int.to_bytes(self.boot_index, 2) tmd_data += int.to_bytes(self.boot_index, 2)
# Minor version. Unused so write \x00. # Minor version.
tmd_data += b'\x00\x00' tmd_data += int.to_bytes(self.minor_version, 2)
# Iterate over content records, write them back into raw data, then add them to the TMD. # Iterate over content records, write them back into raw data, then add them to the TMD.
for content_record in range(self.num_contents): for content_record in range(self.num_contents):
content_data = b'' content_data = b''
@@ -223,6 +227,36 @@ class TMD:
tmd_data += content_data tmd_data += content_data
return tmd_data return tmd_data
def fakesign(self) -> None:
"""
Fakesigns this TMD for the trucha bug.
This is done by brute-forcing a TMD body hash starting with 00, causing it to pass signature verification on
older IOS versions that incorrectly check the hash using strcmp() instead of memcmp(). The signature will also
be erased and replaced with all NULL bytes.
The hash is brute-forced by incrementing an unused 16-bit integer in the TMD by 1 until an appropriate hash is
found.
This modifies the TMD object in place. You will need to call this method after any changes, and before dumping
the TMD object back into bytes.
"""
# Clear the signature, so that the hash derived from it is guaranteed to always be
# '0000000000000000000000000000000000000000'.
self.signature = b'\x00' * 256
current_int = 0
test_hash = ''
while test_hash[:2] != '00':
current_int += 1
self.minor_version = current_int
# Trim off the first 320 bytes, because we're only looking for the hash of the TMD's body.
# This is a try-except because an OverflowError will be thrown if the number being used to brute-force the
# hash gets too big, as it is only a 16-bit integer. If that happens, then fakesigning has failed.
try:
test_hash = hashlib.sha1(self.dump()[320:]).hexdigest()
except OverflowError:
raise Exception("An error occurred during fakesigning. TMD could not be fakesigned!")
def get_title_region(self) -> str: def get_title_region(self) -> str:
""" """
Gets the region of the TMD's associated title. Gets the region of the TMD's associated title.