Введение в крэкинг с нуля, используя OllyDbg - Глава 8

  • На форуме работает ручное одобрение пользователей. Это значит, что, если Ваша причина регистрации не соответствует тематике форума, а также Вы используете временную почту, Ваша учётная запись будет отклонена без возможности повторной регистрации. В дальнейшем - пожизненная блокировка обоих аккаунтов за создание мультиаккаунта.
  • Мы обновили Tor зеркало до v3!
    Для входа используйте следующий url: darkv3nw2...bzad.onion/
  • Мы вновь вернули telegram чат форуму, вступайте, общайтесь, задавайте любые вопросы как администрации, так и пользователям!
    Ссылка: https://t.me/chat_dark_time

AnGel

Администратор
Команда форума

AnGel

Администратор
Команда форума
27 Авг 2015
3,411
2,025
В этой главе мы рассмотрим ещё несколько базовых инструкций, которые остались без внимания в предыдущих главах. Как только мы их изучим, сможем, наконец, перейти непосредственно к крэкингу!

Циклы

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

MOV ECX,15h
Инициализируем счётчик повторов значением 15h. Далее следует сам цикл:

метка_начала_цикла:
DEC ECX
Счётчик уменьшается на 1 при каждом повторе.

Далее следуют произвольные инструкции, которые мы пытаемся зациклить - это тело цикла.

В конце нужно добавить проверку на равенство нулю или любому другому условию завершения цикла:

CMP ECX,0
JNE метка_начала_цикла
При первой проверке на равенство нулю, в счётчике будет значение 14h, ведь мы уже успели 1 раз выполнить DEC ECX. Т.к. 14h не равно 0, цикл повторится снова и снова, пока значение счётчика не достигнет значения 0.

Давайте перепишем наш цикл в более компактной форме с целью оптимизации по размеру:

XOR ECX,ECX
ADD ECX,15h
метка_начала_цикла:
DEC ECX
; тело цикла
TEST ECX,ECX
JNE метка_начала_цикла
Можно придумать ещё более компактные версии циклов, но пока ограничимся последней. Давайте впишем этот код в OllyDbg:

c167e84da970b25f8d22ee411813ab09.png

Жёлтым выделен код цикла, который будет повторяться пока значение ECX не достигнет нуля. Тело цикла содержит несколько инструкций NOP.

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

f1e66d0c66b64eb4fb32b6c0692b1d47.png

Ещё раз F7 - прибавляем к счётчику 15h, чем и задаём желаемое количество повторов.

a0588bd7530b01b985e9cb631ddea79e.png

Далее нажимаем на F7 пока не выполнится инструкция DEC ECX, которая сократит значение нашего счётчика до 14h.

Продолжаем трассировать до инструкции TEST ECX, ECX. Что делает эта инструкция в данном случае? - Правильно, проверяет значение счётчика на предмет равенства нулю.

cfddbf3b507b7226818baf97562ffb8f.png

Т.к. счётчик ещё не равен нулю, флаг Z не устанавливается и, следовательно, выполняется условный переход JNZ - он передаёт управление на адрес 401007. Далее счётчик снова уменьшается - на этот раз до 13h. Продолжаем трассировать пока он не достигнет значения 0.

При нулевом значении счётчика, сравнение на ноль устанавливает флаг Z. Поэтому JNZ более не выполняется.

1683875abcb277ab71e651d272a0c753.png

Помните, раньше мы использовали инструкцию JZ, которая осуществляла переход при установленном флаге Z? JNZ работает с точностью до наоборот - переход осуществляется при сброшенном флаге Z.

52af353442d9264858d1e5169bad217e.png

Видите серую стрелочку? - это значит, что переход не будет осуществлён. Повторное нажатие на F7 выводит нас из цикла.

39e3dbcd9df14b7f3d8643795891f7cf.png

Мы рассмотрели простейший пример цикла с использованием уже знакомых нам инструкций. Существуют ещё и специальные инструкции для реализации циклов. Давайте рассмотрим и их.

LOOP

Инструкция LOOP берёт на себя уменьшение значения счётчика ECX, проверку на равенство нулю и переход по заданному адресу - всё в одном флаконе. (К сожалению, на большинстве современных процессоров данная инструкция отличается более низкой скоростью, чем аналог с DEC и JNZ - прим. пер.)

4ef360823114f2d8eceab4c3351cf054.png

Подсвечиваем инструкцию DEC ECX - осуществляем правый клик мышью - Binary - Fill with NOPs. Аналогично поступаем с инструкциями TEST ECX,ECX и JNZ 401007. Все 3 инструкции можно заменить одной - LOOP 401007.

Подсвечиваем первую строчку (401000) - осуществляем правый клик мышью - New origin here. Теперь мы готовы опробовать наш новый цикл, использующий инструкцию LOOP.

Нажимаем F7 - снова видим как счётчик ECX сначала инициализируется значением 0, потом ему присваивается значение 15h. Продолжаем трассировать пока не достигнем инструкцию LOOP.

b1a010e55e1d2a23dbdcfc34f96b43a1.png

Также как и раньше, осуществляется переход на адрес 401007, т.к. счётчик ещё не равен нулю. Замечаем, что счётчик уменьшился на 1 - теперь в ECX хранится значение 14h.

c15ebf26070cd78ea335709c94809b99.png

Продолжаем трассировать. Когда счётчик, наконец, достигает нуля, цикл более не будет повторяться.

dd045c80d896c92aca6fd8d1af975374.png

Существуют некоторые вариации инструкции LOOP:

  • LOOPZ, LOOPE Цикл повторяется пока флаг Z установлен
  • LOOPNZ, LOOPNE Цикл повторяется пока флаг Z сброшен
Обе вариации работают как обычный LOOP, т.е. повторяют тело цикла пока счётчик не достигнет нуля, уменьшая на 1 значение счётчика при каждом повторе. В добавок, LOOPZ и LOOPNZ ещё проверяют значение флага Z и прерывают цикл если Z сброшен или установлен соответственно. Таким образом, цикл можно прервать досрочно (до того как ECX достигнет нуля), используя флаг Z.

Обратите внимание на поведение цикла в том случае, если счётчик изначально равен нулю. - прим. пер.

Цепочечные инструкции

Далее рассмотрим основные инструкции, относящиеся к категории цепочечных, т.е. позволяющие обрабатывать сразу цепочки байт.

MOVS

Данная инструкция копирует данные из одного адреса в другой. Адрес источника хранится в регистре ESI, адрес приёмника - в EDI. Инструкция не нуждается в явном указании параметров, но OllyDbg дизассемблирует её как MOVS DWORD PTR ES:[EDI],DWORD PTR DS:[ESI] для наглядности. Вот как это выглядит:

f84977fff556ed803920bb7db199711b.png

Предварительно инициализируем ESI адресом ячейки, содержимое которой нужно скопировать (источник), и EDI - адресом ячейки, в которую нужно осуществить копирование (проёмник).

Давайте посмотрим содержимое обоих ячеек в окне дампа непосредственно перед копированием.

В окне дампа можно задать Go to - Expression = 40366C. Есть более простой способ: Follow in dump - Immediate constant, как показано ниже:

0bb2c9f664205f12d7d0e4e51c728eb5.png

b0753efc26beae2ed66b20edd332a985.png

В дампе выделено содержимое ячейки, адрес которой указан в источнике (ESI).

А теперь посмотрим куда указывает EDI:

87ae519faa467353c235093608c378a9.png

Это приёмник - сюда будет скопировано содержимое ячейки-источника.

Нажимаем F7 пока не выполнится MOVS - в приёмнике теперь находятся те самые 4 байта.

6b216ffa101a74cc90d24a080c40cc64.png



MOVS копирует 4x-байтные значения, т.е. DWORD, из-за чего этой инструкции соответствует ещё одна мнемоника - MOVSD. Существуют ещё инструкции MOVSW и MOVSB, копирующие 2 байта (WORD) и 1 байт (BYTE) соответственно.

Обратите внимание как изменяются значения ESI и EDI при копировании, в зависимости от флага D - прим. пер.

REP

Это префикс, который можно использовать перед некоторыми инструкциями, в частности MOVS. Данный префикс указывает, что текущую инструкцию нужно выполнить ECX количество раз. Фактически счётчик ECX уменьшается при каждом повторе, как и в обычном цикле. Таким образом, REP MOVS копирует уже не строго 4 байта, а массив размером 4 * ECX байт, ведь каждый раз указатели ESI и EDI сами увеличиваются на 4 (или уменьшаются, в зависимости от флага D - прим. пер). Полезная фишка, не так ли?

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

Давайте немного подправим наш предыдущий пример: добавим префикс REP и проинициализируем ECX.

4bec6386c6abb6787230339817f07179.png

Адрес источника теперь будет 40365C (всего лишь для наглядности), а приёмника - тот же, что и раньше, т.е. 40369C. Возвращаемся к первой строчке кода дежурным New origin here и трассируем до REP MOVS.

05baedf445a8eb5553a31cf76495e6b2.png

Обратите внимание на подсказку в OllyDbg - там отображаются адреса источника и проёмника, содержимое начальных ячеек и другая полезная информация. Нажимаем F7.

acdd106d7422d09f840df0954774f040.png

Как видите, первые 4 байта были успешно скопированы, но мы всё ещё находимся на REP, ведь цикл должен выполняться пока ECX не достигнет нуля. На данном этапе в ECX уже хранится значение 3, а указатели ESI и EDI уже увеличились на 4, указывая таким образом на следующие 4 байта, которые будут скопированы как только мы снова нажмём F7.

6a63a012fb1eaf54e5b643958f2131a0.png

Нажимаем F7.

dccb7ea670db2613739a6e9365bef69d.png

Вот и следующие 4 байта были удачно перемещены и счётчик ECX уже равен 2.

2db52a7f822d2fcc65188437aa2cd572.png

Ещё раз F7 - ECX равен 1.

018226b0e0847cf12e420ee47088ec38.png

И ещё раз.

5f16f69ffc7706d8750fce50cb418f23.png

Всё, цикл завершился, т.к. счётчик достиг нуля.

58ccd0493d4d92595af8b8d0070cf5ea.png

Подведём итог: инструкция MOVS выполнилась 4 раза, благодаря префиксу REP, тем самым переместив цепочку из 16 байт из одного массива в другой.

Имейте в виду, что MOVS, также как и обычная MOV, не может записать данные по адресу, принадлежащему странице не имеющей прав на запись. В таком случае будет вызвано исключение.

Кроме REP есть ещё 2 производных префикса REPE/REPZ и REPNE/REPNZ, которые позволяют прервать цикл досрочно по флагу Z. Т.к. MOVS никак не влияет на флаг Z, использовать REPE/REPZ или REPNE/REPNZ совместно с MOVS не имеет особого смысла, но есть и другие инструкции, которые также поддерживают префиксы повторения и, в добавок, влияют на флаги. Например, CMPS и SCAS являются таковыми. Одну из них мы рассмотрим чуть позже.

LODS

Данная инструкция загружает (от англ. LOAD) данные из источника (куда, как обычно, указывает ESI) в аккумулятор.

6695d6b78ab27852523c74d7d9595006.png

OllyDbg в своём репертуаре: дизассемблирует LODS как LODS DWORD PTR DS:[ESI].

Если кому-то лень посмотреть на значение ESI на панели регистров, OllyDbg заботливо дублирует его в окне подсказок.

e9392c18b8c52679b4b0ec76cbdf909d.png

Что у нас в дампе?

00c24c4c46aac8529d9873cb2b2b7749.png

Эти 4 байта будут загружены в аккумулятор (EAX). Нажимаем F7.

03d212b81a53a78b73053b825a78ed81.png

Свершилось! EAX действительно содержит те самые 4 байта.

cb27fd6ef740c16bb97c478f0a7bbb3f.png

Префикс REP можно применить и перед инструкцией LODS для автоповтора, пока счётчик ECX не достигнет нуля.

При выполнении цикла REP LODS, в окне подсказки оказываются текущие значения ECX, ESI и содержимое ячеек памяти, которые будут загружены в аккумулятор при следующем повторе.

bd62289f9c6ee49a8d8976b7f5a85f27.png



Нажимаем F7.

ef0145821c3f102b9bef680c2d251cd6.png

ECX теперь имеет значение 3 и адрес в ESI увеличился на 4 - теперь он указывает на следующие 4 байта, которые будут загружены в EAX.

Есть ещё инструкции LODSW и LODSB для загрузки 2-байтовых (WORD) и 1-байтных (BYTE) значений соответственно.

STOS

Сохраняет (от англ. STORE) значение аккумулятора по адресу приёмника EDI.

7c4ceebc960da28cedc2c9e1f03a7816.png

При трассировании данного примера, когда очередь доходит до инструкции STOS, в окне подсказки появляется текущее значение аккумулятора и адрес, по которому будет сохранено это значение.

82a30c2cc579705fa183cdc989fa95f4.png

Вот результат в дампе:

84a49bd87b71bcb641528f4c11384bd5.png

Также как и в предыдущем случае, можно использовать префикс REP для автоповторения и существуют 2-байтовая и 1-байтная версии: STOSW и STOSB.

CMPS

Осуществляет сравнение ячеек, указанных в источнике (ESI) и приёмнике (EDI).

0d69f304e1c31146d6d4f37cada1df79.png

OllyDbg дизассемблирует данную инструкцию как CMPS DWORD PTR DS:[ESI],DWORD PTR ES:[EDI].

В окне подсказки отображаются значения, подлежащие сравнению:

1c041c7dd5bdbc85f8235a1a68e26e49.png

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

ea34c32f5fb033b7586472683fbd6d60.png

Т.к. операция сравнения влияет на состояние флага Z, можно использовать префикс REPE или REPZ для цепочечного сравнения до тех пор пока не истощится счётчик ECX или флаг Z не окажется сброшен.

9fc8f6fd5d9bd10aa092ebf416e3edf3.png

От предыдущих примеров по адресам 40365C и 40369C осталось то, что видим в дампе.

Инициализируем ECX = 10h. При использовании префикса REPE, цикл прервётся когда ECX станет равен нулю или же по состоянию флага Z - любое из этих условий приведёт к завершению цикла.

99e420c0e0244e16883b07474c999167.png

Трассируем до строчки с REPE. Значения в обоих ячейках оказываются равны, что приводит к установке флага Z.

216adf6925394f66edc326d622fd426c.png

Установленный флаг Z сигнализирует REPE, что пока не нужно прерывать цикл. Нажимаем F7.

Продолжаем нажимать F7 до тех пор, пока операнды не окажутся разными. В данном случае это должно произойти до того как истощится счётчик ECX. Ага, вот:

80073824127126be5675840c0e2aa2a6.png

Последующее нажатие на F7 сбрасывает флаг Z и цикл прерывается.

d6e21ddd8feb704d2428a393c430946c.png

Если бы все 16 двойных слов оказались равными в обоих цепочках, цикл завершился бы по условию ECX = 0, а не раньше.

Ранее упоминался префикс REPNZ, который тоже применим к CMPS и позволяет прервать цикл досрочно, но условие в этом случае противоположно только что рассмотренному REPE/REPZ.

Мы уже изучили основную массу наиболее полезных инструкций, за исключением, пожалуй, только инструкций FPU (для операций над дробными числами), которые мы рассмотрим позже. Не лишним будет повторить пройденный материал, если у Вас остались какие-то пробелы.

СПОСОБЫ АДРЕСАЦИИ

Прямая адресация

Это самый простой способ - адрес явно указывается в одном из операндов инструкции. Например:

MOV dword ptr [00513450], ecx
MOV ax, word ptr [00510A25]
MOV al, byte ptr [00402811]
CALL 452200
JMP 421000
Не нужно производить какие-либо вычисления для разрешения адреса, т.к. его значение указано открытым текстом в листинге.

Косвенная адресация

MOV dword ptr [eax], ecx
CALL EAX
JMP [ebx + 4]
Чтобы узнать настоящий адрес в любой из предыдущих инструкций, необходимо остановить отладчик на нужной инструкции и посмотреть значения регистров или, что гораздо удобнее, посмотреть в окно подсказки.

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

Чтобы было нагляднее, давайте снова воспользуемся крэкми Cruehead'а. Загружаем его в отладчик.

0782551f74c51e7682b22984631a150b.png

Подмечаем пример косвенной адресации: PUSH [ebp+8]

Т.к. мы всё ещё находимся на точке входа (Entry Point), мы не можем заранее узнать значение регистра EBP к моменту выполнения того самого PUSH'а. Поэтому, нам не известно пока ещё что именно попадёт в стек. Давайте выделим данный PUSH (он находится по адресу 401009) и нажмём F2, чтобы повесить на него точку останова (бряк).

Потом нажимаем F9 (RUN) - крэкми запускается и вскоре останавливается там, где мы установили бряк.

831fd3f16f3a88a70780dc999f8dd921.png

В окне подсказки высвечивается настоящий адрес EBP+8 = 12FFF8. Кстати, у Вас адрес может отличаться - это свойственно стековым адресам! Значение EBP в данный момент у меня равно 12FFF0, на основании чего и получилось, что EBP+8 = 12FFF8.

84685d377fefcc68bb28116c54962dcb.png

e9fd1eb004776bb04ff862088b6727cb.png

Теперь давайте посмотрим, что хранится в дампе по адресу EBP+8 : Go to - Expression = EBP+8

6a053de173e858b24b2d77229a6a708e.png

И что мы там видим?

9ea567427b15e6fd188a95557de21051.png

Это значение будет помещено в стек. Нажимаем F7 - так и есть.

f2198ead79baf8537ff318537aab341f.png

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

Существуют другие способы адресации, но мы их частично уже рассмотрели в примерах. На данном этапе мне нечего добавить.

Кто смог осилить весь материал (включая, наверное, и переводчика :) - прим. пер.) имеет неплохие шансы познать захватывающий мир крэкинга, но для этого понадобится ещё очень много практики и чтения серьёзной литературы. В 9й главе мы, наконец, окунёмся в азы прикладного крэкинга.

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

О нас

  • Наше сообщество существует уже много лет и гордится тем, что предлагает непредвзятое, критическое обсуждение различных тем среди людей разных слоев общества. Мы работаем каждый день, чтобы убедиться, что наше сообщество является одним из лучших.

    Dark-Time 2015 - 2022

    При поддержке: XenForo.Info

Быстрая навигация

Меню пользователя