-
-
Save xshill/a4a41b76bb980f766657a2828989c84a to your computer and use it in GitHub Desktop.
#!/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) |
Hey @xshill, I do not want to appear impatient but have you got any updates so far?
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.
Also, thanks for the reminder!
Alright, good to hear! Good luck with school. I hope to hear from the team (or you) soon :)
All right, now that school and DefCon is over, I can finally get to work on this!
@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
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!
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).
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.
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?
Oh and yes, a sample would be nice :)
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)
I think you can just use your github if you want to create a keybase account.
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.
@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.
Thanks this is really helpful just knowing there is a lot more work to do.
Awesome! I'd be happy to provide you with a sample if you need one. When using the older commits (and deleting the progressbar) it stumbles upon this issue:
When using the most recent version of pyrdp (and changing PlayerMessageType to PlayerPDUType in pcap-to-replay.py) it breaks because of the MCSRouter rewrite.
I used Wireshark's function 'Export to PDU's' with the 'OSI layer 7' after I decrypted the TLS session to be used as input for the script (tried both pcap and pcap-ng file formats). The client and server IP-addresses in the pcap-to-replay.py script were changed accordingly.
Thanks again!