Информационные технологииStfw.Ru 🔍

Низкоуровневый кодинг

Учимся программировать на ассемблере Крис Касперски ака мыщъх
🕛 17.09.2009, 12:09
Добыча недокументированных функций и возможностей из недр операционной системы, создание и обезвреживание вирусов, адаптация приложений под собственные нужды, рассекречивание алгоритмов и заимствование чужих идей, взлом приложений… Список можно продолжать до бесконечности. Сфера применения ассемблера настолько широка, что сложно представить, как некоторые хакеры без него обходятся. Хакеру «асм» просто необходим. Именно поэтому в Кодинге и появляется эта новая замечательная рубрика, открывающая двери в удивительный мир, расположенный за фасадом высокоуровневого программирования.
Ассемблер - мощное оружие, дающее безграничную власть над системой. Это седьмое чувство и второе зрение. Когда выскакивает хорошо известное окошко с воплем о критической ошибке, прикладники лишь матерятся и разводят руками (мол, это карма у программы такая). Информация об ошибке для них китайская грамота. Но не для ассемблерщика! Он спокойно идет по указанному адресу и правит баг, зачастую даже без потери несохраняемых данных! Давай же разберемся, что такое на самом деле этот ассемблер и как им пользовать и как на нем программировать.

Философия ассемблера

Ассемблер - это низкоуровневый язык, оперирующий машинными понятиями и концепциями. Не ищи команду вывода строки "hello, world!". Здесь ее нет. В асме тебе придется довольствоваться тем, что умеет процессор. А умеет он вот что: сложить/вычесть/разделить/умножить/сравнить два числа и в зависимости от полученного результата передать управление на ту или иную ветку программы, переслать число с одного места в другое, записать число в порт или прочитать его оттуда. Управление периферией, кстати, осуществляется именно через порты или через специальную область памяти (например, видеопамять). Чтобы вывести символ на терминал необходимо обратиться к технической документации на видеокарту, а чтобы прочитать сектор с диска - к документации по накопителю. К счастью, эту часть работы берут на себя драйверы, и выполнять ее вручную обычно не требуется (к тому же, в нормальных операционных системах, таких, например, как Windows NT с прикладного уровня порты вообще недоступны).
Другой машинной концепцией является регистр. Объяснить, что это такое, не погрешив против истины, невозможно. Поэтому вместо того чтобы врубаться в определение (которое наверняка в ужасном виде можно найти в учебниках по асму), лучше просто запомнить, что основных регистров на x86 всего семь. И прежде чем складывать, вычитать или каким-нибудь другим образом манипулировать двумя числами, по крайней мере, одно из них необходимо загрузить в регистр. Другое же может находиться почти где угодно. Хочешь - в оперативке, хочешь - в регистре. Регистры предпочтительнее тем, что они намного быстрее оперативной памяти, частых обращений к которой следует избегать.
Все эти действия (работа с памятью и т.п.) происходят на арене, называемой адресным пространством. Адресное пространство - это просто совокупность ячеек виртуальной памяти, доступной процессору. Операционные системы типа Windows 9x и большинство *nix-систем создают для каждого приложения свой независимый 4 Гбайтный регион, в котором можно выделить, по меньшей мере, три области: область кода, область данных и стек.

Стек - это такой способ хранения данных. Что-то среднее между списком и массивом (читайте Кнута, он крут). Команда PUSH кладет новую порцию данных на верхушку стека, а команда POP - снимает ее оттуда. Это позволяет сохранять данные в памяти не заботясь об их абсолютных адресах. Очень удобно! Вызов функции и возврат из нее происходит как раз с помощью этого механизма. Команда CALL func забрасывает в стек адрес следующей за ней команды, а RET стягивает его оттуда. Указатель на текущую вершину стека хранится в регистре ESP, а дно… формально стек ограничен лишь протяженностью адресного пространства, а на самом деле - количеством выделенной ему памяти. Направление роста стека: от больших адресов - к меньшим. Еще говорят, что он растет снизу вверх.
Вернемся к нашим баранам. Поговорим об основных в x86 регистрах. Ты наверняка видел в асм-листингах такие обозначения как EAX, EBX, ECX, EDX, ESI, EDI - это регистры общего назначения. Они могут свободно участвовать в любых математических операциях или операциях обращения к памяти. Их всего семь. Семь 32-разрядных регистров. Четыре первых из них (EAX, EBX, ECX и EDX) допускают обращения к своим 16-разрядным половинкам, хранящим младшее слово - AX, BX, CX и DX. Каждый из них в свою очередь делиться на старший и младший байты - AH/AL, BH/BL, CH/CL и DH/DL. Важно понять, что AL, AX и EAX это не три разных регистра, а разные части одного и того же регистра!
Регистр же, обозначение которого ты вряд ли мог встретить в листинге - это EIP, содержащий указатель на следующую выполняемую команду. Непосредственно он недоступен для модификации, но его можно изменить манипулируя инструкциями перехода (Jxx, CALL etc).
Существуют так же и другие регистры - сегментные, мультимедийные, регистры математического сопроцессора, отладочные регистры. Без хорошего справочника в них легко запутаться и утонуть, и, на первых порах мы их касаться не будем.

