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

Add a statcounter that prints statistics about the connection #117

Merged
merged 6 commits into from
Aug 1, 2019
Merged
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
163 changes: 163 additions & 0 deletions pyrdp/logging/StatCounter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2019 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

import time
from logging import LoggerAdapter


class STAT:
"""
Type of statistics that a StatCounter object can hold.
"""

CONNECTION_TIME = "connectionTime"
# Duration (in secs) for the TCP connection

CLIENT_SERVER_RATIO = "clientServerRatio"
# Ratio of the # of messages coming from the client vs from the server. High value (>1) means high client interaction.

TOTAL_INPUT = "totalInput"
# # of messages coming from the client to the server.

TOTAL_OUTPUT = "totalOutput"
# # of messages coming from the server to the client.

IO_INPUT = "input"
# Packet coming from the client to the server for the io channel

IO_INPUT_FASTPATH = "fastPathInput"
# Packet coming from the client to the server for the io channel as a fastpath packet

IO_INPUT_SLOWPATH = "slowPathInput"
# Packet coming from the client to the server for the io channel as a slowpath packet

IO_OUTPUT = "output"
# Packet coming from the server to the client for the io channel

IO_OUTPUT_FASTPATH = "fastPathOutput"
# Packet coming from the server to the client for the io channel as a fastpath packet

IO_OUTPUT_SLOWPATH = "slowPathOutput"
# Packet coming from the server to the client for the io channel as a slowpath packet

MCS = "mcs"
# Packet Coming from either end for any channel

MCS_OUTPUT = "mcsOutput"
# Packet Coming from the server to the client for any channel

MCS_OUTPUT_ = "mcsOutput_"
# Packet Coming from the server to the client for a given channel (must append channel # after it)

MCS_INPUT = "mcsInput"
# Packet Coming from the client to the server for any channel

MCS_INPUT_ = "mcsInput_"
# Packet Coming from the client to the server for a given channel (must append channel # after it)

VIRTUAL_CHANNEL = "virtualChannel"
# Packet Coming from either end for any virtual channel that doesnt have a specific implementation (ex clipboard)

VIRTUAL_CHANNEL_INPUT = "virtualChannelInput"
# Packet Coming from the client to the server for any virtual channel that doesnt have a specific implementation (ex clipboard)

VIRTUAL_CHANNEL_OUTPUT = "virtualChannelOutput"
# Packet Coming from the server to the client for any virtual channel that doesnt have a specific implementation (ex clipboard)

DEVICE_REDIRECTION = "deviceRedirection"
# Packet coming from either end for the rdpdr channel

DEVICE_REDIRECTION_CLIENT = "deviceRedirectionClient"
# Packet coming from the client to the server for the rdpdr channel

DEVICE_REDIRECTION_SERVER = "deviceRedirectionServer"
# Packet coming from the server to the client for the rdpdr channel

DEVICE_REDIRECTION_IOREQUEST = "deviceRedirectionIORequest"
# IORequest packets for the rdpdr channel

DEVICE_REDIRECTION_IORESPONSE = "deviceRedirectionIOResponse"
# IOResponse packets for the rdpdr channel

DEVICE_REDIRECTION_IOERROR = "deviceRedirectionIOError"
# IO error packets for the rdpdr channel

DEVICE_REDIRECTION_FILE_CLOSE = "deviceRedirectionFileClose"
# File Close packets for the rdpdr channel

DEVICE_REDIRECTION_FORGED_FILE_READ = "deviceRedirectionForgedFileRead"
# File read packets forged by pyrdp for the rdpdr channel

DEVICE_REDIRECTION_FORGED_DIRECTORY_LISTING = "deviceRedirectionForgedDirectoryListing"
# Directory listing packets forged by pyrdp for the rdpdr channel

CLIPBOARD = "clipboard"
# Number of clipboard PDUs coming from either end

CLIPBOARD_CLIENT = "clipboardClient"
# Number of clipboard PDUs coming from the client

CLIPBOARD_SERVER = "clipboardServer"
# Number of clipboard PDUs coming from the server

CLIPBOARD_COPY = "clipboardCopies"
# Number of times data has been copied by either end

CLIPBOARD_PASTE = "clipboardPastes"
# Number of times data has been pasted by either end


class StatCounter:
"""
Class that keeps track of various statistics during an RDP connection (See STAT)
"""

