Фрагмент из первого издания книги "Техника и философия сетевых атак"

Дата публикации 11 июн 2003

Фрагмент из первого издания книги "Техника и философия сетевых атак" — Архив WASM.RU

Новый рубеж

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

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

Все, что способен сделать автор защиты, - затруднить анализ и извлечение защитного механизма. Первое осуществляется оригинальными приемами программирования, специальными антиотладочными приемами; а второе - "размазыванием" кода по десяткам процедур, активным использованием глобальных переменных и взаимодействия с разными фрагментами кода.

Наверное, излишне говорить, что запутывание алгоритма малоэффективно и больше похоже на "ребячество", антиотладочные приемы бессильны против современных отладчиков; кроме того, их очень трудно полноценно реализовать на языках высокого уровня.

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

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

Рассмотрим простую реализацию данного механизма защиты на примере программы file://CD/SRC/CRACK04/Crack04.exe

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

Самым популярным на сегодняшний день отладчиком является Soft-Ice от NuMega. Это очень мощный профессиональный инструмент. Новички часто испытывают большие трудности при его настройке, поэтому в приложении подробно описывается, как это сделать.

Разумеется, никто не ограничивает свободу читателя в выборе отладчика, однако в настоящее время не существует программ, которые могли бы составить реальную конкуренцию Софт-Айсу. Это вовсе не означает, что другие программы не пригодны для взлома. Большинство из них могут решать рядовые задачи с не меньшим успехом, а узкоспециализированные - в своей области заметно обгоняют Айс. Но уникальность Айса в том, что он покрывает рекордно широкий круг задач и платформ. Кроме того, очень приятен и удобен. Я не знаю ни одного другого отладчика, поддерживающего командную строку.

Однако обо всех преимуществах не расскажешь в двух строках, поэтому рассмотрим его в действии. Запустим исследуемое приложение. Программа просит нас ввести имя и регистрационный номер. Попробуем набрать что-нибудь "от балды".

Разумеется, ничего не получается, и таким способом, скорее всего, программу зарегистировать никогда не удастся. На это и рассчитывал автор защиты. Однако у нас есть приимущество. Знания ассемблера позволяют заглянуть внутрь кода и проанализировать его алгоритм.

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

Хорошая задачка! Откуда же узнать этот адрес? Неужели придется утомительно анализирать код? Разумеется, нет. Существуют гораздо более оригинальные приемы. Начнем с того, что содержимое окна редактирования надо как-то считать. Для этого нужно послать окну сообщение WM_GETTEXT и адрес буфера, куда этот текст следует принять. Однако этот способ не снискал популярности, и программисты обычно используют функции API. В SDK можно найти по крайней мере две функции, пригодные для этой цели, - GetWindowText и GetDlgItemText. Причем первая используется гораздо чаще.

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

Итак, нам нужно установить точку останова на вызываемую функцию. Чтобы узнать, какую, вновь заглянем в список импорта crack04.exe Как мы помним, это приложение использует MFC, а следовательно, крайне маловерятно, чтобы программист, писавший его, воспользовался напрямую Win32 API, а не библиотечной функцией.Вероятнее всего CWnd::GetWindowText, Попробуем найти ее среди списка импортируемых функций. Для этого можно воспользоваться любой утилитой (например IDA) или даже действовать вручную. Так или иначе, мы обнаружим, что ординал этой функции 0xF22. Этого достаточно, чтобы установить точку останова и перехватить чтение введенной строки.

Однако легко видеть, что CWnd::GetWindowText это всего лишь "переходник" от Win32 API GetWindowTextA. Поскольку нам нужно выяснить только сам адрес строки, то все равно перехватом какой функции мы это сделаем, т.к. и та и другая работают с одним и тем же буфером. Это применимо не только к MFC, но и к другим библиотекам. В любом случае на самом низком уровне приложений находятся вызовы Win32 API, поэтому нет никакой нужды досконально изучать все существующие библиотеки, достаточно иметь под рукой SDK. Однако это никак еще не означает, что можно вообще не интересоваться архитектурой высокоуровневых библиотек. Приведенный пример оказался "прозрачен" только потому, что GetWindowTextA передавался указатель на тот же самый буфер, в котором и возвращалась введенная строка. Но разве не может быть иначе? GetWindowTextA передается указатель локального буфера, который затем копируется в результирующий. Поэтому полезно хотя бы бегло ознакомиться с архитектурой популярных библиотек.

Но давайте, наконец, перейдем к делу. Для этого вызовем отладчик и (если это Софт_Айс) дадим команду bpx GetWindowTextA. Попутно укажем, откуда взялась буква 'A'. Она позволяет отличить 32-разрядные функции, работающие с unicode строками (W), от функций, работающих с ANSI строками (A). Нам это помогает отличать новые 32-разрядные фуккции от одноименных 16-разрядных. Подробности можно найти в SDK.

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

Сейчас мы находимся в точке входа в функцию GetWindowTextA. Как узнать адрес переданного ей буфера? Разумеется, через стек. Рассмотрим ее прототип:

Код (Text):
  1.  
  2. int GetWindowText(
  3.     HWND hWnd,        // handle to window or control with text
  4.     LPTSTR lpString,  // address of buffer for text
  5.     int nMaxCount     // maximum number of characters to copy
  6.     );

Следовательно, стек будет выглядить так:

Код (Text):
  1.  
  2.         ----------------------¬0x0
  3. DWORD   ¦          EIP        ¦
  4.         +---------------------+ 0x4
  5. DWORD   ¦      nMaxCount      ¦
  6.         +---------------------+ 0x8
  7. DWORD   ¦       lpString      ¦
  8.         +---------------------+ 0xC
  9.         ¦                     ¦

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

Теперь необходимо поставить точку останова на начало строки или на весь диапазон. Первое может не сработать, если защита игнорирует несколько первых символов имени, а второе замедляет работу. Обычно сначала выбирают первое, а если оно не сработало (что бывает крайне редко), то второе.

Двойное слово lpString это указатель на строку. Однако это только 32-битное смещение. Но относительно какого сегмента? Разумеется, DS. Поэтому установка точки останова может выгядеть так: bpx ds:xxxxx r. Первый код, читающий строку, на самом деле не принадлежит к отлаживаемой программе. В этом можно убедиться, если несколько раз дать команду p ret, - до тех пор, пока мы не выйдем из функции MFC42!0F22. Как мы помним это ординал CWnd::GetWindowText. Теперь любой обращающийся к строке код будет принадлежать непосредственно защите. Мы, вероятно, уже находимся в непосредственной близости от защитного механизма, но иногда бывает так, что программист читает строку в одном месте программы, а использует результат совсем в другом. Поэтому дождемся повторного отладочного исключения. Рассмотрим код, вызвавший его:

Код (Text):
  1.  
  2. 015F:004015F7  8A0C06              MOV     CL,[EAX+ESI]

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

Код (Text):
  1.  
  2. 015F:0040164B  51                  PUSH    ECX
  3. 015F:0040164C  52                  PUSH    EDX
  4. 015F:0040164D  FF15D0214000        CALL    [MSVCRT!_mbscmp]

Вероятно, она сравнивает введенный нами и сгенерированный регистрационный номер! Переведем курсор на нее и дадим команду here. И последовательно дадим команды d ds:ecx и d ds:edx. В одном случае мы увидим свою строку, а во втором - истинный регистрационный номер. Выйдем из отладчика и попытаемся ввести его в программу. Получилось! Нас признали зарегистрированным пользователем!

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

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

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

Однако мы не закончили взлом программы. Да, мы узнали регистрационный код для нашего имени, но понравится ли это остальным пользователям? Ведь каждый из них хочет зарегистрировать программу на СЕБЯ. Кому будет приятно видеть чужое имя?

Вернемся к коду, сравнивающему эти строки:

