Обфускация строк на этапе компиляции

Возник на днях у нас вопрос: «Как спрятать от любителей hex-редаторов строчки текста в скомпилированном приложении?». Но спрятать так, чтобы это не требовало особых усилий, так, между прочим…
Задача состоит в том, что бы использовать в коде строки как обычно, но при этом в исполняемом файле эти строки в явном виде не хранились, возможности сторонних утилит, которые работают с уже скомпилированными бинарными файлами, задействовать так же не хочется, все нужно делать из обычного C++ кода.

Ясно, что нам придется подключить возможности С++ в области метапрограммирования и вычислять шифрование строк на этапе компиляции. Но шаблоны в чистом виде не позволяют использовать в качестве параметров инициализации строки. К счастью, в C++11 появились constexpr – функции, результат которых может быть вычислен на этапе компиляции. В собственно C++11 их возможности довольно ограничены (нельзя использовать, например, циклы и условия), но в новом стандарте C++14 они были существенно расширены практически до возможностей обычных функций (естественно, это должны быть только чистые функции без побочных эффектов).
Получившийся небольшой пример:

#include <string>
#include <iostream>
#include <iterator>

//хранилице зашифрованных строк
template<std::size_t SIZE>
struct hiddenString
{
    //буффер для зашифрованной строки
     short s[SIZE];

     //конструктор для создания объекта на этапе компиляции
     constexpr hiddenString():s{0} { }

     //функция дешифрации в процессе исполнения приложения
     std::string decode() const
     {
			std::string rv;
			rv.reserve(SIZE + 1);
			std::transform(s, s + SIZE - 1, std::back_inserter(rv), [](auto ch) {
				return ch - 1;
			});
			return rv;
     }
};

//вычисление размера строки на этапе компиляции

template<typename T, std::size_t N> constexpr std::size_t sizeCalculate(const T(&)[N]) 
{
     return N; 
} 


//функция шифрации на этапе компиляции
template<std::size_t SIZE>
constexpr auto encoder(const char str[SIZE])
{
    hiddenString<SIZE> encoded;
	for(std::size_t i = 0; i < SIZE - 1; i++)
        encoded.s[i] = str[i] + 1;
	encoded.s[SIZE - 1] = 0;
    return encoded;
}

//макрос для удобства использования
#define CRYPTEDSTRING(name, x) constexpr auto name = encoder<sizeCalculate(x)>(x)

int main()
{
    //выведем зашифрованную на этапе компиляции строку,
    //если посмотреть содержимое скомпилированного файла,
    //то оригинал там отсутствует
    CRYPTEDSTRING(str, "Big big secret!");
    std::cout << str.decode() << std::endl;
    return 0;
}

Пример не претендует на законченную программу и демонстрирует лишь сам принцип.
Шифратор и дешифратор просто для примера инкрементируют и декрементируют оригинальные символы строки, в теории можно прикрутить достаточно сложные алгоритмы с ключами и расшифровкой хоть на удаленном сервере. Правда есть ложка дегтя, потребовалось задействовать возможности С++14, возможно кто-то знает способ лучше?
ПС. Пример компилировался на Arch Linux с помощью clang 3.5.0 следующей командой:
$: clang++ -std=c++1y -stdlib=libc++ -lc++abi sample.cpp -o sample


