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 basic support for parsing Dynamic Channels (drdynvc) #232

Draft
wants to merge 6 commits into
base: main
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
6 changes: 3 additions & 3 deletions pyrdp/layer/layer.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018, 2019, 2021 GoSecure Inc.
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

from abc import ABCMeta, abstractmethod
from typing import Union
from typing import List, Union

from pyrdp.core import EventEngine, ObservedBy, Observer, Subject
from pyrdp.exceptions import UnknownPDUTypeError, ParsingError
Expand Down Expand Up @@ -162,7 +162,7 @@ def __init__(self):
self.next: BaseLayer = None

@staticmethod
def chain(first: 'LayerChainItem', second: Union['BaseLayer', 'LayerChainItem'], *layers: [Union['BaseLayer', 'LayerChainItem']]):
def chain(first: 'LayerChainItem', second: Union['BaseLayer', 'LayerChainItem'], *layers: List[Union['BaseLayer', 'LayerChainItem']]):
"""
Chain a series of layers together by calling setNext iteratively.
:param first: first layer in the chain.
Expand Down
4 changes: 2 additions & 2 deletions pyrdp/layer/rdp/virtual_channel/dynamic_channel.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018 GoSecure Inc.
# Copyright (C) 2018, 2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand All @@ -13,5 +13,5 @@ class DynamicChannelLayer(Layer):
Layer to receive and send DynamicChannel channel (drdynvc) packets.
"""

def __init__(self, parser = DynamicChannelParser()):
def __init__(self, parser: DynamicChannelParser):
super().__init__(parser)
11 changes: 10 additions & 1 deletion pyrdp/logging/StatCounter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2019 GoSecure Inc.
# Copyright (C) 2019-2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand Down Expand Up @@ -111,6 +111,15 @@ class STAT:
CLIPBOARD_PASTE = "clipboardPastes"
# Number of times data has been pasted by either end

DYNAMIC_CHANNEL = "dynamicChannel"
# Number of Dynamic Virtual Channel PDUs coming from either end

DYNAMIC_CHANNEL_CLIENT = "dynamicChannelClient"
# Number of Dynamic Virtual Channel PDUs coming from the client

DYNAMIC_CHANNEL_SERVER = "dynamicChannelServer"
# Number of Dynamic Virtual Channel PDUs coming from the server


