gobbletools/python1/python1_dumper.py

514 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import argparse
import ctypes
import os
import string
import struct
def get_filename_hash(filename):
hash = 0
for cidx, c in enumerate(filename):
for i in range(6):
hash = ctypes.c_int(((hash >> 31) & 0x4c11db7) ^ ((hash << 1) | ((ord(c) >> i) & 1))).value
return hash & 0xffffffff
def decrypt_data(data):
from Crypto.Cipher import Blowfish
keys = {
'13G0': {'iv': 'desvsrow', 'key': 'パパパヤーぶっちゃけPOP’N大迷惑'},
'13S0': {'iv': 'wacvsjun', 'key': 'いろはにほへぽちりぬるぽ'},
'13P0': {'iv': 'tvCxT5no', 'key': 'キャホーイ!祭りだ!わっしょこい!'},
'13G1': {'iv': 'LTgStteb', 'key': 'いつだってお祭りさわぎのカーニバル。なんでもあるよ〜。'},
'13G2': {'iv': 'wNyxSyDQ', 'key': 'ドンドンチキチキカーニバル!'},
'13G3': {'iv': 'wClBXmGr', 'key': 'ぼくらの街にやってきた素敵で愉快なカーニバル'},
'13G4': {'iv': 'PiWHlj56', 'key': 'さぁさぁ\u3000みなさんお待ちかね\u3000とってもゆかいなカーニバル!'},
'13G5': {'iv': 'dkWEN8Qk', 'key': 'いつだってお祭りさわぎのカーニバル。なんでもあるよ〜。'},
'13G6': {'iv': 'WmPMVqCr', 'key': 'わくわくカーニバル!なにで遊ぶかまよっちゃう〜'},
'13G7': {'iv': '2V00iGp5', 'key': 'リズムにのってボタンを叩けば、わくわくカーニバル・オン・ステージ!'},
'13G8': {'iv': 'WlMhESmP', 'key': 'ようこそ、きらめきカーニバルへ'},
'13G9': {'iv': 'cisXpifl', 'key': 'わくわく\u3000はじまる\u3000カーニバル!'},
'13S1': {'iv': 'C47vuHWv', 'key': 'カーニバルがやってきた'},
'13S2': {'iv': 'DUtTySYu', 'key': 'ポップンカーニバルのお通りだい'},
'13S3': {'iv': 'bLMNB8DN', 'key': 'ポップンカーニバルでココロうきうき'},
'13S4': {'iv': 'o5INlY6V', 'key': 'さわげやポップン'},
'13S5': {'iv': '2brvTlYu', 'key': '集まれポップンカーニバル'},
'13S6': {'iv': 'grnOhljs', 'key': 'ポップンサーカスが街にやってきた!'},
'13S7': {'iv': '43fFbQqx', 'key': '吃驚仰天摩訶不思議'},
'13S8': {'iv': 'T9je5pa5', 'key': 'ぼくらの街にやってきた素敵で愉快なカーニバル'},
'13S9': {'iv': 'zzAqUyUa', 'key': '摩訶不思議な宴をお楽しみあそばせ。'},
'13P1': {'iv': 'XfXeyeGD', 'key': 'ぼくもわたしもお祭り大好き'},
'13P2': {'iv': 'hUgqKFXC', 'key': 'うかれてはじけて'},
'13P3': {'iv': 'MgOREWWd', 'key': 'みんなの町にやってきた!'},
'13P4': {'iv': 'vvDq1vyZ', 'key': '夢のチケット発売中。'},
'13P5': {'iv': 'KvdgJSRY', 'key': '聞こえてくるよ遠くから'},
'13P6': {'iv': 'K8osNm4z', 'key': '乙女心に恋心'},
'13P7': {'iv': 'AToePkrl', 'key': 'さぁさぁみんな寄っといで!'},
'13P8': {'iv': 'N2JPvhOd', 'key': '楽しい音楽ショー'},
'13P9': {'iv': 'NZuuPltz', 'key': '火の輪くぐりに\u3000トライアゲエイン'},
'13A0': {'iv': 'rP2eb6LK', 'key': 'さあ!ハッピータイム'},
'13A1': {'iv': 'X9YNso6d', 'key': 'サーカスがぼくらの街にやってくるくるミラクル'},
'13A2': {'iv': 'oP2U6O7R', 'key': '世界じゅうの音楽のどきわく大行進ー♪お祭り騒ぎはまだまだ続くよ。'},
'13A3': {'iv': 'MzkBVL9m', 'key': 'すてきなショーの\u3000\u3000'},
'13A4': {'iv': 'AbzKfkud', 'key': '見てるだけじゃ楽しくないない'},
'13A5': {'iv': 'sqYSM25b', 'key': 'みんなでお祭り盛り上げろ。'},
'13A6': {'iv': 'R63VSaLE', 'key': 'あの娘もポワンと頬染める。'},
'13A7': {'iv': 'K5eDOnXm', 'key': 'ひとときの夢をごらんあれ〜'},
'13A8': {'iv': 'ww6JPyWf', 'key': '1人で・2人で・3人で'},
'13A9': {'iv': 'XS7JUaUJ', 'key': 'うっふんもあるでよ〜。'},
}
try:
key_info = keys[data[:4].decode('ascii')]
enc_len = int.from_bytes(data[4:8], 'little')
cipher = Blowfish.new(key_info['key'].encode('shift-jis'), Blowfish.MODE_CBC, key_info['iv'].encode('ascii'))
dec_data = cipher.decrypt(data)[8:]
return bytearray(dec_data)[:enc_len]
except:
# Not encrypted?
return data
def decompress_gcz(data):
data_length = len(data)
offset = 0
output = []
while offset < data_length:
flag = data[offset]
offset += 1
for bit in range(8):
if flag & (1 << bit):
output.append(data[offset])
offset += 1
else:
if offset + 2 > data_length:
break
cmd1, cmd2 = data[offset:offset+2]
lookback_length = (cmd1 & 0x0f) + 3
lookback_offset = ((cmd1 & 0xf0) << 4) + cmd2
offset += 2
if cmd1 == 0 and cmd2 == 0:
break
for _ in range(lookback_length):
loffset = len(output) - lookback_offset
if loffset <= 0 or loffset >= len(output):
output.append(0)
else:
output.append(output[loffset])
return bytearray(output)
def get_file_data(infile, fileinfo):
cur_offset = infile.tell()
infile.seek(fileinfo['offset'])
data = infile.read(fileinfo['size'])
infile.seek(cur_offset)
return data
def parse_system_idx(data):
filename_hashes_add = {}
count = int.from_bytes(data[6:8], byteorder="little")
stroff = int.from_bytes(data[8:10], byteorder="little") - 0x180
for i in range(count):
filename = data[stroff+i*0x20:stroff+(i+1)*0x20].decode('ascii').strip('\0').lstrip('/')
filename = "image/%s" % filename.lower()
filename_hashes_add[get_filename_hash(filename)] = filename
return filename_hashes_add
def find_strings_binary(exe_data, search_key):
found_strings = []
i = 0
while i < len(exe_data) - len(search_key):
if exe_data[i:i+len(search_key)] == search_key or exe_data[i:i+len(search_key)] == search_key.upper() or exe_data[i:i+len(search_key)] == search_key.lower():
str_start = i
str_end = i + len(search_key)
while exe_data[str_end] != 0:
str_end += 1
while chr(exe_data[str_start - 1]) in string.printable:
str_start -= 1
found_str = exe_data[str_start:str_end].decode('ascii').lower()
if found_str.startswith("disk0:/"):
found_str = found_str[len("disk0:/"):]
if found_str.startswith("disk0:"):
found_str = found_str[len("disk0:"):]
if found_str[0] == '/':
found_str = found_str[1:]
if '%' not in found_str:
found_strings.append(found_str)
i = str_end + 1
else:
i += 1
return found_strings
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument('-i', '--input', help='Input folder', default=None, required=True)
parser.add_argument('-o', '--output', help='Output folder', default=None)
parser.add_argument('-d', '--decrypt', help='Decryption for pop\'n 13 and 14', default=False, action='store_true')
args = parser.parse_args()
if not os.path.exists(args.output):
os.makedirs(args.output)
entries = []
with open(args.input, "rb") as infile:
system_files = []
# Not sure how this really works
for offset in [0x20]:
infile.seek(offset)
sysfile_offset = int.from_bytes(infile.read(4), byteorder="little")
sysfile_offset2 = int.from_bytes(infile.read(4), byteorder="little")
infile.seek(offset + 0x10)
sysfile_size = int.from_bytes(infile.read(4), byteorder="big")
checksum = int.from_bytes(infile.read(4), byteorder="big")
system_files.append({
'offsets': [x for x in [sysfile_offset, sysfile_offset2] if x != 0],
'size': sysfile_size,
'checksum': checksum,
})
for offset in [0xa0]:
infile.seek(offset)
sysfile_offset = int.from_bytes(infile.read(4), byteorder="little") * 0x100
infile.seek(offset + 0x10)
sysfile_size = int.from_bytes(infile.read(4), byteorder="little")
infile.seek(4, 1)
checksum = int.from_bytes(infile.read(4), byteorder="little")
system_files.append({
'offsets': [x for x in [sysfile_offset] if x != 0],
'size': sysfile_size,
'checksum': checksum,
})
filetable_offset = 0
for offset in [0x100]:
infile.seek(offset)
sysfile_offset = int.from_bytes(infile.read(4), byteorder="little") * 0x100
sysfile_offset2 = int.from_bytes(infile.read(4), byteorder="little") * 0x100
infile.seek(offset + 0x10)
sysfile_size = int.from_bytes(infile.read(4), byteorder="little")
checksum = int.from_bytes(infile.read(4), byteorder="little")
filetable_offset = sysfile_offset + 0x100000
system_files.append({
'offsets': [x for x in [sysfile_offset, sysfile_offset2] if x != 0],
'size': sysfile_size,
'checksum': checksum,
})
for idx, sysfile in enumerate(system_files):
for idx2, offset in enumerate(sysfile['offsets']):
infile.seek(offset)
print(sysfile)
if idx2 == 0:
output_filename = "sysfile_%d.bin" % idx
else:
output_filename = "sysfile_%d_%d.bin" % (idx, idx2)
raw_data = infile.read(sysfile['size'])
with open(os.path.join(args.output, "raw_" + output_filename), "wb") as outfile:
outfile.write(raw_data)
checksum = sum([int.from_bytes(raw_data[i:i+2], 'big') for i in range(0, len(raw_data), 2)]) & 0x7fffffff
if checksum != sysfile.get('checksum', 0):
print("sysfile %s checksum did not match! %08x vs %08x" % (output_filename, checksum, sysfile.get('checksum', 0)))
try:
dec_data = decompress_gcz(raw_data)
with open(os.path.join(args.output, output_filename), "wb") as outfile:
outfile.write(dec_data)
except:
pass
filenames = [
"ee/boot/game.bin",
"boot/game.bin",
"game.bin",
"boot.bin",
"title.txt",
]
exe_data = open(os.path.join(args.output, "sysfile_2.bin"), "rb").read()
print("Filetable Offset: %08x" % filetable_offset)
infile.seek(filetable_offset)
# Find game.bin if it exists. Hacky piece of shit code
filename_hashes = {}
for filename in filenames:
filename_hashes[get_filename_hash(filename)] = filename
while True:
filename_hash, file_offset, file_size, file_unk, checksum, file_unk3 = struct.unpack("<IIIIII", infile.read(0x18))
if filename_hash == 0:
break
file_offset *= 0x200
entry = {
'filename_hash': filename_hash,
'offset': file_offset,
'size': file_size,
'checksum': checksum,
}
entries.append(entry)
for entry in entries:
infile.seek(entry['offset'])
if entry['filename_hash'] in filename_hashes:
output_filename = filename_hashes[entry['filename_hash']]
else:
continue
output_filename = os.path.join(args.output, output_filename)
if os.path.exists(output_filename):
continue
basedir = os.path.dirname(output_filename)
if basedir and not os.path.exists(basedir):
os.makedirs(basedir)
with open(output_filename, "wb") as outfile:
print(entry)
print(output_filename)
data = infile.read(entry['size'])
if args.decrypt:
data = decrypt_data(data)
outfile.write(data)
exe_path = os.path.join(args.output, "ee", "boot", "game.bin")
if os.path.exists(exe_path):
exe_data += open(exe_path, "rb").read()
print("Found", exe_path)
infile.seek(filetable_offset)
found_filenames = [
'libsd.irx',
'mcman.irx',
'mcserv.irx',
'padman.irx',
'sddrviop.irx',
'sio2man.irx',
'snd.irx',
'snd2.irx',
'filesys.irx',
'filesys2.irx',
]
found_filenames += find_strings_binary(exe_data, b".bin")
found_filenames += find_strings_binary(exe_data, b".gcz")
found_filenames += find_strings_binary(exe_data, b".gzz")
found_filenames += find_strings_binary(exe_data, b".irx")
found_filenames += find_strings_binary(exe_data, b".mhd")
found_filenames += find_strings_binary(exe_data, b".idx")
for folder in ["image/", "patch/", "modules/", "irx/", "iop/", "iop/modules", ""]:
for filename in found_filenames + filenames:
filenames.append(folder + filename)
filenames.append((folder + filename).upper())
filenames.append((folder + filename).lower())
# TODO: Find strings that contain number formats and try to bruteforce them automatically
# Specific files from pop'n music 9
for i1 in range(0, 10):
for i2 in range(0, 10):
for i3 in range(0, 100):
filenames.append("md_xx/md_%d%d%02d.dat" % (i1, i2, i3))
for i in range(0, 1000):
for subfolder in ["", "image/"]:
for ext in ["gcz", "gzz"]:
filenames.append("%snorma/d_n_%03d.%s" % (subfolder, i, ext))
filenames.append("%sbg/bg_%03d.%s" % (subfolder, i, ext))
filenames.append("%snt/nt_%03d.%s" % (subfolder, i, ext))
filenames.append("%skc/kc_%03d.%s" % (subfolder, i, ext))
filenames.append("%skc_normal/banner_%02d.%s" % (subfolder, i, ext))
filenames.append("%srj/rj_%03d.%s" % (subfolder, i, ext))
filenames.append("%scg/cg_%03d.%s" % (subfolder, i, ext))
filenames.append("%sha_art/ha_at%03d.%s" % (subfolder, i, ext))
filenames.append("%sha_art/ha_art_%03d.%s" % (subfolder, i, ext))
filenames.append("%sprize_picture/g_%02d.%s" % (subfolder, i, ext))
filename_hashes = {}
for filename in filenames:
filename_hashes[get_filename_hash(filename)] = filename
while True:
filename_hash, file_offset, file_size, file_unk, checksum, file_unk3 = struct.unpack("<IIIIII", infile.read(0x18))
if filename_hash == 0:
break
file_offset *= 0x200
entry = {
'filename_hash': filename_hash,
'offset': file_offset,
'size': file_size,
'checksum': checksum,
}
entries.append(entry)
if entry['filename_hash'] in filename_hashes and filename_hashes[entry['filename_hash']].endswith("system.idx"):
# Parse system.idx for more filenames
filename_hashes.update(parse_system_idx(get_file_data(infile, entry)))
base_filename = filename_hashes[entry['filename_hash']].lstrip("image/").rstrip("/system.idx")
for subfolder in ["", "image/"]:
for ext in ["gcz", "gzz"]:
ha_chara_filename = "%sha_chara/ha_%s.%s" % (subfolder, base_filename, ext)
filename_hashes[get_filename_hash(ha_chara_filename)] = ha_chara_filename
for entry in entries:
infile.seek(entry['offset'])
if entry['filename_hash'] in filename_hashes:
output_filename = filename_hashes[entry['filename_hash']]
else:
output_filename = "%08x.bin" % entry['filename_hash']
output_filename = os.path.join(args.output, output_filename)
if os.path.exists(output_filename):
continue
basedir = os.path.dirname(output_filename)
if basedir and not os.path.exists(basedir):
os.makedirs(basedir)
with open(output_filename, "wb") as outfile:
print(entry)
print(output_filename)
data = infile.read(entry['size'])
if args.decrypt:
data = decrypt_data(data)
outfile.write(data)
checksum = sum(data) & 0xffffffff
if checksum != entry['checksum']:
print("%s checksum did not match! %08x vs %08x" % (output_filename, checksum, entry['checksum']))
# Seems to always be 0x20000000? But later HDDs use encrypted data so the PM09 header won't be found
next_file_offset = 0x20000000
file_id = 0
while True:
infile.seek(next_file_offset)
next_file_offset = infile.tell() + 0x1400000
header = infile.read(4)
if len(header) != 4:
break
if header != b'PM09':
continue
print("Dumping sound data @ %08x" % (infile.tell() - 4))
filecount = int.from_bytes(infile.read(4), byteorder="little")
file_id2 = 0
for i in range(filecount):
file_offset, file_size = struct.unpack("<II", infile.read(8))
file_offset *= 0x200
if file_offset == 0 and file_size == 0:
break
output_folder = os.path.join(args.output, "_data/%04d/" % file_id)
if not os.path.exists(output_folder):
os.makedirs(output_folder)
output_filename = os.path.join(output_folder, "%04d_%d.bin" % (file_id, file_id2))
if os.path.exists(output_filename):
continue
print("Dumping", output_filename)
with open(output_filename, "wb") as outfile:
entry = {
'offset': file_offset,
'size': file_size,
}
data = get_file_data(infile, entry)
if args.decrypt:
data = decrypt_data(data)
try:
data = decompress_gcz(data)
except:
pass
outfile.write(data)
file_id2 += 1
file_id += 1