
Часто при передаче продукта заказчику, в виде готовой программы или некоторого аппаратного продукта, необходимо также защитить интеллектуальную собственность в виде исходных текстов программ и скриптов.
Компилируемые языки хоть как-то защищаются, соответственно, с помощью компиляции, хотя и это не панацея. А вот что делать со скриптами, которые могут быть написаны на bash или pyton?
Как вариант решения такой проблемы, может быть, шифрование скриптов с аппаратной привязкой дешифратора к платформе, на которой этот скрипт исполняется. Звучит красиво, но надёжно ли? Насколько будет эффективен и взломостойкий этот метод?
У меня в одном проекте была проба пера такого решения. Заодно проверил, и вскрыл это шифрование.
❯ Obash
Поиск готовых решений по шифрации скриптов bash, вывел меня на популярный проект obash, о котором даже есть статья в вики. Само решение прикольно тем, что оно позволяет шифровать также и python-файлы.
Дам вольный перевод вики, что же это такое:
Obash — это обфускатор скриптов bash, написанный на языке программирования C. obash кодирует и шифрует скрипты bash в исполняемые бинарные файлы, аналогично проекту shc, который послужил ему вдохновением, но использует шифрование AES-256, а ключ и инициализационный вектор получают из аппаратного обеспечения вместо того, чтобы быть жестко закодированными в самом бинарном файле. Проект obash был создан для решения некоторых проблем, присущих shc, одной из которых является возможность увидеть исходный скрипт оболочки, просто выполнив команду ps -ef. Хотя цели обоих проектов схожи, obash не разделяет код с shc и был создан с нуля, любые сходства в коде являются случайными и обусловлены общими задачами.Obash всё ещё находится в разработке, однако основная ветка на GitHub обычно содержит рабочие исходники, в то время как тестовая ветка в любой момент может находиться в переходном состоянии.
Также полезная информация, кратко, как же это работает:
Obash берёт входной скрипт и шифрует его с помощью AES-256, а также кодирует полученный шифротекст в base64, чтобы его можно было использовать для объявления массива unsigned char. Затем он создает промежуточный файл на языке C, который в основном является интерпретатором (см. interpreter.c), функциями, массивом текста, содержащим шифротекст, а также необязательными ключом и вектором инициализации для повторно используемых бинарных файлов (не привязанных к аппаратному обеспечению) и основной частью программы. Этот промежуточный файл на C затем компилируется в исполняемый файл.Промежуточный файл на C создаётся следующим образом (см. функцию mk_sh_c в functions.c):
• включает блок из interpreter.h
• содержит переменную crypted_script с зашифрованным с помощью AES-256 скриптом, закодированным в base64
• переменные serial и uuid (пустые, если бинарь не является повторно используемым)
• блок функций из interpreter.h
• основной блок кода main из interpreter.h
Стоит отметить, что данная система защиты имеет аппаратную привязку, и даже если запустить в другом месте получить доступ к коду и выполнить скрипт не получится.
Проще продемонстрировать, как же это работает, и сразу всё станет понятно.
❯ Пример работы obash
Клонируем репозиторий и ставим нужные либы для работы программы:
git clone https://github.com/louigi600/obash.git
cd obash/
sudo apt install libssl-dev
make
В этом репозитории есть тестовый скрипт, который позволяет сделать проверку шифрования - testme.
#!/bin/bash
echo "my pid is: $$"
echo $0
echo $*
echo -n "enter something ... "
read A
echo $A
sleep 10 &
echo "sleep pid is: $!"
ps -ef | grep $$
ps -ef | grep $!
sleep 2
Одной из первых проблем, при попытке зашифровать будет следующий выхлоп:
/obash testme Output filename will be: testme /sys/firmware/dmi/tables/smbios_entry_point: Permission denied /dev/mem: Permission denied Machine uuid: of length -2 /sys/firmware/dmi/tables/smbios_entry_point: Permission denied /dev/mem: Permission denied Insufficient data to identify.
Проблема достаточно банальная: для чтения содержимого этих файлов требуются рутовые права. Эти файлы нужны для того, чтобы зашифровать данные и привязать скрипт к текущей машине. В наших исследовательских целях — это ни к чему.
Поэтому для упрощения нашего примера, сделаю эти файлики локальными.
sudo cat /sys/devices/virtual/dmi/id/product_uuid > product_uuid
sudo cat /sys/devices/virtual/dmi/id/product_serial > product_serial
И в коде заменить эти строки в файле interpreter.c
#gv char prod_serial[256]="/sys/devices/virtual/dmi/id/product_serial";
#gv char prod_uuid[256]="/sys/devices/virtual/dmi/id/product_uuid";
на
#gv char prod_serial[256]="./product_serial";
#gv char prod_uuid[256]="./product_uuid";
В файле obfuscated_bash.c
char prod_uuid[256]="/sys/devices/virtual/dmi/id/product_uuid";
char prod_serial[256]="/sys/devices/virtual/dmi/id/product_serial";
на
char prod_uuid[256]="./product_uuid";
char prod_serial[256]="./product_serial";
Теперь всё готово к натурным испытаниям и проверке на прочность.
❯ Анализируем шифрованные скрипты
Итак, создаём шифрованный скрипт:
./obash testme -o testme.x
Можно убедиться, что он работает и запускается. Если же удалить файлы product_serial
и product_uuid
, то работать перестаёт (потому что файлы содержать ключевые данные для дешифрации).
Давайте проанализируем полученные шифрованные скрипты. Не буду использовать тяжёлую артиллерию типа IDA Pro
или ghidra
. Для анализа нам потребуется только редактор mcedit
или любой аналогичный.
Открываем файл:
mcedit testme.x
И видим следующую картину:

