Фундаментальные основы хакерства

         

Самомодифицирующийся код в современных операционных системах


"- Это мы говорим, будто мы выдумываем. На самом деле все давным-давно выдумано."

Братья Стругацкие "Трудно быть богом"

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

В середине девяностых началась массовая миграция пользователей с MS-DOS на Windows 95\Windows NT, и разработчиком пришлось задуматься о переносе накопленного опыта и приемов программирования на новую платформу – от бесконтрольного доступа к "железу", памяти, компонентам операционной системы и связанным с ними хитроумными трюками программирования пришлось отвыкать. В частности стала невозможна непосредственная модификация исполняемого кода приложений, поскольку Windows защищает его от непреднамеренных изменений. Это привело к рождению нелепого убеждения, дескать, под Windows создание самомодифицирующегося кода вообще невозможно, по крайней мере, без использования VxD и недокументированных возможностей операционной системы.

На самом деле существует по крайней мере два документированных способа изменения кода приложений, одинаково хорошо работающих как под управлением Windows 95\Windows 98\Windows Me, так и под Windows NT\Windows 2000, и вполне удовлетворяющихся привилегиями гостевого пользователя.

Во-первых, kernel32.dll экспортирует функцию WriteProcessMemory, предназначенную, как и следует из ее названия, для модификации памяти процесса. Во-вторых, практически все операционные системы, включая Windows и LINUX, разрешают выполнение и модификацию кода, размещенного в стеке.

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

Материал, изложенный в настоящей главе, большей частью ориентирован на компилятор Microsoft Visual C++ и 32-разрядный исполняемый код. Под Windows 3.x приведенные примеры работать не будут. Но это вряд ли представляет существенную проблему - доля машин с Windows 3.x на рынке очень невелика, поэтому, ими можно полностью пренебречь.



четвертый. Знакомство с отладчиком


Оставь свои мозги за дверью и внеси сюда только тело

Фредерик Тейлор

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

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

– отслеживание обращений на запись/чтение/исполнение к заданной ячейке (региону) памяти, далее по тексту именуемое "бряком" ("брейком");

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

на уровне драйверов реализованы очень немногие защиты);

– отслеживание загрузки DLL и вызова из них таких-то функций, включая системные компоненты (как мы увидим далее – это основное оружие современного взломщика);

– отслеживание вызова программных/аппаратных прерываний (большей частью уже не актуально, - не так много защит балуется с прерываниями);



– отслеживание сообщений посылаемых приложением окну;

– и, разумеется, контекстный поиск в памяти.

Как именно делает отладчик – пока знать необязательно, достаточно знать, что он это умеет и все. Куда актуальнее вопрос, – какой отладчик умеет это делать? Широко известный в пользовательских кругах Turbo Debugger на само деле очень примитивный и никчемный отладчик – очень мало хакеров им что-то ломает.

Самое мощное и универсальное средство – Soft-Ice, сейчас доступный для всех Windows-платформ (а когда он поддерживал лишь одну Windows 95, но не Windows NT). Последняя на момент написания книги, четвертая версия, не очень-то стабильно работалает

с моим видеоадаптером, поэтому пришлось приходится ограничитваться более ранней, но зато устойчивой версией 3.25.



первый. Разминочный.


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

Аноним

Алгоритм простейшего механизма аутентификации состоит в посимвольном сравнении введенного пользователем паролем с эталонным значением,

хранящимся либо в самой программе (как часто и бывает), либо вне ее, например, в конфигурационном файле или реестре (что встречается реже).

Достоинство такой защиты – крайне простая программная реализация. Ее ядро состоит фактически из одной строки, котораяую

на языке Си обычно выглядит можно записать так: – "if (strcmp(&введенный пароль, &эталонный пароль)) { /* Пароль неверен */} else {/* Пароль ОК */}"

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

// Простейшая система аутентификации

// посимвольное сравнение пароля

#include <stdio.h>

#include <string.h>

#define PASSWORD_SIZE 100

#define PASSWORD      "myGOODpassword\n"

// этот перенос нужен затем, чтобы  ^^^^

// не выкусывать перенос из строки,

// введенной пользователем

int main()

{

// Счетчик неудачных попыток аутентификации

int count=0;

// Буфер для пароля, введенного пользователем

char buff[PASSWORD_SIZE];

// Главный цикл аутентификации

for(;;)

{

// Запрашиваем и считываем пользовательский

// пароль

printf("Enter password:");

fgets(&buff[0],PASSWORD_SIZE,stdin);

// Сравниваем оригинальный и введенный пароль

if (strcmp(&buff[0],PASSWORD))

// Если пароли не совпадают – "ругаемся"

printf("Wrong password\n");

// Иначе (если пароли идентичны)

// выходим из цикла аутентификации

else break;

// Увеличиваем счетчик неудачных попыток

// аутентификации и, если все попытки


// исчерпаны – завершаем программу
if
(++count>3) return
–1;
}
// Раз мы здесь, то пользователь ввел правильный пароль
printf("Password OK\n");
}
Листинг 1 Пример простейшей системы аутентификации
 
В популярных кинофильмах крутые хакеры легко проникают в любые жутко защищенные системы, каким-то непостижимым образом угадывая искомый пароль с нескольких попыток. Почему бы неи
попробовать пойти их путем?
Не так уж редко пароли представляют собой осмысленные слова, наподобие "Ferrari", "QWERTY", имена любимых хомячков, названия географических пунктов и т.д. Угадывание пароля сродни гаданию на кофейной гуще – никаких гарантий на успех нет, и остается рассчитывать на одно лишь везение. А удача, как известно, птица гордая – палец ей в рот не клади. Нет ли более надежного способа взлома?
Давайте подумаем – раз эталонный пароль хранится в теле программы, то, если он не зашифрован каким-нибудь хитрым образом, его можно обнаружить тривиальным просмотром двоичного кода программы. Перебирая все, встретившиеся в ней текстовые строки, начиная с тех, что более всего смахивают на пароль, мы очень быстро подберем нужный ключ и "откроем" им программу!
Причем, область просмотра можно существенно сузить, – в подавляющем большинстве случаев компиляторы размешают все инициализированные переменные в сегменте данных (в PE-файлах он размещается в секции ".data"). Исключение составляют, пожалуй, ранние Багдадские (Borland-вые в смысле) компиляторы с их маниакальной любовью всовывать текстовые строки в сегмент кода – непосредственно по месту их вызова. Это упрощает сам компилятор, но порождает множество проблем. Современные операционные системы, в отличие от старушки MS-DOS, запрещают модификацию кодового сегмента, и все, размешенные в нем переменные, доступны лишь для чтения. К тому же, на процессорах с раздельной системой кэширования (на тех же Pentium-ах, например) они "засоряют" кодовый кэш, попадая туда при упреждающем чтении, но при первом же к ним обращении вновь загружаются из медленной оперативной памяти (кэша второго уровня) в кэш данных.


В результате – тормоза и падение производительности.
Что ж, пусть это будет секция данных! Остается только найти удобный инструмент для просмотра двоичного файла. Можно, конечно, нажать <F3> в своей любимой оболочке (FAR, DOS Navigator) и, придавив кирпичом <Page Down> любоваться бегущими циферками до тех пор, пока не надоест. Можно воспользоваться любым hex-редактором (QVIEW, HIEW…) – кому какой по вкусу, но в книге по соображениям наглядности я приведу результат работы утилиты DUMPBIN из штатной поставки Microsoft Visual Studio.
Попросим ее распечатать секцию данных (ключ "/SECTION:.data") в "сыром" виде (ключ "/RAWDATA:BYTES"), указав значок ">" для перенаправления вывода в файл (ответ программы занимает много места и на экране помещается один лишь "хвост").
> dumpbin /RAWDATA:BYTES /SECTION:.data simple.exe >filename
RAW DATA #3
00406000: 00 00 00 00 00 00 00 00 00 00 00 00 3B 11 40 00  ............;.@.
00406010: A4 40 40 00 00 00 00 00 00 00 00 00 E0 11 40 00  д@@.........р.@.
00406020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00406030: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
00406040: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..
                                                           ^^^^^^^^^^^^^^ 
00406050: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
00406060: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
00406070: 40 6E 40 00 00 00 00 00 40 6E 40 00 01 01 00 00  @n@.....@n@.....
Смотрите! Среди всего прочего тут есть одна строка до боли похожая на эталонный пароль (в тексте она выделена жирным шрифтом). Испытаем ее? Впрочем, какой смысл – судя по исходному тексту программы,
это действительно искомый пароль, открывающий защиту, словно Золотой Ключик. Только Слишком уж видное место выбрал компилятор для его хранения – пароль не мешало бы запрятать получше.


Один из способов сделать это – насильно поместить эталонный пароль в собственноручно выбранную нами секцию. Такая возможность не предусмотрена стандартном и потому каждый разработчик компилятора (строго говоря, не компилятора, а линкера, но это не суть важно) волен реализовывать ее по-своему (или не реализовывать вообще). В Microsoft Visual C++ для этой цели предусмотрена специальная прагма data_seg, указывающая в какую секцию помещать следующие за ней инициализированные переменные. Неинициализированные переменные по умолчанию располагаются в секции ".bbs" и управляются прагмой bss_seg соответственно.
Добавим в Листинг 1 следующие строки и посмотрим, что из этого у нас получится.
int count=0;
// С этого момента все инициализированные переменные будут
// размещаться в секции ".kpnc"
#pragma data_seg(".kpnc") // точку перед именем ставить
  // не обязательно – просто так
  // принято
char passwd[]=PASSWORD;
#pragma data_seg()
// Теперь все инициализированные переменные вновь будут
// размещаться в секции по умолчанию, т.е. ".data"
char buff[PASSWORD_SIZE]="";
...
if (strcmp(&buff[0],&passwd[0]))
> dumpbin /RAWDATA:BYTES /SECTION:.data simple2.exe >filename
RAW DATA #3
  00406000: 00 00 00 00 00 00 00 00 00 00 00 00 9B 11 40 00  ............Ы.@.
  00406010: 04 41 40 00 00 00 00 00 00 00 00 00 40 12 40 00  .A@.........@.@.
  00406020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00406030: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
  00406040: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
  00406050: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
  00406060: 20 6E 40 00 00 00 00 00 20 6E 40 00 01 01 00 00   n@..... n@.....
  00406070: 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  ................
Ага, теперь в секции данных пароля нет и хакеры "отдыхают"! Но не спешите с выводами.


Давайте сначала выведем на экран список всех секций, имеющихся в файле:
> dumpbin simple2.exe
Summary
2000 .data
1000 .kpnc
      ^^^^
1000 .rdata
4000 .text
Нестандартная секция ".kpnc" сразу же приковывает к себе внимание. А ну-ка глянем, что там в ней?
dumpbin /SECTION:.kpnc /RAWDATA simple2.exe
RAW DATA #4
  00408000: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..
                                                             ^^^^^^^^^^^^^^
Вот он, пароль! Спрятали, называется… Можно, конечно, извратится и засунуть секретные данные в секцию неинициализированных данных (".bss"), служебную RTL-секцию (".rdata") или даже секцию кода (".text") – не все там догадаются поискать, а работоспособность программы такое размещение не нарушит. Но не стоит забывать о возможности автоматизированного поиска текстовых строк в двоичном фале.
Пример реализации такого фильтра приведен в "Приложении" (см. "исходный текст filter.c"). В какой бы секции ни содержался эталонный пароль – фильтр без труда его найдет (единственная проблема – определить какая из множества текстовых строк представляет собой искомый ключ; возможно,
потребуется перебрать с десяток-другой потенциальных "кандидатов").
Правда, если пароль записан в уникоде, его поиск несколько осложняется, т.к. не все утилиты поддерживают эту кодировку, но надеяться, что это препятствие надолго задержит хакера – несколько наивно.

пятый. На сцене появляется IDA


"Реальность такова, какой ее описывает язык" тезис лингвистической относительности Б.Л. Уорфа

С легкой руки Дениса Ричи повелось начинать освоение нового языка программирования с создания простейшей программы “Hello, World!”, -- и здесь не будет нарушена эта традиция. Оценим возможности IDA Pro следующим примером (для совместимости с книгой рекомендуется откомпилировать его с помощью Microsoft Visual C++ 6.0 вызовом “cl.exe first.cpp” в командной строке):

#include <iostream.h>

void main()

{

      cout<<"Hello, Sailor!\n";

}

a) исходный текст программы first.cpp

Компилятор сгенерирует исполняемый файл размером почти в 40 килобайт, большую часть которого займет служебный, стартовый или библиотечный код! Попытка дизассемблирования с помощью таких дизассемблеров как W32DASM (или аналогичных ему) не увенчается  быстрым успехом, поскольку над полученным листингом размером в пятьсот килобайт (!) можно просидеть не час и не два. Легко представить сколько времени уйдет на серьезные задачи, требующие изучения десятков мегабайт дизассемблированного текста.

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

Рисунок 4 “0x000.bmp” Так выглядит результат работы консольной версии IDA Pro 3.6

Рисунок 5 “0x001.bmp” Так выглядит результат работы консольной версии IDA Pro 4.0

Рисунок 6 “0x002.bmp” Так выглядит результат работы графической версии IDA Pro 4.0

С версии 3.8x[1] в IDA появилась поддержка «сворачивания» (Collapsed) функций. Такой прием значительно упрощает навигацию по тексту, позволяя убрать с экрана не интересные в данный момент строки. По умолчанию все библиотечные функции сворачиваются автоматически.

Развернуть функцию можно подведя к ней курсор и нажав <+> на дополнительной цифровой клавиатуре, расположенной справа.
Соответственно, клавиша <-> предназначена для сворачивания.
По окончании автоматического анализа файла “first.exe”, IDA переместит курсор к строке “.text:00401B2C” – точке входа в программу. Среди начинающих программистов широко распространено заблуждение, якобы программы, написанные на Си, начинают выполняться с функции “main”, но в действительности это не совсем так. На самом деле сразу после загрузки файла управление передается на функцию “Start”, вставленную компилятором. Она подготавливает глобальные переменные _osver (билд), _winmajor (старшая версия операционной системы), _winminor (младшая версия операционной системы), _winver (полная версия операционной системы), __argc (количество аргументов командной строки), __argv (массив указателей на строки аргументов), _environ (массив указателей на строки переменных окружения); инициализирует кучи (heap); вызывает функцию main, а после возращения управления завершает процесс с помощью функции Exit.
Наглядно продемонстрировать инициализацию переменных, совершаемую стартовым кодом, позволяет следующая программа.
#include <stdio.h>
#include <stdlib.h>
void main()
{
      int a;
      printf(">Версия
OS:\t\t\t%d.%d\n\
      >Билд:\t\t\t%d\n\
      >Количество агрументов:\t%d\n",\
      _winmajor,_winminor,_osver,__argc);
      for (a=0;a<__argc;a++)
            printf(">\tАгрумент     %02d:\t\t%s\n",a+1,__argv[a]);
      a=!a-1;
      while(_environ[++a]) ;
      printf(">Количество переменных окружения:%d\n",a);
      while(a) printf(">\tПеременная
%d:\t\t%s\n",a,_environ[--a]);
}
a) исходный текст программы CRt0.demo.c
Прототип функции main как будто указывает, что приложение не принимает ни каких аргументов командной строки, но результат работы программы доказывает обратное и на машине автора выглядит так (приводится в сокращенном виде):
>Версия OS:                        5.0
>Билд:                             2195


>Количество агрументов:            1
>     Агрумент    01:               CRt0.demo
>Количество переменных окружения:  30
>     Переменная  29:               windir=C:\WINNT
>...
b) результат работы программы CRt0.demo.c
Очевидно, нет никакой необходимости анализировать стандартный стартовый код приложения, и первая задача исследователя – найти место передачи управления на функцию main. К сожалению, гарантированное решение этой
задачи требует полного анализа содержимого функции “Start”. У исследователей существует множество хитростей, но все они базируются на особенностях реализации конкретных компиляторов[2]
и не могут считаться универсальными.
Рекомендуется изучить исходные тексты стартовых функций популярных компиляторов, находящиеся в файлах CRt0.c (Microsoft Visual C) и c0w.asm (Borland C) – это упросит анализ дизассемблерного листинга.
Ниже, в качестве иллюстрации, приводится содержимое стартового кода программы “first.exe”, полученное в результате работы W32Dasm:
//******************** Program Entry Point ********
:00401B2C 55                      push ebp
:00401B2D 8BEC                    mov ebp, esp
:00401B2F 6AFF                    push FFFFFFFF
:00401B31 6870714000              push 00407170
:00401B36 68A8374000              push 004037A8
:00401B3B 64A100000000            mov eax, dword ptr fs:[00000000]
:00401B41 50                      push eax
:00401B42 64892500000000          mov dword ptr fs:[00000000], esp
:00401B49 83EC10                  sub esp, 00000010
:00401B4C 53                      push ebx
:00401B4D 56                      push esi
:00401B4E 57                      push edi
:00401B4F 8965E8                  mov dword ptr [ebp-18], esp
Reference To: KERNEL32.GetVersion, Ord:0174h
|
:00401B52 FF1504704000            Call dword ptr [00407004]
:00401B58 33D2                    xor edx, edx
:00401B5A 8AD4                    mov dl, ah
:00401B5C 8915B0874000            mov dword ptr [004087B0], edx


:00401B62 8BC8                    mov ecx, eax
:00401B64 81E1FF000000            and ecx, 000000FF
:00401B6A 890DAC874000            mov dword ptr [004087AC], ecx
:00401B70 C1E108                  shl ecx, 08
:00401B73 03CA                    add ecx, edx
:00401B75 890DA8874000            mov dword ptr [004087A8], ecx
:00401B7B C1E810                  shr eax, 10
:00401B7E A3A4874000              mov dword ptr [004087A4], eax
:00401B83 6A00                    push 00000000
:00401B85 E8D91B0000              call 00403763
:00401B8A 59                      pop ecx
:00401B8B 85C0                    test eax, eax
:00401B8D 7508                    jne 00401B97
:00401B8F 6A1C                    push 0000001C
:00401B91 E89A000000              call 00401C30
:00401B96 59                      pop ecx
Referenced by a (U)nconditional or (C)onditional Jump at Address:
|:00401B8D(C)
|
:00401B97 8365FC00                and dword ptr [ebp-04], 00000000
:00401B9B E8D70C0000              call 00402877
Reference To: KERNEL32.GetCommandLineA, Ord:00CAh
|
:00401BA0 FF1560704000            Call dword ptr [00407060]
:00401BA6 A3E49C4000              mov dword ptr [00409CE4], eax
:00401BAB E8811A0000              call 00403631
:00401BB0 A388874000              mov dword ptr [00408788], eax
:00401BB5 E82A180000              call 004033E4
:00401BBA E86C170000              call 0040332B
:00401BBF E8E1140000              call 004030A5
:00401BC4 A1C0874000              mov eax, dword ptr [004087C0]
:00401BC9 A3C4874000              mov dword ptr [004087C4], eax
:00401BCE 50                      push eax
:00401BCF FF35B8874000            push dword ptr [004087B8]
:00401BD5 FF35B4874000            push dword ptr [004087B4]
:00401BDB E820F4FFFF              call 00401000
:00401BE0 83C40C                  add esp, 0000000C
:00401BE3 8945E4                  mov dword ptr [ebp-1C], eax
:00401BE6 50                      push eax
:00401BE7 E8E6140000              call 004030D2


:00401BEC 8B45EC                  mov eax, dword ptr [ebp-14]
:00401BEF 8B08                    mov ecx, dword ptr [eax]
:00401BF1 8B09                    mov ecx, dword ptr [ecx]
:00401BF3 894DE0                  mov dword ptr [ebp-20], ecx
:00401BF6 50                      push eax
:00401BF7 51                      push ecx
:00401BF8 E8AA150000              call 004031A7
:00401BFD 59                      pop ecx
:00401BFE 59                      pop ecx
:00401BFF C3                      ret
a) стартовый код программы “first.exe”, полученный дизассемблером W32Dasm
Иначе выглядит результат работы IDA, умеющей распознавать библиотечные функции по их сигнатурам (приблизительно по такому же алгоритму работает множество антивирусов). Поэтому, способности дизассемблера тесно связаны с его версией и полнотой комплекта поставки – далеко не все версии IDA Pro в состоянии работать с программами, сгенерированными современными компиляторами. (Перечень поддерживаемых компиляторов можно найти в файле “%IDA%/SIG/list”).
00401B2C start      proc near
00401B2C
00401B2C var_20     = dword ptr -20h
00401B2C var_1C     = dword ptr -1Ch
00401B2C var_18     = dword ptr -18h
00401B2C var_14     = dword ptr -14h
00401B2C var_4      = dword ptr -4
00401B2C
00401B2C            push    ebp
00401B2D            mov     ebp, esp
00401B2F            push    0FFFFFFFFh
00401B31            push    offset stru_407170
00401B36            push    offset __except_handler3
00401B3B            mov     eax, large fs:0
00401B41            push    eax
00401B42            mov     large fs:0, esp
00401B49            sub     esp, 10h
00401B4C            push    ebx
00401B4D            push    esi
00401B4E            push    edi
00401B4F            mov     [ebp+var_18], esp
00401B52            call    ds:GetVersion 
00401B58            xor     edx, edx
00401B5A            mov     dl, ah
00401B5C            mov     dword_4087B0, edx
00401B62            mov     ecx, eax


00401B64            and     ecx, 0FFh
00401B6A            mov     dword_4087AC, ecx
00401B70            shl     ecx, 8
00401B73            add     ecx, edx
00401B75            mov     dword_4087A8, ecx
00401B7B            shr     eax, 10h
00401B7E            mov     dword_4087A4, eax
00401B83            push    0
00401B85            call    __heap_init
00401B8A            pop     ecx
00401B8B            test    eax, eax
00401B8D            jnz     short loc_401B97
00401B8F            push    1Ch
00401B91            call    sub_401C30      ; _fast_error_exit
00401B96            pop     ecx
00401B97
00401B97 loc_401B97:                         ; CODE XREF: start+61­j
00401B97            and     [ebp+var_4], 0
00401B9B            call    __ioinit
00401BA0            call    ds:GetCommandLineA
00401BA6            mov     dword_409CE4, eax
00401BAB            call    ___crtGetEnvironmentStringsA
00401BB0            mov     dword_408788, eax
00401BB5            call    __setargv
00401BBA            call    __setenvp
00401BBF            call    __cinit
00401BC4            mov     eax, dword_4087C0
00401BC9            mov     dword_4087C4, eax
00401BCE            push    eax
00401BCF            push    dword_4087B8
00401BD5            push    dword_4087B4
00401BDB            call    sub_401000
00401BE0            add     esp, 0Ch
00401BE3            mov     [ebp+var_1C], eax
00401BE6            push    eax
00401BE7            call    _exit
00401BEC ; ------------------------------------------------------
00401BEC
00401BEC loc_401BEC:              ; DATA XREF: _rdata:00407170¯o
00401BEC            mov     eax, [ebp-14h]
00401BEF            mov     ecx, [eax]
00401BF1            mov     ecx, [ecx]
00401BF3            mov     [ebp-20h], ecx
00401BF6            push    eax
00401BF7            push    ecx
00401BF8            call    __XcptFilter
00401BFD            pop     ecx
00401BFE            pop     ecx
00401BFF            retn   


00401BFF start      endp ; sp = -34h
b) стартовый код программы “first.exe”, полученный дизассемблером IDA Pro 4.01
С приведенным примером IDA Pro успешно справляется, о чем свидетельствует стока “Using FLIRT signature: VC v2.0/4.x/5.0 runtime” в окне сообщений
Рисунок 7 "0x003" Загрузка библиотеки сигнатур
Дизассемблер сумел определить имена всех функций вызываемых стартовым кодом, за исключением одной, расположенной по адресу 0х0401BDB. Учитывая передачу трех аргументов и обращение к _exit, после возращения функцией управления, можно предположить, что это main и есть.
Перейти по адресу 0x0401000 для изучения содержимого функции main можно несколькими способами – прокрутить экран с помощью стрелок управления курсором, нажать клавишу <G> и ввести требуемый адрес в появившемся окне диалога, но проще и быстрее всего воспользоваться встроенной в IDA Pro системой навигации. Если подвести курсор в границы имени, константы или выражения и нажать <Enter>, IDA автоматически перейдет на требуемый адрес.
В данном случае требуется подвести к строке “sub_401000” (аргументу команды call) и нажать на <Enter>, если все сделано правильно, экран дизассемблера должен выглядеть следующим образом:
00401000 ; -------------- S U B R O U T I N E ----------------------
00401000
00401000 ; Attributes: bp-based frame
00401000
00401000 sub_401000 proc near               ; CODE XREF: start+AF¯p
00401000            push   ebp
00401001            mov    ebp, esp
00401003            push   offset aHelloSailor ; "Hello, Sailor!\n"
00401008            mov    ecx, offset dword_408748
0040100D            call ??6ostream@@QAEAAV0@PBD@Z ; ostream::operator<<(char const *)
00401012            pop     ebp
00401013            retn   
00401013 sub_401000 endp
Дизассемблер сумел распознать строковую переменную и дал ей осмысленное имя “aHelloSailor”, а в комментарии, расположенном справа, для наглядности привел оригинальное содержимое “Hello, Sailor!\n”.


Если поместить курсор в границы имени “aHelloSailor”:и нажать <Enter>, IDA автоматически перейдет к требуемой строке:
00408040 aHelloSailor    db 'Hello, Sailor!',0Ah,0 ; DATA XREF: sub_401000+3­o
Выражение “DATA XREF: sub_401000+3­o” называется перекрестной ссылкой и свидетельствует о том, что в третьей строке  процедуры sub_401000, произошло обращение к текущему адресу по его смещению (“o” от offset), а стрелка, направленная вверх, указывает на относительное расположение источника перекрестной ссылки.
Если в границы выражения “sub_401000+3” подвести курсор и нажать <Enter>, IDA Pro перейдет к следующей строке:
00401003         push   offset aHelloSailor ; "Hello, Sailor!\n"
Нажатие клавиши <Ecs> отменяет предыдущее перемещение, возвращая курсор в исходную позицию. (Аналогично команде “back” в web-браузере). Смещение строки “Hello, Sailor!\n”, передается процедуре “??6ostream@@QAEAAV0@PBD@Z”, представляющей собой оператор “<<” языка С++. Странное имя объясняется ограничениями, наложенными на символы, допустимые в именах библиотечных функций. Поэтому, компиляторы автоматически преобразуют (замангляют) такие имена в “абракадабру”, пригодную для работы с линкером, и многие начинающие программисты даже не догадываются об этой скрытой “кухне”.
Для облегчения анализа текста, IDA Pro в комментариях отображает «правильные» имена, но существует возможность заставить ее везде показывать незамангленные имена. Для этого необходимо в меню “Options” выбрать пункт “Demangled names” и в появившемся окне диалога переместить радио кнопку на “Names”, после этого вызов оператора “<<” станет выглядеть так:
0040100D         call    ostream::operator<<(char const *)
На этом анализ приложения “first.cpp” можно считать завершенным. Для полноты картины остается переименовать функцию “sub_401000” в main. Для этого необходимо подвести курсор к строке 0x0401000 (началу функции) и нажать клавишу <N>, в появившемся диалоге ввести “main”.


Конечный результат должен выглядеть так:
00401000 ; --------------- S U B R O U T I N E ---------------------------------------
00401000
00401000 ; Attributes: bp-based frame
00401000
00401000 main            proc near               ; CODE XREF: start+AF¯p
00401000                 push    ebp
00401001                 mov     ebp, esp
00401003                 push    offset aHelloSailor ; "Hello, Sailor!\n"
00401008                 mov     ecx, offset dword_408748
0040100D                 call    ostream::operator<<(char const *)
00401012                 pop     ebp
00401013                 retn   
00401013 main            endp
Для сравнения результат работы W32Dasm выглядит следующим образом (ниже приводится лишь содержимое функции main):
:00401000 55                      push ebp
:00401001 8BEC                    mov ebp, esp
Possible StringData Ref from Data Obj ->"Hello, Sailor!"
|
:00401003 6840804000              push 00408040
:00401008 B948874000              mov ecx, 00408748
:0040100D E8AB000000              call 004010BD
:00401012 5D                      pop ebp
:00401013 C3                      ret
Другое важное преимущество IDA – способность дизассемблировать зашифрованные программы. В демонстрационном примере ??? “/SRC/Crypt.com” использовалась статическая шифровка, часто встречающаяся в “конвертных” защитах. Этот простой прием полностью “ослепляет” большинство дизассемблеров. Например, результат обработки файла “Crypt.com” SOURCER-ом выглядит так:
Crypt                       proc     far
7E5B:0100                   start:
7E5B:0100  83 C6 06         add      si,6
7E5B:0103  FF E6            jmp      si                          ;*
                                                                           ;*No entry point to code
7E5B:0105  B9 14BE          mov      cx,14BEh
7E5B:0108  01 AD 5691       add      ds:data_1e[di],bp  ; (7E5B:5691=0)


7E5B:010C  80 34 66         xor      byte ptr [si],66h  ; 'f'
7E5B:010F  46               inc      si
7E5B:0110  E2 FA            loop     $-4                         ; Loop if cx > 0
7E5B:0112  FF E6            jmp      si                          ;*
                                                                           ;* No entry point to code
7E5B:114 18 00              sbb      [bx+si],al
7E5B:116 D2 6F DC           shr      byte ptr [bx-24h],cl        ; Shift w/zeros fill
7E5B: 119 6E 67 AB 47 A5 2E  db 6Eh, 67h,0ABh, 47h,0A5h, 2Eh
7E5B:11F 03 0A 0A 09 4A 35  db 03h, 0Ah, 0Ah, 09h, 4Ah, 35h
7E5B:125 07 0F 0A 09 14 47  db 07h, 0Fh, 0Ah, 09h, 14h, 47h
7E5B:12B 6B 6C 42 E8 00 00  db 6Bh, 6Ch, 42h, E8h, 00h, 00h
7E5B:131 59 5E BF 00 01 57  db 59h, 5Eh, BFh, 00h, 01h, 57h
7E5B:137 2B CE F3 A4 C3     db 2Bh, CEh, F3h, A4h, C3h
Crypt                       endp
SOURCER половину кода вообще не смог дизассемблировать, оставив ее в виде дампа, а другую половину дизассемблировал неправильно! Команда “JMP SI” в строке :0x103 осуществляет переход по адресу :0x106 (значение регистра SI после загрузки com файла равно 0x100, поэтому после команды “ADD SI,6” регистр SI равен 0x106). Но следующая за “JMP” команда расположена по адресу 0x105! В исходном тексте в это место вставлен байт-пустышка, сбивающий дизассемблер с толку.
Start:
       ADD    SI,6
       JMP    SI
       DB     0B9h         ;
       LEA    SI,_end             ; На начало зашифрованного фрагмента
SOURCER не обладает способностью предсказывать регистровые переходы и, встретив команду “JMP SI” продолжает дизассемблирование, молчаливо предполагая, что команды последовательно расположены вплотную друг к другу. Существует возможность создать файл определений, указывающий, что по адресу:0x105 расположен байт данных, но подобное взаимодействие с пользователем очень неудобно.
Напротив, IDA изначально проектировалась как дружественная к пользователю интерактивная среда.


В отличие от SURCER- подобных дизассемблеров, IDA не делает никаких молчаливых предположений, и при возникновении затруднений обращается за помощью к человеку. Поэтому, встретив регистровый переход по неизвестному адресу, она прекращает дальнейший анализ, и результат анализа файла “Crypt.com” выглядит так:
seg000:0100 start           proc near
seg000:0100                 add     si, 6
seg000:0103                 jmp     si
seg000:0103 start           endp
seg000:0103
seg000:0103 ; ------------------------------------------------------------------------
seg000:0105                 db 0B9h ; ¦
seg000:0106                 db 0BEh ; -
seg000:0107                 db  14h ; 
seg000:0108                 db    1 ; 
seg000:0109                 db 0ADh ; í
seg000:010A                 db  91h ; Ñ
...
Необходимо помочь дизассемблеру, указав адрес перехода. Начинающие пользователи в этой ситуации обычно подводят курсор к соответствующей строке и нажимают клавишу <C>, заставляя IDA дизассемблировать код с текущей позиции до конца функции. Несмотря на кажущуюся очевидность, такое решение ошибочно, ибо по-прежнему остается неизвестным куда указывает условный переход в строке :0x103 и откуда код, расположенный по адресу :0x106 получает управление.
Правильное решение – добавить перекрестную ссылку, связывающую строку :0x103, со строкой :0x106. Для этого необходимо в меню “View” выбрать пункт “Cross references” и в появившемся окне диалога заполнить поля “from” и “to” значениями seg000:0103 и seg000:0106 соответственно.
После этого экран дизассемблера должен выглядеть следующим образом (в IDA версии 4.01.300 содержится ошибка, и добавление новой перекрестной ссылки не всегда приводит к автоматическому дизассемблированию):
seg000:0100                 public start
seg000:0100 start           proc near
seg000:0100                 add     si, 6
seg000:0103                 jmp     si
seg000:0103 start           endp
seg000:0103


seg000:0103 ; -----------------------------------------------------------------------
seg000:0105                 db 0B9h ; ¦
seg000:0106 ; -----------------------------------------------------------------------
seg000:0106
seg000:0106 loc_0_106:                              ; CODE XREF: start+3­u
seg000:0106                 mov     si, 114h
seg000:0109                 lodsw
seg000:010A                 xchg    ax, cx
seg000:010B                 push    si
seg000:010C
seg000:010C loc_0_10C:                              ; CODE XREF: seg000:0110¯j
seg000:010C                 xor     byte ptr [si], 66h
seg000:010F                 inc     si
seg000:0110                 loop    loc_0_10C
seg000:0112                 jmp     si
seg000:0112 ; ----------------------------------------------------------------------
seg000:0114                 db  18h ; 
seg000:0115                 db    0 ; 
seg000:0116                 db 0D2h ; T
seg000:0117                 db  6Fh ; o
...
Поскольку IDA Pro не отображает адреса- приемника перекрестной ссылки, то рекомендуется выполнить это самостоятельно. Такой примем улучшит наглядность текста и упростит навигацию. Если повести курсор к строке :0x103 нажать клавишу <:>, введя в появившемся диалоговом окне любой осмысленный комментарий (например “переход по адресу 0106”), то экран примет следующий вид:
seg000:0103                 jmp     si              ; Переход по адресу 0106
Ценность такого приема заключается в возможности быстрого перехода по адресу, на который ссылается “JMP SI”, - достаточно лишь подвести курсор к числу “0106” и нажать <Enter>. Важно соблюдать правильность написания – IDA Pro не распознает шестнадцатеричный формат ни в стиле Си (0x106), ни в стиле MASM\TASM (0106h).
Что представляет собой число “114h” в строке :0x106 – константу или смещение? Чтобы узнать это, необходимо проанализировать следующую команду – “LODSW”, поскольку ее выполнение приводит к загрузке в регистр AX слова, расположенного по адресу DS:SI, очевидно, в регистр SI заносится смещение.


seg000:0106                 mov     si, 114h
seg000:0109                 lodsw
Однократное нажатие клавиши <O> преобразует константу в смещение и дизассемблируемый текст станет выглядеть так:
seg000:0106                 mov     si, offset unk_0_114
seg000:0109                 lodsw

seg000:0114 unk_0_114       db  18h ;               ; DATA XREF: seg000:0106­o
seg000:0115                 db    0 ;  
seg000:0116                 db 0D2h ; T
seg000:0117                 db  6Fh ; o

IDA Pro автоматически создала новое имя “unk_0_114”, ссылающееся на переменную неопределенного типа размером в байт, но команда “LODSW” загружает в регистр AX слово, поэтому необходимо перейти к строке :0144 и дважды нажать <D> пока экран не станет выглядеть так:
seg000:0114 word_0_114      dw 18h                  ; DATA XREF: seg000:0106­o
seg000:0116                 db 0D2h ; T
Но что именно содержится в ячейке “word_0_144”? Понять это позволит изучение следующего кода:
seg000:0106                 mov     si, offset word_0_114
seg000:0109                 lodsw
seg000:010A                 xchg    ax, cx
seg000:010B                 push    si
seg000:010C
seg000:010C loc_0_10C:                              ; CODE XREF: seg000:0110¯j
seg000:010C                 xor     byte ptr [si], 66h
seg000:010F                 inc     si
seg000:0110                 loop    loc_0_10C
В строке :0x10A значение регистра AX помещается в регистр CX, и затем он используется командой “LOOP LOC_010C” как счетчик цикла. Тело цикла представляет собой простейший расшифровщик – команда “XOR” расшифровывает один байт, на который указывает регистр SI, а команда “INC SI” перемещает указатель на следующий байт. Следовательно, в ячейке “word_0_144” содержится количество байт, которые необходимо расшифровать. Подведя к ней курсор, нажатием клавиши <N> можно дать ей осмысленное имя, например “BytesToDecrypt”.


После завершения цикла расшифровщика встречается еще один безусловный регистровый переход.
seg000:0112                 jmp     si
Чтобы узнать куда именно он передает управление, необходимо проанализировать код и определить содержимое регистра SI. Часто для этой цели прибегают к помощи отладчика – устанавливают точку останова в строке 0x112 и дождавшись его «всплытия» просматривают значения регистров. Специально для этой цели, IDA Pro поддерживает генерацию map-файлов, содержащих символьную информацию для отладчика. В частности, чтобы не заучивать численные значения всех «подопытных» адресов, каждому из них можно присвоить легко запоминаемое символьное имя. Например, если подвести курсор к строке “seg000:0112”, нажать <N> и ввести “BreakHere”, отладчик сможет автоматически вычислить обратный адрес по его имени.
Для создания map-файла в меню “File” необходимо кликнуть по «Produce output file» и в развернувшемся подменю выбрать «Produce MAP file» или вместо всего этого нажать на клавиатуре «горячую» комбинацию «<Shift-F10»>. Независимо от способа вызова на экран должно появится диалоговое окно следующего вида. Оно позволяет выбрать какого рода данные будут включены в map-файл – информация о сегментах, имена автоматически сгенерированные IDA Pro (такие как, например, “loc_0_106”, “sub_0x110” и т.д.) и «размангленные» (т.е. приведенные в читабельный вид) имена. Содержимое полученного map-файла должно быть следующим:
Start  Stop   Length Name               Class
00100H 0013BH 0003CH seg000             CODE
Address         Publics by Value
0000:0100       start
0000:0112       BreakHere
0000:0114       BytesToDecrypt
Program entry point at 0000:0100
Такой формат поддерживают большинство отладчиков, в том числе и популярнейший Soft-Ice, в поставку которого входит утилита “msym”, запускаемая с указанием имени конвертируемого map-файла в командной стоке. Полученный sym-файл необходимо разместить в одной директории с отлаживаемой программой, загружаемой в загрузчик без указания расширения, т.е., например, так “WLDR Crypt”.


В противном случае символьная информация не будет загружена!
Затем необходимо установить точку останова командой “bpx BreakHere” и покинуть отладчик командной “x”. Спустя секунду его окно вновь появиться на экране, извещая о достижении процессором контрольной точки. Посмотрев на значения регистров, отображаемых по умолчанию вверху экрана, можно выяснить, что содержимое SI равно 0x12E.
С другой стороны, это же значение можно вычислить «в уме», не прибегая к отладчику. Команда MOV в строке 0x106 загружает в регистр SI смещение 0x114, откуда командой LODSW считывается количество расшифровываемых байт – 0x18, при этом содержимое SI увеличивается на размер слова – два байта. Отсюда, в момент завершения цикла расшифровки значение SI будет равно 0x114+0x18+0x2 = 0x12E.
Вычислив адрес перехода в строке 0x112, рекомендуется создать соответствующую перекрестную ссылку (from: 0x122; to: 0x12E) и добавить комментарий к строке 0x112 (“Переход по адресу 012E”). Создание перекрестной ссылки автоматически дизассемблирует код, начиная с адреса seg000:012E и до конца файла.
seg000:012E loc_0_12E:             ; CODE XREF: seg000:0112u
seg000:012E             call  $+3
seg000:0131             pop   cx
seg000:0132             pop   si
seg000:0133             mov   di, 100h
seg000:0136             push  di
seg000:0137             sub   cx, si
seg000:0139             repe  movsb
seg000:013B             retn
Назначение команды “CALL $+3” (где $ обозначает текущее значение регистра указателя команд IP) состоит в заталкивании в стек содержимого регистра IP, откуда впоследствии оно может быть извлечено в любой регистр общего назначения. Необходимость подобного трюка объясняется тем, что в микропроцессорах серии Intel 80x86 регистр IP не входит в список непосредственно адресуемых и читать его значение могут лишь команды, изменяющие ход выполнения программы, в том числе и call.
Для облегчения анализа листинга можно добавить к стокам 0x12E и 0x131 комментарий – “MOV CX, IP”, или еще лучше – сразу вычислить и подставить непосредственное значение – “MOV CX,0x131”.


