Skip to content

Commit 5b94ed4

Browse files
feat(handler): add Apple IMG4/IM4P handler
Adds a handler for Apple IMG4 containers (DER-encoded ASN.1). Supports both 0x82 (16-bit length) and 0x83 (24-bit length) DER header variants and extracts the raw IM4P payload.
1 parent aa7e12c commit 5b94ed4

6 files changed

Lines changed: 162 additions & 0 deletions

File tree

docs/handlers.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
| [`AIROHA BT FIRMWARE`](#airoha-bt-firmware) | ARCHIVE | :octicons-alert-fill-12: |
66
| [`ANDROID EROFS`](#android-erofs) | FILESYSTEM | :octicons-check-16: |
77
| [`ANDROID SPARSE`](#android-sparse) | FILESYSTEM | :octicons-check-16: |
8+
| [`APPLE IMG4/IM4P`](#apple-img4im4p) | ARCHIVE | :octicons-alert-fill-12: |
89
| [`AR`](#ar) | ARCHIVE | :octicons-check-16: |
910
| [`ARC`](#arc) | ARCHIVE | :octicons-check-16: |
1011
| [`ARJ`](#arj) | ARCHIVE | :octicons-check-16: |
@@ -167,6 +168,26 @@
167168

168169
- [Android Sparse Image Format Documentation](https://formats.kaitai.io/android_sparse/){ target="_blank" }
169170
- [simg2img Tool](https://github.com/anestisb/android-simg2img){ target="_blank" }
171+
## Apple IMG4/IM4P
172+
173+
!!! warning "Partially supported"
174+
175+
=== "Description"
176+
177+
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.
178+
179+
---
180+
181+
- **Handler type:** Archive
182+
- **Vendor:** Apple
183+
184+
=== "References"
185+
186+
- [libimg4 - Apple open-source IMG4 implementation](https://github.com/apple-oss-distributions/libimg4){ target="_blank" }
187+
188+
=== "Limitations"
189+
190+
- Only IM4P payload extraction is supported; full IMG4 manifests are not verified
170191
## AR
171192

172193
!!! success "Fully supported"

python/unblob/handlers/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .archive import (
1919
zip as ziparchive,
2020
)
21+
from .archive.apple import img4
2122
from .archive.autel import ecc
2223
from .archive.dlink import alpha_encimg, deafbead, encrpted_img, fpkg, shrs
2324
from .archive.engeniustech import engenius
@@ -117,6 +118,7 @@
117118
ar.ARHandler,
118119
arc.ARCHandler,
119120
arj.ARJHandler,
121+
img4.IMG4Handler,
120122
cab.CABHandler,
121123
msi.MsiHandler,
122124
tar.TarUstarHandler,

python/unblob/handlers/archive/apple/__init__.py

Whitespace-only changes.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
from pathlib import Path
2+
3+
from unblob.file_utils import File, FileSystem, InvalidInputFormat, iterate_patterns
4+
from unblob.models import (
5+
Extractor,
6+
ExtractResult,
7+
HandlerDoc,
8+
HandlerType,
9+
HexString,
10+
Reference,
11+
StructHandler,
12+
ValidChunk,
13+
)
14+
15+
# IM4P (Image4 Payload) DER structure inside an IMG4 container:
16+
# SEQUENCE
17+
# IA5String "IM4P" (image type tag)
18+
# IA5String <name> (component name, e.g. "illb")
19+
# OCTET STRING <payload> (compressed or raw binary)
20+
_IM4P_MAGIC = b"IM4P"
21+
_IA5STRING_TAG = 0x16
22+
_OCTET_STRING_TAG = 0x04
23+
24+
25+
def _read_der_length(file: File, offset: int) -> tuple[int, int]:
26+
"""Return (value, header_len) for the DER length field starting at offset."""
27+
file.seek(offset)
28+
first = file.read(1)[0]
29+
if first < 0x80: # short form: the byte itself is the length
30+
return first, 1
31+
num_bytes = first & 0x7F # long form: low 7 bits count the length bytes
32+
return int.from_bytes(file.read(num_bytes), "big"), 1 + num_bytes
33+
34+
35+
class IM4PExtractor(Extractor):
36+
def extract(self, inpath: Path, outdir: Path) -> ExtractResult:
37+
fs = FileSystem(outdir)
38+
39+
with File.from_path(inpath) as file:
40+
for pos in iterate_patterns(file, _IM4P_MAGIC):
41+
# pos is the start of the "IM4P" IA5String content (4 bytes).
42+
p = pos + len(_IM4P_MAGIC)
43+
44+
# component name IA5String: tag, length, name bytes
45+
file.seek(p)
46+
if file.read(1)[0] != _IA5STRING_TAG:
47+
continue
48+
p += 2 + file.read(1)[0]
49+
50+
# payload OCTET STRING: tag, DER length, payload bytes
51+
file.seek(p)
52+
if file.read(1)[0] != _OCTET_STRING_TAG:
53+
continue
54+
size, header_len = _read_der_length(file, p + 1)
55+
payload_start = p + 1 + header_len
56+
57+
available = file.size() - payload_start
58+
fs.carve(
59+
Path(f"{inpath.stem}.bin"),
60+
file,
61+
payload_start,
62+
min(size, available),
63+
)
64+
break
65+
66+
return ExtractResult(reports=fs.problems)
67+
68+
69+
class IMG4Handler(StructHandler):
70+
NAME = "img4"
71+
72+
PATTERNS = [
73+
# 30 — DER SEQUENCE tag
74+
# 82 — long-form length: next 2 bytes encode the length
75+
# ?? ?? — 2-byte big-endian container length (variable)
76+
# 16 04 — IA5String tag + length 4
77+
# 49 4D 34 50 — "IM4P" (image type identifier)
78+
HexString("30 82 ?? ?? 16 04 49 4D 34 50"),
79+
# 30 — DER SEQUENCE tag
80+
# 83 — long-form length: next 3 bytes encode the length
81+
# ?? ?? ?? — 3-byte big-endian container length (variable)
82+
# 16 04 — IA5String tag + length 4
83+
# 49 4D 34 50 — "IM4P" (image type identifier)
84+
HexString("30 83 ?? ?? ?? 16 04 49 4D 34 50"),
85+
]
86+
87+
# DER header covering the worst case (3-byte length form)
88+
C_DEFINITIONS = r"""
89+
typedef struct img4_header {
90+
uint8 tag; // 0x30 (SEQUENCE)
91+
uint8 length_type; // 0x82 or 0x83
92+
uint8 b2; // high byte of length (or high byte for 0x83)
93+
uint8 b3; // low byte of length (or middle byte for 0x83)
94+
uint8 b4; // only valid for 0x83
95+
} img4_header_t;
96+
"""
97+
HEADER_STRUCT = "img4_header_t"
98+
99+
EXTRACTOR = IM4PExtractor()
100+
101+
DOC = HandlerDoc(
102+
name="Apple IMG4/IM4P",
103+
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.",
104+
handler_type=HandlerType.ARCHIVE,
105+
vendor="Apple",
106+
references=[
107+
Reference(
108+
title="libimg4 - Apple open-source IMG4 implementation",
109+
url="https://github.com/apple-oss-distributions/libimg4",
110+
),
111+
],
112+
limitations=[
113+
"Only IM4P payload extraction is supported; full IMG4 manifests are not verified"
114+
],
115+
)
116+
117+
def calculate_chunk(self, file: File, start_offset: int) -> ValidChunk | None:
118+
header = self.parse_header(file)
119+
120+
if header.length_type == 0x82:
121+
length = int.from_bytes([header.b2, header.b3], "big")
122+
total_size = 4 + length
123+
elif header.length_type == 0x83:
124+
length = int.from_bytes([header.b2, header.b3, header.b4], "big")
125+
total_size = 5 + length
126+
else:
127+
raise InvalidInputFormat(
128+
f"IMG4: unexpected DER length type {header.length_type:#x}"
129+
)
130+
131+
return ValidChunk(
132+
start_offset=start_offset, end_offset=start_offset + total_size
133+
)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:02dd05725fc957ef5da319c6a8fb31e2b8ab80ceaaaa0189a31935a6ac148243
3+
size 35
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
version https://git-lfs.github.com/spec/v1
2+
oid sha256:b618477b694662269994f2b7481cbc92cb50aab779cacb1ecd5164337d200bc6
3+
size 17

0 commit comments

Comments
 (0)