Авторы: Токарев А.В., Гришин М.Л.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 40

    0
    Фантастика, вчера как раз искал варианты обфускации строк в бинарниках :) Вы случаем не из NSA?
      0
      Интересная идея :)
        +5
        А теперь выполните
        objdump -s -j .rodata sample
        и удивитесь…
          0
          Это временное неудобство ))))
          Оптимизация сыграла злую шутку — сейчас поправим…
            0
            Исправлено, использование немного по другому, но не сильно сложнее
              0
              А достаточно было включить -O2 или выше.
                0
                Вы правы, но текущий вариант работает и без оптимизаций, а старый только с -O2
            –1
            А что помешало выделить память под строку за один раз, а дальше делать push_back или вообще использовать operator[]?
            Попробуйте скомпилировать с O3, и наверняка найдете свои строки внутри бинарника в нетронутом виде.
            Длину строкового литерала на этапе компиляции можно посчитать намного проще.
              0
              Строки можно сложить в отдельный .cpp файл, который собирать с -O0.
                +1
                Если сложить только строки, и использовать в другом translation unit, то их оценинование не будет происходить на этапе компиляции.
                0
                с -O3 результат не меняется, проверено. С какой стати компилятору выбрасывать с оптимизацией вычисления времени компиляции?)
                  +2
                  Вы видимо не поняли, строки шифруются в compile-time, а расшифровываются в runtime, с O3 компилятор может заменить простой алгоритм расшифрования строки на саму оригинальную строку, если увидит, что он и сам может получить результат.
                  Длина строкового литерала без циклов:
                  template<typename T, std::size_t N> constexpr std::size_t array_length(const T(&)[N]) { return N; }
                    0
                    Добавил ваш вариант длины, хорошее решение, в спешке не догадался до него.
                  0
                  Всё с точностью до наоборот, константа как раз и выкидывается из объектного файла оптимизатором за ненадобностью. Нетронутая строка останется при -O1 и ниже.
                  0
                  Разве что как эксперимент. В реальном проекте имхо правильнее использовать для таких целей готовый обфускатор или упаковщик — в первую очередь потому, чтобы не замусоривать исходный код и не усложнять его поддержку.
                    +1
                    Или нежелание возиться с экзешниками (обработанный, не обработанный — собрал и забыл), когда надо сделать банальное сокрытие пары строк от дурака.
                      +8
                      Сокрытие пары строк от дурака


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

                      Это очень плохое архитектурное решение по массе причин:

                      • Повысилась минимальная требуемая версия компилятора
                      • Повысился порог вхождения для нового программиста по причине наличия велосипеда
                      • Про макрос теперь все время нужно помнить и компилятор ничего не подскажет

                      А с упаковщиком никакой возни, нужно просто добавить в одну из конфигураций лишний build step и больше о нем не вспоминать.
                        –3
                        Все это надуманно, но отвечу:
                        1) Если есть новый стандарт и он имеет практическую пользу, его нужно внедрять, а не уповать на сторонние решения, требующие еще и капиталовложений, чем больше будет решений на его базе — тем быстрее подтянутся разработчики компиляторов.
                        2) Если для программиста какой-то там велосипед в коде (в виде одного единственного макроса) — есть проблема, тогда стоит задуматься о его компетентности.
                        3) Зачем о нем помнить, и что должен о нем подсказывать компилятор?

                        Кроме того внешние обфускаторы вносят в код не мало тормозов и местами их даже требуется отключать. Подделкой копирайтов, например, занимается обычно школота, которая хочет потроллить бесплатный, но закрытый в плане кода проект — там не нужен профессиональный инструмент, и тем более нет желания за него платить в бесплатном продукте. Лишний build step — как правильно сказанно — лишний.
                          +1
                          1) Бесплатный классный упаковщик UPX вполне подходит для скрывания строк от дурака (кто знает про upx -d — уже не дурак). Новый стандарт внедрять не надо, он и так «внедрится», никуда не денется: компиляторы сами по себе соревнуются в поддержке новых стандартов. Не стоит все новые фишки стандартов использовать без оглядки не по назначению.
                          2) Программисту-то не проблема, но поддерживать код, написанный с использованием таких макросов — ужас. Это палки в колёса на пустом месте. Толку 0, а влияет эта «мелочь» абсолютно на весь код.
                          3) Любой хитроумный макрос и тем более шаблон превращает короткую и понятную ругань компилятора в полотна в несколько мегабайт. Вот в программах на C сообщения об ошибках обычно помещаются на 1 экран, а в программах на С++, где замешаны шаблоны, получается портянка. Удалите где-нибудь обязательный const при обходе STL-контейнера и насладитесь неразборчивой руганью. Такие сообщения труднее читать вне зависимости от компетентности.

                          бесплатный, но закрытый в плане кода проект
                          Сразу о скайпе подумал. Там обфускаторы порезвились наславу. Но если проект не скайп (а разработчик не лучший враг открытого софта), то не понимаю, какой смысл в закрытом коде для бесплатной программы. Тем более тщательно охранять этот код.
                            0
                            1..2) На весь код влияет упаковщик, и уж точно не показанный пример в статье.
                            3) У вас вылезла портянка в указанном примере? Что там хитроумного? По вашей логике — вообще нельзя использовать шаблоны, макросы — вообще ничего, может вы язык не тот используете?

                            Видимо кто-то тут разрабатывает обфускаторы и очень неуютно начинает себя чувствовать в свете новых возможностей языка и начинает агрессивно втыкать, что так делать ни в коем разе нельзя. Все ваши доводы абсолютно надуманные — хоть сто минусов мне наставьте.
                              0
                              Обфускаторы я не разрабатываю и не использую, но осознаю, что подход «зашифровать строки в бинарнике» сильно уступает продвинутым обфускаторам. Вашу защиту снимут за 1 минуту, а со скайпа годами снимали.

                              Давайте сравним подход, предложенный в статье, с бесплатным упаковщиком UPX.

                              1) И UPX и шифрование строк при компиляции прячут строки в бинарнике, но любой специалист восстановит оригинал за минуту.
                              2) UPX можно применять к любому бинарнику (и не только, он и скриптовые языки вроде питона понимает), а шифрование строк — только в C++, только в новых компиляторах, так как используются фичи нового стандарта.
                              3) UPX подключается прозрачно и не мешает использовать привычные инструменты отладки. Если в команду приходит новый человек, ему про UPX не нужно объяснять, по крайней мере в первую очередь. Кстати, странно, что автор статьи как раз разрабатывает PVS-Studio. «Прожуёт» ли PVS-Studio такой код?

                              Самое важное: нельзя смешивать уровни. Если шифрование строк происходит в коде С++, то уровни защиты и логики самой программы скованы. Хорошо, когда уровней несколько и каждый из них допускает замену. Такие системы часто называют стеками. Хорошо известен сетевой стек, чуть меньше известен стек систем, вовлечённых в разметку жестких дисков, о которых пойдёт речь дальше. В примере ниже можно изменить тип шифрования или ФС, не меняя всего остального.

                              Пример. Надо разметить несколько разделов в нескольких ЖД (с дублированием данных на нескольких ЖД), да ещё всё это зашифровать, да ещё в некоторых ФС включить сжатие данных (допустим, там хранятся в основном текстовые файлы). Правильный подход: RAID + шифровалка томов (LUKS / TrueCrypt) + LVM + Btrfs там, где нужно сжатие. Получили стек из 4 уровней. Простая конфигурация и высокая надёжность. Если захотим, можем даже порядок уровней изменить: сначала LVM, потом шифрование там, где нужно. Неправильный подход: всё это отдать на откуп одной системе. Насколько мне известно, так происходит в ZFS и NTFS.

                              Ещё один пример. Новая система systemd, которая «заглотнула» десяток вещей, которые до этого были сами по себе и могли быть заменены независимо. Как я понимаю, это источник критики в адрес systemd.
                                0
                                Если читали статью — цель — защитить от дурака с hex-редактором подмену оригинальных строк — не больше не меньше.

                                1) За минуту ни у кого не получится, как минимум придется найти участок где код выводится, написать патч для программы, чтобы подменить оригинал на что-то свое, поскольку в случае с открытым и закрытым ключем не выйдет просто поменять данные зная алгоритм шифрования. Да это легко ломается — если умеешь, строго говоря любая защита ломается, все зависит от оправданности взлома.
                                2) И что? Почему это должно считаться аргументом в пользу запрета на подобные решения в тех местах где они достаточны?
                                3) Все это лирика, а PVS-студию я не разрабатываю, но думаю прожует и не подавится.

                                По поводу всего остального — никто не заставляет пользоваться такими методами, и ни кто не спорит, что специальные инструменты эффективней, но вот только не надо говорить, что при малейшей мысли о защите от мелких хулиганов нужно хвататься за гранатомет.
                                  0
                                  UPX — не гранатомёт, а штатное средство, стандартный шаг при сборке сотен тысяч программ. Назовите всё-таки хоть одно существенное преимущество вашего подхода над UPX. Преимущества UPX (они же недостатки вашего подхода) в комментариях уже разбирались и не раз.

                                  Про PVS-Studio у меня ошибочка вышла. В вашем профиле и в профиле FoxCanFly сказано, что вы работаете в PVS-Studio, вот я и решил, что вы занимаетесь разработкой PVS-Studio. Хотелось бы позвать в тред кого-нибудь из старших в PVS-Studio: могут ли они рекоммендовать такую практику своим пользователям и станут ли сами её применять для защиты PVS-Studio от обратной разработки.
                                    0
                                    Статья была выложена не от имени компании, и не в ее блоге.
                                      0
                                      Я не про UPX, который кстати вообще не для этого предназначен — сжатые экзешники делались еще под RiscOS в конце 80-х и распаковывались без всяких проблем, надо быть полным чайником, чтобы такую защиту не снять.

                                      Никто никому ничего не навязывает, кроме вас, просто получился неплохой вариант, решили поделиться, к чему эти нападки?
                      +2
                      А что не user-defined literals?
                        0
                        Hidden же
                          0
                          std::string rv;
                          for(std::size_t i=0; i<SIZE; i++)
                          rv.push_back(s[i] — 1);
                          не проще тут строке resize/reserve сделать перед циклом? или гцц уже автоматически делает такие оптимизации?
                            0
                            Проще, но делалось наспех, код писался для проверки идеи. Не вижу смысла в нем делать то, что к этой идее не относится.
                              0
                              Немного отрефакторил код в статье.
                            +1
                            Какая получилась ужасная смесь сторого Си c Си++14.
                              0
                              Если это про char*, то следует знать, что в constexpr нельзя использовать не-литеральные типы (например std::string)
                                0
                                Позволю себе поревьювить ваш код:

                                1) в encoder SIZE — размер массива с завершающим нулем, то есть достаточно хранить SIZE байтов, а не SIZE+1
                                2) encoded.s[SIZE] = 0 — явный выход за границу массива
                                3) тут много ошибок работы с окончанием строки, например, push_back нуля в std::string делать нельзя, для него это нормальное значение, то есть нужно так:
                                td::transform(s, s + SIZE - 1, ...
                                

                                небольшие замечания:
                                а) sizeCalculate — совсем лишний, если переписать encoder так:
                                template<typename T, std::size_t SIZE>
                                constexpr auto encoder(const T (&str)[SIZE])
                                

                                б) почему для буфера был выбран short вместо char?
                                в) не силен в с++11, но, по-моему, можно указать значение по умолчанию и не описывать конструктор:
                                short s[SIZE + 1] = {0};
                                

                                  0
                                  А так идея мне понравилась :)
                                    0
                                    1-2 — ранее это был размер строки без завершающего нуля, исправлю в статье.
                                    3 — не вижу причин, почему бы std::string не сохранить 0, как вы подметили, для нее это нормально значение. Но все касательно SIZE, сказано выше.
                                    а) — не так наглядно, и функция будет неверна, если кто то вдруг захочет передать туда неконстантную строку.
                                    б) — для избежания получения 0 при шифровании, если значение окажется #FF, ну и в бинарнике будет выглядеть более странно.
                                    в) — способ требует наличия constexpr конструктора, конструктор по умолчанию им не является.
                                      +1
                                      3) нельзя, потому что при выводе с помощью std::cout в консоли грязь будет в конце, и длина строки с помощью size будет неверной, stackoverflow.com/questions/2845769/can-a-stdstring-contain-embedded-nulls

                                      а) не, мой способ нагляднее, а ваш избыточен :)

                                      б) ничего страшного, если для символа FF получится при шифровании 0, ведь вы никак не ориентируетесь при получении длины строки на завершающий ноль, длина строки вам всегда известна. Тем более, при других способах шифрования вы никак не гарантируете, что не получится ноль, да и не нужно за этим тут следить.
                                      0
                                      в означает следующее: инициализировать первый элемент нулем, а остальные — нулями. Почему не просто пустые фигурные скобки (value-initialization)?
                                  0
                                  #ifndef __PREPROCESSOR_ENCODER_DECORED_INC_
                                  #define __PREPROCESSOR_ENCODER_DECORED_INC_

                                  #define __HEADER(x,s,l) ((wchar_t*)x)[0]=_countof(s);((wchar_t*)x)[1]=l;
                                  #define __CVTNS(x,s,k,i) if( i
                                    0
                                    Прошу прошения, время редактирования истекло.

                                    я тут на коленке накропал за полчаса — в принципе даже на махровом C 30-летней давности будет работать.

                                    dpaste.com/0QBHMDQ

                                    Использовать так:

                                    void testEncrypt1()
                                    {
                                    wchar_t rst[256] = { 0 };

                                    // 0, 1 последовательность для создания уникальных строк, следуюший вызов должен увеличить последние 2 нoмера на единицу ,,, 1, 2
                                    const wchar_t* aa = __LIT(L«0000000000111111111122222222223333333333444444444455555», 0, 1);
                                    const wchar_t* bb = __LIT(L«habrahabr.ru/post/245719/», 1, 2); // следуюший вызов должен увеличить последние 2 нoмера на единицу ,,, 2, 3
                                    wchar_t* result1 = local_encdec(aa, rst);
                                    CPPUNIT_ASSERT_EQUAL_MESSAGE(«Failed to load object», 0, ::wcscmp(rst, L«0000000000111111111122222222223333333333444444444455555»));

                                    wchar_t* result2 = local_encdec(bb, rst);
                                    CPPUNIT_ASSERT_EQUAL_MESSAGE(«Failed to load object», 0, ::wcscmp(rst, L«habrahabr.ru/post/245719/»));
                                    }

                                    Не придумал с ходу как счетчик нумерации прикрутить.

                                    В принципе можно сделать так чтобы ГОСТ-м кодировала и функция декодирования меняла код тела и сигнатуру для функции декодирования.
                                    0
                                    Ваш пост очень похож на презентацию Sebastien Andrivet с Blackhat Europe 2014

                                    Only users with full accounts can post comments. Log in, please.