mirror of
https://github.com/NinjaCheetah/libWiiPy.git
synced 2026-03-05 08:35:28 -05:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
4f96e1b0d9
|
|||
|
bcd61b8a37
|
|||
|
a56fa6e051
|
|||
|
535de7f228
|
|||
|
adac67b158
|
@@ -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]
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user