Skip to content

Instantly share code, notes, and snippets.

@Dobby233Liu
Last active March 13, 2026 17:44
Show Gist options
  • Select an option

  • Save Dobby233Liu/05e643c7034c1a14840d8ffa5e3ae9e1 to your computer and use it in GitHub Desktop.

Select an option

Save Dobby233Liu/05e643c7034c1a14840d8ffa5e3ae9e1 to your computer and use it in GitHub Desktop.
SoonerXTR if it was good
# SoonerXTR: Extracts a HTC EXCA300 system.img
# which presumably uses a modified or early version of YAFFS1
# Author: AyeTSG
# Cleanup by another guy
import os
from ctypes import LittleEndianStructure, c_char, c_uint16, c_uint32
from enum import IntEnum, auto
PAGE_SIZE = 0x800
PAGE_DATA_SIZE = 0x200
def roundr(n, step):
return ((n - 1) // step + 1) * step
def decode_null_terminated_string(b, encoding="ascii"):
return b.split(b"\x00")[0].decode(encoding)
# https://github.com/kempniu/yaffs2/blob/master/yaffs_guts.h
class yaffs_obj_type(IntEnum):
YAFFS_OBJECT_TYPE_UNKNOWN = 0
YAFFS_OBJECT_TYPE_FILE = auto()
YAFFS_OBJECT_TYPE_SYMLINK = auto()
YAFFS_OBJECT_TYPE_DIRECTORY = auto()
YAFFS_OBJECT_TYPE_HARDLINK = auto()
YAFFS_OBJECT_TYPE_SPECIAL = auto()
YAFFS_OBJECTID_ROOT = 1
YAFFS_MAX_NAME_LENGTH = 255
YAFFS_MAX_ALIAS_LENGTH = 159
class yaffs_obj_hdr(LittleEndianStructure):
_pack_ = 1
_fields_ = [
("type", c_uint32),
# for everything
("parent_obj_id", c_uint32),
("sum_no_longer_used", c_uint16),
("name", c_char * (YAFFS_MAX_NAME_LENGTH + 1)),
# for all except hardlinks
("yst_mode", c_uint32),
("yst_uid", c_uint32),
("yst_gid", c_uint32),
("yst_atime", c_uint32),
("yst_mtime", c_uint32),
("yst_ctime", c_uint32),
("unknown", c_uint16), # FORMAT DIFFERENCE
# file only
("file_size_low", c_uint32),
# hardlink only
("equiv_id", c_uint32),
# symlink only
("alias", c_char * (YAFFS_MAX_ALIAS_LENGTH + 1)),
#
("yst_rdev", c_uint32),
]
YAFFS_NOBJECT_BUCKETS = 0x100
# these need to be done manually at the moment, but can easily be done
# using the data that gets output to console
# TODO: I think this can be solved by making the reading process two-passed
DIR_NODE_NAMES = {
(YAFFS_OBJECTID_ROOT - YAFFS_NOBJECT_BUCKETS): "/",
1: "/bin/",
49: "/usr/",
50: "/usr/share/",
51: "/usr/share/bsk/",
54: "/usr/share/zoneinfo/",
57: "/usr/keylayout/",
62: "/usr/cert/",
64: "/usr/keychars/",
69: "/fonts/",
78: "/media/",
79: "/media/audio/",
80: "/media/audio/ringtones/",
89: "/sounds/",
97: "/lib/",
98: "/lib/modules/",
152: "/app/",
197: "/javalib/",
}
def read_entry(img):
cur_obj_id = YAFFS_OBJECTID_ROOT
while True:
# FORMAT DIFFERENCE: I don't think each page contains spare data
chunk_data = img.read(PAGE_SIZE)
if len(chunk_data) == 0:
break
oh = yaffs_obj_hdr.from_buffer_copy(chunk_data[:PAGE_DATA_SIZE])
type_known = True
# if it's a file...
if oh.type == yaffs_obj_type.YAFFS_OBJECT_TYPE_FILE:
# -- GET DIR IDENTIFIER
dir_ident = oh.parent_obj_id
print(f"PARENT OBJ ID: {dir_ident}")
# -- GET FILE NAME
file_name = decode_null_terminated_string(oh.name)
print("FILE: " + file_name)
# -- GET FILE SIZE
file_size_low = oh.file_size_low
# -- READ THE BINARY!
path = "mtd5_system" + DIR_NODE_NAMES[dir_ident - YAFFS_NOBJECT_BUCKETS] + file_name
print(path)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(img.read(file_size_low))
## -- PROCEED TO NEXT ENTRY
img.seek(roundr(img.tell(), PAGE_SIZE))
# if it's a dir...
elif oh.type == yaffs_obj_type.YAFFS_OBJECT_TYPE_DIRECTORY:
# -- GET DIR IDENTIFIER
parent_obj_id = oh.parent_obj_id
print(f"PARENT OBJ ID: {parent_obj_id}")
# -- GET DIR NAME
dir_name = decode_null_terminated_string(oh.name)
print(f"DIR: {dir_name}")
else:
type_known = False
if type_known:
print("OBJ ID: " + str(cur_obj_id))
print()
yield cur_obj_id
if cur_obj_id < YAFFS_NOBJECT_BUCKETS:
cur_obj_id = YAFFS_NOBJECT_BUCKETS
cur_obj_id += 1
with open("mtd5_system.img", "rb") as img:
for _ in read_entry(img):
pass
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment