Skip to content

Commit

Permalink
Add basic support for parsing Dynamic Channels (drdynvc) during an RD…
Browse files Browse the repository at this point in the history
…P connection.
  • Loading branch information
Res260 committed Jun 14, 2020
1 parent 95e9d75 commit c134518
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 21 deletions.
2 changes: 1 addition & 1 deletion pyrdp/layer/rdp/virtual_channel/dynamic_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
9 changes: 9 additions & 0 deletions pyrdp/logging/StatCounter.py
Original file line number Diff line number Diff line change
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-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)
40 changes: 35 additions & 5 deletions pyrdp/mitm/RDPMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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.
Expand Down
95 changes: 80 additions & 15 deletions pyrdp/parser/rdp/virtual_channel/dynamic_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,49 +6,114 @@

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):
"""
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)
header = Uint8.unpack(stream)
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()
header = pdu.cbid
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):
Expand Down
10 changes: 10 additions & 0 deletions pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit c134518

Please sign in to comment.