mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 16:45:28 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4f96e1b0d9
|
|||
|
bcd61b8a37
|
|||
|
a56fa6e051
|
|||
|
535de7f228
|
|||
|
adac67b158
|
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "libWiiPy"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
authors = [
|
||||
{ name="NinjaCheetah", email="ninjacheetah@ncxprogramming.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"
|
||||
requires-python = ">=3.10"
|
||||
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",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
dependencies = [
|
||||
"pycryptodome",
|
||||
"requests"
|
||||
]
|
||||
keywords = ["Wii", "wii"]
|
||||
|
||||
[project.urls]
|
||||
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"
|
||||
|
||||
[build-system]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import io
|
||||
import binascii
|
||||
import hashlib
|
||||
from dataclasses import dataclass as _dataclass
|
||||
from .crypto import decrypt_title_key
|
||||
from typing import List
|
||||
@@ -162,8 +163,7 @@ class Ticket:
|
||||
|
||||
def dump(self) -> bytes:
|
||||
"""
|
||||
Dumps the Ticket object back into bytes. This also sets the raw Ticket attribute of Ticket object to the
|
||||
dumped data, and triggers load() again to ensure that the raw data and object match.
|
||||
Dumps the Ticket object back into bytes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -226,6 +226,40 @@ class Ticket:
|
||||
ticket_data += title_limit_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:
|
||||
"""
|
||||
Gets the Title ID of the ticket's associated title.
|
||||
|
||||
@@ -234,3 +234,18 @@ class Title:
|
||||
"""
|
||||
# Load the decrypted content.
|
||||
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()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import io
|
||||
import binascii
|
||||
import hashlib
|
||||
import struct
|
||||
from typing import List
|
||||
from ..types import _ContentRecord
|
||||
@@ -50,8 +51,10 @@ class TMD:
|
||||
self.reserved2: bytes = b'' # Other "Reserved" data from WiiBrew.
|
||||
self.access_rights: bytes = b''
|
||||
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.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] = []
|
||||
|
||||
def load(self, tmd: bytes) -> None:
|
||||
@@ -128,18 +131,23 @@ class TMD:
|
||||
# Access rights of the title; DVD-video access and AHBPROT.
|
||||
tmd_data.seek(0x1D8)
|
||||
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)
|
||||
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))
|
||||
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.
|
||||
tmd_data.seek(0x1DE)
|
||||
self.num_contents = int.from_bytes(tmd_data.read(2))
|
||||
# The content index that contains the bootable executable.
|
||||
tmd_data.seek(0x1E0)
|
||||
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.
|
||||
self.content_records = []
|
||||
for content in range(0, self.num_contents):
|
||||
@@ -152,8 +160,7 @@ class TMD:
|
||||
|
||||
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,
|
||||
and triggers load() again to ensure that the raw data and object match.
|
||||
Dumps the TMD object back into bytes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -200,16 +207,13 @@ class TMD:
|
||||
# Access rights.
|
||||
tmd_data += self.access_rights
|
||||
# Title version.
|
||||
title_version_high = round(self.title_version / 256)
|
||||
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)
|
||||
tmd_data += int.to_bytes(self.title_version, 2)
|
||||
# Number of contents.
|
||||
tmd_data += int.to_bytes(self.num_contents, 2)
|
||||
# Boot index.
|
||||
tmd_data += int.to_bytes(self.boot_index, 2)
|
||||
# Minor version. Unused so write \x00.
|
||||
tmd_data += b'\x00\x00'
|
||||
# Minor version.
|
||||
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.
|
||||
for content_record in range(self.num_contents):
|
||||
content_data = b''
|
||||
@@ -223,6 +227,36 @@ class TMD:
|
||||
tmd_data += content_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:
|
||||
"""
|
||||
Gets the region of the TMD's associated title.
|
||||
|
||||
Reference in New Issue
Block a user