TUTORIAL: Interception

As promised, here’s the “downlow” on address interception. As a reminder, the chaperone is already configured to support this. Here’s a quick overview of the architecture.

Slide1

In this diagram, a node with address 20161.X.Y.Z. (hereafter X.Y.Z) sends a message to 20161.A.B.C (hereafter A.B.C). The Interceptor has already succeeded at the challenge and arranged to intercept all messages to A.B.C (either destined for, or coming from). Accordingly the message to A.B.C is NOT sent to the node (or nodes) with that address. Instead, it is sent over the special pipe to the interceptor.

TIP #1: the interceptor does NOT have an address. The connection is a special connection with the Chaperone not associated with a Playground address.

In the diagram above, the Interceptor receives the C2C packet from X.Y.Z, modify its contents, and send it back to the Chaperone. The Chaperone forwards it on to the target.

But the interceptor does not have to forward it. It can do nothing (effectively dropping it), send a completely replaced packet, or send a dozen custom packets. It can send messages to A.B.C, or it can send them to any address it wishes. Unlike the normal playground communication that is tied to a specific address and connected to a specific address, the communication between the Chaperone and the Interceptor is generic. The Interceptor sends C2C messages to the Chaperone, and the Chaperone routes them based on the src and dst data within them.

Here’s another example:

Slide2

In this example, The interceptor modifies the C2C packet so that instead of being destined for A.B.C, it is destined for J.K.L. When the packet is sent to the Chaperone, the Chaperone will route it appropriately.

TIP #2: In the example above, the Interceptor can get messages to J.K.L, but will not get the response. This is because it is only intercepting messages destined for A.B.C or sent by A.B.C. The message from X.Y.Z was destined for A.B.C, so the Interceptor can catch it and it can forward the message to J.K.L. But when J.K.L responds to X.Y.Z, the message is not intercepted.

One way around the above problem is to have the Interceptor also have a normal playground connection with a normal playground address. This allows messages to be sent from the Interceptor to the redirected address and then also to receive the responses. The problem is, the redirected node will see that the messages are from M.N.P and not X.Y.Z

Slide3

However, this may be a minor issue if you (the bad guy) already control J.K.L. In this case, the responses from J.K.L to M.N.P can be rewritten to X.Y.Z as if they came from A.B.C as shown below:

Slide4

TIP #3: Messages sent from the Interceptor to the Chaperone over the interception channel are not, themselves, intercepted. BUT, messages sent by the Interceptor over a normal channel with a normal playground address will be!

Now let’s go look at some code.

ClientInterceptor

Although you are free to write your own Interception client, one is already provided for you in playground/network/client/ClientInterceptor.py. The primary classes you will use are ClientInterceptor and InterceptorFactory.

To get things started, you will create an instance of InterceptorFactory. The constructor takes three parameters: an interception address, a protocol for handling messages destined for the intercepted address, and a protocol for handling messages from the intercepted address. You can use the same protocol for both of the latter two parameters, but both parameters are protocols, not factories.

Once the InterceptorFactory is constructed, it is connected to the Chaerpone using its own “connectToChaperone” method.

Note, you will not connect to the Chaperone using the normal “connectToChaperone” of ClientBase.

Here’s some example code:

interceptProtocol = # my interception protocol...
intercept = InterceptorFactory(interceptAddr, interceptProtocol, interceptProtocol)
intercept.connectToChaperone(chaperoneAddr, 9090)

Now, unlike the “connectToChaperone” in the ClientBase class, this one does not start the twisted reactor. So, you can either start the reactor directly (reactor.run()) or you can use ClientBase’s connectToChaperone if your setup requires a normal chaperone connection.

 

How to Use the Interceptor

When your interceptor factory connects to the chaperone, it will start the process for doing the challenge-response with the proof of work. The proof of work is done in a thread, so it won’t halt your system. When the connection is made and packets start getting routed, they’ll be sent to your protocols.

Any packets routed to your protocols are effectively blocked from Playground. If the interceptor doesn’t write them back over the channel, they will never reach their destination. Suppose, for example, that you receive a packet that you don’t want to modify, drop, or replace. It needs to be written back to the chaperone using the interceptor protocol’s transport.

The InterceptorFactory, as shown in the previous section, is constructed with two protocols. The first handles all packets destined for the intercepted address, and the second handles all packets sent by the intercepted address. They can, and perhaps often will, be the same protocol. These protocols will *only* receive C2C packets. If you want PTCL, PSST, or application information, you will have to extract that yourself.

