import errno
import pickle
import select
import socket
import struct
import sys
import time
import uuid
from boofuzz import exception
[docs]
class Client:
def __init__(self, host, port):
self.__host = host
self.__port = port
self.__dbg_flag = False
self.__server_sock = None
self.__retry = 0
self.NOLINGER = struct.pack("ii", 1, 0)
self.known_server = None
def __getattr__(self, method_name):
"""
This routine is called by default when a requested attribute (or method) is accessed that has no definition.
Unfortunately __getattr__ only passes the requested method name and not the arguments. So we extend the
functionality with a little lambda magic to the routine method_missing(). Which is actually how Ruby handles
missing methods by default ... with arguments. Now we are just as cool as Ruby.
@type method_name: str
@param method_name: The name of the requested and undefined attribute (or method in our case).
@rtype: lambda
@return: Lambda magic passing control (and in turn the arguments we want) to self.method_missing().
"""
return lambda *args, **kwargs: self.__method_missing(method_name, *args, **kwargs)
def __connect(self):
"""
Connect to the PED-RPC server.
"""
# if we have a pre-existing server socket, ensure it's closed.
self.__disconnect()
# connect to the server, timeout on failure.
self.__server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.__server_sock.settimeout(3.0)
try:
self.__server_sock.connect((self.__host, self.__port))
except socket.error as e:
if self.__retry != 5:
self.__retry += 1
time.sleep(5)
self.__connect()
else:
raise exception.BoofuzzRpcError(
'PED-RPC> unable to connect to server {0}:{1}. Error message: "{2}"\n'.format(
self.__host, self.__port, e
)
)
# disable timeouts and lingering.
self.__server_sock.settimeout(None)
self.__server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, self.NOLINGER)
def __disconnect(self):
"""
Ensure the socket is torn down.
"""
if self.__server_sock is not None:
self.__debug("closing server socket")
self.__server_sock.close()
self.__server_sock = None
def __debug(self, msg):
if self.__dbg_flag:
print("PED-RPC> %s" % msg)
def __method_missing(self, method_name, *args, **kwargs):
"""
See the notes for __getattr__ for related notes. This method is called, in the Ruby fashion, with the method
name and arguments for any requested but undefined class method.
@type method_name: str
@param method_name: The name of the requested and undefined attribute (or method in our case).
@type *args: tuple
@param *args: Tuple of arguments.
@type **kwargs dict
@param **kwargs: Dictioanry of arguments.
@rtype: Mixed
@return: Return value of the mirrored method.
"""
# return a value so lines of code like the following work:
# x = pedrpc.client(host, port)
# if x:
# x.do_something()
if method_name == "__bool__":
return 1
# subclasses run into this as they call a trampoline method...
# not sure if this is the right way to handle it, but it seems to work
if method_name.endswith("__method_missing"):
return self.__method_missing(*args, **kwargs)
elif method_name.endswith("__hot_transmit"):
return self.__hot_transmit(*args, **kwargs)
# ignore all other attempts to access a private member.
if method_name.startswith("__"):
return
# connect to the PED-RPC server.
self.__connect()
server_uuid = self.__pickle_recv()
if server_uuid != self.known_server:
self.on_new_server(server_uuid)
self.known_server = server_uuid
# transmit the method name and arguments.
self.__pickle_send((method_name, (args, kwargs)))
# snag the return value.
ret = self.__pickle_recv()
# close the sock and return.
self.__disconnect()
return ret
def __hot_transmit(self, data):
self.__pickle_send(data)
self.__pickle_recv()
self.__disconnect()
self.__connect()
# Grab the instance id. assume it hasn't changed, otherwise we're doomed.
self.__pickle_recv()
def __pickle_recv(self):
"""
This routine is used for marshaling arbitrary data from the PyDbg server. We can send pretty much anything here.
For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple
length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received.
@raise pdx: An exception is raised if the connection was severed.
@rtype: Mixed
@return: Whatever is received over the socket.
"""
try:
# TODO: this should NEVER fail, but alas, it does and for the time being i can't figure out why.
# it gets worse. you would think that simply returning here would break things, but it doesn't.
# gotta track this down at some point.
recvd = self.__server_sock.recv(4)
length = struct.unpack("<L", recvd)[0]
except Exception:
return
try:
received = b""
while length:
chunk = self.__server_sock.recv(length)
received += chunk
length -= len(chunk)
except socket.error as e:
raise exception.BoofuzzRpcError(
"PED-RPC> unable to connect to server "
'{0}:{1}. Error message: "{2}"\n'.format(self.__host, self.__port, e)
)
return pickle.loads(received)
def __pickle_send(self, data):
"""
This routine is used for marshaling arbitrary data to the PyDbg server. We can send pretty much anything here.
For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple
length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received.
@type data: Mixed
@param data: Data to marshal and transmit. Data can *pretty much* contain anything you throw at it.
@raise pdx: An exception is raised if the connection was severed.
"""
data = pickle.dumps(data, protocol=2)
self.__debug("sending %d bytes" % len(data))
try:
self.__server_sock.send(struct.pack("<L", len(data)))
self.__server_sock.send(data)
except socket.error as e:
raise exception.BoofuzzRpcError(
"PED-RPC> unable to connect to server "
'{0}:{1}. Error message: "{2}"\n'.format(self.__host, self.__port, e)
)
[docs]
def on_new_server(self, new_server):
"""Override this Method in a child class to be notified when the RPC server was restarted."""
return
[docs]
class Server:
"""
The main PED-RPC Server class. To implement an RPC server, inherit from this class. Call ``serve_forever`` to start
listening for RPC commands.
"""
def __init__(self, host, port):
self.__host = host
self.__port = port
self.__dbg_flag = False
self.__client_sock = None
self.__client_address = None
self.__running = True
# This is a bad solution for a problem that should not even exist in the first place.
# The Problem is that the client disconnects after each RPC call,
# and reconnects on the next without any way to know if the state
# of the RPC server has changed. This becomes a problem if e.g.
# a Virtual Machine with automatic restarting is used in conjunction
# with a Monitor that runs a RPC daemon on the target. In this case,
# a monitor may want to ensure that a set of options are in a known
# state on the target.
# For this to work, the client needs to know if the server has changed
# since the last time it connected to it so it can notify the implementation
# to re-send any initialisation code. This is implemented by the server
# generating a random uuid on startup and sending it to each new connection.
#
# In a perfect world, this protocol wouldn't reconnect for every command
# and options were associated with a connection, but at the moment I don't
# feel like cleaning up this mess.
self.__instance = uuid.uuid4()
try:
# create a socket and bind to the specified port.
self.__server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.__server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.__server.settimeout(None)
self.__server.bind((host, port))
self.__server.listen(1)
except socket.error:
sys.stderr.write("unable to bind to %s:%d\n" % (host, port))
sys.exit(1)
def __disconnect(self):
"""
Ensure the socket is torn down.
"""
if self.__client_sock is not None:
self.__debug("closing client socket")
try:
self.__client_sock.shutdown(socket.SHUT_RDWR)
except socket.error as e:
if e.errno in [errno.ENOTCONN, errno.EBADF]:
pass
else:
raise
self.__client_sock.close()
def __debug(self, msg):
if self.__dbg_flag:
print("PED-RPC> %s" % msg)
def __pickle_recv(self):
"""
This routine is used for marshaling arbitrary data from the PyDbg server. We can send pretty much anything here.
For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple
length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received.
@raise pdx: An exception is raised if the connection was severed.
@rtype: Mixed
@return: Whatever is received over the socket.
"""
try:
length = struct.unpack("<L", self.__client_sock.recv(4))[0]
received = b""
while length:
chunk = self.__client_sock.recv(length)
received += chunk
length -= len(chunk)
except Exception:
sys.stderr.write("PED-RPC> connection client severed during recv()\n")
raise Exception
return pickle.loads(received)
def __pickle_send(self, data):
"""
This routine is used for marshaling arbitrary data to the PyDbg server. We can send pretty much anything here.
For example a tuple containing integers, strings, arbitrary objects and structures. Our "protocol" is a simple
length-value protocol where each datagram is prefixed by a 4-byte length of the data to be received.
@type data: Mixed
@param data: Data to marshal and transmit. Data can *pretty much* contain anything you throw at it.
@raise pdx: An exception is raised if the connection was severed.
"""
data = pickle.dumps(data, protocol=2)
self.__debug("sending %d bytes" % len(data))
try:
self.__client_sock.send(struct.pack("<L", len(data)))
self.__client_sock.send(data)
except Exception:
sys.stderr.write("PED-RPC> connection to client severed during send()\n")
raise Exception
[docs]
def serve_forever(self):
self.__debug("serving up a storm")
while self.__running:
# close any pre-existing socket.
self.__disconnect()
# accept a client connection.
while self.__running:
readable, writeable, errored = select.select([self.__server], [], [], 0.1)
if len(readable) > 0:
assert readable[0] == self.__server
(self.__client_sock, self.__client_address) = self.__server.accept()
break
self.__debug("accepted connection from %s:%d" % (self.__client_address[0], self.__client_address[1]))
self.__pickle_send(self.__instance)
# receive the method name and arguments, continue on socket disconnect.
try:
(method_name, (args, kwargs)) = self.__pickle_recv()
self.__debug("%s(args=%s, kwargs=%s)" % (method_name, args, kwargs))
except Exception:
continue
try:
method = getattr(self, method_name)
except AttributeError:
# if the method can't be found notify the user and raise an error
sys.stderr.write('PED-RPC> remote method "{0}" of {1} cannot be found\n'.format(method_name, self))
raise
ret = method(*args, **kwargs)
# transmit the return value to the client, continue on socket disconnect.
try:
self.__pickle_send(ret)
except Exception:
continue
[docs]
def stop(self):
self.__running = False
self.__disconnect()
try:
self.__server.shutdown(socket.SHUT_RDWR)
except socket.error as e:
if e.errno == errno.ENOTCONN:
pass
else:
raise
self.__server.close()