страничка tripsin'а


Главная | Статьи | Заметки | Файлы| Ссылки | я

Шифруйте строки, Шура!

encryptstringssample.rar (10,2кб) - скачай демо-проекты.

Наверное однажды, накодив что-нибудь стоящее, тебе станет жалко вот так просто всем раздавать свое творение и вполне справедливо захочется получить за свою работу денег. Для этого в программе надо создать защитный механизм, который не позволит ей запуститься, не получив заветный регистрационный ключик. И вот счастливо сидя на печке, ты мечтаешь куда потратишь заработанные барыши. Но не тут-то было. Крэкеры (и прочие хакеры) не дремлют, и если твоя программа представляет хоть что-то стоящее, то она в скором времени будет сломана. Почему такой облом? А потому что ты ее плохо защитил. Конечно любая программа рано или поздно может быть взломана, но ведь лучше поздно, чем рано. И пока хакеры будут корячиться с защитой твой проги, какая-то денежка все-таки попадет в твой ненасытный карман.

Я не буду рассказывать тебе о способах защиты, самомодификации кода, антиотладочных приемах и прочем. Во первых об этом всем где уже только не написано. А во вторых я не считаю себя достаточно компетентным в разработке защитных механизмов. Вместо это я расскажу об одном приеме, который повысит стойкость программы к взлому в дизассмеблере. Довольно часто бывает, что установив мощную защиту, программер забывает о самом простом, в результате чего стойкость защиты становится равна нулю. В частности это можно сказать о строковых константах, используемых программой. Бывает даже так, что регистрационный ключ можно найти среди строк в секции ресурсов (я использую eXeScope) или он легко обнаруживается при просмотре бинарника в hex-редакторе. Я уже не говорю про дизассемблеры, которые выведут все найденные в коде строки в виде удобного списочка. Но даже если программист озаботился спрятать серийник, то такие строки как “Unregistered”, “Wrong Password” и т.п. позволяют зацепиться за защитный механизм и значительно облегчить взлом. Короче! Шифруйте строки, Шура! (они конечно не золотые, но пару копеек добавят)

Вот этим мы сейчас и займемся. Напишем простенький крякмис (crackme) и потом зашифруем в нем строки. Чтобы не рыться потом в кучах GUI-шного кода, сделаем крякмис консольным. Код элементарный и ясен без комментариев.

program crackme1;
{$APPTYPE CONSOLE}
const
  title = 'Simple crackme №1 by tripsin';
  promth = 'Enter password: ';
  hellohacker = 'Invalid password! Try else.';
  registered = 'Correct password !! You are hacker!';
  serial = 'superpuperp@ssvvord';
  exit = 'Press  to Exit.';
var
  password: String;
begin
  WriteLn(title);
  Write(promth);
  ReadLn(password);
  if password = serial then WriteLn(registered)
  else WriteLn(hellohacker);
  Write(exit);
  Readln;
end.

В этом коде мы пользуемся строковыми константами, среди которых есть и наш серийник (serial). И хотя эти строки не светятся в ресурсах, их легко найти и hex-редактором, и дизассемблером.

Вот что нашел мой любимый Hex Workshop:

А вот что выдал небезызвестный дизассемблер W32Dasm:

Таким образом степень защищенности кода крякмиса №1 равна нулю. Все строчки лежат в открытом виде и очень помогают нам при взломе. Значит надо их зашифровать, чтобы в бинарнике был виден только мусор. А потом расшифровывать нужную строку непосредственно перед выводом на экран и сразу уничтожать. Применять мощные алгоритмы шифрования тут бессмысленно. Все равно, что стрелять из пушки по воробьям. Вполне сойдут простенькие, но быстрые методы. Для шифрования очень подходят несколько ассемблерных команд.

  1. XOR – общеизвестный оператор исключающего “или”. Набил уже оскомину и поэтому неинтересен (хотя вполне актуален);
  2. XCHG – Обменивает значения операндов. Можно тупо поменять местами старший и младший байт в слове, или старшее и младшее слово в двойном слове.
  3. BSWAP – появилась еще в 486-м, но до сих пор используется нечасто. Меняет порядок следования байтов в двойном слове (т.е операндом может быть только 32-разрядный регистр). Удобен для шифрования чисел. Перестановка байт из порядка «младший – старший» в порядок «старший – младший» (т.е. разряды 7-0 обмениваются с разрядами 31-24, а разряды 15-8 с разрядами 23-16).
  4. XLAT - Используется для шифрования по таблице. Загружает в AL байт из таблицы в сегменте данных, на начало которой указывает EBX (BX), при этом начальное значение AL играет роль смещения. Таблицу эту создавать лениво, но тоже можно. Очень легко сделать преобразование из одной кодировки в другую (например KOI-8 – Windows 1251)
  5. Связка ROL-ROR. Операторы циклического сдвига. Сдвигают биты в операнде влево или вправо на определенное количество позиций. При этом биты, выходящие за границы записываются с другой стороны числа (rol - старшие биты в младшие, ror – наоборот). Вот такая получается карусель. Отлично подходят для простого шифрования строк. Вот ими и воспользуемся.