class StatCounter:
"""
Expand Down
75 changes: 75 additions & 0 deletions pyrdp/mitm/DynamicChannelMITM.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#
import binascii
from logging import LoggerAdapter
from typing import Dict

from pyrdp.core import Subject
from pyrdp.layer.rdp.virtual_channel.dynamic_channel import DynamicChannelLayer
from pyrdp.logging.StatCounter import STAT, StatCounter
from pyrdp.mitm.state import RDPMITMState
from pyrdp.pdu.rdp.virtual_channel.dynamic_channel import CreateRequestPDU, DataPDU, \
DynamicChannelPDU


class DynamicChannelMITM(Subject):
"""
MITM component for the dynamic virtual channels (drdynvc).
"""

def __init__(self, client: DynamicChannelLayer, server: DynamicChannelLayer, log: LoggerAdapter,
statCounter: StatCounter, state: RDPMITMState):
"""
:param client: DynamicChannel layer for the client side
:param server: DynamicChannel layer for the server side
:param log: logger for this component
:param statCounter: Object to keep miscellaneous stats for the current connection
:param state: the state of the PyRDP MITM connection.
"""
super().__init__()

self.client = client
self.server = server
self.state = state
self.log = log
self.statCounter = statCounter

self.channels: Dict[int, str] = {}

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

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

def onClientPDUReceived(self, pdu: DynamicChannelPDU):
self.statCounter.increment(STAT.DYNAMIC_CHANNEL_CLIENT, STAT.DYNAMIC_CHANNEL)
self.handlePDU(pdu, self.server)

def onServerPDUReceived(self, pdu: DynamicChannelPDU):
self.statCounter.increment(STAT.DYNAMIC_CHANNEL_SERVER, STAT.DYNAMIC_CHANNEL)
self.handlePDU(pdu, self.client)

def handlePDU(self, pdu: DynamicChannelPDU, destination: DynamicChannelLayer):
"""
Handle the logic for a PDU and send the PDU to its destination.
:param pdu: the PDU that was received
:param destination: the destination layer
"""
if isinstance(pdu, CreateRequestPDU):
self.channels[pdu.channelId] = pdu.channelName
self.log.info("Dynamic virtual channel creation received: ID: %(channelId)d Name: %(channelName)s", {"channelId": pdu.channelId, "channelName": pdu.channelName})
elif isinstance(pdu, DataPDU):
if pdu.channelId not in self.channels:
self.log.error("Received a data PDU in an unkown channel: %(channelId)s", {"channelId": pdu.channelId})
else:
self.log.debug("Data PDU for channel %(channelName)s: %(data)s", {"data": binascii.hexlify(pdu.payload), "channelName": self.channels[pdu.channelId]})
else:
self.log.debug("Dynamic Channel PDU received: %(dynVcPdu)s", {"dynVcPdu": pdu})

destination.sendPDU(pdu)
34 changes: 33 additions & 1 deletion pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
from pyrdp.enum import MCSChannelName, ParserMode, PlayerPDUType, ScanCode, SegmentationPDUType
from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, \
VirtualChannelLayer
from pyrdp.layer.rdp.virtual_channel.dynamic_channel import DynamicChannelLayer
from pyrdp.layer.segmentation import SegmentationObserver
from pyrdp.logging import RC4LoggingObserver
from pyrdp.logging.StatCounter import StatCounter
from pyrdp.logging.adapters import SessionLogger
Expand All @@ -25,6 +27,7 @@
from pyrdp.mitm.AttackerMITM import AttackerMITM
from pyrdp.mitm.ClipboardMITM import ActiveClipboardStealer, PassiveClipboardStealer
from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITM
from pyrdp.mitm.DynamicChannelMITM import DynamicChannelMITM
from pyrdp.mitm.FastPathMITM import FastPathMITM
from pyrdp.mitm.FileCrawlerMITM import FileCrawlerMITM
from pyrdp.mitm.MCSMITM import MCSMITM
Expand All @@ -38,6 +41,7 @@
from pyrdp.mitm.config import MITMConfig
from pyrdp.mitm.layerset import RDPLayerSet
from pyrdp.mitm.state import RDPMITMState
from pyrdp.parser.rdp.virtual_channel.dynamic_channel import DynamicChannelParser
from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver, \
Recorder
from pyrdp.security import NTLMSSPState
Expand Down Expand Up @@ -274,6 +278,8 @@ def buildChannel(self, client: MCSServerChannel, server: MCSClientChannel):
self.buildClipboardChannel(client, server)
elif self.state.channelMap[channelID] == MCSChannelName.DEVICE_REDIRECTION:
self.buildDeviceChannel(client, server)
elif self.state.channelMap[channelID] == MCSChannelName.DYNAMIC_CHANNEL:
self.buildDynamicChannel(client, server)
else:
self.buildVirtualChannel(client, server)

Expand Down Expand Up @@ -366,7 +372,9 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel)
LayerChainItem.chain(client, clientSecurity, clientVirtualChannel, clientLayer)
LayerChainItem.chain(server, serverSecurity, serverVirtualChannel, serverLayer)

deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.statCounter, self.state, self.tcp)
deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer,
self.getLog(MCSChannelName.DEVICE_REDIRECTION),
self.statCounter, self.state, self.tcp)
self.channelMITMs[client.channelID] = deviceRedirection

if self.config.enableCrawler:
Expand All @@ -375,6 +383,30 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel)
if self.attacker:
self.attacker.setDeviceRedirectionComponent(deviceRedirection)

def buildDynamicChannel(self, client: MCSServerChannel, server: MCSClientChannel):
"""
Build the MITM component for the dynamic channel.
Ref: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/0147004d-1542-43ab-9337-93338f218587
:param client: MCS channel for the client side
:param server: MCS channel for the server side
"""

clientSecurity = self.state.createSecurityLayer(ParserMode.SERVER, True)
clientVirtualChannel = VirtualChannelLayer(activateShowProtocolFlag=False)
clientLayer = DynamicChannelLayer(DynamicChannelParser(isClient=True))
serverSecurity = self.state.createSecurityLayer(ParserMode.CLIENT, True)
serverVirtualChannel = VirtualChannelLayer(activateShowProtocolFlag=False)
serverLayer = DynamicChannelLayer(DynamicChannelParser(isClient=False))

clientLayer.addObserver(LayerLogger(self.getClientLog(MCSChannelName.DYNAMIC_CHANNEL)))
serverLayer.addObserver(LayerLogger(self.getServerLog(MCSChannelName.DYNAMIC_CHANNEL)))

LayerChainItem.chain(client, clientSecurity, clientVirtualChannel, clientLayer)
LayerChainItem.chain(server, serverSecurity, serverVirtualChannel, serverLayer)

dynamicChannelMITM = DynamicChannelMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DYNAMIC_CHANNEL), self.statCounter, self.state)
self.channelMITMs[client.channelID] = dynamicChannelMITM

def buildVirtualChannel(self, client: MCSServerChannel, server: MCSClientChannel):
"""
Build a generic MITM component for any virtual channel.
Expand Down
8 changes: 4 additions & 4 deletions pyrdp/parser/rdp/fastpath.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Copyright (C) 2018-2021 GoSecure Inc.
# Licensed under the GPLv3 or later.
#
import typing
from typing import List, Union
from binascii import hexlify
from io import BytesIO

