Touched
Resident ASMAGICIAN
- 625
- Posts
- 10
- Years
- Age 123
- Seen Feb 1, 2018
While most tools can tell what version a ROM is by looking at the 4-byte code at 0A0h, we have no way of telling whether a ROM is unmodified or not. One possible way to do this is to calculate hashes for each ROM in its unmodified state and compare them to the hashes of other ROMs. This way we can tell whether the target ROM is clean or not. However, getting these hashes can be difficult. In order to do so, you would need many hundreds of ROMs, one for each version and language.
To resolve this problem, I've created a Python script to outsource this procedure. Simply run the script on every clean ROM you own, and post the utility's (JSON encoded) output here. You can run it on either GBA roms (.gba) or on zip files containing them (.zip). This utility will also output some other useful information associated with the ROM.
In order to run the utliity, you'll need Python 3.2 or later.
Sample Usage (asumming python3 is in your path):
Python Source:
Example output (Pokemon Emerald):
Download the attached file or paste the source in the spoiler tag into a Python script.
To resolve this problem, I've created a Python script to outsource this procedure. Simply run the script on every clean ROM you own, and post the utility's (JSON encoded) output here. You can run it on either GBA roms (.gba) or on zip files containing them (.zip). This utility will also output some other useful information associated with the ROM.
In order to run the utliity, you'll need Python 3.2 or later.
Sample Usage (asumming python3 is in your path):
Code:
python3 rominfo.py --pretty rom.gba
Python Source:
Spoiler:
PHP:
#!/usr/bin/python3
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import argparse
import hashlib
import json
import math
import os
import pprint
import shutil
import statistics
import sys
import tempfile
import zipfile
import zlib
NINTENDO_HASH = '6b76f5de82d23e500528ce4b8b1d937df40d2110bab17d5b4651d4312ba2a227'
# ROM Code meanings from GBATEK
locale = {
'J': 'Japanese',
'P': 'European',
'F': 'French',
'S': 'Spanish',
'E': 'USA',
'D': 'German',
'I': 'Italian'
}
game_type = {
'A': 'Legacy',
'B': 'Modern',
'C': 'Unused',
'F': 'NES',
'K': 'Accelerometer',
'P': 'e-Reader',
'R': 'Gyroscope',
'U': 'Solar',
'V': 'Rumble'
}
# Manufacturers list courtesy of HackMew's tool RHEA
manufacturers = {
'01': 'Nintendo',
'02': 'Rocket Games',
'03': 'Imagineer-Zoom',
'04': 'Gray Matter',
'05': 'Zamuse',
'06': 'Falcom',
'07': 'Enix',
'08': 'Capcom',
'09': 'Hot B Co.',
'0A': 'Jaleco',
'0B': 'Coconuts Japan',
'0C': 'Coconuts Japan/G.X.Media',
'0D': 'Micronet',
'0E': 'Technos',
'0F': 'Mebio Software',
'0G': 'Shouei System',
'0H': 'Starfish',
'0J': 'Mitsui Fudosan/Dentsu',
'0L': 'Warashi Inc.',
'0N': 'Nowpro',
'0P': 'Game Village',
'12': 'Infocom',
'13': 'Electronic Arts Japan',
'15': 'Cobra Team',
'16': 'Human/Field',
'17': 'KOEI',
'18': 'Hudson Soft',
'19': 'S.C.P.',
'1A': 'Yanoman',
'1C': 'TecmoProducts',
'1D': 'Japan Glary Business',
'1E': 'Forum/OpenSystem',
'1F': 'Virgin Games (Japan)',
'1G': 'SMDE',
'1J': 'Daikokudenki',
'1P': 'Creatures Inc.',
'1Q': 'TDK Deep Impression',
'20': 'Destination Software',
'21': 'Sunsoft/Tokai Engineering',
'22': 'POW (Planning Office Wada)',
'23': 'Micro World',
'25': 'San-X',
'26': 'Enix',
'27': 'Loriciel/Electro Brain',
'28': 'Kemco Japan',
'29': 'Seta',
'2A': 'Culture Brain',
'2C': 'Palsoft',
'2E': 'Intec',
'2F': 'System Sacom',
'2G': 'Poppo',
'2H': 'Ubisoft Japan',
'2J': 'Media Works',
'2K': 'NEC InterChannel',
'2L': 'Tam',
'2M': 'Jordan',
'2N': 'Smilesoft',
'2Q': 'Mediakite',
'30': 'Viacom',
'31': 'Carrozzeria',
'32': 'Dynamic',
'34': 'Magifact',
'35': 'Hect',
'36': 'Codemasters',
'37': 'Taito/GAGA Communications',
'38': 'Laguna',
'39': 'Telstar Fun & Games',
'3B': 'Arcade Zone Ltd',
'3C': 'Ent. International/Empire Software',
'3D': 'Loriciel',
'3E': 'Gremlin Graphics',
'3F': 'K.Amusement Leasing Co.',
'40': 'Seika Corp.',
'41': 'Ubi Soft Entertainmen',
'42': 'Sunsoft US',
'44': 'Life Fitness',
'46': 'System 3',
'47': 'Spectrum Holobyte',
'49': 'IREM',
'4B': 'Raya Systems',
'4C': 'Renovation Products',
'4D': 'Malibu Games',
'4F': 'Eidos',
'4G': 'Playmates Interactive',
'4J': 'Fox Interactive',
'4K': 'Time Warner Interactive',
'4Q': 'Disney Interactive',
'4S': 'Black Pearl',
'4U': 'Advanced Productions',
'4X': 'GT Interactive',
'4Y': 'RARE',
'4Z': 'Crave Entertainment',
'50': 'Absolute Entertainment',
'51': 'Acclaim',
'52': 'Activision',
'53': 'American Sammy',
'54': 'Take 2 Interactive',
'55': 'Hi Tech',
'56': 'LJN LTD.',
'58': 'Mattel',
'5A': 'Mindscape',
'5B': 'Romstar',
'5C': 'Taxan',
'5D': 'Midway',
'5F': 'American Softworks',
'5G': 'Majesco Sales Inc',
'5H': '3DO',
'5K': 'Hasbro',
'5L': 'NewKidCo',
'5M': 'Telegames',
'5N': 'Metro3D',
'5P': 'Vatical Entertainment',
'5Q': 'LEGO Media',
'5S': 'Xicat Interactive',
'5T': 'Cryo Interactive',
'5W': 'Red Storm Entertainment',
'5X': 'Microids',
'5Z': 'Conspiracy/Swing',
'60': 'Titus',
'61': 'Virgin Interactive',
'62': 'Maxis',
'64': 'LucasArts Entertainment',
'67': 'Ocean',
'69': 'Electronic Arts',
'6B': 'Laser Beam',
'6E': 'Elite Systems',
'6F': 'Electro Brain',
'6G': 'The Learning Company',
'6H': 'BBC',
'6J': 'Software 2000',
'6L': 'BAM! Entertainment',
'6M': 'Studio 3',
'6Q': 'Classified Games',
'6S': 'TDK Mediactive',
'6U': 'DreamCatcher',
'6V': 'JoWood Productions',
'6W': 'SEGA',
'6X': 'Wannado Edition',
'6Y': 'LSP',
'6Z': 'ITE Media',
'70': 'Infogrames',
'71': 'Interplay',
'72': 'JVC (US)',
'73': 'Parker Brothers',
'75': 'Sales Curve (Storm/SCI)',
'78': 'THQ',
'79': 'Accolade',
'7A': 'Triffix Entertainment',
'7C': 'Microprose Software',
'7D': 'Universal Interactive',
'7F': 'Kemco',
'7G': 'Rage Software',
'7H': 'Encore',
'7J': 'Zoo',
'7K': 'BVM',
'7L': 'Simon & Schuster Interactive',
'7M': 'Asmik Ace Entertainment Inc./AIA',
'7N': 'Empire Interactive',
'7Q': 'Jester Interactive',
'7S': 'RockstarGames',
'7T': 'Scholastic',
'7U': 'Ignition Entertainment',
'7V': 'Summitsoft',
'7W': 'Stadlbauer',
'80': 'Misawa',
'81': 'Teichiku',
'82': 'Namco Ltd.',
'83': 'LOZC',
'84': 'KOEI',
'86': 'Tokuma Shoten Intermedia',
'87': 'Tsukuda Original',
'88': 'DATAM-Polystar',
'8B': 'BulletProof Software (BPS)',
'8C': 'Vic Tokai Inc.',
'8E': 'Character Soft',
'8F': "I'Max",
'8G': 'Saurus',
'8J': 'General Entertainment',
'8N': 'Success',
'8P': 'SEGA Japan',
'90': 'Takara Amusement',
'91': 'Chun Soft',
'92': 'Video System',
'93': 'BEC',
'95': 'Varie',
'96': "Yonezawa/S'pal",
'97': 'Kaneko',
'99': 'Victor Interactive Software',
'9A': 'Nichibutsu/Nihon Bussan',
'9B': 'Tecmo',
'9C': 'Imagineer',
'9F': 'Nova',
'9G': 'ottom Up',
'9H': 'TGL',
'9J': 'Hasbro Japan',
'9L': 'Marvelous Entertainmen',
'9N': 'Keynet Inc.',
'9P': 'Hands On Entertainment',
'9Q': 'Telenet',
'A0': 'Hori',
'A1': 'Konami',
'A4': 'K.Amusement Leasing Co.',
'A5': 'Kawada',
'A6': 'Takara',
'A7': 'Technos Japan Corp.',
'A9': 'JVC (Europe/Japan)',
'AA': 'Toei Animation',
'AC': 'Toho',
'AD': 'Namco',
'AF': 'Media Rings Corporation',
'AG': 'J-Wing',
'AH': 'Pioneer LDC',
'AJ': 'KID',
'AK': 'Mediafactory',
'AL': 'Infogrames Hudson',
'AP': 'Kiratto Ludic Inc',
'AQ': 'Acclaim Japan',
'B0': 'ASCII',
'B1': 'Bandai',
'B2': 'Enix',
'B4': 'HAL Laboratory',
'B6': 'SNK',
'B7': 'Pony Canyon (Hanbai/Inc)',
'B9': 'Culture Brain',
'BA': 'Sunsof',
'BB': 'Toshiba EMI',
'BC': 'Data East',
'BD': 'Sammy',
'BF': 'Magical',
'BG': 'Visco',
'BH': 'Compile',
'BJ': 'MTO Inc.',
'BL': 'Sunrise Interactive',
'BN': 'Global A Entertainment',
'BP': 'Fuuki',
'BQ': 'Taito',
'C0': 'Square',
'C3': 'Tokuma Shoten',
'C4': 'Data East',
'C5': 'Tonkin House',
'C6': 'Koei',
'C8': 'Konami/Ultra/Palcom',
'CA': 'NTVIC/VAP',
'CB': 'Meldac',
'CD': 'Pony Canyon (J)/FCI (U)',
'CE': 'Angel',
'CF': 'Boss',
'CJ': 'Axela/Crea-Tech',
'CK': 'Konami Computer Entert. Osaka',
'CM': 'Atlus',
'CN': 'Enterbrain',
'CP': 'Taito/Disco',
'D0': 'Sofel',
'D1': 'Quest',
'D2': 'Sigma',
'D3': 'Ask Kodansha',
'D4': 'Naxat',
'D6': 'Copya System',
'D7': 'Banpresto',
'D9': 'TOMY',
'DA': 'LJN Japan',
'DB': 'NCS',
'DD': 'Human Entertainment',
'DE': 'Altron',
'DF': 'Jaleco',
'DG': 'Gaps Inc.',
'DH': 'Elf',
'DN': 'Jaleco',
'E0': 'Yutaka',
'E2': 'Varie',
'E3': 'T&ESoft',
'E4': 'Epoch',
'E5': 'Athena',
'E7': 'Asmik',
'E8': 'Natsume',
'E9': 'King Records',
'EA': 'Atlus',
'EB': 'Epic/Sony Records (J)',
'EC': 'IGS',
'EE': 'Chatnoir',
'EG': 'Right Stuff',
'EH': 'Spike',
'EL': 'Konami Computer Entert. Tokyo',
'EM': 'Alphadream Corporation',
'EN': 'A Wave',
'F0': 'Motown Software',
'F1': 'Left Field Entertainment',
'F2': 'Extreme Ent. Grp.',
'F3': 'TecMagik',
'F4': 'Cybersoft',
'F9': 'Psygnosis',
'FB': 'Davidson/Western Tech.',
'FE': 'Interactive Vision',
'FK': 'Hip Games',
'FL': 'Aspyr',
'FM': 'iQue',
'FQ': 'XS Games',
'FS': 'Daiwon',
'FT': 'PCCW Japan',
'G1': 'KiKi Co Ltd',
'G4': 'Open Sesame Inc',
'G5': 'Sims',
'G6': 'Broccoli',
'G7': 'Avex',
'G8': 'D3 Publisher',
'G9': 'Konami Computer Entert. Japan',
'GB': 'Square-Enix',
'GD': 'KSG',
'GE': 'Micott & Basara Inc.',
'GF': 'Aruze',
'H2': 'Ertain',
'H3': '',
'HI': ''
}
def verifiy_rom(rom):
'''Check if the file actually is a valid GBA ROM'''
# Read the Nintendo logo data
rom.seek(4)
nintendo = bytearray(rom.read(156))
# Clear debugging bits (2 and 7)
nintendo[152] &= 0b11011110
# Clear the Cartridge Key Number bits (0 and 1)
nintendo[154] &= 0b00111111
# Check hash
h = hashlib.sha256(nintendo)
if h.hexdigest() != NINTENDO_HASH:
return False
# Read stored checksum
rom.seek(0xBD)
checksum = int.from_bytes(rom.read(1), 'little')
# Header checksum
rom.seek(0xA0)
data = rom.read(28)
# Calculate checksum - Algorithm from GBATEK
chk = 0
for byte in data:
chk = chk - byte
chk = (chk - 0x19) & 0xFF
# Compare checksums
if checksum != chk:
return False
return True
def rom_header(rom):
'''Read the GBA ROM header information'''
rom.seek(0xA0)
# Read header
# Structure: https://problemkaputt.de/gbatek.htm#gbacartridgeheader
header = {}
# Read ASCII encoded info
title = rom.read(12).rstrip().decode()
try:
game_code = rom.read(4).decode()
header['game'] = {
'code': game_code,
'title': title,
'type': game_type[game_code[0]],
'locale': locale[game_code[-1]]
}
except KeyError:
print('Game code error')
sys.exit()
except IndexError:
print('Game code error')
sys.exit()
try:
maker_code = rom.read(2).decode()
header['manufacturer'] = {
'code': maker_code,
'name': manufacturers[maker_code]
}
except KeyError:
print('Manufacturer code error')
sys.exit()
# Read other header data - probably useless
rom.seek(0xB3)
header['main_unit_code'] = int.from_bytes(rom.read(1), byteorder='little')
header['device_type'] = int.from_bytes(rom.read(1), byteorder='little')
rom.seek(0xBC)
header['version'] = int.from_bytes(rom.read(1), byteorder='little')
return header
def calculate_hash(rom):
'''Calculate hash digests and CRCs'''
rom.seek(0)
data = rom.read()
output = {}
for algo in hashlib.algorithms_guaranteed:
h = hashlib.new(algo)
h.update(data)
output[algo] = h.hexdigest()
output['crc32'] = '{:0x}'.format(zlib.crc32(data))
output['adler32'] = '{:0x}'.format(zlib.adler32(data))
return output
def calculate_partial_hash(rom, offset, length):
'''Calculate partial hash'''
output = {}
output['data'] = {}
output['offset'] = offset
output['length'] = length
rom.seek(offset)
data = rom.read(length)
for algo in hashlib.algorithms_guaranteed:
h = hashlib.new(algo)
h.update(data)
output['data'][algo] = h.hexdigest()
output['data']['crc32'] = '{:0x}'.format(zlib.crc32(data))
output['data']['adler32'] = '{:0x}'.format(zlib.adler32(data))
return output
def free_space_byte(rom):
'''Guess free space byte'''
rom.seek(-100, 2)
data = list(rom.read(100))
mode = statistics.mode(data)
return mode
def hash_rom(path):
try:
output = {}
ext = os.path.splitext(path)[-1]
if ext == '.gba':
with open(path, 'rb') as rom:
# File size
info = os.stat(path)
size = info.st_size
if not math.log2(size).is_integer():
raise ValueError('Not a power of 2')
if not verifiy_rom(rom):
raise ValueError("Not a rom")
output['header'] = rom_header(rom)
output['hash'] = calculate_hash(rom)
output['size'] = size
output['freespace'] = free_space_byte(rom)
partials = [
(0x100, 0x100),
(0x100, 0x28)
]
output['partial'] = []
for partial in partials:
output['partial'].append(calculate_partial_hash(rom, *partial))
elif ext == '.zip':
with zipfile.ZipFile(path) as archive:
folder = tempfile.mkdtemp()
for file in archive.infolist():
ext = os.path.splitext(file.filename)[-1]
if ext == '.gba':
archive.extract(file, path=folder)
output = hash_rom(os.path.join(folder, file.filename))
shutil.rmtree(folder)
return output
return output
except IOError:
print('Failed to read ROM')
def main():
parser = argparse.ArgumentParser(description='Get information about a GBA ROM.')
parser.add_argument('--pretty', '-p', dest='pretty', action='store_true', help='Pretty-print the result.')
parser.add_argument('rom', action='store', help='ROM file to get information from.')
args = parser.parse_args()
data = hash_rom(args.rom)
if args.pretty:
json.dump(data, sys.stdout, indent=4, sort_keys=True)
else:
json.dump(data, sys.stdout)
if __name__ == '__main__':
main()
Example output (Pokemon Emerald):
Spoiler:
Code:
{
"freespace": 255,
"hash": {
"adler32": "5c036582",
"crc32": "5df70329",
"md5": "7b058a7aea5bfbb352026727ebd87e17",
"sha1": "4c743011d7f9af0fbc1ef1de7bff157dde718f56",
"sha224": "5d9342323c2f45c7deb49f26c6f9ad808b7049b09619c46197de9d00",
"sha256": "482baae087bafc8f60e3d4ad73b54fa9d940d1981e21829914cd72d1192b5ef0",
"sha384": "538a78baa23362cc2c61c1d0fe14ce48ff67dc4dc84ab33bb7520015f3d6c5c49614af3baf1b96a2b68856b83d201636",
"sha512": "a582f7729e1eeded92339b2da6d1f4f32d91ae35f078605bc7ba1f3dc6fae1b10f730f42d319046e218346dcd13d50f4ac40141f39503913cba91cf895da0720"
},
"header": {
"device_type": 0,
"game": {
"code": "BPEE",
"locale": "USA",
"title": "POKEMON EMER",
"type": "Modern"
},
"main_unit_code": 0,
"manufacturer": {
"code": "01",
"name": "Nintendo"
},
"version": 0
},
"partial": [
{
"data": {
"adler32": "a2002eff",
"crc32": "734e655e",
"md5": "8a4dcae4cf9aafebe60949bbcf88c7ed",
"sha1": "2b979d20c07eae6c9a79df8ae098801a6d31ad9d",
"sha224": "80a472dc8d88188e1313d36a869b4ee19c9ad6176597b5d136a08083",
"sha256": "5325bf8a37d93bac774ea9e2aa2c66cc1dc841824a9cf8496f87fc0b527b8eca",
"sha384": "b0535624260ae21c5b6dea6817b9246d923a1e4f51b45166f01615f984be8d6e9fe61124da8c910ce1aae9fea3592e0e",
"sha512": "244a231993727ae56c26b5c878bd50ab97c7f8fb41dcc1d9f8a23c7ce56ac01378c43e83b32b9e99d984c7d2c57fca7d2c587a385f17b94d0399c75ba7f68032"
},
"length": 256,
"offset": 256
},
{
"data": {
"adler32": "bfa7091f",
"crc32": "cacd0a40",
"md5": "8676b2e1b0210b69a952e2d597fbf9e9",
"sha1": "acfc187705496942e1900fcf42aa153c222c7700",
"sha224": "e079d5b9519c8b1d115799467e963af89a8fb021cff47e15a6e18216",
"sha256": "fb521f4041eb6e759eb6bad50ecf56f624db385a5e585f263a97c201006d6b4e",
"sha384": "b655634275021d810cc8da7993588ded1071b3ba1561cc02a9a5bd704c3e3149fb53ffa13bdf843a2e4895f7808beb80",
"sha512": "957d9e997b6ebdf15d8cae7ca17c910e8441944f5dc6f56631a9797c2ffbd0d9d64c8830c0a233da56a293389d885c7d66884474b057d97659b0d368cac14881"
},
"length": 40,
"offset": 256
}
],
"size": 16777216
}
Download the attached file or paste the source in the spoiler tag into a Python script.