Skip to content

Instantly share code, notes, and snippets.

@jernejsk
Created February 16, 2025 10:08
Show Gist options
  • Save jernejsk/7290b340ecc636bafd52f80fa3ed9fff to your computer and use it in GitHub Desktop.
Save jernejsk/7290b340ecc636bafd52f80fa3ed9fff to your computer and use it in GitHub Desktop.
import configparser
import binascii
import struct
import sys
signature = b'IMAGEWTY'
header_ver = 0x300
header_size = 96
format_ver = 0x100234
part_magic = b'softw411'
part_ver = 0x200
chunk_size = 1024 * 1024
sparse_magic = 0xED26FF3A
gpt_file = ("12345678", "1234567890___GPT")
script_file = ("12345678", "1234567890SCRIPT")
dlinfo_file = ("12345678", "1234567890DLINFO")
dlmain = "RFSFAT16"
class PhoenixImage:
def __init__(self):
self.files = {}
self.fd = None
def parse(self, fn):
if self.fd:
self.fd.close()
self.fd = open(fn, 'rb')
# validate file size
self.fd.seek(0, 2)
size = self.fd.tell()
self.fd.seek(0, 0)
if size < header_size:
print("File too short!")
return False
# validate common header part
hdr = self.fd.read(32)
sig, hdr_ver, hdr_size, fmt_ver, img_size = struct.unpack("<8sLL4xLL4x", hdr)
if sig != signature:
print("Not an PhoenixCard image! Is it encrypted?")
return False
if hdr_ver != header_ver:
print(f"Unknown header version: {hex(hdr_ver)}")
return False
if hdr_size != header_size:
print(f"Invalid header size! Should be {header_size}, currenty {hdr_size}")
return False
if fmt_ver != format_ver:
print(f"Unknown format version: {hex(fmt_ver)}")
return False
if size < img_size:
print("File is too short!")
return False
# read file count from header v3 part
hdr = self.fd.read(64)
count = struct.unpack('<28xL32x', hdr)[0]
# header is aligned to 1024
self.fd.seek(1024, 0)
self.files = {}
for i in range(count):
hdr = self.fd.read(32)
fn_len, hdr_size, main, sub = struct.unpack("<LL8s16s", hdr)
hdr = self.fd.read(24 + fn_len)
length, offset = struct.unpack(f"<{fn_len + 12}xL4xL", hdr)
self.files[(main.decode('ascii'), sub.decode('ascii'))] = (offset, length)
self.fd.seek(hdr_size - 56 - fn_len, 1)
return True
def read_file(self, part_file):
info = self.files[part_file]
self.fd.seek(info[0], 0)
return self.fd.read(info[1])
def __copy_chunk(self, count, of):
while (count):
to_read = count
if count > chunk_size:
to_read = chunk_size
count -= to_read
data = self.fd.read(to_read)
of.write(data)
def __copy_sparse(self, count, of):
header = struct.unpack("<I4H4I", self.fd.read(28))
bsize = header[5]
for i in range(header[7]):
ctype, _, csize, _ = struct.unpack("<2H2I", self.fd.read(12))
if ctype == 0xcac1:
self.__copy_chunk(csize * bsize, of)
elif ctype == 0xcac2:
data = self.fd.read(4) * (bsize // 4)
for blk in range(csize):
of.write(data)
elif ctype == 0xcac3:
of.seek(csize * bsize, 1)
elif ctype == 0xcac4:
self.fd.read(4)
def copy_part(self, part_file, of, offset):
info = self.files[part_file]
self.fd.seek(info[0], 0)
of.seek(offset, 0)
magic = struct.unpack("<L", self.fd.read(4))[0]
self.fd.seek(info[0], 0)
if magic == sparse_magic:
self.__copy_sparse(info[1], of)
else:
self.__copy_chunk(info[1], of)
def __del__(self):
if self.fd:
self.fd.close()
def parse_partitions(image):
data = image.read_file(dlinfo_file)
hdr = data[:32]
ver, magic, count = struct.unpack("<4xL8sL12x", hdr)
if magic != part_magic:
print(f"Unknown partitions magic {magic.decode('ascii')}")
return []
if ver != part_ver:
print(f"Unknown partitions version: {hex(ver)}")
return []
parts = []
for i in range(count):
offset = 32 + i * 72
info = data[offset : offset + 72]
name, dlname = struct.unpack("<16s16x16s24x", info)
name = name.decode('ascii').strip('\x00')
dlname = dlname.decode('ascii').strip('\x00')
parts.append((name, dlname))
return parts
def parse_gpt(image):
data = image.read_file(gpt_file)
hdr = data[0x200:0x25c]
backup, start, count, part_size = struct.unpack("<32xQ32xQLL4x", hdr)
partitions = {}
offset = 512 * start
for i in range(count):
hdr = data[offset:offset+part_size]
offset += part_size
start, end, name = struct.unpack("<32x2Q8x72s", hdr)
name = name.decode("utf-16").strip('\x00')
partitions[name] = [i, start, end]
if (end + 1 > backup):
backup = end + 1
# this shouldn't be needed, but broken GPT exists
if (start + 1 > backup):
backup = start + 1
return backup, partitions
def write_gpt(image, size, partitions, of):
data = bytearray(image.read_file(gpt_file))
# fix partition ends
for part in partitions.values():
if (part[1] > part[2]):
offset = 0x428 + part[0] * 128
data[offset:offset+8] = struct.pack("<Q", part[1])
# update partitions crc
count = struct.unpack("<L", data[0x250:0x254])[0]
crc = binascii.crc32(data[0x400:0x400 + count * 128]) & 0xFFFFFFFF
data[0x258:0x25c] = struct.pack("<L", crc)
backup = struct.unpack("<Q", data[0x220:0x228])[0]
if (backup < size):
# backup gpt lba
data[0x220:0x228] = struct.pack("<Q", size)
# last usable lba
data[0x230:0x238] = struct.pack("<Q", size - 1)
# update header crc
data[0x210:0x214] = b'\x00\x00\x00\x00'
crc = binascii.crc32(data[0x200:0x25c]) & 0xFFFFFFFF
data[0x210:0x214] = struct.pack("<L", crc)
of.seek(0, 0)
of.write(data)
def parse_script(image):
data = image.read_file(script_file)
config = configparser.ConfigParser()
config.read_string(data.decode("utf-8").strip('\x00'))
boot = []
for section in config.sections():
if section.startswith('boot'):
item = config[section]
boot.append(((item['main'], item['sub']), int(item['start'])))
return boot
def create_image(image, of):
parts = parse_partitions(image)
size, partitions = parse_gpt(image)
boot = parse_script(image)
of.truncate(512 * (size + 1))
write_gpt(image, size, partitions, of)
for name, dlname in parts:
lba = partitions[name][1]
image.copy_part((dlmain, dlname), of, lba * 512)
for item in boot:
image.copy_part(item[0], of, item[1] * 512)
def convert_image(infn, outfn):
image = PhoenixImage()
if not image.parse(infn):
print("Can't parse input image!")
return
with open(outfn, 'wb') as of:
create_image(image, of)
if __name__ == '__main__':
if (len(sys.argv) != 3):
print(f"Usage: {sys.argv[0]} <phoenix image> <output image>")
sys.exit(0)
convert_image(sys.argv[1], sys.argv[2])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment