Справочник функций

Ваш аккаунт

Войти через: 
Забыли пароль?
Регистрация
Информацию о новых материалах можно получать и без регистрации:

Почтовая рассылка

Подписчиков: -1
Последний выпуск: 19.06.2015

Техника и философия хакерских атак II (фрагмент [2/3]) часть 3

Переведем окно дампа в режим отображения двойных слов командой "DD" и командой "d ss:esp + 8" заставим его отобразить искомый адрес. Запомним его (запишем на бумажке) или выделим мышью и скопируем в буфер (последние версии soft-ice поддерживают мышь). В частности, на компьютере автора содержимое стека выглядело так:

:dd
:d ss:esp+8
0023:0012F9EC 002F4018  0000000F  00402310  004015D8      .@/......#@...@.
0023:0012F9FC 0012FA04  0012FE14  002F4018  6C361C58      .........@/.X.6l
0023:0012FA0C 6C361C58  0012F9F8  0012FB44  00401C48      X.6l....D...H.@.
0023:0012FA1C 00000002  6C2923D8  00402310  00000111      .....#)l.#@.....

Листинг 15 определение значения указателя lpString

Выделенное жирным шрифтом число и есть адрес буфера, готового принять прочитанную из окна строку. Посмотрим, что у нас там? Переключившись из режима двойных слов в режим байтов командой "DB", мы говорим отладчику "D SS:2F4018" и: ну конечно же видим вокруг себя один мусор, что и не удивительно, ведь функция GetWindowTextA еще и начинала своего выполнения! Что ж, приказываем Айсу выйти из функции ("P RET") и: вот она, наша строка!

:db
:d ss:2f4018
:p ret
0023:002F4018 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00  Kris Kaspersky..
0023:002F4028 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4038 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4048 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Листинг 16 строка, считанная функцией GetWindowText

Теперь установим точку останова на адрес начала строки (в листинге, приведенном выше он обведен рамкой) или на всю строку целиком. Заметим, что обоим решениям присущи свои недостатки: если защита игнорирует несколько первых символов имени, то первый примем просто не сработает. С другой стороны, точки останова на диапазон адресов аппаратно не поддерживаются и отладчик вынужден прибегать к хитрым манипуляциям с атрибутами страницы, заставляя процессор генерировать исключение при всякой попытке доступа к ней, а затем вручную анализировать - произошло ли обращение к контролируемой области или нет. Естественно, это значительно снижает производительность и отлаживаемое приложение исполняется со скоростью, которой не позавидует и черепаха! Поэтому, к этому трюку имеет смысл прибегать лишь тогда, когда не сработал первый (а не срабатывает он крайне редко).

Уничтожив ставшей ненужной точку останова на GetWindowText (команда "bc *") мы устанавливаем новую точку останова "bpm ss:2F4018" (разумеется, на вашем компьютере адрес строки может быть и другим) и покидаем отладчик нажатием <Ctrl-D>. Не желая коротать свои дни в одиночестве, отладчик тут же всплывает, сигнализируя нам о том, что некий код попытался обратиться к нашей строке:

001B:77E9736D	REPNZ	SCASB
001B:77E9736F	NOT	ECX
001B:77E97371	DEC	ECX
001B:77E97372	OR	DWORD PTR [EBP-04],-01

Листинг 17 перехват обращения к регистрационной строке

Судя по адресу, мы имеем дело с некоторой системной функцией (ибо они традиционно размешаются в верхних адресах), но вот с какой именно? Сейчас выясним! Долго ли умеючи! Наскоро набив на клавиатуре трехбуквенное сочетание "mod" мы заставляем отладчик вывести список всех модулей системы на экран:

:mod
hMod Base     PEHeader Module Name      File Name
     80400000 804000C8 ntoskrnl         \WINNT\System32\ntoskrnl.exe
     77E10000 77E100D8 user32           \WINNT\system32\user32.dll
     77E80000 77E800D0 kernel32         \WINNT\system32\kernel32.dll
     77F40000 77F400C8 gdi32            \WINNT\system32\gdi32.dll
     77F80000 77F800C0 ntdll            \WINNT\system32\ntdll.dll
     78000000 780000D8 msvcrt           \WINNT\system32\msvcrt.dll

Листинг 18 определение принадлежности адреса к модулю

Очевидно, что адрес 77E9736Dh принадлежит динамической библиотеке kernel32.dll, а точнее, - функции lstrlenA, которая, как и следует из ее названия определяет длину строки. Поскольку, в определении длины для нас нет ничего интересного, мы безо всякого зазрения совести оставляем этот код жить на бозе и вновь выходим из отладчика, позволяя ему продолжить поиски защитного кода.

Следующее всплытие отладчика оказывается более информативным, смотрите (внимание: в силу архитектурных особенностей x86 процессоров, отладочное исключение возникает не до, а после выполнения команды, "зацепившей" точку останова, а потому отладчик подсвечивает не ее саму, а следующую за ней команду):

001B:004015F7  MOV     CL,[EAX+ESI] ;эта команда "зацепила" breakpoint
001B:004015FA  MOVSX   AX,BYTE PTR [EAX+ESI+01] ;здесь отладчик получил управление
001B:00401600  MOVSX   CX,CL
001B:00401604  IMUL    EAX,ECX
001B:00401607  AND     EAX,0000FFFF
001B:0040160C  AND     EAX,8000001F ; STATUS_BEGINNING_OF_MEDIA
001B:00401611  JNS     00401618
001B:00401613  DEC     EAX

Листинг 19 ловля защитного кода за длинные уши и короткий хвост

Используемая адресация наталкивает нас на мысль, что EAX, возможно, параметр цикла, а вся эта конструкция посимвольно читает строку. Очень похоже, что в мы находимся самом "сердце" защитного механизма - генераторе серийного номера. Если мы посмотрим чуть-чуть ниже, то в глаза бросится очень любопытная строка (в тексте она выделена жирным шрифтом) :

001B:0040164E	PUSH	ECX
001B:0040164F	PUSH	EDX
001B:00401650	CALL	[MSVCRT!_mbscmp]
001B:00401656	ADD	ESP,08
001B:00401659	TEST	EAX,EAX
001B:0040165B	POP	ESI
001B:0040165C	PUSH	00
001B:0040165E	PUSH	00
001B:00401660	JNZ	00401669
001B:00401662	PUSH	00403030
001B:00401667	JMP	0040166E

Листинг 20 в недрах генератора регистрационных номеров

Вероятно, здесь-то защита и сравнивает введенный пользователем регистрационный номер с только что сгенерированным эталоном! Переведем курсор на строку 401650h и дадим команду "HERE", обозначающую буквально "сюда!" Теперь последовательно дадим команды "D DS:ECX" и "D DS:EDX", посредством которых мы сможем подсмотреть содержимое указателей, передаваемых функции в качестве аргументов. Скорее всего, один из них принадлежит введенной нами строки, а другой - сгенерированному защитой регистрационному номеру.

:d ecx
0023:002F40B8 36 36 36 00 00 00 00 00-00 00 00 00 00 00 00 00  666.............
0023:002F40C8 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

:d edx
0023:002F4068 47 43 4C 41 41 4C 54 51-51 5B 57 52 54 00 35 38  GCLAALTQQ[WRT.58
0023:002F4078 44 44 32 44 36 39 2E 2E-2E 00 00 00 00 00 00 00  DD2D69..........

Листинг 21 просмотр аргументов, передаваемых компаратору

Итак, наше предположение на счет "введенного регистрационного номера" полностью подтверждается, и шансы на то, что абракадабра "GCLAALTQQ[WRT" и есть эталонный регистрационный номер весьма велики (обратите внимание на завершающий ее нуль, отсекающий остаток строки ":58DD2D69", который по невнимательности можно принять за саму строку.

Выйдем из отладчика и попытаемся ввести "GCLAALTQQ[WRT" в программу: Защита, благополучно проглотив регистрационный номер, выводит диалог с победной надписью "ОК". Получилось! Нас признали зарегистрированным пользователем! Вся операция не должна была занять порядка пары-тройки минут. Обычно для подобных защит большего и не требуется. С другой стороны, на их написание автор потратил как минимум полчаса. Это очень плохой баланс между накладными расходами на создание защиты и ее стойкостью. Тем не менее, использование таких защит вовсе не лишено смысла (ведь не все же пользователи - хакеры). Нельзя сказать, что создатели защит совсем уж не представляют насколько их легко вскрыть. Косвенным это подтверждением этого являются убедительные просьбы не ломать защиту, а зарегистрироваться и способствовать развитию отечественного рынка (что особенно характерно для российских программистов). Иной раз они бывают настолько красноречивы и длинны, что за время, потраченное на сочинение подобных опусов, можно было бы значительно усилить защиту.

Вышеописанная технология взлома доступна невероятно широкому кругу людей и не требует даже поверхностного знания ассемблера и операционной системы. Просто ставим точку останова на GetWindowText, затем еще одну на строковой буфер и, дождавшись всплытия отладчика, пытаемся найти в каком месте происходит сравнение введенного регистрационного номера со сгенерированным на основе имени эталоном. Любопытно, но большинство кракеров довольно смутно представляют себе "внутренности" операционной системы и знают API куда хуже прикладных программистов. Воистину "умение снять защиту еще не означает умения ее поставить". Чего греха таить! И автор этой книги сначала научился ломать и лишь затем программировать.

Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу на себя. Кому приятно видеть чужое имя?! Вернемся к коду, сравнивающему строки введенного и эталонного регистрационного номера. Если мы заменим в строке 0040164Eh команду PUSH ECX (опкод 52h) на команду PUSH EDX (опкод 51h), то защита станет сравнивать эталонный регистрационный номер с: самим эталонным регистрационным номером! Разумеется, не совпадать с самим собой регистрационный номер просто не может и какие бы строки мы не вводили, защита воспримет их как правильные. Другой путь - заменить условный переход JNZ в строке 401660h (в тексте он выделен квадратиком) на безусловный переход JZ (тогда защита будет "проглатывать" любые регистрационные номера, кроме правильных), или же забить его любой незначащей командой подходящего размера, например SUB EAX, EAX (тогда будут "проглатываться" любые регистрационные номера, включая правильные), хотя последнее и неоригинально. Запускаем HIEW, переводим его в ASM-режим двойным нажатием <Enter>, переходим по адресу 401660h (<F5>, ".401660") и меняем "jne 1669" на "je 1669", скидываем изменения в файл <F9> и запускаем программу. Вводим в нее любую понравившуюся вам комбинации и: это работает!!!

Замечу, что это не самый лучший способ взлома и в ряде случаев он не срабатывает. Типичные защитные механизмы имеют как минимум два уровня обороны. На первом осуществляется проверка корректности введенного регистрационного номера и, если он воспринимается защитой как правильный, то данные пользователя заносятся в реестр или дисковый файл. Затем, при перезапуске программы, защитный механизм извлекает пользовательские данные из места их постоянного хранения и проверяет: а соответствует ли имя пользователя его регистрационному номеру?

Блокировав первую проверку, мы добьемся лишь того, что позволим защите сохранить неверные данные, но наш обман будет немедленно раскрыт как только программа попытается загрузить поддельные данные! Конечно, второй "укрепрайон" защитного механизма можно разбить тем же самым способом, которым мы воспользовались для захвата первого (только на этот раз вместо перехвата функции GetWindowText следует установить точки останова на функции, манипулирующие с файлом и реестром), однако это очень утомительно. Другой, и все такой же утомительный, путь - отследить все вызовы процедуры генерации регистрационного номера по перекрестным ссылкам (если одна и та же процедура вызывалась из разных мест защитного механизма), либо же по ее сигнатуре (если создатель защиты дублировал процедуру генерации). Действительно, крайне маловероятно, чтобы разработчик использовал не один, а несколько независимых вариантов генератора. Но даже в последнем случае очень трудно избежать отсутствия совпадающих фрагментов (во всяком случае на языках высокого уровня). Далеко не каждый программист знает, что "(!a) ? b = 0 : b = 1" и "if (a) b=1; els b=0" в общем случае компилируются в идентичный код. Реализовать один и тот же алгоритм так, чтобы ни в одном из вариантов не присутствовало повторяющихся фрагментов кода, представляется достаточно нетривиальной задачей! Тем не менее, выделение уникальной последовательности, присущей одному лишь защитному коду, - задача ничуть не менее нетривиальная, особенно если в защите присутствует множество проверок, расположенных в самых неожиданных местах.

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

001B:004015EF	PUSH	ESI
001B:004015F0	XOR	ESI,ESI
001B:004015F2	DEC	ECX
001B:004015F3	TEST	ECX,ECX
001B:004015F5	JLE	00401639
001B:004015F7	MOV	CL,[EAX+ESI]	; эта команда обратилась к строке
001B:004015FA	MOVSX	AX,BYTE PTR [EAX+ESI+01]
001B:00401600	MOVSX	CX,CL
001B:00401604	IMUL	EAX,ECX
001B:00401607	AND	EAX,0000FFFF
001B:0040160C	AND	EAX,8000001F
001B:00401611	JNS	00401618		; адрес направлен "вниз", это не цикл
001B:00401611				; а оператор "IF"
001B:00401613	DEC	EAX
001B:00401614	OR	EAX,-20
001B:00401617	INC	EAX
001B:00401618	ADD	AL,41
001B:0040161A	LEA	ECX,[ESP+0C]
001B:0040161E	MOV	[ESP+14],AL
001B:00401622	MOV	EDX,[ESP+14]
001B:00401626	PUSH	EDX
001B:00401627	CALL	0040192E
001B:0040162C	MOV	EAX,[ESP+08]
001B:00401630	INC	ESI
001B:00401631	MOV	ECX,[EAX-08]
001B:00401634	DEC	ECX
001B:00401635	CMP	ESI,ECX
001B:00401637	JL	004015F7		; "наивысший" адрес из всех
001B:00401637				; 4015F7 - начало цикла генератора
001B:00401637				; 401637 - конец  цикла генератора
001B:00401639	LEA	EAX,[ESP+10]
001B:0040163D	LEA	ECX,[EDI+60]
001B:00401640	PUSH	EAX
001B:00401641	CALL	00401934
001B:00401646	MOV	ECX,[ESP+10]
001B:0040164A	MOV	EDX,[ESP+0C]
001B:0040164E	PUSH	ECX
001B:0040164F	PUSH	EDX
001B:00401650	CALL	[MSVCRT!_mbscmp]	; _ тут сравниваются строки
					;    очевидно это конец генератор

Листинг 22 дизассемблерный код генератора регистрационных номеров

Прежде нем приступать к восстановлению алгоритма генерации регистрационных номеров, отметим, что отладчики вообще-то не предназначены для декомпиляции кода и нам лучше прибегнуть к помощи дизассемблера. Найти же в дизассемблерном листинге требуемый фрагмент очень просто, - ведь адрес процедуры генератора нам уже известен. Для быстрого перемещения к исследуемому коду в IDA достаточно отдать к консоли команду Jump(0x4015EF) , а в HIEW'e - <F5>, ".4015EF". Так или иначе мы встретим следующие строки (а еще лучше, если из мазохистских соображений, мы будем анализировать этот код под отладчиком, поскольку дизассемблер - особенно IDA - доступен не всем):

001B:004015EF	PUSH	ESI
001B:004015F0	XOR	ESI,ESI
001B:004015F2	DEC	ECX
001B:004015F3	TEST	ECX,ECX
001B:004015F5	JLE	00401639

Листинг 23 фронтовая часть генератора регистрационных номеров

Регистр ESI здесь инициализируется явно (ESI ^ ESI := 0), а вот чему равен ECX?! Прокручиваем экран отладчика вверх до тех пор, пока не встретим машинную команду, присваивающую ECX то или иное значение:

001B:004015D8	MOV	EAX,[ESP+04]
001B:004015DC	MOV	ECX,[EAX-08]
001B:004015DF	CMP	ECX,0A
001B:004015E2	JGE	004015EF

Листинг 24 определение значение, присваиваемого регистру ECX в листинге $ -- 2

Ага, здесь в ECX пересылается значение ячейки по адресу [EAX-08], но что это за ячейка и куда указывает сам EAX? Что ж, под отладчиком (в отличии от дизассемблера) его содержимое очень просто подсмотреть! Достаточно дать команду "D EAX" и область памяти на которую указывает EAX немедленно отобразится в окне дампа:

:d eax
0023:002F4018 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00  Kris Kaspersky..
0023:002F4028 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4038 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
0023:002F4048 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Листинг 25 текстовая строка на которую указывает регистр EAX (и эта та самая строка, которая только что была введена нами с клавиатуры)

Да это же только что введенная нами строка! А в регистр ECX тогда загружается что? Смотрим: так, значение ECX равно 0Eh или 14 в десятичной системе исчисления. Очень похоже на длину этой строки (как известно, MFC -строки, точнее объекты класса CString хранят свою длину в специальном 32-разрядном поле, "родимым пятном" которого как раз и является смещение на 8 байт влево относительно начала самой строки). Действительно, имя "Kris Kaspersky" как раз и насчитывает ровно 14 символов (считая вместе с пробелом). Тогда становятся понятными две следующие машинные команды: CMP ECX,0Ah/JGE 4015EFh, осуществляющие контроль строк на соответствие минимально допустимой длине. При попытке ввода имени, состоящего из девяти или менее символов, программа откинет его как непригодное для регистрации. Это важный момент! Многие хакеры игнорируют подобные тонкости алгоритма и создают не вполне корректные генераторы, не осуществляющие таких проверок вообще. Как следствие - пользователь вводит свое короткое имя в генератор (например, "KPNC"), получает регистрационный код, подсовывает его защите и: обложив матом хакера, вводит в генератор другое имя - на сей раз подлиннее. А если защита имеет ограничение на предельно допустимую длину? Сколько так пользователю придется мотаться между защитой и генератором?

Ладно, оставим вопросы профессиональной этики и вернемся к коду генератора, черкнув в лежащем справа от Клавы листке белой бумаги, что EAX указывает на имя пользователя, а ECX содержит его длину.

001B:004015F2	DEC	ECX
001B:004015F3	TEST	ECX,ECX
001B:004015F5	JLE	00401639

Листинг 26 заголовок цикла обработки введенной пользователем строки

Здесь: мотаем цикл до тех пор, пока не будут обработаны все символы строки (читатели, знакомые с "Фундаментальными основами хакерства" уже наверняка распознали в этой конструкции цикл for).

Теперь заглянем в тело цикла, спустившись еще на одну строчку вниз:

001B:004015F7	MOV	CL,[EAX+ESI]

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

001B:004015FA	MOVSX	AX,BYTE PTR [EAX+ESI+01]

MOVe whith Signed eXtension (пересылка со знаковым расширением) загружает следующий байт строки в регистр AX, автоматически расширяя его до слова и загаживая тем самым указатель на саму строку с именем. На редкость уродливый код! Но дальше - больше.

001B:00401600	MOVSX	CX,CL

Преобразуем первый прочитанный символ строки к слову (обратим внимание, что здесь и далее под "первым" и "вторым" символом мы будем понимать отнюдь не NameString[0] и NameString[0], а NameString[ESI] и NameString[ESI + 1] соответственно, а сам ESI условно обозначим как index или, сокращенно, idx). Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как MOVSX CX, [ESI+EAX]

001B:00401604	IMUL	EAX,ECX

Подставим вместо регистров их смысловые значения, мы получаем: EDX:EAX := NameString[idx] * String[idx + 1]

001B:00401607	AND	EAX,0000FFFF

Преобразуем EAX к машинному слову, откидывая старшие 16 бит.

001B:0040160C	AND	EAX,8000001F

Выделяем пять младших бит от оставшегося слова (почему именно пять? просто переведите 1Fh в двоичную форму и сами увидите). Так же, выделяется и старший, знаковый, бит слова, однако, он всегда равен нулю, так как его принудительно сбрасывает предыдущая команда. Зачем же тогда его компилятор так старательно выделает? Осел он - вот почему. Программист присваивает результат беззнаковой переменной, вот компилятор и понимает его буквально!

001B:00401611	JNS	00401618

Если знаковый бит не установлен (ха! а с какой такой радости ему быть установленным?!), то прыгаем на 401618h. Ну что ж! Прыгаем, так прыгаем, избавляя себя от "радости" анализа нескольких никогда не исполняющихся команд защитного кода:

001B:00401618	ADD	AL,41
001B:0040161A	LEA	ECX,[ESP+0C]
001B:0040161E	MOV	[ESP+14],AL
001B:00401622	MOV	EDX,[ESP+14]

Листинг 27 код, знакомящий нас с плавающими фреймами

Первая машинная команда добавляет к содержимому регистра AL константу 41h (литера 'А' в символьном представлении) и полученная сумма перегоняется в регистр EDX, минуя по пути локальную переменную [ESP + 14].

С конструкцией LEA ECX, [ESP + 0Ch] разобраться несколько сложнее. Во-первых, ячейка [ESP +0Ch] явным образом не инициализируется в программе, а, во-вторых, значение регистра ECX ни здесь, ни далее не используются. Если бы оптимизирующие компиляторы не выкидывали все лишние операции присвоения (т .е. такие, чей результат не используется), мы бы просто списали эту команду на ляп разработчика защитного механизма, но сейчас такая стратегия уже не проходит. К тому же это удачный повод для знакомства с плавающими фреймами, без умения работать с которыми невозможно побороть практически ни одну современную защиту.

Для начала давайте вспомним устройство "классического" кадра стека. При выходе в функцию компилятор сохраняет в стеке прежнее значение регистра EBP (а так же при желании и всех остальных регистров общего назначения, если они действительно должны быть сохранены), а затем приподнимает регистр ESP немного "вверх", резервируя тем самым то или иное количество памяти для локальных переменных. Область памяти, расположенная между сохраненным значением регистра EBP и новой вершиной стека, и называется кадром. Начальный адрес только что созданного кадра копируется в регистр EBP, и этот регистр используется в качестве опорной точки для доступа ко всем локальным переменным. По мере разбухания стека поверх кадра могут громоздиться и другие данные, заталкиваемые туда машинными командами PUSH и PUSHF (например: аргументы функций, временные переменные, сохраняемые регистры и т. д.). Достоинство этой системы заключается в том, что для доступа к локальным переменным нам достаточно знать всего лишь одно число - смещение переменной относительно вершины кадра стека. Благодаря этому, машинные команды, обращающиеся к одной и той же локальной переменной, из какой бы точки функции они ни шли, выглядят одинаково. То есть, нам не требуется никаких усилий, чтобы догадаться, что MOV EAX, [EBP + 69h] и MOV [EBP + 69h], ECX в действительности обрабатывают одну локальную переменную, а не две. Между прочим, вы зря смеетесь! Хотите получить кукурузный початок в зад? Ну так получайте! (Знаю, что больно, но ведь я же предупреждал!).

Поскольку регистров общего назначения в архитектуре IA-32 всего семь, то отдавать даже один из них на организацию поддержки фиксированного кадра стека по меньшей мере не логично, тем более, что локальные переменные можно адресовать и через ESP. Ну и в чем же разница? - спросите вы. А разница между тем принципиальна! В отличии от EBP, жестко держащего верхушку кадра за хвост, значение ESP изменяется всякий раз, когда в стек что-то вложат или, наоборот, что-то вытащат оттуда. Рассмотрим это на следующем примере: MOV EAX, [ESP+10h]/PUSH EAX/MOV ECX, [ESP + 10h]/PUSH ECX/MOV [ESP + 18h], EBP, - как вы думаете, к каким локальным переменным здесь происходит обращение? На первый взгляд, значение ячейки [ESP + 10h] дважды засылается в стек, а затем в ячейку [ESP +18h] копируется содержимое регистра EBP. На самом же деле тут все не так! После засылки в стек содержимого регистра EAX, указатель вершины стека приподнимается на одно двойное слово вверх и дистанция между ним и локальными переменными неотвратимо увеличивается! Следующая машинная команда - MOV ECX, [ESP + 10h] на самом деле копирует в регистр ECX содержимое совсем другой ячейки! А вот [ESP + 18h] после засылки ECX указывает на ту же самую ячейку, что вначале копировалась в регистр EAX. Ну и как теперь насчет "посмеяться"?

Такие оптимизированные кадры стека по-русски называются "плавающими", а в англоязычной литературе обычно обозначаются аббревиатурой FPO - Frame Pointer Omission. Это едва ли не самое страшное проклятие для хакеров. Основной камень преткновения заключается в том, что для определения смещения переменной в кадре мы должны знать текущее состояние регистра ESP, а узнать его можно лишь путем отслеживания всех предшествующих ему машинных команд, манипулирующих с указателем верхушки стека и, если мы случайно упустим хоть одну из них, вычисленный таким трудом адрес локальной переменной окажется неверным! Следовательно, неверным окажется и результат дизассемблирования!!! Вернемся к нашему примеру LEA ECX, [ESP + 0Ch]. Будем прокручивать экран "CODE" отладчика вверх до тех пор, пока не обнаружим пролог функции или не накопим по меньшей мере 0Ch байт, закинутых на стек командами PUSH (в квадратных скобках показано смещение соответствующих ячеек относительно вершины стека на момент вызова нашего LEA).

001B:00401580	PUSH	FF			[ +24h]
001B:00401582	PUSH	00401C48		[ +20h]
001B:00401587	MOV	EAX,FS:[00000000]
001B:0040158D	PUSH	EAX			[ +1Сh]
001B:0040158E	MOV	FS:[00000000],ESP
001B:00401595	SUB	ESP,10			[ +18h] (40161A:04h)
001B:00401598	PUSH	EDI			[ +08h]
001B:00401599	MOV	EDI,ECX
:
001B:004015CD	PUSH	EAX			[ +04h]
:
001B:004015EF	PUSH	ESI			[ +00h]

Листинг 28 отслеживание манипуляций с вершиной стека

Ну, что Шура, я Вам могу сказать, - если считать, что SUB ESP, 10h открывает фрейм функции, то LEA ECX, [ESP + 0Ch] лежит по смещению 04h от его начала, - аккурат посередине. А что у нас здесь? Листаем код ниже (в квадратных скобках показано смещение соответствующих ячеек относительно начала кадра стека):

001B:00401595	SUB	ESP,10			[ +00h]
001B:00401598	PUSH	EDI			[ +20h]
001B:00401599	MOV	EDI,ECX
001B:0040159B	LEA	ECX,[ESP+04]		[ +00h]
001B:0040159F	CALL	40190Ah
001B:004015A4	LEA	ECX,[ESP+0C]		[ +08h]
001B:004015A8	MOV	DWORD PTR [ESP+1C],00h
001B:004015B0	CALL	40190Ah
001B:004015B5	LEA	ECX,[ESP+08]		[ +04h]
001B:004015B9	MOV	BYTE PTR [ESP+1C],01
001B:004015BE	CALL	40190Ah

Листинг 29 инициализация локальных переменных

Ага! Вот теперь мы видим, что указатель на локальную переменную, расположенную по смещению 04h от начала кадра стека (далее просто var_04h) передается функции 40190Ah очевидно для ее, переменной, инициализации. Но вот что делает эта загадочная функция? Если, находясь в отладчике, нажать <F8> для входа в ее тело, мы обнаружим следующий код:

001B:0040190A	JMP	[00402164h]

Узнаете? Ну да, это характерный способ вызова функций из динамических библиотек. Но вот какая функция какой именно библиотеки сейчас вызывается? Ответ хранит ячейка 402164h, содержащая непосредственно сам вызываемый адрес. Посмотрим ее содержимое?

:dd
:d 402164
0010:00402164 6C29198E  6C294A70  6C2918DD  6C298C74      ..)lpJ)l..)lt.)l

Листинг 30 просмотр содержимого ячейки 402164h (двойное слово, выделенное квадратиком)

Остается только узнать какому модулю принадлежит адрес 6C9198Eh. Не выходя из soft-ice даем ему команду "mod" и смотрим (протокол, приведенный ниже по понятным соображениям сильно сокращен):

Base	 PEHeader Module Name		File Name
10000000 10000100 pdshell		\WINNT\system32\pdshell.dll
6C120000 6C1200A8 mfc42loc		\WINNT\system32\mfc42loc.dll
6C290000 6C2900F0 mfc42		\WINNT\system32\mfc42.dll
6E380000 6E3800C8 indicdll		\WINNT\system32\indicdll.dll

Листинг 31 определение принадлежности адреса 6C9198Eh

Легко видеть, что адрес 6C29199Eh принадлежит модулю MFC42.DLL, что совершенно неудивительно ввиду того, что данная программа действительно интенсивно использует библиотечку MFC. Чтобы не вычислять принадлежность всех остальных функций вручную давайте просто загрузим символьную информацию из MFC42.DLL в отладчик. Запустив NuMega "Symbol Loader" (если только вы еще не сделали этого ранее), выберите команду "Load Exports" в меню "File", а затем, перейдя в папку "\WINNT\System32\" дважды щелкните по строке с именем "MFC42.DLL". Теперь, тот же самый код под отладчиком будет выглядеть так:

001B:004015B5	LEA	ECX,[ESP+08]
001B:004015B9	MOV	BYTE PTR [ESP+1C],01
001B:004015BE	CALL	MFC42!ORD_021B

Листинг 32 определение ординала функций

Умница soft-ice определил не только название динамической библиотеки, экспортирующей вызываемую функцию, но и ее ординал! Что же касается имени функции, его можно вычислить с помощью DUMPBIN и библиотеки MFC42.lib. Даем команду "DUMPBIN /HEADRES MFC42.LIB >MFC42.headrs.txt" и затем в образовавшемся файле простым контекстным поиском ищем строку "Ordinal : 539", где "539" наш ординал 021Bh записанный в десятичном виде (именно так выдает оридиналы этот dumpbin). Если все идет пучком, мы должны получить следующую информацию:

Version      : 0
Machine      : 14C (i386)
TimeDateStamp: 35887C4E Thu Jun 18 06:32:46 1998
SizeOfData   : 00000020
DLL name     : MFC42.DLL
Symbol name  : ??0CString@@QAE@PBG@Z (__thiscall CString::CString(unsigned short *))
Type         : code
Name type    : ordinal
Ordinal      : 539

Листинг 33 определение символьного имени функции MFC42!ORD_021B

Так, это конструктор объекта типа CString, а указатель, передаваемый ему, стало быть и есть тот самый this, что указывает на свой экземпляр CString! Следовательно, var_4 - это локальная переменная типа "MFC-строка". Теперь, не грех вернуться к изучению прерванной темы (а прервали мы ее на строке 40161Ah, где осуществлялась загрузка указателя на var_4 в регистр ECX посредством машинной команды LEA; регистр же EDX, как мы помним, содержит в себе результат умножения двух символов исходной строки, преобразованный в литерал):

001B:00401626	PUSH	EDX
001B:00401627	CALL	MFC42!ORD_03AB

Листинг 34 передача результата умножения двух символов функции MFC42!ORD_03AB

Следующими двумя командами мы заталкиваем полученный литерал в стек, передавая его в качестве второго аргумента функции MFC42!ORD_03AB (первый аргумент функций типа __thiscall передается через регистр ECX, содержащий указатель на экземпляр соответствующего объекта, с которым мы сейчас и манипулируем). Преобразовав ординал в символьное имя функции, мы получаем "оператор +=", что очень хорошо вписывается в обстановку окружающей действительности. Другими словами, здесь осуществляется посимвольное наращивание строки var_4 генерируемыми налету литералами.

001B:0040162C	MOV	EAX,[ESP+08]

Что у нас в [ESP + 8]? Прокручивая экран с дизассемблерным листингом вверх, находим, что здесь лежит самая первая ячейка из принадлежащих кадру стека. Условимся называть ее var_0. Давайте определим, что же за информация в ней находится?

001B:00401595	SUB	ESP,10				; [ +00h]
001B:00401598	PUSH	EDI				; [ +04h]
:
001B:004015C3	LEA	EAX, [ESP+04]			; var_0
001B:004015C7	LEA	ECX,[EDI+000000A0]
001B:004015CD	PUSH	EAX				; [ +08h]
001B:004015CE	MOV	BYTE PTR [ESP+20],02
001B:004015D3	CALL	MFC42!ORD_0F21			; CWnd::GetWindowText

Листинг 35 определение содержимого ячейки [ESP + 8]

Кое-что начинает уже проясняться. Переменная var_0 содержит указатель на MFC-строку, бережно хранящую в себе регистрационное имя пользователя.

001B:00401630	INC	ESI

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

001B:00401631	MOV	ECX,[EAX-08]			; EAX := var_4
001B:00401634	DEC	ECX
001B:00401635	CMP	ESI,ECX
001B:00401637	JL	004015F7

Листинг 36 хвост цикла

Первая машинная команда из четырех загружает длину регистрационной MFC-строки в регистр ECX, команда "DEC" уменьшает ее на единицу, а "CMP ESI, ECX" сравнивает полученное значение с индексом текущего обрабатываемого символа регистрационной строки. И, до тех пор, пока индекс не достигнет предпоследнего символа строки, условный переход "JL" прыгает на адрес 4015F7h, мотая цикл.

001B:00401639	LEA	EAX,[ESP+10]
001B:0040163D	LEA	ECX,[EDI+60]
001B:00401640	PUSH	EAX
001B:00401641	CALL	MFC42!ORD_0F21
001B:00401646	MOV	ECX,[ESP+10]
001B:0040164A	MOV	EDX,[ESP+0C]
001B:0040164E	PUSH	ECX
001B:0040164F	PUSH	EDX
001B:00401650	CALL	[MSVCRT!_mbscmp]

Листинг 37 сравнение сгенерированной строки с регистрационным номером, введенным пользователем

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

Брр! Вы еще не запутались?! Что ж, тогда давайте подытожим все вышесказанное краткими комментариями к защитному коду:

:ESI            = 0 (индекс)			[index];
:[ESP+08h], EAX - на регистрационную строку	[NameString];
:[ESP+0Ch]      - на генерируемую строку	[GenString]
001B:004015F7	MOV	CL,[EAX+ESI]		; CL := (char) NameString[index]
                               ;AX := (uint)((char) NameString[index+1])
001B:004015FA	MOVSX	AX,BYTE PTR [EAX+ESI+1]
001B:00401600	MOVSX	CX,CL			; 
001B:00401604	IMUL	EAX,ECX			; EAX := EAX * ECX
001B:00401607	AND	EAX,0000FFFF		; EAX := LOW_WORD(EAX)
001B:0040160C	AND	EAX,8000001F		; EAX := EAX ^ 1Fh
001B:00401611	JNS	00401618		; GOTO 401618h
001B:00401618	ADD	AL,41			; EAX := EAX + 'A'
001B:0040161A	LEA	ECX,[ESP+0C]		; ECX := &GenString
001B:0040161E	MOV	[ESP+14],AL		; tmp := AL
001B:00401622	MOV	EDX,[ESP+14]		; EDX := tmp
001B:00401626	PUSH	EDX			; 
001B:00401627	CALL	0040192E		; GetString += EDX
001B:0040162C	MOV	EAX,[ESP+08]		; EAX := &NameString
001B:00401630	INC	ESI			; index++
   ; ECX := NameString->GetLength()
001B:00401631	MOV	ECX,[EAX-08]		
001B:00401634	DEC	ECX			; ECX--
001B:00401635	CMP	ESI,ECX			; 
001B:00401637	JL	004015F7		; if (index < ECX) GOTO 4015F7h

Листинг 38 сводный дизассемблерный листинг генератора регистрационных номеров

Вот теперь - другое дело и нам уже ничего не стоит восстановить исходный код генератора.


часть 1 | часть 2 | часть 3 | часть 4

Оставить комментарий

Комментарий:
можно использовать BB-коды
Максимальная длина комментария - 4000 символов.
 
Реклама на сайте | Обмен ссылками | Ссылки | Экспорт (RSS) | Контакты
Добавить статью | Добавить исходник | Добавить хостинг-провайдера | Добавить сайт в каталог