dff-py 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dff_py-0.1.0/PKG-INFO +158 -0
- dff_py-0.1.0/README.md +147 -0
- dff_py-0.1.0/dff/__init__.py +7 -0
- dff_py-0.1.0/dff/client.py +211 -0
- dff_py-0.1.0/dff/server.py +343 -0
- dff_py-0.1.0/dff/shm.py +186 -0
- dff_py-0.1.0/dff_py.egg-info/PKG-INFO +158 -0
- dff_py-0.1.0/dff_py.egg-info/SOURCES.txt +10 -0
- dff_py-0.1.0/dff_py.egg-info/dependency_links.txt +1 -0
- dff_py-0.1.0/dff_py.egg-info/top_level.txt +1 -0
- dff_py-0.1.0/pyproject.toml +21 -0
- dff_py-0.1.0/setup.cfg +4 -0
dff_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dff-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple differential fuzzing framework
|
|
5
|
+
Author-email: Justin Traglia <jtraglia@pm.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jtraglia/dff
|
|
8
|
+
Project-URL: Repository, https://github.com/jtraglia/dff
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# DFF Python Implementation
|
|
13
|
+
|
|
14
|
+
A Python implementation of the DFF (Differential Fuzzing Framework) that uses Unix domain sockets and System V shared memory for high-performance IPC.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### From PyPI (once published)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install dff-py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### From Source
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd python
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Python 3.8 or higher
|
|
34
|
+
- Linux or macOS (Windows is not supported due to Unix domain sockets and System V shared memory)
|
|
35
|
+
- System configured for 100 MiB shared memory segments (see main README)
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Client
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dff import Client
|
|
43
|
+
|
|
44
|
+
def process_func(method: str, inputs: list[bytes]) -> bytes:
|
|
45
|
+
"""Process function that handles fuzzing inputs."""
|
|
46
|
+
if method != "sha":
|
|
47
|
+
raise ValueError(f"Unknown method: {method}")
|
|
48
|
+
|
|
49
|
+
# Process the first input (matching Go/Java behavior)
|
|
50
|
+
import hashlib
|
|
51
|
+
return hashlib.sha256(inputs[0]).digest()
|
|
52
|
+
|
|
53
|
+
# Create and run client
|
|
54
|
+
client = Client("python", process_func)
|
|
55
|
+
client.connect()
|
|
56
|
+
client.run()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Server
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from dff import Server
|
|
63
|
+
|
|
64
|
+
def provider() -> list[bytes]:
|
|
65
|
+
"""Generate fuzzing inputs."""
|
|
66
|
+
import random
|
|
67
|
+
size = random.randint(1024, 4096)
|
|
68
|
+
data = bytes(random.randint(0, 255) for _ in range(size))
|
|
69
|
+
return [data]
|
|
70
|
+
|
|
71
|
+
# Create and run server
|
|
72
|
+
server = Server("sha")
|
|
73
|
+
server.run(provider)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
See the `examples/python/` directory for complete working examples:
|
|
79
|
+
|
|
80
|
+
- `client.py` - SHA256 hashing client implementation
|
|
81
|
+
- `server.py` - Fuzzing server with random data provider
|
|
82
|
+
|
|
83
|
+
### Running the Examples
|
|
84
|
+
|
|
85
|
+
Start the server:
|
|
86
|
+
```bash
|
|
87
|
+
./examples/python/server.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
In another terminal, start one or more clients:
|
|
91
|
+
```bash
|
|
92
|
+
./examples/python/client.py
|
|
93
|
+
./examples/python/client.py python2
|
|
94
|
+
./examples/golang/client/client golang
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server will detect any differences in the outputs from different clients.
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
The framework uses:
|
|
102
|
+
- **Unix domain sockets** for control messages and coordination
|
|
103
|
+
- **System V shared memory** for efficient data transfer
|
|
104
|
+
- **Multiple client support** for differential testing
|
|
105
|
+
|
|
106
|
+
### Protocol
|
|
107
|
+
|
|
108
|
+
1. Client connects to server via Unix socket at `/tmp/dff`
|
|
109
|
+
2. Client sends its name
|
|
110
|
+
3. Server responds with:
|
|
111
|
+
- Input shared memory ID (4 bytes, big-endian)
|
|
112
|
+
- Output shared memory ID (4 bytes, big-endian)
|
|
113
|
+
- Method name (up to 64 bytes)
|
|
114
|
+
4. For each fuzzing iteration:
|
|
115
|
+
- Server writes input data to shared memory
|
|
116
|
+
- Server sends message with input count and sizes
|
|
117
|
+
- Client processes data and writes result to output shared memory
|
|
118
|
+
- Client sends result size back to server
|
|
119
|
+
- Server compares results across clients
|
|
120
|
+
|
|
121
|
+
## Performance
|
|
122
|
+
|
|
123
|
+
The Python implementation is functional but slower than compiled language implementations (Go, Rust) due to:
|
|
124
|
+
- Python's Global Interpreter Lock (GIL)
|
|
125
|
+
- Interpreter overhead
|
|
126
|
+
- Dynamic typing
|
|
127
|
+
|
|
128
|
+
For better performance, consider:
|
|
129
|
+
- Using PyPy instead of CPython
|
|
130
|
+
- Implementing compute-heavy processing in C extensions
|
|
131
|
+
- Running multiple client instances
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
### Running Tests
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cd python
|
|
139
|
+
pip install -e .[dev]
|
|
140
|
+
pytest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Code Quality
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Format code
|
|
147
|
+
black dff/
|
|
148
|
+
|
|
149
|
+
# Lint
|
|
150
|
+
ruff dff/
|
|
151
|
+
|
|
152
|
+
# Type checking
|
|
153
|
+
mypy dff/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT License - see the LICENSE file in the root directory.
|
dff_py-0.1.0/README.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# DFF Python Implementation
|
|
2
|
+
|
|
3
|
+
A Python implementation of the DFF (Differential Fuzzing Framework) that uses Unix domain sockets and System V shared memory for high-performance IPC.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### From PyPI (once published)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install dff-py
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### From Source
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd python
|
|
17
|
+
pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Python 3.8 or higher
|
|
23
|
+
- Linux or macOS (Windows is not supported due to Unix domain sockets and System V shared memory)
|
|
24
|
+
- System configured for 100 MiB shared memory segments (see main README)
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Client
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from dff import Client
|
|
32
|
+
|
|
33
|
+
def process_func(method: str, inputs: list[bytes]) -> bytes:
|
|
34
|
+
"""Process function that handles fuzzing inputs."""
|
|
35
|
+
if method != "sha":
|
|
36
|
+
raise ValueError(f"Unknown method: {method}")
|
|
37
|
+
|
|
38
|
+
# Process the first input (matching Go/Java behavior)
|
|
39
|
+
import hashlib
|
|
40
|
+
return hashlib.sha256(inputs[0]).digest()
|
|
41
|
+
|
|
42
|
+
# Create and run client
|
|
43
|
+
client = Client("python", process_func)
|
|
44
|
+
client.connect()
|
|
45
|
+
client.run()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Server
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from dff import Server
|
|
52
|
+
|
|
53
|
+
def provider() -> list[bytes]:
|
|
54
|
+
"""Generate fuzzing inputs."""
|
|
55
|
+
import random
|
|
56
|
+
size = random.randint(1024, 4096)
|
|
57
|
+
data = bytes(random.randint(0, 255) for _ in range(size))
|
|
58
|
+
return [data]
|
|
59
|
+
|
|
60
|
+
# Create and run server
|
|
61
|
+
server = Server("sha")
|
|
62
|
+
server.run(provider)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Examples
|
|
66
|
+
|
|
67
|
+
See the `examples/python/` directory for complete working examples:
|
|
68
|
+
|
|
69
|
+
- `client.py` - SHA256 hashing client implementation
|
|
70
|
+
- `server.py` - Fuzzing server with random data provider
|
|
71
|
+
|
|
72
|
+
### Running the Examples
|
|
73
|
+
|
|
74
|
+
Start the server:
|
|
75
|
+
```bash
|
|
76
|
+
./examples/python/server.py
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
In another terminal, start one or more clients:
|
|
80
|
+
```bash
|
|
81
|
+
./examples/python/client.py
|
|
82
|
+
./examples/python/client.py python2
|
|
83
|
+
./examples/golang/client/client golang
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The server will detect any differences in the outputs from different clients.
|
|
87
|
+
|
|
88
|
+
## Architecture
|
|
89
|
+
|
|
90
|
+
The framework uses:
|
|
91
|
+
- **Unix domain sockets** for control messages and coordination
|
|
92
|
+
- **System V shared memory** for efficient data transfer
|
|
93
|
+
- **Multiple client support** for differential testing
|
|
94
|
+
|
|
95
|
+
### Protocol
|
|
96
|
+
|
|
97
|
+
1. Client connects to server via Unix socket at `/tmp/dff`
|
|
98
|
+
2. Client sends its name
|
|
99
|
+
3. Server responds with:
|
|
100
|
+
- Input shared memory ID (4 bytes, big-endian)
|
|
101
|
+
- Output shared memory ID (4 bytes, big-endian)
|
|
102
|
+
- Method name (up to 64 bytes)
|
|
103
|
+
4. For each fuzzing iteration:
|
|
104
|
+
- Server writes input data to shared memory
|
|
105
|
+
- Server sends message with input count and sizes
|
|
106
|
+
- Client processes data and writes result to output shared memory
|
|
107
|
+
- Client sends result size back to server
|
|
108
|
+
- Server compares results across clients
|
|
109
|
+
|
|
110
|
+
## Performance
|
|
111
|
+
|
|
112
|
+
The Python implementation is functional but slower than compiled language implementations (Go, Rust) due to:
|
|
113
|
+
- Python's Global Interpreter Lock (GIL)
|
|
114
|
+
- Interpreter overhead
|
|
115
|
+
- Dynamic typing
|
|
116
|
+
|
|
117
|
+
For better performance, consider:
|
|
118
|
+
- Using PyPy instead of CPython
|
|
119
|
+
- Implementing compute-heavy processing in C extensions
|
|
120
|
+
- Running multiple client instances
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
### Running Tests
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
cd python
|
|
128
|
+
pip install -e .[dev]
|
|
129
|
+
pytest
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Code Quality
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Format code
|
|
136
|
+
black dff/
|
|
137
|
+
|
|
138
|
+
# Lint
|
|
139
|
+
ruff dff/
|
|
140
|
+
|
|
141
|
+
# Type checking
|
|
142
|
+
mypy dff/
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License - see the LICENSE file in the root directory.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""DFF Client implementation."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import socket
|
|
5
|
+
import struct
|
|
6
|
+
import signal
|
|
7
|
+
import time
|
|
8
|
+
from typing import Callable, List, Optional
|
|
9
|
+
|
|
10
|
+
from .shm import SharedMemory
|
|
11
|
+
|
|
12
|
+
SOCKET_PATH = "/tmp/dff"
|
|
13
|
+
MAX_METHOD_LENGTH = 64
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
ProcessFunc = Callable[[str, List[bytes]], bytes]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Client:
|
|
20
|
+
"""Client encapsulates the client-side behavior for connecting to the fuzzing server."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str, process_func: ProcessFunc):
|
|
23
|
+
"""Initialize a new Client.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
name: The client identifier sent to the server
|
|
27
|
+
process_func: The callback function used to process fuzzing inputs
|
|
28
|
+
"""
|
|
29
|
+
self.name = name
|
|
30
|
+
self.process_func = process_func
|
|
31
|
+
self.conn: Optional[socket.socket] = None
|
|
32
|
+
self.input_shm: Optional[ctypes.Array] = None
|
|
33
|
+
self.output_shm: Optional[ctypes.Array] = None
|
|
34
|
+
self.input_shm_obj: Optional[SharedMemory] = None
|
|
35
|
+
self.output_shm_obj: Optional[SharedMemory] = None
|
|
36
|
+
self.method: str = ""
|
|
37
|
+
self.shutdown = False
|
|
38
|
+
|
|
39
|
+
def connect(self) -> None:
|
|
40
|
+
"""Establish a connection to the fuzzing server.
|
|
41
|
+
|
|
42
|
+
Connects to the server, sends the client name, attaches to shared memory
|
|
43
|
+
segments, and reads the fuzzing method from the server.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ConnectionError: If connection fails
|
|
47
|
+
OSError: If shared memory operations fail
|
|
48
|
+
"""
|
|
49
|
+
max_retries = 10
|
|
50
|
+
retry_delay = 0.1
|
|
51
|
+
|
|
52
|
+
for attempt in range(max_retries):
|
|
53
|
+
try:
|
|
54
|
+
self.conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
55
|
+
self.conn.connect(SOCKET_PATH)
|
|
56
|
+
break
|
|
57
|
+
except (ConnectionError, FileNotFoundError) as e:
|
|
58
|
+
if attempt == max_retries - 1:
|
|
59
|
+
raise ConnectionError(
|
|
60
|
+
f"Failed to connect to server after {max_retries} attempts: {e}"
|
|
61
|
+
)
|
|
62
|
+
time.sleep(retry_delay)
|
|
63
|
+
|
|
64
|
+
if not self.conn:
|
|
65
|
+
raise ConnectionError("Failed to establish connection")
|
|
66
|
+
|
|
67
|
+
self.conn.sendall(self.name.encode())
|
|
68
|
+
|
|
69
|
+
input_shm_id_bytes = self.conn.recv(4)
|
|
70
|
+
if len(input_shm_id_bytes) != 4:
|
|
71
|
+
raise ConnectionError("Failed to read input shared memory ID")
|
|
72
|
+
input_shm_id = struct.unpack(">I", input_shm_id_bytes)[0]
|
|
73
|
+
|
|
74
|
+
self.input_shm_obj = SharedMemory(input_shm_id)
|
|
75
|
+
self.input_shm = self.input_shm_obj.attach()
|
|
76
|
+
|
|
77
|
+
output_shm_id_bytes = self.conn.recv(4)
|
|
78
|
+
if len(output_shm_id_bytes) != 4:
|
|
79
|
+
raise ConnectionError("Failed to read output shared memory ID")
|
|
80
|
+
output_shm_id = struct.unpack(">I", output_shm_id_bytes)[0]
|
|
81
|
+
|
|
82
|
+
self.output_shm_obj = SharedMemory(output_shm_id)
|
|
83
|
+
self.output_shm = self.output_shm_obj.attach()
|
|
84
|
+
|
|
85
|
+
method_bytes = self.conn.recv(MAX_METHOD_LENGTH)
|
|
86
|
+
if not method_bytes:
|
|
87
|
+
raise ConnectionError("Failed to read method name")
|
|
88
|
+
self.method = method_bytes.decode().rstrip('\x00')
|
|
89
|
+
|
|
90
|
+
print(f"Connected with fuzzing method: {self.method}")
|
|
91
|
+
|
|
92
|
+
def run(self) -> None:
|
|
93
|
+
"""Run the client fuzzing loop.
|
|
94
|
+
|
|
95
|
+
Waits for the server to send input sizes, extracts data from shared memory,
|
|
96
|
+
processes it via the provided process_func, writes results to output shared
|
|
97
|
+
memory, and sends back the result size.
|
|
98
|
+
|
|
99
|
+
Raises:
|
|
100
|
+
RuntimeError: If not connected
|
|
101
|
+
Exception: Any exception from the process function
|
|
102
|
+
"""
|
|
103
|
+
if not self.conn or not self.input_shm or not self.output_shm:
|
|
104
|
+
raise RuntimeError("Client not connected")
|
|
105
|
+
|
|
106
|
+
def signal_handler(_signum: int, _frame: object) -> None:
|
|
107
|
+
self.shutdown = True
|
|
108
|
+
print("\nShutting down gracefully...")
|
|
109
|
+
|
|
110
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
111
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
112
|
+
|
|
113
|
+
print("Client running... Press Ctrl+C to exit.")
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
while not self.shutdown:
|
|
117
|
+
# Read the message containing number of inputs and their sizes
|
|
118
|
+
input_msg = self.conn.recv(1024)
|
|
119
|
+
if not input_msg:
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
if len(input_msg) < 4:
|
|
123
|
+
raise ValueError(f"Invalid input message length: {len(input_msg)}")
|
|
124
|
+
|
|
125
|
+
# First 4 bytes: number of inputs
|
|
126
|
+
num_inputs = struct.unpack(">I", input_msg[0:4])[0]
|
|
127
|
+
|
|
128
|
+
# Following bytes: sizes of each input (4 bytes each)
|
|
129
|
+
expected_length = 4 + (num_inputs * 4)
|
|
130
|
+
if len(input_msg) < expected_length:
|
|
131
|
+
raise ValueError(f"Input message too short: expected {expected_length}, got {len(input_msg)}")
|
|
132
|
+
|
|
133
|
+
# Parse all sizes at once
|
|
134
|
+
input_sizes = struct.unpack(f">{num_inputs}I", input_msg[4:4 + num_inputs * 4])
|
|
135
|
+
|
|
136
|
+
inputs: List[bytes] = []
|
|
137
|
+
offset = 0
|
|
138
|
+
for size in input_sizes:
|
|
139
|
+
if offset + size > len(self.input_shm):
|
|
140
|
+
raise ValueError(f"Input size {size} at offset {offset} exceeds buffer")
|
|
141
|
+
# Use string_at to read bytes directly from memory address
|
|
142
|
+
data_ptr = ctypes.addressof(self.input_shm) + offset
|
|
143
|
+
data = ctypes.string_at(data_ptr, size)
|
|
144
|
+
inputs.append(data)
|
|
145
|
+
offset += size
|
|
146
|
+
|
|
147
|
+
try:
|
|
148
|
+
start_time = time.perf_counter()
|
|
149
|
+
result = self.process_func(self.method, inputs)
|
|
150
|
+
elapsed_time = (time.perf_counter() - start_time) * 1000
|
|
151
|
+
print(f"Processing time: {elapsed_time:.2f}ms")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
print(f"Process function error: {e}")
|
|
154
|
+
result = b""
|
|
155
|
+
|
|
156
|
+
if not isinstance(result, bytes):
|
|
157
|
+
raise TypeError(f"Process function must return bytes, got {type(result)}")
|
|
158
|
+
|
|
159
|
+
if len(result) > len(self.output_shm):
|
|
160
|
+
raise ValueError(f"Result size {len(result)} exceeds output buffer")
|
|
161
|
+
|
|
162
|
+
# Write result bytes to output shared memory using memmove for performance
|
|
163
|
+
if isinstance(result, bytes) and len(result) > 0:
|
|
164
|
+
dest_ptr = ctypes.addressof(self.output_shm)
|
|
165
|
+
src = ctypes.c_char_p(result)
|
|
166
|
+
ctypes.memmove(dest_ptr, src, len(result))
|
|
167
|
+
else:
|
|
168
|
+
# Fallback for empty or non-bytes
|
|
169
|
+
self.output_shm[0:len(result)] = result
|
|
170
|
+
|
|
171
|
+
self.conn.sendall(struct.pack(">I", len(result)))
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
if not self.shutdown:
|
|
175
|
+
print(f"Client error: {e}")
|
|
176
|
+
raise
|
|
177
|
+
finally:
|
|
178
|
+
self.close()
|
|
179
|
+
|
|
180
|
+
def close(self) -> None:
|
|
181
|
+
"""Close the client connection and clean up resources."""
|
|
182
|
+
if self.conn:
|
|
183
|
+
try:
|
|
184
|
+
self.conn.close()
|
|
185
|
+
except:
|
|
186
|
+
pass
|
|
187
|
+
self.conn = None
|
|
188
|
+
|
|
189
|
+
if self.input_shm_obj:
|
|
190
|
+
try:
|
|
191
|
+
self.input_shm_obj.detach()
|
|
192
|
+
except:
|
|
193
|
+
pass
|
|
194
|
+
self.input_shm_obj = None
|
|
195
|
+
self.input_shm = None
|
|
196
|
+
|
|
197
|
+
if self.output_shm_obj:
|
|
198
|
+
try:
|
|
199
|
+
self.output_shm_obj.detach()
|
|
200
|
+
except:
|
|
201
|
+
pass
|
|
202
|
+
self.output_shm_obj = None
|
|
203
|
+
self.output_shm = None
|
|
204
|
+
|
|
205
|
+
def __enter__(self) -> "Client":
|
|
206
|
+
"""Context manager entry."""
|
|
207
|
+
return self
|
|
208
|
+
|
|
209
|
+
def __exit__(self, *_args: object) -> None:
|
|
210
|
+
"""Context manager exit."""
|
|
211
|
+
self.close()
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""DFF Server implementation."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import hashlib
|
|
5
|
+
import os
|
|
6
|
+
import socket
|
|
7
|
+
import struct
|
|
8
|
+
import signal
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from typing import Callable, Dict, List, Optional
|
|
12
|
+
from collections import defaultdict
|
|
13
|
+
|
|
14
|
+
from .shm import SharedMemory, IPC_CREAT, IPC_EXCL, IPC_RMID
|
|
15
|
+
|
|
16
|
+
SOCKET_PATH = "/tmp/dff"
|
|
17
|
+
DEFAULT_INPUT_SHM_KEY = 1000
|
|
18
|
+
DEFAULT_SHM_MAX_SIZE = 100 * 1024 * 1024 # 100 MiB
|
|
19
|
+
DEFAULT_SHM_PERM = 0o666
|
|
20
|
+
|
|
21
|
+
ProviderFunc = Callable[[], List[bytes]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ClientEntry:
|
|
25
|
+
"""Represents a connected client."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, name: str, conn: socket.socket, shm_id: int, method: str):
|
|
28
|
+
self.name = name
|
|
29
|
+
self.conn = conn
|
|
30
|
+
self.shm_id = shm_id
|
|
31
|
+
self.shm_buffer = bytearray(DEFAULT_SHM_MAX_SIZE)
|
|
32
|
+
self.method = method
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Server:
|
|
36
|
+
"""Server encapsulates the server-side behavior for the fuzzing framework."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, method: str):
|
|
39
|
+
"""Initialize a new Server.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
method: The fuzzing method name to send to clients
|
|
43
|
+
"""
|
|
44
|
+
self.method = method
|
|
45
|
+
self.input_shm_key = DEFAULT_INPUT_SHM_KEY
|
|
46
|
+
self.shm_max_size = DEFAULT_SHM_MAX_SIZE
|
|
47
|
+
self.shm_perm = DEFAULT_SHM_PERM
|
|
48
|
+
self.clients: Dict[str, ClientEntry] = {}
|
|
49
|
+
self.clients_lock = threading.Lock()
|
|
50
|
+
self.shutdown = False
|
|
51
|
+
self.iteration_count = 0
|
|
52
|
+
self.total_duration = 0.0
|
|
53
|
+
self.input_shm: Optional[SharedMemory] = None
|
|
54
|
+
self.input_shm_buffer: Optional[ctypes.Array] = None
|
|
55
|
+
self.listener: Optional[socket.socket] = None
|
|
56
|
+
|
|
57
|
+
def _cleanup_existing_shm(self) -> None:
|
|
58
|
+
"""Clean up any existing shared memory with our key."""
|
|
59
|
+
try:
|
|
60
|
+
existing_shm = SharedMemory.get(self.input_shm_key)
|
|
61
|
+
existing_shm.remove()
|
|
62
|
+
print(f"Removed existing input shared memory segment with key {self.input_shm_key}")
|
|
63
|
+
except OSError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def _create_shared_memory(self) -> None:
|
|
67
|
+
"""Create the input shared memory segment."""
|
|
68
|
+
self._cleanup_existing_shm()
|
|
69
|
+
|
|
70
|
+
self.input_shm = SharedMemory.create(
|
|
71
|
+
self.input_shm_key,
|
|
72
|
+
self.shm_max_size,
|
|
73
|
+
self.shm_perm
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.input_shm_buffer = self.input_shm.attach()
|
|
77
|
+
|
|
78
|
+
def _handle_client(self, conn: socket.socket, addr: str) -> None:
|
|
79
|
+
"""Handle a new client connection."""
|
|
80
|
+
try:
|
|
81
|
+
# Receive client name
|
|
82
|
+
name_bytes = conn.recv(256)
|
|
83
|
+
if not name_bytes:
|
|
84
|
+
conn.close()
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
client_name = name_bytes.decode().rstrip('\x00')
|
|
88
|
+
# Create output shared memory for this client
|
|
89
|
+
output_shm_key = self.input_shm_key + len(self.clients) + 1
|
|
90
|
+
|
|
91
|
+
# Clean up if it exists
|
|
92
|
+
try:
|
|
93
|
+
existing = SharedMemory.get(output_shm_key)
|
|
94
|
+
existing.remove()
|
|
95
|
+
except OSError:
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
output_shm = SharedMemory.create(
|
|
99
|
+
output_shm_key,
|
|
100
|
+
self.shm_max_size,
|
|
101
|
+
self.shm_perm
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Send input shared memory ID
|
|
105
|
+
conn.sendall(struct.pack(">I", self.input_shm.shmid))
|
|
106
|
+
|
|
107
|
+
# Send output shared memory ID
|
|
108
|
+
conn.sendall(struct.pack(">I", output_shm.shmid))
|
|
109
|
+
|
|
110
|
+
# Send method name
|
|
111
|
+
conn.sendall(self.method.encode())
|
|
112
|
+
|
|
113
|
+
# Store client
|
|
114
|
+
with self.clients_lock:
|
|
115
|
+
if client_name not in self.clients:
|
|
116
|
+
self.clients[client_name] = ClientEntry(
|
|
117
|
+
client_name, conn, output_shm.shmid, self.method
|
|
118
|
+
)
|
|
119
|
+
print(f"Registered new client: {client_name}")
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"Error handling client: {e}")
|
|
123
|
+
conn.close()
|
|
124
|
+
|
|
125
|
+
def _status_updates(self) -> None:
|
|
126
|
+
"""Print status updates every 5 seconds."""
|
|
127
|
+
while not self.shutdown:
|
|
128
|
+
time.sleep(5)
|
|
129
|
+
|
|
130
|
+
if self.iteration_count > 0:
|
|
131
|
+
with self.clients_lock:
|
|
132
|
+
client_names = sorted(self.clients.keys())
|
|
133
|
+
|
|
134
|
+
avg_duration = self.total_duration / self.iteration_count
|
|
135
|
+
total_seconds = int(self.total_duration)
|
|
136
|
+
avg_ms = int(avg_duration * 1000)
|
|
137
|
+
|
|
138
|
+
print(f"Fuzzing Time: {total_seconds}s, Iterations: {self.iteration_count}, "
|
|
139
|
+
f"Average Iteration: {avg_ms}ms, Clients: {','.join(client_names)}")
|
|
140
|
+
|
|
141
|
+
def _accept_clients(self) -> None:
|
|
142
|
+
"""Accept client connections in a separate thread."""
|
|
143
|
+
while not self.shutdown:
|
|
144
|
+
try:
|
|
145
|
+
self.listener.settimeout(1.0)
|
|
146
|
+
conn, addr = self.listener.accept()
|
|
147
|
+
# Handle each client in a separate thread
|
|
148
|
+
thread = threading.Thread(
|
|
149
|
+
target=self._handle_client,
|
|
150
|
+
args=(conn, addr)
|
|
151
|
+
)
|
|
152
|
+
thread.daemon = True
|
|
153
|
+
thread.start()
|
|
154
|
+
except socket.timeout:
|
|
155
|
+
continue
|
|
156
|
+
except Exception as e:
|
|
157
|
+
if not self.shutdown:
|
|
158
|
+
print(f"Error accepting client: {e}")
|
|
159
|
+
|
|
160
|
+
def run(self, provider: ProviderFunc) -> None:
|
|
161
|
+
"""Run the fuzzing server.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
provider: Function that generates fuzzing inputs
|
|
165
|
+
"""
|
|
166
|
+
# Setup signal handler
|
|
167
|
+
def signal_handler(_signum: int, _frame: object) -> None:
|
|
168
|
+
self.shutdown = True
|
|
169
|
+
print("\nShutting down server...")
|
|
170
|
+
|
|
171
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
172
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
173
|
+
|
|
174
|
+
# Clean up any existing socket
|
|
175
|
+
if os.path.exists(SOCKET_PATH):
|
|
176
|
+
os.unlink(SOCKET_PATH)
|
|
177
|
+
|
|
178
|
+
# Create shared memory
|
|
179
|
+
self._create_shared_memory()
|
|
180
|
+
|
|
181
|
+
# Create and bind socket
|
|
182
|
+
self.listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
183
|
+
self.listener.bind(SOCKET_PATH)
|
|
184
|
+
self.listener.listen(10)
|
|
185
|
+
|
|
186
|
+
# Start accepting clients in a separate thread
|
|
187
|
+
accept_thread = threading.Thread(target=self._accept_clients)
|
|
188
|
+
accept_thread.daemon = True
|
|
189
|
+
accept_thread.start()
|
|
190
|
+
|
|
191
|
+
# Start status updates thread
|
|
192
|
+
status_thread = threading.Thread(target=self._status_updates)
|
|
193
|
+
status_thread.daemon = True
|
|
194
|
+
status_thread.start()
|
|
195
|
+
|
|
196
|
+
# Main fuzzing loop
|
|
197
|
+
try:
|
|
198
|
+
while not self.shutdown:
|
|
199
|
+
# Wait until at least one client is connected
|
|
200
|
+
if len(self.clients) == 0:
|
|
201
|
+
print("Waiting for a client...")
|
|
202
|
+
time.sleep(1)
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
start_time = time.perf_counter()
|
|
206
|
+
|
|
207
|
+
# Generate inputs
|
|
208
|
+
inputs = provider()
|
|
209
|
+
if not inputs:
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
# Prepare input data
|
|
213
|
+
input_sizes = []
|
|
214
|
+
offset = 0
|
|
215
|
+
for input_data in inputs:
|
|
216
|
+
size = len(input_data)
|
|
217
|
+
if offset + size > self.shm_max_size:
|
|
218
|
+
print(f"Warning: Input data exceeds shared memory size")
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
# Write to shared memory using memmove for better performance
|
|
222
|
+
dest_ptr = ctypes.addressof(self.input_shm_buffer) + offset
|
|
223
|
+
if isinstance(input_data, bytes):
|
|
224
|
+
src = ctypes.c_char_p(input_data)
|
|
225
|
+
ctypes.memmove(dest_ptr, src, size)
|
|
226
|
+
else:
|
|
227
|
+
# Fallback for other types
|
|
228
|
+
self.input_shm_buffer[offset:offset + size] = input_data
|
|
229
|
+
input_sizes.append(size)
|
|
230
|
+
offset += size
|
|
231
|
+
|
|
232
|
+
# Prepare message with number of inputs and their sizes
|
|
233
|
+
msg = struct.pack(">I", len(input_sizes))
|
|
234
|
+
for size in input_sizes:
|
|
235
|
+
msg += struct.pack(">I", size)
|
|
236
|
+
|
|
237
|
+
# Send to all clients and collect results
|
|
238
|
+
results = {}
|
|
239
|
+
dead_clients = []
|
|
240
|
+
|
|
241
|
+
with self.clients_lock:
|
|
242
|
+
for name, client in self.clients.items():
|
|
243
|
+
try:
|
|
244
|
+
# Send input sizes
|
|
245
|
+
client.conn.sendall(msg)
|
|
246
|
+
|
|
247
|
+
# Read result size
|
|
248
|
+
result_size_bytes = client.conn.recv(4)
|
|
249
|
+
if len(result_size_bytes) != 4:
|
|
250
|
+
dead_clients.append(name)
|
|
251
|
+
continue
|
|
252
|
+
|
|
253
|
+
result_size = struct.unpack(">I", result_size_bytes)[0]
|
|
254
|
+
|
|
255
|
+
# Read result from client's output shared memory
|
|
256
|
+
output_shm = SharedMemory(client.shm_id)
|
|
257
|
+
output_buffer = output_shm.attach()
|
|
258
|
+
result = bytes(output_buffer[0:result_size])
|
|
259
|
+
output_shm.detach()
|
|
260
|
+
|
|
261
|
+
results[name] = result
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
print(f"Error communicating with client {name}: {e}")
|
|
265
|
+
dead_clients.append(name)
|
|
266
|
+
|
|
267
|
+
# Remove dead clients
|
|
268
|
+
for name in dead_clients:
|
|
269
|
+
print(f"Removing dead client: {name}")
|
|
270
|
+
with self.clients_lock:
|
|
271
|
+
if name in self.clients:
|
|
272
|
+
try:
|
|
273
|
+
self.clients[name].conn.close()
|
|
274
|
+
except:
|
|
275
|
+
pass
|
|
276
|
+
del self.clients[name]
|
|
277
|
+
|
|
278
|
+
# Check for differences
|
|
279
|
+
if len(results) > 1:
|
|
280
|
+
first_result = None
|
|
281
|
+
all_same = True
|
|
282
|
+
for result in results.values():
|
|
283
|
+
if first_result is None:
|
|
284
|
+
first_result = result
|
|
285
|
+
elif result != first_result:
|
|
286
|
+
all_same = False
|
|
287
|
+
break
|
|
288
|
+
|
|
289
|
+
if not all_same:
|
|
290
|
+
print("Values are different:")
|
|
291
|
+
for name, result in results.items():
|
|
292
|
+
result_hash = hashlib.sha256(result).hexdigest()
|
|
293
|
+
print(f"Key: {name}, Value: {result_hash}")
|
|
294
|
+
|
|
295
|
+
# Update statistics
|
|
296
|
+
duration = time.perf_counter() - start_time
|
|
297
|
+
self.iteration_count += 1
|
|
298
|
+
self.total_duration += duration
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
except Exception as e:
|
|
302
|
+
if not self.shutdown:
|
|
303
|
+
print(f"Server error: {e}")
|
|
304
|
+
raise
|
|
305
|
+
finally:
|
|
306
|
+
self._cleanup()
|
|
307
|
+
|
|
308
|
+
def _cleanup(self) -> None:
|
|
309
|
+
"""Clean up resources."""
|
|
310
|
+
print("Cleaning up server resources...")
|
|
311
|
+
|
|
312
|
+
# Close client connections
|
|
313
|
+
with self.clients_lock:
|
|
314
|
+
for client in self.clients.values():
|
|
315
|
+
try:
|
|
316
|
+
client.conn.close()
|
|
317
|
+
except:
|
|
318
|
+
pass
|
|
319
|
+
self.clients.clear()
|
|
320
|
+
|
|
321
|
+
# Close listener
|
|
322
|
+
if self.listener:
|
|
323
|
+
try:
|
|
324
|
+
self.listener.close()
|
|
325
|
+
except:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
# Clean up socket file
|
|
329
|
+
if os.path.exists(SOCKET_PATH):
|
|
330
|
+
try:
|
|
331
|
+
os.unlink(SOCKET_PATH)
|
|
332
|
+
except:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
# Clean up shared memory
|
|
336
|
+
if self.input_shm:
|
|
337
|
+
try:
|
|
338
|
+
self.input_shm.detach()
|
|
339
|
+
self.input_shm.remove()
|
|
340
|
+
except:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
print("Server shutdown complete")
|
dff_py-0.1.0/dff/shm.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""System V shared memory wrapper for Python."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import ctypes.util
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
IPC_CREAT = 0o1000
|
|
9
|
+
IPC_EXCL = 0o2000
|
|
10
|
+
IPC_RMID = 0
|
|
11
|
+
|
|
12
|
+
if sys.platform == "darwin":
|
|
13
|
+
libc_name = ctypes.util.find_library("c")
|
|
14
|
+
elif sys.platform.startswith("linux"):
|
|
15
|
+
libc_name = ctypes.util.find_library("c")
|
|
16
|
+
else:
|
|
17
|
+
raise OSError(f"Unsupported platform: {sys.platform}")
|
|
18
|
+
|
|
19
|
+
if not libc_name:
|
|
20
|
+
raise OSError("Could not find libc")
|
|
21
|
+
|
|
22
|
+
libc = ctypes.CDLL(libc_name)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ShmidDs(ctypes.Structure):
|
|
26
|
+
"""Shared memory segment descriptor structure."""
|
|
27
|
+
if sys.platform == "darwin":
|
|
28
|
+
_fields_ = [
|
|
29
|
+
("shm_perm", ctypes.c_void_p),
|
|
30
|
+
("shm_segsz", ctypes.c_size_t),
|
|
31
|
+
("shm_lpid", ctypes.c_int),
|
|
32
|
+
("shm_cpid", ctypes.c_int),
|
|
33
|
+
("shm_nattch", ctypes.c_short),
|
|
34
|
+
("shm_atime", ctypes.c_long),
|
|
35
|
+
("shm_dtime", ctypes.c_long),
|
|
36
|
+
("shm_ctime", ctypes.c_long),
|
|
37
|
+
("shm_internal", ctypes.c_void_p),
|
|
38
|
+
]
|
|
39
|
+
else:
|
|
40
|
+
_fields_ = [
|
|
41
|
+
("shm_perm", ctypes.c_void_p),
|
|
42
|
+
("shm_segsz", ctypes.c_size_t),
|
|
43
|
+
("shm_atime", ctypes.c_long),
|
|
44
|
+
("shm_dtime", ctypes.c_long),
|
|
45
|
+
("shm_ctime", ctypes.c_long),
|
|
46
|
+
("shm_cpid", ctypes.c_int),
|
|
47
|
+
("shm_lpid", ctypes.c_int),
|
|
48
|
+
("shm_nattch", ctypes.c_short),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
libc.shmget.argtypes = [ctypes.c_int, ctypes.c_size_t, ctypes.c_int]
|
|
53
|
+
libc.shmget.restype = ctypes.c_int
|
|
54
|
+
|
|
55
|
+
libc.shmat.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
|
|
56
|
+
libc.shmat.restype = ctypes.c_void_p
|
|
57
|
+
|
|
58
|
+
libc.shmdt.argtypes = [ctypes.c_void_p]
|
|
59
|
+
libc.shmdt.restype = ctypes.c_int
|
|
60
|
+
|
|
61
|
+
libc.shmctl.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(ShmidDs)]
|
|
62
|
+
libc.shmctl.restype = ctypes.c_int
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class SharedMemory:
|
|
66
|
+
"""Wrapper for System V shared memory operations."""
|
|
67
|
+
|
|
68
|
+
def __init__(self, shmid: int, size: int = 0):
|
|
69
|
+
"""Initialize shared memory wrapper.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
shmid: Shared memory ID
|
|
73
|
+
size: Size of the shared memory segment (for reference)
|
|
74
|
+
"""
|
|
75
|
+
self.shmid = shmid
|
|
76
|
+
self.size = size
|
|
77
|
+
self.addr: Optional[int] = None
|
|
78
|
+
self._attached = False
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def create(cls, key: int, size: int, perm: int = 0o666) -> "SharedMemory":
|
|
82
|
+
"""Create a new shared memory segment.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
key: IPC key for the shared memory segment
|
|
86
|
+
size: Size of the segment in bytes
|
|
87
|
+
perm: Permissions for the segment
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
SharedMemory object
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
OSError: If creation fails
|
|
94
|
+
"""
|
|
95
|
+
flags = perm | IPC_CREAT | IPC_EXCL
|
|
96
|
+
shmid = libc.shmget(key, size, flags)
|
|
97
|
+
if shmid == -1:
|
|
98
|
+
raise OSError(f"Failed to create shared memory with key {key}")
|
|
99
|
+
return cls(shmid, size)
|
|
100
|
+
|
|
101
|
+
@classmethod
|
|
102
|
+
def get(cls, key: int, size: int = 0) -> "SharedMemory":
|
|
103
|
+
"""Get existing shared memory segment.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
key: IPC key for the shared memory segment
|
|
107
|
+
size: Expected size (0 to get existing)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
SharedMemory object
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
OSError: If segment doesn't exist
|
|
114
|
+
"""
|
|
115
|
+
shmid = libc.shmget(key, size, 0)
|
|
116
|
+
if shmid == -1:
|
|
117
|
+
raise OSError(f"Failed to get shared memory with key {key}")
|
|
118
|
+
return cls(shmid, size)
|
|
119
|
+
|
|
120
|
+
def attach(self, addr: int = 0, flags: int = 0) -> ctypes.Array:
|
|
121
|
+
"""Attach to the shared memory segment.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
addr: Preferred attach address (0 for system choice)
|
|
125
|
+
flags: Attach flags
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
ctypes array of the attached segment
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
OSError: If attach fails
|
|
132
|
+
"""
|
|
133
|
+
if self._attached:
|
|
134
|
+
raise RuntimeError("Already attached to shared memory")
|
|
135
|
+
|
|
136
|
+
result = libc.shmat(self.shmid, addr, flags)
|
|
137
|
+
if result == -1:
|
|
138
|
+
raise OSError(f"Failed to attach to shared memory ID {self.shmid}")
|
|
139
|
+
|
|
140
|
+
self.addr = result
|
|
141
|
+
self._attached = True
|
|
142
|
+
|
|
143
|
+
if self.size > 0:
|
|
144
|
+
return (ctypes.c_ubyte * self.size).from_address(self.addr)
|
|
145
|
+
else:
|
|
146
|
+
return (ctypes.c_ubyte * (100 * 1024 * 1024)).from_address(self.addr)
|
|
147
|
+
|
|
148
|
+
def detach(self) -> None:
|
|
149
|
+
"""Detach from the shared memory segment."""
|
|
150
|
+
if not self._attached or self.addr is None:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
result = libc.shmdt(self.addr)
|
|
154
|
+
if result == -1:
|
|
155
|
+
raise OSError("Failed to detach from shared memory")
|
|
156
|
+
|
|
157
|
+
self.addr = None
|
|
158
|
+
self._attached = False
|
|
159
|
+
|
|
160
|
+
def remove(self) -> None:
|
|
161
|
+
"""Remove the shared memory segment."""
|
|
162
|
+
result = libc.shmctl(self.shmid, IPC_RMID, None)
|
|
163
|
+
if result == -1:
|
|
164
|
+
raise OSError(f"Failed to remove shared memory ID {self.shmid}")
|
|
165
|
+
|
|
166
|
+
def __del__(self) -> None:
|
|
167
|
+
"""Cleanup - detach if still attached."""
|
|
168
|
+
if self._attached:
|
|
169
|
+
try:
|
|
170
|
+
self.detach()
|
|
171
|
+
except:
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def attach_by_id(shmid: int, size: int = 0) -> ctypes.Array:
|
|
176
|
+
"""Convenience function to attach to shared memory by ID.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
shmid: Shared memory ID
|
|
180
|
+
size: Expected size (0 for default max)
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
ctypes array of the attached segment
|
|
184
|
+
"""
|
|
185
|
+
shm = SharedMemory(shmid, size)
|
|
186
|
+
return shm.attach()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dff-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A simple differential fuzzing framework
|
|
5
|
+
Author-email: Justin Traglia <jtraglia@pm.me>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jtraglia/dff
|
|
8
|
+
Project-URL: Repository, https://github.com/jtraglia/dff
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# DFF Python Implementation
|
|
13
|
+
|
|
14
|
+
A Python implementation of the DFF (Differential Fuzzing Framework) that uses Unix domain sockets and System V shared memory for high-performance IPC.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
### From PyPI (once published)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install dff-py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### From Source
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd python
|
|
28
|
+
pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Python 3.8 or higher
|
|
34
|
+
- Linux or macOS (Windows is not supported due to Unix domain sockets and System V shared memory)
|
|
35
|
+
- System configured for 100 MiB shared memory segments (see main README)
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Client
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dff import Client
|
|
43
|
+
|
|
44
|
+
def process_func(method: str, inputs: list[bytes]) -> bytes:
|
|
45
|
+
"""Process function that handles fuzzing inputs."""
|
|
46
|
+
if method != "sha":
|
|
47
|
+
raise ValueError(f"Unknown method: {method}")
|
|
48
|
+
|
|
49
|
+
# Process the first input (matching Go/Java behavior)
|
|
50
|
+
import hashlib
|
|
51
|
+
return hashlib.sha256(inputs[0]).digest()
|
|
52
|
+
|
|
53
|
+
# Create and run client
|
|
54
|
+
client = Client("python", process_func)
|
|
55
|
+
client.connect()
|
|
56
|
+
client.run()
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Server
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from dff import Server
|
|
63
|
+
|
|
64
|
+
def provider() -> list[bytes]:
|
|
65
|
+
"""Generate fuzzing inputs."""
|
|
66
|
+
import random
|
|
67
|
+
size = random.randint(1024, 4096)
|
|
68
|
+
data = bytes(random.randint(0, 255) for _ in range(size))
|
|
69
|
+
return [data]
|
|
70
|
+
|
|
71
|
+
# Create and run server
|
|
72
|
+
server = Server("sha")
|
|
73
|
+
server.run(provider)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
See the `examples/python/` directory for complete working examples:
|
|
79
|
+
|
|
80
|
+
- `client.py` - SHA256 hashing client implementation
|
|
81
|
+
- `server.py` - Fuzzing server with random data provider
|
|
82
|
+
|
|
83
|
+
### Running the Examples
|
|
84
|
+
|
|
85
|
+
Start the server:
|
|
86
|
+
```bash
|
|
87
|
+
./examples/python/server.py
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
In another terminal, start one or more clients:
|
|
91
|
+
```bash
|
|
92
|
+
./examples/python/client.py
|
|
93
|
+
./examples/python/client.py python2
|
|
94
|
+
./examples/golang/client/client golang
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The server will detect any differences in the outputs from different clients.
|
|
98
|
+
|
|
99
|
+
## Architecture
|
|
100
|
+
|
|
101
|
+
The framework uses:
|
|
102
|
+
- **Unix domain sockets** for control messages and coordination
|
|
103
|
+
- **System V shared memory** for efficient data transfer
|
|
104
|
+
- **Multiple client support** for differential testing
|
|
105
|
+
|
|
106
|
+
### Protocol
|
|
107
|
+
|
|
108
|
+
1. Client connects to server via Unix socket at `/tmp/dff`
|
|
109
|
+
2. Client sends its name
|
|
110
|
+
3. Server responds with:
|
|
111
|
+
- Input shared memory ID (4 bytes, big-endian)
|
|
112
|
+
- Output shared memory ID (4 bytes, big-endian)
|
|
113
|
+
- Method name (up to 64 bytes)
|
|
114
|
+
4. For each fuzzing iteration:
|
|
115
|
+
- Server writes input data to shared memory
|
|
116
|
+
- Server sends message with input count and sizes
|
|
117
|
+
- Client processes data and writes result to output shared memory
|
|
118
|
+
- Client sends result size back to server
|
|
119
|
+
- Server compares results across clients
|
|
120
|
+
|
|
121
|
+
## Performance
|
|
122
|
+
|
|
123
|
+
The Python implementation is functional but slower than compiled language implementations (Go, Rust) due to:
|
|
124
|
+
- Python's Global Interpreter Lock (GIL)
|
|
125
|
+
- Interpreter overhead
|
|
126
|
+
- Dynamic typing
|
|
127
|
+
|
|
128
|
+
For better performance, consider:
|
|
129
|
+
- Using PyPy instead of CPython
|
|
130
|
+
- Implementing compute-heavy processing in C extensions
|
|
131
|
+
- Running multiple client instances
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
### Running Tests
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
cd python
|
|
139
|
+
pip install -e .[dev]
|
|
140
|
+
pytest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Code Quality
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
# Format code
|
|
147
|
+
black dff/
|
|
148
|
+
|
|
149
|
+
# Lint
|
|
150
|
+
ruff dff/
|
|
151
|
+
|
|
152
|
+
# Type checking
|
|
153
|
+
mypy dff/
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT License - see the LICENSE file in the root directory.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dff
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dff-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A simple differential fuzzing framework"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Justin Traglia", email = "jtraglia@pm.me"}]
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
|
+
dependencies = []
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/jtraglia/dff"
|
|
17
|
+
Repository = "https://github.com/jtraglia/dff"
|
|
18
|
+
|
|
19
|
+
[tool.setuptools.packages.find]
|
|
20
|
+
where = ["."]
|
|
21
|
+
include = ["dff*"]
|
dff_py-0.1.0/setup.cfg
ADDED