Skip to content

Instantly share code, notes, and snippets.

@ma5ter
Created February 13, 2026 21:02
Show Gist options
  • Select an option

  • Save ma5ter/2f71eadb81d2d9516e8e35659d19bfcf to your computer and use it in GitHub Desktop.

Select an option

Save ma5ter/2f71eadb81d2d9516e8e35659d19bfcf to your computer and use it in GitHub Desktop.
Creality K1C 2025 Update Image Unpacker
# This script implements the same checking and unpacking logic as upgbox utility
import struct
import os
import time
from typing import List, Optional
import sys
import zlib
def crc32(crc: int, data: bytes, length: int) -> int:
return zlib.crc32(data[:length], crc & 0xFFFFFFFF) & 0xFFFFFFFF
def fprintf(level: int, fmt: str, *args):
"""Default fprintf implementation."""
msg = fmt % args if args else fmt
if level == 0: # Error
print(f"ERROR: {msg}", file=sys.stderr)
elif level == 1: # Warning
print(f"WARNING: {msg}", file=sys.stderr)
else: # Info (level 2)
print(msg)
class Partition:
"""Partition structure."""
def __init__(self):
self.partname = b''
@staticmethod
def unpack(data: bytes):
"""Unpack partition from bytes."""
part = Partition()
# Assuming partition name is 64 bytes to fit the layout
part.partname = data[:64]
return part
class ImageStruct:
"""Image node structure (220 bytes for CRC, 224 total with CRC field)."""
STRUCT_SIZE = 224 # 0xE0
CRC_SIZE = 220 # Everything except no_crc32
def __init__(self):
self.img_name = b''
self.partition = Partition()
self.img_size = 0
self.img_offset = 0
self.write_offset = 0
self.img_crc32 = 0
self.no_crc32 = 0
@staticmethod
def unpack(data: bytes):
"""Unpack image node from bytes."""
if len(data) < ImageStruct.STRUCT_SIZE:
raise ValueError("Insufficient data for image node")
node = ImageStruct()
offset = 0
# img_name (128 bytes) - Main identifier, analogous to PkgHdStr.name
node.img_name = data[offset:offset + 128]
offset += 128
# partition (64 bytes) - Struct containing partname
node.partition = Partition.unpack(data[offset:offset + 64])
offset += 64
# img_size (8 bytes, uint64)
node.img_size = struct.unpack('<Q', data[offset:offset + 8])[0]
offset += 8
# img_offset (8 bytes, uint64)
node.img_offset = struct.unpack('<Q', data[offset:offset + 8])[0]
offset += 8
# write_offset (8 bytes, uint64)
node.write_offset = struct.unpack('<Q', data[offset:offset + 8])[0]
offset += 8
# img_crc32 (4 bytes, uint32)
node.img_crc32 = struct.unpack('<I', data[offset:offset + 4])[0]
offset += 4
# no_crc32 (4 bytes, uint32)
node.no_crc32 = struct.unpack('<I', data[offset:offset + 4])[0]
offset += 4
return node
class Image:
"""Partition image node wrapper class."""
def __init__(self):
self.fd = None
self.index = 0
self.already_ckc = False
self.node = ImageStruct()
self.partname = b''
self.next = None
def check_header_crc(self, raw_data: bytes) -> int:
"""Check image node CRC32."""
# CRC over first 220 bytes
crc = crc32(0, raw_data, ImageStruct.CRC_SIZE)
if crc == self.node.no_crc32:
return 0
partname = self.node.partition.partname.decode('utf-8', errors='ignore').rstrip('\x00')
print(f"Partition {partname} node CRC32 check failed: {hex(self.node.no_crc32)} - {hex(crc)}")
return -1
def check_data_crc(self, fd: int) -> int:
"""Check image data CRC32."""
if self.fd is None:
fprintf(0, "File descriptor is not available")
return -1
try:
os.lseek(fd, self.node.img_offset, os.SEEK_SET)
except OSError as e:
fprintf(0, "Failed to seek to image offset: %s", e)
return -1
try:
rbuf = os.read(fd, self.node.img_size)
rlen = len(rbuf)
except OSError as e:
fprintf(0, "Failed to read package: %s", e)
return -1
if crc32(0, rbuf, rlen) == self.node.img_crc32:
return 0
else:
return -1
def save(self, path: str) -> int:
"""Save image data to a file."""
if self.fd is None:
fprintf(0, "File descriptor is not available")
return -1
# Extract image name and clean it
img_name = self.node.img_name.decode('utf-8', errors='ignore').rstrip('\x00')
if not img_name:
fprintf(0, "Image name is empty")
return -1
# Create output file path
os.makedirs(path, exist_ok=True)
output_path = os.path.join(path, img_name)
# Read image data
try:
os.lseek(self.fd, self.node.img_offset, os.SEEK_SET)
img_data = os.read(self.fd, self.node.img_size)
except OSError as e:
fprintf(0, "Failed to read image data: %s", str(e))
return -1
if len(img_data) != self.node.img_size:
fprintf(0, "Incomplete image data read (expected %d, got %d)", self.node.img_size, len(img_data))
return -1
# Write to file
try:
with open(output_path, 'wb') as f:
if img_data.startswith(b'SCBT'):
sig_path = output_path + '.sig'
with open(sig_path, 'wb') as sig_f:
sig_f.write(img_data[:2048])
f.write(img_data[2048:])
else:
f.write(img_data)
fprintf(2, "Saved image '%s' to %s", img_name, output_path)
return 0
except OSError as e:
fprintf(0, "Failed to write image to %s: %s", output_path, str(e))
return -1
def print_info(self, buffer: List[str]):
"""Format image node info."""
node = self.node
img_size_mb = node.img_size / (1024 * 1024)
img_name = node.img_name.decode('utf-8', errors='ignore').rstrip('\x00')
partname = node.partition.partname.decode('utf-8', errors='ignore').rstrip('\x00')
buffer.append(f"# Node image: {img_name}")
buffer.append(f" Partition: {partname}")
buffer.append(f" Size: {hex(node.img_size)} ({node.img_size}B {img_size_mb:.2f}MB)")
buffer.append(f" Offset: {hex(node.img_offset)} ({node.img_offset})")
buffer.append(f" Write_ofs: {hex(node.write_offset)} ({node.write_offset})")
buffer.append(f" Img crc32: {hex(node.img_crc32)}")
buffer.append(f" Node crc32: {hex(node.no_crc32)}")
buffer.append("")
class Update:
"""Update Package"""
class Head:
"""Package header structure (312 bytes for CRC, 316 total with CRC field)."""
STRUCT_SIZE = 0x13C # 316 bytes
CRC_SIZE = 0x138 # 312 bytes (everything except hd_crc32)
def __init__(self):
self.name = b''
self.soft_desc = b''
self.soft_sn = b''
self.version = b''
self.build_time = 0
self.pack_size = 0
self.img_num = 0
self.img_w_mode = 0
self.hd_crc32 = 0
@staticmethod
def unpack(data: bytes):
"""Unpack package header from bytes."""
if len(data) < Update.Head.STRUCT_SIZE:
raise ValueError("Insufficient data for package header")
head = Update.Head()
offset = 0
# name (128 bytes) - offset 0x00
head.name = data[offset:offset + 128]
offset = 0x80
# soft_desc (64 bytes) - offset 0x80
head.soft_desc = data[offset:offset + 64]
offset = 0xC0
# soft_sn (64 bytes) - offset 0xC0
head.soft_sn = data[offset:offset + 64]
offset = 0x100
# version (32 bytes) - offset 0x100
head.version = data[offset:offset + 32]
offset = 0x120
# build_time (8 bytes, qword) - offset 0x120
head.build_time = struct.unpack('<Q', data[offset:offset + 8])[0]
offset = 0x128
# pack_size (8 bytes, qword) - offset 0x128
head.pack_size = struct.unpack('<Q', data[offset:offset + 8])[0]
offset = 0x130
# img_num (4 bytes, dword) - offset 0x130
head.img_num = struct.unpack('<I', data[offset:offset + 4])[0]
offset = 0x134
# img_w_mode (4 bytes, dword) - offset 0x134
head.img_w_mode = struct.unpack('<I', data[offset:offset + 4])[0]
offset = 0x138
# hd_crc32 (4 bytes, dword) - offset 0x138
head.hd_crc32 = struct.unpack('<I', data[offset:offset + 4])[0]
return head
def __init__(self, upgfile: str):
self.upgfile = upgfile
self.head = Update.Head()
self.images: List[Image] = []
self.fd = None
@staticmethod
def load(upgfile: str, fprintf=None) -> Optional['Update']:
"""Main package check function."""
pack = Update(upgfile)
if not os.path.exists(upgfile):
fprintf(0, "Package file (%s) does not exist or is unreadable", upgfile)
return None
if not os.access(upgfile, os.R_OK):
fprintf(0, "Package file (%s) is not accessible", upgfile)
return None
try:
fd = os.open(upgfile, os.O_RDONLY | (os.O_BINARY if hasattr(os, 'O_BINARY') else 0))
except OSError:
fprintf(0, "Failed to open %s", upgfile)
return None
try:
if pack.load_head(fd) != 0:
return None
pack.images = []
if pack.load_nodes(fd) != 0:
return None
if pack.check_images(fd) != 0:
pack.images.clear()
return None
pack.fd = fd
return pack
except Exception as e:
fprintf(0, "Unexpected error: %s", str(e))
os.close(fd)
return None
def check_head_crc(self, raw_data: bytes) -> int:
"""Check package header CRC32."""
# CRC over first 312 bytes
crc = crc32(0, raw_data, Update.Head.CRC_SIZE)
if crc == self.head.hd_crc32:
return 0
name = self.head.name.decode('utf-8', errors='ignore').rstrip('\x00')
print(f"Package {name} header CRC32 check failed: {hex(self.head.hd_crc32)} - {hex(crc)}")
return -1
def load_head(self, fd: int) -> int:
"""Check package header."""
os.lseek(fd, 0, os.SEEK_SET)
header_data = os.read(fd, Update.Head.STRUCT_SIZE)
if len(header_data) != Update.Head.STRUCT_SIZE:
fprintf(0, "Failed to read %s header", self.upgfile)
return -1
# Unpack structure
try:
self.head = Update.Head.unpack(header_data)
except Exception as e:
fprintf(0, "Failed to unpack header: %s", str(e))
return -1
if self.check_head_crc(header_data):
fprintf(0, "Package file %s header check failed", self.upgfile)
return -1
# Check filename match
filename = os.path.basename(self.upgfile)
pkg_name = self.head.name.decode('utf-8', errors='ignore').rstrip('\x00')
if filename != pkg_name:
fprintf(1, "Package file %s has been modified (original: %s)", self.upgfile, pkg_name)
# Display header info
str_info = []
self.print_info(str_info)
fprintf(2, "%s", "\n" + "\n".join(str_info))
fprintf(2, "Package file %s header check passed", self.upgfile)
return 0
def load_nodes(self, fd: int) -> int:
"""Check all image nodes."""
os.lseek(fd, Update.Head.STRUCT_SIZE, os.SEEK_SET)
num = self.head.img_num
for index in range(num):
node = Image()
node.fd = fd
node.index = index
node.already_ckc = False
node_data = os.read(fd, ImageStruct.STRUCT_SIZE)
if len(node_data) != ImageStruct.STRUCT_SIZE:
fprintf(0, "Failed to read image node")
return -1
# Unpack structure
try:
node.node = ImageStruct.unpack(node_data)
except Exception as e:
fprintf(0, "Failed to unpack node: %s", str(e))
return -1
if node.check_header_crc(node_data):
fprintf(0, "Image node check failed")
return -1
node.partname = node.node.partition.partname
# Add to list
self.images.append(node)
# Display node info
str_info = []
node.print_info(str_info)
fprintf(2, "\n" + "\n".join(str_info))
fprintf(2, "Package file %s node check passed", self.upgfile)
return 0
def check_images(self, fd: int) -> int:
"""Check image content CRCs with duplicate detection."""
for node in self.images:
skip = False
# Check for duplicates
for od in self.images:
if (od.already_ckc and
od.node.img_offset == node.node.img_offset and
od.node.img_size == node.node.img_size and
od.node.img_crc32 == node.node.img_crc32):
skip = True
break
partname = node.partname.decode('utf-8', errors='ignore').rstrip('\x00')
if skip:
node.already_ckc = True
fprintf(2, "# Check index %d image %s skipped", node.index, partname)
else:
if node.check_data_crc(fd):
fprintf(0, "Check index %d image %s failed", node.index, partname)
return -1
node.already_ckc = True
fprintf(2, "# Check index %d image %s passed", node.index, partname)
return 0
def print_info(self, buffer: List[str]):
"""Format package header info."""
head = self.head
build_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(head.build_time))
pack_size_mb = head.pack_size / (1024 * 1024)
name = head.name.decode('utf-8', errors='ignore').rstrip('\x00')
version = head.version.decode('utf-8', errors='ignore').rstrip('\x00')
soft_sn = head.soft_sn.decode('utf-8', errors='ignore').rstrip('\x00')
soft_desc = head.soft_desc.decode('utf-8', errors='ignore').rstrip('\x00')
buffer.append(f"# Package name: {name}")
buffer.append(f" Build time: {build_time}")
buffer.append(f" Size: {hex(head.pack_size)} ({head.pack_size}B {pack_size_mb:.2f}MB)")
buffer.append(f" Image num: {head.img_num}")
buffer.append(f" Write mode: {head.img_w_mode}")
buffer.append(f" Version: {version}")
buffer.append(f" Serial num: {soft_sn}")
buffer.append(f" Desc: {soft_desc}")
buffer.append(f" Head crc32: {hex(head.hd_crc32)}")
buffer.append("")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python unpack.py <update_file>")
sys.exit(1)
update = Update.load(sys.argv[1])
if update is not None:
print("\n=== Package check PASSED! ===\n")
for image in update.images:
image.save('/tmp')
os.close(update.fd)
sys.exit(0)
else:
print("\n=== Package check FAILED! ===")
sys.exit(1)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment