Pull to refresh

Обфускация строк C++ в Visual Studio

Reading time 8 min
Views 24K
Бинарная защита своих программ — дело часто нелёгкое и неблагодарное, ведь если продукт кому-то нужен, его всё равно сломают, как ни старайся. При этом самая лучшая защита всегда должна писаться, ну или по крайней мере настраиваться, вручную, а всякие там пакеры/кодировщики/виртуальные машины тоже конечно помогают, но чем более автоматически работает защита, тем легче она потом ломается, к тому же если использовать какой-то известный пакер, то кракеры его уже 10 раз ломали в других продуктах, и знают что в нём к чему. К тому же все более-менее удачные пакеты защит стоят немалых денег.

Опять-таки, чем более наворочена система защиты и чем больше ручной настройки она требует, тем больше это влияет на внешний вид самого кода, всякие там проверки, кодировки, контрольные суммы, которые если делать по-хорошему, нужно запихать в самые разные места в коде, чтобы кракеру было труднее найти. Код принимает нечитабельный вид, изменять его становится сложнее, это всё занимает ещё больше времени и т. д.

Вот и встаёт вопрос в том чтобы найти компромис между уровнем защиты/затраченным временем/удобочитаемостью кода/стоимостью и т. д.

Хочу поделиться с вами моим собственным решением для обфускации строк в программе, которое хоть и даёт лишь минимальную защиту, но является 1) бесплатным 2) лёгким 3) почти не портящим внешний вид кода 4) новым, которое кракер скорее всего в данной конкретной конфигурации ещё не видел.

Сразу хочу сказать что это решение лишь ненадолго замедлит уверенного хакера со стажем, но является очень простым и занимает лишь от силы минут 15 в начальной настройке. Оно призвано скорее дать минимальную защиту от нубо-хакеров и скорее натолкнуть вас на более защищённый тип мышления в кодинге, и дать вам шанс самим развить этот метод и реализовать другие типы защит.

Обфускация строк — зачем это нужно



При обычном компилировании программы на С/С++, все использованные в ней строки лежат в .exe файле открытым текстом. Это плохо потому что:
  1. Если в программе используются какие-либо пароли/ключи, они будут сразу видны человеку который просто откроет файл и просмотрит его.
  2. Видя названия протоколов/функций/сообщений и других системных строк, атакующий сможет получить информацию о том какие функции/библиотеки ваша программа использует, и вообще на каких алгоритмах она построена, тем самым значительно упростить себе работу по анализу и взлому программы. Один очень хороший пример который мне как раз недавно попался — pokerbotoscar.wordpress.com/2009/03/20/how-casinos-detect-pokerbots внизу этой страницы автор показывает как он сделал немало выводов о том что покер-клиент делает просто по строкам расположенным в его бинарнике.
  3. Найдя определённые строки в образе программы, атакующий сможет непосредственно на них поставить брейкпойнт отладчика, и к примеру мгновенно найти место в вашей программе, из которого выводится к примеру строка «купите лицензию». После этого он легко обнаружит вашу функцию проверки лицензии (к примеру), а там уже дело техники. Если строк в бинарнике не будет, то поиски функции чуть-чуть замедлятся.
    Просмотреть все строки можно легко используя программу Sysinternals Process Explorer, которая сразу показывает все найденные символьные строки длиннее трёх букв в любой загруженной программе.


Требования



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

Многие решения просто кодируют строки в бинарнике, а при старте программы проходят по всем строкам и декодируют их. Это контр-продуктивно, ведь из образа строки читаются также легко как и из файла — даже тем же Process Explorer. (Который вообще даже не кракерский инструмент а просто заменитель taskmgr.)

Скрипт для непосредственно кодирования строк я написал в php, так что вам понадобится и сам php и путь к php.exe в PATH.

Итак, преступим к делу.
Я использовал Visual Studio 2008, но всё будет работать примерно точно также и в других версиях Visual Studio. В других компиляторах будет даже легче, учитывая некоторые странности VS, о которых чуть позже.

Для начала — заголовок обфускации, obfuscator.h
Его нужно будет подключить к каждому файлу в котором будет использоваться обфускация.
#ifndef _OBFUSCATOR_H
#define _OBFUSCATOR_H

#ifdef X
#pragma message("MACRO X IS ALREADY DEFINED, EXPECT SERIOUS ERRORS")
#endif

#ifdef DO_OBFUSCATE_STRINGS

__forceinline char *obDecodeStr(char *inst);

#define X(s)obDecodeStr(OBPREPROCESSENCODEDSTR(s))
#else
#define X(s)s
#endif

#endif


Как вы видите, я использую макроc X(), в который будет оборачиваться каждая обфуцированная строка. Поскольку название состоит из всего лишь одной буквы (для минимального влияния на защищяемый код), и есть шанс что в другом месте проекта (или в чужой библиотеке) такой же макрос тоже объявлен, я добавил сообщение которое в таком случае покажется в окне лога билда.

