Developer vs Cracker: Косвенная адресация

Источник: delphisources

Открою вам одну вещь - я с другой стороны баррикад. Я занимаюсь reversing engineering. Это означает, что мне не чуждо копание в чужом коде, причем не в исходниках, а в уже откомпилированном. Но поскольку я занимаюсь этим только ради образовательных целей (я не зарелизил ни одного крэка), то я решил поделиться с вами той информацией, которая поможет усложнить взлом ваших творений.

Как говориться - врага нужно знать в лицо, поэтому знание ассемблера в данном случае просто необходимо. Конечно писать целиком приложение на ассемблере мы не будем, но знать, что какая команда делает нужно. Также необходимо наличие таких инструментов как дизассемблер (подойдет WinDASM), редактор РЕ-файлов (PEBrowser, можно взять и мой), редактор двоичных файлов (Hiew, WinHex) ну и конечно же отладчик OllyDebugger. Все вышеперечисленные инструменты можно бесплатно скачать и бесплатно пользоваться. Также необходимо знать устройство РЕ-файлов, а также уметь извлекать информацию из map-файла.

Часть 1. PE-Файлы

Я не буду глубоко копаться в формате РЕ-файлов, поскольку это не является основной задачей статьи. Если кому-то понадобятся эти знания, то на просторах Интернета есть огромное количество материала, посвященного описанию данного формата. Я лишь ограничусь описанием тех моментов, которые пригодятся в дальнейшем.

РЕ-файлы состоят из так называемых секций. Каждая секция имеет свое предназначение - в ней может находиться код, ресурсы, таблица импорта, данные пользователя, короче всё что угодно. В отличии от NE-файлов, РЕ-файлы совершенно по-другому загружаются в память. Дело в том, что РЕ-файл можно загружать по разным адресам, т.е начало файла может быть расположено там, где это удобно системе (хотя некоторые компиляторы, делают так, чтобы РЕ-файл грузился именно туда, куда ему указали). Но суть не в этом.

Для более толкового понятия введем такие понятия:

Image Base - адрес в памяти с которого начинается РЕ-файл. Обычно равен 0400000h.

Relocated Virtual Adress RVA- адрес в памяти относительно Image Base.

Virtual Adress VA- адрес в памяти с учетом Image Base.

Image Base - прописан в заголовке РЕ-файла. Когда системный загрузчик загружает файл в память, он считывает значение Image Base, а потом выделяет необходимое количество памяти для загружаемого файла. Адресация в этом участке памяти будет начинаться как раз с ImageBase.

Как было сказано выше, РЕ-файл состоит из секций. Каждая из секций имеет такие параметры:

Object Name - Имя объекта. (Имя секции. )

Virtual Size - виртуальный размер секции, именно столько памяти будет отведено под секцию

Section RVA - размещение секции в памяти, виртуальный ее адрес относительно Image Base.

Physical Size - размер секции (ее инициализированной части) в файле,

Physical Offset -

Object Flags - битовые флаги секции.

Теперь немножечко подробнее. Object Name - имя секции. Вообще может быть любым, но большинство компиляторов дают секциям выразительные имена. Например CODE, DATA, .rsrc. Только посмотрев на них сразу понятно - что содержится в данной секции.

Virtual Size - Количество памяти, которое отводится для секции. Т.е секция на диске может занимать меньше места, чем в памяти. Яркий пример этому программы запакованные каким-нибудь упаковщиком, например UPX.

Physical Size - Размер секции в файле. Должен быть меньше или равен Virtual Size.

Physical Offset - физическое смещение начала секции относительно начала EXE файла.

Object Flags - битовые флаги. Определяют какие операции можно делать с секцией

  • 00000004h - используется для кода с 16 битными смещениями
  • 00000020h - секция кода
  • 00000040h - секция инициализированных данных
  • 00000080h - секция неинициализированных данных
  • 00000200h - комментарии или любой другой тип информации
  • 00000400h - оверлейная секция
  • 00000800h - не будет являться частью образа программы
  • 00001000h - общие данные
  • 00500000h - выравнивание по умолчанию, если не указано иное
  • 02000000h - может быть выгружен из памяти
  • 04000000h - не кэшируется
  • 08000000h - не подвергается страничному преобразованию
  • 10000000h - разделяемый
  • 20000000h - выполнимый
  • 40000000h - можно читать
  • 80000000h - можно писать

Логически предположить, что для секции кода значение Object Flags будет равно:

20000000h +40000000h+00000020h= 60000020 h

Для секции ресурсов значение Object Flags будет равно :

40000000h + 10000000h + 00000040h = 50000040h

Часть 2. Map-файл

Для тех кто не знает - map-файл нужен для отладки приложения под сторонними отладчиками. В этом файле содержится информация о глобальных переменных, адреса процедур и т.д. Выставить опцию генерации детального map-файла нужно в настройках проекта (см. рисунок).

Настройки проекта

Теперь скомпилируем приложение. И посмотрим на наш map-файл. Сначала идет таблицасегментов, которые присутствуют в скомпилированном приложении:

 Start         Length     Name                   Class

 0001:00000000 0004F5C4H .text                   CODE

 0002:00000000 00001170H .data                   DATA

 0002:00001170 00000BD9H .bss                    BSS

Итак что есть что.

Start - адрес начала сегмента. (Не путать с секцией! Подробнее - ниже.)

Length - виртуальный размер секции. (Количество памяти, которое отводится для сегмента)

Name - имя секции .

Class - тип секции.

Самое интересное, что имя секции в исполняемом файле не совпадает с именем секции, которое указано в map-файле, а совпадает с типом секции.

 В столбце Start описывается префикс и смещение относительно начала сегмента. Сегмент - это данные, которые объединяются по типу их обработки. Тип сегмента описывается префиксом и принимает следующие значения: 0001 - это сегмент исполняемого кода, 0002 - это сегмент данных. В сегменте могут размещаться несколько секций. Смещение указывается относительно начала сегмента. Как видно из таблицы, сегмент исполняемого кода состоит из одной секции, а сегмент данных из двух. Смещение отсчитывается от начала сегмента, а начало сегмента - это начало первой по расположению секции, из которых этот сегмент состоит.

Нам важен сегмент с исполняемым кодом, поэтому другие мы попросту рассматривать не будем. Да, еще такой момент - адресация в map-файле идет относительно начала сегмента.

Далее в map-файле располагается информация о подключенных модулях:

Detailed map of segments

 0001:00000000 00004B03 C=CODE     S=.text    G=(none)   M=System   ACBP=A9

 0001:00004B04 00000140 C=CODE     S=.text    G=(none)   M=SysInit  ACBP=A9

 0001:00004C44 00000078 C=CODE     S=.text    G=(none)   M=Types    ACBP=A9

 0001:00004CBC 00000BFC C=CODE     S=.text    G=(none)   M=Windows  ACBP=A9

 0001:000058B8 00000038 C=CODE     S=.text    G=(none)   M=Messages ACBP=A9

 0001:000058F0 00000310 C=CODE     S=.text    G=(none)   M=SysConst ACBP=A9

 0001:00005C00 00006378 C=CODE     S=.text    G=(none)   M=SysUtils ACBP=A9

 0001:0000BF78 000007FB C=CODE     S=.text    G=(none)   M=VarUtils ACBP=A9

 0001:0000C774 00002A42 C=CODE     S=.text    G=(none)   M=Variants ACBP=A9

 ... ... ...

После идет информация о размещении процедур, функций, обработчиков, методов класса.

 Address         Publics by Name

 0001:000449F4       DoneApplication

 0001:000398E4       DoneControls

 0001:0000A7B8       DoneExceptions

 0001:00049010       DoNestedActivation

 0001:00017D38       DoneThreadSynchronization

 0001:0004BDE8       DoPosition

 ... ... ...

 0001:0004EA10       TApplication.ActivateHint

 0001:0004D664       TApplication.BringToFront

 0001:0004E778       TApplication.CancelHint

 0001:0004CC48       TApplication.CheckIniChange

 0001:0004C9C4       TApplication.ControlDestroyed

 0001:0004C50C       TApplication.Create

 0001:0004DA64       TApplication.CreateForm

 0001:0004C814       TApplication.CreateHandle

 0001:0004C6F8       TApplication.Destroy

 ... ... ...

 0001:00023BBC       TTimer.Create

 0001:00023C0C       TTimer.Destroy

 0001:00023C44       TTimer.WndProc

 0001:00023CB8       TTimer.UpdateTimer

 0001:00023D44       TTimer.SetEnabled

 0001:00023D54       TTimer.SetInterval

 0001:00023D64       TTimer.SetOnTimer

 0001:00023D7C       TTimer.Timer

 ... ... ...