def __init__(self):
self.stats = {"report": 1.0} # 1.0 = True

def increment(self, *args: str):
"""
Increments all statistics passed in arguments
:param args: list of statistics to increment by one. See STAT for list of allowed values.
"""
for stat in args:
if stat not in self.stats:
self.stats[stat] = 0
self.stats[stat] += 1

def incrementWith(self, statDestination: str, *statsSource: str):
"""
Increments statDestination by all provided statSources
"""
if statDestination not in self.stats:
self.stats[statDestination] = 0
for statSource in statsSource:
if statSource in self.stats:
self.stats[statDestination] += self.stats[statSource]

def start(self):
"""
Initialize some statistics such as connectionTime
"""
self.stats[STAT.CONNECTION_TIME] = time.time()

def stop(self):
"""
Calculates the last statistics such as interaction ratio and connectionTime
"""
self.stats[STAT.CONNECTION_TIME] = time.time() - self.stats[STAT.CONNECTION_TIME]
self.incrementWith(STAT.TOTAL_INPUT, STAT.MCS_INPUT, STAT.IO_INPUT_FASTPATH, STAT.VIRTUAL_CHANNEL_INPUT, STAT.CLIPBOARD_CLIENT, STAT.DEVICE_REDIRECTION_CLIENT)
self.incrementWith(STAT.TOTAL_OUTPUT, STAT.MCS_OUTPUT, STAT.IO_OUTPUT_FASTPATH, STAT.VIRTUAL_CHANNEL_OUTPUT, STAT.CLIPBOARD_SERVER, STAT.DEVICE_REDIRECTION_SERVER)
if self.stats[STAT.TOTAL_OUTPUT] > 0:
self.stats[STAT.CLIENT_SERVER_RATIO] = self.stats[STAT.TOTAL_INPUT] / self.stats[STAT.TOTAL_OUTPUT]

def logReport(self, log: LoggerAdapter):
"""
Create an INFO log message to log the Connection report using the keys in self.stats.
:param log: Logger to use to log the report
"""
keys = ", ".join([f"{key}: %({key})s" for key in self.stats.keys()])
log.info(f"Connection report: {keys}", self.stats)
5 changes: 3 additions & 2 deletions pyrdp/mitm/BasePathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
from pyrdp.enum import ScanCode
from pyrdp.pdu.pdu import PDU
from pyrdp.layer.layer import Layer

from pyrdp.logging.StatCounter import StatCounter, STAT

class BasePathMITM:
"""
Base MITM component for the fast-path and slow-path layers.
"""

def __init__(self, state: RDPMITMState, client: Layer, server: Layer):
def __init__(self, state: RDPMITMState, client: Layer, server: Layer, statCounter: StatCounter):
self.state = state
self.client = client
self.server = server
self.statCounter = statCounter

def onClientPDUReceived(self, pdu: PDU):
raise NotImplementedError("onClientPDUReceived must be overridden")
Expand Down
20 changes: 16 additions & 4 deletions pyrdp/mitm/ClipboardMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from pyrdp.core import decodeUTF16LE
from pyrdp.enum import ClipboardFormatNumber, ClipboardMessageFlags, ClipboardMessageType, PlayerPDUType
from pyrdp.layer import ClipboardLayer
from pyrdp.logging.StatCounter import StatCounter, STAT
from pyrdp.pdu import ClipboardPDU, FormatDataRequestPDU, FormatDataResponsePDU
from pyrdp.recording import Recorder

