Smuggler and Cove: A PoC for data exfiltration using Scapy.
I’ve long been fascinated with the concept of covert channels and data smuggling. A couple of years ago I came up with the idea that I could smuggle data out of a network to another machine on the Internet without directly talking to that machine using the sequence numbers in TCP packets, and bouncing packets off of third-party servers to a forged source IP system. I talked to some friends about it, and even drew up a diagram of how it would work and presented it to a friend of mine with a solid networking background, who said the idea was plausible. (Thanks Kyle!)
Fast forward a year or so while taking my GCIH course and I learned this concept was not new. (Of course not, I’m not that original it turns out.) Covert_TCP is a thing. Who knew? Apparently not me.
Anyways, in an effort to understand covert channels from a hands-on approach and having lots of free time over the holiday, I decided to see if I could send some covert data. The goal was to send messages to a remote machine by bouncing packets off of a third party machine so that all incoming packets on the receiving end never see the originating system. This would be an interesting way to C2 a machine if you were able to setup the receiving system to sniff data from legitimate trusted servers, or even exfil data from a machine inside a company that was watching your packets. Note: This method is easily defeated from data exfiltration if your network equipment is setup to drop all outgoing packets that contain a source IP that is not of your network.
Goal: Create a client that can smuggle data to a third party system on the Internet, without directly talking to it and hide information in the packets. This is done by forging the source IP of each packet, and hiding data in the ISN of each SYN packet. (The Smuggler)
Goal: Create a listener that can receive data from different servers around the world, and reorder them to form messages/commands. (The Cove)
Advanced Goal: Build 2-way communication through covert channels, relying on non-existent servers within trusted domains. (More on this later)
These goals require a few things:
- A silent listening “Cove” server. This server listens for SYN/ACKs coming in from the wild, and knows these are part of the message being sent from the Smuggler. The Cove server will capture these packets and reorganize them into a message or, more nefariously, a command? Maybe? Arr.
- A new kind of “protocol”. We only have 4 bytes of data to work with using the ISN of a SYN packet, which means we have to create an entirely new language so the client and server can communicate in this limited space. Initial thoughts are to create a new kind of “Smuggle SYN” which is the initial sequence sent to the Cove server, which then tells it to wake up and pay attention — packets are incoming! The beauty of listening for these packets is that there is no open socket needed. The server can quietly sniff for ACKs coming from the wild, send back their RSTs, and form the data without being “alive”. Example — First ISN could be 4294967295, which forces a SYN/ACK (and therefore a RST) of 0. (This wraps the sequence number back to 0). If the server sees an ACK of 0, it knows it’s time to wake up and pay attention, a request is coming in.
Whatever method this “Smuggle SYN” is sent, the idea is hopefully clear. Knowing we have 4294967294 other numbers to work with, we can create an entire code set to represent whatever we like from the client to the server, and an entirely different set from the server to the client if we desire. We could even build padded systems where predetermined codes represent certain functions, much like sending a 404 to a browser or an error 49 (invalid credentials) from an LDAP server.
The Start
So here we are. The concept is simple enough, but I needed to build a PoC for myself to see if I could do it. I first built a script in Perl to generate raw packets, which worked okay, but wasn’t robust like I was hoping. I also built some basic packets in C, but again it was like building my own roads just to drive on them.
After some research, I learned about Scapy, a wonderful packet crafting tool for Python that makes crafting packets really easy. Scapy allows you to build packets like legos or in a way that feels logical.
Take your layer 2 frame; you expect an Ethernet layer, an IP layer, a TCP layer, and your payload for a standard TCP communication. In Scapy, you can build a packet using the default values by simply doing:
>lyr_two_packet = Ether()/IP()/TCP()/load
>sendp(lyr_two_packet) ## sendp() is for layer 2!
>lyr_three_packet = IP()/TCP()/load
>send(lyr_three_packet) ## Notice this is send(), not sendp()!
Scapy will broadcast for Ethernet if needed in a send(), if you leave off the Ether layer.
The default settings for these functions will not work outright, but the structure will be there. Changing the attributes of a packet is as easy as:
>lyr_two_packet[IP].src=”192.168.1.2" or
>lyr_two_packet[TCP].seq=11111111 or
>mytcp=TCP(sport=20, dport=80, seq=11111111) and then stacking that layer on the others as above. Note, the “seq” value is where the magic happens.
Once you’ve built your packet, you can view it using show() or show2(). I recommend show2() because it will show you the assembled packet, checksum and all. Protip: if you assemble your packet and then change an attribute, you may need to force scapy to recalculate your checksum. You can do this by deleting the checksum and then calling the packet again, forcing it to rebuild.
>del lyr_two_packet[IP].chksum
>del lyr_two_packet[TCP].chksum
Let’s Build
The first thing we need is to come up with a way to jam as much communication into a tiny space as possible. Since we only have 10 characters to work with, (really only 9, you’ll see why) in the ISN of a SYN packet, we need to come up with a simple method to prototype. I didn’t want to spend too much time on this, so I decided to use a 6/3 divide in a 9 digit space, the first 6 digits can be used for the ordering number, and the last 3 for the character itself. In other words, we can transmit up to 999,999 individual characters before having to start over with a new batch of data. The last 3 digits are used to translate the character we want to send by passing the character to python’s ord() function. ord() simply translate a character into a number, and chr() translates a number into a character (with 256 being the largest allowable value.)
im_now_a_number = ord(im_a_character)
im_now_a_character = chr(i_was_a_number)
What we end up with is something like this:
9115 or 000009115. Break this down into 6/3 groups, so we get 000009 115. The first six numbers are the order, and the last 3 is the character. In this case, the letter ‘s’ is the 9th character sent in this transmission.
The reason we do not use the leftmost space is because it caps at 4. If you recall, the highest allowed ISN is 4294967295, so we cannot tick past 4 in the leftmost position. We can get crafty and use that as a “signed” bit if we like, or even a 4 position switch of some type!
Next, we need a list of servers that we want to bounce off of. In this case, I’ll bounce off web servers since they’re easy to find and we know they will SYN/ACK back to us.
When all put together, a transmission looks like this:
Great! We’ve just smuggled some data in the ISN of SYN packets going out to the world. Each server caught our SYN, and SYN/ACK’d with a +1 incremented sequence number.
Note the ACKs come trickling in out of order as expected and is why we need an ordering system. What you won’t see in this example (because I grep’d for “ack” only) are the re-transmissions coming from those servers. Because of the bounce, the Cove machines kernel is not expecting a SYN/ACK and sends a RST or does not reply — depending on the kernel. You can expect multiple re-transmits from each machine until they give up.
Now we need to build a better way to catch and organize the data.
Back to Scapy.
Building a Sniffer with Scapy
Scapy has a nice function called simply enough, sniff(). It does what you’d expect it to do. One nice feature about the function is the “store” argument which, when set to 0, will not hold onto the packets as they come in, and instead stream them to whatever callback (the prn argument) value you pass it.
Example: sniff(iface=”eth0", prn=p_catch, filter=”tcp”, store=0)
This will send every TCP packet to a function called p_catch, where you can then disassemble it, or do whatever you want with it. This could be really handy for writing your own IDS! In my case, I’m catching the packets and breaking off their ISN’s so I can capture the secret message. (Notice above, you can change the filter to whatever type of packet you want to watch for — tcp, icmp, etc. Removing the filter altogether catches everything.)
So now we send all TCP packets to p_catch. What does p_catch do? It first checks if a packet has a TCP layer — if(p.haslayer(TCP)):
Next, it pulls out the ack value of the packet. Super easy!
>ack_n = p.getlayer(TCP).ack
Yep! It’s really that simple. Scapy is awesome!
Now we deduct one from the value and split off the character and the order. Notice by putting the character value on the far right, we can easily subtract one from it, and then pop it off, pass it into chr() and be done.
Here’s what it all looks like when done:
Now we simply reorder the message, drop the RTX’s, and we’re done. A message successfully sent to a third party system without knowing the origin!
The transmission rate falls into a 300 to 600 baud modem I’d say. Not fast, but certainly stealthy.
There are all kinds of things you can do with this already if you think about it, but what’s more interesting would be two-way communication. I’ll briefly discuss some ideas on that subject now, but would need more research and a better lab environment to build it, which I currently do not have access to.
Theorizing two-way communication
Let’s pretend you own a house, and you trust packages that come in from Amazon. Let’s say you have a door bouncer that also filters/blocks any packages that come in from untrusted places in the world. We know this bouncer as a firewall. Let’s say the bouncer normally throws away any packages from untrusted sources, and let’s say the bouncer ONLY accepts packages from Amazon if they know you sent a package to Amazon first, and are expecting a return package.
What if you had a person in the house decide at a predetermined time, say 5 PM, to send out a package to Amazon’s campus, but to a building that does not exist on their campus. The bouncer checks the outgoing package, see’s “Toilet Department, Amazon”, affirms it, and sends it on it’s way to wait for Amazon to send back a package. Suddenly a package arrives that says it’s from the Toilet Department at Amazon, and the bouncer lets it in.
In this way, a package with a forged source has arrived at the destination, and was allowed into the house.
Currently a lot of C2 detection is based on reoccurring timing (via a reverse shell for example), reaching out to some bad-guys domain and gathering up a command to execute on the exploited machine. Bad guy domains can be seen and blocked. What if a reoccurring timing was going out to a trusted IP in a trusted companies range? “Hmm, we see a random ping echo-request/echo-reply coming in every 5 minutes to Google.com’s IP address range. What’s that about?” The hypothesis in this case is in the fact that no such server exists, but the domain itself is trusted. The attacker capitalizes on this and forges a packet ready to come inside with the proper expected checksums. TCP prevents attacks like this by sending a SYN/ACK with a randomized sequence number which has to be repeated back a third time by the sender for two-way verification. ICMP does not. ICMP relies only on the payload equaling the echo-request via checksum — I think. This is where my knowledge begins to fall short, so if anyone knows the answer, please let me know!
My understanding is that an ICMP echo-request is sent with a payload, and the echo-reply must contain the same payload, but with a 0 flag, rather than an 8. But is this payload checked via a proper checksum, or by the payload itself? Is this done on the kernel level, or by a deep-packet inspection firewall? If the firewall allows the payload via checksum, then is it possible to overwrite sections of a payload with a message and maintain the checksum?
Example: \x00\x00\x00\x00 becomes \xde\xad\xbe\xef ?
If the above is true, then 2-way communication is now established without the sender ever being known.
Let me know what you think if you have the answer to this, or if I’m way off in some way. Comments appreciated!
Happy hacking!