Техника и философия хакерских атак II (фрагмент [2/3]) часть 4
for (int idx=0;idx<String.GetLength()-1;idx++) RegCode+= ((WORD) sName[a]*sName[a+1] % 0x20) + 'A';
Листинг 39 восстановленный исходный код генератора регистрационных номеров
Остается лишь написать собственный генератор регистрационных номеров. Это можно сделать на любом симпатичном вам языке, например на ассемблере. На диске находится один вариант (file://CD/SRC/crackme.58DD2D69h/HACKGEN/KeyGen.asm). Ключевая процедура может выглядеть так:
; ГЕНЕРАЦИЯ РЕГИСТРАЦИОННОГО НОМЕРА ; ======================================================================== MOV ECX, [Nx] ; ECX := strlen(NameString) SUB ECX, 2 ; выкусываем перенос строки DEC ECX ; уменьшаем длину строки на единицу MOV EBX, 20h ; магическое число LEA ESI, hello ; указатель на буфер с именем пользователя LEA EDI, buf_in ; ^ указатель на буфер для генерации ; ЯДРО ГЕНЕРАТОРА ; ======================================================================== gen_repeat: ;<<<---------------------------------------------; CORE LODSW ; читаем слово ; CORE MUL AH ; AX := NameString[ESI]*NameString[ESI+1] ; CORE XOR EDX, EDX ; EDX := NULL ; CORE DIV EBX ; DX := NameString[ESI]*NameString[ESI+1] % 1Ah ; CORE ADD EDX, 'A' ; переводим в символ ; CORE ; ; CORE XCHG EAX, EDX ; ; CORE STOSB ; записываем результат ; CORE DEC ESI ; на символ назад ; CORE LOOP gen_repeat ; ---- цикл --------------------------------->>> ; CORE
Листинг 40 ключевая процедура генератора регистрационных номеров, написанная на ассемблере
Испытаем написанный генератор. Запустив откомпилированный файл KeyGen.exe на выполнение, введем в качестве регистрационного имени какую ни будь текстовую строку (например, свое собственное имя или псевдоним), - не пройдет и секунды как генератор выдаст подходящий regnum в ответ. В частности, имени "Kris Kaspersky" соответствует следующий регистрационный код: "GCLAALTQQ[WRT"
Рисунок 5 - 0х00С демонстрация работы ключеделки
Генератор успешно работает и вычисляет правильные регистрационные номера. Однако, вводить регистрационный номер вручную не только утомительно, но и неэлегантно. Да, можно скопировать его и через буфер обмена, но все равно возня будет еще та. В конечном итоге, компьютер на то и придуман, чтобы служить пользователю, но не наоборот. Идеальный crack - это такой crack который не докучает пользователю теми вопросами, ответ на которые знает сам, равно как и не требует от последнего никаких действий, которые он может выполнить и самостоятельно. Единственное, что требует такой crack - своего запуска. Короче, хорошая программа должна заботиться о себе сама!
Первое, что приходит на ум: просто пропадчить защитный код на диске или в памяти. В предыдущей главе мы как раз разбирали как это сделать. Однако, падчики, во-первых, просто вопиюще незаконны, во-вторых, крайне чувствительны к версии билда. Генераторы регистрационных номеров, напротив, весьма мирно уживаются с уголовным кодексом, поскольку они не подделывают, а именно генерируют регистрационный номер на основе имени, введенного пользователем (см. эпиграф) и их написание столь же "незаконно", сколько открытие мастерской по изготовлению дубликатов ключей например. К тому же алгоритм генерации регистрационного номера если и изменяется, то во всяком случае не в каждой версии программы .
Во времена старушки MS-DOS эта проблема решалась перехватом прерывания int 16h с целью эмуляции ввода с клавиатуры. Ломалка, грубо говоря, прикидывалась пользователем и подсовывала защищенной программе сначала имя, а затем и сгенерированный регистрационный номер. От самого же пользователя не требовалось ничего, кроме запуска такой программы. Ну разве не красота? К сожалению, с переходом на Windows прямой контроль над прерываниями оказался безвозвратно утерян и все трюки старой Лисы перестали работать:
Но, "мало того, что их сосед в жилом доме свинью держит, так он еще и круглосуточно над ней измывается..." . Незадачливого музыканта подвела хорошая межквартирная слышимость (читай: хреновая звукоизоляция). Так вот, Windows с точки зрения безопасности - та же хрущоба и слышимость в ней о-го-го! Архитектура подсистемы пользовательского интерфейса, достающаяся NT/9x в наследство от незаконно рожденной Windows 1.0, неотделима от концепции сообщений (messages) - эдакой собачей будке, перенесенной с заднего двора на самое видное место. Любой процесс в системе может посылать сообщения окнам любого другого процесса, что позволяет ему управлять этими окнами по своему усмотрению. Хотите "подсмотреть" содержимое чужого окна? Пожалуйста! Пошлите ему SendMessage с WM_GETTEXT и все дела! Хотите послать окну свою строку с приветствием? Нет проблем, - SendMessage в купе с WM_SETTEXT спасут отца русской демократии! Аналогичным образам вы можете нажимать на кнопки, двигать мышь, раскрывать пункты меню, словом полностью контролировать работу приложения. Самое интересное, что уровень привилегий при этом никак не проверяется, - процесс с гостевыми правами может свободно манипулировать окнами, принадлежащими процессу-администратору. Знаете, в NT/w2k есть такое забавное окошко "запуск программы от имени другого пользователя", обычно используемое для запуска привилегированных приложений из сеанса непривилегированного пользователя? Ну вот например захотели проверить вы свой жесткий диск на предмет целостности файловой структуры, а перезапускать систему под "Администратором" вам лень (точнее, просто не хочется закрывать все активные приложения). На первый взгляд никакой угрозы для безопасности в этом нет, ведь "запуск программы от имени другого пользователя" требует явного ввода пароля! А вот получи треска гранату, - любое злопакостное приложение сможет перехватить ваш пароль только так! Причем, речь идет не о какой-то непринципиальной недоработке, которая легко устранима простой заплаткой (в просторечии называемой "падчем"). Нет! Все так специально и задумывалось. Не верите? Откроем Рихтера ":система отслеживает сообщения WM_SETTEXT и обрабатывает их не так, как большинство других сообщений. При вызове SendMessage внутренний код функции проверяет, не пытаетесь ли вы послать сообщение WM_SETTEXT. Если это так, функция копирует строку из вашего адресного пространства в блок памяти и делает его доступным другим процессам. Затем сообщение посылается потоку другого процесса. Когда поток-приемник готов к обработке WM_SETTEXT, он определяет адрес общего блока памяти (содержащего новый текст окна) в адресном пространстве своего процесса. Параметру lParam пристраивается значение именного этого адреса, и WM_SETTEXT направляется нужной оконной процедуре. Не слишком ли накручено, а?" Выходит, разработчики оконной подсистемы искусственно и крайне неэлегантно обошли подсистему защиты Windows, разделяющую процессы по их адресным пространствам. Естественно, это делалось отнюдь не с целью диверсии, - просто запрети Microsoft посылку сообщений между процессами куча существующих приложений (написанных большей частью под Windows 3.x) тут же перестала бы работать! А значит, эмуляция ввода с клавиатуры жила, жива и будет жить!
Единственное, что нужно знать - так это дескриптор (handle) окна, которого вы хотите "осчастливить" своим сообщением. Существует множество путей получить эту информацию. Можно например воспользоваться API-функцией FindWindow, которая возвращает дескриптор окна по его названию (текстовой строке, красующейся в заголовке) или тупо переворошить все окна одно за другим, в надежде что рано или поздно среди них встретиться подходящее. Перечисление окон верхнего уровня осуществляется функцией EnumWindows, а дочерних окон (к которым диалоговые элементы управления как раз и принадлежат) - EnumChildWindows.
Собственно, получить дескриптор главного окна ломаемого приложения - не проблема, ведь мы знаем его имя, которое в большинстве случаев однозначно идентифицирует данное окно среди прочих запущенных приложений. С дочерними окнами справиться не в пример сложнее. Ладно, кнопки еще можно распознать по их надписи (получаем дескрипторы всех дочерних окон вызовом EnumChildWindows, а затем посылаем каждому из них сообщение WM_GETTEXT с требованием сказать как кого зовут, после чего нам останется лишь сопоставить дескрипторы кнопок с их названиями). К сожалению с окнами редактирования такой фокус не пройдет, ибо по умолчанию они вообще не содержат в себе никакой информации, - вот и разбирайся это окно для ввода регистрационного имени или номера?
На помощь приходит тот факт, что порядок перечисления окон всегда постоянен и не меняется от одной операционной системе к другой. То есть, определив назначения каждого из дочерних окон экспериментально (или с помощью шпионских средств типа Spyxx из комплекта SDK) мы можем жестко прописать их номера в своей программе. Например, применительно к crackme.58DD2D69h это может выглядеть так: запускаем наш любимый soft-ice и даем команду "HWND" для выдачи списка всех окон, включая дочерние, зарегистрированных в системе.
0B0416 #32770 (Dialog) 6C291B81 43C CRACKME_ 0B0406 Button 77E18721 43C CRACKME_ 0B040A Static 77E186D9 43C CRACKME_ 0D0486 Edit 6C291B81 43C CRACKME_ 0904C6 Static 77E186D9 43C CRACKME_ 0D0412 Edit 6C291B81 43C CRACKME_ 0A047C Button 77E18721 43C CRACKME_
Листинг 41 определение порядка перечисления окон с помощью soft-ice
Ага! Вот они окна редактирования (см. текст выделенный жирным шрифтом), - третье и пятое по счету дочернее окно в списке перечисления. Одно из них наверняка принадлежит строке регистрационного имени, а другое - регистрационного номера. Но как узнать какое кому? Воспользовавшись ключом xc, заставим sof-ice выдать более подробную информацию по каждому из окон:
HWND -xc Hwnd : 0D0486 (A0368EF8) Class Name : Edit Module : CRACKME_ Window Proc : 6C291B81 (SuperClassed from: 77E19896) Win Version : 0.00 Parent : 0B0416 (A0368A88) Next : 0904C6 (A0368FB8) Style : Window Rect : 387, 546, 615, 566 (228 x 20) Client Rect : 2, 2, 226, 18 (224 x 16) : Hwnd : 0D0412 (A03690A8) Class Name : Edit Module : CRACKME_ Window Proc : 6C291B81 (SuperClassed from: 77E19896) Win Version : 0.00 Parent : 0B0416 (A0368A88) Next : 0A047C (A0369168) Style : Window Rect : 387, 572, 615, 592 (228 x 20) Client Rect : 2, 2, 226, 18 (224 x 16)
Листинг 42 получение координат окон редактирования (строка с координатами, выделена жирным шрифтом, а координаты верхнего левого угла окна взяты в рамочку)
Как легко установить по координатам вершин окон, первое из них находится на 26 пикселей выше другого (546 против 572), следовательно первое окно - окно регистрационного имени, а второе - окно регистрационного номера.
Теперь, когда порядковые номера окон редактирования известны можно накрапать следующую несложную программку:
// ПЕРЕЧИСЛЕНИЕ ДОЧЕРНИХ ОКОН crackme // =========================================================================== // получаем хэндлы всех интересующих нас окон // (порядок окон определяем либо экспериментально, либо тестовым прогоном // с отладочным выводом информации по каждому из окон) BOOL CALLBACK EnumChildWindowsProc(HWND hwnd,LPARAM lParam) { static N = 0; switch(++N) { case 3: // окно с именем пользователя username = hwnd; break; case 4: // text со строкой "reg. num." hackreg = hwnd; break; case 5: // окно для ввода регистрационного номера regnum = hwnd; break; case 6: // конопка ввода input_but = hwnd; return 0; } return 1; }
Листинг 43 определение дескрипторов элементов управления по их порядковым номерам в списке перечисления
Теперь перейдем непосредственно к технике эмуляции ввода. Ну, ввод/вывод текста в окна редактирования больших проблем не вызывает: WM_SETTEXT/WM_GETTEXT и все пучком, а вот "программно" нажать на кнопку несколько сложнее. Но ведь вам же хочется, чтобы программа не только ввела в соответствующие поля всю необходимую регистрационную информацию, но и самостоятельно долбанула по <Enter>, чтобы закончить ввод?!
Как показывает практика, посылка сообщения BM_SETSTATE элементу управления типа "кнопка" не приводит к ее нажатию. Почему? Наша ошибка заключается в том, что для корректной эмуляции ввода мы во-первых, должны установить фокус (WM_SETFOCUS), а после перевода кнопки в состояние "нажато" этот фокус убить (WM_KILLFOCUS), ведь, как известно даже желторотым пользователям, кнопки срабатывают не в момент их нажатия, но в момент отпускания. Не верите? Поэкспериментируйте с любым приложениям и убедитесь в справедливости сказанного. Кстати, забавный трюк: если под NT/w2k в сообщение WM_KILLFOCUS передать недействительный дескриптор окна, получающего на себя бразды правления, то операционная система по понятным соображениям не передаст фокус несуществующему окну, но у активного окна фокус все-таки отберет. Windows 9x, напротив, оставляет фокус активного окна неизменным! Вот такая разница между двумя операционными системами. Еще одна делать на последок. Если в роли убийцы фокуса выступает функция SendMessage по поток, эмулирующий ввод, блокируется вплоть до того момента, пока обработчик нажатия кнопки не возвратит циклу выборки сообщений своего управления. Чтобы этого не произошло, - используйте функцию PostMessage, которая посылает убийцу фокуса и, не дожидаясь от него ответа, как ни в чем не бывало продолжает выполнение.
Рисунок 6 - 0х00D "автоматическое" считывание имени пользователя, ввод регистрационного номера и эмуляция нажатия на клавишу "ввод"
Испытаем наш автоматический регистратор? (file://CD/SRC/crack-me58DD2D69h/HACKGEN2/autocrack.c). Запустив защищенную программу и при желании заполнив поле имени пользователя (если его оставить пустым, автоматический регистратор использует имя по умолчанию), мы дрожащей от волнения рукой запускаем autocrack.exe: Держите нас! Это сработало! Вот это автоматизация! Вот это хакерство! Вот это мы понимаем!
как сделать исполняемые файлы меньше
Даже будучи написанным на чистом ассемблере, исполняемый файл генератора регистрационных номеров занимает целых 16 килобайт! Хорошенький монстр, нечего сказать! Хакерам, чей первый компьютер был IBM PC с процессором Pentium-4, может показаться, что 16 килобайт это просто фантастически мало, однако еще в восьмидесятых годах существовали компьютеры с объемом памяти равным этому числу! Впрочем, зачем нам так далеко ходить, - откроем первое издание настоящей книги: "Без текстовых строк исполняемый файл [генератора] занимает менее пятидесяти байт и еще оставляет простор для оптимизации". Сравните пятьдесят байт и шестнадцать килобайт, - переход с MS-DOS на Windows увеличил аппетит к памяти без малого в триста раз!
Вообще-то, с чисто потребительской точки зрения никакой проблемы в этом нет. Размеры жестких дисков сегодня измеряются сотнями гигабайт и лишний десяток килобайт особой погоды не делает. К тому же, наш исполняемый файл замечательно ужимается pkzip'ом до семисот с небольшим байт, что существенно для его передачи по медленным коммуникационным сетям, - да только где такие нынче найдешь?!
С чисто же эстетической точки зрения держать у себя такой файл действительно нехорошо. Обиднее всего, что на 99% генератор состоит из воздуха и воды, - нулей, пошедших на вырывание секций по адресам, кратным 4Кб. Три секции (кодовая секция .text, секция данных .data и таблица импорта .itable) плюс PE-заголовок, - вместе они эти самые 16 Кб и создают. Полезного же кода в исполняемом файле просто пшик - немногим менее двухсот байт. Конечно, двести это не пятьдесят и с переходом на Windows мы все равно проигрываем и в компактности, и в скорости, но все-таки кое-какой простор для оптимизации у нас имеется.
Начнем с того, что прикажем линкеру использовать минимальную кратность выравнивания из всех имеющихся, - составляющую всего четыре байта. Указывав в командной строке ключ "/ALIGN:4" мы сократим размер исполняемого файла с 16.384 до 1.032 байт! Согласитесь, что с таким размером уже можно жить!
Причем, это далеко не предел оптимизации! При желании можно: а) выкинуть MS-DOS stub, который все равно бесполезен; б) подчистить IMAGE_DIRECTORY; в) использовать незадействованные поля OLD EXE/PE-заголовков для хранения глобальных переменных; г) объединить секции .text, .data, .rdata в одну общую секцию, сведя тем самым эффективную кратность выравнивая к одному и высвободив еще трохи места за счет ликвидации двух секций. Словом, возможности для самовыражения под Windows все-таки имеются!
Перехват WM_GETTEXT
Использование функций GetWindowText и GetDlgItemText - не единственный путь для извлечения содержимого окна редактирования. Как было показано в предыдущей главе, ту же самую операцию можно осуществить и посылкой сообщения WM_GETTEXT (и некоторые разработчики защитных механизмов именно так и поступают). Достоинство этого метода в том, что он легко и элегантно отсекает большую армию wannabe-хакеров, ничего не смыслящие ни в программировании, ни в операционных системах, но прочитавшие FAQ "ED!SON's Windows 95 Cracking Tutorial v1.oo" и мало помалу пытающие что ни будь взломать.
Чтение регистрационного имени пользователя в обход функций GetWinowsText/ GetDlgItemText ставит таких неопытных хакеров в тупик. Попытка поставить точку останова на SendMessageA так же ничего не дает - уж слишком интенсивно она вызывается и, если не предпринять дополнительных ухищрений, мы просто утонем в море этих вызовов! Как автоматически отсечь все лишние срабатывания? Обратимся к прототипу функции SendMessage. Согласно Platform SDK он выглядит так:
LRESULT SendMessage( HWND hWnd, // handle of destination window (дескриптор окна-получателя) UINT Msg, // message to send (посылаемое сообщение) WPARAM wParam, // first message parameter (первый параметр сообщения) LPARAM lParam // second message parameter (второй параметр сообщения) );
Листинг 44 прототип функции SendMessage
Пара аргументов hWnd + Msg позволяют однозначно идентифицировать любое действие, происходящее в системе. Применительно к данному случаю, чтобы перехватить обращение к строке редактирования мы должны узнать дескриптор соответствующего ей окна. А как его узнать? Даем отладчику команду "HWND" и смотрим:
:hwnd Handle Class WinProc TID Module 240428 #32770 (Dialog) 6C291B81 400 crackme 110468 Edit 6C291B81 400 crackme 0B04A4 Button 77E18721 400 crackme
Листинг 45 определение дескриптора окна редактирования под soft-ice
Вот он, дескриптор! (см. обведенное рамкой число в самой первой колонке слева). Следовательно, нас будут интересовать все вызовы SendMessage(0x110468, WM_GETTEXT,:), а все остальные мы можем и проигнорировать. Интеллектуальность ранних версий soft-ice была недостаточно велика для автоматизации столь ювелирной работы и "игнорировать" лишние вызовы хакерам приходилось вручную. Хакеры, начинающие свой жизненный путь с soft ice 3.25 или выше, наверное, и не представляют: каким каторжным был этот труд! Сегодня же практически все отладчики оснащены поддержкой условных точек останова и львиную долю рутиной работы берут на себя. Давайте попробуем "объяснить" отладчику нашу ситуацию с WM_GETTEXT и посмотрим справиться ли он с ней или нет. К сожалению, soft-ice не поддерживает "прозрачной" адресации аргументов и потому их смещения относительно вершины стека мы должны вычислять самостоятельно. Впрочем, невелика проблема! Памятуя о том, что все API-функции придерживаются соглашения stdcall, т. е. передают свои аргументы справа налево, можно легко рассчитать, что дескриптор окна лежит на четыре байта ниже ESP, а непосредственно под ним располагается и код посылаемого окну сообщения. Следовательно, команда установки соответствующего точки останова будет выглядеть приблизительно так: "bpx SendMessageA IF (*(esp + 4) == 110468) && ( *(esp+8) == WM_GETTEXT)", однако, это не единственный вариант. Если хотите выражение "*(esp+4)" можете заменить на синтаксически более короткое, но полностью эквивалентное по смыслу: "esp->4". Более подробную информацию о формате условных точек останова вы найдете в прилагаемой к отладчику документации. Здесь же нас в первую очередь интересует то, что установленная нами точка останова действительно срабатывает и срабатывает правильно:
:bpx SendMessageA IF (esp-> == 110468) && (esp->8 == WM_GETTEXT) x /* нажимаем на кнопку "ENTER" ломаемого приложения */ Break due to BPX USER32!SendMessageA IF ((*((ESP+4))==0x140430)&&((ESP->8)==0xD)) (ET=2.83 seconds) USER32!SendMessageA 001B:77E1A57C PUSH EBP 001B:77E1A57D MOV EBP,ESP 001B:77E1A57F PUSH ESI 001B:77E1A580 MOV ESI,[EBP+0C]
Листинг 46 перехват чтения содержимого окна путем посылки ему WM_GETTEXT
Адрес буфера-приемника считываемой строки лежит в стеке на 10h байт ниже его вершины и при желании мы можем его узнать:
:? esp->10 0012FA40 0001243712 " ·@"
Листинг 47 определение адреса буфера-приемника, в который помещается считываемая строка
В ответ на команду "? esp->10" soft-ice сообщает "12FA40". Запомним (записав на бумажке) полученное смещение мы "выпрыгиваем" из функции по команде "P RET" и смотрим содержимое буфера:
:p ret :d 12FA40 0010:0012FA40 4B 72 69 73 20 4B 61 73-70 65 72 73 6B 79 00 00 Kris Kaspersky.. 0010:0012FA50 38 FA 12 00 40 27 2F 00-BC FA 12 00 49 1D E6 77 8...@'/.....I..w 0010:0012FA60 D8 23 29 6C 00 23 40 00-11 01 00 00 9C FA 12 00 .#)l.#@......... 0010:0012FA70 AE 22 29 6C 54 FE 12 00-EA 03 00 00 00 00 00 00 .")lT...........
Листинг 48 просмотр содержимого буфера
Это сработало! Мы рассекретили адрес считываемой строки и теперь нам ничего не стоит поставить на него точку останова для отслеживания всех попыток обращения к последнему (как вариант: можно просто немного потрассировать код в надежде на то, что защитный механизм окажется где-то поблизости).
Вообще-то, для перехвата сообщений существует специальная команда - "BMSG" (Break on MesSaGe), но по малопонятным для меня причинам, в некоторых версиях soft-ice она не работает, выдавая сообщение "Invalid window handle" даже при попытке установить точку останова на заведомо корректный дескриптор окна!