Skip to content

Commit

Permalink
Merge pull request #187 from GoSecure/gdi-support
Browse files Browse the repository at this point in the history
Add support for Graphics Device Interface Acceleration Extensions (MS-RDPEGDI)
  • Loading branch information
alxbl committed Apr 9, 2020
2 parents c133371 + 5906da4 commit eb222b1
Show file tree
Hide file tree
Showing 29 changed files with 4,239 additions and 209 deletions.
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
# ignore =
max-line-length = 120
1 change: 1 addition & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/master[
* Added `--disable-active-clipboard` switch to prevent clipboard request injection
* Added `--no-downgrade` switch to prevent protocol downgrading where possible {uri-issue}189[#189]
* Added `--no-files` switch to prevent extracting transferred files {uri-issue}195[#195]
* Added `--gdi` MITM switch to enable accelerated graphics pipeline (MS-RDPEGDI) ({uri-issue}50[#50])

=== Bug fixes

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ PyRDP was [introduced in 2018](https://www.gosecure.net/blog/2018/12/19/rdp-man-
+ [Other MITM arguments](#other-mitm-arguments)
- [--no-downgrade](#--no-downgrade)
- [--transparent](#--transparent)
- [`--gdi`: Accelerated Graphics Pipeline](#--gdi-accelerated-graphics-pipeline)
* [Using the PyRDP Player](#using-the-pyrdp-player)
+ [Playing a replay file](#playing-a-replay-file)
+ [Listening for live connections](#listening-for-live-connections)
Expand Down Expand Up @@ -386,6 +387,16 @@ This setup is a base example and might be much more complex depending on your en
To cleanup, you should delete the created routes, remove the custom routing table from `rt_tables` and
remove the `ip rule`.

##### `--gdi`: Accelerated Graphics Pipeline

Tells the MITM to allow clients to use [Graphics Device Interface Acceleration][gdi] Extensions to stream
drawing orders instead of raw bitmaps. The advantage of this mode is a significant reduction in required bandwidth
for high resolution connections.

Note that some GDI drawing orders are currently unimplemented because they appear to be unused.
If you have a replay which contains any unsupported or untested order, do not hesitate to share it with the project maintainers so that support can be added as required. (Make sure that the trace does not contain sensitive information)

[gdi]: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpegdi/745f2eee-d110-464c-8aca-06fc1814f6ad

### Using the PyRDP Player
Use `pyrdp-player.py` to run the player.
Expand Down
36 changes: 21 additions & 15 deletions bin/pyrdp-player.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@

# asyncio needs to be imported first to ensure that the reactor is
# installed properly. Do not re-order.
import asyncio
import asyncio # noqa
from twisted.internet import asyncioreactor
asyncioreactor.install(asyncio.get_event_loop())

from pyrdp.core import settings
from pyrdp.logging import LOGGER_NAMES, NotifyHandler, configure as configureLoggers
from pyrdp.player import HAS_GUI
from pyrdp.player.config import DEFAULTS
from pyrdp.core import settings # noqa
from pyrdp.logging import LOGGER_NAMES, NotifyHandler, configure as configureLoggers # noqa
from pyrdp.player import HAS_GUI # noqa
from pyrdp.player.config import DEFAULTS # noqa

from pathlib import Path
import argparse
import logging
import logging.handlers
import sys
import os
from pathlib import Path # noqa
import argparse # noqa
import logging # noqa
import logging.handlers # noqa
import sys # noqa
import os # noqa

if HAS_GUI:
from pyrdp.player import MainWindow
Expand All @@ -34,7 +34,7 @@ def enableNotifications(logger):
# https://docs.python.org/3/library/os.html
if os.name != "nt":
notifyHandler = NotifyHandler()
notifyHandler.setFormatter(logging.Formatter("[{asctime}] - {message}", style = "{"))
notifyHandler.setFormatter(logging.Formatter("[{asctime}] - {message}", style="{"))

uiLogger = logging.getLogger(LOGGER_NAMES.PLAYER_UI)
uiLogger.addHandler(notifyHandler)
Expand All @@ -52,8 +52,10 @@ def main():
parser.add_argument("-b", "--bind", help="Bind address (default: 127.0.0.1)", default="127.0.0.1")
parser.add_argument("-p", "--port", help="Bind port (default: 3000)", default=3000)
parser.add_argument("-o", "--output", help="Output folder", default="pyrdp_output")
parser.add_argument("-L", "--log-level", help="Log level", default=None, choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"], nargs="?")
parser.add_argument("-F", "--log-filter", help="Only show logs from this logger name (accepts '*' wildcards)", default=None)
parser.add_argument("-L", "--log-level", help="Log level", default=None,
choices=["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL"], nargs="?")
parser.add_argument("-F", "--log-filter",
help="Only show logs from this logger name (accepts '*' wildcards)", default=None)
parser.add_argument("--headless", help="Parse a replay without rendering the user interface.", action="store_true")
args = parser.parse_args()

Expand All @@ -67,14 +69,18 @@ def main():
if args.output:
cfg.set('vars', 'output_dir', args.output)

outDir = Path(cfg.get('vars', 'output_dir'))
outDir.mkdir(exist_ok=True)

configureLoggers(cfg)
logger = logging.getLogger(LOGGER_NAMES.PYRDP)

if cfg.getboolean('logs', 'notifications', fallback=False) and not args.headless:
enableNotifications(logger)

if not HAS_GUI and not args.headless:
logger.error('Headless mode is not specified and PySide2 is not installed. Install PySide2 to use the graphical user interface.')
logger.error('Headless mode is not specified and PySide2 is not installed.'
' Install PySide2 to use the graphical user interface.')
sys.exit(127)

if not args.headless:
Expand Down
4 changes: 3 additions & 1 deletion pyrdp/enum/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018 GoSecure Inc.
# Copyright (C) 2018-2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#
# flake8: noqa

from pyrdp.enum.core import ParserMode
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 MouseButton, PlayerPDUType
from pyrdp.enum.rdp import *
from pyrdp.enum.orders import DrawingOrderControlFlags
from pyrdp.enum.scancode import ScanCode, ScanCodeTuple
from pyrdp.enum.segmentation import SegmentationPDUType
from pyrdp.enum.virtual_channel.clipboard import ClipboardFormatName, ClipboardFormatNumber, ClipboardMessageFlags, \
Expand Down
78 changes: 78 additions & 0 deletions pyrdp/enum/orders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

"""
Enumerations for Drawing Orders.
"""

from enum import IntEnum


class DrawingOrderControlFlags(IntEnum):
"""
https://msdn.microsoft.com/en-us/library/cc241574.aspx
"""
TS_STANDARD = 0x01
TS_SECONDARY = 0x02
TS_BOUNDS = 0x04
TS_TYPE_CHANGE = 0x08
TS_DELTA_COORDS = 0x10
TS_ZERO_BOUNDS_DELTAS = 0x20
TS_ZERO_FIELD_BYTE_BIT0 = 0x40
TS_ZERO_FIELD_BYTE_BIT1 = 0x80


class Primary(IntEnum):
DSTBLT = 0x00
PATBLT = 0x01
SCRBLT = 0x02
DRAW_NINE_GRID = 0x07
MULTI_DRAW_NINE_GRID = 0x08
LINE_TO = 0x09
OPAQUE_RECT = 0x0A
SAVE_BITMAP = 0x0B
MEMBLT = 0x0D
MEM3BLT = 0x0E
MULTI_DSTBLT = 0x0F
MULTI_PATBLT = 0x10
MULTI_SCRBLT = 0x11
MULTI_OPAQUE_RECT = 0x12
FAST_INDEX = 0x13
POLYGON_SC = 0x14
POLYGON_CB = 0x15
POLYLINE = 0x16
FAST_GLYPH = 0x18
ELLIPSE_SC = 0x19
ELLIPSE_CB = 0x1A
GLYPH_INDEX = 0x1B


class Secondary(IntEnum):
BITMAP_UNCOMPRESSED = 0x00
CACHE_COLOR_TABLE = 0x01
CACHE_BITMAP_COMPRESSED = 0x02
CACHE_GLYPH = 0x03
BITMAP_UNCOMPRESSED_V2 = 0x04
BITMAP_COMPRESSED_V2 = 0x05
CACHE_BRUSH = 0x07
BITMAP_COMPRESSED_V3 = 0x08


class Alternate(IntEnum):
SWITCH_SURFACE = 0x00
CREATE_OFFSCREEN_BITMAP = 0x01
STREAM_BITMAP_FIRST = 0x02
STREAM_BITMAP_NEXT = 0x03
CREATE_NINE_GRID_BITMAP = 0x04
GDIPLUS_FIRST = 0x05
GDIPLUS_NEXT = 0x06
GDIPLUS_END = 0x07
GDIPLUS_CACHE_FIRST = 0x08
GDIPLUS_CACHE_NEXT = 0x09
GDIPLUS_CACHE_END = 0x0A
WINDOW = 0x0B
COMPDESK_FIRST = 0x0C
FRAME_MARKER = 0x0D
41 changes: 32 additions & 9 deletions pyrdp/enum/rdp.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
#
# This file is part of the PyRDP project.
# Copyright (C) 2018, 2019 GoSecure Inc.
# Copyright (C) 2018-2020 GoSecure Inc.
# Licensed under the GPLv3 or later.
#

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

from enum import IntEnum, IntFlag


Expand Down Expand Up @@ -60,10 +63,12 @@ class SecurityHeaderType(IntEnum):
# Header type used for Client Info and Licensing PDUs if no encryption is used
DEFAULT = 1


class FastPathSecurityFlags:
FASTPATH_OUTPUT_SECURE_CHECKSUM = 0x40
FASTPATH_OUTPUT_ENCRYPTED = 0x80


class FastPathInputType(IntEnum):
FASTPATH_INPUT_EVENT_SCANCODE = 0
FASTPATH_INPUT_EVENT_MOUSE = 1
Expand All @@ -74,6 +79,9 @@ class FastPathInputType(IntEnum):


class FastPathOutputType(IntEnum):
"""
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/a1c4caa8-00ed-45bb-a06e-5177473766d3
"""
FASTPATH_UPDATETYPE_ORDERS = 0
FASTPATH_UPDATETYPE_BITMAP = 1
FASTPATH_UPDATETYPE_PALETTE = 2
Expand All @@ -86,6 +94,23 @@ class FastPathOutputType(IntEnum):
FASTPATH_UPDATETYPE_CACHED = 10
FASTPATH_UPDATETYPE_POINTER = 11

@staticmethod
def getText(updateType: 'FastPathOutputType'):
MESSAGES = {
FastPathOutputType.FASTPATH_UPDATETYPE_ORDERS: 'ORDERS',
FastPathOutputType.FASTPATH_UPDATETYPE_BITMAP: 'BITMAP',
FastPathOutputType.FASTPATH_UPDATETYPE_PALETTE: 'PALETTE',
FastPathOutputType.FASTPATH_UPDATETYPE_SYNCHRONIZE: 'SYNCHRONIZE',
FastPathOutputType.FASTPATH_UPDATETYPE_SURFCMDS: 'SURFCMDS',
FastPathOutputType.FASTPATH_UPDATETYPE_PTR_NULL: 'PTR_NULL',
FastPathOutputType.FASTPATH_UPDATETYPE_PTR_DEFAULT: 'PTR_DEFAULT',
FastPathOutputType.FASTPATH_UPDATETYPE_PTR_POSITION: 'PTR_POSITION',
FastPathOutputType.FASTPATH_UPDATETYPE_COLOR: 'COLOR',
FastPathOutputType.FASTPATH_UPDATETYPE_CACHED: 'CACHED',
FastPathOutputType.FASTPATH_UPDATETYPE_POINTER: 'POINTER',
}
return MESSAGES.get(updateType)


class FastPathOutputCompressionType(IntEnum):
"""
Expand Down Expand Up @@ -162,12 +187,17 @@ class SlowPathPDUType(IntEnum):
DATA_PDU = 0x7
SERVER_REDIR_PKT_PDU = 0xA


class SlowPathUpdateType(IntEnum):
"""
https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpbcgr/bd3c4df4-87b9-43dd-88cb-ce5b24698e19
"""
SLOWPATH_UPDATETYPE_ORDERS = 0
SLOWPATH_UPDATETYPE_BITMAP = 1
SLOWPATH_UPDATETYPE_PALETTE = 2
SLOWPATH_UPDATETYPE_SYNCHRONIZE = 3


class SlowPathDataType(IntEnum):
"""
Slow-path data PDU types.
Expand Down Expand Up @@ -361,6 +391,7 @@ class NegotiationFailureCode(IntEnum):
HYBRID_REQUIRED_BY_SERVER = 0x00000005
SSL_WITH_USER_AUTH_REQUIRED_BY_SERVER = 0x00000006


@staticmethod
def getMessage(code: "NegotiationFailureCode"):
MESSAGES = {
Expand Down Expand Up @@ -796,14 +827,6 @@ class ChannelOption(IntFlag):
REMOTE_CONTROL_PERSISTENT = 0x00100000


class DrawingOrderControlFlags(IntEnum):
"""
https://msdn.microsoft.com/en-us/library/cc241574.aspx
"""
TS_STANDARD = 0b00000001
TS_SECONDARY = 0b00000010


class BitmapFlags(IntEnum):
"""
https://msdn.microsoft.com/en-us/library/cc240612.aspx
Expand Down

0 comments on commit eb222b1

Please sign in to comment.