I am doing a case study on Mirai, and I am stuck on the XOR encryption/decryption. From the source code, I copied the toggle_obf function and I tried several ways to reverse the "\x22\x35" into 23.
I also tried the method provided in What type of encryption is used with Mirai? But it doesn't seem to work even if table_key is 0xdeadbeef. If anybody can explain how it gets to the 23 in the entry, that would be great and would help me understand the XOR better.
Function:
static uint32_t table_key = 0xdeadbeef;
static void toggle_obf(uint8_t id){
int i;
struct table_value *val = &table[id];
uint8_t k1 = table_key & 0xff,
k2 = (table_key >> 8) & 0xff,
k3 = (table_key >> 16) & 0xff,
k4 = (table_key >> 24) & 0xff;
for (i = 0; i < val->val_len; i++)
{
val->val[i] ^= k1;
val->val[i] ^= k2;
val->val[i] ^= k3;
val->val[i] ^= k4;
}
Entry:
add_entry(TABLE_CNC_PORT, "\x22\x35", 2); // 23, TABLE_CNC_PORT 4(id)
Let's do a bit of reverse engineering.
TCP and UDP allot 16 bits for port numbers, so port 23 (decimal) is conventionally stored internally as two bytes, 0x0017 (hex).
Considering ...
add_entry(TABLE_CNC_PORT, "\x22\x35", 2)
... we see two bytes (ignoring the string terminator). Let's look at some representations:
decimal | hex | binary |
---|---|---|
0 | 0x00 | 0b00000000 |
23 | 0x17 | 0b00010111 |
34 | 0x22 | 0b00100010 |
53 | 0x35 | 0b00110101 |
222 | 0xde | 0b11011110 |
173 | 0xad | 0b10101101 |
190 | 0xbe | 0b10111110 |
239 | 0xef | 0b11101111 |
Now consider what we would observe if the two bytes 0x00
and 0x17
were each encrypted by XORing it with the same 8-bit key (equivalent to XORing with the same combination of any number of separate 8-bit keys):
0x00
and 0x17
differ, AND0x00
would be the (combined) encryption key.Now note that the binary representations of 0x17 and 0x00 differ exactly where 0x17 has 1 bits, and note that these are also exactly the positions where the two cipherbytes differ. Under the circumstances, I think we can conclude that "\x22\x35"
represents a two-byte cipherbyte sequence corresponding to a 16-bit integer representation of 23 (decimal).
Note further that the result of XORing together the bytes of 0xdeadbeef
is 0b00100010
, or 0x22
. That is the same as the first cipherbyte, so if we take 0xdeadbeef
for the original key material then we can recognize that the first cipherbyte corresponds to the 0x00
of the supposed plain bytes. That means that the encoded representation is big-endian, also known as "network byte order". This is not particularly surprising for a port number.
So, to get from the cipherbytes to the number 23, we might use something like this:
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
const uint32_t table_key = 0xdeadbeef;
const uint8_t key8 =
table_key
^ (table_key >> 8)
^ (table_key >> 16)
^ (table_key >> 24);
uint16_t decode16n(const uint8_t cipherbytes[]) {
return ((cipherbytes[0] ^ key8) << 8) | (cipherbytes[1] ^ key8);
}
int main(void) {
printf("Deciphered port number: %" PRIu16 "\n", decode16n("\x22\x35"));
}