Skip to content

Instantly share code, notes, and snippets.

@xshill
Last active December 27, 2023 02:01
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save xshill/a4a41b76bb980f766657a2828989c84a to your computer and use it in GitHub Desktop.
Save xshill/a4a41b76bb980f766657a2828989c84a to your computer and use it in GitHub Desktop.
Sample script that was used to create a replay file from a pcap using PyRDP and scapy
#!/usr/bin/python3
import logging
import os
from progressbar import progressbar
from scapy.all import *
from pyrdp.enum import ParserMode, PlayerMessageType
from pyrdp.layer import SegmentationLayer, TPKTLayer, X224Layer, MCSLayer, TLSSecurityLayer, SlowPathLayer, FastPathLayer, VirtualChannelLayer, ClipboardLayer
from pyrdp.mcs import MCSRouter
from pyrdp.parser import ClientInfoParser, BasicFastPathParser
from pyrdp.pdu import FormatDataResponsePDU
from pyrdp.recording import FileLayer, Recorder, RecordingFastPathObserver, RecordingSlowPathObserver
def bytesToIP(data: bytes):
return ".".join(str(b) for b in data)
def parseExportedPdu(packet: packet.Raw):
source = packet.load[12 : 16]
source = bytesToIP(source)
destination = packet.load[20 : 24]
destination = bytesToIP(destination)
data = packet.load[60 :]
return source, destination, data
class CustomRecorder(Recorder):
currentTimeStamp: float = None
def getCurrentTimeStamp(self) -> float:
return self.currentTimeStamp
def setTimeStamp(self, timeStamp: float):
self.currentTimeStamp = timeStamp
class RDP(MCSRouter):
def __init__(self, mode: ParserMode, recorder: Recorder):
self.recorder = recorder
self.currentClipboardData = None
self.segmentation = SegmentationLayer()
self.tpkt = TPKTLayer()
self.x224 = X224Layer()
self.mcs = MCSLayer()
self.security = TLSSecurityLayer()
self.io = SlowPathLayer()
self.fastPath = FastPathLayer(BasicFastPathParser(mode))
self.clipboardSecurity = TLSSecurityLayer()
self.virtualChannelLayer = VirtualChannelLayer()
self.clipboardLayer = ClipboardLayer()
self.segmentation.attachLayer(0, self.fastPath)
self.segmentation.attachLayer(3, self.tpkt)
self.security.securityHeaderExpected = True
self.security.createObserver(onClientInfoReceived = self.onClientInfoReceived, onLicensingDataReceived = self.onLicensingDataReceived)
self.io.addObserver(RecordingSlowPathObserver(recorder))
self.fastPath.addObserver(RecordingFastPathObserver(recorder, PlayerMessageType.FAST_PATH_OUTPUT if mode == ParserMode.CLIENT else PlayerMessageType.FAST_PATH_INPUT))
self.clipboardLayer.createObserver(onPDUReceived = self.onClipboard)
self.mcs.addObserver(self)
self.tpkt.setNext(self.x224)
self.x224.setNext(self.mcs)
self.security.setNext(self.io)
self.clipboardSecurity.setNext(self.virtualChannelLayer)
self.virtualChannelLayer.setNext(self.clipboardLayer)
MCSRouter.__init__(self, self.mcs)
def recv(self, data: bytes):
self.segmentation.recv(data)
def recvChannel(self, pdu):
if pdu.channelID == 1003:
# I/O channel
self.security.recv(pdu.payload)
elif pdu.channelID == 1004:
# Clipboard channel
self.clipboardSecurity.recv(pdu.payload)
def onSendDataRequest(self, pdu):
self.recvChannel(pdu)
def onSendDataIndication(self, pdu):
self.recvChannel(pdu)
def onClientInfoReceived(self, data):
pdu = ClientInfoParser().parse(data)
recorder.record(pdu, PlayerMessageType.CLIENT_INFO)
def onLicensingDataReceived(self, pdu):
self.security.securityHeaderExpected = False
def onClipboard(self, pdu):
if isinstance(pdu, FormatDataResponsePDU):
if pdu.requestedFormatData != self.currentClipboardData:
self.recorder.record(pdu, PlayerMessageType.CLIPBOARD_DATA)
self.currentClipboardData = pdu.requestedFormatData
logging.basicConfig(level = logging.CRITICAL)
logging.getLogger("scapy").setLevel(logging.ERROR)
CLIENT = "10.0.0.3"
SERVER = "10.0.0.2"
directory = os.path.dirname(os.path.realpath(__file__))
packets = rdpcap(directory + "capture.pcap")
replayFile = open("replay.pyrdp", "wb")
fileLayer = FileLayer(replayFile)
recorder = CustomRecorder([fileLayer])
client = RDP(ParserMode.CLIENT, recorder)
server = RDP(ParserMode.SERVER, recorder)
rdp = {
CLIENT: client,
SERVER: server
}
# The last packet of this pcap is incomplete
for packet in progressbar(packets[: -1]):
# The packets start with a Wireshark exported PDU structure
source, destination, data = parseExportedPdu(packet)
try:
recorder.setTimeStamp(packet.time)
rdp[destination].recv(data)
server.security.securityHeaderExpected = client.security.securityHeaderExpected
except NotImplementedError:
pass
recorder.record(None, PlayerMessageType.CONNECTION_CLOSE)
@xshill
Copy link
Author

