Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added IPSocketConnection for L4 protocols #514

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ Features
- Implemented visual request-graph rendering functions for Session
- Added to web UIL: runtime, exec speed, current test case name.
- Added simple custom checksum and example usage.
- Added `IPSocketConnection` for working with protocol on top of IP.

Fixes
^^^^^
- Clarified Checksum() data for custom checksum function.
- Socket Connections no longer return b"" on timeout but raise `BoofuzzTargetConnectionTimeout`.
- When the target closes the connection, b"" is returned.
- With session option check_data_received_each_request enabled, any socket error or close is handles as a test case failure.
Before this was only checked on non-fuzzed requests and is now checked on fuzzed requests too.

v0.3.0
------
Expand Down
2 changes: 2 additions & 0 deletions boofuzz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
ITargetConnection,
RawL2SocketConnection,
RawL3SocketConnection,
IPSocketConnection,
SerialConnection,
SerialConnectionLowLevel,
SocketConnection,
Expand Down Expand Up @@ -117,6 +118,7 @@
"RandomData",
"RawL2SocketConnection",
"RawL3SocketConnection",
"IPSocketConnection",
"Repeat",
"Repeater",
"Request",
Expand Down
2 changes: 2 additions & 0 deletions boofuzz/connections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .itarget_connection import ITargetConnection
from .raw_l2_socket_connection import RawL2SocketConnection
from .raw_l3_socket_connection import RawL3SocketConnection
from .ip_socket_connection import IPSocketConnection
from .serial_connection import SerialConnection
from .serial_connection_low_level import SerialConnectionLowLevel
from .socket_connection import SocketConnection
Expand All @@ -20,6 +21,7 @@
"ITargetConnection",
"RawL2SocketConnection",
"RawL3SocketConnection",
"IPSocketConnection",
"SerialConnection",
"SerialConnectionLowLevel",
"SocketConnection",
Expand Down
140 changes: 140 additions & 0 deletions boofuzz/connections/ip_socket_connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from __future__ import absolute_import

import errno
import socket
import sys

from future.utils import raise_

from boofuzz import exception
from boofuzz.connections import base_socket_connection


class IPSocketConnection(base_socket_connection.BaseSocketConnection):
"""BaseSocketConnection implementation for use with IP based protocols.

.. versionadded:: 0.3.1

Args:
target_ip (str): Destination IP address to send packets to.
local_ip (str, optional): eIP address of the local interface. Defaults to auto-detect with fallback to 0.0.0.0.
ip_proto (int, optional): IP protocol to set in the header. Defaults to IPPROTO_RAW. If using IPPROTO_RAW, you
have to provide a valid IP header, else the OS will reject the packet.
send_timeout (float, optional): Seconds to wait for recv before timing out. Default 5.0.
recv_timeout (float, optional): Seconds to wait for recv before timing out. Default 5.0.
validate_sender_address (bool, optional): When receiving, only return packages where the sender IP address
matches target_ip. Retry on mismatch. Set to False to receive from any source. Default True.
"""

def __init__(
self,
target_ip,
local_ip=None,
ip_proto=socket.IPPROTO_RAW,
send_timeout=5.0,
recv_timeout=5.0,
validate_sender_address=True,
):
super(IPSocketConnection, self).__init__(send_timeout, recv_timeout)

self._src_ip = local_ip
self._dst_ip = target_ip
self._ip_proto = ip_proto

def open(self):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, self._ip_proto)
if self._src_ip is not None:
self._sock.bind((self._src_ip, 0))
else:
local_ip = socket.gethostbyname(socket.gethostname())
if local_ip.startswith("127.0."):
local_ip = "0.0.0.0"
self._sock.bind((local_ip, 0))

super(IPSocketConnection, self).open()

def recv(self, max_bytes):
"""
Receives a packet from the socket. Only packets whose destination IP address equals `local_ip` and whose IP
protocol equals `ip_proto` are forwarded.
If max_bytes < packet_size, only the first max_bytes are returned and the rest of the packet is discarded.
Otherwise, return the whole packet.

.. note::
The full IP header is returned in addition to the payload. Keep this in mind when parsing the response.

Args:
max_bytes (int): Maximum number of bytes to return.

Returns:
bytes: Received IP header + payload
"""
data = b""
address = ""

while address != self._dst_ip:
try:
data, address = self._sock.recvfrom(max_bytes)
except socket.timeout:
raise_(exception.BoofuzzTargetConnectionTimeout(), None, sys.exc_info()[2])
except socket.error as e:
if e.errno == errno.ECONNABORTED:
raise_(
exception.BoofuzzTargetConnectionAborted(socket_errno=e.errno, socket_errmsg=e.strerror),
None,
sys.exc_info()[2],
)
elif e.errno in [errno.ECONNRESET, errno.ENETRESET, errno.ETIMEDOUT]:
raise_(exception.BoofuzzTargetConnectionReset(), None, sys.exc_info()[2])
elif e.errno == errno.EWOULDBLOCK:
raise_(exception.BoofuzzTargetConnectionTimeout(), None, sys.exc_info()[2])
else:
raise

return data

def send(self, data):
"""
Send data to the target. Only valid after calling open!

.. note::
If using IPPROTO_RAW you have to include a valid IP header, else the OS will reject the operation.
For any other protocol, the OS will generate the IP header for you.

Args:
data (bytes): Data to send.

Returns:
int: Number of bytes actually sent.
"""
num_sent = 0