Expand Down Expand Up @@ -77,7 +77,7 @@ def parseLength(self, stream: BytesIO) -> int:

return length

def parseEvents(self, data: bytes) -> [FastPathEvent]:
def parseEvents(self, data: bytes) -> List[FastPathEvent]:
events = []

while len(data) > 0:
Expand Down Expand Up @@ -443,9 +443,9 @@ def write(self, event: FastPathOutputEvent) -> bytes:

def createFastPathParser(tls: bool,
encryptionMethod: EncryptionMethod,
crypter: typing.Union[RC4Crypter, RC4CrypterProxy],
crypter: Union[RC4Crypter, RC4CrypterProxy],
mode: ParserMode) \
-> typing.Union[BasicFastPathParser, SignedFastPathParser, FIPSFastPathParser]:
-> Union[BasicFastPathParser, SignedFastPathParser, FIPSFastPathParser]:
"""
Create a fast-path parser based on which encryption method is used.
:param tls: whether TLS is used or not.
Expand Down
16 changes: 8 additions & 8 deletions pyrdp/parser/rdp/virtual_channel/device_redirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ def writeFileInformationList(self, dataList: List[bytes], stream: BytesIO):

def parseFileDirectoryInformation(self, data: bytes) -> List[FileDirectoryInformation]:
stream = BytesIO(data)
information: [FileDirectoryInformation] = []
information: List[FileDirectoryInformation] = []

while stream.tell() < len(data):
nextEntryOffset = Uint32LE.unpack(stream)
Expand Down Expand Up @@ -547,7 +547,7 @@ def parseFileDirectoryInformation(self, data: bytes) -> List[FileDirectoryInform


def writeFileDirectoryInformation(self, information: List[FileDirectoryInformation], stream: BytesIO):
dataList: [bytes] = []
dataList: List[bytes] = []

for info in information:
substream = BytesIO()
Expand All @@ -571,7 +571,7 @@ def writeFileDirectoryInformation(self, information: List[FileDirectoryInformati

def parseFileFullDirectoryInformation(self, data: bytes) -> List[FileFullDirectoryInformation]:
stream = BytesIO(data)
information: [FileFullDirectoryInformation] = []
information: List[FileFullDirectoryInformation] = []

while stream.tell() < len(data):
nextEntryOffset = Uint32LE.unpack(stream)
Expand Down Expand Up @@ -612,7 +612,7 @@ def parseFileFullDirectoryInformation(self, data: bytes) -> List[FileFullDirecto


def writeFileFullDirectoryInformation(self, information: List[FileFullDirectoryInformation], stream: BytesIO):
dataList: [bytes] = []
dataList: List[bytes] = []

for info in information:
substream = BytesIO()
Expand All @@ -637,7 +637,7 @@ def writeFileFullDirectoryInformation(self, information: List[FileFullDirectoryI

def parseFileBothDirectoryInformation(self, data: bytes) -> List[FileBothDirectoryInformation]:
stream = BytesIO(data)
information: [FileBothDirectoryInformation] = []
information: List[FileBothDirectoryInformation] = []

while stream.tell() < len(data):
nextEntryOffset = Uint32LE.unpack(stream)
Expand Down Expand Up @@ -683,7 +683,7 @@ def parseFileBothDirectoryInformation(self, data: bytes) -> List[FileBothDirecto


def writeFileBothDirectoryInformation(self, information: List[FileBothDirectoryInformation], stream: BytesIO):
dataList: [bytes] = []
dataList: List[bytes] = []

for info in information:
substream = BytesIO()
Expand Down Expand Up @@ -712,7 +712,7 @@ def writeFileBothDirectoryInformation(self, information: List[FileBothDirectoryI

def parseFileNamesInformation(self, data: bytes) -> List[FileNamesInformation]:
stream = BytesIO(data)
information: [FileNamesInformation] = []
information: List[FileNamesInformation] = []

while stream.tell() < len(data):
nextEntryOffset = Uint32LE.unpack(stream)
Expand All @@ -733,7 +733,7 @@ def parseFileNamesInformation(self, data: bytes) -> List[FileNamesInformation]:


def writeFileNamesInformation(self, information: List[FileNamesInformation], stream: BytesIO):
dataList: [bytes] = []
dataList: List[bytes] = []

for info in information:
substream = BytesIO()
Expand Down
Loading