Last active
May 14, 2023 20:11
-
-
Save WitherOrNot/e20d3d6bbdd0aa7f1b7de4b0cb7ec88a to your computer and use it in GitHub Desktop.
Convert WiiU Wii VC games into playable Wii ISOs
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 python3 | |
from struct import pack, unpack, calcsize | |
from Crypto.Cipher import AES | |
from Crypto.Hash import SHA1 | |
from binascii import hexlify, unhexlify | |
from os.path import join, exists | |
from os import makedirs, remove | |
import sys | |
COMMON_KEY = b'\xeb\xe4*"^\x85\x93\xe4H\xd9\xc5Es\x81\xaa\xf7' | |
SECTOR_SIZE = 0x8000 | |
HEADER_SIZE = 0x200 | |
HASH_TABLE_SIZE = 0x400 | |
NFS_SIZE = 0xFA00000 | |
def hexify(s): | |
return hexlify(s).decode("utf-8") | |
def ihexify(n, b): | |
return hex(n)[2:].zfill(b * 2) | |
def aes_decrypt(key, iv, ctext): | |
iv += b"\x00" * (16 - len(iv)) | |
aes = AES.new(key, AES.MODE_CBC, iv) | |
return aes.decrypt(ctext) | |
def aes_encrypt(key, iv, ptext): | |
iv += b"\x00" * (16 - len(iv)) | |
aes = AES.new(key, AES.MODE_CBC, iv) | |
return aes.encrypt(ptext) | |
def sha1(s): | |
sha = SHA1.new() | |
sha.update(s) | |
return sha.digest() | |
def btoint(b): | |
return unpack(">I", b)[0] | |
def readstr(f): | |
s = "" | |
c = f.read(1) | |
while c[0] != 0: | |
s += chr(c[0]) | |
c = f.read(1) | |
return s | |
def combine_nfs(): | |
print("Combining NFS parts...") | |
i = 0 | |
header = b"" | |
with open("hif.nfs", "wb") as cf: | |
while True: | |
chunk_path = join(game_dir, "content", f"hif_{str(i).zfill(6)}.nfs") | |
if not exists(chunk_path): | |
break | |
print(f"Appending hif_{str(i).zfill(6)}.nfs") | |
with open(chunk_path, "rb") as nf: | |
if i == 0: | |
header = nf.read(HEADER_SIZE) | |
cf.write(nf.read()) | |
i += 1 | |
print() | |
return header | |
def decrypt_nfs(): | |
print("Decrypting total NFS...") | |
with open("hif_dec.nfs", "wb") as df, open("hif.nfs", "rb") as ef: | |
size = tot_size = ef.seek(0, 2) | |
ef.seek(0) | |
while size > 0: | |
read_size = min(SECTOR_SIZE, size) | |
sector = aes_decrypt(game_key, b"\x00", ef.read(read_size)) | |
df.write(sector) | |
size -= read_size | |
print() | |
def unpack_nfs(header): | |
print("Unpacking total NFS...") | |
with open("hif_unpack.nfs", "wb") as df, open("hif_dec.nfs", "rb") as ef: | |
parts = btoint(header[0x10:0x14]) | |
pos = 0 | |
for i in range(parts): | |
start = SECTOR_SIZE * btoint(header[0x14 + i * 8:0x18 + i * 8]) | |
length = SECTOR_SIZE * btoint(header[0x18 + i * 8:0x1C + i * 8]) | |
zsize = start - pos | |
print(f"Segment {i}: padding {hex(zsize)} zero bytes...") | |
while zsize > 0: | |
df.write(b"\x00" * SECTOR_SIZE) | |
zsize -= SECTOR_SIZE | |
dsize = length | |
print(f"Segment {i}: writing {hex(dsize)} data bytes...") | |
while dsize > 0: | |
df.write(ef.read(SECTOR_SIZE)) | |
dsize -= SECTOR_SIZE | |
pos = start + length | |
print() | |
def gen_iso(): | |
print("Generating ISO...") | |
print() | |
with open(out_file, "wb") as df, open("hif_unpack.nfs", "rb") as ef: | |
tot_size = ef.seek(0, 2) | |
ef.seek(0) | |
df.write(ef.read(0x40000)) | |
part_table = ef.read(0x20) | |
df.write(part_table) | |
part_info = [] | |
print("Parsing partition info...") | |
for i in range(4): | |
pinfo1 = btoint(part_table[0x0 + i * 8:0x4 + i * 8]) | |
pinfo2 = 0 | |
if pinfo1: | |
pinfo2 = 4 * btoint(part_table[0x4 + i * 8:0x8 + i * 8]) | |
part_info.append([pinfo1, pinfo2]) | |
print(f"PIT Offset: {ihexify(pinfo2, 4)}") | |
print() | |
part_info = sorted(part_info, key=lambda x: x[1]) | |
pit = [] | |
pof = [] | |
pos = 0x40020 | |
for i in range(4): | |
if part_info[i][0]: | |
df.write(ef.read(part_info[i][1] - pos)) | |
pos = part_info[i][1] | |
pit_off = 8 * part_info[i][0] | |
pit_entry = ef.read(pit_off) | |
pit.append(pit_entry) | |
pos += pit_off | |
for j in range(part_info[i][0]): | |
if pit_entry[0x7 + 8 * j] == 0: | |
pof_entry = btoint(pit_entry[0x0 + 8 * j:0x4 + 8 * j]) * 4 | |
pof.append(pof_entry) | |
print(f"Data partition at {ihexify(pof_entry, 4)}") | |
df.write(pit_entry) | |
print() | |
pof = sorted(pof) | |
sizeinf = pof[0] | |
print("Writing partitions...") | |
for pof_entry in pof: | |
df.write(ef.read(pof_entry - pos)) | |
pos = pof_entry | |
df.write(ef.read(0x1BF)) | |
titlekey = ef.read(0x10) | |
df.write(titlekey) | |
df.write(ef.read(0xD)) | |
titleid = ef.read(0x8) | |
df.write(titleid) | |
df.write(ef.read(0xC0)) | |
part_header = ef.read(0x1FD5C) | |
part_size = btoint(part_header[0x18:0x1C]) * 4 | |
print(f"Partition size: {ihexify(part_size, 4)}") | |
df.write(part_header) | |
pos += 0x20000 + part_size | |
titlekey = aes_decrypt(COMMON_KEY, titleid, titlekey) | |
while part_size >= SECTOR_SIZE: | |
hashtable = ef.read(HASH_TABLE_SIZE) | |
hashtable = aes_encrypt(titlekey, b"\x00", hashtable) | |
df.write(hashtable) | |
if ef.tell() >= tot_size: | |
break | |
iv = hashtable[0x3D0:0x3E0] | |
sector = ef.read(SECTOR_SIZE - HASH_TABLE_SIZE) | |
sector = aes_encrypt(titlekey, iv, sector) | |
df.write(sector) | |
part_size -= SECTOR_SIZE | |
print() | |
print("Padding with zeroes...") | |
rest = 0x118240000 - pos | |
if pos > 0x118240000: | |
rest = 0x1FB4E0000 - pos | |
while rest > 0: | |
df.write(b"\x00" * min(rest, SECTOR_SIZE)) | |
rest -= SECTOR_SIZE | |
print() | |
print("Done!") | |
if __name__ == "__main__": | |
if len(sys.argv) < 3: | |
print("Converts WiiU Wii VC games into playable Wii ISOs.") | |
print(f"Usage: {sys.argv[0]} in_dir out_file") | |
exit(1) | |
game_dir = sys.argv[1] | |
out_file = sys.argv[2] | |
with open(join(game_dir, "code", "htk.bin"), "rb") as f: | |
game_key = f.read() | |
header = combine_nfs() | |
decrypt_nfs() | |
remove("hif.nfs") | |
unpack_nfs(header) | |
remove("hif_dec.nfs") | |
gen_iso() | |
remove("hif_unpack.nfs") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment