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

Вступление

   Открою вам одну вещь - я с другой стороны баррикад. Я занимаюсь 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 - Физическое смещение начала секции относительно начала EXE файла.
   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. Кто не ленивый, напишет свой генератор правильных серийных номеров для этого приложения.

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


   Обратите внимание - я вынес в отдельные процедуры функции выдачи сообщений и эти процедуры не являются методами класса 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
  


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

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


   Компилируем. Открываем 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 во встроенном отладчике точку останова на строке.
 


   Когда она сработает, откроем окно с командами процессора. (Для его вызова нужно нажать 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. Глобальные и локальные переменные. Влияние на косвенную адресацию

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


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


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

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

004019EF: EB04 jmps .0004019F5

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


 Я уже предупреждал - этот способ сложнее. Предвижу вопрос - почему увеличилось значение адреса возврата на 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 соответственно). Путем исследования можно найти где идет проверка и с минимальными усилиями нейтрализовать ее либо сделать так, что будет возвращаться необходимый крекеру результат.
   Для усложнения взлома воспользуемся применением косвенной адресации. Напишем тестовое приложение, в котором будет проверка пароля. Пароль шифроваться не будет, поскольку нам важен сам механизм скрытого перехода. Вот текст главного модуля:
  


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

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


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

Дата: 03.07.2008, Автор: Thrasher.
Оригинал статьи в формате Word (97 Кб): indirect-addressing.zip



    No results found.
Отменить.