Сразу бросается в глаза блок шифрованных данных BASE64 (1), который сразу раскрывает, что содержимое шифровано. И также целый кусок текста (2), по которому можно загуглить исходники этого проекта. Очень большая ошибка оставлять в теле шифрованной программы любые открытые текстовые данные, которые могут оказать помощь по её вскрытию.
Теперь главное: насколько взломостойкое это решение?
❯ Взлом шифровальщика скриптов
Представим себе, что мы получили доступ к системе, где есть такой шифрованный скрипт. И у нас появилась возможность провести исследование, что же это за программа.
Мы не знаем, что она делает, ни как устроена, и попробуем провести некоторый анализ. Ранее мы её уже посмотрели с помощью mcedit, поняли, что что-то там шифрованное. Больше мы ничего о ней не знаем.
Первое, что сделаем, посмотрим, какие библиотеки она использует:
ldd ./testme.x
linux-vdso.so.1 (0x00007ffe010a4000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f6bc4372000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6bc4149000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6bc47de000)
Уже понятно, что используется криптографическая библиотека. Давайте запустим программу и посмотрим системные вызовы. Для удобства сохраним в логфайл:
trace -o log ./testme.x
Вначале достаточно много инициализирующей информации, которая пока для нас малоинформативная. Если посмотреть выхлоп этого вывода, то в середине можно увидеть, что происходит чтение некоторых файлов, для каких-то своих целей. Если внимательно посмотреть, то становится понятно, что программа вычитывает серийные номера "железа", означает то что программа проверяет установлена ли она на этом оборудовании и использует их для дешифрации:
...
openat(AT_FDCWD, "./product_uuid", O_RDONLY) = 3
newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=37, ...}, AT_EMPTY_PATH) = 0
read(3, "25219240-d7da-11dd-a7e1-08606e55"..., 4096) = 37
close(3) = 0
openat(AT_FDCWD, "./product_serial", O_RDONLY) = 3
newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=21, ...}, AT_EMPTY_PATH) = 0
read(3, "System Serial Number\n", 4096) = 21
close(3)
...
Главная магия кроется в конце лога:
...
getpid() = 105140
mknodat(AT_FDCWD, "/tmp/105140", S_IFIFO|0666) = 0
clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7104fb6a10) = 105141
openat(AT_FDCWD, "/tmp/105140", O_WRONLY) = 3
write(3, "#!/bin/bash\n\necho \"my pid is: $$"..., 175) = 175
close(3) = 0
unlink("/tmp/105140") = 0
wait4(105141, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 105141
...

Опачки, вот оно.
Распишу, что происходит:
Программа получает свой PID процесса равный 105140
.
mknodat создаёт именованный канал (FIFO) с именем /tmp/105140
.
Далее делается форк программы, в котором, видимо, запускается bash (нам эта часть кода недоступна, потому что это уже другой процесс), PID дочернего процесса равен 105141
.
В родительском процессе, который мы исследуем, этот файл открывается для записи и в него записывается 175 байт текстовых данных, которые начинаются с "#!/bin/bash\n\necho \"my pid is: $$"
. Опа, вот оно!
Далее файл закрывается и удаляется, и после чего родительский процесс ожидает завершение дочернего процесса с PID=105141
.
Обратите внимание, что в именованный канал мы записываем 175 байт. И по «странному» стечению обстоятельств, наш тестовый скрипт, до процедуры шифрования, занимает как раз 175 байт!

В принципе, уже с помощью команды strace
можно выдрать содержимое скрипта. Но мы пойдём другим путём.
❯ Автоматизируем взлом шифрованных данных
Мы поняли, что это bash скрипт, и таким же образом может быть спрятан и python. Любой шифрованный скрипт должен иметь на выходе нешифрованный интерпретатор, который и является дыркой в безопасности.
Если дверь имеет множество систем защиты, хорошо укреплена, может попробовать проломить стену? Как правило рядом стена из картона и можно легко войти в пролом, не надо биться в укреплённое место.
Поэтому, чтобы не возится с strace
, набросаем программу для автоматической дешифрации скриптов. Она будет прикидываться интерпретатором bash и python и позволит сохранить все шифрованные файлы. По-быстрому её, полный код под спойлером.
программа fake_interpreter
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_BUFFER 65536 // 64 КБ
int main(int argc, char *argv[]) {
const char *filename = NULL;
char *program_name = strrchr(argv[0], '/');
program_name = program_name ? program_name + 1 : argv[0];
if (strcmp(program_name, "bash") == 0) {
if (argc < 3 || strcmp(argv[1], "-c") != 0) {
fprintf(stderr, "Использование bash: %s -c \"source <файл>\"\n", argv[0]);
return 1;
}
if (strncmp(argv[2], "source ", 7) == 0) {
filename = argv[2] + 7; // Пропускаем "source "
} else {
fprintf(stderr, "Ожидалась команда 'source'\n");
return 1;
}
} else if (strcmp(program_name, "python") == 0) {
if (argc < 3 || strcmp(argv[1], "-u") != 0) {
fprintf(stderr, "Использование python: %s -u <файл>\n", argv[0]);
return 1;
}
filename = argv[2];
} else {
fprintf(stderr, "Неизвестная программа: %s\n", program_name);
return 1;
}
FILE *file = fopen(filename, "r");
if (file == NULL) {
fprintf(stderr, "Не удалось открыть файл: %s\n", filename);
return 1;
}
char buffer[MAX_BUFFER];
size_t bytes_read;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
fwrite(buffer, 1, bytes_read, stdout);
}
fclose(file);
return 0;
}
Программу собираем следующим образом:
gcc fake_interpreter.c -o bash
gcc fake_interpreter.c -o python
Далее, простая задача: запустить шифрованную программу с подменой интерпретатора. Делается это простой командой:
PATH=.:$PATH ./testme.x
Всё! Теперь в качестве интерпретатора запускается наш код, и весь шифрованный код вываливается сразу в консоль, после чего его достаточно просто перенаправить в файл.

❯ Выводы
Для меня вывод оказался печальным: это решение нельзя использовать, чтобы скрыть свой код. Оно будет как-то работать при передаче этого файла через открытые сети, но если у взломщика есть физический доступ к машине, то код будет вскрыт.
Ещё одним важным замечанием для всех, кто хочет делать какую-либо минимальную обфускацию кода – это скрывать все текстовые переменные. Так, чтобы невозможно было в программе, открыв mcedit
увидеть её содержимое. Это просто важнейшее правило.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud - в нашем Telegram-канале ↩

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.