From c134518905b76e4ec9aae9c1a02f6e881e810f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89milio=20Gonzalez?= Date: Sun, 14 Jun 2020 15:07:23 -0400 Subject: [PATCH] Add basic support for parsing Dynamic Channels (drdynvc) during an RDP connection. --- .../rdp/virtual_channel/dynamic_channel.py | 2 +- pyrdp/logging/StatCounter.py | 9 ++ pyrdp/mitm/DynamicChannelMITM.py | 75 +++++++++++++++ pyrdp/mitm/RDPMITM.py | 40 +++++++- .../rdp/virtual_channel/dynamic_channel.py | 95 ++++++++++++++++--- .../rdp/virtual_channel/dynamic_channel.py | 10 ++ 6 files changed, 210 insertions(+), 21 deletions(-) create mode 100644 pyrdp/mitm/DynamicChannelMITM.py diff --git a/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py b/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py index 4176555fa..68d528800 100644 --- a/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py +++ b/pyrdp/layer/rdp/virtual_channel/dynamic_channel.py @@ -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..53d768208 100644 --- a/pyrdp/logging/StatCounter.py +++ b/pyrdp/logging/StatCounter.py @@ -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..2d2d59d9c --- /dev/null +++ b/pyrdp/mitm/DynamicChannelMITM.py @@ -0,0 +1,75 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2020-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.DEVICE_REDIRECTION) + 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 01961b95c..ecd21a7b2 100644 --- a/pyrdp/mitm/RDPMITM.py +++ b/pyrdp/mitm/RDPMITM.py @@ -15,6 +15,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.adapters import SessionLogger from pyrdp.logging.observers import FastPathLogger, LayerLogger, MCSLogger, SecurityLogger, \ @@ -25,6 +27,7 @@ from pyrdp.mitm.ClipboardMITM import ActiveClipboardStealer, PassiveClipboardStealer from pyrdp.mitm.config import MITMConfig 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.layerset import RDPLayerSet @@ -37,10 +40,9 @@ from pyrdp.mitm.TCPMITM import TCPMITM from pyrdp.mitm.VirtualChannelMITM import VirtualChannelMITM from pyrdp.mitm.X224MITM import X224MITM -from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver, \ - Recorder - -from pyrdp.layer.segmentation import SegmentationObserver +from pyrdp.parser.rdp.virtual_channel.dynamic_channel import DynamicChannelParser +from pyrdp.recording import FileLayer, Recorder, RecordingFastPathObserver, \ + RecordingSlowPathObserver class PacketForwarder(SegmentationObserver): @@ -238,6 +240,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) @@ -330,7 +334,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) + deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, + self.getLog(MCSChannelName.DEVICE_REDIRECTION), + self.statCounter, self.state) self.channelMITMs[client.channelID] = deviceRedirection if self.config.enableCrawler: @@ -339,6 +345,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/virtual_channel/dynamic_channel.py b/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py index 43ffdb5cc..5475993bd 100644 --- a/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py +++ b/pyrdp/parser/rdp/virtual_channel/dynamic_channel.py @@ -6,11 +6,12 @@ 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 parse(self, data: bytes) -> PDU: stream = BytesIO(data) @@ -27,16 +60,48 @@ def parse(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..74c6ea42a 100644 --- a/pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py +++ b/pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py @@ -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