xshill commented Jul 24, 2019

Sorry about the delay, I tried forwarding it to the team but I think they were more focused on other issues. It's currently the end of the term for me so school is taking a lot of my time. I would also like this feature to work so I'll be looking into it once I have more free time.

@xshill
Copy link
Author

xshill commented Jul 24, 2019

Also, thanks for the reminder!

@bobskee
Copy link

bobskee commented Jul 24, 2019

Alright, good to hear! Good luck with school. I hope to hear from the team (or you) soon :)

@xshill
Copy link
Author

xshill commented Aug 14, 2019

All right, now that school and DefCon is over, I can finally get to work on this!

@xshill
Copy link
Author

xshill commented Aug 14, 2019

@bobskee I just pushed a new branch called pyrdp-replay to the repo. Let me know if this works for the PCAPs you have. For now the script only expects one session per input PCAP.

Installation:
Follow the same instructions for installation as the master branch. You should reinstall for this branch because of the added dependencies and the new file in bin/.

Usage:

usage: pyrdp-replay.py [-h] input client server output

positional arguments:
  input       Path to PCAP file with exported PDUs
  client      Client IP
  server      Server IP
  output      Output file path

optional arguments:
  -h, --help  show this help message and exit

@bobskee
Copy link

bobskee commented Aug 15, 2019

That is awesome! It looks like there was a lot of interest in your demo at blackhat :)

I tested the new script and it succesfully extracted the files that were transferred in the session. However, even though the replay file has quite the same size as I expected (from a sample pyrdp mitm capture), the player cannot play it successfully. A part of the screen is visible and the log output (with keystrokes etc.) seems to match but the player isn't able to play the session. If you'd like I can provide you a sample somehow. Is there any other platform I can contact you on?

Thanks!

@xshill
Copy link
Author

xshill commented Aug 15, 2019

The player cannot play the session at all? Were there any exceptions when running the player or pyrdp-replay?

You can find me on keybase (username: xshill).

@bobskee
Copy link

bobskee commented Aug 15, 2019

It shows like a quarter (top left) of the original screen and the progressbar (timeline) is filled at about 80% which I cannot adjust. AFAIK there are no exceptions and both programs run fine.

@xshill
Copy link
Author

xshill commented Aug 15, 2019

Quick question: can you tell me what the color depth is for your session? It should be under ClientData->clientCoreData in Wireshark. I assume this session wasn't captured by PyRDP?

@xshill
Copy link
Author

xshill commented Aug 15, 2019

Oh and yes, a sample would be nice :)

@bobskee
Copy link

bobskee commented Aug 15, 2019

Color depth is '8 bits-per-pixel (bpp) (0xca01)' according to Wireshark. The session was actually captured by PyRDP but at this moment I am not certain anymore that the capture only contains a single session. I will check that tomorrow and I will also provide you with a sample then (have to strip some data first or do a new capture if I am uploading it here in public since I don't have a Twitter account to contact you privately)

@xshill
Copy link
Author

xshill commented Aug 15, 2019

I think you can just use your github if you want to create a keybase account.

@robeving
Copy link

Hi @xshill. I've been playing with your branch - it looks really cool.

I can use pyrdp-replay to get an output file (providing I add some exception handling in recv line 81). The output file is around 100k which seems about right. The pcap contains one session with everything from login to terminate, I've exported the decrypted PDUs. PCAP looks OK.

The player can open the file and get the login and keystrokes out but not the actual screen capture.

I feel really close to this actually working well. I'm happy to try and debug this if you have some pointers to where I can look. Alternatively I can give you the PCAP and outfile.

@xshill
Copy link
Author

xshill commented Sep 26, 2019

@robeving I assume this is you on Twitter?

The issue is with how PyRDP handles graphics vs. how RDP normally handles it. Basically there's two ways to handle output: either the server sends drawing orders that the client then renders locally or it sends bitmaps of the screen. Normal RDP usually sends GDI drawing orders over the network that you have to render locally. To avoid implementing a bunch of drawing orders, we coded PyRDP so it only accepts bitmaps. This works fine for situations when PyRDP is running but, obviously, since we have no support for drawing orders, it means we can't parse output for "normal" RDP connections.

So, basically, we need to implement support for the drawing orders API before this becomes usable with generic RDP pcaps. Judging from this spec, I wouldn't expect that to happen soon because there's a bunch of drawing order types to implement for it to work properly.

It was bit of an oversight on our part not to mention that on the blog post - guess we were too used to only handling bitmaps. I'll add this to the README in the pyrdp-replay branch and create an issue to track this.

@robeving
Copy link

Thanks this is really helpful just knowing there is a lot more work to do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment