diff --git a/pyrdp/layer/layer.py b/pyrdp/layer/layer.py index 0369348b9..5050fe881 100644 --- a/pyrdp/layer/layer.py +++ b/pyrdp/layer/layer.py @@ -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 @@ -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. diff --git a/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py b/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py index 4176555fa..d171d13b5 100644 --- a/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py +++ b/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py @@ -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. # @@ -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) diff --git a/pyrdp/logging/StatCounter.py b/pyrdp/logging/StatCounter.py index 31111cf47..a20c2dcad 100644 --- a/pyrdp/logging/StatCounter.py +++ b/pyrdp/logging/StatCounter.py @@ -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. # @@ -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: """ diff --git a/pyrdp/mitm/DynamicChannelMITM.py b/pyrdp/mitm/DynamicChannelMITM.py new file mode 100644 index 000000000..11a079a28 --- /dev/null +++ b/pyrdp/mitm/DynamicChannelMITM.py @@ -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) diff --git a/pyrdp/mitm/RDPMITM.py b/pyrdp/mitm/RDPMITM.py index 04ec699bc..f4364789d 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -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 @@ -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 @@ -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 @@ -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) @@ -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: @@ -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. diff --git a/pyrdp/parser/rdp/fastpath.py b/pyrdp/parser/rdp/fastpath.py index 7a70343ba..532821582 100644 --- a/pyrdp/parser/rdp/fastpath.py +++ b/pyrdp/parser/rdp/fastpath.py @@ -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 @@ -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: @@ -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. diff --git a/pyrdp/parser/rdp/virtual_channel/device_redirection.py b/pyrdp/parser/rdp/virtual_channel/device_redirection.py index b2a83cf38..52985ffd7 100644 --- a/pyrdp/parser/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/parser/rdp/virtual_channel/device_redirection.py @@ -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) @@ -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() @@ -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) @@ -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() @@ -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) @@ -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() @@ -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) @@ -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() diff --git a/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py b/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py index 11d955951..13ff2cf94 100644 --- a/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py +++ b/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py @@ -1,16 +1,17 @@ # # This file is part of the PyRDP project. -# Copyright (C) 2018, 2021 GoSecure Inc. +# Copyright (C) 2018-2021 GoSecure Inc. # Licensed under the GPLv3 or later. # from io import BytesIO -from pyrdp.core import Uint16LE, Uint32LE, Uint8 +from pyrdp.core import Uint16LE, Uint8 from pyrdp.enum.virtual_channel.dynamic_channel import CbId, DynamicChannelCommand from pyrdp.parser import Parser from pyrdp.pdu import PDU -from pyrdp.pdu.rdp.virtual_channel.dynamic_channel import CreateRequestPDU, CreateResponsePDU, DynamicChannelPDU +from pyrdp.pdu.rdp.virtual_channel.dynamic_channel import CreateRequestPDU, DataPDU, \ + DynamicChannelPDU class DynamicChannelParser(Parser): @@ -18,8 +19,40 @@ class DynamicChannelParser(Parser): Parser for the dynamic channel (drdynvc) packets. """ - def __init__(self): + def __init__(self, isClient): super().__init__() + self.isClient = isClient + + if self.isClient: + # Parsers and writers unique for client + + self.parsers = { + + } + + self.writers = { + DynamicChannelCommand.CREATE: self.writeCreateRequest + } + else: + # Parsers and writers unique for server + + self.parsers = { + DynamicChannelCommand.CREATE: self.parseCreateRequest + } + + self.writers = { + + } + + # Parsers and writers for both client and server + + self.parsers.update({ + DynamicChannelCommand.DATA: self.parseData + }) + + self.writers.update({ + DynamicChannelCommand.DATA: self.writeData + }) def doParse(self, data: bytes) -> PDU: stream = BytesIO(data) @@ -27,16 +60,48 @@ def doParse(self, data: bytes) -> PDU: cbid = (header & 0b00000011) sp = (header & 0b00001100) >> 2 cmd = (header & 0b11110000) >> 4 + pdu = DynamicChannelPDU(cbid, sp, cmd, stream.read()) + if cmd in self.parsers: + return self.parsers[cmd](pdu) + else: + return pdu - if cmd == DynamicChannelCommand.CREATE: - channelId = self.readChannelId(stream, cbid) - channelName = "" + def parseCreateRequest(self, pdu: DynamicChannelPDU) -> CreateRequestPDU: + """ + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/4448ba4d-9a72-429f-8b65-6f4ec44f2985 + :param pdu: The PDU with the payload to decode. + """ + stream = BytesIO(pdu.payload) + channelId = self.readChannelId(stream, pdu.cbid) + channelName = "" + char = stream.read(1).decode() + while char != "\x00": + channelName += char char = stream.read(1).decode() - while char != "\x00": - channelName += char - char = stream.read(1).decode() - return CreateRequestPDU(cbid, sp, channelId, channelName) - return DynamicChannelPDU(cbid, sp, cmd, stream.read()) + return CreateRequestPDU(pdu.cbid, pdu.sp, channelId, channelName) + + def writeCreateRequest(self, pdu: CreateRequestPDU, stream: BytesIO): + """ + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/4448ba4d-9a72-429f-8b65-6f4ec44f2985 + """ + self.writeChannelId(stream, pdu.cbid, pdu.channelId) + stream.write(pdu.channelName.encode() + b"\x00") + + def parseData(self, pdu: DynamicChannelPDU) -> DataPDU: + """ + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803 + """ + stream = BytesIO(pdu.payload) + channelId = self.readChannelId(stream, pdu.cbid) + data = stream.read() + return DataPDU(pdu.cbid, pdu.sp, channelId, payload=data) + + def writeData(self, pdu: DataPDU, stream: BytesIO): + """ + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803 + """ + self.writeChannelId(stream, pdu.cbid, pdu.channelId) + stream.write(pdu.payload) def write(self, pdu: DynamicChannelPDU) -> bytes: stream = BytesIO() @@ -44,11 +109,11 @@ def write(self, pdu: DynamicChannelPDU) -> bytes: header |= pdu.sp << 2 header |= pdu.cmd << 4 Uint8.pack(header, stream) - if isinstance(pdu, CreateResponsePDU): - self.writeChannelId(stream, pdu.cbid, pdu.channelId) - Uint32LE.pack(pdu.creationStatus, stream) + if pdu.cmd in self.writers: + self.writers[pdu.cmd](pdu, stream) else: - raise NotImplementedError() + stream.write(pdu.payload) + return stream.getvalue() def readChannelId(self, stream: BytesIO, cbid: int): diff --git a/pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py b/pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py index 5817fcea4..4d3734f52 100644 --- a/pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py +++ b/pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py @@ -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. # @@ -41,3 +41,13 @@ def __init__(self, cbid, sp, channelId: int, creationStatus: int): super().__init__(cbid, sp, DynamicChannelCommand.CREATE) self.channelId = channelId self.creationStatus = creationStatus + + +class DataPDU(DynamicChannelPDU): + """ + https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803 + """ + + def __init__(self, cbid, sp, channelId: int, payload: bytes): + super().__init__(cbid, sp, DynamicChannelCommand.DATA, payload=payload) + self.channelId = channelId