Expand All @@ -18,13 +19,15 @@ class PassiveClipboardStealer:
MITM component for the clipboard layer. Logs clipboard data when it is pasted.
"""

def __init__(self, client: ClipboardLayer, server: ClipboardLayer, log: LoggerAdapter, recorder: Recorder):
def __init__(self, client: ClipboardLayer, server: ClipboardLayer, log: LoggerAdapter, recorder: Recorder,
statCounter: StatCounter):
"""
:param client: clipboard layer for the client side
:param server: clipboard layer for the server side
:param log: logger for this component
:param recorder: recorder for clipboard data
"""
self.statCounter = statCounter
self.client = client
self.server = server
self.log = log
Expand All @@ -40,9 +43,11 @@ def __init__(self, client: ClipboardLayer, server: ClipboardLayer, log: LoggerAd
)

def onClientPDUReceived(self, pdu: ClipboardPDU):
self.statCounter.increment(STAT.CLIPBOARD, STAT.CLIPBOARD_CLIENT)
self.handlePDU(pdu, self.server)

def onServerPDUReceived(self, pdu: ClipboardPDU):
self.statCounter.increment(STAT.CLIPBOARD, STAT.CLIPBOARD_SERVER)
self.handlePDU(pdu, self.client)

def handlePDU(self, pdu: ClipboardPDU, destination: ClipboardLayer):
Expand All @@ -63,6 +68,10 @@ def handlePDU(self, pdu: ClipboardPDU, destination: ClipboardLayer):
self.log.info("Clipboard data: %(clipboardData)r", {"clipboardData": clipboardData})
self.recorder.record(pdu, PlayerPDUType.CLIPBOARD_DATA)

if self.forwardNextDataResponse:
# Means it's NOT a crafted response
self.statCounter.increment(STAT.CLIPBOARD_PASTE)

self.forwardNextDataResponse = True

def decodeClipboardData(self, data: bytes) -> str:
Expand All @@ -79,8 +88,9 @@ class ActiveClipboardStealer(PassiveClipboardStealer):
clipboard is updated.
"""

def __init__(self, client: ClipboardLayer, server: ClipboardLayer, log: LoggerAdapter, recorder: Recorder):
super().__init__(client, server, log, recorder)
def __init__(self, client: ClipboardLayer, server: ClipboardLayer, log: LoggerAdapter, recorder: Recorder,
statCounter: StatCounter):
super().__init__(client, server, log, recorder, statCounter)

def handlePDU(self, pdu: ClipboardPDU, destination: ClipboardLayer):
"""
Expand All @@ -99,6 +109,8 @@ def sendPasteRequest(self, destination: ClipboardLayer):
Sets forwardNextDataResponse to False to make sure that this request is not actually transferred to the other end.
"""

self.statCounter.increment(STAT.CLIPBOARD_COPY)

formatDataRequestPDU = FormatDataRequestPDU(ClipboardFormatNumber.GENERIC)
destination.sendPDU(formatDataRequestPDU)
self.forwardNextDataResponse = False
self.forwardNextDataResponse = False
17 changes: 16 additions & 1 deletion pyrdp/mitm/DeviceRedirectionMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, IOOperationSeverity, \
MajorFunction, MinorFunction
from pyrdp.layer import DeviceRedirectionLayer
from pyrdp.logging.StatCounter import StatCounter, STAT
from pyrdp.mitm.config import MITMConfig
from pyrdp.mitm.FileMapping import FileMapping, FileMappingDecoder, FileMappingEncoder
from pyrdp.mitm.state import RDPMITMState
Expand Down Expand Up @@ -54,7 +55,8 @@ class DeviceRedirectionMITM(Subject):
FORGED_COMPLETION_ID = 1000000


def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter, config: MITMConfig, state: RDPMITMState):
def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter,
config: MITMConfig, statCounter: StatCounter, state: RDPMITMState):
"""
:param client: device redirection layer for the client side
:param server: device redirection layer for the server side
Expand All @@ -67,6 +69,7 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye
self.server = server
self.state = state
self.log = log
self.statCounter = statCounter
self.config = config
self.currentIORequests: Dict[int, DeviceIORequestPDU] = {}
self.openedFiles: Dict[int, FileProxy] = {}
Expand Down Expand Up @@ -108,9 +111,11 @@ def saveMapping(self):
f.write(json.dumps(self.fileMap, cls=FileMappingEncoder, indent=4, sort_keys=True))

def onClientPDUReceived(self, pdu: DeviceRedirectionPDU):
self.statCounter.increment(STAT.DEVICE_REDIRECTION, STAT.DEVICE_REDIRECTION_CLIENT)
self.handlePDU(pdu, self.server)

def onServerPDUReceived(self, pdu: DeviceRedirectionPDU):
self.statCounter.increment(STAT.DEVICE_REDIRECTION, STAT.DEVICE_REDIRECTION_SERVER)
self.handlePDU(pdu, self.client)

def handlePDU(self, pdu: DeviceRedirectionPDU, destination: DeviceRedirectionLayer):
Expand Down Expand Up @@ -143,6 +148,7 @@ def handleIORequest(self, pdu: DeviceIORequestPDU):
:param pdu: the device IO request
"""