Из Си в Асм

Основной ассемблерной командой является MOV (пересылка данных), которую можно уподобить оператору присвоения. c = 0x333 из Си на языке ассемблера записывается примерно как MOV EAX, 333h (обрати внимание на разницу записи шестнадцатеричных чисел!). Можно так же написать MOV EAX, EBX (записать в регистр EAX значение регистра EBX).
Указатели заключаются в квадратные скобки. Сишное a = *b на ассемблере записывается как MOV EAX, [EBX]. При желании, к указателю можно добавить смещение: a = b[0x66] эквивалентно MOV EAX, [EBX + 0x66].
Переменные объявляются директивами DB (переменная в один байт), DW (переменная в одно слово), DD (переменная в двойное слово) и т.д. Знаковость переменных при их объявлении не указывается. Одна и та же переменная в различных участках программы может интерпретироваться и как число со знаком и как число без знака.
Для загрузки переменной в указатель применяется либо команда LEA, либо MOV с директивой offset.

Основные типы пересылок данных

LEA EDX,b ;// регистр EDX содержит указатель на переменную b
MOV EBX,a ;// регистр EBX содержит значение переменной a
MOV ECX, offset a ;// регистр ECX содержит указатель на переменную a
MOV [EDX],EBX ;// скопировать переменную a в b
MOV b, EBX ;// скопировать переменную b в а
MOV b, a ;// !!!ошибка!!! так делать нельзя!!!
;// оба аргумента команды MOV не могут быть в памяти!
a DD 66h ;// long a = 0x66;
b DD ? ;// long b;
Теперь перейдем к условным переходам. Никакого "if" в обычном ассемблере нет, и эту операцию приходится осуществлять в два этапа. Первый - использование команды CMP, которая сравнит два числа и сохранит результат своей работы во флагах. Флаги, кстати - это биты специального регистра, описание которого заняло бы слишком много места и поэтому здесь не рассматривается. Достаточно запомнить три основных состояния флагов: меньше (bellow или less), больше (above или great) или равно (equal). Второй этап - это переход в нужную часть программы в зависимости от результатов сравнивания. Для этого существует семейство команд условного перехода Jxx. Команды проверяют условие «xx» и, если оно истинно, совершают прыжок по указанному адресу. Например, JE прыгает, если числа равны (Jump if Equal), а JNE если неравны (Jump if Not Equal). JB/JA работают с беззнаковыми числами, а с JL/JG - со знаковыми. Любые два не противоречащих друг другу условия могут быть скомбинированы друг с другом, например, JBE - переход в случае, если одно беззнаковое число меньше или равно другому. Безусловный же переход осуществляется командой JMP.

Конструкция CMP/Jxx больше всего похожа на Бейсковское IF xxx GOTO, чем на Си. Вот несколько примеров ее использования:

Основные типы условных переходов
CMP EAX, EBX ;// сравнить EAX и EBX
JZ xxx ;// если они равны переход на xxx
CMP [ECX], EDX ;// сравнить *ECX и EDX
JAE yyy ;// если беззнаковый *ECX >= EDX перейти на yyy

Вызов функций на ассемблере реализуется намного сложнее, чем на Си. Во-первых, существует, по меньшей мере, два типа «соглашений» передачи функции параметров - Си и Паскаль. В Си-соглашении параметры передаются справа налево, а из стека их вычищает вызывающий функцию код. В Паскаль-соглашении все происходит наоборот! Аргументы передаются слева направо, а из стека их вычищает сама функция. Большинство API-функций Windows придерживаются комбинированного соглашения stdcall, при котором аргументы заносятся в соответствии с Си-соглашением, а из стека вычищаются по соглашению Паскаль.

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

Вызов API-функции
PUSH offset LibName ;// засылаем в стек смещение строки
CALL LoadLibrary ;// вызов функции
MOV h, EAX ;// EAX содержит возращенное значение

Ассемблерные вставки