Далее идет информация о расположении глобальных переменных, адресах системных функций и классов

Address         Publics by Value

 0002:FFBAF010       TlsLast

 0001:000001F4       GetStdHandle

 0001:000001FC       RaiseException

 0001:00000204       RtlUnwind

 0001:0000020C       UnhandledExceptionFilter

 0001:00000214       WriteFile

 0001:0000021C       CharNext

 0001:00000224       ExitProcess

 0001:0000022C       MessageBox

 0001:00000234       FindClose

 0001:0000023C       FindFirstFile

 0001:00000244       FreeLibrary

 0001:0000024C       GetCommandLine

 0001:00000254       GetLocaleInfo

 0001:0000025C       GetModuleFileName

 0001:00000264       GetModuleHandle

 0001:0000026C       GetProcAddress

... ... ...

 0001:00013144       TStrings.Error

 0001:0001317C       TStrings.Error

 0001:000131D4       TStrings.Exchange

 0001:000132BC       TStrings.GetCapacity

 0001:000132C4       TStrings.GetObject

 0001:000132C8       TStrings.GetText

 0001:0001331C       TStrings.GetTextStr

 0001:00013444       TStrings.IndexOf

 0001:000134D0       TStrings.IndexOfName

 0001:000135A4       TStrings.IndexOfObject

... ... ...

В конце содержится информация соответствия каждой строки кода адресу в откомпилированном коде для каждого модуля:

Line numbers for MainF(MainF.pas) segment .text

    35 0001:0004F1D4    36 0001:0004F1EE    37 0001:0004F1F2    38 0001:0004F205

    39 0001:0004F218    40 0001:0004F222    41 0001:0004F224    42 0001:0004F23A

    43 0001:0004F248    42 0001:0004F257    44 0001:0004F25B    45 0001:0004F288

    49 0001:0004F290    50 0001:0004F2AC    54 0001:0004F2D8    55 0001:0004F2F4

    56 0001:0004F300    60 0001:0004F324    62 0001:0004F32D    63 0001:0004F332

    65 0001:0004F333    66 0001:0004F338    67 0001:0004F36C    67 0001:0004F373

Line numbers for IndirectA(C:\Program Files\Borland\Delphi7\Projects\indirect addressing\IndirectA.dpr) segment .text

     9 0001:0004F57C    10 0001:0004F58C    11 0001:0004F598    12 0001:0004F5B0

    13 0001:0004F5BC

Какие ресурсы были подлинкованы:

Bound resource files

c:\program files\borland\delphi7\Lib\Buttons.res

c:\program files\borland\delphi7\Lib\ExtDlgs.res

c:\program files\borland\delphi7\Lib\Controls.res

MainF.dfm

IndirectA.res

IndirectA.drf

И точка входа в программу:

Program entry point at 0001:0004F57C.

Возникает вполне нормальный вопрос, а  как найти адрес нужной нам процедуры? Все очень просто - открываем в редакторе РЕ-файлов наш откомпилированный проект, смотрим на значение ImageBase и Виртуальное смещение (RVA) секции кода:

PE Editor

Далее открываем map-файл проекта, и ищем расположение (адрес) необходимой нам функции (например Button1Click):

0001:0004F324       TMainForm.Button1Click

Дальше вычисляем адрес данной функции: 04F324 (Это адрес из map-файла)+01000 (А это адрес начала секции с кодом в РЕ-файле)+0400000 (Это ImageBase)= 0450324. Теперь мы можем узнавать адреса необходимых нам функций. И теперь пора переходить от теории к практике.

Часть 3. Косвенная адресация

Для чего нужна косвенная адресация? И вообще что это такое. Для более глубокого понимания напишем тестовое приложение, в котором будет проводиться проверка правильного серийного номера. Алгоритм генерации серийного номера выберем максимально простой: сначала поднимем в верхний регистр все символы имени, а потом сложим их значение. В случае неправильного ввода будет выдано сообщение и программа завершит работу. В качестве пары имя/серийный номер я выбрал Thrasher/609. Кто не ленивый, напишет свой генератор правильных серийных номеров для этого приложения.

Текст модуля главной формы:

unit MainF;

interface

uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

  Dialogs, StdCtrls;

type

  TMainForm = class(TForm)

    Button1: TButton;

    Button2: TButton;

    Edit1: TEdit;

    Edit2: TEdit;

    Label1: TLabel;

    Label2: TLabel;

    procedure Button1Click(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

  end;

var

  MainForm: TMainForm;

implementation

{$R *.dfm}

function CheckSerial:boolean;

 Var SN_num,SN_num2:integer;

     SN,Name:string;

     I:byte;

 begin

  Result:=false;

  Name:=MainForm.Edit1.Text;

  SN:=MainForm.Edit2.Text;

  SN_num:=StrToInt(SN);

  SN_num2:=0;

  Name:=UpperCase(Name);

  For I:=1 to Length(Name) do

    SN_num2:=SN_num2+Ord(Name[i]);

  If SN_num=SN_num2 then Result:=true;

 end;

 procedure GoodCheck;

   begin

    messageBox(Application.Handle,'Проверка прошла удачно.','Поздравления!',0);

   end;

 procedure BedCheck;

   begin

    messageBox(Application.Handle,'Проверка не прошла.','Неудача!',0);

    Application.Terminate;

   end;

procedure TMainForm.Button1Click(Sender: TObject);

begin

if CheckSerial=true then

 begin

  GoodCheck;

  Exit;

 end;

BedCheck;

end;

 

end.

Обратите внимание - я вынес в отдельные процедуры функции выдачи сообщений и эти процедуры не являются методами класса TMainForm. Да, чуть не забыл, нужно включить режим создания детального map-файла. Теперь дизассемблируем полученный ЕХЕ (я использую WinDASM). Пока файл дизассемблируется, найдем необходимые нам адреса процедур GoodCheck и BedCheck:

Для GoodCheck - 0450290.

Для BedCheck - 04502D8.

Теперь посмотрим  на листинг, что сгенерировал дизассемблер, начиная с указанных адресов:

* Referenced by a CALL at Address:

/:0045032D   <= Это адрес откуда вызывалась данная процедура

/

:00450290 6A00                    push 00000000

:00450292 68B0024500              push 004502B0

:00450297 68C0024500              push 004502C0

:0045029C A108204500              mov eax, dword ptr [00452008]

:004502A1 8B00                    mov eax, dword ptr [eax]

:004502A3 8B4030                  mov eax, dword ptr [eax+30]

:004502A6 50                      push eax

* Reference To: user32.MessageBoxA, Ord:0000h

                                  /

:004502A7 E8EC61FBFF              Call 00406498

:004502AC C3                      ret

Перейдем на адрес откуда вызывалась процедура GoodCheck.

:00450329 3C01                    cmp al, 01

:0045032B 7506                    jne 00450333

:0045032D E85EFFFFFF              call 00450290 <= А тут происходит вызов

:00450332 C3                      ret

Данный тип адресации процедур называется прямым - т.е. когда адрес перехода указывается конкретно. Чем это хорошо - для отладки. Чем плохо - можно быстро вычислить место вызова и подправить. Поэтому для усложнения нахождения нужно использовать косвенную адресацию.

Немного перепишем наше приложение.

unit MainF;

interface

uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

  Dialogs, StdCtrls;

type

  TMainForm = class(TForm)

    Button1: TButton;

    Button2: TButton;

    Edit1: TEdit;

    Edit2: TEdit;

    Label1: TLabel;

    Label2: TLabel;

    procedure Button1Click(Sender: TObject);

    procedure FormCreate(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

  end;

 

var

  MainForm: TMainForm;

  GoodCheckAddress,BedCheckAddress:LongInt; <= Переменные для адреса процедур

 

implementation

{$R *.dfm}

function CheckSerial:boolean;

 Var SN_num,SN_num2:integer;

     SN,Name:string;

     I:byte;

 begin

  Result:=false;

  Name:=MainForm.Edit1.Text;

  SN:=MainForm.Edit2.Text;

  SN_num:=StrToInt(SN);

  SN_num2:=0;

  Name:=UpperCase(Name);

  For I:=1 to Length(Name) do

    SN_num2:=SN_num2+Ord(Name[i]);

  If SN_num=SN_num2 then Result:=true;

 end;

 procedure GoodCheck;

   begin

    messageBox(Application.Handle,'Проверка прошла удачно.','Поздравления!',0);

   end;

 procedure BedCheck;

   begin

    messageBox(Application.Handle,'Проверка не прошла.','Неудача!',0);

    Application.Terminate;

   end;

procedure TMainForm.Button1Click(Sender: TObject);

begin

if CheckSerial=true then

 begin

  asm

  call GoodCheckAddress <= вызываем GoodCheck

  end;

  Exit;

 end;

 asm

 call BedCheckAddress asm <= вызываем BedCheck

 end;

end;

procedure TMainForm.FormCreate(Sender: TObject);

begin

asm <= заносим в GoodCheckAddress адрес GoodCheck

push eax

mov eax, offset GoodCheck

mov GoodCheckAddress,eax

pop eax

end;

asm <= заносим в BedCheckAddress адрес BedCheck

push eax

mov eax, offset BedCheck

mov BedCheckAddress,eax

pop eax

end;

 

end;

 

end.

Компилируем. Открываем map-файл и ищем адреса процедуры GoodCheck. Это будет 045029F. Снова пропускаем файл через дизассемблер, переходим на адрес откуда начинается GoodCheck

:0045029F 90                      nop

:004502A0 6A00                    push 00000000

:004502A2 68C0024500              push 004502C0

:004502A7 68D0024500              push 004502D0

:004502AC A108204500              mov eax, dword ptr [00452008]

:004502B1 8B00                    mov eax, dword ptr [eax]

:004502B3 8B4030                  mov eax, dword ptr [eax+30]

:004502B6 50                      push eax

* Reference To: user32.MessageBoxA, Ord:0000h

                                  /

:004502B7 E8DC61FBFF              Call 00406498

:004502BC C3                      ret

Сразу бросается в глаза тот факт, что не видно откуда вызывается данная процедура. Ставим в  Delphi во встроенном отладчике точку останова на строке

procedure TMainForm.Button1Click(Sender: TObject);

begin

if CheckSerial=true then

 begin

  asm

  call GoodCheckAddress<= тут точка останова

  end;

  Exit;

 end;

 asm

 call BedCheckAddress asm <= вызываем BedCheck

 end;

end;

Когда она сработает, откроем окно с командами процессора. (Для его вызова нужно нажать Alt+C либо выбрать в меню View->Debug Window->CPU). Смотрим адрес вызова процедуры GoodCheck.

0045033D FF15D43B4500     call dword ptr [GoodCheckAddress]

Переходим на данный адрес в дизассемблерном листинге:

:00450339 3C01                    cmp al, 01

:0045033B 7507                    jne 00450344

:0045033D FF15D43B4500            call dword ptr [00453BD4] <= Вызов GoodCheck

:00450343 C3                      ret

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

Часть 4. Глобальные и локальные переменные. Влияние на косвенную адресацию

Как вы могли заметить, я хранил адреса процедур в глобальных переменных. А как измениться код вызова, если для этих целей использовать локальные переменные. Изменим наш код:

unit MainF;

interface

 

uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

  Dialogs, StdCtrls;

type

  TMainForm = class(TForm)

    Button1: TButton;

    Button2: TButton;

    Edit1: TEdit;

    Edit2: TEdit;

    Label1: TLabel;

    Label2: TLabel;

    procedure Button1Click(Sender: TObject);

    procedure FormCreate(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

  end;

var

  MainForm: TMainForm;

implementation

{$R *.dfm}

function CheckSerial:boolean;

 Var SN_num,SN_num2:integer;

     SN,Name:string;

     I:byte;

 begin

  Result:=false;

  Name:=MainForm.Edit1.Text;

  SN:=MainForm.Edit2.Text;

  SN_num:=StrToInt(SN);

  SN_num2:=0;

  Name:=UpperCase(Name);

  For I:=1 to Length(Name) do

    SN_num2:=SN_num2+Ord(Name[i]);

  If SN_num=SN_num2 then Result:=true;

 end;

 procedure GoodCheck;

   begin

    messageBox(Application.Handle,'Проверка прошла удачно.','Поздравления!',0);

   end;

 procedure BedCheck;

   begin

    messageBox(Application.Handle,'Проверка не прошла.','Неудача!',0);

    Application.Terminate;

   end;

procedure TMainForm.Button1Click(Sender: TObject);

Var

  GoodCheckAddress,BedCheckAddress:LongInt;

begin

asm

push eax

mov eax, offset GoodCheck

mov GoodCheckAddress,eax

pop eax

end;

asm

push eax

mov eax, offset BedCheck

mov BedCheckAddress,eax

pop eax

end;

if CheckSerial=true then

 begin

  asm

  call GoodCheckAddress

  end;

  Exit;

 end;

 asm

 call BedCheckAddress

 end;

end;

 

end.

Скомпилируем и поставим точку останова на строке

begin

  asm

  call GoodCheckAddress<= тут точка останова

  end;

  Exit;

 end;

 

 asm

 call BedCheckAddress

 end;

end;

После срабатывания точки останова вызываем окно процессорных команд:

Debugger

Обратите внимание, насколько поменялся вызов:

При использовании локальной переменной:

:00450357 FF55FC           call dword ptr [ebp-$04]

При использовании гловальной переменной :

:0045033D FF15D43B4500     call dword ptr [00453BD4]

Как нетрудно заметить, в случае использования локальных переменных, определить адрес процедуры становиться сложнее. А если скомбинировать эти два способа, то определить что куда вызывалось еще сложнее.

Часть 5. Избавление от "хвостов" или адрес возврата своими руками

Опять немного теории. В ассемблере все подпрограммы вызываются при помощи команды call. Вот пример вызова:

push eax

call SomeThing

mov ebx,eax

inc ebx

Что же делает команда call? Во-первых помещает адрес возврата в стек, а во-вторых совершает переход на указанную метку. Т.е. фактически это есть безусловный переход, только с возвращением на последующую поcле call команду. Осталось дело за малым - узнать адрес возврата. Оказывается - это не так уж и сложно. Дело в том, что адресация в ассемблере относительная, а не абсолютная. Это означает, что переходы при прямой адресации осуществляются относительно команды перехода. Т.е

Такая конструкция как

004019EF: EB04                         jmps       .0004019F5

означает не прямой переход на адрес 4019F5, а переход на инструкцию, адрес которой больше на 4 адреса команды перехода. Поэтому можно немного "пошаманить". Предупреждаю - этот способ сложнее, но хочется почувствовать себя немного хакером! Изменим код обработчика на следующий:

procedure TMainForm.Button1Click(Sender: TObject);

Var

  GoodCheckAddress,BedCheckAddress:LongInt;

begin

asm

push eax

mov eax, offset GoodCheck

mov GoodCheckAddress,eax

pop eax

end;

asm

push eax

mov eax, offset BedCheck

mov BedCheckAddress,eax

pop eax

end;

if CheckSerial=true then

 begin

  asm

  push eax             ;сохраняем значение регистра eax

  call @1              ;узнаем адрес следующей за call команды

  @1:                  ;он будет на верхушке стека

  pop eax              ;выталкиваем из стека адрес в регистр eax

  add eax,8;           ;увеличиваем значение адреса на 8 (ниже объясню)

  push eax             ;проталкиваем в стек значение адреса возврата

  jmp GoodCheckAddress ;прыгаем на начало функции GoodCheck

  pop eax             ;восстанавливаем значение eax

end;

  Exit;

 end;

 asm

  push eax

  call @1

  @1:

  pop eax

  add eax,8

  push eax

  jmp GoodCheckAddress

  pop eax

 end;

end;

 

Я же предупреждал - этот способ сложнее. Предвижу вопрос - почему увеличилось значение адреса возврата на 8. Все очень просто - это длина команд между call @1 и pop eax. Дело в том, что каждая команда имеет определенную длину в байтах. Узнать эти длины можно из отладчика, а можно просто вычислить разницу между адресами команд.

Умело применив вышеописанный способ можно добиться весьма сложной системы перехода из одного места программы в другой, что может сильно затруднить взлом.

Часть 6. Скрытый переход

При написании програм с испытательным сроком или с проверкой серийного номера неизбежно используется конструкция:

If EnteredSerialNumber<>ValidSerialNumber  then

                       ErrorMessageProc  else  CongratulationProc

Или такая контсрукция:

if  Registration<>true then

                        ErrorMessageProc  else  CongratulationProc

Встречаются и другие конструкции, типа

Registration:=RegistrationProc;

Messsage(Registration); //В зависимости от значения переменной Registration  

                        //будут выполнятся различные действия

Недостатком таких конструкций перед взломом есть то, что явно видно место проверки (в 2 первых конструкциях) или явное присваивание (в последней). Дело в том, что все операции результатом которых есть булевая переменная, а сравнение строк относятся к данному типу операций, в ассемблере выглядят одинаково, а именно

call StringCompare

test al,al

jz Failed

В случае третьей конструкции, функция RegistrationProc возвращает в регистр eax 1 либо 0 (True или False соответственно). Путем исследования можно найти где идет проверка и с минимальными усилиями нейтрализовать ее либо сделать так, что будет возвращаться необходимый крекеру результат.

Для усложнения взлома воспользуемся применением косвенной адресации. Напишем тестовое приложение, в котором будет проверка пароля. Пароль шифроваться не будет, поскольку нам важен сам механизм скрытого перехода. Вот текст главного модуля:

unit Unit1;

interface

uses

  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,

  Dialogs, StdCtrls;

type

  TForm1 = class(TForm)

    Button1: TButton;

    Edit1: TEdit;

    Label1: TLabel;

    procedure Button1Click(Sender: TObject);

  private

    { Private declarations }

  public

    { Public declarations }

  end;

var

  Form1: TForm1;

  ResultAddress:LongInt;

  Passed:boolean;

const

 password='simsim';

implementation

{$R *.dfm}

procedure Good;

 begin

  MessageBox(Application.Handle,'PASSWORD PASSED!','Congratulation',MB_OK);

 end;

procedure Bad;

 begin

  MessageBox(Application.Handle,'PASSWORD FAILED.','Error',MB_OK);

 end;

Procedure CheckPassword;

 Var psw:String;

 begin

  psw:=Form1.Edit1.Text;

  Passed:=False;

  if psw=password then Passed:=true;

  asm

  pushad                           ; сохранить значение всех регистров в стек

  mov al,passed                    ; занести в al значение passed

  test al,al                       ; сравнить - если

  jz @no                           ; false, тогда перейти на метку @no

  mov eax, offset Good             ; поместить в eax адрес процедуры Good

  mov ResultAddress, eax           ; поместить в ResultAddress значение eax

  jmp @finish                      ; перейти в конец процедуры

  @no:

  mov eax, offset Bad              ; поместить в eax адрес процедуры Bad

  mov ResultAddress, eax           ; поместить в ResultAddress значение eax

  @finish:

  popad                            ; восстановить значение всех регистров

  end;

 end;

procedure TForm1.Button1Click(Sender: TObject);

Var Addr:LongInt;

begin

CheckPassword;

Addr:=ResultAddress;

asm

 call Addr

end;

end;

 

end.

Глобальной переменная ResultAddress используеться только для передачи значения адреса процедур, этот прием конечно усложнит жизнь взломщику, поскольку придется анализировать весь код процедуры CheckPassword. Если использовать таблицы косвенных переходов или генератор случайных чисел для вызова процедур проверки (например, таких процедур будет 10 и вызываться они будут в случайном порядке из одного и того же места), то взломать такую программу будет весьма трудно.

Как вариант можно использовать такую конструкцию:

 

procedure TForm1.Button1Click(Sender: TObject);

Var Addr:LongInt;

 

procedure CheckPasswordLocal;

  begin

   CheckPassword;

   Addr:=ResultAddress;

  end;

 

 begin

  CheckPasswordLocal;

   asm

    call Addr

   end;

 end;

 

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=20417