Created
February 16, 2025 10:08
-
-
Save jernejsk/7290b340ecc636bafd52f80fa3ed9fff to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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