rats and rat accessories
Stealing from the Greek, for fun and profit
Published Apr 23, 2026
For my first major project, I wanted to write a custom loader/implant in c/asm, but I have always been rather obsessed with really small/tight code. I hated the idea of importing large libraries for things like AES, and the smaller simple encoders are too easy for EDR to break, as well as throwing up red flags. The solution had to be a custom encoder. Where to start, though? For inspiration, I decided to research some novel/exotic/esoteric/ancient methods, to see if anything popped out at me. It didn’t take long before I stumbled on an ancient Greek cipher method called a ‘scytale’. So, how did it work?
A leather strip was wrapped around a rod of a specific diameter, and then a message would be written horizontally across the strip. Afterward, extra letters are written across the remaining space on the strip, making it so that you can’t find the original message, unless you have a rod of the correct diameter. This was rather clever in ancient Greece, but in this day and age, you can probably already see the problem - if you know the base concept, you can relatively easily brute force the diameter, by just adjusting it a little until some letters start to match. Expressed in code, this would just result in your payload bytes being offset by a static distance, which is very easy to write rules to find. Alas, while really cool history, it’s just not meant to be… or is it? What if we used a variable diameter for each letter on our rod? Something like…
This! This prevents the byte-distance dilemma and gives us a solid base to work off of. Like the original cipher, this will be a stream of data. My first thought was to create an actual ‘key’ that works like the rod (and I may still reimplement a version that does it this way), but I wanted the decoder to be broad in use, and be able to fit in the smallest places possible. To make this work, I essentially embed the key in the stream itself. Each segment of the stream will start with a 1 byte skip value, in plain ascii, between 0-7. This value is generated at random, and determines how much garbage/padding that will follow the skip byte. The garbage padding will be entirely filled with ascii hex values (0-9,A-F). Following the padding, we use 2 bytes to represent the little-endian ascii value of the byte of the encoded string.
typedef struct {
uint8_t skip_byte; // 1 byte value indicating padding size
uint8_t random_padding[]; // garbage
uint16_t payload_word; // little-endian word hex value of the payload byte
} scytale_segment;
To ensure that the stream never has any predictable starting point, we add in a 2 byte ascii hex (0-9,A-F) header, which is simply 2 random values. A visual example of the structure for a 2 byte payload would look like this:
+--+-+-----+--+-+-----+--+--+
|AA|B|C...C|DD|B|C...C|DD|00|
+--+-+-----+--+-+-----+--+--+
A = HEADER
B = SKIP BYTE
C = PADDING
D = PAYLOAD BYTE
As a sample of this in action, this is ‘ABC’ encoded in scytale:
2F3BBC14739DFBB1241E34
2F is our garbage header. 3 indicates 3 garbage characters (BBC) to skip, which brings us to ‘14’, that is our 0x41. 7 is our next part of the stream, indicating 7 garbage characters (39DFBB1) to skip. ‘24’ is our 0x42. 1 indicates a single garbage character (E) to skip, so ‘34’ is our final byte, 0x43.
The resulting assembly decoder is at just 60 bytes, which is small enough to fit in some tight places. The encoder/decoder together come in at 154 bytes. A natural benefit of this encoding method is that the entropy counts come in rather low - around ~4.0! What’s the catch? We’re paying a hefty price in overhead. On average (since the skip value would be generated randomly), this increases the size of any payload by ~6x, which is costly. A sample msf payload bin of 511 bytes turns into about 3309 bytes in scytale, but you can see the entropy significantly drop into safe territory:
$ ent msf.bin | grep Entropy
Entropy = 6.160108 bits per byte.
$ ent scytale.txt | grep Entropy
Entropy = 3.949166 bits per byte.
Since the actual values of the skip bytes are not super important, you could bias your skip bytes to be lower values, in order to lower the average, but your resulting string will lose some unpredictability. Doing this in the encoder would only be some minor changes, but any pre-compile encoding would be easy to generate in this way.
Ultimately, scytale is right for your project if you don’t fear some size overhead in your payloads, or if you don’t want to import large/common libraries to covertly move/process data.