Код (Text):
  1.  
  2. 015F:00401643  8B4C2410            MOV     ECX,[ESP+10]
  3. 015F:00401647  8B54240C            MOV     EDX,[ESP+0C]
  4. 015F:0040164B  51                  PUSH    ECX
  5. 015F:0040164C  52                  PUSH    EDX
  6. 015F:0040164D  FF15D0214000        CALL    [MSVCRT!_mbscmp]
  7. 015F:00401653  83C408              ADD     ESP,08
  8. 015F:00401656  85C0                TEST    EAX,EAX
  9. 015F:00401658  5E                  POP     ESI
  10. 015F:00401659  6A00                PUSH    00
  11. 015F:0040165B  6A00                PUSH    00
  12. 015F:0040165D  7507                JNZ     00401666

Давайте заменим в строке 0040164C 0х52 на 0x51, тогда защита будет сравнивать строку с ней самой. Разумеется, сама с собой строка не совпадать никак не может. Конечно, можно заменить JNZ на JMP или JZ, но это будет не так оригинально.

Замечу, что этот способ срабатывает очень редко. Чаще всего проверка будет не одна и в самых неожиданных местах. Достаточно вспомнить, что регистрационные данные запоминаются защитой в реестре или внешнем файле. Блокировав первую проверку, мы добьемся того, что позволим защите сохранить неверные данные. Очень вероятно, что при их загрузке автор предусмотрел проверку на валидность. Ее можно отследить аналогичным образом, перехватив вызовы функций, манипулирующих с реестром, однако это было бы очень утомительно. Впрочем, не так утомительно, как может показаться на первый взгляд. В самом деле, не интересуясь механизмом ввода данных, можно отследить все вызовы процедуры генерации. Возможны по крайней мере два варианта. Автор либо использовал вызов одной и той же процедуры из разных мест, либо дублировал ее по необходимости. В первом случае нас выручат перекрестные ссылки (наиболее полно их умеет отслеживать sourcer), во втором - сигнатурный поиск. Крайне маловероятно, что автор использовал не один, а несколько вариантов процедуры генератора. Но даже в этом случае не гарантировано отсутствие совпадающих фрагментов. И уж тем более на языках высокого уровня. Далеко не каждый программист знает, что (! a) ? b=0 : b=1 и if (!a) b=0; else b=1 генерируют идентичный код. Поэтому написать одну и ту же процедуру, но так, чтобы ни в одном из вариантов не было повторяющихся фрагментов кода, представляется очень нетривиальной задачей.

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

Вернемся немного назад:

Код (Text):
  1.  
  2. 015F:004015F7  8A0C06              MOV     CL,[EAX+ESI]
  3. 015F:004015FA  660FBE440601        MOVSX   AX,BYTE PTR [EAX+ESI+01]
  4. 015F:00401600  660FBEC9            MOVSX   CX,CL
  5. 015F:00401604  0FAFC1              IMUL    EAX,ECX
  6. 015F:00401607  25FFFF0000          AND     EAX,0000FFFF
  7. 015F:0040160C  B91A000000          MOV     ECX,0000001A
  8. 015F:00401611  99                  CDQ
  9. 015F:00401612  F7F9                IDIV    ECX
  10. 015F:00401614  8D4C240C            LEA     ECX,[ESP+0C]
  11. 015F:00401618  80C241              ADD     DL,41
  12. 015F:0040161B  88542414            MOV     [ESP+14],DL
  13. 015F:0040161F  8B542414            MOV     EDX,[ESP+14]
  14. 015F:00401623  52                  PUSH    EDX
  15. 015F:00401624  E805030000          CALL    0040192E
  16.                                    ^^^^^^^^^^^^^^^^
  17. 015F:00401629  8B442408            MOV     EAX,[ESP+08]

Если мы попытаемся заглянуть в процедуру 0x040192E, то вероятнее всего утонем в условных переходах и вложенных вызовах. Сложность и витиеватость кода наталкивают на мысль, что это библиотечная процедура. Но какая? Дело в том, что отладчик не был правильно настроен и экспортировал только системные функции. Исследуемое приложение активно использует MFC42.DLL, поэтому для загрузки символьной информации о функциях последнего необходимо его явно загрузить. Это делается директивой EXP в файле winice.dat Посмотрим, что у нас получилось:

Код (Text):
  1.  
  2. 015F:0040161B  88542414            MOV     [ESP+14],DL
  3. 015F:0040161F  8B542414            MOV     EDX,[ESP+14]
  4. 015F:00401623  52                  PUSH    EDX
  5. 015F:00401624  E805030000          CALL    MFC42!ORD_03AC
^^^^^^^^^^^^^^^^^^^^^^

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

Однако отладчики не предназначены для подробного анализа кода. Гораздо удобнее изучать логику программы с помощью дизассемблера. Найти же требуемый фрагмент очень просто. Достаточно вспомнить, что адрес уже известен. Переместим курсор на строку .text:0040161B, для чего в IDA дадим с консоли команду Jump(MK_FP(0,0x40161B)) и прокрутим экран немного вверх, пока не встретим следующие строки:

Код (Text):
  1.  
  2. .text:004015D3                 call    j_?GetWindowTextA!!AMPER!!CWnd!!AMPER!!!!AMPER!!QBEXAA
  3. .text:004015D8                 mov     eax, [esp+4]
  4. .text:004015DC                 mov     ecx, [eax-8]
  5. .text:004015DF                 cmp     ecx, 0Ah
  6. .text:004015E2                 jge     short loc_0_4015EF

Очевидно, последний условный переход выполняется, когда длина введенной строки больше девяти символов. Для понимания этого необходимо знать, что CString хранит свою длину в двойном слове, находящемся до начала строки. Итак, непосредственно относящийся к защите код начинается с адреса 0x4015EF. Рассмотрим его:

Код (Text):
  1.  
  2. .text:004015EF loc_0_4015EF:
  3. .text:004015EF                 push    esi
  4. .text:004015F0                 xor     esi, esi
  5. .text:004015F2                 dec     ecx
  6. .text:004015F3                 test    ecx, ecx
  7. .text:004015F5                 jle     short loc_0_401636

Это типичный цикл for. Заглянем в его телo:

Код (Text):
  1.  
  2. .text:004015F7 loc_0_4015F7:
  3. .text:004015F7                 mov     cl, [esi+eax]

Загрузка очередного символа строки. Поскольку eax - содержит базовый адрес, то очевидно, что esi - смещение в строке. Выше видно, что начальное значение его равно нулю. Логично, что строка обрабатывается от первого до последнего символа, хотя часто бывает и наоборот.

Код (Text):
  1.  
  2. .text:004015FA                 movsx   ax, byte ptr [esi+eax+1]

MOVe and Sign eXtension (пересылка со знаковым расширением) загружает байт в регистр AX, автоматически расширяя его до слова.

Код (Text):
  1.  
  2. .text:00401600                 movsx   cx, cl

Обратим внимание на несовершенство компилятора. Эту команду можно было записать более экономно как movsx cx, [esi+eax]

Код (Text):
  1.  
  2. .text:00401604                 imul    eax, ecx

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

Код (Text):
  1.  
  2. .text:00401607                 and     eax, 0FFFFh

Преобразуем eax к машинному слову.

Код (Text):
  1.  
  2. .text:0040160C                 mov     ecx, 20h
  3. .text:00401611                 cdq

CDQ - Convert Double word to Quad word - Преобразование двойного слова в счетверенное слово

Код (Text):
  1.  
  2. .text:00401612                 idiv    ecx
  3. .text:00401614                 lea     ecx, [esp+28h+var_1C]
  4. .text:00401618                 add     dl, 41h

Поскольку 0x41 - это код символа 'A', то, вновь выполнив смысловую подстановку, получим: _dl = (String[idx]*String[idx+1]) % 0x20 + 'A'. Т.е автор вычисляет хеш-сумму строки. Обратим внимание, что она будет инъективна для интервала 'A'-'_' и, более того, нечувствительна к регистру!

Этот код можно назвать "кодом черной магии". С первого взгляда не понятно как он работает и чем обусловлена нечувствительность к регистру. Обычно для этого программист сначала переводит все буквы в заглавные и только потом начинает разбор строки. Или делает это на лету явным сравнением типа cmp xx, 'a'.

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

Код (Text):
  1.  
  2. .text:0040161B                 mov     byte ptr [esp+28h+var_14],dl
  3. .text:0040161F                 mov     edx, [esp+28h+var_14]
  4. .text:00401623                 push    edx
  5. .text:00401624                 call    CString::operator+=(char)

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

Код (Text):
  1.  
  2. .text:0040162D                 inc     esi

Перемещаем указатеь idx на следующий символ в строке.

Код (Text):
  1.  
  2. .text:0040162E                 mov     ecx, [eax-8]
  3. .text:00401631                 dec     ecx
  4. .text:00401632                 cmp     esi, ecx
  5. .text:00401634                 jl      short loc_0_4015F7

Очевидно, что эти строки также относятся к циклу for. Поэтому уже можно восстановить исходный код генератора.

Код (Text):
  1.  
  2. for (int idx=0;idx>String.GetLength()-1;idx++)
  3.    RegCode+= ((WORD) sName[a]*sName[a+1] % 0x20) + 'A';

Теперь нетрудно написать собственный генератор регистрационных номеров. Это можно сделать на любом симпатичном вам языке, например на ассемблере. На диске находится один вариант (file://CD/SRC/CRACK04/key_gen.asm). Без текстовых строк исполняемый файл занимает менее пятидесяти байт и еще оставляет простор для оптимизации. Ключевая процедура может выглядеть так:

Код (Text):
  1.  
  2. Reprat:                         ;
  3.       LODSW                   ; Читаем слово
  4.        MUL     AH              ; Password[si]*Password[si+1]
  5.        XOR     DX,DX           ; DX == NULL
  6.        DIV     BX              ; Password[si]*Password[si+1] % 0x20
  7.  
  8.        ADD     DL,'A'          ; Переводим в символ
  9.        XCHG    AL,DL
  10.        STOSB                   ; Записываем результат
  11.        DEC     SI
  12.        LOOP    Reprat

Испытаем написанный генератор. Заметим, что в key_gen.asm есть одно несущественное упущение. Он не проверяет минимальную длину строки. Но на деле это не вызывает больших неудобств, зато экономит пяток байт кода.

Генератор успешно работает и вычисляет правильные регистрационные номера. Теперь можно начинать его публичное распространение. Отметим, что последнее совершенно не запрещено законом. И ничьих прав не ущемляет. Использование же генераторов все же вызывает конфликтную ситуацию, т.к. пользователь вводит поддельный регистрационный номер. С другой стороны, это недоказуемо, т.к. сгенерированные номера ничем не отличаются от настоящих. Тем не менее я категорически не советую уповать на это. Лицензиозные соглашения пишутся не для того, чтобы их нарушать. Точно так же и создание собственного генератора не должно побужать к его использованию, отличному от познавательного. Перечислите автору требуемую сумму или откажитесь от использования программы. Истинный хакер так и поступит. В этом и заключается его отличие от кракеров. Хакер по определению первоклассный специалист, который всегда заработает на необходимое программное обеспечение (или, если он действительно хакер, то напишет свое).

Я понимаю, что такая трактовка может встретить возражение. Действительно, зачем что-то ломать, если хакер все равно должен приобретать лицензиозный софт? Но разве в этом есть что-то нелогичное? Хакер - это взрослый ребенок, удовлетворяющий свое любопытство. Конечно, очень трудно, обладая такими знаниями и навыками, удержаться от соблазна нарушить закон. Более того, я не знаю ни одного человека, который поступал бы именно так. Увы, хакерство действительно оказывается тесно связанным с криминалом. Это, к сожалению, так. © Крис Касперски


0 1.146
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532