When you receive the C2C packet, you can easily determine if it is from the intercepted address or to the intercepted address by checking the source and destination information in the packet header.

 

Putting it All Together: The Redirector Example

The code already provides a redirector example that follows the architecture shown:

Slide2

The code is found in apps.samples.attackers.RedirectInterceptor

Let’s look at this in detail. Here’s the main code:

clientBase = ClientBase(PlaygroundAddress.FromString(playgroundAddr))
interceptProtocol = RedirectInterceptor(None, interceptAddr, rerouteAddr, clientBase)
intercept = InterceptorFactory(interceptAddr, interceptProtocol, interceptProtocol)
clientBase.runWhenConnected(lambda: intercept.connectToChaperone(chaperoneAddr, 9090))
clientBase.connectToChaperone(chaperoneAddr, 9090)

The first line instantiates a ClientBase. We’ll use this for our normal connection to the Chaperone with the address in “playgroundAddr”.

The next line creates an “interceptProtocol”. Notice that we are not creating it from a factory. We’ll look at its code in a minute, but for now, just accept that it will be handling our intercepted communication.

After that, we build the InterceptorFactory. This is the code that comes out of playground.network.client.ClientInterceptor. It takes an interception address and the two protocols we’ve discussed (the first for handling interception messages to the target address, and the second for handling messages from the intercepted address). Notice that we are using the same protocol for both.

Finally we connect our ClientBase object to the Chaperone. In the “runWhenConnected” instruction, we indicate that after connecting we will have the interceptor connect to the Chaperone. Remember, this is completely separate from the normal Chaperone connection. The only reason we wait until the ClientBase makes its normal connection is so we can piggy back of its twisted reactor. If we were running without this, we would need to call “reactor.run()” separately.

The interceptor’s connectToChaperone will start the challenge response. Once it has proved its work, it will begin receiving packets that are passed up to the interception handling protocol (RedirectInterceptor in this case). Let’s look at that code next:

def __init__(self, factory, addr, rerouteAddr, clientBase):
  SimpleMessageHandlingProtocol.__init__(self, factory, addr)
  self.clientBase = clientBase
  self.rerouteAddr = PlaygroundAddress.FromString(rerouteAddr)
  self.connections = {}
  self.registerMessageHandler(ClientToClientMessage, self.handleC2CMessage)

As with all protocols, “factory” is the first argument. You’ll remember that in the main code, we passed “None” for this argument because this protocol isn’t being built from a factory. We also pass in two playground addresses and a ClientBase instance. The first address is the interception address, the second address is the new address we’ll re-route to, and the clientBase is for opening connections to the redirected address. Note that the super-constructor for SimpleMesssageHandlingProtocol will save “addr” into “self._addr” (one underscore so that the subclass can see it).

You’ll also notice the “connections” dictionary. Recall that we’re intercepting all incoming traffic going to an address. We might receive data from many sources bound for the intercepted destination and we’ll need to track each connection separately. This dictionary is for holding these connections.

Finally, we register to receive a single type of message: C2C messages.

Now let’s look at the C2C handler itself:

def handleC2CMessage(self, protocol, msg):
  msgObj = msg.data()
  if msgObj.srcAddress == self._addr:
    # this is from the src. Just drop it
    pass
  elif msgObj.dstAddress == self._addr:
    # this is to the dst. Redirect.
    connKey = (msgObj.srcAddress, msgObj.srcPort, msgObj.dstPort)
    if not self.connections.has_key(connKey):
      connSrcPort, self.connections[connKey] = self.clientBase.connect(RerouteFactory(),
        self.rerouteAddr,
        msgObj.dstPort)
      self.connections[connKey].respHandler = lambda buf: self.rerouteResponse(srcAddress=msgObj.dstAddress, 
        srcPort=msgObj.dstPort, 
        dstAddress=msgObj.srcAddress, 
        dstPort=msgObj.srcPort, 
        buf=buf)
    self.connections[connKey].transport.write(msgObj.clientPacket)

Right at the top, we figure out if we’re receiving data from the intercepted address or sent to the intercepted address. We check the “srcAddress” and “dstAddress” fields of the C2C message. Recall that a C2C message has 5 mandated fields:

  • srcAddress
  • srcPort
  • dstAddress
  • dstPort
  • clientPacket

