Самомодифицирующийся код в современных операционных системах
"- Это мы говорим, будто мы выдумываем. На самом деле все давным-давно выдумано."
Братья Стругацкие "Трудно быть богом"
Лет десять-двадцать тому назад, в эпоху рассвета 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+61j
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+3o
Выражение “DATA XREF: sub_401000+3o” называется перекрестной ссылкой и свидетельствует о том, что в третьей строке процедуры 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+3u
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:0106o
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:0106o
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-разрядов, а потом обнуляют ее старшую часть. Естественно, содержимое переменной между этими операциями содержит мусор.