Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/handlers.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| [`AIROHA BT FIRMWARE`](#airoha-bt-firmware) | ARCHIVE | :octicons-alert-fill-12: |
| [`ANDROID EROFS`](#android-erofs) | FILESYSTEM | :octicons-check-16: |
| [`ANDROID SPARSE`](#android-sparse) | FILESYSTEM | :octicons-check-16: |
| [`APPLE IMG4/IM4P`](#apple-img4im4p) | ARCHIVE | :octicons-alert-fill-12: |
| [`AR`](#ar) | ARCHIVE | :octicons-check-16: |
| [`ARC`](#arc) | ARCHIVE | :octicons-check-16: |
| [`ARJ`](#arj) | ARCHIVE | :octicons-check-16: |
Expand Down Expand Up @@ -169,6 +170,26 @@

- [Android Sparse Image Format Documentation](https://formats.kaitai.io/android_sparse/){ target="_blank" }
- [simg2img Tool](https://github.com/anestisb/android-simg2img){ target="_blank" }
## Apple IMG4/IM4P

!!! warning "Partially supported"

=== "Description"

IMG4 is Apple's DER-encoded firmware image container used for signed payloads in the iOS and macOS secure boot chain. An IM4P (Image4 Payload) embeds a compressed or raw binary (typically LZFSE or LZSS) together with metadata used for cryptographic verification.

---

- **Handler type:** Archive
- **Vendor:** Apple

=== "References"

- [libimg4 - Apple open-source IMG4 implementation](https://github.com/apple-oss-distributions/libimg4){ target="_blank" }

=== "Limitations"

- Only IM4P payload extraction is supported; full IMG4 manifests are not verified
## AR

!!! success "Fully supported"
Expand Down
2 changes: 2 additions & 0 deletions python/unblob/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from .archive import (
zip as ziparchive,
)
from .archive.apple import img4
from .archive.autel import ecc
from .archive.dlink import alpha_encimg, deafbead, encrpted_img, fpkg, shrs
from .archive.engeniustech import engenius
Expand Down Expand Up @@ -119,6 +120,7 @@
ar.ARHandler,
arc.ARCHandler,
arj.ARJHandler,
img4.IMG4Handler,
cab.CABHandler,
msi.MsiHandler,
tar.TarUstarHandler,
Expand Down
Empty file.
133 changes: 133 additions & 0 deletions python/unblob/handlers/archive/apple/img4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from pathlib import Path

from unblob.file_utils import File, FileSystem, InvalidInputFormat, iterate_patterns
from unblob.models import (
Extractor,
ExtractResult,
HandlerDoc,
HandlerType,
HexString,
Reference,
StructHandler,
ValidChunk,
)

# IM4P (Image4 Payload) DER structure inside an IMG4 container:
# SEQUENCE
# IA5String "IM4P" (image type tag)
# IA5String <name> (component name, e.g. "illb")
# OCTET STRING <payload> (compressed or raw binary)
_IM4P_MAGIC = b"IM4P"
_IA5STRING_TAG = 0x16
_OCTET_STRING_TAG = 0x04


def _read_der_length(file: File, offset: int) -> tuple[int, int]:
"""Return (value, header_len) for the DER length field starting at offset."""
file.seek(offset)
first = file.read(1)[0]
if first < 0x80: # short form: the byte itself is the length
return first, 1
num_bytes = first & 0x7F # long form: low 7 bits count the length bytes
return int.from_bytes(file.read(num_bytes), "big"), 1 + num_bytes


class IM4PExtractor(Extractor):
def extract(self, inpath: Path, outdir: Path) -> ExtractResult:
fs = FileSystem(outdir)

with File.from_path(inpath) as file:
for pos in iterate_patterns(file, _IM4P_MAGIC):
# pos is the start of the "IM4P" IA5String content (4 bytes).
p = pos + len(_IM4P_MAGIC)

# component name IA5String: tag, length, name bytes
file.seek(p)
if file.read(1)[0] != _IA5STRING_TAG:
continue
p += 2 + file.read(1)[0]

# payload OCTET STRING: tag, DER length, payload bytes
file.seek(p)
if file.read(1)[0] != _OCTET_STRING_TAG:
continue
size, header_len = _read_der_length(file, p + 1)
payload_start = p + 1 + header_len

available = file.size() - payload_start
fs.carve(
Path(f"{inpath.stem}.bin"),
file,
payload_start,
min(size, available),
)
break

return ExtractResult(reports=fs.problems)


class IMG4Handler(StructHandler):
NAME = "img4"

PATTERNS = [
# 30 — DER SEQUENCE tag
# 82 — long-form length: next 2 bytes encode the length
# ?? ?? — 2-byte big-endian container length (variable)
# 16 04 — IA5String tag + length 4
# 49 4D 34 50 — "IM4P" (image type identifier)
HexString("30 82 ?? ?? 16 04 49 4D 34 50"),
# 30 — DER SEQUENCE tag
# 83 — long-form length: next 3 bytes encode the length
# ?? ?? ?? — 3-byte big-endian container length (variable)
# 16 04 — IA5String tag + length 4
# 49 4D 34 50 — "IM4P" (image type identifier)
HexString("30 83 ?? ?? ?? 16 04 49 4D 34 50"),
]

# DER header covering the worst case (3-byte length form)
C_DEFINITIONS = r"""
typedef struct img4_header {
uint8 tag; // 0x30 (SEQUENCE)
uint8 length_type; // 0x82 or 0x83
uint8 b2; // high byte of length (or high byte for 0x83)
uint8 b3; // low byte of length (or middle byte for 0x83)
uint8 b4; // only valid for 0x83
} img4_header_t;
"""
HEADER_STRUCT = "img4_header_t"

EXTRACTOR = IM4PExtractor()

DOC = HandlerDoc(
name="Apple IMG4/IM4P",
description="IMG4 is Apple's DER-encoded firmware image container used for signed payloads in the iOS and macOS secure boot chain. An IM4P (Image4 Payload) embeds a compressed or raw binary (typically LZFSE or LZSS) together with metadata used for cryptographic verification.",
handler_type=HandlerType.ARCHIVE,
vendor="Apple",
references=[
Reference(
title="libimg4 - Apple open-source IMG4 implementation",
url="https://github.com/apple-oss-distributions/libimg4",
),
],
limitations=[
"Only IM4P payload extraction is supported; full IMG4 manifests are not verified"
],
)

def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
header = self.parse_header(file)

if header.length_type == 0x82:
length = int.from_bytes([header.b2, header.b3], "big")
total_size = 4 + length
elif header.length_type == 0x83:
length = int.from_bytes([header.b2, header.b3, header.b4], "big")
total_size = 5 + length
else:
raise InvalidInputFormat(
f"IMG4: unexpected DER length type {header.length_type:#x}"
)

return ValidChunk(
start_offset=start_offset, end_offset=start_offset + total_size
)
3 changes: 3 additions & 0 deletions tests/integration/archive/apple/img4/__input__/sample.img4
Git LFS file not shown
Git LFS file not shown
Loading