Если DO_OBFUSCATE_STRINGS определён, то строка заменяется на X(s)obDecodeStr(OBPREPROCESSENCODEDSTR(s))
«OBPREPROCESSENCODEDSTR» — это всего лишь токен, который будет потом искать мой скрипт кодирующий строки. Название специально сделано длинным чтобы исключить что это сочетание букв встретится где-то ещё в проекте. Итак, сам скрипт:

<?php
date_default_timezone_set('UTC');

  function parseArgs($argv){

         array_shift($argv);
         $out                            = array();

         foreach ($argv as $arg){

             // --foo --bar=baz
             if (substr($arg,0,2) == '--'){
                 $eqPos                  = strpos($arg,'=');

                 // --foo
                 if ($eqPos === false){
                     $key                = substr($arg,2);
                     $value              = isset($out[$key]) ? $out[$key] : true;
                     $out[$key]          = $value;
                 }
                 // --bar=baz
                 else {
                     $key                = substr($arg,2,$eqPos-2);
                     $value              = substr($arg,$eqPos+1);
                     $out[$key]          = $value;
                 }
             }
             // -k=value -abc
             else if (substr($arg,0,1) == '-'){

                 // -k=value
                 if (substr($arg,2,1) == '='){
                     $key                = substr($arg,1,1);
                     $value              = substr($arg,3);
                     $out[$key]          = $value;
                 }
                 // -abc
                 else {
                     $chars              = str_split(substr($arg,1));
                     foreach ($chars as $char){
                         $key            = $char;
                         $value          = isset($out[$key]) ? $out[$key] : true;
                         $out[$key]      = $value;
                     }
                 }
             }
             // plain-arg
             else {
                 $value                  = $arg;
                 $out[]                  = $value;
             }
         }
         return $out;
     }
$args = parseArgs($argv);

echo "Obfuscating strings in ".$args[1]."\r\n";

$f = fopen($args[0], 'rb');
$o = fopen($args[1], 'wb');

define('ENCODESTRTOKEN', 'OBPREPROCESSENCODEDSTR(');

while ($line= fgets ($f)) 
{
	while (($esp = strpos($line, ENCODESTRTOKEN))!==false)
	{
		$sesp = $esp;
		$esp+=strlen(ENCODESTRTOKEN);
		while ($line[$esp]!='"') $esp++;
		$esp++;
		$sstart = $esp;
		$s = '';
		while (true)
		{
			if ($line[$esp]=='"') break;
			if ($esp>=strlen($line)) break;
			if ($line[$esp]=='\\')
			{
				if ($line[$esp+1]=='\\') $s.='\\';
				if ($line[$esp+1]=='r') $s.="\r";
				if ($line[$esp+1]=='n') $s.="\n";
				if ($line[$esp+1]=='t') $s.="\t";
				$esp+=2;
				continue;
			}
			$s.=$line[$esp];
			$esp++;
		}
		
		$enc = "";
		
		$ch = 0;
		$chphase = 0;
		while ($ch<strlen($s))
		{
			if ($chphase==0) 
				$cod = ord($s[$ch]) & 15;
			else
				$cod = (ord($s[$ch]) & (255-15))/16;
			$cod = dechex(rand(1,15)*16 + $cod);
			
			$enc.="\\x$cod";
			
			if ($chphase==0) $chphase = 1; 
				else 
			{ $ch++; $chphase = 0;};
		}
		
		echo "Obfuscating string \"$s\" to \"$enc\"\r\n";
		$line = substr_replace($line, $enc, $sstart, $esp-$sstart);
		$line = substr_replace($line, "", $sesp, strlen(ENCODESTRTOKEN)-1);
	};
	fputs($o, $line);
};

?>


Как вы видите, скрипт читает имя файла из командной строки и ищет наши токены, «OBPREPROCESSENCODEDSTR». Найдя такой токен, строка «кодируется».

Алгоритм кодирования конечно не самый стойкий, но его вы можете легко изменить под себя, если считаете что это добавит больше защиты. Здесь он приведён просто как рабочий пример.

Положите этот файл в корневую директорию вашего проекта и назовите obfuscate-i.php

Ну и наконец, файл содержащий функцию декодирования
#include "obfuscator.h"
typedef char odecoded[4095];
odecoded obbuf[4];
unsigned short lastbuf = 0;

__forceinline char *obDecodeStr(char *inst)
{
	lastbuf++;
	if (lastbuf>3) lastbuf = 0;
	unsigned int i = 0;
	unsigned int db = 0;
	bool phase = true;
	unsigned short schar = 0;
	while (inst[i]!=(char)0)
	{
		if (phase) 
		{
			schar = 0;
			schar+=(((unsigned short)inst[i]) & 0x0F);
		}
		else
		{
			schar+=(((unsigned short)inst[i]) & 0x0F) * 16; 
			obbuf[lastbuf][db] = (char)schar;
			db++;
		}

		phase = !phase;
		i++;
	}
	obbuf[lastbuf][db] = (char)0;

	return obbuf[lastbuf];
}


Что интересно в этой функции это что она не декодирует строку в том месте где она расположена, а декодирует её в специальный буфер, адрес которого уже возвращает. Это приводит к тому что все строки никогда не будут в декодированном состоянии в образе программы. Декодированными будут всего лишь 4 из них, те которые были декодированы последними.

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

Почему буферов 4 а не 1?


Представьте себе вызов MessageBox(0, X(“Some value”), X(“some another value”), MB_OK)
Если бы не обфускация, то в функцию MessageBox попали бы просто адреса строк и всё было бы нормально. Но когда обфускация активирована, этот вызов превращается в MessageBox(0, obDecodeStr(“Some value”), obDecodeStr(“some another value”), MB_OK), и оба вызова obDecodeStr выполняются *перед* тем как выполняется непосредственно MessageBox. И если бы использовался всего один буфер, то второй вызов obDecodeStr просто переписал бы первоначальную строку, и в оба аргумента функции попало бы одно и тоже:MessageBox(0, “some another value”, “some another value”, MB_OK).

Поэтому 4 буфера вместо 1. Если вы используете функции которые принимают больше чем 4 обфусцированных char* параметра сразу, то вам придётся увеличить количество буферов.

Конфигурация проекта



Итак, как это всё сконфигурировать автоматически?
(Все опции пишу на английском, я думаю разберётесь если даже у вас русская студия)

  1. Первым делом, скопируем свою Release- конфигурацию проекта 2 раза. Первую копию назовём Release-obfuscated-prestep, вторую Release-obfuscated. (prestep нужен потому что VisualStudio не умеет сохранять preprocessed файлы, пропускать их через внешний инструмент и потом компилировать в один шаг).
  2. В конфигурации Release-obfuscated-prestep, выделите все .cpp файлы кроме obfuscator.сpp и зайдите в Properties. Там, под C++/Preprocessor/Generate Preprocessed file выберите Without Line Numbers /EP /P
    Это приведёт к тому что вместо того чтобы компилироваться, все .cpp файлы будут сохраняться с расширением .i в обработанном препроцессором виде. То есть все макросы в них уже развёрнуты, в том числе и наш X() макрос.
  3. Постройте эту конфигурацию (Release-obfuscated-prestep). Линкер будет ругаться на то что нехватает объектных файлов и build не завершится, но это неважно, нам нужны только сгенерированные .i файлы.
  4. Найдите все эти новые .i файлы и добавьте их в проект. Если студия спросит не создать ли новое правило для этого расширения, можете смело отвечать НЕТ.
  5. Теперь выберите все эти файлы и зайдите в Properties. Для всех конфигураций кроме Release-obfuscated поставьте для этих файлов Excluded from build → YES.
  6. В конфигурации Release-obfuscated поставьте для этих файлов Excluded from build → NO, и, наконец, для тех же файлов, в настройках Custom Build Step, в Command Line, введите
    php obfuscate-i.php $(InputPath) src-obfuscated\$(InputName).ob.cpp
    И в поле Outputs введите
    src-obfuscated\$(InputName).ob.cpp
    Возможно, вам придётся создать директорию src-obfuscated в которую будут сохранятся обработанные файлы.
  7. Теперь выберите все эти .i файлы, кликните правой кнопкой и выберите Compile. Visual Studio должна вызвать наш скрипт, и вы должны увидеть процесс работы в окне лога, а также должны появится файлы *.ob.cpp
  8. Добавьте эти новые файлы в проект, и установите Exluded from build в NO для конфигурации Release-obfuscated, и в YES для всех остальных конфигураций.
  9. Выберите все ваши изначальные .cpp файлы, кроме obfuscator.cpp, и в конфигурации Release-obfuscated установите Excluded From Build в YES
  10. Ну и наконец, в конфигурациях Release-obfuscated-prestep и Release-obfuscated зайдите в Properties самого проекта и в С/C++/Preprocessor/Preperocessor Definitions добавьте символ DO_OBFUSCATE_STRINGS, чтобы собственно и активировать обфускацию.


Теперь достаточно обернуть каждую строку обернуть в X(), подключить заголовок обфускации и ваши строки будут защищены.

При обычной разработке используйте старые конфигурации Release/Debug, которые должны остаться неизменными, а когда придёт время выпускать бинарник, постройте сначала конфигурацию Release-obfuscated-prestep (которая не построится до конца), и наконец Release-obfuscated, которая и сгенерирует «защищённый» бинарник.
Tags:
Hubs:
+5
Comments 0
Comments Leave a comment

Articles