С появлением языков программирования высокого уровня, таких как C/C++, Delphi все меньше и меньше программистов используют ассемблер для создания программных продуктов. Постепенно программирование на ассемблере уходит в небытие. Сейчас ассемблер используют только высоко - квалифицированные специалисты.
Итак, какие же преимущества дает знание ассемблера:
- Глубокое понимание работы компьютера и операционной системы.
- Максимальная гибкость при работе с аппаратными ресурсами.
- Оптимизация программ по скорости выполнения.
- Оптимизация программ по размеру кода.
- Дизассемблирование и отладка.
Только знание ассемблер позволяет лучше понять принципы работы компьютера и операционной системы.
Итак, что же такое PE-файлы и Х-код.
Portable Executable - это формат исполняемых файлов, объектного кода и динамических библиотек, используемый в 32- и 64-битных версиях операционной системы Microsoft Windows. Формат PE представляет собой структуру данных, содержащую всю информацию, необходимую PE загрузчику для проецирования файла в память. Исполняемый код включает в себя ссылки для связывания динамически загружаемых библиотек, таблицы экспорта и импорта API функций, данные для управления ресурсами и данные локальной памяти потока (TLS). В операционных системах семейства Windows NT формат PE используется для EXE, DLL, SYS (драйверов устройств), и других типов файлов.
Х-код – это программный код, написанный на ассемблере, который внедряется (программой, человеком) в исполняемый PE-файл для изменения поведения программы.
Перед Х-кодом стоят, по меньшей мере, три основные задачи: а) разместить свое тело внутри подопытного файла; б) перехватить управление до начала выполнения основной программы; в) определения адресов системных API-функций, жизненно важных для собственного функционирования.
Перехват управления осуществляется следующими путями: переустановкой точки входа на тело Х-кода;
внедрение в окрестности оригинальное точки входа команды перехода на Х-код;
модификации одного или нескольких элементов таблицы импорта с целью подмены вызываемых функций своими собственными.
Определение адресов API-функций обычно осуществляется следующими путями:
поиска необходимых функций в таблице импорта файла-хозяина; поиском LoadLibrary/GetProcAddress в таблице импорта файла-хозяина с последующим импортированием всех необходимых функций вручную; прямым вызовом API-функций по их абсолютным адресам, жестко прописанным внутри Х-кода; добавление в таблицу импорта необходимых функций (LoadLibrary/GetProcAddress);
Существуют следующие типы (методы, способы) внедрения:
- Размещение Х-кода поверх оригинальной программы (затирание).
- Размещение Х-кода в свободном месте программы (интеграция).
- Дописывание Х-кода в начало, середину или конец файла с сохранением оригинального содержимого.
Х-код проектируется с учетом всей жесткости требований, предъявляемых неизвестной средой чужеродного кода, в который он будет внедрен.
Во-первых, Х-код должен быть полностью перемещаем, то есть сохранять свою работоспособность независимо от базового адреса загрузки. Это достигается путем использования относительной адресации: определив свое текущее расположение вызовом команды CALL $+5/POP EBP, Х-код сможет преобразовать смещение внутри своего тела в эффективные адреса простым сложением их с EBP.
Во-вторых, грамотно сконструированный Х-код никогда не модифицирует свои ячейки, поскольку не знает, имеется ли у него права на запись или нет. Стандартная секция кода лишена атрибута записи.
В-третьих, Х-код должен быть предельно компактным, поскольку объем пространства, пригодного для внедрения ограничен.
Следующая разновидность Х-кода это Shell-код обычно используется хакерами для взлома удаленных компьютеров через программные ошибки (переполнение буфера). Shell-код пишется длякаждой комбинации аппаратной платформы и операционной системы. К Shell-коду предъявляется еще более жесткие требования.
Во-первых, проблема нулевого байта, Shell-код часто внедряется в программу с помощью таких функций, как read(), sprintf() и strcpy(). Большинство функций работы со строками ожидают, что строка завершается нулевым байтом. Если в shell-коде встречается нулевой байт, то он интерпретируется как конец строки, следовательно, все последующие байты будут проигнорированы при копировании.
Во-вторых, проблема адресации, обычные программы обращаются к переменным и функциям с помощью указателя, который вычисляется компилятором или возвращается функцией, например, malloc (эта функция выделяет область памяти и возвращает указатель на нее). В shell-коде тоже часто возникает необходимость сослаться на строку или какую-нибудь другую переменную. Например, если shell-код пытается запустить некоторую программу с помощью системного вызова WinExec, то понадобится указатель на строку, содержащую имя этой программы. Поскольку shell-код внедряется в программу во время выполнения, то необходимо статически определить используемые в нем адреса памяти.
В-третьих, переносимость, Ассемблерный код зависит от машинной архитектуры, поэтому переносить shell-код на другую платформу трудно. Сложно не только перенести shell-код на другой процессор, но даже на другую операционную систему, работающую на том же процессоре, поскольку в ассемблерном коде часто бывают «зашиты» номера системных вызовов.
Внедрение shell-кода в удаленную программу. Один и тот же shell- код можно использовать в локальных и удаленных эксплойтах. Разница в том, что shell-код, внедренный в удаленную программу, может выполнять привязку к порту и запуск удаленного интерпретатора команд. Один из самых распространенных shell-кодов просто привязывает интерпретатор команд к порту с большим номером. В результате сервер на взломанном хосте будет запускать оболочку в ответ на запрос о соединении.
Внедрение shell-кода в локальную программу. Shell-код, внедренный в локальную программу, не выполняет никаких сетевых операций, в остальном он может ничем не отличаться от shell-кода, предназначенного для внедрения в удаленную программу.
Язык ассемблера - это ключ к созданию эффективного shell-кода. При компиляции программы, написанной на C, генерируется код, содержащий много «мусора», который в shell-коде ни к чему. Если же программа написана на ассемблере, то она транслируется именно в те машинные команды, которые необходимы. Данные, помещаемые в стек, могут оказаться длиннее выделенной для них области и в результате затереть значение в регистре EIP, что приведет к изменению пути выполнения программы. Если удастся записать в EIP адрес полезной нагрузки, посланной хосту, то можно выполнить команды (shell-код). Уязвимости, вызванные переполнением буфера, на практике встречаютсячаще всего. Хотя сейчас они уже не так распространены, все же эта беда еще не изжита полностью. Разобравшись в том, что такое переполнение стека и как используют подобные уязвимости, можно приступать к ознакомлению с опубликованными отчетами об уязвимостях. Цель любого эксплойта для Windows - перехватить управление регистром EIP (счетчиком команд) и записать в него адрес отправленного shell-кода, который выполнит ту или иную программу в системе.
С учетом всего сказанного уровень подготовки программиста должен быть очень высоким для использования данной техники. Во всем есть свои плюсы, и минусы, одни используют эти знания для создания антивирусных программ, другие для написания вирусов и Shell-кода.
Литература
- Касперский К., Компьютерные вирусы изнутри и снаружи, СПб: Питер, 2006,С. 257.
- Фостер Д., Техника взлома - сокеты, эксплойты, shell-код: Пер. с англ. Слинкина А. А. - М.: ДМК-пресс, 2006, С. 784.
- Эриксон Д., Хакинг. Искусство эксплойта, - Пер. с англ. СПб: Символ- Плюс, 2005, С.240.