Skip to content

Instantly share code, notes, and snippets.

@GMMan

GMMan/Program.cs Secret

Created April 23, 2022 04:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GMMan/d274e4fd07ccd51c7d3b85bc3645ec85 to your computer and use it in GitHub Desktop.
Save GMMan/d274e4fd07ccd51c7d3b85bc3645ec85 to your computer and use it in GitHub Desktop.
THEA500 Mini bodypack unpacking
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Security.Cryptography;
using System.Text;
namespace BodUnpack
{
class Program
{
static void Main(string[] args)
{
string inPath = @"C:\projects\a500mini\resources.bod";
string outPath = @"C:\projects\a500mini\resources_unpacked";
int totalEntries = 147;
int dataStartOffset = 0x1f790;
string[] gameNames = new[]
{
"alien_breed_3d",
"alien_breed_se",
"another_world",
"arcade_pool",
"atr",
"battle_chess",
"cadaver",
"california_games",
"dragons_breath",
"f16_combat_pilot",
"kick_off_2",
"paradroid90",
"pinball_dreams",
"project_x_se",
"qwak",
"simon_the_sorcerer",
"speedball_2",
"stunt_car_racer",
"super_cars_ii",
"chaos_engine",
"lost_patrol",
"sentinel",
"titus_the_fox",
"usb", // non-official name
"worms_dc",
"zool",
};
Dictionary<int, string> nameMapping = new Dictionary<int, string>()
{
[0x80] = "font_atlas.bin",
[0x81] = "fragment.glsl",
[0x82] = "vertex.glsl",
[0x83] = "music.mp3",
[0x84] = "splash.mp3",
[0x85] = "ui_atlas.png",
[0x86] = "ctrlatlas.png",
[0x87] = "Roboto-Regular.ttf",
[0x88] = "RobotoMono-Regular.ttf",
[0x89] = "licenses.txt", // non-official name
[0x8a] = "close.wav",
[0x8b] = "flip.wav",
[0x8c] = "open.wav",
[0x8d] = "select.wav",
[0x8e] = "deny.wav",
[0x8f] = "pop.wav",
[0x90] = "select_game.wav",
[0x91] = "click.wav",
};
Dictionary<int, string> extMapping = new Dictionary<int, string>()
{
[0] = ".png",
[1] = ".uae",
[2] = ".uss", // GZipped
[3] = ".bin", // Font atlas
[4] = ".glsl",
[5] = ".mp3",
[6] = ".ttf",
[7] = ".txt",
[8] = ".wav",
};
//List<string> ussNames = new List<string>();
Directory.CreateDirectory(outPath);
using (FileStream fs = File.OpenRead(inPath))
{
if (!VerifyHash(fs)) throw new InvalidDataException("Hash mismatch");
BinaryReader br = new BinaryReader(fs);
fs.Seek(0x20, SeekOrigin.Begin);
List<int> offsets = new List<int>();
for (int i = 0; i < totalEntries; ++i)
{
offsets.Add(br.ReadInt32());
}
for (int i = 0; i < offsets.Count - 1; ++i)
{
// Read metadata
// Each file has some padding and file type field and padding+meta length
// Extra bytes pad out to 8-byte alignment
fs.Seek(dataStartOffset + offsets[i + 1] - 4, SeekOrigin.Begin);
ushort fileType = br.ReadUInt16();
ushort metaSize = br.ReadUInt16();
if (!extMapping.TryGetValue(fileType, out string ext))
{
ext = ".bin";
}
if (!nameMapping.TryGetValue(i, out string fileName))
{
fileName = $"{i}{ext}";
}
if (i < 128)
{
int j = i;
// USB entry doesn't have .uae and .uss files, account for this
if (j >= 118) j += 2;
string append = string.Empty;
switch (j % 5)
{
case 0:
append = "_cover.png";
break;
case 1:
append = "_screen1.png";
break;
case 2:
append = "_screen2.png";
break;
case 3:
append = ".uae";
break;
case 4:
append = ".uss";
break;
}
fileName = $"{gameNames[j / 5]}{append}";
}
Console.WriteLine($"{i} {fileType} {fileName}");
int fileSize = offsets[i + 1] - offsets[i] - metaSize;
fs.Seek(dataStartOffset + offsets[i], SeekOrigin.Begin);
byte[] data = br.ReadBytes(fileSize);
if (fileType == 2)
{
// Try to extract filename
if (data[0] == 0x1f && data[1] == 0x8b && data[3] == 0x08)
{
for (int j = 10; ; ++j)
{
if (data[j] == 0)
{
//fileName = Encoding.ASCII.GetString(data, 10, j - 10);
//ussNames.Add(fileName);
break;
}
}
}
using (MemoryStream ms = new MemoryStream(data))
using (GZipStream gz = new GZipStream(ms, CompressionMode.Decompress))
using (MemoryStream oms = new MemoryStream())
{
gz.CopyTo(oms);
oms.Flush();
data = oms.ToArray();
}
}
File.WriteAllBytes(Path.Combine(outPath, fileName), data);
}
}
//Console.WriteLine();
//foreach (var name in ussNames)
//{
// Console.WriteLine(name);
//}
}
static bool VerifyHash(Stream stream)
{
// 32 bytes @ 0x7414c per Ghidra
byte[] salt = {
// Gotta find your own here
};
using (SHA256 sha = SHA256.Create())
{
sha.TransformBlock(salt, 0, salt.Length, salt, 0);
BinaryReader br = new BinaryReader(stream);
stream.Seek(0x20, SeekOrigin.Begin);
while (stream.Position < stream.Length)
{
int readCount = (int)Math.Min(4096, stream.Length - stream.Position);
byte[] buf = br.ReadBytes(readCount);
if (readCount >= 4096)
sha.TransformBlock(buf, 0, buf.Length, buf, 0);
else
sha.TransformFinalBlock(buf, 0, buf.Length);
}
stream.Seek(0, SeekOrigin.Begin);
byte[] expectedHash = br.ReadBytes(0x20);
for (int i = 0; i < expectedHash.Length; ++i)
{
if (expectedHash[i] != sha.Hash[i]) return false;
}
}
return true;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment