TCP the long way home
by kpcyrd, medium read,
In addition to the last blogpost, I’ve extended rshijack with a ghetto tcp stack.
So… why? Before this patch, we’ve been able to read the SEQ and ACK fields so we can inject arbitrary packets. This works nicely if you only need to execute one command, but there are two drawbacks:
- you need to sniff the reply manually
- you’re only going to get a single chunk of data
Since we do not ACK any packets, the kernel assumes the packet was lost in transit and is going to spam it over and over, until eventually giving up and RSTing the connection.
We can certainly do better. To keep complexity and CPU load low, the basic idea is having two threads, one for stdin->network and one for network->stdout. Both of them are going to block until more data is available, preventing rough idle. You could theoretically use a select(2)-like construct to achieve this in one thread, but this would greatly increase complexity.
The naive first try was copying the sniffed state into both threads, then sniff for more packets. This works to some extend. We’re able to receive data, but we still only get a single chunk each time the sender tries again.
So, obviously we need to ack this data as well. After some short hackery we’re also sending ACK packets, but we’re hitting our first wall here: The ACK packets aren’t accepted by the sender. Since we just copy the state once and then both threads operate on their own copy, the SEQ we use to send our ACKs gets desynced as soon as we send any data. To get our ACK packets accepted, we need to set the most recent SEQ of our sending threads.
This was solved by changing the seq and ack fields of the connection from u32
to Arc<Mutex<u32>>
. Basically, our connection state doesn’t hold those counters directly anymore, but holds a pointer to those counters that are protected by a mutex. This allows us to safely change this number from one thread, then read it back in the other thread.
With this change in place, we’re now successfully ACKing the data we receive and we can receive more than one chunk of data. But there was still one weird problem left: The data we receive is printed twice. But according to tcpdump the data is only sent once by our remote peer, but we’re ACKing it twice as well. After some initial confusion this makes sense: since we’re in a man in the middle position, we can see the packet twice, once when we receive it and once when we forward it. At this point, we didn’t verify the SEQ ourself, so this isn’t detected as duplicate. The solution for this was trivial, we only accept packets if SEQ + len is greater than our saved ack.
With these changes in place rshijack behaves very simial to netcat. Except that it operates on connections we didn’t originally own.