Пусть зашифровывать строки мы будем с помощью ROL, а расшифровывать соответственно ROR. Шифровать текст будем побайтно. Количество позиций сдвига битов в байте и будет ключом нашего «шифра» - от 1 до 7. Не шибко много конечно :) Зашифрованные строки будем вводить прямо в листинг программы в виде последовательности ansi-кодов (типа #20#46#240). Почему именно так, а не просто писать в кавычках шифрованную строку? Да потому, что после шифрования появятся непечатаемые символы и служебные коды (типа #1, #9, #13 и т.п.), которые нам все испортят.

Чтобы не мучаться с ручной зашифровкой строк, я написал небольшую утилитку Simple string encryptor (исходничек прилагается) и внес ее в список утилит Delphi (Tools -> Configure Tools … -> Add). Собственно все шифрование выполняется в функции EncryptString, а остальное – интерфейс.

unit Unit1;

interface

uses
  SysUtils, Forms, Classes, Controls, StdCtrls, ExtCtrls, Spin;

type
  TForm1 = class(TForm)
    LabeledEdit1: TLabeledEdit;
    LabeledEdit2: TLabeledEdit;
    SpinEdit1: TSpinEdit;
    Label1: TLabel;
    Button1: TButton;
    LabeledEdit3: TLabeledEdit;
    procedure LabeledEdit1Change(Sender: TObject);
    procedure SpinEdit1Change(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

// Функция шифрования нультерминальной строки
// For ANSI-strings. Not for UNICODE!
procedure EncryptString(Data: PChar; Shift: Byte); register;
asm                     // Data – eax; Shift – dl(edx)
  test eax,eax;         // Если передан nil ...
  jz @stop;             // ... то выходим
  mov cl, dl;           // загружаем смещение в CL
@next: 
  cmp byte ptr [eax],0; //если конец строки (#0) ...
  jz @stop;             // ... то выходим
  rol byte ptr [eax],cl;// смещаем биты в текущем байте строки
  inc eax;              // получаем следующий байт строки
  jmp @next;            // крутим цикл
@stop:
end;

// Функция преобразовывает строку в последовательность кодов символов
function ToAnsiCodes(Str: String): String;
var
  i: Integer;
begin
  for i := 1 to Length(Str) do
    Result := Result + '#' + IntToStr(Ord(Str[i]));
end;

// При изменении текста в LabeledEdit1 запускаем шифрование 
procedure TForm1.LabeledEdit1Change(Sender: TObject);
var
  tmp_str: String;
begin
  tmp_str := LabeledEdit1.Text;
  EncryptString(PChar(tmp_str), SpinEdit1.Value);
  LabeledEdit3.Text := tmp_str;
  LabeledEdit2.Text := ToAnsiCodes(tmp_str);
end;

// При изменении смещения в SpinEdit1 тоже перезашифровываем строку
procedure TForm1.SpinEdit1Change(Sender: TObject);
begin
  LabeledEdit1Change(Self);
end;

// Копируем строку в буфер обмена при нажатии на «Copy to Clipboard»
procedure TForm1.Button1Click(Sender: TObject);
begin
  LabeledEdit2.SelectAll;
  LabeledEdit2.CopyToClipboard;
  LabeledEdit2.SelLength := 0;
end;

end.

Функция шифрования получилась очень короткой – всего 8 ассемблерных команд. А распаковщик будет занимать еще меньше. Код очень быстрый и к тому же, совершенно не расходует память. С помощью только что созданной утилиты шифруем все строки из секции const, и, чтобы потом не запутаться, комментируем их нормальными строками. Я зашифровал строки со смешением = 3. Дальше дописываем расшифровщик и вот что у нас получается:

program crackme2;
{$APPTYPE CONSOLE}
const
  // title = 'Simple crackme N2 by tripsin';
  title = #154#75#107#131#99#43#1#27#147#11#27#91#107#43#1#114#145#1#19#203
#1#163#147#75#131#155#75#115;

  // promth = 'Enter password: ';
  promth = #42#115#163#43#147#1#131#11#155#155#187#123#147#35#209#1;

  // hellohacker = 'Invalid password! Try else.';
  hellohacker = #74#115#179#11#99#75#35#1#131#11#155#155#187#123#147#35#9#1
#162#147#203#1#43#99#155#43#113;

  // registered = 'Correct password !! You are hacker!';
  registered = #26#123#147#147#43#27#163#1#131#11#155#155#187#123#147#35#1
#9#9#1#202#123#171#1#11#147#43#1#67#11#27#91#43#147#9;

  // serial = 'superpuperp@ssvvord';
  serial = #155#171#131#43#147#131#171#131#43#147#131#2#155#155#
179#179#123#147#35;

  // exit = 'Press  to Exit.';
  exit = #130#147#43#155#155#1#225#42#115#163#43#147#241#1#163#123#1#
42#195#75#163#113;
var
  password: String; // переменная для вводимого пароля
  str: String;  // здесь хранится расшифрованный серийник

// Функция расшифровки строки, зашифрованной rol со смещением 3
procedure DecryptString(Str: PChar);
asm
@next:
  cmp byte ptr [eax],0; //если конец строки (#0) ...
  jz @stop;             // ... то выходим
  ror byte ptr [eax],3; // смещаем биты в текущем байте строки
  inc eax;              // получаем следующий байт строки
  jmp @next;            // крутим цикл
@stop:
end;

// Функция выводит на экран расшифрованную строку
procedure Print(str: String);
begin
  UniqueString(str); // Обязательно создаем уникальную копию строки
  DecryptString(PChar(str));
  Write(str);
end;

begin
  str := serial;
  DecryptString(PChar(str)); //расшифровка серийника
  Print(title);
  WriteLn;
  Print(promth);  // Просим ввести пароль
  ReadLn(password); // И записываем его в password
  // Сравнение введенного пароля и расшифрованного серийника
  if password = str then Print(registered)
                    else Print(hellohacker);
  WriteLn;
  Print(exit);
  Readln;
end.

Расшифровка строк происходит в функции DecryptString. Заметь - всего 5 ассемблерных команд. Смотри комментарии в коде. В основном коде программы вызовы Write и WriteLn заменены на Print. Тут надо чуть поподробнее.

Функция получает строку и в своем теле делает уникальную копию этой строки с помощью UniqueString. Это сделано для того чтобы быть уверенным, что для строки будет выделена отдельная область памяти, а не просто увеличился счетчик ссылок (подробности читай в моей статье «String. То, чего ты мог и не знать.» в разделе про AnsiString) Мы ведь собираемся ассемблером над строкой издеваться, а Delphi об этом не знает и будет большой облом. Кстати попробуй закомментировать строку с UniqueString – у меня программа закрывалась, даже не пекнув.

Дальше Print вызывает DecryptString и в созданной копии строки расшифровывает каждый байт с помощью ROR. Затем уже строка выводится на экран: Write(str), а при выходе из функции расшифрованная строка прибивается Delphi, как и любая другая локальная переменная. Поэтому строчка нигде больше не светится.

Вот собственно и все. Hex-редактором строки просто так уже и не найдешь, они растворились где-то в коде. Осталось скормить дизассмеблеру – и тут тоже молчок. Только какие-то служебные Дельфиньи строки светятся. Можно считать, что цель достигнута.

Конечно, сделанный тут крякмис очень легко ломается в отладчике. Ведь в нем можно подсмотреть уже расшифрованное значение серийника. А в дизассемблере можно скорректировать результат сравнения серийника и введенного пароля, или проанализировать расшифровщик и восстановить все строки. А еще … Да полно способов в общем. Но никто и не говорил, что мы тут сделаем панацею от всех бед. Это всего лишь один из довольно простых методов повышающий общую стойкость защиты. А против отладчика есть антиотладочные приемы.

Исходники компилились в Delphi 7. Удачи.
Орехов Роман also known as tripsin
tripsin@yandex.ru
http://tripsin.narod.ru

Hosted by uCoz