Hopefully it goes without saying what each of the first four fields represent. The last field, “clientPacket” encapsulates the data from the higher layer (e.g. PTCL). So, we start by seeing if the interception address (self._addr) is in the srcAddress. This would indicate that the packet is FROM the intercepted address. We don’t care, so we do nothing which effectively drops the packet (all outbound communication from the intercepted address is blocked).

But, if that address is in the dstAddress field, we know that this packet is destined for the server and we decide to handle it. We start by establishing a “connection key”. We need to recognize different incoming connections, so we identify them by the triple (srcAddress, srcPort, dstPort). If we haven’t seen this triple before, we create a new outbound connection to the rerouted address (but same port) using clientbase and save the outbound protocol into the dictionary. We can now tie this unique incoming flow to this unique outgoing flow.

We use a different protocol for handling this rerouted connection. Let’s look at this RerouteFactory before finishing up with the interception C2C handler. Remember, the RerouteFactory is for the protocol that is connecting to the rerouted address.

class RerouteProtocol(Protocol):
  def __init__(self, factory, addr):
    Protocol.__init__(self, factory, addr)
    self.respHandler = None
 
  def dataReceived(self, buf):
    if self.respHandler: self.respHandler(buf)

class RerouteFactory(ClientApplicationClient):
 Protocol = RerouteProtocol

The RerouteFactory builds a RerouteProtocol that is incredibly simplistic. Whatever data comes in, it simply passes to a “respHandler”. Now back to the interception C2C handler:

self.connections[connKey].respHandler = lambda buf: self.rerouteResponse(srcAddress=msgObj.dstAddress, 
        srcPort=msgObj.dstPort, 
        dstAddress=msgObj.srcAddress, 
        dstPort=msgObj.srcPort, 
        buf=buf)

Look at this! We set a “respHandler”. The function takes a buffer and calls “rerouteResponse” with a number of parameters pre-configured. Remember, the msgObj here is from the intercepted packet, not the response that is being handled. So by setting srcAddress to msgObj.dstAddress, we’re basically saying that we’re going to send a response to the Playground node that sent the original packet to the intercepted address.

Let’s pretend that 20161.1.2.3 is communicating to the echo server on 20161.10.20.30 port 101 and that we’re intercepting all traffic to 10.20.30. We want to redirect the traffic to our fake server on 100.200.300. The message comes into the interceptor from 1.2.3 with the following header parameters:

  • SrcAddress: 20161.1.2.3
  • SrcPort: <some outgoing port>
  • DstAddress: 20161.10.20.30
  • DstPort: 101

Let’s pretend that we’ve never seen this connection before, so we open a new connection to our rerouted address 100.200.300 and store it in the dictionary. We also need to configure a respHandler for this connection. The lambda in the code above sets our response srcAddress to the current dstAddress (20161.10.20.30), our srcPort to the current dstPort (101) and so forth.

With our new connection, we can now send data out to 100.200.300 by using that protocol’s .transport.  Note that this is a RAW connection (over C2C). So, we extract the contents of the incoming intercepted C2C packet (msgObj.clientPacket) and send that through the transport.write method (NOT transport.writeMessage because it is already serialized).

self.connections[connKey].transport.write(msgObj.clientPacket)

Notice that this transport is NOT the transport back to the chaperone for the interception channel. It is a normal connection that we opened in the normal fashion. When data comes back over the channel, it calls back to the rerouteResponse as described above. The code for that function is:

def rerouteResponse(self, srcAddress, srcPort, dstAddress, dstPort, buf):
mb = MessageData.GetMessageBuilder(ClientToClientMessage)
mb[“srcAddress”].setData(srcAddress)
mb[“srcPort”].setData(srcPort)
mb[“dstAddress”].setData(dstAddress)
mb[“dstPort”].setData(dstPort)
mb[“clientPacket”].setData(buf)
self.transport.writeMessage(mb)

In this function, we create our own C2C message. We set the srAddress, srcPort, dstAddress, dstPort, and the clientPacket. Then we call “self.transport.writeMessage”. This transport is the one that writes back to the chaperone over the interception channel, which is why we had to custom create our packet.

We have rerouted traffic to a new destination without the source being aware of it.

 

Leave a Reply

Your email address will not be published. Required fields are marked *