Skip to content

Commit fa5f437

Browse files
committed
Aligned diverged branch with master
2 parents f50a4a8 + c134518 commit fa5f437

File tree

6 files changed

+213
-21
lines changed

6 files changed

+213
-21
lines changed

pyrdp/layer/rdp/virtual_channel/dynamic_channel.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# This file is part of the PyRDP project.
3-
# Copyright (C) 2018 GoSecure Inc.
3+
# Copyright (C) 2018, 2020 GoSecure Inc.
44
# Licensed under the GPLv3 or later.
55
#
66

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

16-
def __init__(self, parser = DynamicChannelParser()):
16+
def __init__(self, parser: DynamicChannelParser):
1717
super().__init__(parser)

pyrdp/logging/StatCounter.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# This file is part of the PyRDP project.
3-
# Copyright (C) 2019 GoSecure Inc.
3+
# Copyright (C) 2019-2020 GoSecure Inc.
44
# Licensed under the GPLv3 or later.
55
#
66

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

114+
DYNAMIC_CHANNEL = "dynamicChannel"
115+
# Number of Dynamic Virtual Channel PDUs coming from either end
116+
117+
DYNAMIC_CHANNEL_CLIENT = "dynamicChannelClient"
118+
# Number of Dynamic Virtual Channel PDUs coming from the client
119+
120+
DYNAMIC_CHANNEL_SERVER = "dynamicChannelServer"
121+
# Number of Dynamic Virtual Channel PDUs coming from the server
122+
114123

115124
class StatCounter:
116125
"""

pyrdp/mitm/DynamicChannelMITM.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#
2+
# This file is part of the PyRDP project.
3+
# Copyright (C) 2020 GoSecure Inc.
4+
# Licensed under the GPLv3 or later.
5+
#
6+
import binascii
7+
from logging import LoggerAdapter
8+
from typing import Dict
9+
10+
from pyrdp.core import Subject
11+
from pyrdp.layer.rdp.virtual_channel.dynamic_channel import DynamicChannelLayer
12+
from pyrdp.logging.StatCounter import STAT, StatCounter
13+
from pyrdp.mitm.state import RDPMITMState
14+
from pyrdp.pdu.rdp.virtual_channel.dynamic_channel import CreateRequestPDU, DataPDU, \
15+
DynamicChannelPDU
16+
17+
18+
class DynamicChannelMITM(Subject):
19+
"""
20+
MITM component for the dynamic virtual channels (drdynvc).
21+
"""
22+
23+
def __init__(self, client: DynamicChannelLayer, server: DynamicChannelLayer, log: LoggerAdapter,
24+
statCounter: StatCounter, state: RDPMITMState):
25+
"""
26+
:param client: DynamicChannel layer for the client side
27+
:param server: DynamicChannel layer for the server side
28+
:param log: logger for this component
29+
:param statCounter: Object to keep miscellaneous stats for the current connection
30+
:param state: the state of the PyRDP MITM connection.
31+
"""
32+
super().__init__()
33+
34+
self.client = client
35+
self.server = server
36+
self.state = state
37+
self.log = log
38+
self.statCounter = statCounter
39+
40+
self.channels: Dict[int, str] = {}
41+
42+
self.client.createObserver(
43+
onPDUReceived=self.onClientPDUReceived,
44+
)
45+
46+
self.server.createObserver(
47+
onPDUReceived=self.onServerPDUReceived,
48+
)
49+
50+
def onClientPDUReceived(self, pdu: DynamicChannelPDU):
51+
self.statCounter.increment(STAT.DYNAMIC_CHANNEL_CLIENT, STAT.DYNAMIC_CHANNEL)
52+
self.handlePDU(pdu, self.server)
53+
54+
def onServerPDUReceived(self, pdu: DynamicChannelPDU):
55+
self.statCounter.increment(STAT.DYNAMIC_CHANNEL_SERVER, STAT.DEVICE_REDIRECTION)
56+
self.handlePDU(pdu, self.client)
57+
58+
def handlePDU(self, pdu: DynamicChannelPDU, destination: DynamicChannelLayer):
59+
"""
60+
Handle the logic for a PDU and send the PDU to its destination.
61+
:param pdu: the PDU that was received
62+
:param destination: the destination layer
63+
"""
64+
if isinstance(pdu, CreateRequestPDU):
65+
self.channels[pdu.channelId] = pdu.channelName
66+
self.log.info("Dynamic virtual channel creation received: ID: %(channelId)d Name: %(channelName)s", {"channelId": pdu.channelId, "channelName": pdu.channelName})
67+
elif isinstance(pdu, DataPDU):
68+
if pdu.channelId not in self.channels:
69+
self.log.error("Received a data PDU in an unkown channel: %(channelId)s", {"channelId": pdu.channelId})
70+
else:
71+
self.log.debug("Data PDU for channel %(channelName)s: %(data)s", {"data": binascii.hexlify(pdu.payload), "channelName": self.channels[pdu.channelId]})
72+
else:
73+
self.log.debug("Dynamic Channel PDU received: %(dynVcPdu)s", {"dynVcPdu": pdu})
74+
75+
destination.sendPDU(pdu)

pyrdp/mitm/RDPMITM.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
from pyrdp.enum import MCSChannelName, ParserMode, PlayerPDUType, ScanCode, SegmentationPDUType
1717
from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, \
1818
VirtualChannelLayer
19+
from pyrdp.layer.rdp.virtual_channel.dynamic_channel import DynamicChannelLayer
20+
from pyrdp.layer.segmentation import SegmentationObserver
1921
from pyrdp.logging import RC4LoggingObserver
2022
from pyrdp.logging.StatCounter import StatCounter
2123
from pyrdp.logging.adapters import SessionLogger
@@ -25,6 +27,7 @@
2527
from pyrdp.mitm.AttackerMITM import AttackerMITM
2628
from pyrdp.mitm.ClipboardMITM import ActiveClipboardStealer, PassiveClipboardStealer
2729
from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITM
30+
from pyrdp.mitm.DynamicChannelMITM import DynamicChannelMITM
2831
from pyrdp.mitm.FastPathMITM import FastPathMITM
2932
from pyrdp.mitm.FileCrawlerMITM import FileCrawlerMITM
3033
from pyrdp.mitm.MCSMITM import MCSMITM
@@ -38,6 +41,7 @@
3841
from pyrdp.mitm.config import MITMConfig
3942
from pyrdp.mitm.layerset import RDPLayerSet
4043
from pyrdp.mitm.state import RDPMITMState
44+
from pyrdp.parser.rdp.virtual_channel.dynamic_channel import DynamicChannelParser
4145
from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver, \
4246
Recorder
4347
from pyrdp.security import NTLMSSPState
@@ -274,6 +278,8 @@ def buildChannel(self, client: MCSServerChannel, server: MCSClientChannel):
274278
self.buildClipboardChannel(client, server)
275279
elif self.state.channelMap[channelID] == MCSChannelName.DEVICE_REDIRECTION:
276280
self.buildDeviceChannel(client, server)
281+
elif self.state.channelMap[channelID] == MCSChannelName.DYNAMIC_CHANNEL:
282+
self.buildDynamicChannel(client, server)
277283
else:
278284
self.buildVirtualChannel(client, server)
279285

@@ -366,7 +372,10 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel)
366372
LayerChainItem.chain(client, clientSecurity, clientVirtualChannel, clientLayer)
367373
LayerChainItem.chain(server, serverSecurity, serverVirtualChannel, serverLayer)
368374

369-
deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.statCounter, self.state, self.tcp)
375+
deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer,
376+
self.getLog(MCSChannelName.DEVICE_REDIRECTION),
377+
self.statCounter, self.state, self.tcp)
378+
370379
self.channelMITMs[client.channelID] = deviceRedirection
371380

372381
if self.config.enableCrawler:
@@ -375,6 +384,30 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel)
375384
if self.attacker:
376385
self.attacker.setDeviceRedirectionComponent(deviceRedirection)
377386

387+
def buildDynamicChannel(self, client: MCSServerChannel, server: MCSClientChannel):
388+
"""
389+
Build the MITM component for the dynamic channel.
390+
Ref: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/0147004d-1542-43ab-9337-93338f218587
391+
:param client: MCS channel for the client side
392+
:param server: MCS channel for the server side
393+
"""
394+
395+
clientSecurity = self.state.createSecurityLayer(ParserMode.SERVER, True)
396+
clientVirtualChannel = VirtualChannelLayer(activateShowProtocolFlag=False)
397+
clientLayer = DynamicChannelLayer(DynamicChannelParser(isClient=True))
398+
serverSecurity = self.state.createSecurityLayer(ParserMode.CLIENT, True)
399+
serverVirtualChannel = VirtualChannelLayer(activateShowProtocolFlag=False)
400+
serverLayer = DynamicChannelLayer(DynamicChannelParser(isClient=False))
401+
402+
clientLayer.addObserver(LayerLogger(self.getClientLog(MCSChannelName.DYNAMIC_CHANNEL)))
403+
serverLayer.addObserver(LayerLogger(self.getServerLog(MCSChannelName.DYNAMIC_CHANNEL)))
404+
405+
LayerChainItem.chain(client, clientSecurity, clientVirtualChannel, clientLayer)
406+
LayerChainItem.chain(server, serverSecurity, serverVirtualChannel, serverLayer)
407+
408+
dynamicChannelMITM = DynamicChannelMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DYNAMIC_CHANNEL), self.statCounter, self.state)
409+
self.channelMITMs[client.channelID] = dynamicChannelMITM
410+
378411
def buildVirtualChannel(self, client: MCSServerChannel, server: MCSClientChannel):
379412
"""
380413
Build a generic MITM component for any virtual channel.

pyrdp/parser/rdp/virtual_channel/dynamic_channel.py

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,119 @@
11
#
22
# This file is part of the PyRDP project.
3-
# Copyright (C) 2018, 2021 GoSecure Inc.
3+
# Copyright (C) 2018-2021 GoSecure Inc.
44
# Licensed under the GPLv3 or later.
55
#
66

77
from io import BytesIO
88

9-
from pyrdp.core import Uint16LE, Uint32LE, Uint8
9+
from pyrdp.core import Uint16LE, Uint8
1010
from pyrdp.enum.virtual_channel.dynamic_channel import CbId, DynamicChannelCommand
1111
from pyrdp.parser import Parser
1212
from pyrdp.pdu import PDU
13-
from pyrdp.pdu.rdp.virtual_channel.dynamic_channel import CreateRequestPDU, CreateResponsePDU, DynamicChannelPDU
13+
from pyrdp.pdu.rdp.virtual_channel.dynamic_channel import CreateRequestPDU, DataPDU, \
14+
DynamicChannelPDU
1415

1516

1617
class DynamicChannelParser(Parser):
1718
"""
1819
Parser for the dynamic channel (drdynvc) packets.
1920
"""
2021

21-
def __init__(self):
22+
def __init__(self, isClient):
2223
super().__init__()
24+
self.isClient = isClient
25+
26+
if self.isClient:
27+
# Parsers and writers unique for client
28+
29+
self.parsers = {
30+
31+
}
32+
33+
self.writers = {
34+
DynamicChannelCommand.CREATE: self.writeCreateRequest
35+
}
36+
else:
37+
# Parsers and writers unique for server
38+
39+
self.parsers = {
40+
DynamicChannelCommand.CREATE: self.parseCreateRequest
41+
}
42+
43+
self.writers = {
44+
45+
}
46+
47+
# Parsers and writers for both client and server
48+
49+
self.parsers.update({
50+
DynamicChannelCommand.DATA: self.parseData
51+
})
52+
53+
self.writers.update({
54+
DynamicChannelCommand.DATA: self.writeData
55+
})
2356

2457
def doParse(self, data: bytes) -> PDU:
2558
stream = BytesIO(data)
2659
header = Uint8.unpack(stream)
2760
cbid = (header & 0b00000011)
2861
sp = (header & 0b00001100) >> 2
2962
cmd = (header & 0b11110000) >> 4
63+
pdu = DynamicChannelPDU(cbid, sp, cmd, stream.read())
64+
if cmd in self.parsers:
65+
return self.parsers[cmd](pdu)
66+
else:
67+
return pdu
3068

31-
if cmd == DynamicChannelCommand.CREATE:
32-
channelId = self.readChannelId(stream, cbid)
33-
channelName = ""
69+
def parseCreateRequest(self, pdu: DynamicChannelPDU) -> CreateRequestPDU:
70+
"""
71+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/4448ba4d-9a72-429f-8b65-6f4ec44f2985
72+
:param pdu: The PDU with the payload to decode.
73+
"""
74+
stream = BytesIO(pdu.payload)
75+
channelId = self.readChannelId(stream, pdu.cbid)
76+
channelName = ""
77+
char = stream.read(1).decode()
78+
while char != "\x00":
79+
channelName += char
3480
char = stream.read(1).decode()
35-
while char != "\x00":
36-
channelName += char
37-
char = stream.read(1).decode()
38-
return CreateRequestPDU(cbid, sp, channelId, channelName)
39-
return DynamicChannelPDU(cbid, sp, cmd, stream.read())
81+
return CreateRequestPDU(pdu.cbid, pdu.sp, channelId, channelName)
82+
83+
def writeCreateRequest(self, pdu: CreateRequestPDU, stream: BytesIO):
84+
"""
85+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/4448ba4d-9a72-429f-8b65-6f4ec44f2985
86+
"""
87+
self.writeChannelId(stream, pdu.cbid, pdu.channelId)
88+
stream.write(pdu.channelName.encode() + b"\x00")
89+
90+
def parseData(self, pdu: DynamicChannelPDU) -> DataPDU:
91+
"""
92+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803
93+
"""
94+
stream = BytesIO(pdu.payload)
95+
channelId = self.readChannelId(stream, pdu.cbid)
96+
data = stream.read()
97+
return DataPDU(pdu.cbid, pdu.sp, channelId, payload=data)
98+
99+
def writeData(self, pdu: DataPDU, stream: BytesIO):
100+
"""
101+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803
102+
"""
103+
self.writeChannelId(stream, pdu.cbid, pdu.channelId)
104+
stream.write(pdu.payload)
40105

41106
def write(self, pdu: DynamicChannelPDU) -> bytes:
42107
stream = BytesIO()
43108
header = pdu.cbid
44109
header |= pdu.sp << 2
45110
header |= pdu.cmd << 4
46111
Uint8.pack(header, stream)
47-
if isinstance(pdu, CreateResponsePDU):
48-
self.writeChannelId(stream, pdu.cbid, pdu.channelId)
49-
Uint32LE.pack(pdu.creationStatus, stream)
112+
if pdu.cmd in self.writers:
113+
self.writers[pdu.cmd](pdu, stream)
50114
else:
51-
raise NotImplementedError()
115+
stream.write(pdu.payload)
116+
52117
return stream.getvalue()
53118

54119
def readChannelId(self, stream: BytesIO, cbid: int):

pyrdp/pdu/rdp/virtual_channel/dynamic_channel.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# This file is part of the PyRDP project.
3-
# Copyright (C) 2018 GoSecure Inc.
3+
# Copyright (C) 2018-2020 GoSecure Inc.
44
# Licensed under the GPLv3 or later.
55
#
66

@@ -41,3 +41,13 @@ def __init__(self, cbid, sp, channelId: int, creationStatus: int):
4141
super().__init__(cbid, sp, DynamicChannelCommand.CREATE)
4242
self.channelId = channelId
4343
self.creationStatus = creationStatus
44+
45+
46+
class DataPDU(DynamicChannelPDU):
47+
"""
48+
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedyc/15b59886-db44-47f1-8da3-47c8fcd82803
49+
"""
50+
51+
def __init__(self, cbid, sp, channelId: int, payload: bytes):
52+
super().__init__(cbid, sp, DynamicChannelCommand.DATA, payload=payload)
53+
self.channelId = channelId

0 commit comments

Comments
 (0)