self.statCounter.increment(STAT.DEVICE_REDIRECTION_IOREQUEST)
self.currentIORequests[pdu.completionID] = pdu

def handleIOResponse(self, pdu: DeviceIOResponsePDU):
Expand All @@ -151,6 +157,8 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU):
:param pdu: the device IO response.
"""

self.statCounter.increment(STAT.DEVICE_REDIRECTION_IORESPONSE)

if pdu.completionID in self.forgedRequests:
request = self.forgedRequests[pdu.completionID]
request.handleResponse(pdu)
Expand All @@ -162,6 +170,7 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU):
requestPDU = self.currentIORequests.pop(pdu.completionID)

if pdu.ioStatus >> 30 == IOOperationSeverity.STATUS_SEVERITY_ERROR:
self.statCounter.increment(STAT.DEVICE_REDIRECTION_IOERROR)
self.log.warning("Received an IO Response with an error IO status: %(responsePDU)s for request %(requestPDU)s", {"responsePDU": repr(pdu), "requestPDU": repr(requestPDU)})

if pdu.majorFunction in self.responseHandlers:
Expand Down Expand Up @@ -240,6 +249,8 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResp
:param _: the device IO response to the request
"""

self.statCounter.increment(STAT.DEVICE_REDIRECTION_FILE_CLOSE)

if request.fileID in self.openedFiles:
file = self.openedFiles.pop(request.fileID)
file.close()
Expand Down Expand Up @@ -314,6 +325,8 @@ def sendForgedFileRead(self, deviceID: int, path: str) -> int:
:param path: path of the file to download. The path should use '\' instead of '/' to separate directories.
"""

self.statCounter.increment(STAT.DEVICE_REDIRECTION_FORGED_FILE_READ)

completionID = self.findNextRequestID()
request = DeviceRedirectionMITM.ForgedFileReadRequest(deviceID, completionID, self, path)
self.forgedRequests[completionID] = request
Expand All @@ -332,6 +345,8 @@ def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int:
\Documents\*
"""

self.statCounter.increment(STAT.DEVICE_REDIRECTION_FORGED_DIRECTORY_LISTING)

completionID = self.findNextRequestID()
request = DeviceRedirectionMITM.ForgedDirectoryListingRequest(deviceID, completionID, self, path)
self.forgedRequests[completionID] = request
Expand Down
14 changes: 9 additions & 5 deletions pyrdp/mitm/FastPathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#

from pyrdp.layer import FastPathLayer
from pyrdp.logging.StatCounter import StatCounter, STAT
from pyrdp.mitm.state import RDPMITMState
from pyrdp.pdu import FastPathPDU, FastPathScanCodeEvent
from pyrdp.player import keyboard
Expand All @@ -16,23 +17,25 @@ class FastPathMITM(BasePathMITM):
MITM component for the fast-path layer.
"""

def __init__(self, client: FastPathLayer, server: FastPathLayer, state: RDPMITMState):
def __init__(self, client: FastPathLayer, server: FastPathLayer, state: RDPMITMState, statCounter: StatCounter):
"""
:param client: fast-path layer for the client side
:param server: fast-path layer for the server side
:param state: the MITM state.
"""
super().__init__(state, client, server)

super().__init__(state, client, server, statCounter)

self.client.createObserver(
onPDUReceived = self.onClientPDUReceived,
onPDUReceived=self.onClientPDUReceived,
)

self.server.createObserver(
onPDUReceived = self.onServerPDUReceived,
onPDUReceived=self.onServerPDUReceived,
)

def onClientPDUReceived(self, pdu: FastPathPDU):
self.statCounter.increment(STAT.IO_INPUT_FASTPATH)
if self.state.forwardInput:
self.server.sendPDU(pdu)

Expand All @@ -42,5 +45,6 @@ def onClientPDUReceived(self, pdu: FastPathPDU):
self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & keyboard.KBDFLAGS_EXTENDED != 0)

def onServerPDUReceived(self, pdu: FastPathPDU):
self.statCounter.increment(STAT.IO_OUTPUT_FASTPATH)
if self.state.forwardOutput:
self.client.sendPDU(pdu)
self.client.sendPDU(pdu)
Loading