try:
num_sent = self._sock.sendto(data, (self._dst_ip, 0))
except socket.timeout:
raise_(exception.BoofuzzTargetConnectionTimeout(), None, sys.exc_info()[2])
except socket.error as e:
if e.errno == errno.ECONNABORTED:
raise_(
exception.BoofuzzTargetConnectionAborted(socket_errno=e.errno, socket_errmsg=e.strerror),
None,
sys.exc_info()[2],
)
elif e.errno in [errno.ECONNRESET, errno.ENETRESET, errno.ETIMEDOUT, errno.EPIPE]:
raise_(exception.BoofuzzTargetConnectionReset(), None, sys.exc_info()[2])
elif e.errno in [errno.EINVAL]:
raise_(
exception.BoofuzzError(
"Invalid argument in sendto(). Make sure to provide a valid IP header in " "case of IPPROTO_RAW"
),
None,
sys.exc_info()[2],
)
else:
raise

return num_sent

@property
def info(self):
return "{0}, IP protocol 0x{1:02x}".format(self._dst_ip, self._ip_proto)
2 changes: 2 additions & 0 deletions boofuzz/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

ERR_CONN_RESET = "Target connection reset."

ERR_CONN_TIMEOUT = "Target connection timed out."

ERR_CONN_RESET_FAIL = "Target connection reset -- considered a failure case when triggered from post_send"

ERR_CALLBACK_FUNC = "A custom {func_name} callback function raised an uncought error.\n"
Expand Down
4 changes: 4 additions & 0 deletions boofuzz/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class BoofuzzTargetConnectionAborted(BoofuzzError):
socket_errmsg = attr.ib()


class BoofuzzTargetConnectionTimeout(BoofuzzError):
pass


class BoofuzzNoSuchTestCase(BoofuzzError):
pass

Expand Down
33 changes: 24 additions & 9 deletions boofuzz/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,11 +406,10 @@ class Session(pgraph.Graph):
Set to 0 to save after every test case (high disk I/O!). Default 0.
receive_data_after_each_request (bool): If True, Session will attempt to receive a reply after transmitting
each non-fuzzed node. Default True.
check_data_received_each_request (bool): If True, Session will verify that some data has
been received after transmitting each non-fuzzed node, and if not,
register a failure. If False, this check will not be performed. Default
False. A receive attempt is still made unless
receive_data_after_each_request is False.
check_data_received_each_request (bool): If True, Session will verify that some data has been received after
transmitting each node, and if not, register a failure. If False, this
check will not be performed. Default False. A receive attempt is still
made unless receive_data_after_each_request is False.
receive_data_after_fuzz (bool): If True, Session will attempt to receive a reply after transmitting
a fuzzed message. Default False.
ignore_connection_reset (bool): Log ECONNRESET errors ("Target connection reset") as "info" instead of
Expand Down Expand Up @@ -1130,9 +1129,14 @@ def transmit_normal(self, sock, node, edge, callback_data, mutation_context):
self._fuzz_data_logger.log_check("Verify some data was received from the target.")
if not self.last_recv:
# Assume a crash?
raise BoofuzzFailure(message="Nothing received from target.")
raise BoofuzzFailure(message="The target closed the connection.")
else:
self._fuzz_data_logger.log_pass("Some data received from target.")
except exception.BoofuzzTargetConnectionTimeout:
if self._check_data_received_each_request:
raise BoofuzzFailure(message=constants.ERR_CONN_TIMEOUT)
else:
self._fuzz_data_logger.log_info(constants.ERR_CONN_TIMEOUT)
except exception.BoofuzzTargetConnectionReset:
if self._check_data_received_each_request:
raise BoofuzzFailure(message=constants.ERR_CONN_RESET)
Expand Down Expand Up @@ -1185,10 +1189,22 @@ def transmit_fuzz(self, sock, node, edge, callback_data, mutation_context):
else:
raise BoofuzzFailure(str(e))

received = b""
try: # recv
if self._receive_data_after_fuzz:
received = self.targets[0].recv()
self.last_recv = self.targets[0].recv()

if self._check_data_received_each_request:
self._fuzz_data_logger.log_check("Verify some data was received from the target.")
if not self.last_recv:
# Assume a crash?
raise BoofuzzFailure(message="The target closed the connection.")
else:
self._fuzz_data_logger.log_pass("Some data received from target.")
except exception.BoofuzzTargetConnectionTimeout:
if self._check_data_received_each_request:
raise BoofuzzFailure(message=constants.ERR_CONN_TIMEOUT)
else:
self._fuzz_data_logger.log_info(constants.ERR_CONN_TIMEOUT)
except exception.BoofuzzTargetConnectionReset:
if self._check_data_received_each_request:
raise BoofuzzFailure(message=constants.ERR_CONN_RESET)
Expand All @@ -1207,7 +1223,6 @@ def transmit_fuzz(self, sock, node, edge, callback_data, mutation_context):
else:
self._fuzz_data_logger.log_fail(str(e))
raise BoofuzzFailure(str(e))
self.last_recv = received

def build_webapp_thread(self, port=constants.DEFAULT_WEB_UI_PORT):
app.session = self
Expand Down