Как же сложно программировать на чистом ассемблере! Минимально работающая программа содержит чертовую уйму разнообразных конструкций, непонятным образом взаимодействующих друг с другом и открывающих огонь без предупреждения. Одним махом мы отрезаем себя от привычного окружения. Сложить два числа на ассемблере не проблема, но вот вывести их результат на экран…
Ассемблерные вставки - другое дело. В то время как классические руководства по асму, буквально с первых же строк бросают читателя в пучину системного программирования, устрашая его сложностью архитектуры процессора и операционной системы, ассемблерные вставки оставляют читателя в привычном ему окружении языка Си (и/или Паскаля) и постепенно, безо всяких резких скачков, знакомят с внутренним миром процессора.
Они позволяют начать изучение ассемблера непосредственно с 32-разрядного защищенного режима процессора. Дело в том, что в чистом виде защищенный режим настолько сложен, что не может быть усвоен даже гением, а потому практически все руководства начинают изложение ассемблера с описания морально устаревшего 16-разрядного реального режима. Это не только оказывается бесполезным балластом, но и замечательным средством запутывания ученика (помнишь, "забудьте все, чему вас учили раньше…").
По своему личному опыту и опыту моих друзей могу сказать, что такая методика обучения превосходят все остальные как минимум по двум категориям:

а) Скорость - буквально через три-четыре дня интенсивных занятий человек, ранее никогда не знавший ассемблера, начинает сносно на нем программировать. б) Легкость освоения - изучение ассемблера происходит практически безо всякого напряжения и усилий. Ученика не заваливают ворохом неподъемной и непроходимой информации: каждый последующий шаг интуитивно понятен и с дороги познания заботливо убраны все потенциальные препятствия.
Ну, так чего же мы ждем? Пора программировать. Для объявления ассемблерных вставок в Microsoft Visual C++ служит ключевое слово _asm, а простейшая ассемблерная программа выглядит так:
Вставка, складывающая два числа
main()
{
int a = 1; // объявляем переменную a и кладем туда значение 1
int b = 2; // объявляем переменную b и кладем туда значение 2
int c; // объявляем переменную c, но не инициализируем ее
_asm{ // начало ассемблерной вставки
mov eax, a ;// загружаем значение переменной a в регистр EAX
mov ebx, b ;// загружаем значение переменной b в регистр EBX
add eax, ebx;// складываем EAX с EBX, записывая результат в EAX
mov c, eax ;// загружаем значение EAX в переменную c
} // конец ассемблерной вставки
// выводим содержимое c на экран
// с помощью привычной для нас функции printf
printf("a + b = %x + %x = %xn", a, b, c);
}

О планах на будущее

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

Изучай ассемблер.


Инструментарий
Программируя методом ассемблерных вставок, достаточно иметь компилятор с его IDE (например, Microsoft Visual Studio). Чрезвычайно удобно, что вставки отлаживаются точно так же, как и весь остальной высокоуровневый код.
Для программ, целиком написанных на ассемблере, понадобится транслятор. Под dos'ом большой популярностью пользовался пакет TASM от компании Borland, но в Windows его позиция выглядит неубедительной, и большинство программистов использует транслятор MASM от Microsoft, входящий в состав DDK (Device Driver Kit - набор инструментов разработчика драйверов). С ним конкурирует некоммерческий транслятор FASM (http://flatassembler.net/), заточенный под нужды системных программистов и поддерживающий более естественный синтаксис языка.
Существуют ассемблеры и под *nix, например, NASM, входящий в штатный комплект поставки большинства дистрибутивов. В общем, какой ассемблер выбрать - дело вкуса.
Прежде чем ассемблированная программа заработает ее необходимо скомпоновать. Для этого вполне подойдет стандартный линкер, выдернутый из той же Microsoft Visual Studio или Platform SDK. Из нестандартных можно порекомендовать ulink от Юрия Харона, поддерживающий большое количество форматов файлов и множество тонких настроек, которых другие линкеры крутить не дают. Его можно скачать с сайта фирмы Стикс: ftp://ftp.styx.cabel.net/pub/UniLink/ulnbXXXX.zip. Для некоммерческого использования он бесплатен.
Еще нам понадобиться отладчик и дизассемблер. Отладчик - это инструмент для поиска ошибок в своих собственных приложениях и взламывания чужих. Debugger’ов много разных: Microsoft Visual Debugger, интегрированный в состав Microsoft Visual Studio, Microsoft Windows Debugger (сокращенно WDB), и Kernel Debugger, входящие в состав SDK и DDK, SoftIce от NuMega, OllyDbg от Олега Яшкина и т.д. Самый мощный - SoftIce, самый расширяемый - WDB, самый простой и неприхотливый - OllyDbg. Дизассемблер же нормальный есть только один - это IDA Pro. Другие с ним и рядом не лежали.
Мелочь типа hex-редакторов, сравнивателей файлов, дамперов памяти, упаковщиков/распаковщиков так же должна быть все время под рукой. Скачать полный комплект необходимого инструментария можно, например, с сайта www.wasm.ru.

Если вдруг у тебя возникла необходимость в учебнике по асму, то могу порекомендовать:
Юров "Ассемблер - учебник",
Зубков "Ассемблер - язык неограниченных возможностей",
Ровдо "Микропроцессоры от 8086 до Pentium-III Xeon и AMD K6-3".

Assembler   Теги:

Читать IT-новости в Telegram
Информационные технологии
Мы в соцсетях ✉