Команда “POP SI” в строке 0x132 снимает слово из стека и помещает его в регистр SI. Прокручивая экран дизассемблера вверх в строке 0x10B можно обнаружить парную ей инструкцию “PUSH SI”, заносящую в стек смещение первого расшифровываемого байта. После этого становится понятным смысл последующих команд “MOV DI, 0x100\SUB CX,SI\REPE MOVSB”. Они перемещают начало расшифрованного фрагмента по адресу, начинающегося со смещения 0x100. Такая операция характерна для «конвертных» защит, накладывающихся на уже откомпилированный файл, который перед запуском должен быть размещен по своим «родным» адресам.
Перед началом перемещения в регистр CX заносится длина копируемого блока, вычисляемая путем вычитания смещения первого расшифрованного байта от смещения второй команды перемещающего кода. В действительности, истинная длина на три байта короче и по идее от полученного значения необходимо вычесть три. Однако, такое несогласование не нарушает работоспособности, поскольку содержимое ячеек памяти, лежащих за концом расшифрованного фрагмента, не определено и может быть любым.
Пара команд “0x136:PUSH DI” и “0x13B:RETN” образуют аналог инструкции “CALL DI” – “PUSH” заталкивает адрес возврата в стек, а “RETN” извлекает его оттуда и передает управление по соответствующему адресу. Зная значение DI (оно равно 0x100) можно было бы добавить еще одну перекрестную ссылку (“from:0x13B; to:0x100”) и комментарий к строке :0x13B – “Переход по адресу 0x100”, но ведь к этому моменту по указанным адресам расположен совсем другой код! Поэтому, логически правильнее добавить перекрестную ссылку “from:0x13B; to:0x116” и комментарий “Переход по адресу 0x116”.
Сразу же после создания новой перекрестной ссылки IDA попытается дизассемблировать зашифрованный код, в результате чего получится следующее:
seg000:0116 loc_0_116:                  ; CODE XREF: seg000:013Bu
seg000:0116                shr          byte ptr [bx-24h], cl
seg000:0119                outsb
seg000:011A                stos         word ptr es:[edi]


seg000:011C                inc          di
seg000:011D                movsw
seg000:011E                add          cx, cs:[bp+si]
seg000:0121                or           cl, [bx+di]
seg000:0123                dec          dx
seg000:0124                xor          ax, 0F07h
seg000:0127                or           cl, [bx+di]
seg000:0129                adc          al, 47h
seg000:0129;------------------------------------------------------
seg000:012B                db           6Bh ; k
seg000:012C                db           6Ch ; l
seg000:012D                db           42h ; B
seg000:012E;------------------------------------------------------
Непосредственное дизассемблирование зашифрованного кода невозможно – предварительно его необходимо расшифровать. Подавляющее большинство дизассемблеров не могут модифицировать анализируемый текст налету и до загрузки в дизассемблер исследуемый файл должен быть полностью расшифрован. На практике, однако, это выглядит несколько иначе – прежде чем расшифровывать необходимо выяснить алгоритм расшифровки, проанализировав доступную часть файла. Затем выйти из дизассемблера, тем или иным способом расшифровать «секретный» фрагмент, вновь загрузить файл в дизассемблер (причем предыдущие результаты дизассемблирования окажутся утеряны) и продолжить его анализ до тех пор, пока не встретится еще один зашифрованный фрагмент, после чего описанный цикл «выход из дизассемблера –расшифровка – загрузка - анализ» повторяется вновь.
Достоинство IDA заключается в том, что она позволяет выполнить ту же задачу значительно меньшими усилиями, никуда не выходя из дизассемблера. Это достигается за счет наличия механизма виртуальной памяти, – если не вдаваться в технические тонкости, упрощенно можно изобразить IDA в виде «прозрачной» виртуальной машины, оперирующей с физической памятью компьютера. Для модификации ячеек памяти необходимо знать их адрес, состоящий из пары чисел – сегмента и смещения.
Слева каждой строки указывается ее смещение и имя сегмента, например “seg000:0116”.


Узнать базовый адрес сегмента по его имени можно, открыв окно «Сегменты» выбрав в меню «View» пункт «Segments».
г=[¦]=========================== Program Segmentation ==========================3=[^]=¬
¦    Name     Start     End   Align Base
Type Cls  32es   ss   ds                     ^
¦ seg000    00000100 0000013C byte  1000
pub CODE  N FFFF FFFF 1000 00010100 0001013C -
¦                                                                                     -
¦                                                                                     Ў
L1/1          =================<¦--------------------------------------------------->--
Рисунок 8 Окно «Сегменты»
Искомый адрес находится в столбце “Base” и для наглядности на приведенной копии экрана выделен жирным шрифтом. Обратится к любой ячейке сегмента поможет конструкция “[segment:offset]”, а для чтения и модификации ячеек предусмотрены функции Byte и PatchByte соответственно. Их вызов может выглядеть, например, так: a=Byte([0x1000,0x100]) – читает ячейку, расположенную по смещению 0x100 в сегменте с базовым адресом 0x1000; PatchByte([0x1000,0x100],0x27) – присваивает значение 0x27 ячейке памяти, расположенной по смещению 0x100 в сегменте с базовым адресом 0x1000. Как следует из названия функций, они манипулируют с ячейками размером в один байт.
Знания этих двух функций вполне достаточно для написания скрипта -расшифровщика при условии, что читатель знаком с языком Си. Реализация IDA-Си не полностью поддерживается стандарта – подробнее об этом рассказывается в главе «Язык скриптов IDA-Си», здесь же достаточно заметить, что в частности IDA не позволяет разработчику задавать тип переменной и определяет его автоматически по ее первому использованию, а объявление осуществляется ключевым словом “auto”. Например, “auto MyVar, s0” объявляет две переменных – MyVar и s0.
Для создания скрипта необходимо нажать комбинацию клавиш <Shift-F2> или выбрать в меню “File” пункт “IDC Command” и в появившемся окне диалога ввести исходный текст программы:


г=[¦]================ Notepad =====================¬
¦  Enter IDC statement(s)                          ¦
¦ auto a;                             ^            ¦
¦ for (a=0x116;a<0x12E;a++)           -            ¦
¦ PatchByte([0x1000,a],               -     OK   - ¦
¦ Byte([0x1000,a])^0x66);             -   -------- ¦
¦                                     -            ¦
¦                                     -            ¦
¦                                     -   Cancel - ¦
¦                                     -   -------- ¦
¦                                     -            ¦
¦                                     -            ¦
¦                                     -    Help  - ¦
¦                                     Ў   -------- ¦
¦0===== 5:1 ===<¦-------------------->             ¦
L==================================================-
Рисунок 9 Встроенный редактор скриптов
auto a;
for (a=0x116;a<0x12E;a++)
PatchByte([0x1000,a],Byte([0x1000,a])^0x66);
a) исходный текст скрипта - расшифровщика
Пояснение: как было показано выше алгоритм расшифровщика сводится к последовательному преобразованию каждой ячейки зашифрованного фрагмента операцией XOR 0x66, (см. ниже – выделено жирным шрифтом)
seg000:010C                 xor     byte ptr [si], 66h
seg000:010F                 inc     si
seg000:0110                 loop    loc_0_10C
Сам же зашифрованный фрагмент начинается с адреса seg000:0x116 и продолжается вплоть до seg000:0x12E. Отсюда – цикл расшифровки на языке Си выглядит так: for (a=0x116;a<0x12E;a++) PatchByte([0x1000,a],Byte([0x1000,a])^0x66);
В зависимости от версии IDA для выполнения скрипта необходимо нажать либо <Enter> (версия 3.8x и старше), либо <Ctrl-Enter> в более ранних версиях. Если все сделано правильно, после выполнения скрипта экран дизассемблера должен выглядеть так (b).
Возможные ошибки – несоблюдение регистра символов (IDA к этому чувствительна), синтаксические ошибки, базовый адрес вашего сегмента отличается от 0x1000 (еще раз вызовете окно «Сегменты» чтобы узнать его значение).


В противном случае необходимо подвести курсор к строке “seg000:0116”, нажать клавишу <U> для удаления результатов предыдущего дизассемблирования зашифрованного фрагмента и затем клавишу <C> для повторного дизассемблирования расшифрованного кода.
seg000:0116 loc_0_116:                              ; CODE XREF: seg000:013Bu
seg000:0116                 mov     ah, 9
seg000:0118                 mov     dx, 108h
seg000:011B                 int     21h             ; DOS - PRINT STRING
seg000:011B                                         ; DS:DX -> string terminated by "$"
seg000:011D                 retn
seg000:011D ; ---------------------------------------------------------------------------
seg000:011E                 db  48h ; H
seg000:011F                 db  65h ; e
seg000:0120                 db  6Ch ; l
seg000:0121                 db  6Ch ; l
seg000:0122                 db  6Fh ; o
seg000:0123                 db  2Ch ; ,
seg000:0124                 db  53h ; S
seg000:0125                 db  61h ; a
seg000:0126                 db  69h ; i
seg000:0127                 db  6Ch ; l
seg000:0128                 db  6Fh ; o
seg000:0129                 db  72h ; r
seg000:012A                 db  21h ; !
seg000:012B                 db  0Dh ;
seg000:012C                 db  0Ah ;
seg000:012D                 db  24h ; $
seg000:012E ; ---------------------------------------------------------------------------
b) результат работы скрипта расшифровщика
Цепочку символов, расположенную начиная с адреса “seg000:011E” можно преобразовать в удобочитаемый вид, подведя к ней курсор и нажав клавишу “<A>”. Теперь экран дизассемблера будет выглядеть так:
seg000:0116 loc_0_116:                              ; CODE XREF: seg000:013Bu
seg000:0116                 mov     ah, 9
seg000:0118                 mov     dx, 108h
seg000:011B                 int     21h             ; DOS - PRINT STRING
seg000:011B                                         ; DS:DX -> string terminated by "$"


seg000:011D                 retn
seg000:011D ; ---------------------------------------------------------------------------
seg000:011E aHelloSailor    db 'Hello,Sailor!',0Dh,0Ah,'$'
seg000:012E ; ---------------------------------------------------------------------------
с) создание ASCII-строки
Команда “MOV AH,9” в строке : 0116 подготавливает регистр AH перед вызовом прерывания 0x21, выбирая функцию вывода строки на экран, смещение которой заносится следующей командой в регистр DX. Т.е. для успешного ассемблирования листинга необходимо заменить константу 0x108 соответствующим смещением. Но ведь выводимая строка на этапе ассемблирования (до перемещения кода) расположена совсем в другом месте! Одно из возможных решений этой проблемы заключается в создании нового сегмента с последующим копированием в него расшифрованного кода – в результате чего достигается эмуляции перемещения кода работающей программы.
Для создания нового сегмента можно выбрать в меню «View» пункт «Segments» и в раскрывшемся окне нажать клавишу <Insert>. Появится диалог следующего вида (см. рис. 10):
г=[¦]============ Create a new segment ================¬
¦                                                      ¦
¦  Start address and end address should be valid.      ¦
¦          End address > Start address                 ¦
¦                                                      ¦
¦ Segment name     MySeg        ¦v¦                    ¦
¦ Start address    0x20100      ¦v¦ C-notation:        ¦
¦ End   address    0x20125      ¦v¦   hex is 0x...     ¦
¦ Base             0x2000       ¦v¦ in paragraphs      ¦
¦ Class                         ¦v¦ (class is any text)¦
¦                                                      ¦
¦  [ ] 32-bit segment                                  ¦
¦                                                      ¦
¦           OK -    Cancel -    F1 - Help -            ¦
¦           ----    --------    -----------            ¦
L======================================================-


Рисунок 10 IDAC: Создание нового сегмента
Пояснение: Базовый адрес сегмента может быть любым если при этом не происходит перекрытия сегментов seg000 и MySeg; начальный адрес сегмента задается так, чтобы смещение первого байта было равно 0x100; разница между конечным и начальным адресом равна длине сегмента, вычислить которую можно вычитанием смещения начала расшифрованного фрагмента от смещения его конца – 0x13B – 0x116 = 0x25.
Скопировать требуемый фрагмент в только что созданный сегмент можно скриптом следующего содержания.
auto a;
for (a=0x0;a<0x25;a++) PatchByte([0x2000,a+0x100],Byte([0x1000,a+0x116]));
a) исходный текст скрипта - копировщика
Для его ввода необходимо вновь нажать <Shift-F2>, при этом предыдущий скрипт будет утерян (IDA позволяет работать не более чем с один скриптом одновременно). После завершения его работы экран дизассемблера будет выглядеть так:
MySeg:0100 MySeg           segment byte public '' use16
MySeg:0100                 assume cs:MySeg
MySeg:0100                 ;org 100h
MySeg:0100                 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
MySeg:0100                 db 0B4h ; +
MySeg:0101                 db    9 ;
MySeg:0102                 db 0BAh ; ¦
MySeg:0103                 db    8 ;
MySeg:0104                 db    1 ;
MySeg:0105                 db 0CDh ; =
MySeg:0106                 db  21h ; !
MySeg:0107                 db 0C3h ; +
MySeg:0108                 db  48h ; H
MySeg:0109                 db  65h ; e
MySeg:010A                 db  6Ch ; l
MySeg:010B                 db  6Ch ; l
MySeg:010C                 db  6Fh ; o
MySeg:010D                 db  2Ch ; ,
MySeg:010E                 db  53h ; S
MySeg:010F                 db  61h ; a
MySeg:0110                 db  69h ; i
MySeg:0111                 db  6Ch ; l
MySeg:0112                 db  6Fh ; o
MySeg:0113                 db  72h ; r
MySeg:0114                 db  21h ; !


MySeg:0115                 db  0Dh ;
MySeg:0116                 db  0Ah ;
MySeg:0117                 db  24h ; $
MySeg:0117 MySeg           ends
b) результат работы скрипта-копировщика
Теперь необходимо создать перекрестную ссылку “from:seg000:013B; to:MySeg:0x100”, преобразовать цепочку символов в удобочитаемую строку, подведя курсор к строке MySeg:0108 и нажав клавишу <A>. Экран дизассемблера должен выглядеть так:
MySeg:0100 loc_1000_100:                           ; CODE XREF: seg000:013Bu
MySeg:0100                 mov     ah, 9
MySeg:0102                 mov     dx, 108h
MySeg:0105                 int     21h             ; DOS - PRINT STRING
MySeg:0105                                         ; DS:DX -> string terminated by "$"
MySeg:0107                 retn
MySeg:0107 ; ---------------------------------------------------------------------------
MySeg:0108
aHelloSailorS   db 'Hello,Sailor!',0Dh,0Ah
MySeg:0108                 db '$'
MySeg:0118 MySeg           ends
с) результат дизассемблирования скопированного фрагмента
Результатом всех этих операций стало совпадение смещения строки со значением, загружаемым в регистр DX (в тексте они выделены жирным шрифтом). Если подвести курсор к константе “108h” и нажать клавишу <Ctrl-O> она будет преобразована в смещение:
MySeg:0102                 mov     dx, offset aHelloSailorS ; "Hello,Sailor!\r\n$ш"
MySeg:0105                 int     21h                      ; DOS - PRINT STRING
MySeg:0105                                                  ; DS:DX -> string terminated by "$"
MySeg:0107                 retn
MySeg:0107 ; ---------------------------------------------------------------------------
MySeg:0108 aHelloSailorS   db 'Hello,Sailor!',0Dh,0Ah ; DATA XREF: MySeg:0102o
d) преобразование константы в смещение
Полученный листинг удобен для анализа, но все еще не готов к ассемблированию, хотя бы уже потому, что никакой ассемблер не в состоянии зашифровать требуемый код.


Конечно, эту операцию можно выполнить вручную, после компиляции, но IDA позволит проделать то же самое не выходя из нее и не прибегая к помощи стороннего инструментария.
Демонстрация получится намного нагляднее, если в исследуемый файл внести некоторые изменения, например, добавить ожидание клавиши на выходе. Для этого можно прибегнуть к интегрированному в IDA ассемблеру, но прежде, разумеется, необходимо несколько «раздвинуть» границы сегмента MySeg, дабы было к чему дописывать новый код.
Выберете в меню “View” пункт “Segments” и в открывшемся окне подведите курсор к стоке “MySeg”. Нажатие <Ctrl-E> открывает диалог свойств сегмента, содержащий среди прочих полей конечный адрес, который и требуется изменить. Не обязательно указывать точное значение – можно «растянуть» сегмент с небольшим запасом от предполагаемых изменений.
Если попытаться добавить к программе код “XOR AX,AX; INT 16h” он неминуемо затрет начало строки “Hello, Sailor!”, поэтому, ее необходимо заблаговременно передвинуть немного «вниз» (т.е. в область более старших адресов), например, с помощью скрипта следующего содержания «for(a=0x108;a<0x11A;a++) PatchByte([0x2000,a+0x20],Byte([0x2000,a]);».
Пояснение: объявление переменной a для краткости опущено (сами должны понимать, не маленькие :-), длина строки, как водится, берется с запасом, чтобы не утомлять себя лишними вычислениями и перемещение происходит справа налево, поскольку исходный и целевой фрагменты заведомо не пересекаются.
Подведя к курсор к строке :0128 нажатием <A> преобразуем цепочку символов к удобно-читаемому виду; подведем курсор к строке :0102 и, выбрав в меню “Edir” пункт “Path program”, “Assembler”, введем команду “MOV DX,128h”, где «128h» - новое смещение строки, и тут же преобразуем его в смещение нажатием <Ctrl-O>.
Вот теперь можно вводить новый текст – переместив курсор на инструкцию “ret”, вновь вызовем ассемблер и введем “XOR AX,AX<ENTER>INT 16h<Enter>RET<Enter><Esc>”.


На последок рекомендуется произвести «косметическую» чистку – уменьшить размер сегмента до необходимого и переместить строку “Hello, Sailor” вверх, прижав ее вплотную к коду.
Пояснение: удалить адреса, оставшиеся при уменьшении размеров сегмента за его концом можно взводом флажка “Disable Address” в окне свойств сегмента, вызываемом нажатием <Alt-S>
Если все было сделано правильно конечный результат должен выглядеть как показано ниже:
seg000:0100 ; File Name   : F:\IDAN\SRC\Crypt.com
seg000:0100 ; Format      : MS-DOS COM-file
seg000:0100 ; Base Address: 1000h Range: 10100h-1013Ch Loaded length: 3Ch
seg000:0100
seg000:0100
seg000:0100 ; ===========================================================================
seg000:0100
seg000:0100 ; Segment type: Pure code
seg000:0100 seg000          segment byte public 'CODE' use16
seg000:0100                 assume cs:seg000
seg000:0100                 org 100h
seg000:0100                 assume es:nothing, ss:nothing, ds:seg000, fs:nothing, gs:nothing
seg000:0100
seg000:0100 ; --------------- S U B R O U T I N E ---------------------------------------
seg000:0100
seg000:0100
seg000:0100                 public start
seg000:0100 start           proc near
seg000:0100                 add     si, 6
seg000:0103                 jmp     si              ; Ïåðåõîä ïî àäðåñó 0106
seg000:0103 start           endp
seg000:0103
seg000:0103 ; ---------------------------------------------------------------------------
seg000:0105                 db 0B9h ; ¦
seg000:0106 ; ---------------------------------------------------------------------------
seg000:0106                 mov     si, offset BytesToDecrypt
seg000:0109                 lodsw
seg000:010A                 xchg    ax, cx
seg000:010B                 push    si
seg000:010C
seg000:010C loc_0_10C:                              ; CODE XREF: seg000:0110j


seg000:010C                 xor     byte ptr [si], 66h
seg000:010F                 inc     si
seg000:0110                 loop    loc_0_10C
seg000:0112
seg000:0112 BreakHere:                              ; Ïåðåõîä ïî àäðåñó 012E
seg000:0112                 jmp     si
seg000:0112 ; ---------------------------------------------------------------------------
seg000: 0114 BytesToDecrypt  dw 18h                  ; DATA XREF: seg000:0106o
seg000:0116 ; ---------------------------------------------------------------------------
seg000:0116
seg000:0116 loc_0_116:                              ; CODE XREF: seg000:013Bu
seg000:0116                 mov     ah, 9
seg000:0118                 mov     dx, 108h        ; "Hello,Sailor!\r\n$"
seg000:011B                 int     21h             ; DOS - PRINT STRING
seg000:011B                                         ; DS:DX -> string terminated by "$"
seg000:011D                 retn   
seg000:011D ; ---------------------------------------------------------------------------
seg000:011E aHelloSailor    db 'Hello,Sailor!',0Dh,0Ah,'$' ; DATA XREF: seg000:0118o
seg000:012E ; ---------------------------------------------------------------------------
seg000:012E
seg000:012E loc_0_12E:                              ; CODE XREF: seg000:0112u
seg000:012E                 call    $+3
seg000:0131                 pop     cx
seg000:0132                 pop     si
seg000:0133                 mov     di, 100h
seg000:0136                 push    di
seg000:0137                 sub     cx, si
seg000:0139                 repe movsb
seg000:013B                 retn   
seg000:013B seg000          ends
seg000:013B
MySeg:0100 ; ---------------------------------------------------------------------------
MySeg:0100 ; ===========================================================================
MySeg:0100
MySeg:0100 ; Segment type: Regular


MySeg:0100 MySeg           segment byte public '' use16
MySeg:0100                 assume cs:MySeg
MySeg:0100                 ;org 100h
MySeg:0100                 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing
MySeg:0100
MySeg:0100 loc_1000_100:                           ; CODE XREF: seg000:013Bu
MySeg:0100                 mov     ah, 9
MySeg:0102                 mov     dx, offset aHelloSailor_0 ; "Hello,Sailor!\r\n$"
MySeg:0105                 int     21h             ; DOS - PRINT STRING
MySeg:0105                                         ; DS:DX -> string terminated by "$"
MySeg:0107                 xor     ax, ax
MySeg:0109                 int     16h             ; KEYBOARD - READ CHAR FROM BUFFER, WAIT IF EMPTY
MySeg:0109                                         ; Return: AH = scan code, AL = character
MySeg:010B                 retn   
MySeg:010B ; ---------------------------------------------------------------------------
MySeg:010C aHelloSailor_0  db 'Hello,Sailor!',0Dh,0Ah,'$' ; DATA XREF: MySeg:0102o
MySeg:010C MySeg           ends
MySeg:010C
MySeg:010C
MySeg:010C                 end start
a) окончательно дизассемблированный текст
Структурно программа состоит из следующих частей – расшифровщика, занимающего адреса seg000:0x100 – seg000:0x113, переменной размером в слово, содержащей количество расшифровываемых байт, занимающей адреса seg000:0x114-seg000:0x116, исполняемого кода программы, занимающего целиком сегмент MySeg и загрузчика, занимающего адреса seg000:0x12E-seg000:0x13B. Все эти части должны быть в перечисленном порядке скопированы в целевой файл, причем исполняемый код программы необходимо предварительно зашифровать, произведя над каждым его байтом операцию XOR 0x66.
Ниже приведен пример скрипта, автоматически выполняющего указанные действия. Для его загрузки достаточно нажать <F2> или выбрать в меню “File” пункт “Load file”, “IDC file”.
// Компилятор для файла Crypt


//
static main()
{
auto a,f;
// Открывается файл Crtypt2.com для записи в двоичном режиме
f=fopen("crypt2.com","wb");
// В файл Crypt2 копируется расшифровщик
for (a=0x100;a<0x114;a++) fputc(Byte([0x1000,a]),f);
// Определяется и копируется в файл слово, содержащее число
// байтов для расшифровки
fputc( SegEnd([0x2000,0x100]) - SegStart([0x2000,0x100]),f);
fputc(0,f);
// Копируется и налету шифруется расшифрованный фрагмент
for(a=SegStart([0x2000,0x100]);a!=SegEnd([0x2000,0x100]);a++)
fputc(Byte(a) ^ 0x66,f);
// Дописывается загрузчик
for(a=0x12E;a<0x13C;a++)
fputc(Byte([0x1000,a]),f);
// Закрывается файл.
fclose(f);
}
a) исходный код скрипта-компилятора
Выполнение скрипта приведет к созданию файла “Crypt2.com”, запустив который можно убедиться в его работоспособности – он выводит строку на экран и, дождавшись нажатия любой клавиши, завершает свою работу.
Огромным преимуществом такого подхода является «сквозная» компиляция файла, т.е. дизассемблированный листинг в действительности не ассемблировался! Вместо этого из виртуальной памяти байт-за-байтом читалось оригинальное содержимое, которое за исключением модифицированных строк доподлинно идентично исходному файлу. Напротив, повторное ассемблирование практически никогда не позволяет добиться полного сходства с дизассемблируемым файлом.
IDA – очень удобный инструмент для модификации файлов, исходные тексты которых утеряны или отсутствуют; она практически единственный дизассемблер, способный анализировать зашифрованные программы, не прибегая к сторонним средствам; она обладает развитым пользовательским интерфейсом и удобной системой навигации по исследуемому тексту; она дает может справится с любой мыслимой и немыслимой задачей…
…но эти, и многие другие возможности, невозможно реализовать в полной мере, без владения языком скриптов, что и подтвердил приведенный выше пример.
___Рассказать о языке комментариев. "Дом который построил Джек"
___Трассированное дизасссемблирование
___Большинство защит вскрываются стандартными приемами, которые вовсе не требуют понимания "как это работает". Мой тезка (широко известный среди спектрумистов уже едва ли не десяток лет) однажды сказал "Умение снимать защиту, еще не означает умения ее ставить". Это типично для кракера, которому, судя по всему, ничто не мешает ломать и крушить. Хакер же не ставит целью взлом (т.е. способ любой ценой заставить программу работать), а интересуется именно МЕХАНИЗМОМ: "как оно работает". Взлом для него вторичен.

седьмой. Идентификация ключевых структур языков высокого уровня


"Если твое оружие стоит только малой части энергии, затраченной  твоим врагом, ты имеешь мощный рычаг, который может одолеть непреодолимые с виду трудности"

Френк Херберт "Дом глав Дюны"

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

и глобальных

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

Современные дизассемблеры достаточно интеллектуальны и львиную долю работы по распознаванию ключевых структур берут на себя. В частности, IDA Pro успешно справляется с идентификацией стандартных библиотечных функций, локальных переменных, адресуемых через регистр ESP, case-ветвлений и т.д. Однако порой она ошибается, вводя исследователя в заблуждение, к тому же ее высокая стоимость не всегда оправдывает применение. Так, например, студентам, изучающим ассемблер (а лучше средство изучение ассемблера – дизассемблирование чужих программ), она едва ли по карману.

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

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



шестой. Дизассемблер & отладчик в связке


"Кот с улыбкой - и то редкость, но уж улыбка без кота - это я прямо не знаю что такое"

Льюис Кэрролл. Алиса в стране чудес

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

Однако тот дизассемблер, что включен в отладчик, обычно слишком примитивен и не может похвастаться богатыми функциональными возможностями. Во всяком случае, дизассемблер, встроенный в популярнейший отладчик Soft-Ice, недалеко ушел от DUMPBIN, с недостатками которого мы уже имели честь столкнуться. Насколько же понятнее становится код, если его загрузить в IDA!

Чем же тогда ценен отладчик? Дело в том, что дизассемблер в силу своей статичности имеет ряд ограничений. Во-первых, исследователю приходится выполнять программу на "эмуляторе" процессора, "зашитом" в их собственной голове, следовательно, необходимо знать и назначение всех команд процессора, и все структуры операционной системы (включая недокументированные), и… Во-вторых, начать анализ с произвольного места программы не так-то просто – требуется знать содержимое регистров и ячеек памяти на данный момент, а как их узнать? С регистрами и локальными переменными еще бы куда ни шло – прокрутим экран дизассемблера вверх и посмотрим какие значения им присваиваются, но этот фокус не пройдет с глобальными переменными, модифицировать которые может кто угодно и когда угодно. Вот бы установить точку останова… но какая же в дизассемблере может быть точка останова? В третьих, дизассемблирование вынуждает на полную реконструкцию алгоритма каждой функции, в то время как отладка позволяет рассматривать ее как "черный ящик" со входом и выходом. Допустим, имеется у нас функция, которая расшифровывает основной модуль программы. В дизассемблере нам придется сначала разобраться в алгоритме шифрования (что может оказаться совсем не просто), затем "переложить" эту функцию на IDA-Си, отладить ее, запустить расшифровщик… В отладчике же можно поручить выполнение этой функции процессору, не вникая в то, как она работает, и дождавшись ее завершения, продолжить анализ расшифрованного модуля программы.
Можно перечислять бесконечно, но и без того ясно, что отладчик отнюдь не конкурент дизассемблеру, а партнер.
Опытные хакеры всегда используют эти два инструмента в паре. Алгоритм реконструируется в дизассемблере, а все непонятные моменты оперативно уточняются, прогоном под отладчиком. При этом возникает естественное желание видеть в отладчике все те символические имена, которые были внесены в дизассемблерный листинг.
И IDA Pro действительно позволяет это сделать! Выберем в меню "Fail" подменю "Produce output file", а в нем пункт "Produce MAP file" (или нажмем "горячую" клавишу <Shift-F10>). На экране появится окно с запросом имени файла (введем, например, "simple.map"), а затем возникнет модальный диалог, уточняющий какие именно имена стоит включать в map-файл. Нажмем <Enter>, оставив все галочки в состоянии по умолчанию (подробно о назначении каждой из них можно прочитать в моей книге "Образ мышления – дизассемблер IDA"). Парой секунд спустя на диске образуется "simple.map" файл, содержащий всю необходимую отладочную информацию, представленную в map-формате Borland. Отладчик Soft-ice не поддерживает такой формат, поэтому, перед его использованием файл необходимо конвертировать в sym-формат специально на то предназначенной утилитой idasym, которую можно бесплатно скачать с сайта www.idapro.com или получить у дистрибьютора, продавшего вам IDA.
Набрав в командной строке "idasym simple.map" и, с удовлетворением убедившись, что файл "simple.sym" действительно создан, запустим загрузим исследуемое приложение "simple.exe" в отладчик любым возможным способом. Дождавшись появления Soft-Ice на экране, отладим ему команду "SYM" для отображения содержимого таблицы символов. Если все было сделано правильно, ответ Soft-Ice должен выглядеть приблизительно так (ниже приведен сокращенный вариант):
:sym
CODE(001B)
     001B:00401000 start
     001B:00401074 __GetExceptDLLinfo


     001B:0040107C _Main
     001B:00401104 _memchr
     001B:00401124 _memcpy
     001B:00401148 _memmove
     001B:00401194 _memset
     001B:004011C4 _strcmp
     001B:004011F0 _strlen
     001B:0040120C _memcmp
     001B:00401250 _strrchr
     001B:00403C08 _printf
DATA(0023)
     0023:00407000 aBorlandCCopyri
     0023:004070D9 aEnterPassword
     0023:004070E9 aMygoodpassword
     0023:004070F9 aWrongPassword
     0023:00407109 aPasswordOk
     0023:00407210 aNotype
     0023:00407219 aBccxh1
Wow! Это функциклирует! Теперь символьные имена не только отображаются на экране, упрощая понимание кода, – на любое из них можно быстро и с комфортом установить точку останова, скажем "bpm aMygoodpassword" и отладчик поймет, что от него хотят! Нет больше нужны держать в серо-мозговой памяти эти трудно запоминаемые шестнадцатеричные адреса!

третий. Хирургический


Не торопитесь на встречу с Богом, еще встретитесь.

Народная мудрость

Внесение изменений непосредственно в исполняемый файл – дело серьезное. Стиснутыми

уже существующим кодом, нам приходится довольствоваться только тем, что есть – и ни раздвинуть команды, ни даже "сдвинуть" их, выкинув из защиты "лишние запчасти",

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

Ну, с "выкидываем запчастей" справится как раз таки просто – достаточно забить код командами NOP (опкод который 0x90, а вовсе не 0х0, как почему-то думают многие начинающие кодокопатели) – т.е. пустой операцией (вообще-то NOP это просто другая форма записи инструкции XCHG EAX,EAX – если интересно). С "раздвижкой" куда сложнее! К счастью, в WindowsPE-файлах всегда присутствует множество "дыр", оставшихся от выравнивания – в них-то и можно разместить свой код или данные.

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

Приходится "резать" программу в "живую". Легче всего это сделать с помощью утилиты HIEW, "переваривающей" PE-формат файлов и упрощающей тем самым поиск нужного фрагмента. Запустим его, указав имя файла в командной строке "hiew simple.exe", двойным нажатием <Enter> переключимся в режим ассемблера и по <F5> перейдем к требуемому адресу. Как мы помним, команда "TEST", проверяющая результат, возвращенный функцией на равенство нулю, располагалась по адресу 0x401056.


  0040104E: E8 4D 00 00 00     call        004010A0
  00401053: 83 C4 08           add         esp,8
  00401056: 85 C0              test        eax,eax
  ^^^^^^^^^                    ^^^^^^^^^^^^^^^^^^^^^
  00401058: 74 0F              je          00401069
Чтобы HIEW мог отличить адрес от смещения в самом файле, предварим его символом точки: ".401056"
00401056: 85C0                 test      eax,eax
00401058: 740F                 je       .000401069   -------- (1)
Ага, как раз то, что нам надо! Нажмем <F3> для перевода HIEW в режим правки, подведем курсор к команде "TEST EAX,EAX" и, нажав <Enter>, заменим ее на "XOR EAX,EAX".
00001056: 33C0                         xor       eax,eax
00001058: 740F                         je        000001069
С удовлетворением заметив, что новая команда в аккурат вписалась в предыдущую, нажмем <F9> для сохранения изменений на диске, а затем выйдет из HIEW и попробуем запустить программу, вводя первый пришедший на ум пароль.
>simple.exe
Enter password:Привет, шляпа!
Password OK
Получилось! Защита пала! Хорошо, а как бы мы действовали, не умей HIEW "переваривать" PE-файлы? Тогда бы пришлось прибегнуть к контекстному поиску. Обратим свой взор на шестнадцатеричный дамп, расположенный дизассемблером слева от ассемблерных команд. Конечно, если пытаться найти последовательность "85 C0" – код команды "TEST EAX,EAX" ничего путного из этого не выйдет, – этих самых TEST-ов в программе может быть несколько сотен, а то и больше. Комбинация "ADD ESP,8\TEST EAX,EAX" так же вряд ли будет уникальна, поскольку встречается во многих типовых конструкциях языка Си "if (func(arg1,arg2))…", "if (!func(arg1,arg2))…", "while(func(arg1,arg2)" и т.д. А вот адрес перехода, скорее всего, во всех ветках программы различен и подстрока "ADD ESP,8/TEST EAX,EAX/JE 00401069" имеет хорошие шансы на уникальность.


Попробуем найти в файле соответствующий ей код: "83 C4 08 85 C0 74 0F" (в HIEW-е для этого достаточно нажать <F7>).
Опп-с! Найдено только одно вхождение, что нам собственно и нужно. Давайте теперь попробуем модифицировать файл непосредственно в hex-режиме,
не переходя в ассемблер. Попутно возьмем себе на заметку – инверсия младшего бита кода команды приводит к изменению условия перехода на противоположное. Т.е. 74 JE à
75 JNE.
Работает? (В смысле защита свихнулась окончательно – не признает истинные пароли, зато радостно приветствует остальные). Замечательно! Остается решить: как эту взломанную программу распространять. То есть, распространить-то ее дело не хитрое – на то и существуют CDR-писцы, BBS-ы, сеть Интернет, наконец! Заливай, пиши, нарезай – не хочу. Не хотите – и правильно! Незаконное это дело – распространять программное обеспечение в обход его владельца. Эдак, и засадить могут (причем прецеденты уже имеются). Куда безопаснее возложить распространение программы на ее дистрибьюторов, но до каждого пользователя донести: как эту программу сломать. Ковыряться в законном образом приобретенном приложении потребитель вправе, а распространение информации о взломе не запрещено в силу закона о свободе информации. Правда, при ближайшем рассмотрении выясняется, что этот закон и у нас, и за океаном действует лишь формально, и, если не посадить, то по крайне мере попытаться это сделать, право охранительные органы вполне могут (и не только могут, но и делают). Когда дело касается чьих-то финансовых интересов, правосудие "отдыхает". Наивно думать, что соблюдение закона автоматически дает некие гарантии. Нет, и еще раз нет! Чувствовать себя в относительной безопасности можно лишь при условии соблюдения кодекса "да не навреди сильным мира сего".
В любом случае – информация о взломе это не совсем то же, что сам взлом и за это труднее привлечь к ответственности. Единственная проблема – попробуй-ка, объясни этим пользователям: как пользоваться hex-редактором и искать в нем такие-то байтики.


Запорют же ведь файл за милую душу! Вот для этой цели и существуют автоматические взломщики.
Для начала нужно установить, какие именно байты были изменены. Для этого нам вновь потребуется оригинальная копия модифицированного файла (предусмотрительно сохраненная перед его правкой) и какой-нибудь "сравниватель" файлов. Наиболее популярными на сегодняшний день являются c2u by Professor Nimnul и MakeCrk by Doctor Stein's labs. Первый гораздо предпочтительнее, т.к. он не только более точно придерживается наиболее популярного "стандарта", но и  умеет генерировать расширенный xck формат. На худой конец можно воспользоваться и штатной утилитой, входящей в поставку MS-DOS\Windows – fc.exe (сокращение от FileCompare).
Запустим свой любимый компаратор (это уж какой кому больше по душе) и посмотрим на результат его работы:
> fc simple.exe simple.ex_ > simple.dif
       ^-оригинальный ^ файл     ^
                      L- хакнутый?файл
                                 L- файл различий
> type simple.dif
Сравнение файлов simple.exe и SIMPLE.EX_
00001058: 74 75
Первая слева колонка указывает смещение байта от начала файла, вторая – содержимое байта оригинального файла, а третья – его значение после модификации. Теперь сравним это с отчетом утилиты c2u:
>c2u simple.exe simple.ex_
Все исправления заносятся в файл *.crx, где "*" – имя оригинального файла. Рассмотрим результат сравнения поближе:
>type simple.crx
[BeginXCK]-----------------------------------
¦ Description   : $) 1996 by Professor Nimnul
¦ Crack subject :
¦ Used packer   : None/UnKn0wN/WWPACK/PKLITE/AINEXE/DIET/EXEPACK/PRO-PACK/LZEXE
¦ Used unpacker : None/UNP/X-TRACT/iNTRUDER/AUT0Hack/CUP/TR0N
¦ Comments      :
¦ Target OS     : D0S/WiN/WNT/W95/0S¤/UNX
¦ Protection    : [--------------------] %17
¦ Type of hack  : Bit hack/JMP Correction
¦ Language      : UnKn0wN/Turbo/Borland/Quick/MS/Visual C/C++/Pascal/Assembler
¦ Size          : 28672


¦ Price         : $000
¦ Used tools    : TD386 v3.2, HiEW 5.13, C2U/486 v0.10
¦ Time for hack : 00:00:00
¦ Crack made at : 21-07-2001 12:34:21
¦ Under Music   : iR0N MAiDEN
[BeginCRA]-----------------------------------
Difference(s) between simple.exe & simple.ex_
SIMPLE.EXE
00001058: 74 75
[EndCRA]-------------------------------------
[EndXCK]-------------------------------------
Собственно, сам результат сравнений ничуть не изменился, разве что к файлу добавился текстовой заголовок, поясняющий, что это за серверный олень такой и с чем его едят. Все поля не стандартизированы, и их набор сильно разнится от одного взломщика к другому, – при желании вы можете снабдить заголовок своими собственными полями или же, напротив, выкинуть из него чужие. Однако не стоит злоупотреблять этим без серьезной необходимости и лучше придерживаться какого-то одного шаблона.
Итак. "Description" – пояснение к взлому, заполняемое в меру буйства фантазии и уровня распущенности. В нашем случае оно может выглядеть, например, так: "Тестовой взлом N1".
"Crack subject" – предмет крака, - т.е. что собственно мы только что сломали. Пишем "Парольная защита simple.exe"
"Used packer" – используемый упаковщик. Еще во времена старушки MS-DOS существовали и были широко распространены упаковщики исполняемых файлов, автоматически разжимающие файл в памяти при его запуске. Этим достигалась экономия дискового пространства (вспомните: какими смехотворными по нынешним временам были размеры винчестеров конца восьмидесятых-начала девяностых?) и параллельно с этим усиливалась защита – ведь упакованный файл недоступен для непосредственного изучения, а тем более – правки. Прежде, чем начать что-то делать, файл необходимо распаковать, причем это делать приходится и самому ломателю, и всем пользователям этого crk-файла. Поскольку, наш файл не был упакован – оставим это поле пустым, или запишем в него "None".


"Used unpacker" – рекомендуемый распаковщик (если он необходим). Дело в том, что не все распаковщики одинаковы, многие упаковщики весьма продвинуты в технике защиты и умело сопротивляются попыткам их "снять". Понятное дело, распаковщики то же не лыком шиты, и держат своих "тузов" в рукавах, но… автоматическая распаковка – штука капризная. Бывает "интеллектуальный" unpacker легко расправляется со всеми "крутыми" packer-ми, но тихо сдыхает на простых защитах, и, соответственно, случается и наоборот. Дабы не мучить пользователей утомительным перебором всех имеющихся у них распаковщиков (пользователь – он ведь то же человек!) правила хорошо тона обязывают указывать по крайней мере один заведомо подходящий unpacker, а лучше два или три сразу (вдруг какого-то из них у пользователя и не будет). Если же распаковщик не требуется – оставляйте это поле пустым или "None".
"Comments" – комментарии. Вообще-то это поле задумано для перечисления дополнительных действий, которые пользователь должен выполнить перед взломом, ну, например, снять с файла атрибут "системный" или, напротив, установить его. Но, поскольку, какие-либо дополнительные действия требуются только в экзотических случаях, в это поле обычно помещают разнообразные лозунги и комментарии (да, правильно, бывает и нецензурную брань по поводу умственных способностей разработчика защиты).
"Target OS" – операционная система для которой предназначен и (внимание!) в которой хакер тестировал сломанный продукт. Вовсе не факт, что программа сохранит после взлома черты своей прежней совместимости. Так, например, поле контрольной суммы Win 9x всегда игнорирует, а Win NT – нет и если его не скорректировать, файл запускаться не будет! В нашем случае контрольная сумма заголовка PE-файла равна нулю (так ведет себя компилятор), что означает – целостность файла не проверяется и он, после хака, будет успешно работать как под Win 9x, так и под Win NT.


"Protection" – степень "крутизны" защиты, выражаемой в процентах. 100% по идее соответствуют пределу интеллектуальных возможностей хакера – но кто же в этом захочет признаваться? Неудивительно, что "крутизну" защиты обычно занижают, порой даже больше, чем на порядок (смотрите все, вот я какой крутой хакер, для меня что угодно взломать не сложнее чем кончик хвоста обмочить!). Нечестность – не порок, но…
"Type of hack" – тип хака, - поле полезное, скорее для других хакеров, чем для пользователей, ничего не смыслящих в защитах и типах их взлома. Впрочем, с типами взломов не все гладко и у самих хакеров – общепризнанных классификацией нет. Наиболее употребляемый термин "bit-hack", как и следует из его названия, обозначает взлом посредством изменения одного или нескольких бит в одном или нескольких байтах. Частный случай bit-hack-а – JMP correction (jumping) – модификация адреса или условия перехода (то, что мы только что и проделали). "NOPing" – это bit-hack с заменой прежних инструкций на команду NOP или вставку незначащих команд, например для затирания двухбайтового JZ xxx можно применить сочетание однобайтовых INC EAX/DEC EAX.
"Language" – язык, а точнее компилятор, на котором написана программа. В нашем случае Microsoft Visual C++ (мы это знаем, поскольку только что ее компилировали), а вот как быть с чужими программами? Первое, что приходит на ум, – поискать в файле копирайты – их оставляют очень многие компиляторы, в том числе и Visual C++ - сморите: "000053d9:Microsoft Visual C++ Runtime Library". Если же компиляторов нет, то пробуем прогнать файл через IDA – она автоматически распознает большинство стандартных библиотек даже с указанием конкретной версии. В крайнем случае – пробует определить язык по самому коду, вспоминая о соглашениях Си и Паскаль, и пытаясь найти знакомые черты известных вам компиляторов (у каждого компилятора свой "почерк" и опытный хакер можно узнать не только чем компилировалась программа, но даже определить ключ оптимизации).


"Size" – размер ломаемой программы, служащий для контроля версии (чаще всего, хотя и не всегда, каждая версия программы имеет свой размер). Размер автоматически определяется утилитой c2u и самостоятельно его вставлять нет никакой нужды.
"Price" – стоимость лицензионной копии программы (должен же пользовать знать: сколько денег ему сэкономитсэкономил
этот крак!)
"Used tools" – используемые инструменты. Не заполнение этого поля считается дурным тоном – действительно же, интересно, чем именно была хакнута программа! Особенно этим интересуются пользователи, наивно полагающие, что если они раздобудут тот же DUMPBIN и HIEW защита сама собой сломается.
"Time for hack" – время, затраченное на хак, включая перерывы на "перекурить" и "сходить водички попить". Интересно, какой процент людей честно заполняет это поле, не пытаясь показаться "куче" в чужих глазах. Так что особенно доверять ему не следует…
"Crack made at" – дата завершения крака. Подставляется автоматически и править ее нет необходимости (разве что вы "жаворонок" и хотите выдать себя за "сову", проставляя время окончания взлома 3 часами ночи 31 декабря)
"Under Music" – музыка, прослушиваемая во время хака (еще не хватает поля "Имя любимого хомячка"). Вы слушали музыку во время хака? Если да – то пишете – пусть все знают ваши вкусы (за одно не забудьте цвет майки и температуру воздуха за ботом выше нуля).
В результате всех мучений у нас должно получится приблизительно следующее:
[BeginXCK]-----------------------------------
¦ Description   : Тестовый взлом №1
¦ Crack subject : Парольная
защита simple.exe
¦ Used packer   : None
¦ Used unpacker : None
¦ Comments      : Hello, Sailor! Ты слишклм долго плавал!
¦ Target OS     : WNT/W95
¦ Protection    : [--------------------] %1
¦ Type of hack  : JMP Correction
¦ Language      : Visual C/C++


¦ Size          : 28672
¦ Price         : $000
¦ Used tools    : DUMPBIN, HiEW 6.05, C2U/486 v0.10 & Brain
¦ Time for hack : 00:10:00
¦ Crack made at : 21-07-2001 12:34:21
¦ Under Music   : Paul Mauriat L'Ete Indeien "Africa"
[BeginCRA]-----------------------------------
Difference(s) between simple.exe & simple.ex_
SIMPLE.EXE
00001058: 74 75
[EndCRA]-------------------------------------
[EndXCK]-------------------------------------
Теперь нам потребуется другая утилита, цель которой прямо противоположна: используя crk (xcrk) файл, изменить эти самые байты в оригинальной программе. Таких утилит на сегодняшний день очень много, что не лучшим образом сказывается на их совместимости с различными crk форматами. Самые известные из них, – cra386 by Professor и pcracker by Doctor Stein's labs.
Из современных Windows-разработок можно отметить "Patch maker" с продвинутым пользовательским интерфейсом (см. Рисунок 2). Он включает в себя сравниватель файлов, crk-редактор, hex-редактор (для ручной замены?) и компилятор crk в исполняемые файлы, чтобы пользователям не приходилось ломать голову: что это за крак такой и как им ломать.
Может, кому-то такой интерфейс и понравится, а вот хакеры в свой массе мышь органически не переносят и любят текстовые (консольные) приложения и тетю Клаву.

Рисунок 2 0x001 Patch Maker за работой!

второй. Знакомство с дизассемблером


Надо ли милостивого бога все время просить о пощаде?

Велимир

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

Хакнуть говорите?! Что ж, это не сложно! Куда проблематичнее определиться – чем именно ее хакать. Инструментарий хакеров чрезвычайно разнообразен – чего тут только нет: и дизассемблеры, и отладчики, и API-, и message- шпионы, и мониторы обращений к файлам (портам, реестру), и распаковщики исполняемых файлов, и… Попробуй-ка, начинающему кодокопателю со всем этих хозяйством разобраться!

Впрочем, шпионы, мониторы, распаковщики – второстепенные утилиты заднего плана, а основное оружие взломщика – отладчик и дизассемблер. Рассмотрим их поближе.

Как и следует из его названия, диз-ассемблер, предназначен для диз-ассемблирования или "раз-ассемблирования" если перейти с латыни на русский {ДИС…, ДИЗ… [лат. dis, ге. dys] – приставка, обозначающая разделение отделение, отрицание; соответствует русским "раз…", "не…", сообщает понятию, к которому прилагается, отрицательный или противоположный смысл, напр. дизассоциация, дисгармония – "словарь иностранных слов"}. То есть если ассемблирование – перевод ассемблерных команд в машинный код, то дизассемблирование, напротив, перевод машинного кода в ассемблерные команды.

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

Грубо говоря, все реализации языков программирования делятся на компиляторы и интерпретаторы.

::Интерпретаторы исполняют программу в том виде, в каком она была набрана программистом. Другими словами говоря – интерпретаторы "пережевывают" исходный текст, при этом код программы доступен для непосредственного изучения безо всяких дополнительных средств.
Примером могут служить приложения, написанные на Бацике или Перле. Как известно, для их запуска требуется помимо исходного текста программы требуется иметь еще и сам интерпретатор, что неудобно ни пользователям (для исполнения программы в 10 килобайт приходится устанавливать интерпретатор в 10 мегабайт), ни разработчикам (в здравом уме и трезвой памяти раздавать всем исходные тексты своей программы!), к тому же синтаксический разбор отнимает много времени и ни один интерпретатор не может похвастаться производительностью.
 
::Компиляторы
ведут себя иначе – при первом запуске они "перемалывают" программу в машинный код, исполняемый непосредственно самим процессором без обращений к исходным текстам или самому компилятору. С человеческой точки зрения человека откомпилированная программа представляет бессмысленную мешанину шестнадцатеричных байт, разобраться в которой неспециалисту абсолютно невозможно. Это облегчает разработку защитных механизмов – не зная алгоритма, вслепую защиту не сломаешь, ну разве что она будет совсем простая.
Можно ли из машинного кода получить исходный текст программы? Нет,!
Компиляция – процесс однонаправленный. И дело тут не только в том, что безвозвратно удаляются метки и комментарии (ррразберемся и без комментариев – хакеры мы или нет?!), основной камень преткновения неоднозначность соответствия машинных инструкций конструкциям языков высокого уровня. Более того, ассемблирование так же являет собой однонаправленный процесс и автоматическое дизассемблирование принципиально невозможно. Впрочем, не будем сейчас забивать голову начинающих кодокопателей такими тонкостями и оставим эту проблему на потом.
 
::Ряд систем разработки занимает промежуточное положение между компиляторами и интерпретаторами, – исходная программа преобразуется не в машинный код, а в некоторый другой интерпретируемый язык, для исполнения которого к "откомпилированному" файлу дописывается собственный интерпретатор. Именно по такой схеме функционируют FoxPro, Clipper, многочисленные диалекты Бацика и некоторые другие языки.


Да, код программы по-прежнему исполняется в режиме интерпретации, но теперь из него удалена вся избыточная информация – метки, имена переменных, комментарии, а осмысленные названия операторов заменены их цифровыми кодами. Этот "выстрел" укладывает сразу двух зайцев: а) язык, на который переведена программа, заранее "заточен" под быструю интерпретацию и оптимизирован по размеру; б) код программы теперь недоступен для непосредственного изучения (и/или модификации).
Дизассемблирование таких программ невозможно – дизассемблер нацелен именно на машинный код, а неизвестный ему интерпретируемый язык (так же называемый p-кодом) он "не переваривает". Разумеется, p-код не переваривает и процессор!,
Его исполняет интерпретатор, дописанный к программе. Вот интерпретатор-то дизассемблер и "возьмет"! Изучая алгоритм его работы, можно понять "устройство" p-кода и выяснить назначение всех его команд. Это очень трудоемкий процесс! Интерпретаторы порой так сложны и занимают столько много мегабайт, что их анализ растягивается на многие месяцы, а то и годы. К счастью, нет нужны анализировать каждую программу – ведь интерпретаторы одной версии идентичны, а сам p-код обычно мало меняется от версии к версии, во всяком случае его ядро не переписывается каждый день. Поэтому, вполне возможно создать программу, занимающуюся переводом p-кода обратно в исходный язык. Конечно, символьные имена восстановить не удастся, но в остальном листинг будет выглядеть вполне читабельно.
 
Итак, дизассемблер применим для исследования откомпилированных программ и частично пригоден для анализа "псевдокомпилированного" кода. Раз так – он должен подойти для вскрытия парольной защиты simple.exe. Весь вопрос в том, – какой дизассемблер выбрать.
Не все дизассемблеры одинаковы. Есть среди них и "интеллектуалы", автоматически распознающие многие конструкции как-то: прологи и эпилоги функций, локальные переменные, перекрестные ссылки и т.д., а есть и "простаки" чьи способности ограничены одним лишь переводом машинных команд в ассемблерные  инструкции.


Логичнее всего воспользоваться услугами дизассемблера - интеллектуала (если он есть), но… давайте не будем спешить, а попробуем выполнить весь анализ вручную. Техника, понятное дело,
штука хорошая, на то она и придумана, чтобы решать проблемы, а не создавать новые, да вот только не всегда она оказывается под рукой и неплохо бы заранее научиться работе "в полевых условиях". на том, что всегда есть под рукой. К тому же, общение с плохим дизассемблером как нельзя лучше подчеркивает "вкусности" хорошего.
Воспользуемся уже знакомой нам утилитой DUMPBIN, настоящим "Швейцарским ножиком" со множеством полезных функций, среди которых притаился и дизассемблер. Дизассемблируем секцию кода (как мы помним, носящую имя ".text"), перенаправив вывод в файл, т.к. на экран он, очевидно, не помститься.
> dumpbin /SECTION:.text /DISASM simple.exe
>.code
Так, менее чем через секунду образовался файл ".code" с размером… с размером в целых триста с четвертью килобайт. Да исходная программа была на два порядка короче! Это же сколько времени потребуется, чтобы со всей этой шаманской грамотой разобраться?! Самое обидное – подавляющая масса кода никакого отношения к защитному механизму не имеет и представляет собой функции стандартных библиотек компилятора, анализировать которые нам ни к чему. Но как же их отличить от "полезного" кода?
Давайте подумаем. Мы не знаем, где именно расположена процедура сравнения паролей и нам неизвестно ее устройство, но можно с уверенностью утверждать, что один из ее аргументов – указатель на эталонный пароль. Остается только выяснить – по какому адресу расположен этот пароль в памяти – он-то и будет искомым значением указателя.
Заглянем еще раз в секцию данных (или в другую – в зависимости от того,
где хранится пароль):
> dumpbin /SECTION:.data /RAWDATA simple.exe >.data
RAW DATA #3
  00406000: 00 00 00 00 00 00 00 00 00 00 00 00 7B 11 40 00  ............{.@.


  00406010: E4 40 40 00 00 00 00 00 00 00 00 00 20 12 40 00  ф@@......... .@.
  00406020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
  00406030: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.
  00406040: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..
  ^^^^^^^^^                                                  ^^^^^^^^^^^^^^^
  00406050: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
  00406060: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....
Ага, пароль расположен по смещению 0x406040
(левая колонка чисел), стало быть и указатель на него равен 0x406040. Попробуем найти это число в дизассемблированном листинге тривиальным контекстным поиском в любом текстовом редакторе.
Нашли? Вот оно (в тексте выделено жирным шрифтом):
  00401045: 68 40 60 40 00     push        406040h
  0040104A: 8D 55 98           lea         edx,[ebp-68h]
  0040104D: 52                 push        edx
  0040104E: E8 4D 00 00 00     call        004010A0
  00401053: 83 C4 08           add         esp,8
  00401056: 85 C0              test        eax,eax
  00401058: 74 0F              je          00401069
Это один из двух аргументов функции 0х04010A0,
заносимых в стек машинной командой push. Второй аргумент – указатель на локальный буфер, вероятно, содержащий введенный пользователем пароль.
Тут нам придется немного отклониться от темы разговора и подробно рассмотреть передачу параметров. Наиболее распространенны всего два следующие способаы
передачи аргументов функции – через регистры и через стек.
Передача параметров через регистры наиболее быстра, но не лишена недостатков – во-первых, количество регистров весьма ограничено, а во-вторых, это затрудняет реализацию рекурсии – вызова функции из самой себя. Прежде чем заносить в регистры новые аргументы, необходимо предварительно сохранить старые в оперативной памяти. А раз так – не проще ли сразу передать аргументы через оперативную память, не мучаясь с регистрами?


Подавляющее большинство компиляторов передает аргументы через стек. Единого мнения по вопросам передачи у разработчиков компиляторов нет и встречаются по крайней мере два различных механизма, именуемые соглашениями "Си" и "Паскаль".
 
::Си-соглашение
предписывает заталкивать в стек аргументы справа на лево, т.е. самый первый аргумент функции заносится в стек последним и оказывается на его верхушке. Удаление аргументов из стека возложено не на саму функцию, а на вызываемый ее код. Это довольно расточительное решение, т.к. каждый вызов функции утяжеляет программу на несколько байт кода, но зато оно это позволяет создавать функции с переменным числом аргументов – ведь удаляет-то их из стека не сама функция, а вызывающий ее код, который наверняка знает точное количество переданных аргументов.
Очистка стека обычно выполняется командой "ADD ESP,xxx" – где 'xxx'
количество удаляемых байт. Поскольку, в 32-разрядном режиме каждый аргумент, как правило, занимает четыре байта, количество аргументов функции вычисляется так: . Оптимизирующие компиляторы могут использовать более хитрый код – для очистки стека от нескольких аргументов они частенько из "выталкивают" в неиспользуемые регистры командой "POP" или и вовсе очищают стек не сразу же после выхода из функции, а совсем в другом месте – где это удобнее компилятору.
 
::Паскаль-соглашение предписывает заносить аргументы в стек слева на право, т.е. самый первый аргумент функции заносится в стек в первую очередь и оказывается в самом его "низу". Удаление аргументов из функции возложено на саму функцию, и обычно осуществляется командой "RET xxx" – т.е. возврат из подпрограммы со снятием xxx байт со стека.
Возвращаемое функцией значение в обоих соглашениях передается через регистр EAX (или EDX:EAX при возвращении 64-разрядных переменных).
Поскольку, исследуемая нами программа написана на Си и, стало быть, заносит аргументы справа налево, ее исходный текст выглядел приблизительно так:


(*0x4010A0) (ebp-68, "myGOODpassword")
В том, что аргументов именно два, а не, скажем, четные или десять, нас убеждает команда "ADD ESP,8", расположенная вслед за CALL.
  0040104E: E8 4D 00 00 00     call        004010A0
  00401053: 83 C4 08           add         esp,8
Остается выяснить назначение функции 0x4010A0, хотя… если поднапрячь свою интуицию этого можно и не делать.!
И так ясно – это функция сравнивает пароль, иначе, зачем бы ей его передавали? Как она это делает – вопрос десятый, а вот что нас действительно интересует – возвращенное ею значение. Так, опускаемся на одну строчку ниже:
  0040104E: E8 4D 00 00 00     call        004010A0
  00401053: 83 C4 08           add         esp,8
  00401056: 85 C0              test        eax,eax
  00401058: 74 0F              je          00401069
Что мы видим? Команда TEST EAX,EAX проверяет возвращенное функцией значение на равенство нулю, и если оно действительно равно нулю следующая за ней команда JE совершает прыжок на 0x401096
строку.
В противном же случае (т.е. если EAX !=0)…
  0040105A: 68 50 60 40 00     push        406050h
Похоже еще на один указатель. Не правда ли? Проверим это предположение, заглянув в сегмент данных:
  00406050: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..
Уже теплее! Указатель вывел нас на строку "Wrong password", очевидно выводимую следующей функцией на экран. Значит, ненулевое значение EAX свидетельствует о ложном пароле, а нуль – об истинном.
О'кей, тогда переходим к анализу валидной ветви программы…
  0040105F: E8 D0 01 00 00     call        00401234
  00401064: 83 C4 04           add         esp,4
  00401067: EB 02              jmp         0040106B
  00401069: EB 16              jmp         00401081

  00401081: 68 60 60 40 00     push        406060h
  00401086: E8 A9 01 00 00     call        00401234
Так еще, один указатель.Ну, а с функцией 0x401234 мы уже встречались выше – она (предположительно) служит для вывода строк на экран. Ну а сами строки можно отыскать в сегменте данных. На этот раз там притаилась "Password OK"
Оперативные соображения следующие: если заменить команду JE на JNE, то программа отвергнет истинный пароль, как неправильный, а любой неправильный пароль воспримет как истинный. А если заменить "TEST EAX,EAX" на "XOR EAX,EAX", то после исполнения этой команды регистр EAX будет всегда
равен нулю, какой бы пароль не вводился.
Дело за малым – найти эти самые байтики в исполняемом файле и малость поправить их.

Способ 0. Бряк на оригинальный пароль.


Используя поставляемую вместе с "Айсом" утилиту "wldr" загрузим ломаемый нами файл, указав его имя в командной строке, например, так:

>wldr simple.exe

Да, я знаю, что wldr – 16-разрядный загрузчик, и NuMega рекомендует использовать его 32-разрядную версию loadrer32, специально разработанную для Win 9x\NT. Это так, но loader32 частенько глючит (в частности не всегда останавливается на первой строчке запускаемой программы), а wldr успешно работает и 32-разрядными приложениями, единственный присущий ему недостаток – отсутствие поддержки длинных имен файлов.

Если отладчик настроен корректно, на экране появится черное текстовое окно, обычно вызывающее большое удивление у начинающих – это в нашу-то это эпоху визуальщины серый текст и командный язык a la command.com.!

А почему бы и нет? Набрать на клавиатуре нужную команду куда быстрее, чем отыскать ее в длинной веренице вложенных меню, мучительно вспоминая где же вы ее в последний раз видели. К тому же язык – это естественное средство выражения мыслей, а меню – оно годится разве что для выбора блюд в ресторане. Вот хороший пример – попробуйте с помощью проводника Windows вывести на печать список файлов такой-то директории. Не получается? А в MS-DOS это было так просто dir >PRN и никаких лаптей!

Если в окне кода видны одни лишь инструкции "INVALID" (а оно так и будет) не пугайтесь – просто Windows еще не успела спроецировать исполняемый файл в память и выделилавыделить ему страницы. Стоит нажать <F10> (аналог команды "P" – трассировка без заходов в функцию) или <F8> (аналог команды "T" – трассировка с заходами в функции) как все сразу же станет на свои места.

001B:00401277  INVALID

001B:00401279  INVALID

001B:0040127B  INVALID

001B:0040127D  INVALID

:P

001B:00401285  PUSH    EBX

001B:00401286  PUSH    ESI

001B:00401287  PUSH    EDI

001B:00401288  MOV     [EBP-18],ESP

001B:0040128B  CALL    [KERNEL32!GetVersion]


001B:00401291  XOR     EDX,EDX

001B:00401293  MOV     DL,AH

001B:00401295  MOV     [0040692C],EDX

Обратите внимание: в отличие от дизассемблера DUMPBIN, Айс распознает имена системных функций, чем существенно упрощает анализ. Впрочем, анализировать всю программу целиком,

нет никакой нужды. Давайте попробуем наскоро найти защитный механизм, и, не вникая в подробности его функционирования, напрочь отрубить защиту. Легко сказать, но сделать еще проще! Вспомним: по какому адресу расположен в памяти оригинальный пароль. Э… что-то плохо у нас с этим получается – то ли память битая, то ли медведь на лапоть наступил, но точный адрес никак не хочет вспоминаться. Не хочет – не надо. Найдем-ка мы его самостоятельно!

В этом нам поможет команда "map32" выдающая карту памяти выбранного модуля (наш модуль называется "simple" – по имени исполняемого файла за вычетом расширения).

:map32 simple

Owner     Obj Name  Obj#  Address        Size      Type

simple    .text     0001  001B:00401000  00003F66  CODE  RO

simple    .rdata    0002  0023:00405000  0000081E  IDATA RO

simple    .data     0003  0023:00406000  00001E44  IDATA RW

           ^^^^           ^^^^^^^^^^^^^

Вот он, адрес начала секции  ".data". То, что пароль находится в секции ".data", надеюсь, читатель все еще помнит. Даем команду "d 23:406000" (возможно предварительно придется создать окно командой "wc" – если окна данных нет) и, нажав, <ALT-D> для перехода в это окно, прокрутим его содержимое <стрелкой вниз> или кирпичом на <Page Down>. Впрочем, кирпич излишен, – долго искать не придется:

0023:00406040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..

0023:00406050 57 72 6F 6E 67 20 70 61-73 73 77 6F 72 64 0A 00  Wrong password..

0023:00406060 50 61 73 73 77 6F 72 64-20 4F 4B 0A 00 00 00 00  Password OK.....

0023:00406070 47 6E 40 00 00 00 00 00-40 6E 40 00 01 01 00 00  Gn@.....@n@.....



0023: 00406080 00 00 00 00 00 00 00 00-00 10 00 00 00 00 00 00  ................

0023:00406090 00 00 00 00 00 00 00 00-00 00 00 00 02 00 00 00  ................

0023:004060A0 01 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

0023:004060B0 00 00 00 00 00 00 00 00-00 00 00 00 02 00 00 00  ................

Есть контакт! Задумаемся еще раз (второй раз за этот день) чтобы проверить корректность введенного пользователем пароля защита, очевидно должна сравнить его с оригинальным. А раз так – установив точку останова на чтение памяти по адресу 0x406040, мы поймаем "за хвост" сравнивающий механизм. Сказано – сделано.

:bmpm 406040

Теперь нажимаем <CTRL-D> для выхода из отладчика (или отдаем команду "x") и вводим любой пришедший на ум пароль, например, "KPNC++". Отладчик "всплывает" незамедлительно:

001B:004010B0  MOV     EAX,[EDX]

001B:004010B2  CMP     AL,[ECX]

001B:004010B4  JNZ     004010E4                                      (JUMP ^)

001B:004010B6  OR      AL,AL

001B:004010B8  JZ      004010E0

001B:004010BA  CMP     AH,[ECX+01]

001B:004010BD  JNZ     004010E4

001B:004010BF  OR      AH,AH

Break due to BPMB #0023:00406040 RW DR3  (ET=752.27 milliseconds)

  MSR LastBranchFromIp=0040104E

    MSR LastBranchToIp=004010A0

В силу архитектурных особенностей процессоров Intel, бряк срабатывает после инструкции, выполнившей "поползновение", т.е. CS:EIP указывают на следующую выполняемую команду. В нашем случае – JNZ 004010E4, а к памяти, стало быть, обратилась инструкция CMP AL, [ECX]. А что находится в AL? Поднимаем взгляд еще строкой выше – "MOV EAX,[EDX]". Можно предположить, что EСX содержит указатель на строку оригинального пароля (поскольку он вызвал всплытие отладчика), а EDX в таком случае – указатель на введенный пользователем пароль. Проверим наше предположение.

:d edx

0023:00406040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..



:d edx

0023: 0012FF18 4B 50 4E 43 2B 2B 0A 00-00 00 00 00 00 00 00 00  KPNC++..........

И правда – догадка оказалась верна. Теперь вопрос – а как это заломить? Вот, скажем, JNZ можно поменять на JZ или, еще оригинальнее, заменить EDX на ECX – тогда оригинальный пароль будет сравниваться сам с собой! Погодите, погодите… не стоит так спешить! А что если мы находится не в теле защиты, а в библиотечной функции (действительно, мы находится в теле strcmp), – ее изменение приведет к тому, что программа любые строки будет воспринимать как идентичные. Любые – а не только строки пароля. Это не повредит нашему примеру, где strcmp вызывалась лишь однажды, но завалит нормальное полнофункциональное приложение. Что же делать?

Выйти из strcmp и подкорректировать тот самый "IF",

который анализирует правильный – не правильный пароль. Для этого служит команда "P RET" (трассировать пока не встреться ret – инструкция возврата из функции).

:P RET

001B:0040104E  CALL    004010A0

001B:00401053  ADD     ESP,08

001B:00401056  TEST    EAX,EAX

001B:00401058  JZ      00401069

001B:0040105A  PUSH    00406050

001B:0040105F  CALL    00401234

001B:00401064  ADD     ESP,04

001B:00401067  JMP     0040106B

Знакомые места! Помните, мы их посещали дизассемблером? Алгоритм действий прежний – запоминаем адрес команды "TEST" для последующей замены ее на "XOR" или записываем последовательность байт, идентифицирующую… эй, постойте, а где же наши байты – шестнадцатеричное представление команд? Коварный Айс по умолчанию их не выводит, и заставить его это делать помогает команда "CODE ON"

code on

001B:0040104E  E84D000000          CALL    004010A0

001B:00401053  83C408              ADD     ESP,08

001B:00401056  85C0                TEST    EAX,EAX

001B:00401058  740F                JZ      00401069

001B:0040105A  6850604000          PUSH    00406050

001B:0040105F  E8D0010000          CALL    00401234

001B:00401064  83C404              ADD     ESP,04



001B:00401067  EB02                JMP     0040106B

Вот, теперь совсем другое дело! Но можно ли быть уверенным, что эти байтики по этим самым адресам будут находиться в исполняемом файле? Вопрос не так глуп, каким кажется на первый взгляд. Попробуйте сломать описанным выше методом пример "crackme0x03". На первый взгляд он очень похож на simple.exe, - даже оригинальный пароль располагается по тому же самому адресу. Ставим на него бряк, дожидаемся всплытия отладчика, выходим из сравнивающей процедуры и попадаем на точно такой же код, который уже встречался нам ранее.

001B:0042104E  E87D000000          CALL    004210D0

001B:00421053  83C408              ADD     ESP,08====

001B:00421056  85C0                TEST    EAX,EAX

001B:00421058  740F                JZ      00421069

Сейчас мы запустим HIEW, перейдем по адресу 0x421053 и… эй, постой, HIEW ругается и говорит, что в файле нет такого адреса! Последний байт заканчивается на 0x407FFF. Быть может, мы находимся в теле системной функции Windows? Но нет – системные функции Windows расположены значительно выше – начиная с адреса 0x80000000.

Фокус весь в том, что PE-файл может быть загружен по адресу отличному от того, для которого он был создан (это свойство называется перемещаемостью), - при этом система автоматически корректирует все ссылки на абсолютные адреса, заменяя их новыми значениями. В результате – образ файла в памяти не будет соответствовать тому, что записано на диске. Хорошенькое начало! Как же теперь найти место, которое нужно править?

Задачу несколько облегчает тот факт, что системный загрузчик умеет перемещать только DLL, а исполняемые файлы всегда пытается загрузить по "родному" для них адресу. Если же это невозможно – загрузка прерывается с выдачей сообщения об ошибке. Выходит, мы имеем дело с DLL, загруженной исследуемой нами защитой. Хм… вроде бы не должно быть здесь никаких DLL – да и откуда бы им взяться?

Что ж, изучим листинг 2 на предмет выяснения: как же он работает.



#include <stdio.h>

#include <windows.h>

__declspec(dllexport) void Demo()

^^^^^^^^^^^^^^^^^^^^^

{

#define PASSWORD_SIZE 100

#define PASSWORD      "myGOODpassword\n"

int count=0;

char buff[PASSWORD_SIZE]="";

for(;;)

{

printf("Enter password:");

fgets(&buff[0],PASSWORD_SIZE-1,stdin);

if (strcmp(&buff[0],PASSWORD))

printf("Wrong password\n");

else break;

if (++count>2) return -1;        

}

printf("Password OK\n");

}

main()

{

HMODULE hmod;

void (*zzz)();

if ((hmod=LoadLibrary("crack0~1.exe"))

&& (zzz=(void (*)())GetProcAddress(h,"Demo")))

zzz();

}

Листинг 2 Исходный текст защиты crackme 0x3

Какой, однако, извращенный способ вызова функции! Защита экспортирует ее непосредственно из самого исполняемого файла и этот же файл загружает как DLL (да, один и тот же файл может быть одновременно и исполняемым приложением и динамической библиотекой!).

"Все равно ничего не сходится", - возразит программист средней квалификации, - "всем же известно, что Windows не настолько глупа, чтобы дважды грузить один и тот же файл, - LoadLibrary всего лишь возвратит базовый адрес модуля crackme0x03, но не станет выделять для него память". А вот как бы не так! Хитрая защита обращается к файлу по его альтернативному короткому имени, вводя системный загрузчик в глубокое заблуждение!

Система выделяет память и возвращает базовый адрес загружаемого модуля в переменной hmod. Очевидно, код и данные этого модуля смещены на расстояние hmod – base, где base – базовый адрес модуля – тот, с которым работают HIEW и дизассемблер. Базовый адрес узнать нетрудно – достаточно вызвать тот же DUMPBIN с ключом "/HEADERS" (его ответ приведен в сокращенном виде)

>dumpbin /HEADERS crack0x03

OPTIONAL HEADER VALUES

...

400000 image base

^^^^^^^^^^^^^^^^^

...



Значит, базовый адрес – 0x400000

(в байтах). А опередить адрес загрузки  можно командой "mod -u" отладчика: (ключ u разрешает выводить только прикладные, т.е. не системные модули).

:mod -u

hMod Base     PEHeader Module Name      File Name

     00400000 004000D8 crack0x0         \.PHCK\src\crack0x03.exe

     00420000 004200D8 crack0x0         \.PHCK\src\crack0x03.exe

     ^^^^^^^^

     77E80000 77E800D0 kernel32         \WINNT\system32\kernel32.dll

     77F80000 77F800C0 ntdll            \WINNT\system32\ntdll.dll

Смотрите, загружено сразу две копии crack0x03, причем последняя расположена по адресу 0x420000, как раз что нам надо! Теперь нетрудно посчитать, что адрес 0x421056 (тот, что мы пытались последний раз найти в ломаемом файле) "на диске" будет соответствовать адресу 0x421056

– (0x42000

– 0x400000) == 0x421056 – 0x20000 == 0x401056. Смотрим:

00401056: 85C0                     test      eax,eax

00401058: 740F                     je       .000401069   -------- (1)

Все верно – посмотрите, как хорошо это совпадает с дампом отладчика:

001B:00421056  85C0                TEST    EAX,EAX

001B:00421058  740F                JZ      00421069

Разумеется, описанная методика вычислений применима к любым DLL, а не только тем, что представляют собой исполняемый файл.

А вот, если бы мы пошли не путем адресов, а попытались найти в ломаемой программе срисованную с отладчика последовательность байт, включая и ту часть, которая входит в CALL 00422040 – интересно, нашли бы мы ее или нет?

001B:0042104E  E87D000000          CALL    004210D0

001B:00421053  83C408              ADD     ESP,08

001B:00421056  85C0                TEST    EAX,EAX

001B:00421058  740F                JZ      00421069

:Образ файла в памяти.

.0040104E: E87D000000              call     .0004010D0   -------- (1)

.00401053: 83C408                  add       esp,008 ;"•"

.00401056: 85C0                    test      eax,eax



.00401058: 740F                    je       .000401069   -------- (2)

:Образ файла на диске

Вот это новость – командам CALL 0x4210D0 и CALL 0x4010D0

соответствует один и тот же машинный код – E8 7D 00 00 00! Как же такое может быть?! А вот как –  аргумент операнд процессорной инструкции "0xE8" представляет собой не смещение подпрограммы, а разницу смещений подпрограммы и инструкции, следующей за командой call. Т.е. в первом случае: 0x421053

(смещение инструкции, следующей за CALL) + 0x0000007D

(не забываем об обратном порядке байтов в двойном слове) == 0x4210D0, - вот он, искомый адрес. Таким образом, при изменении адреса загрузки, коррекция кодов команд CALL не требуется.

"Оценка по аналогии основывается на предположении, что если два или более объекта согласуются друг с другом в некоторых отношениях, то они, вероятно, согласуются и в других отношениях"

Ганс Селье "От мечты к открытию"

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

В примере crack0x03 среди прочего кода есть и такая строка (найдите ее с помощью hiew):

004012C5: 89154C694000                 mov       [00040694C],edx

Легко видеть, что команда MOV обращается к ячейке не по относительному, а по абсолютному адресу. Вопрос: а) выясните, что произойдет при изменении адреса загрузки модуля; б) как вы думаете – будет ли теперь совпадать образ файла на диске и в памяти?

Заглянув отладчиком по адресу 0x4212C5 (0x4012C5 + 0x2000) мы увидим, что обращение идет совсем не к ячейке 0x42694C, а – 0x40694C! Наш модуль самым бессовестным образом вторгается в чужие владения, модифицируя их по своему усмотрению. Так и до краха системы докатиться недолго! В данном случае этого не происходит только потому, что искомая строка расположена в Startup-процедуре (стартовом коде) и выполняется лишь однажды – при запуске приложения, а из загруженного модуля не вызывается.



Другое дело, если бы функция Demo() обращалась к какой-нибудь статической переменной – компилятор, подставив ее непосредственное смещение, сделал бы модуль неперемещаемым! После сказанного становится непонятно: как же тогда ухитряются работать динамически подключаемые библиотеки (DLL), адрес загрузки которых заранее неизвестен? Поразмыслив некоторое время, мы найдем, по крайней мере, два решения проблемы:

Первое – вместо непосредственной адресации использовать относительную, например: [reg+offset_val], где reg – регистр, содержащий базовый адрес загрузки, а offset_val – смещение ячейки от начала модуля. Это позволит модулю грузится по любому адресу, но заметно снизит производительность программы уже хотя бы за счет потери одного регистра….

Второе – научить загрузчик корректировать непосредственные смещения в соответствии с выбранным базовым адресом загрузки. Это, конечно, несколько замедлит загрузку, но зато не ухудшит быстродействие самой программы. Не факт, что временем загрузки можно свободно пренебречь, но парни из Microsoft выбрали именно этот способ.

Единственная проблема – как отличить действительные непосредственные смещения от констант, совпадающих с ними по значению? Не дизассемблировать же в самом деле DLL, чтобы разобраться какие именно ячейки в ней необходимо "подкрутить"? Верно, куда проще перечислить их адреса в специальной таблице, расположенной непосредственно в загружаемом файле и носящей гордое имя "Таблицы перемещаемых элементов" или (Relocation

[Fix Up] table по-английски). За ее формирование отвечает линкер (он же – компоновщик) и такая таблица присутствует в каждой DLL.

Чтобы познакомиться с ней поближе откомпилируем и изучим следующий пример:

::fixupdemo.c

__declspec(dllexport) void meme(int x)

{

static int a=0x666;

a=x;

}

> cl fixupdemo.c /LD

Листинг 3 Исходный текст fixupdemo.c

Откомпилируем и тут же дизассемблируем его: "DUMPBIN /DISASM fixupdemo.dll" и "DUMPBIN /SECTION:.data /RAWDATA".



  10001000: 55                 push        ebp

  10001001: 8B EC              mov         ebp,esp

  10001003: 8B 45 08           mov         eax,dword ptr [ebp+8]

  10001006: A3 30 50 00 10     mov         [10005030],eax

               ^^^^^^^^^^^                  ^^^^^^^^

  1000100B: 5D                 pop         ebp

  1000100C: C3                 ret

RAW DATA #3

10005000: 00 00 00 00 00 00 00 00 00 00 00 00 33 24 00 10  ............3$..

10005010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

10005020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

10005030: 66 06 00 00 E3 11 00 10 FF FF FF FF 00 00 00 00  f...у...    ....

          ^^^^^

Судя по коду, запись содержимого EAX всегда происходит в ячейку 0x10005030, но не торопите с выводами! "DUMPBIN /RELOCATIONS fixupdemo.dll":

BASE RELOCATIONS #4

    1000 RVA,      154 SizeOfBlock

       7 HIGHLOW

       ^       

      1C HIGHLOW

      23 HIGHLOW

      32 HIGHLOW

      3A HIGHLOW

Таблица перемещаемых элементов-то не пуста! И первая же ее запись указывает на ячейку 0x100001007, полученную алгебраическим сложением смещения 0x7 с RVA-адресом 0x1000 и базовым адресом загрузки 0x10000000 (получите его с помощью DUMPBIN самостоятельно). Смотрим – ячейка 0x100001007 принадлежит инструкции "MOV [0x10005030],EAX" и указывает на самый старший байт непосредственного смещения. Вот это самое смещение и корректирует загрузчик в ходе подключения динамической библиотеки (разумеется, если в этом есть необходимость).

Хотите проверить? Пожалуйста, - создадим две копии одной DLL (например, copy fixupdemo.dll fixupdemo2.dll) и загрузим их поочередной следующей программой:

::fixupload.c

#include <windows.h>

main()

{

void (*demo) (int a);

HMODULE h;

if ((h=LoadLibrary("fixupdemo.dll")) &&

(h=LoadLibrary("fixupdemo2.dll")) &&

(demo=(void (*)(int a))GetProcAddress(h,"meme")))



demo(0x777);

}

> cl fixupload

Листинг 4 Исходный текст fixupload

Поскольку, по одному и тому же адресу две различные DLL не загрузишь (откуда же системе знать, что это одна и та же DLL!), загрузчику приходится прибегать к ее перемещению. Загрузим откомпилированную программу в отладчик и установим точку останова на функцию LoadLibraryA. Это, – понятное дело, – необходимо чтобы пропустить Startup-код и попасть в тело функции main. (Как легко убедиться исполнение программы начинается отнюдь не с main, а со служебного кода, в котором очень легко утонуть). Но откуда взялась загадочная буква 'A' на конце имени функции? Ее происхождение тесно связано с введением в Windows поддержки уникода – специальной кодировки, каждый символ в которой кодируется двумя байтами, благодаря чему приобретает способность выражать любой из 216 = 65.536 знаков, – количество достаточно для вмещения практически всех алфавитов нашего мира. Применительно к LoadLibrary – теперь имя библиотеки может быть написано на любом языке, а при желании и на любом количестве любых языков одновременно, например, на русско-француско-китайском. Звучит заманчиво, но не ухудшает ли это производительность? Разумеется, ухудшает, еще как – уникод требует жертв! Самое обидное – в подавляющем большинстве случаев вполне достаточно старой доброй кодировки ASCII (во всяком случае нам,

русским, и американцам). Так какой же смысл бросать драгоценные такты процесса на ветер? Ради производительности было решено поступиться размером, создав отдельные варианты функций для работы с уникодом и ASCII-символами. Первые получили суффикс 'W' (от Wide – широкий), а вторые – 'A' (от ASCII). Эта тонкость скрыта от прикладных программистов – какую именно функцию вызывать 'W' или 'A' решает компилятор, но при работе с отладчиком необходимо указывать точное имя функции – самостоятельно определить суффикс он не в состоянии. Камень преткновения в том, что некоторые функции, например, ShowWindows вообще не имеют суффиксов – ни 'A', ни 'W' и их библиотечное имя совпадает с каноническим.


Как же быть?

Самое простое – заглянуть в таблицу импорта препарируемого файла и отыскать там вашу функцию. Например, применительно к нашему случаю:

> DUMPBIN /IMPORTS fixupload.exe > filename

> type filename

                 19D  HeapDestroy

                 1C2  LoadLibraryA

                  CA  GetCommandLineA

                 174  GetVersion

                  7D  ExitProcess

                 29E  TerminateProcess

...

Из приведенного выше фрагменты видно, что LoadLibrary все-таки 'A', а вот функции ExitProcess и TerminateProcess не имеют суффиксов, поскольку вообще не работают со строками.

Другой путь – заглянуть в SDK. Конечно, библиотечное имя функций в нем отсутствует, но в "Quick Info" мимоходом приводится информация и поддержке уникода (если таковая присутствует). А раз есть уникод – есть суффиксы 'W' и 'A', соответственно, наоборот – где нет уникода, нет и суффиксов. Проверим?

Вот так выглядит Quick Info от LoadLibrary:

QuickInfo

  Windows NT: Requires version 3.1 or later.

  Windows: Requires Windows 95 or later.

  Windows CE: Requires version 1.0 or later.

  Header: Declared in winbase.h.

  Import Library: Use kernel32.lib.

  Unicode: Implemented as Unicode and ANSI versions on Windows NT.

На чистейшем английском языке здесь сказано – "Реализовано как Unicode и ANSI версии на Windows NT". Стоп! С NT все понятно, а как насчет "народной" девяносто восьмой (пятой)? Беглый взгляд на таблицу экспорта KERNEL32.DLL показывает: такая функция там есть, но, присмотревшись повнимательнее, мы с удивлением обнаружим, что ее точка входа совпадает с точками входа десятка других функций!

    ordinal hint RVA      name

        556  1B3 00039031 LoadLibraryW

Третья колонка в отчете DUMPBIN это RVA-адрес – виртуальный адрес начала функции за вычетом базового адреса загрузки файла. Простой контекстный поиск показывает, что он встречается не единожды. Воспользовавшись программой-фильтром srcln (см. Приложения Исходные тексты)



для получения связного протокола, мы увидим следующее:

   21:        118    1 00039031 AddAtomW

  116:        217   60 00039031 DeleteFileW

  119:        220   63 00039031 DisconnectNamedPipe

  178:        279   9E 00039031 FindAtomW

  204:        305   B8 00039031 FreeEnvironmentStringsW

  260:        361   F0 00039031 GetDriveTypeW

  297:        398  115 00039031 GetModuleHandleW

  341:        442  141 00039031 GetStartupInfoW

  377:        478  165 00039031 GetVersionExW

  384:        485  16C 00039031 GlobalAddAtomW

  389:        490  171 00039031 GlobalFindAtomW

  413:        514  189 00039031 HeapLock

  417:        518  18D 00039031 HeapUnlock

  440:        541  1A4 00039031 IsProcessorFeaturePresent

  455:        556  1B3 00039031 LoadLibraryW

  508:        611  1E8 00039031 OutputDebugStringW

  547:        648  20F 00039031 RemoveDirectoryW

  590:        691  23A 00039031 SetComputerNameW

  592:        693  23C 00039031 SetConsoleCP

  597:        698  241 00039031 SetConsoleOutputCP

  601:        702  245 00039031 SetConsoleTitleW

  605:        706  249 00039031 SetCurrentDirectoryW

  645:        746  271 00039031 SetThreadLocale

  678:        779  292 00039031 TryEnterCriticalSection

Вот это сюрприз! Все уникодеовые  –

функции под одной крышей! Поскольку, трудно поверить в идентичность реализаций LoadLibraryW и, скажем, DeleteFileW, остается предположить, что мы имеем дело с "заглушкой", которая ничего не делает, а только возвращает ошибку. Следовательно, в 9x действительно, функция LoadLibraryW не реализована.

Но, вернемся, к нашим баранам от которых нам пришлось так далеко отойти. Итак, вызываем отладчик, ставим бряк на LoadLibraryA, выходим из отладчика и терпеливо дожидаемся его всплытия. Должно ждать, к счастью, не приходится…

KERNEL32!LoadLibraryA                     

001B:77E98023  PUSH    EBP

001B:77E98024  MOV     EBP,ESP



001B:77E98026  PUSH    EBX

001B:77E98027  PUSH    ESI

001B:77E98028  PUSH    EDI

001B:77E98029  PUSH    77E98054

001B:77E9802E  PUSH    DWORD PTR [EBP+08]

Отдаем команду "P RET" для выхода из LoadLibraryA (анализировать ее, в самом деле, ни к чему) и оказываемся в легко узнаваемом теле функции main.

001B:0040100B  CALL    [KERNEL32!LoadLibraryA]

001B:00401011  MOV     [EBP-08],EAX           

001B:00401014  CMP     DWORD PTR [EBP-08],00

001B:00401018  JZ      00401051

001B:0040101A  PUSH    00405040

001B:0040101F  CALL    [KERNEL32!LoadLibraryA]

001B:00401025  MOV     [EBP-08],EAX

001B:00401028  CMP     DWORD PTR [EBP-08],00

Обратите внимание на содержимое регистра EAX – функция возвратила в нем адрес загрузки (на моем компьютере равный 0x10000000). Продолжая трассировку (<F10>), дождитесь выполнения второго вызова LoadLibraryA – не правда ли, на этот раз адрес загрузки изменился? (на моем компьютере он равен 0x0530000).

Приблизившись к вызову функции demo (в отладчике это выглядит как PUSH 00000777\ CALL [EBP-04] – "EBP-04" ни о чем не говорит, но вот аргумент 0x777 определенно что-то нам напоминает, - см. исходный текст fixupload.c), не забудьте переменить руку с <F10> на <F8>, чтобы войти внутрь функции.

001B:00531000  55                  PUSH    EBP

001B:00531001  8BEC                MOV     EBP,ESP

001B:00531003  8B4508              MOV     EAX,[EBP+08]

001B:00531006  A330505300          MOV     [00535030],EAX

001B:0053100B  5D                  POP     EBP

001B:0053100C  C3                  RET

Вот оно! Системный загрузчик скорректировал адрес ячейки согласно базовому адресу загрузки самой DLL. Это, конечно, хорошо, да вот проблема – в оригинальной DLL нет ни такой ячейки, ни даже последовательности "A3 30 50 53 00", в чем легко убедиться контекстным поиском. Допустим, вознамерились бы мы затереть эту команду NOP-ми.


Как это сделать?! Вернее, как найти это место в оригинальной DLL?

Обратим свой взор выше, на команды, заведомо не содержащие перемещаемых элементов – PUSH EBP/MOV EBP, ESP/MOV EAX,[EBP+08]. Отчего бы не поискать последовательность "55  8B EC  xxx  A3"? В данном случае это сработает, но если бы перемещаемые элементы были густо перемешаны "нормальными" ничего бы не вышло. Опорная последовательность оказалась бы слишком короткой для поиска и выдала бы множество ложных срабатываний.

Более изящно и надежно вычислить истинное содержимое перемещаемых элементов, вычтя их низ разницу между действительным и рекомендуемым адресом загрузки. В данном случае: 0x535030 /модифицированный загрузчиком адрес/ – (0x530000 /базовый адрес загрузки/ - 0x10000000 /рекомендуемый адрес загрузки/) == 0x10005030. Учитывая обратный порядок следования байт, получаем, что инструкция MOV [10005030], EAX в машинном коде должна выглядеть так: "A3 30 50 00 10". Ищем ее HIEW-ом, и чудо – она есть!


Способ 1. Прямой поиск введенного пароля в памяти


Был бы омут, а черти будут.

народная поговорка

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

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

Взглянем еще раз на исходный текст ломаемого нами примера "simple.c"

for(;;)

{

printf("Enter password:");

fgets(&buff[0],PASSWORD_SIZE,stdin);

if (strcmp(&buff[0],PASSWORD))

printf("Wrong password\n");

else break;

if (++count>2) return -1;

}

Обратите внимание – в buff читается введенный пользователем пароль, сравнивается с оригиналом, затем (при неудачном сравнении) запрашивается еще раз, но (!) при этом buff не очищается! Отсюда следует, что если после выдачи ругательства "Wrong password" вызвать отладчик и пройтись по памяти контекстным поиском, можно обнаружить тот заветный buff, а остальное уже – дело техники!

Итак, приступим (мы еще не знаем, во что мы ввязываемся – но, увы – в жизни все сложнее, чем в теории). Запускам SIMPLE.EXE, вводим любой пришедший на ум пароль (например, "KPNC Kaspersky++"), пропускаем возмущенный вопль "Wrong" мимо ушей и нажимаем <Ctrl-D> - "горячую" комбинацию клавиш для вызова Айса.
Так, теперь будем искать? Подождите, не надо бежать впереди лошадей: Windows 9x\NT – это не Windows 3.x и, тем более, не MS-DOS с единым адресным пространством для всех процессоров. Теперь, по соображениям безопасности, - дабы один процесс ненароком не залез во владения другого, каждому из них предоставляется собственное адресное пространство. Например, у процесса A по адресу 23:0146660 может быть записано число "0x66", у процесса B по тому же самому адресу 23:0146660 может находиться "0x0", а у процесса C и вовсе третье значение. Причем, процессы А, B и C не будет даже подозревать о существовании друг друга (ну, разве что воспользуются специальными средствами межпроцессорного взаимодействия).

Подробнее обо всем этом читайте у Хелен или Рихтера, здесь же нас больше заботит другое – вызванный по <Ctrl-D> отладчик "всплывает" в произвольном процессе (скорее всего Idle) и контекстный поиск в памяти ничего не даст. Необходимо насильно переключить отладчик в необходимый контекст адресного пространства и лишь затем что-то предпринимать.

Из прилагаемой к Айсу документации можно узнать, что переключение контекстов осуществляется командой ADDR, за которой следует либо имя процесса, урезанное до восьми символов, либо его PID. Узнать и то, и другое можно с помощью другой команды – PROC (В том, случае если имя процесса синтаксически неотличимо от PID, например, "123", приходится использовать PID процесса – вторая колонка цифр слева, в отчете PROC).

:addr simple

Отдаем команду "addr simple" и… ничего не происходит, даже значения регистров остаются неизменными! Не волнуйтесь – все ОК, что и подтверждает надпись 'simple' в правом нижнем углу, идентифицирующая текущий процесс. А регистры… это небольшой глюк Айса. Он них игнорирует, переключая только адреса. В частности поэтому, трассировка переключенной программы невозможна. Вот поиск – другое дело. Это – пожалуйста!

:s 23:0 L -1 "KPNC Kaspersky"



Пояснения: первый слева аргумент после s – адрес, записанный в виде "селектор: смещение". Под Windows 2000 для адресации данных и стека используется селектор номер 23, в других операционных системах он может отличаться (и отличается!). Узнать его можно загрузив любую программу, и списав содержимое регистра DS. Смещение – вообще-то, начинать поиск с нулевого смещения – идея глупая. Судя по карте памяти, здесь расположен служебный код и искомого пароля быть не может. Впрочем, это ничему не вредит, и так гораздо быстрее, чем разбираться: с какого адреса загружена программа, и откуда именно начинать поиск. Третий аргумент – "L –1" – длина региона для поиска. "-1", как нетрудно догадаться, – поиск "до победы". Далее - обратите внимание, что мы ищем не всю строку – а только ее часть ("KPNC Kaspersky++" против "KPNC Kaspersky") . Это позволяет избавиться от ложных срабатываний – Айс любит выдавать ссылки на свои внутренние буфера, содержащие шаблон поиска. Вообще-то они всегда расположены выше 0х80000000. Там – где никакой нормальный пароль "не живет", но все же будет нагляднее если по неполной подстроке находится именно наша строка.

Pattern found at 0023:00016E40 (00016E40)

Так, по крайней мере, одно вхождение уже найдено. Но вдруг в памяти есть еще несколько? Проверим это, последовательно отдавая команды "s" вплоть до выдачи сообщения "Pattern not found" или превышении адреса поиска 0x80000000.

:s

Pattern found at 0023:0013FF18 (0013FF18)

:s

Pattern found at 0023:0024069C (0024069C)

:s

Pattern found at 0023:80B83F18 (80B83F18)

Целых два вхождения, да еще одно "в уме" – итого три! Не много ли для нас, начинающих? Во-первых, неясно – вводимые пароли они, что плоятся ака кролики? Во-вторых, ну не ставить же все три точки останова. В данном случае четырех отладочных регистров процессора хватит, а как быть, если бы мы нашли десяток вхождений? Да и в трех бряках немудрено заблудиться с непривычки!



Итак – начинаем работать головой. Вхождений много, вероятнее всего потому, что при чтении ввода с клавиатуры символы сперва попадают в системные буфера, которые и дают ложные срабатывания. Звучит вполне правдоподобно, но вот как отфильтровать "помехи"?

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

:map32 simple

Owner     Obj Name  Obj#  Address        Size      Type

simple    .text     0001  001B:00011000  00003F66  CODE  RO

simple    .rdata    0002  0023:00015000  0000081E  IDATA RO

simple    .data     0003  0023:00016000  00001E44  IDATA RW

Ура, держи Тигру за хвост, есть одно отождествление! Буфер на 0x16E40 принадлежит сегменту данных и, видимо, это и есть то, что нам нужно. Но не стоит спешить! Все не так просто. Поищем-ка адрес 0x16E40 в самом файле simple.exe (учитывая обратный порядок байт это будет "40 E6 01 00"):

> dumpbin /SECTION:.data /RAWDATA simple.exe

RAW DATA #3

  00016030: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00  Enter password:.

  00016040: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00  myGOODpassword..

  00016050: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00  Wrong password..

  00016060: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00  Password OK.....

  00016070: 40 6E 01 00 00 00 00 00 40 6E 01 00 01 01 00 00  @n......@n......

  00016080: 00 00 00 00 00 00 00 00 00 10 00 00 00 00 00 00  ................

Есть, да? Даже два раза! Посмотрим теперь, кто на него ссылается – попробуем найти в дизассемблированном тексте подстроку "16070" – адрес первого двойного слова, указывающего на наш буфер.

  00011032: 68 70 60 01 00     push        16070h     ; <<<

  00011037: 6A 64              push        64h ; Макс. длина пароля (== 100 dec)

  00011039: 8D 4D

98           lea         ecx,[ebp-68h]



;Указатель ^^^^^^ на буфер куда записывать пароль

  0001103C: 51                 push        ecx

  0001103D: E8 E2 00 00 00     call        00011124   ; fgets

  00011042: 83 C4 0C           add         esp,0Ch    ; Выталкиваем три аргумента

В общем, все ясно, за исключением загадочного указателя на указатель 0x16070. Заглянув в MSDN, где описан прототип этой функции, мы обнаружим, что "таинственный незнакомец" – указатель на структуру FILE (аргументы по Си-соглашению, как мы помним заносятся в стек справа налево). Первый член структуры FILE – указатель на буфер (файловый ввод-вывод в стандартной библиотеке Си буферизован, и размер буфера по умолчанию составляет 4 Кб). Таким образом, адрес 0x16E40 – это указатель на служебный буфер и из списка "кандидатов в мастера" мы его вычеркиваем.

Двигаемся дальше. Претендент номер два – 0x24069C. Легко видеть он выходит за пределы сегмента данных и вообще непонятно чему принадлежит. Почесав затылок, мы вспомним о такой "вкусности" Windows как куча (heap). Посмотрим, что у нас там…

:heap 32 simple

    Base      Id  Cmmt/Psnt/Rsvd  Segments  Flags     Process

    00140000  01  0003/0003/00FD         1  00000002  simple

    00240000  02  0004/0003/000C         1  00008000  simple

    00300000  03  0008/0007/0008         1  00001003  simple

Ну, Тигр, давай на счастье хвост! Есть отождествление! Остается выяснить, кто выделил этот блок памяти – система под какие-то свои нужды или же сам программист. Первое, что бросается в глаза – какой-то подозрительно-странный недокументированный флаг 0x8000. Заглянув в WINNT.H можно даже найти его определение, которое, впрочем, мало чем нам поможет, разве что намекнет на системное происхождение оного.

#define HEAP_PSEUDO_TAG_FLAG            0x8000

А чтобы окончательно укрепить нашу веру, загрузим в отладчик любое подвернувшееся под лапу приложение и тут же отдадим команду "heap 32 proc_name". Смотрите – система автоматически выделяет из кучи три региона! Точь-в-точь такие, как и в нашем случае.


ОК, значит, и этот кандидат ушел лесом.

Остается последний адрес – 0x13FF18. Ничего он не напоминает? Постой-ка, постой. Какое было значение ESP при загрузке?! Кажется 0x13FFC4 или около того (внимание, в Windows 9x стек расположен совершенно в другом месте, но все рассуждения справедливы и для нее – необходимо лишь помнить местоположение стека в собственной операционной системе и уметь навскидку его узнавать).

Поскольку, стек растет снизу  вверх (т.е. от старших адресов к младшим), адрес 0x13FF18 явно находится в стеке, а потому очень сильно похож на наш буфер. Уверенность подогревает тот факт, что большинство программистов размешают буфера в локальных переменных, ну а локальные переменные, в свою очередь, размешаются компилятором в стеке.

Ну что, попробуем установить сюда бряк?

:bpm 23:13FF18

:x

Break due to BPMB #0023:0013FF18 RW DR3  (ET=369.65 microseconds)

  MSR LastBranchFromIp=0001144F

    MSR LastBranchToIp=00011156

001B:000110B0  MOV     EAX,[EDX]

001B:000110B2  CMP     AL,[ECX]     

001B:000110B4  JNZ     000110E4

001B:000110B6  OR      AL,AL

001B:000110B8  JZ      000110E0

001B:000110BA  CMP     AH,[ECX+01]

001B:000110BD  JNZ     000110E4

001B:000110BF  OR      AH,AH

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

:d edx

0023:0013FF18 4B 50 4E 43 2D 2D 0A 00-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++

:d ecx

0023:00016040 6D 79 47 4F 4F 44 70 61-73 73 77 6F 72 64 0A 00  myGOODpassword..

Ну, а остальное мы уже проходили. Выходим из сравнивающей процедуры по P RET, находим условный переход, записываем его адрес (ключевую последовательность для поиска), правим исполняемый файл и все ОК.

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


Его основное достоинство – простота. А недостатки… недостатков у него много.

– если программист очистит буфера после сравнения, поиск веденного пароля ничего не даст. Разве что останутся системные буфера, которые так просто не затрешь, но отследить перемещения пароля из системных буферов в локальные не так-то просто!

– ввиду изобилия служебных буферов, очень трудно определить: какой из них "настоящий". Программист же может располагать буфер и в сегменте данных (статический буфер), и в стеке (локальный буфер), и в куче, и даже выделять память низкоуровневыми вызовами типа VirtualAlloc или… да мало ли как разыграется его фантазия. В результате, под час приходится "просеивать" все найденные вхождения тупым перебором.

В качестве тренировки разберем другой пример – "crackme 01". Это то же самое, что simple.exe, только с GUI-рым интерфейсом и ключевая процедура выглядит так:

void CCrackme_01Dlg::OnOK()

{

char buff[PASSWORD_SIZE];

m_password.GetWindowText(&buff[0],PASSWORD_SIZE);

if (strcmp(&buff[0],PASSWORD))

{

MessageBox("Wrong password");

m_password.SetSel(0,-1,0);

return;

}

else

{

MessageBox("Password OK");

}

CDialog::OnOK();

}

Листинг 5 Исходный текст ядра защитного механизма crackme 01

Кажется, никаких сюрпризов не предвидится. Что ж, вводим пароль (как обычно "KPNC Kaspersky++"), выслушиваем "ругательство" и, до нажатия ОК, вызываем отладчик, переключаем контекст…

:s 23:0 L -1 'KPNC Kaspersky'

Pattern found at 0023:0012F9FC (0012F9FC)

:s

Pattern found at 0023:00139C78 (00139C78)

Есть два вхождения! И оба лежат в стеке. Подбросим монетку, чтобы определить с какого из них начать? (Правильный ответ – с первого). Устанавливаем точку останова и терпеливо ждем всплытия отладчика. Всплытие ждать себя не заставляет, но показывает какой-то странный, откровенно "левый" код. Ждем "x" для выхода, - следует целый каскад всплытий одно непонятнее другого.



Лихорадочно подергивая бородку (варианты – усики, волосы в разных местах) соображаем: функция "CCrackme_01Dlg::OnOK" вызывается непосредственно в момент нажатия на "ОК" – ей отводится часть стекового пространства под локальные переменные, которая автоматически "экспроприируется" при выходе из функции – переходя во всеобщее пользование. Таким образом, локальный буфер с введенным нами паролем существует только в момент его проверки, а потом автоматически затирается. Единственная зацепка – модальный диалог с ругательством. Пока он на экране – буфер еще содержит пароль и его можно найти в памяти. Но это не сильно помогает в отслеживании когда к этому буферу произведет обращение… Приходится терпеливо ждать, отсеивая ложные всплытия один за другим. Наконец, в окне данных искомая строка, а в окне кода – какой-то осмысленный код:

0023:0012F9FC 4B 50 4E 43 20 4B 61 73-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++

0023:0012FA0C 00 01 00 00 0D 00 00 00-01 00 1C C0 A8 AF 47 00  ..............G.

0023:0012FA1C 10 9B 13 00 78 01 01 00-F0 3E 2F 00 00 00 00 00  ....x....>/.....

0023:0012FA2C 01 01 01 00 83 63 E1 77-F0 AD 47 00 78 01 01 00  .....c.w..G.x...

001B:004013E3  8A10                MOV     DL,[EAX]

001B:004013E5  8A1E                MOV     BL,[ESI] 

001B:004013E7  8ACA                MOV     CL,DL

001B:004013E9  3AD3                CMP     DL,BL

001B:004013EB  751E                JNZ     0040140B

001B:004013ED  84C9                TEST    CL,CL

001B:004013EF  7416                JZ      00401407

001B:004013F1  8A5001              MOV     DL,[EAX+01]

На всякий "пожарный" смотрим, на что указывает ESI:

:d esi

0023:0040303C 4D 79 47 6F 6F 64 50 61-73 73 77 6F 72 64 00 00  MyGoodPassword..

Остается "пропадчить" исполняемый файл, и тут (как и следовало ожидать по закону бутерброда) нас ждут очередные трудности. Во-первых, хитрый компилятор заоптимизировал код, подставив код функции strcmp вместо ее вызова, а во-вторых, условных переходов… да ими все кишит! Попробуй-ка, найди нужный.


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

>dumpbin /DISASM crackme_01.exe

  004013DA: BE 3C 30 40 00     mov         esi,40303Ch

  0040303C: 4D 79 47 6F 6F 64 50 61 73 73 77 6F 72 64 00 MyGoodPassword

В регистр ESI помещается указатель на оригинальный пароль

  004013DF: 8D 44 24 10        lea         eax,[esp+10h]

В регистр EAX – указатель на пароль, введенный пользователем

  004013E3: 8A 16              mov         dl,byte ptr [esi]

  004013E5: 8A 1E              mov         bl,byte ptr [esi]

  004013E7: 8A CA              mov         cl,dl

  004013E9: 3A D3              cmp         dl,bl

Проверка первого символа на совпадение

  004013EB: 75 1E              jne         0040140B ß---(3) ---à (1)

Первый символ уже не совпадает – дальше проверять бессмысленно!

  004013ED: 84 C9              test        cl,cl

Первый символ первой строки  равен нулю?

  004013EF: 74 16              je          00401407 --à

(2)

Да, достигнут конец строки – значит, строки идентичны

  004013F1: 8A 50 01           mov         dl,byte ptr [eax+1]

  004013F4: 8A 5E 01           mov         bl,byte ptr [esi+1]

  004013F7: 8A CA              mov         cl,dl

  004013F9: 3A D3              cmp         dl,bl

Проверяем следующую пару символов

  004013FB: 75 0E              jne         0040140B ---à (1)

Если не равна – конец проверке

  004013FD: 83 C0 02           add         eax,2

  00401400: 83 C6 02           add         esi,2

Перемещаем указатели строк на два символа вперед

  00401403: 84 C9              test        cl,cl

Достигнут конец строки?

  00401405: 75 DC              jne         004013E3 -à (3)

Нет, еще не конец, сравниваем дальше.

  00401407: 33 C0              xor         eax,eax ß--- (2)



  00401409: EB

05              jmp         00401410 --à

(4)

Обнуляем EAX (strcmp в случае успеха возвращает ноль) и выходим

  0040140B: 1B C0              sbb         eax,eax ß--- (3)

  0040140D: 83 D8 FF           sbb         eax,0FFFFFFFFh

Эта ветка получат управление при несовпадении строк. EAX устанавливает равным в ненулевое значение (подумайте почему).

  00401410: 85 C0              test        eax,eax ß--- (4)

Проверка значения EAX на равенство нулю

  00401412: 6A 00              push        0

  00401414: 6A 00              push        0

Что-то заносим в стек…

  00401416: 74 38              je         00401450 <<<< ---à(5)

Прыгаем куда-то….

  00401418: 68 2C 30 40 00     push        40302Ch

  0040302C: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 00 .Wrong password

Ага, "Вронг пысворд". Значит, прыгать все-таки надо…. Смотрим, куда указывает je (а код ниже – уже не представляет интереса – и так ясно: это "матюгальщик").

Теперь, когда алгоритм защиты в общих чертах ясен, можно ее и сломать, например, поменяв условный переход в строке 0x401416 на безусловный jump short (код 0xEB).


Способ 2. Бряк на функции ввода пароля


Вы боитесь творить, потому что творения ваши отражают вашу истинную суть.

Фрэнк Херберт "Ловец душ"

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

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

Раз уж речь зашла за окна, запустим наш GUI "крякмис" и установим точку останова на функцию GetWindowTextA ("bpx GetWinodwTextA"). Поскольку, эта функция – системная, точка останова будет глобальной, т.е. затронет все приложения в системе, поэтому, заблаговременно закройте все лишнее от греха подальше. Если установить бряк до запуска "крякмиса", то мы словим несколько ложных всплытий, возникающих вследствие того, что система сама читает содержимое окна в процессе формирования диалога.

Вводим какой-нибудь пароль ("KPNC Kaspersky++" по обыкновению), нажимаем <ENTER> - отладчик незамедливает всплыть:

USER32!GetWindowTextA                                 

001B:77E1A4E2  55                  PUSH    EBP         

001B:77E1A4E3  8BEC                MOV     EBP,ESP

001B:77E1A4E5  6AFF                PUSH    FF

001B:77E1A4E7  6870A5E177          PUSH    77E1A570

001B:77E1A4EC  68491DE677          PUSH    77E61D49

001B:77E1A4F1  64A100000000        MOV     EAX,FS:[00000000]


001B:77E1A4F7  50                  PUSH    EAX

Во многих руководствах по взлому советуется тут же выйти из функции по P RET, мол, что ее анализировать-то, но не стоит спешить! Сейчас самое время выяснить: где расположен буфер вводимой строки и установить на него бряк. Вспомним какие аргументы и в какой последовательности принимает функция (а, если не вспомним, то заглянем в SDK):

int GetWindowText(

  HWND hWnd,        // handle to window or control with text

  LPTSTR lpString,  // address of buffer for text

  int nMaxCount     // maximum number of characters to copy

);

Может показаться, раз программа написана на Си, то и аргументы заносятся в стек по Си-соглашению. А вот и нет! Все API функции Windows всегда вызываются по Паскаль- соглашению, на каком бы языке программа ни была написана. Таким образом, аргументы заносятся в стек слева направо, а последним в стек попадает адрес возврата. В 32-разрядной Windows все аргументы и сам адрес возврата занимают двойное слово (4 байта), поэтому, чтобы добраться до указателя на строку, необходимо к регистру указателю вершины стека (ESP) добавить восемь (одно двойное слово на nMaxCount, другое – на сам lpString). Нагляднее это изображено на рис. 3



Рисунок 3 0х02 Состояние стека на момент вызова GetWindowsText

Получить содержимое ячейки по заданному адресу в Айсе можно с помощью оператора "звездочка", вызов которого в нашем случае выглядит так (подробнее – см. документацию, прилагаемую к отладчику):

:d *(esp+8)

0023:0012F9FC 1C FA 12 00 3B 5A E1 77-EC 4D E1 77 06 02 05 00  ....;Z.w.M.w....

0023:0012FA0C 01 01 00 00 10 00 00 00-01 00 2A C0 10 A8 48 00  ..........*...H.

0023:0012FA1C 10 9B 13 00 0A 02 04 00-E8 3E 2F 00 00 00 00 00  .........>/.....

0023:0012FA2C 01 02 04 00 83 63 E1 77-08 DE 48 00 0A 02 04 00  .....c.w..H.....

В буфере мусор – так и следовало ожидать, ведь строка еще не считана. Давайте выйдем из функции по p ret и посмотрим что произойдет (только потом уже нельзя будет пользоваться конструкцией d *esp+8, т.к.


после выхода из функции аргументы будут вытолкнуты из стека):

: p ret

:d 0012F9FC

0023:0012F9FC 4B 50 4E 43 20 4B 61 73-70 65 72 73 6B 79 2B 2B  KPNC Kaspersky++

0023:0012FA0C 00 01 00 00 0D 00 00 00-01 00 1C 80 10 A8 48 00  ..............H.

0023:0012FA1C 10 9B 13 00 0A 02 04 00-E8 3E 2F 00 00 00 00 00  .........>/.....

0023:0012FA2C 01 02 04 00 83 63 E1 77-08 DE 48 00 0A 02 04 00  .....c.w..H.....

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

001B:004013E3  8A10                MOV     DL,[EAX]

001B:004013E5  8A1E                MOV     BL,[ESI]

001B:004013E7  8ACA                MOV     CL,DL

001B:004013E9  3AD3                CMP     DL,BL

001B:004013EB  751E                JNZ     0040140B

001B:004013ED  84C9                TEST    CL,CL

001B:004013EF  7416                JZ      00401407

001B:004013F1  8A5001              MOV     DL,[EAX+01]

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

Этот способ – универсален и впоследствии мы еще не раз им воспользуемся. Вся соль – определить ключевую функцию защиты и поставить на нее бряк. Под Windows все "поползновения" (будь то обращения к ключевому файлу, реестру и т.д.) сводятся к вызову API-функций, перечень которых хотя и велик, но все же конечен и известен заранее.


Способ 3. Бряк на сообщения


Любая завершенная дисциплина имеет свои штампы, свои модели, свое влияние на обучающихся.

Френк Херберт "Дюна"

Если у Вас еще не закружилась голова от количества выпитого во время хака пива, с вашего позволения мы продолжим. Каждый, кто хоть однажды программировал под Windows, наверняка знает, что в Windows все взаимодействие с окнами завязано на сообщениях. Практически все оконные API-функции на самом деле представляют собой высокоуровневые "обертки", посылающие окну сообщения. Не является исключением и GetWindowTextA, – аналог сообщения WM_GETTEXT.

Отсюда следует – чтобы считать текст из окна вовсе не обязательно обращаться к GetWindowTextA, - можно сделать это через SendMessageA(hWnd, WM_GETTEXT, (LPARAM) &buff[0]). Именно так и устроена защита в примере "crack 02". Попробуйте загрузить его и установить бряк на GetWindowTextA (GetDlgItemTextA). Что, не срабатывает? Подобная мера используется разработчиками для запутывания совсем уж желторотых новичков, бегло изучивших пару faq по хаку и тут же бросившихся в бой.

Так может, поставить бряк на SendMessageA? В данном случае в принципе можно, но бряк на сообщение WM_GETTEXT – более универсальное решение, срабатывающее независимо от того, как читают окно.

Для установки бряка на сообщение в Айсе предусмотрена специальная команда – "BMSG", которой мы и пользовались в первом издании этой книги. Но не интереснее ли сделать это своими руками?

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

<Ctrl-D>

:addr crack02

:hwnd crack02

Handle    Class                         WinProc    TID  Module

 050140    #32770 (Dialog)              6C291B81    2DC crack02

  05013E    Button                      77E18721    2DC crack02


  05013C    Edit                        6C291B81    2DC crack02

  05013A    Static                      77E186D9    2DC crack02

Быстро обнаруживает себя окно редактирования, с адресом оконной процедуры 0x6C291B81. Поставим сюда бряк? Нет, еще не время – ведь оконная процедура вызывается не только при чтении текста, а гораздо чаще. Как бы установить бряк на то, что нам нужно, отсеяв все остальные сообщения? Для начала изучим прототип этой функции:

LRESULT CALLBACK WindowProc(

  HWND hwnd,      // handle to window

  UINT uMsg,      // message identifier

  WPARAM wParam,  // first message parameter

  LPARAM lParam   // second message parameter

);

Как нетрудно подсчитать, в момент вызова функции, аргумент uMsg – идентификатор сообщения будет лежать по смещению 8 относительно указателя вершины стека ESP. Если он равен WM_GETTEXT (непосредственное значение 0xD) – недурно бы всплыть!

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

:bpx 6C291B81 IF (esp->8)==WM_GETTEXT

:x

Выходим их отладчика, вводим какой-нибудь текст в качесвте пароля, скажем "Hello", нажимаем <ENTER>, отладчик тут же "всплывает"

Break due to BPX #0008:6C291B81  IF ((ESP->8)==0xD) (ET=2.52 seconds)

Вот, он хвост Тигры и уши плюшевого медведя! Остается определить адрес буфера, в который возвращается считанная строка. Начинаем соображать: указатель на буфер передается через аргумент lParam (см. в SDK описание WM_GETTEXT), а сам lParam размещается в стеке по смещению 0x10, относительно ESP:

адрес возврата  ß ESP

hwnd            ß ESP + 0x4

uMsg            ß ESP + 0x8

wParam          ß ESP + 0xC

lParam          ß ESP + 0x10

Даем команду вывода этого буфера в окно данных, выходим из оконной процедуры по P RET и… видим только что введенный нами текст "Hello"



:d *(esp+10)

:p ret

0023: 0012EB28 48 65 6C 6C 6F 00 05 00-0D 00 00 00 FF 03 00 00  Hello...........

0023:0012EB38 1C ED 12 00 01 00 00 00-0D 00 00 00 FD 86 E1 77  ...............w

0023:0012EB48 70 3C 13 00 00 00 00 00-00 00 00 00 00 00 00 00  p<..............

0023:0012EB58 00 00 00 00 00 00 00 00-98 EB 12 00 1E 87 E1 77  ...............w

:bpm 23:12EB28

Установив точку останова, мы ловим одно откровенно "левое" всплытие отладчика (это видно по явно не "юзерскому" значению селектора CS, равного 8) и, уже тянем руку, чтобы нажать 'x' продолжив отслеживание нашего бряка, как вдруг краем глаза замечаем….

0008:A00B017C  8A0A                MOV     CL,[EDX]

0008:A00B017E  8808                MOV     [EAX],CL

0008:A00B0180  40                  INC     EAX

0008:A00B0181  42                  INC     EDX

0008:A00B0182  84C9                TEST    CL,CL

0008:A00B0184  7406                JZ      A00B018C

0008:A00B0186  FF4C2410            DEC     DWORD PTR [ESP+10]

0008:A00B018A  75F0                JNZ     A00B017C

Эге, буфер-то не "сквозной", - система не отдает его "народу", а копирует в другой буфер. Это видно потому, как из указателя на "наш" буфер EDX символ копируется в CL (то, что EDX – указатель на "наш" буфер следует из того, что он вызвал всплытие отладчика), а из CL он копируется в [EAX], где EAX – какой-то указатель (о котором пока мы еще не можем сказать ничего определенного). Далее – оба указателя увеличиваются на единицу и CL (последний считанный символ) проверяется на равенство нулю – если конец строки не достигнут, то все повторяется. Что ж, суждено нам следить сразу за двумя буферами – ставим еще один бряк.

:bpm EAX

:x

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

001B:004013F2  8A1E                MOV     BL,[ESI]

001B:004013F4  8ACA                MOV     CL,DL    



001B:004013F6  3AD3                CMP     DL,BL

001B:004013F8  751E                JNZ     00401418

001B:004013FA  84C9                TEST    CL,CL

001B:004013FC  7416                JZ      00401414

001B:004013FE  8A5001              MOV     DL,[EAX+01]

001B:00401401  8A5E01              MOV     BL,[ESI+01]

  В Windows 9x обработка сообщений реализована несколько иначе, чем в NT. В частности, оконная процедура окна редактирования находится в 16-разрядном коде. А это – сегментная модель памяти (треска хвостом вперед под хвост Тигре) a la сегмент : смещение. Представляется любопытным механизм передачи адреса – в какой же параметр засунут сегмент? Чтобы ответить на это, взглянем на отчет Айса:

 Break due to BMSG 0428 WM_GETTEXT  (ET=513.11 milliseconds) hWnd=0428 wParam=0666 lParam=28D70000 msg=000D WM_GETTEXT        ^           ^        ^--^^--^        |           |  сегмент/    \смещение  дескриптор окна  |                   |              макс. кол-во символов для чтения

Адрес целиком умещается в 32-разрядном аргументе lParam – 16-разрядный сегмент и 16-разрядное смещение. Посему, точка останова должна выглядеть так: "bpm 28D7:0000"


Способы затруднения анализа программ


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

Френк Херберт "Дети Дюны"

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

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

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

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

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

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

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

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

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

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

Но даже на уровне нулевого кольца в Windows очень трудно что-либо скрыть – для обеспечения совместимости со всем парком Windows-подобных операционных систем приходится использовать только документированные возможности. Строить в "окнах" защиту – все равно, что пытаться заблудиться в парке. Будь там хоть миллион деревьев – все они геометрически правильно расположены и обильно увешены табличками "выход – там".

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


Техника хакерских атак Фундаментальные основы хакерства


Крис Касперски

Светлой памяти Сергея Иванова – главного редактора издательства "Солон" –  посвящается эта книга.

Автор.



Выполнение кода в стеке


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

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

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

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

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

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

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



изданий без ошибок не существует.


… изданий без ошибок не существует. Немало их и в оригинальной документации от самой Microsoft, равно как и от других компаний-производителей. Поэтому желательно выработать в себе железное правило – не верить ничему, каким бы логичным и естественным оно ни казалось. Проверяйте! Каждое слово, каждую запятую, каждую строчку примера – проверяете!
Доверчивать читателей – вполне простительна, но для авторов это – тяжкий грех. Если уж взялся за перо (тьфу ты, за клавиатуру) будь любезен прежде чем печатать что-то тщательно проверить – правильно ли это или нет!
[1]
А может и чуточку раньше
[2]
Например, Microsoft Visual C всегда, независимо от прототипа функции main передает ей три аргумента – указатель на массив указателей переменных окружения, указатель на массив указателей аргументов командной строки и количество аргументов командной строки, а все остальные функции стартового кода принимают меньшее количество аргументов
[3]
Тип Turbo-Pascal
[4]
Зачастую сказанное остается верным и для 16- или 8- разрядных переменных, но некоторые компиляторы для увеличения быстродействия расширяют переменную до 32-разрядов, а потом обнуляют ее старшую часть. Естественно, содержимое переменной между этими операциями содержит мусор.