Skip to content

Commit

Permalink
feat: Added rasterizer and glyph support.
Browse files Browse the repository at this point in the history
  • Loading branch information
alxbl committed Mar 16, 2020
1 parent d38134f commit 471d9f5
Show file tree
Hide file tree
Showing 16 changed files with 1,715 additions and 423 deletions.
25 changes: 20 additions & 5 deletions pyrdp/mitm/SlowPathMITM.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ def onConfirmActive(self, pdu: ConfirmActivePDU):

if self.state.config.downgrade:

# Only disable GDI if not explicitly requested.
# Disable surface commands
if CapabilityType.CAPSETTYPE_SURFACE_COMMANDS in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSETTYPE_SURFACE_COMMANDS].cmdFlags = 0

# Disable GDI if not explicitly requested.
if not self.state.config.useGdi:
# Force RDP server to send bitmap events instead of order events.
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER].orderFlags = OrderFlag.NEGOTIATEORDERSUPPORT | OrderFlag.ZEROBOUNDSDELTASSUPPORT
Expand All @@ -70,20 +74,31 @@ def onConfirmActive(self, pdu: ConfirmActivePDU):
# Override the bitmap cache capability set with null values.
if CapabilityType.CAPSTYPE_BITMAPCACHE in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_BITMAPCACHE] = Capability(CapabilityType.CAPSTYPE_BITMAPCACHE, b"\x00" * 36)
else:
# Disable NineGrid support (Not implemented in Player)
if CapabilityType.CAPSTYPE_ORDER in pdu.parsedCapabilitySets:
orders = pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER]
supported = bytearray(orders.orderSupport)
supported[0x7] = 0 # Spoof disable NineGrid support.
orders.orderSupport = supported

# Disable surface commands
if CapabilityType.CAPSETTYPE_SURFACE_COMMANDS in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSETTYPE_SURFACE_COMMANDS].cmdFlags = 0
if CapabilityType.CAPSTYPE_DRAWNINEGRIDCACHE in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_DRAWNINEGRIDCACHE].rawData = b"\x00"*8

# Disable virtual channel compression
if CapabilityType.CAPSTYPE_VIRTUALCHANNEL in pdu.parsedCapabilitySets:
pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_VIRTUALCHANNEL].flags = VirtualChannelCompressionFlag.VCCAPS_NO_COMPR


def onDemandActive(self, pdu: DemandActivePDU):
"""
Disable virtual channel compression.
:param pdu: the demand active PDU
"""

if CapabilityType.CAPSTYPE_ORDER in pdu.parsedCapabilitySets:
orders = pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_ORDER]
supported = bytearray(orders.orderSupport)
supported[0x7] = 0 # DRAWNINEGRID = False
orders.orderSupport = supported

pdu.parsedCapabilitySets[CapabilityType.CAPSTYPE_VIRTUALCHANNEL].flags = VirtualChannelCompressionFlag.VCCAPS_NO_COMPR
13 changes: 9 additions & 4 deletions pyrdp/parser/rdp/orders/alternate.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ def parse(s: BytesIO) -> 'CreateOffscreenBitmap':
cIndices = Uint16LE.unpack(s)
self.delete = [Uint16LE.unpack(s) for _ in range(cIndices)]
else:
self.delete = None
self.delete = []

return self

def __str__(self):
return f'<CreateOffscreenBitmap {self.cx}x{self.cy} Id={self.id} Del={len(self.delete)}>'


class SwitchSurface:
@staticmethod
Expand All @@ -56,14 +59,16 @@ def parse(s: BytesIO) -> 'CreateNineGridBitmap':
self.bpp = Uint8.unpack(s)
self.id = Uint16LE.unpack(s)

# NOTE: According to 2.2.2.2.1.3.4 There should be cx:16 and cy:16 here??
self.cx = Uint16LE.unpack(s)
self.cy = Uint16LE.unpack(s)

# NineGridInfo
self.flFlags = Uint32LE.unpack(s)
self.ulLeftWidth = Uint16LE.unpack(s)
self.ulRightWidth = Uint16LE.unpack(s)
self.ulTopHeight = Uint16LE.unpack(s)
self.ulBottomHeight = Uint16LE.unpack(s)
self.rgb = read_color(s) # FIXME: Bring this in
self.rgb = read_color(s)

