Skip to content

Instantly share code, notes, and snippets.

@WitherOrNot
Last active May 14, 2023 20:11
Show Gist options
  • Save WitherOrNot/e20d3d6bbdd0aa7f1b7de4b0cb7ec88a to your computer and use it in GitHub Desktop.
Save WitherOrNot/e20d3d6bbdd0aa7f1b7de4b0cb7ec88a to your computer and use it in GitHub Desktop.
Convert WiiU Wii VC games into playable Wii ISOs
#!/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