Skip to content

Commit

Permalink
Merge pull request #419 from GoSecure/file-io-fixes
Browse files Browse the repository at this point in the history
Improvements to DeviceRedirection handling
  • Loading branch information
obilodeau authored Nov 15, 2022
2 parents 7d01688 + e7022cf commit e0b63b2
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 39 deletions.
6 changes: 3 additions & 3 deletions pyrdp/core/event.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2019 GoSecure Inc.
# Copyright (C) 2019, 2022 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

import asyncio
import operator
from typing import Callable, Dict
from typing import Callable, Dict, List


class Event:
Expand Down Expand Up @@ -118,7 +118,7 @@ def __init__(self):
"""
Create a new (empty) event engine.
"""
self.events: [Event] = []
self.events: List[Event] = []

def processObject(self, obj):
"""
Expand Down
5 changes: 3 additions & 2 deletions pyrdp/enum/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018-2021 GoSecure Inc.
# Copyright (C) 2018-2022 GoSecure Inc.
# Licensed under the GPLv3 or later.
#
# flake8: noqa
Expand All @@ -20,6 +20,7 @@
from pyrdp.enum.virtual_channel.device_redirection import CreateOption, DeviceRedirectionComponent, \
DeviceRedirectionPacketID, DeviceType, DirectoryAccessMask, FileAccessMask, FileAttributes, \
FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, GeneralCapabilityVersion, \
IOOperationSeverity, MajorFunction, MinorFunction, RDPDRCapabilityType
MajorFunction, MinorFunction, RDPDRCapabilityType
from pyrdp.enum.virtual_channel.virtual_channel import VirtualChannelPDUFlag
from pyrdp.enum.windows import NTSTATUS, NtStatusSeverity
from pyrdp.enum.x224 import X224PDUType
12 changes: 1 addition & 11 deletions pyrdp/enum/virtual_channel/device_redirection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018 GoSecure Inc.
# Copyright (C) 2018, 2022 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand Down Expand Up @@ -60,16 +60,6 @@ class MinorFunction(IntEnum):
IRP_MN_NOTIFY_CHANGE_DIRECTORY = 0x00000002


class IOOperationSeverity(IntEnum):
"""
https://msdn.microsoft.com/en-us/library/cc231200.aspx
"""
STATUS_SEVERITY_SUCCESS = 0x0
STATUS_SEVERITY_INFORMATIONAL = 0x1
STATUS_SEVERITY_WARNING = 0x2
STATUS_SEVERITY_ERROR = 0x3


class CreateOption(IntEnum):
FILE_DIRECTORY_FILE = 0x00000001
FILE_WRITE_THROUGH = 0x00000002
Expand Down
33 changes: 33 additions & 0 deletions pyrdp/enum/windows.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2022 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

# Disable line-too-long lints.
# flake8: noqa

from enum import IntEnum


class NtStatusSeverity(IntEnum):
"""
[MS-ERREF]: Windows Error Codes
https://msdn.microsoft.com/en-us/library/cc231199.aspx
[MS-ERREF]: NTSTATUS
https://msdn.microsoft.com/en-us/library/cc231200.aspx
"""
STATUS_SEVERITY_SUCCESS = 0x0
STATUS_SEVERITY_INFORMATIONAL = 0x1
STATUS_SEVERITY_WARNING = 0x2
STATUS_SEVERITY_ERROR = 0x3


class NTSTATUS(IntEnum):
"""
[MS-ERREF]: Windows Error Codes
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-erref/87fba13e-bf06-450e-83b1-9241dc81e781
"""
STATUS_SUCCESS = 0x00000000
STATUS_NO_MORE_FILES = 0x80000006
STATUS_NO_SUCH_FILE = 0xC000000F
24 changes: 18 additions & 6 deletions pyrdp/mitm/DeviceRedirectionMITM.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2019-2021 GoSecure Inc.
# Copyright (C) 2019-2022 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

Expand All @@ -10,8 +10,8 @@
from pyrdp.core import ObservedBy, Observer, Subject
from pyrdp.enum import CreateOption, DeviceRedirectionPacketID, DeviceType, DirectoryAccessMask, FileAccessMask, \
FileAttributes, \
FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, IOOperationSeverity, \
MajorFunction, MinorFunction
FileCreateDisposition, FileCreateOptions, FileShareAccess, FileSystemInformationClass, \
MajorFunction, MinorFunction, NTSTATUS, NtStatusSeverity
from pyrdp.layer import DeviceRedirectionLayer
from pyrdp.logging.StatCounter import StatCounter, STAT
from pyrdp.mitm.FileMapping import FileMapping
Expand Down Expand Up @@ -177,12 +177,23 @@ def handleIOResponse(self, pdu: DeviceIOResponsePDU):
elif key in self.currentIORequests:
requestPDU = self.currentIORequests.pop(key)

if pdu.ioStatus >> 30 == IOOperationSeverity.STATUS_SEVERITY_ERROR:
if pdu.ioStatus >> 30 == NtStatusSeverity.STATUS_SEVERITY_ERROR:
self.statCounter.increment(STAT.DEVICE_REDIRECTION_IOERROR)
self.log.warning("Received an IO Response with an error IO status: %(responsePDU)s for request %(requestPDU)s", {"responsePDU": repr(pdu), "requestPDU": repr(requestPDU)})
# log only for unexpected errors since "no such files" and "no more files" are frequent:
# "no such files" for all attempts at fetching .desktop.ini
# "no more files" when directory listings are finished
if pdu.ioStatus not in [NTSTATUS.STATUS_NO_SUCH_FILE,
NTSTATUS.STATUS_NO_MORE_FILES]:
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)
# Good place to debug
#else:
# self.log.warning("No handler was triggered for this IO request: %(requestPDU)s. Response: %(responsePDU)s", {"responsePDU": repr(pdu), "requestPDU": repr(requestPDU)})

else:
self.log.error("Received IO response to unknown request #%(completionID)d", {"completionID": pdu.completionID})

Expand Down Expand Up @@ -214,8 +225,9 @@ def handleCreateResponse(self, request: DeviceCreateRequestPDU, response: Device

isFileRead = request.desiredAccess & (FileAccessMask.GENERIC_READ | FileAccessMask.FILE_READ_DATA) != 0
isDirectory = request.createOptions & CreateOption.FILE_NON_DIRECTORY_FILE == 0
fileExists = (response.ioStatus != NTSTATUS.STATUS_NO_SUCH_FILE)

if isFileRead and not isDirectory:
if fileExists and isFileRead and not isDirectory:
mapping = FileMapping.generate(request.path, self.config.fileDir, self.deviceRoot(response.deviceID), self.log)
key = (response.deviceID, response.fileID)
self.mappings[key] = mapping
Expand Down
13 changes: 7 additions & 6 deletions pyrdp/parser/rdp/virtual_channel/device_redirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, \
DeviceType, FileAccessMask, FileAttributes, FileCreateDisposition, \
FileCreateOptions, FileShareAccess, FileSystemInformationClass, \
GeneralCapabilityVersion, MajorFunction, MinorFunction, RDPDRCapabilityType
GeneralCapabilityVersion, MajorFunction, MinorFunction, \
RDPDRCapabilityType, NTSTATUS
from pyrdp.parser import Parser
from pyrdp.pdu import DeviceAnnounce, DeviceCloseRequestPDU, DeviceCloseResponsePDU, DeviceCreateRequestPDU, \
DeviceCreateResponsePDU, DeviceDirectoryControlResponsePDU, DeviceIORequestPDU, DeviceIOResponsePDU, \
Expand Down Expand Up @@ -296,7 +297,7 @@ def writeDeviceIORequest(self, pdu: DeviceIORequestPDU, stream: BytesIO):
def parseDeviceIOResponse(self, stream: BytesIO) -> DeviceIOResponsePDU:
deviceID = Uint32LE.unpack(stream)
completionID = Uint32LE.unpack(stream)
ioStatus = Uint32LE.unpack(stream)
ioStatus = NTSTATUS(Uint32LE.unpack(stream))

majorFunction = self.majorFunctionsForParsingResponse.pop(completionID, None)

Expand Down Expand Up @@ -361,7 +362,7 @@ def writeDeviceCreateRequest(self, pdu: DeviceCreateRequestPDU, stream: BytesIO)
stream.write(path)


def parseDeviceCreateResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceCreateResponsePDU:
def parseDeviceCreateResponse(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, stream: BytesIO) -> 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
Expand Down Expand Up @@ -396,7 +397,7 @@ 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:
def parseDeviceReadResponse(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, stream: BytesIO) -> DeviceReadResponsePDU:
length = Uint32LE.unpack(stream)
payload = stream.read(length)

Expand All @@ -415,7 +416,7 @@ def writeDeviceCloseRequest(self, _: DeviceCloseRequestPDU, stream: BytesIO):
stream.write(b"\x00" * 32) # Padding


def parseDeviceCloseResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceCloseResponsePDU:
def parseDeviceCloseResponse(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, stream: BytesIO) -> DeviceCloseResponsePDU:
stream.read(4) # Padding
return DeviceCloseResponsePDU(deviceID, completionID, ioStatus)

Expand Down Expand Up @@ -461,7 +462,7 @@ def writeDirectoryControlRequest(self, pdu: Union[DeviceIORequestPDU, DeviceQuer
stream.write(b"\x00" * 23) # protocol required padding
stream.write(path)

def parseDirectoryControlResponse(self, deviceID: int, completionID: int, ioStatus: int, stream: BytesIO) -> DeviceIOResponsePDU:
def parseDirectoryControlResponse(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, stream: BytesIO) -> DeviceIOResponsePDU:
minorFunction = self.minorFunctionsForParsingResponse.pop(completionID, None)

if minorFunction is None:
Expand Down
14 changes: 7 additions & 7 deletions pyrdp/pdu/rdp/virtual_channel/device_redirection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from pyrdp.enum import DeviceRedirectionComponent, DeviceRedirectionPacketID, \
DeviceType, FileAccessMask, FileAttributes, FileCreateDisposition, \
FileCreateOptions, FileShareAccess, FileSystemInformationClass, \
MajorFunction, MinorFunction, RDPDRCapabilityType
MajorFunction, MinorFunction, RDPDRCapabilityType, NTSTATUS
from pyrdp.pdu import PDU


Expand Down Expand Up @@ -43,7 +43,7 @@ class DeviceIOResponsePDU(DeviceRedirectionPDU):
https://msdn.microsoft.com/en-us/library/cc241334.aspx
"""

def __init__(self, majorFunction: Optional[MajorFunction], deviceID: int, completionID: int, ioStatus: int, payload=b""):
def __init__(self, majorFunction: Optional[MajorFunction], deviceID: int, completionID: int, ioStatus: NTSTATUS, payload=b""):
super().__init__(DeviceRedirectionComponent.RDPDR_CTYP_CORE, DeviceRedirectionPacketID.PAKID_CORE_DEVICE_IOCOMPLETION)
self.majorFunction = majorFunction
self.deviceID = deviceID
Expand Down Expand Up @@ -75,7 +75,7 @@ class DeviceCreateResponsePDU(DeviceIOResponsePDU):
https://msdn.microsoft.com/en-us/library/cc241335.aspx
"""

def __init__(self, deviceID: int, completionID: int, ioStatus: int, fileID: int, information: int):
def __init__(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, fileID: int, information: int):
super().__init__(MajorFunction.IRP_MJ_CREATE, deviceID, completionID, ioStatus)
self.fileID = fileID
self.information = information
Expand All @@ -98,7 +98,7 @@ class DeviceReadResponsePDU(DeviceIOResponsePDU):
https://msdn.microsoft.com/en-us/library/cc241337.aspx
"""

def __init__(self, deviceID: int, completionID: int, ioStatus: int, readData: bytes):
def __init__(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, readData: bytes):
super().__init__(MajorFunction.IRP_MJ_READ, deviceID, completionID, ioStatus, readData)


Expand All @@ -116,7 +116,7 @@ class DeviceCloseResponsePDU(DeviceIOResponsePDU):
https://msdn.microsoft.com/en-us/library/cc241336.aspx
"""

def __init__(self, deviceID: int, completionID: int, ioStatus: int):
def __init__(self, deviceID: int, completionID: int, ioStatus: NTSTATUS):
super().__init__(MajorFunction.IRP_MJ_CLOSE, deviceID, completionID, ioStatus)


Expand Down Expand Up @@ -184,13 +184,13 @@ def __init__(self, deviceID: int, fileID: int, completionID: int, informationCla


class DeviceDirectoryControlResponsePDU(DeviceIOResponsePDU):
def __init__(self, minorFunction: MinorFunction, deviceID: int, completionID: int, ioStatus: int, payload: bytes = b""):
def __init__(self, minorFunction: MinorFunction, deviceID: int, completionID: int, ioStatus: NTSTATUS, 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: List[FileInformationBase], endByte: bytes):
def __init__(self, deviceID: int, completionID: int, ioStatus: NTSTATUS, informationClass: FileSystemInformationClass, fileInformation: List[FileInformationBase], endByte: bytes):
super().__init__(MinorFunction.IRP_MN_QUERY_DIRECTORY, deviceID, completionID, ioStatus)
self.informationClass = informationClass
self.fileInformation = fileInformation
Expand Down
8 changes: 4 additions & 4 deletions test/test_DeviceRedirectionMITM.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2020-2021 GoSecure Inc.
# Copyright (C) 2020-2022 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

import unittest
from pathlib import Path
from unittest.mock import Mock, MagicMock, patch

from pyrdp.enum import CreateOption, FileAccessMask, IOOperationSeverity, DeviceRedirectionPacketID, MajorFunction, \
MinorFunction
from pyrdp.enum import CreateOption, FileAccessMask, DeviceRedirectionPacketID, MajorFunction, \
MinorFunction, NtStatusSeverity
from pyrdp.logging.StatCounter import StatCounter, STAT
from pyrdp.mitm.DeviceRedirectionMITM import DeviceRedirectionMITM
from pyrdp.pdu import DeviceIOResponsePDU, DeviceRedirectionPDU


def MockIOError():
ioError = Mock(deviceID = 0, completionID = 0, ioStatus = IOOperationSeverity.STATUS_SEVERITY_ERROR << 30)
ioError = Mock(deviceID = 0, completionID = 0, ioStatus = NtStatusSeverity.STATUS_SEVERITY_ERROR << 30)
return ioError


Expand Down

0 comments on commit e0b63b2

Please sign in to comment.