Skip to content

Commit c5d50ea

Browse files
authored
Merge pull request #4 from CyberRoute/c_extension
adding c_extension for arpscan on macosx
2 parents 8bdc44d + 569d650 commit c5d50ea

File tree

4 files changed

+363
-32
lines changed

4 files changed

+363
-32
lines changed

README.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ The tool features a graphical user interface (GUI) built with **PySide6** (Qt fr
1717
## Features
1818
- **Network Scanning**: Identifies devices on the network via ARP requests.
1919
- **Device Details**: Displays IP address, MAC address, hostname, and vendor information.
20-
- **Real-time Sniffing**: Captures and lists ARP packets in real-time.
2120
- **Graphical User Interface**: Easy-to-use UI to display the scanned devices and packet information.
2221
- **Multithreading**: Ensures non-blocking scans using Python's `QThread`.
23-
22+
- **C extension**: for MacOSX there is a C extension that allows slow sequential but very accurate arp scanning
2423
---
2524

2625
## Prerequisites
@@ -29,7 +28,7 @@ Ensure the following dependencies are installed:
2928

3029
1. **Python 3.12 or higher**
3130
2. **scapy**: Used for ARP scanning.
32-
3. **PySide6 or PyQt6**: For building the GUI.
31+
3. **PySide6**: For building the GUI.
3332
4. **netifaces**: To retrieve network interface details.
3433

3534
## Requirements
@@ -70,11 +69,6 @@ Ensure the following dependencies are installed:
7069
sudo `which python3` main.py --interface <interface>
7170
```
7271
73-
Example:
74-
```bash
75-
sudo `which python3` main.py --interface wlp0s20f3
76-
```
77-
7872
On Ubuntu in case you run into this error:
7973
```
8074
(env) alessandro@alessandro-XPS-9315:~/Development/phantom$ sudo /home/alessandro/Development/phantom/env/bin/python3 main.py --interface wlp0s20f3
@@ -87,6 +81,13 @@ Ensure the following dependencies are installed:
8781
```
8882
sudo apt install libxcb-cursor0
8983
```
84+
On Macos there is a C extension that allows accurate but slow arpscan. To build and install the extension:
85+
```
86+
pip install setuptools
87+
cd c_extension
88+
python setup.py build
89+
python setup.py install
90+
```
9091
9192
## Usage Instructions
9293

c_extension/arpscanner.c

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
#define _GNU_SOURCE
2+
3+
#include <Python.h>
4+
#include <stdio.h>
5+
#include <stdlib.h>
6+
#include <string.h>
7+
#include <pcap.h>
8+
#include <arpa/inet.h>
9+
#include <ifaddrs.h>
10+
#include <net/if.h>
11+
#include <net/if_dl.h>
12+
#include <net/ethernet.h>
13+
#include <pthread.h>
14+
#include <unistd.h>
15+
16+
// Define missing ARP constants for macOS
17+
#define ARPHRD_ETHER 1 /* Ethernet hardware format */
18+
#define ARPOP_REQUEST 1 /* ARP request */
19+
#define ARPOP_REPLY 2 /* ARP reply */
20+
21+
#ifdef __APPLE__
22+
struct arphdr {
23+
unsigned short ar_hrd; /* Format of hardware address */
24+
unsigned short ar_pro; /* Format of protocol address */
25+
unsigned char ar_hln; /* Length of hardware address */
26+
unsigned char ar_pln; /* Length of protocol address */
27+
unsigned short ar_op; /* ARP opcode (command) */
28+
};
29+
30+
struct ether_arp {
31+
struct arphdr ea_hdr; /* Fixed-size header */
32+
unsigned char arp_sha[6];/* Sender hardware address */
33+
unsigned char arp_spa[4];/* Sender protocol address */
34+
unsigned char arp_tha[6];/* Target hardware address */
35+
unsigned char arp_tpa[4];/* Target protocol address */
36+
};
37+
#else
38+
#include <net/if_arp.h>
39+
#endif
40+
41+
// Structure to hold ARP response data
42+
typedef struct {
43+
char ip_addr[16];
44+
unsigned char mac_addr[6];
45+
int found;
46+
} arp_response_t;
47+
48+
// Structure for packet capture thread
49+
typedef struct {
50+
pcap_t *handle;
51+
arp_response_t *response;
52+
struct in_addr target_ip;
53+
int timeout_ms;
54+
int finished;
55+
} capture_thread_args_t;
56+
57+
/*
58+
* Function to get MAC address for interface
59+
*/
60+
static int get_mac_address(const char *iface, unsigned char *mac) {
61+
struct ifaddrs *ifap, *ifa;
62+
int found = 0;
63+
64+
if (getifaddrs(&ifap) != 0) {
65+
return -1;
66+
}
67+
68+
for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) {
69+
if (ifa->ifa_addr == NULL) continue;
70+
71+
// Check if this is the interface we want and it's a link-layer address
72+
if ((ifa->ifa_addr->sa_family == AF_LINK) &&
73+
(strcmp(ifa->ifa_name, iface) == 0)) {
74+
struct sockaddr_dl *sdl = (struct sockaddr_dl *)ifa->ifa_addr;
75+
76+
if (sdl->sdl_alen == 6) { // MAC address is 6 bytes
77+
memcpy(mac, LLADDR(sdl), 6);
78+
found = 1;
79+
break;
80+
}
81+
}
82+
}
83+
84+
freeifaddrs(ifap);
85+
return found ? 0 : -1;
86+
}
87+
88+
// Function to convert MAC address to string
89+
static void mac_to_string(unsigned char *mac, char *str) {
90+
snprintf(str, 18, "%02x:%02x:%02x:%02x:%02x:%02x",
91+
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
92+
}
93+
94+
// Packet capture callback function
95+
static void packet_handler(u_char *user_data, const struct pcap_pkthdr *pkthdr, const u_char *packet) {
96+
capture_thread_args_t *args = (capture_thread_args_t *)user_data;
97+
98+
struct ether_header *eth_header = (struct ether_header *)packet;
99+
if (ntohs(eth_header->ether_type) != ETHERTYPE_ARP)
100+
return;
101+
102+
struct ether_arp *arp_packet = (struct ether_arp *)(packet + sizeof(struct ether_header));
103+
if (ntohs(arp_packet->ea_hdr.ar_op) != ARPOP_REPLY)
104+
return;
105+
106+
// Check if this is the response we're looking for
107+
if (memcmp(arp_packet->arp_spa, &args->target_ip.s_addr, 4) == 0) {
108+
memcpy(args->response->mac_addr, arp_packet->arp_sha, 6);
109+
inet_ntop(AF_INET, arp_packet->arp_spa, args->response->ip_addr, 16);
110+
args->response->found = 1;
111+
args->finished = 1;
112+
pcap_breakloop(args->handle);
113+
}
114+
}
115+
116+
// Packet capture thread function
117+
static void *capture_thread(void *arg) {
118+
capture_thread_args_t *args = (capture_thread_args_t *)arg;
119+
pcap_loop(args->handle, -1, packet_handler, (u_char *)args);
120+
return NULL;
121+
}
122+
123+
static PyObject *perform_arp_scan(PyObject *self, PyObject *args) {
124+
char *iface, *src_ip_str, *dst_ip_str;
125+
int timeout_ms = 1000; // Default timeout 1 second
126+
127+
if (!PyArg_ParseTuple(args, "sss|i", &iface, &src_ip_str, &dst_ip_str, &timeout_ms)) {
128+
return NULL;
129+
}
130+
131+
// Get interface MAC address
132+
unsigned char src_mac[6];
133+
if (get_mac_address(iface, src_mac) < 0) {
134+
PyErr_SetString(PyExc_RuntimeError, "Failed to get MAC address");
135+
return NULL;
136+
}
137+
138+
// Convert IP addresses
139+
struct in_addr src_ip, dst_ip;
140+
if (inet_aton(src_ip_str, &src_ip) == 0 || inet_aton(dst_ip_str, &dst_ip) == 0) {
141+
PyErr_SetString(PyExc_ValueError, "Invalid IP address");
142+
return NULL;
143+
}
144+
145+
// Open pcap handle
146+
char errbuf[PCAP_ERRBUF_SIZE];
147+
pcap_t *handle = pcap_open_live(iface, 65536, 1, timeout_ms, errbuf);
148+
if (!handle) {
149+
PyErr_SetString(PyExc_RuntimeError, errbuf);
150+
return NULL;
151+
}
152+
153+
// Set up packet capture filter for ARP
154+
struct bpf_program fp;
155+
char filter_exp[64];
156+
snprintf(filter_exp, sizeof(filter_exp), "arp src host %s", dst_ip_str);
157+
if (pcap_compile(handle, &fp, filter_exp, 0, PCAP_NETMASK_UNKNOWN) == -1) {
158+
pcap_close(handle);
159+
PyErr_SetString(PyExc_RuntimeError, "Failed to compile filter");
160+
return NULL;
161+
}
162+
if (pcap_setfilter(handle, &fp) == -1) {
163+
pcap_freecode(&fp);
164+
pcap_close(handle);
165+
PyErr_SetString(PyExc_RuntimeError, "Failed to set filter");
166+
return NULL;
167+
}
168+
pcap_freecode(&fp);
169+
170+
// Prepare ARP request packet
171+
unsigned char packet[42];
172+
memset(packet, 0, sizeof(packet));
173+
174+
// Fill Ethernet header
175+
struct ether_header *eth_hdr = (struct ether_header *)packet;
176+
memset(eth_hdr->ether_dhost, 0xff, 6);
177+
memcpy(eth_hdr->ether_shost, src_mac, 6);
178+
eth_hdr->ether_type = htons(ETHERTYPE_ARP);
179+
180+
// Fill ARP header
181+
struct ether_arp *arp_hdr = (struct ether_arp *)(packet + sizeof(struct ether_header));
182+
arp_hdr->ea_hdr.ar_hrd = htons(ARPHRD_ETHER);
183+
arp_hdr->ea_hdr.ar_pro = htons(ETHERTYPE_IP);
184+
arp_hdr->ea_hdr.ar_hln = 6;
185+
arp_hdr->ea_hdr.ar_pln = 4;
186+
arp_hdr->ea_hdr.ar_op = htons(ARPOP_REQUEST);
187+
memcpy(arp_hdr->arp_sha, src_mac, 6);
188+
memcpy(arp_hdr->arp_spa, &src_ip.s_addr, 4);
189+
memset(arp_hdr->arp_tha, 0, 6);
190+
memcpy(arp_hdr->arp_tpa, &dst_ip.s_addr, 4);
191+
192+
// Set up response structure and capture thread
193+
arp_response_t response = {0};
194+
capture_thread_args_t thread_args = {
195+
.handle = handle,
196+
.response = &response,
197+
.target_ip = dst_ip,
198+
.timeout_ms = timeout_ms,
199+
.finished = 0
200+
};
201+
202+
// Start capture thread
203+
pthread_t tid;
204+
if (pthread_create(&tid, NULL, capture_thread, &thread_args) != 0) {
205+
pcap_close(handle);
206+
PyErr_SetString(PyExc_RuntimeError, "Failed to create capture thread");
207+
return NULL;
208+
}
209+
210+
// Send ARP request
211+
if (pcap_sendpacket(handle, packet, sizeof(packet)) != 0) {
212+
pthread_cancel(tid);
213+
pcap_close(handle);
214+
PyErr_SetString(PyExc_RuntimeError, "Failed to send ARP packet");
215+
return NULL;
216+
}
217+
218+
// Wait for response or timeout
219+
while (!thread_args.finished && timeout_ms > 0) {
220+
usleep(10000); // Sleep for 10ms
221+
timeout_ms -= 10;
222+
}
223+
224+
// Clean up
225+
pthread_cancel(tid);
226+
pthread_join(tid, NULL);
227+
pcap_close(handle);
228+
229+
// Return results
230+
if (response.found) {
231+
char mac_str[18];
232+
mac_to_string(response.mac_addr, mac_str);
233+
return Py_BuildValue("{s:s,s:s}", "ip", response.ip_addr, "mac", mac_str);
234+
}
235+
236+
Py_RETURN_NONE;
237+
}
238+
239+
// Module method definitions
240+
static PyMethodDef ArpScannerMethods[] = {
241+
{"perform_arp_scan", perform_arp_scan, METH_VARARGS,
242+
"Perform an ARP scan with response handling. Args: interface, src_ip, target_ip, [timeout_ms]"},
243+
{NULL, NULL, 0, NULL}
244+
};
245+
246+
static struct PyModuleDef arpscannermodule = {
247+
PyModuleDef_HEAD_INIT,
248+
"arpscanner",
249+
NULL,
250+
-1,
251+
ArpScannerMethods
252+
};
253+
254+
PyMODINIT_FUNC PyInit_arpscanner(void) {
255+
return PyModule_Create(&arpscannermodule);
256+
}

c_extension/setup.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from setuptools import setup, Extension
2+
import sysconfig
3+
import os
4+
5+
# For macOS, we need to explicitly include libpcap
6+
extra_compile_args = []
7+
extra_link_args = []
8+
9+
if os.uname().sysname == 'Darwin': # macOS
10+
extra_compile_args = ['-I/opt/homebrew/include']
11+
extra_link_args = ['-L/opt/homebrew/lib', '-lpcap']
12+
13+
module = Extension('arpscanner',
14+
sources=['arpscanner.c'],
15+
include_dirs=[sysconfig.get_path('include')],
16+
extra_compile_args=extra_compile_args,
17+
extra_link_args=extra_link_args)
18+
19+
setup(
20+
name='ArpScanner',
21+
version='1.0',
22+
ext_modules=[module]
23+
)

0 commit comments

Comments
 (0)