GoSecure / pyrdp

RDP monster-in-the-middle (mitm) and library for Python with the ability to watch connections live or after the fact
https://www.gosecure.net/blog/2020/10/20/announcing-pyrdp-1/
GNU General Public License v3.0
1.46k stars 243 forks source link

PyRDP's dependency problem : sniff() function of Scapy module. #318

Open yraiesets opened 3 years ago

yraiesets commented 3 years ago

PyRDP offers the possibility of reconstructing TLS packets from a wireshark capture in order to reproduce a replay of the RDP connection. To be able to achieve this, we used the Scapy module (https://github.com/GoSecure/pyrdp/pull/311). However, we have a stack overflow/out-of-memory error, depending on the python interpreter version used, even before we do the TCP / TLS reconstruction in a specific context (still unsure what exact conditions are required).

The error occurs via the sniff() function used in offline mode which allows you to read packets from a pcap and decode them into a seamlessy flow (https://scapy.readthedocs.io/en/latest/usage.html#sniffing).

We have the same error with both scapy 2.4.5 (official latest release) or the dev version (at the time of writing this issue : 2.4.5rc1.dev51). We tried scapy on python 3.9 who caused the stack overflow error and on python 3.8 who caused the out-of-memory error.

Context :

At first, we used a packet-by-packet approach, i.e we tried to find the particular packet that was causing scapy to explode. However, depending on the configuration used, the processes in progress, etc. We never fall into the same packet twice (or rarely), so we needed another approach.

So, analyzing our sample pcap a little more, we can see that we have huge frames that require TCP data reassembly. So we think the problem comes from there: Scapy will call a tcp reassembly method without stopping until the entire package has been reassembled, thus causing a stack overflow error or out of memory error (depending on your python interpreter version).

I.E :

ipdb> pkt.summary() 'TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS / TLS None:None > None:None / TLS / TLS Application Data'

    332             # Reassemble using all previous packets
--> 333             packet = tcp_reassemble(bytes(data), metadata)
    334         # Stack the result on top of the previous frames

session.py(1029)tcp_reassemble()
   1028                 if hasattr(pkt.payload, "tcp_reassemble"):
-> 1029                     if pkt.payload.tcp_reassemble(data[length:], metadata):
   1030                         return pkt

Here's where the recursive call will happen and we can see how much deep the recursion can go with the shown packet. We can also imagine the depth of the recursion if we have more than one frame like the previous one.

We tried to reproduce a pcap which could cause the same error, but without success. We could never get the frames as big as our sample.

Please see :

yraiesets commented 3 years ago

As a workaround to the previous mentioned issue, we can follow the steps outlined here : https://github.com/GoSecure/pyrdp#using-pyrdp-convert