return self

Expand All @@ -75,8 +80,8 @@ def parse(s: BytesIO) -> 'StreamBitmapFirst':

self.flags = Uint8.unpack(s)
self.bpp = Uint8.unpack(s)
self.type = Uint16LE.unpack(s)

self.type = Uint16LE.unpack(s)
self.width = Uint16LE.unpack(s)
self.height = Uint16LE.unpack(s)

Expand Down
95 changes: 79 additions & 16 deletions pyrdp/parser/rdp/orders/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Common stream reading utilities
"""
from io import BytesIO
from pyrdp.core.packing import Uint8, Uint16LE, Uint32LE
from pyrdp.core.packing import Uint8, Int8, Uint16LE, Int16LE, Uint32LE


def read_encoded_uint16(s: BytesIO) -> int:
Expand Down Expand Up @@ -58,8 +58,32 @@ def read_color(s: BytesIO):
return Uint32LE.unpack(s) & 0x00FFFFFF


def read_utf16_str(s: BytesIO, size: int) -> bytes:
return bytes([Uint16LE.unpack(s) for _ in range(size)]) # Decode into str?
def read_utf16_str(s: BytesIO, size: int) -> [int]:
return [Uint16LE.unpack(s) for _ in range(size)] # Decode into str?


def read_glyph_bitmap(w: int, h: int, s: BytesIO) -> bytes:
"""Read and inflate a glyph bitmap."""

# Glyph encoding is specified in section 2.2.2.2.1.2.6.1
scanline = ((w + 7) // 8)
size = scanline * h
packed = s.read(size)
pad = 4 - (size % 4)

if pad < 4: # Skip alignment padding.
s.read(pad)

# Convert to 1 byte per pixel format for debugging.
# data = bytearray(w * h)
# for y in range(h):
# line = y * w
# for x in range(w):
# bits = packed[scanline * y + (x // 8)]
# px = (bits >> (8 - (x % 8))) & 1
# data[line + x] = px
# return data
return packed


class Glyph:
Expand All @@ -72,13 +96,10 @@ def parse(s: BytesIO) -> 'Glyph':
self.cacheIndex = Uint16LE.unpack(s)
self.x = Uint16LE.unpack(s)
self.y = Uint16LE.unpack(s)
self.cx = Uint16LE.unpack(s)
self.cy = Uint16LE.unpack(s)
self.w = Uint16LE.unpack(s)
self.h = Uint16LE.unpack(s)

# Calculate aj length (DWORD-aligned bitfield)
cb = ((self.cx + 7) // 8) * self.cy
cb += 4 - (cb % 4) if ((cb % 4) > 0) else 0
self.aj = s.read(cb)
self.data = read_glyph_bitmap(self.w, self.h, s)

return self

Expand All @@ -95,13 +116,55 @@ def parse(s: BytesIO) -> Glyph:

self.x = read_encoded_int16(s)
self.y = read_encoded_int16(s)
self.cx = read_encoded_uint16(s)
self.cy = read_encoded_uint16(s)
self.w = read_encoded_uint16(s)
self.h = read_encoded_uint16(s)

# Calculate aj length (DWORD-aligned bitfield)

cb = ((self.cx + 7) // 8) * self.cy
cb += 4 - (cb % 4) if ((cb % 4) > 0) else 0
self.aj = s.read(cb)
self.data = read_glyph_bitmap(self.w, self.h, s)

return self


BOUND_LEFT = 0x01
BOUND_TOP = 0x02
BOUND_RIGHT = 0x04
BOUND_BOTTOM = 0x08
BOUND_DELTA_LEFT = 0x10
BOUND_DELTA_TOP = 0x20
BOUND_DELTA_RIGHT = 0x40
BOUND_DELTA_BOTTOM = 0x80


class Bounds:
"""A bounding rectangle."""

def __init__(self):
self.left = 0
self.top = 0
self.bottom = 0
self.right = 0

def update(self, s: BytesIO):
flags = Uint8.unpack(s)

if flags & BOUND_LEFT:
self.left = Int16LE.unpack(s)
elif flags & BOUND_DELTA_LEFT:
self.left += Int8.unpack(s)

if flags & BOUND_TOP:
self.top = Int16LE.unpack(s)
elif flags & BOUND_DELTA_TOP:
self.top += Int8.unpack(s)

if flags & BOUND_RIGHT:
self.right = Int16LE.unpack(s)
elif flags & BOUND_DELTA_RIGHT:
self.right += Int8.unpack(s)

if flags & BOUND_BOTTOM:
self.bottom = Int16LE.unpack(s)
elif flags & BOUND_DELTA_BOTTOM:
self.bottom += Int8.unpack(s)

def __str__(self):
return f'<Bounds {self.left}, {self.top}, {self.right - self.left}, {self.bottom - self.top}'
25 changes: 25 additions & 0 deletions pyrdp/parser/rdp/orders/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,25 @@
StreamBitmapFirst, StreamBitmapNext, GdiPlusFirst, GdiPlusNext, GdiPlusEnd, GdiPlusCacheFirst, \
GdiPlusCacheNext, GdiPlusCacheEnd, FrameMarker

from .common import Bounds
from pyrdp.enum import IntEnum


class BrushStyle(IntEnum):
SOLID = 0x00
NULL = 0x01
HATCHED = 0x02
PATTERN = 0x03


class HatchStyle(IntEnum):
HORIZONTAL = 0x00
VERTICAL = 0x01
FDIAGONAL = 0x02
BDIAGNOAL = 0x03
CROSS = 0x04
DIAGCROSS = 0x05


class GdiFrontend:
"""
Expand All @@ -25,6 +44,12 @@ class GdiFrontend:
"""
# REFACTOR: Move to core, this isn't really relevant to the parser.

def onBounds(self, b: Bounds):
"""
Called by the parser to configure the bounding rectangle.
"""
pass

def dstBlt(self, state):
pass

Expand Down
47 changes: 20 additions & 27 deletions pyrdp/parser/rdp/orders/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from pyrdp.pdu.rdp.capability import CapabilityType

from .frontend import GdiFrontend
from .common import Glyph, GlyphV2

from .secondary import CacheBitmapV1, CacheColorTable, CacheGlyph, CacheBitmapV2, CacheBrush, CacheBitmapV3
from .alternate import CreateOffscreenBitmap, SwitchSurface, CreateNineGridBitmap, \
Expand Down Expand Up @@ -47,7 +46,7 @@ def __init__(self, frontend: GdiFrontend):
:param GdiFrontend frontend: The frontend that will process GDI messages.
"""
self.orders: FastPathOrdersEvent = None

self.notify: GdiFrontend = frontend
self.ctx = Context()
self.glyphLevel: GlyphSupport = GlyphSupport.GLYPH_SUPPORT_NONE
Expand All @@ -63,13 +62,15 @@ def parse(self, orders: FastPathOrdersEvent):
"""
Entrypoint for parsing TS_FP_UPDATE_ORDERS.
"""
self.orders = orders

s = BytesIO(orders.payload)

numberOrders = Uint16LE.unpack(s)
for _ in range(numberOrders):
self._parse_order(s)
try:
for _ in range(numberOrders):
self._parse_order(s)
except:
LOG.warning('Failed to parse drawing order PDU: %s', orders)

return orders

Expand All @@ -88,10 +89,10 @@ def _parse_order(self, s: BytesIO):
def _parse_primary(self, s: BytesIO, flags: int):

orderType = self.ctx.update(s, flags)
self.notify.onBounds(self.ctx.bounds if self.ctx.bounded else None)

fp = _pri[orderType]
LOG.debug('[PRIMARY] %s', _repr(fp))
fp(self, s)
assert orderType >= 0 and orderType < len(_pri)
_pri[orderType](self, s)

def _parse_dstblt(self, s: BytesIO):
"""DSTBLT"""
Expand Down Expand Up @@ -119,7 +120,7 @@ def _parse_line_to(self, s: BytesIO):

def _parse_opaque_rect(self, s: BytesIO):
"""OPAQUE_RECT"""
self.notify.opaqueRec(self.ctx.opaqueRect.update(s))
self.notify.opaqueRect(self.ctx.opaqueRect.update(s))

def _parse_save_bitmap(self, s: BytesIO):
"""SAVE_BITMAP"""
Expand Down Expand Up @@ -184,16 +185,12 @@ def _parse_glyph_index(self, s: BytesIO):
# Secondary drawing orders.
# ----------------------------------------------------------------------
def _parse_secondary(self, s: BytesIO, flags: int):
orderLength = Uint16LE.unpack(s) # Unused?
Uint16LE.unpack(s) # orderLength (unused)
extraFlags = Uint16LE.unpack(s)
orderType = Uint8.unpack(s)
# nxt = orderLength + 7

assert orderType >= 0 and orderType < len(_sec)

fp = _sec[orderType]
LOG.debug('[SECONDARY] %s (len=%d)', _repr(fp), orderLength)
fp(self, s, orderType, extraFlags)
_sec[orderType](self, s, orderType, extraFlags)

def _parse_cache_bitmap_v1(self, s: BytesIO, orderType: int, flags: int):
"""CACHE_BITMAP_V1"""
Expand All @@ -208,10 +205,8 @@ def _parse_cache_glyph(self, s: BytesIO, orderType: int, flags: int):
if self.glyphLevel == GlyphSupport.GLYPH_SUPPORT_NONE:
LOG.warn("Received CACHE_GLYPH but the client reported it doesn't support it!")
# Ignore it.
elif self.glyphLevel == GlyphSupport.GLYPH_SUPPORT_ENCODE:
self.notify.cacheGlyph(CacheGlyph.parse(s, flags, GlyphV2))
else:
self.notify.cacheGlyph(CacheGlyph.parse(s, flags, Glyph))
self.notify.cacheGlyph(CacheGlyph.parse(s, flags, self.glyphLevel))

def _parse_cache_bitmap_v2(self, s: BytesIO, orderType: int, flags: int):
"""CACHE_BITMAP_V2"""
Expand All @@ -232,9 +227,7 @@ def _parse_altsec(self, s: BytesIO, flags: int):

assert orderType >= 0 and orderType < len(_alt)

fp = _alt[orderType]
LOG.debug('[ALTSEC] %s', _repr(fp))
fp(self, s)
_alt[orderType](self, s)

def _parse_create_offscreen_bitmap(self, s: BytesIO):
"""CREATE_OFFSCREEN_BITMAP"""
Expand All @@ -258,27 +251,27 @@ def _parse_stream_bitmap_next(self, s: BytesIO):

def _parse_gdiplus_first(self, s: BytesIO):
"""GDIPLUS_FIRST"""
self.notify.GdiPlusFirst(GdiPlusFirst.parse(s))
self.notify.gdiPlusFirst(GdiPlusFirst.parse(s))

def _parse_gdiplus_next(self, s: BytesIO):
"""GDIPLUS_NEXT"""
self.notify.GdiPlusNext(GdiPlusNext.parse(s))
self.notify.drawGdiPlusNext(GdiPlusNext.parse(s))

def _parse_gdiplus_end(self, s: BytesIO):
"""GDIPLUS_END"""
self.notify.GdiPlusEnd(GdiPlusEnd.parse(s))
self.notify.drawGdiPlusEnd(GdiPlusEnd.parse(s))

def _parse_gdiplus_cache_first(self, s: BytesIO):
"""GDIPLUS_CACHE_FIRST"""
self.notify.GdiPlusCacheFirst(GdiPlusCacheFirst.parse(s))
self.notify.drawGdiPlusCacheFirst(GdiPlusCacheFirst.parse(s))

def _parse_gdiplus_cache_next(self, s: BytesIO):
"""GDIPLUS_CACHE_NEXT"""
self.notify.GdiPlusCacheNext(GdiPlusCacheNext.parse(s))
self.notify.drawGdiPlusCacheNext(GdiPlusCacheNext.parse(s))

def _parse_gdiplus_cache_end(self, s: BytesIO):
"""GDIPLUS_CACHE_END"""
self.notify.GdiPlusCacheEnd(GdiPlusCacheEnd.parse(s))
self.notify.drawGdiPlusCacheEnd(GdiPlusCacheEnd.parse(s))

def _parse_window(self, s: BytesIO):
"""WINDOW"""
Expand Down
Loading

0 comments on commit 471d9f5

Please sign in to comment.