Steganographic Packets
2019-01-30I had an idea a while ago about a method of sending data across a network link without sending a single byte of data in the packets themselves.
Here's one of the packets:
00000000: cc 33 bb c5 11 6e 70 85 c2 3a c4 25 08 00 45 00 .3...np..:.%..E.
00000010: 00 1c 00 01 00 00 40 01 f6 0c c0 a8 01 85 c0 a8 ......@.........
00000020: 01 fe 08 00 f7 ff 00 00 00 00 ..........
Not a single byte of payload data in there! We could break out every single bit (pun intended) to prove it, but by the end of this post, hopefully, I won't need to!
So how is it possible to send data between two hosts without actually sending a single byte of it in the packets themselves?
Here's the secret... we encode our data in the time between packets!
The Encoding Scheme
Let's say we wanted to send the data "Hello World!", firstly we'd need to get each characters numeric value to see what we're dealing with:
H e l l o W o r l d !
72 101 108 108 111 32 87 111 114 108 100 33
Then we need to decide how we're going to represent this as time. Maybe we could wait 72 milliseconds to send a letter "H", 101 milliseconds to send an "e" and so on. Theoretically, that would work, but network jitter is going to kill us if we only leave 1 millisecond between different values. Perhaps we could give ourselves more time; 720 milliseconds for an "H", 1010 milliseconds for an "e", etc. That sounds like reasonable times for a LAN, we should be able to recover our data even with a bit of jitter, it would be pretty slow though. Say we wanted to send a "ÿ" (from the Latin 1 character set), we'd have to wait 2.55 seconds... ouch!
Obviously we're not going for throughput here, but we can definitely do better than a maximum time of 2.55 seconds for a single byte of data!
Instead of sending full bytes (256 different values), maybe we could send half bytes (4 bits, nibbles) instead. This would reduce the number of combinations we need to represent by a factor of 16!
Here's our data broken down into nibbles:
H e l l o W o r l d !
0100 1000 0110 0101 0110 1100 0110 1100 0110 1111 0010 0000 0101 0111 0110 1111 0111 0010 0110 1100 0110 0100 0010 0001
Encoding our data this way does mean sending 2 packets per byte of data though,
so we'd end up having to send N * 2 + 1
packets (where N
is the number of
bytes we want to send), but I think that's a reasonable compromise in this
instance. Depending on the specific application, you might want to alter this.
So here it is, this is our encoding scheme, the nibble we want to send on the left and the interval we should use to represent that data on the right:
Data | Interval |
---|---|
0000 | 10 |
0001 | 20 |
0010 | 30 |
0011 | 40 |
0100 | 50 |
0101 | 60 |
0110 | 70 |
0111 | 80 |
1000 | 90 |
1001 | 100 |
1010 | 110 |
1011 | 120 |
1100 | 130 |
1101 | 140 |
1110 | 150 |
1111 | 160 |
Simple enough. To send a "ÿ" using this encoding scheme would only take us 320 milliseconds (160 x 2); a bit better than 2.55 seconds.
Sending the Packets
Using the Python library Scapy, we can write a quick script to encode data using the above scheme; we'll use ICMP packets, but it would work with anything. You can find my quick effort on GitHub.
So, after a bit of tinkering in Python and monitoring our network card for ICMP
packets (tcpdump 'icmp[icmptype] = 8'
) we get something looking like this on
the wire:
10:58:36.936514 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:36.988860 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.081287 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.153525 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.215639 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.288139 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.421033 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.493561 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.625966 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.698109 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.860629 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.892763 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.904821 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:37.967002 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.049609 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.122016 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.284947 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.368031 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.400642 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.473075 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.605526 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.678002 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.730348 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.763012 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
10:58:38.785684 IP dans-pc.home > BThomehub.home: ICMP echo request, id 0, seq 0, length 8
Now we need to use this information (specifically the times the packets arrived) to recover our encoded data. In the table below we go through this process manually, but we can also write a program to do this for us.
To recover our data, we do the following:
- Calculate the interval between packets.
- Find the closest value in our encoding scheme.
- Store the looked up value until we have a full 8 bits (a byte, two nibbles).
- Convert our 8 bits to decimal.
- Look up the value to recover the original byte value.
Interval | Closest Encoding | Looked Up Value | Full Byte | Decimal Value | ASCII |
---|---|---|---|---|---|
52.3 | 50 | 0100 | |||
92.4 | 90 | 1000 | 01001000 | 72 | H |
72.2 | 70 | 0110 | |||
62.1 | 60 | 0101 | 01100101 | 101 | e |
72.5 | 70 | 0110 | |||
132.9 | 130 | 1100 | 01101100 | 108 | l |
72.5 | 70 | 0110 | |||
132.4 | 130 | 1100 | 01101100 | 108 | l |
72.1 | 70 | 0110 | |||
162.5 | 160 | 1111 | 01101111 | 111 | o |
32.1 | 30 | 0010 | |||
12.1 | 10 | 0000 | 00100000 | 32 | |
62.2 | 60 | 0101 | |||
82.6 | 80 | 0111 | 01010111 | 87 | W |
72.4 | 70 | 0110 | |||
162.9 | 160 | 1111 | 01101111 | 111 | o |
83.1 | 80 | 0111 | |||
32.6 | 30 | 0010 | 01110010 | 114 | r |
72.4 | 70 | 0110 | |||
132.5 | 130 | 1100 | 01101100 | 108 | l |
72.5 | 70 | 0110 | |||
52.3 | 50 | 0100 | 01100100 | 100 | d |
32.7 | 30 | 0010 | |||
22.7 | 20 | 0001 | 00100001 | 33 | ! |
There's our recovered message, Hello World!
!
Mathematically, with this encoding scheme, if we were sending every byte once (0 - 255) it would take 43520 milliseconds (43.52 seconds) to send. A staggering 0.005744 KBps or 5.882 Bps (assuming a perfect distribution)!
We can do Better!
Now, if I really wanted to use this to exfiltrate data from a machine, I'd change a few things ;)
Firstly, I'd perform frequency analysis on the data that I want to exfiltrate, so that I could re-order the encoding scheme to assign the most common bytes to the smallest interval (to increase transmission speed).
Secondly, I'd put some payload data in my packets, maybe just random data (or maybe decoy data), this would make it look like the payload was in the packet to throw off anyone looking.
Thirdly, I'd change our encoding scheme to use prime numbers as the intervals and multiply them by a random number for each packet we send. On the receiving side, we would iterate through our encoding scheme taking the modulus of the calculated interval and determine which interval in our encoding scheme is closest in order to recover the transmitted value. This would let us disguise even the intervals of the packets, as they would be different each time.
Lastly, and you'd have to be really serious to go this far, but lastly, I wouldn't send any packets at all... I would hook in to the network stack of the machine and fractionally delay existing outbound packets to correspond to our encoding scheme. This would require you to have visibility of all outbound traffic though, so may not be as easy to recover the data, but if you did...!