Содержание статьи
- Самомодифицирующийся код в современных операционных системах
- Архитектура памяти Windows
- Использование функции WriteProcessMemory
- Выполнение кода в стеке
- «Подводные камни» перемещаемого кода
- Самомодифицирующийся код как средство защиты приложений
- Зашифрованный код — следующий уровень защиты приложений
- Заключение
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Продолжаем держать оборону нашего приложения от атак злобных хакеров — от их попыток «за просто так» воспользоваться плодами нашего труда, от их подозрительного интереса к нашим программам и скрываемым в них секретам. Для этого мы продолжим создавать изощренные системы защиты, на сей раз — от дизассемблирования.
Чтобы справиться с задачей, нам необходимо узнать о внутренних механизмах операционной системы, о средствах работы с памятью. Также придется разобраться в работе компиляторов, понять, как они генерируют код, вычислить плюсы и минусы оптимизации. И наконец, погрузиться в шифрование, научиться расшифровывать программный код на лету непосредственно перед выполнением.
Самомодифицирующийся код в современных операционных системах
В эпоху расцвета MS-DOS программисты широко использовали самомодифицирующийся код, без которого не обходилась практически ни одна мало‑мальски серьезная защита. Да и не только защита — он встречался в компиляторах, компилирующих код непосредственно в память, в распаковщиках исполняемых файлов, в полиморфных генераторах и так далее.
Когда началась массовая миграция пользователей на Windows, разработчикам пришлось задуматься о переносе накопленного опыта и приемов программирования на новую платформу. От бесконтрольного доступа к железу, памяти, компонентам операционной системы и связанных с ними хитроумных трюков программирования пришлось отвыкать. В частности, стала невозможна непосредственная модификация исполняемого кода приложений, поскольку Windows защищает его от непреднамеренных изменений. Это привело к рождению нелепого убеждения, будто под Windows создание самомодифицирующегося кода вообще невозможно, по крайней мере без использования недокументированных возможностей операционной системы.
На самом деле существует как минимум два документированных способа изменить код приложений, хорошо работающих под Windows NT и вполне удовлетворяющихся привилегиями гостевого пользователя.
Во‑первых, kernel32.
экспортирует функцию WriteProcessMemory
, предназначенную, как и следует из ее названия, для модификации памяти процесса. Во‑вторых, практически все операционные системы, включая Windows и Linux, разрешают выполнение и модификацию кода, размещенного в стеке. Между тем современные версии указанных операционных систем накладывают на стек ограничения, мы подробно поговорим об этом чуть позднее.
В принципе, задача создания самомодифицирующегося кода может быть решена исключительно средствами языков высокого уровня, таких, например, как C/C++ и Delphi, без применения ассемблера.
Архитектура памяти Windows
Создание самомодифицирующегося кода требует знания некоторых тонкостей архитектуры Windows, не очень‑то хорошо освещенных в документации. Точнее, совсем не освещенных, но от этого отнюдь не приобретающих статус «недокументированных особенностей», поскольку, во‑первых, они одинаково реализованы на всех Windows-платформах, а во‑вторых, их активно использует компилятор Visual C++ от Microsoft. Отсюда следует, что никаких изменений даже в отдаленном будущем компания не планирует; в противном случае код, сгенерированный этим компилятором, откажет в работе, а на это Microsoft не пойдет (вернее, не должна пойти, если верить здравому смыслу).
В режиме обратной совместимости для адресации четырех гигабайт виртуальной памяти, выделенной в распоряжение процесса, Windows использует два селектора, один из которых загружается в сегментный регистр CS
, а другой — в регистры DS
, ES
и SS
. Оба селектора ссылаются на один и тот же базовый адрес памяти, равный нулю, и имеют идентичные лимиты, равные четырем гигабайтам. Помимо перечисленных сегментных регистров, Windows еще использует регистр FS
, в который загружает селектор сегмента, содержащего информационный блок потока — TIB.
Фактически существует всего один сегмент, вмещающий в себя и код, и данные, и стек процесса. Благодаря этому управление коду, расположенному в стеке, передается близким (near) вызовом или переходом, и для доступа к содержимому стека использование префикса SS
совершенно необязательно. Несмотря на то что значение регистра CS
не равно значению регистров DS
, ES
и SS
, команды
• MOV
• MOV
• MOV
в действительности обращаются к одной и той же ячейке памяти.
Это точный прообраз реализованной в процессорах на архитектуре x86-64 RIP-относительной адресации памяти, в которой не используются сегменты.
Отличия между регионами кода, стека и данных заключаются в атрибутах принадлежащих им страниц: страницы кода допускают чтение и исполнение, страницы данных — чтение и запись, а стека — чтение, запись и исполнение одновременно.
Помимо этого, каждая страница имеет специальный флаг, определяющий уровень привилегий, которые необходимы для доступа к этой странице. Некоторые страницы, например те, что принадлежат операционной системе, требуют наличия прав супервизора, которыми обладает только код нулевого кольца. Прикладные программы, исполняющиеся в кольце 3, таких прав не имеют и при попытке обращения к защищенной странице порождают исключение. Манипулировать атрибутами страниц, равно как и ассоциировать страницы с линейными адресами, может только операционная система или код, исполняющийся в нулевом кольце.
Среди начинающих программистов ходит совершенно нелепая байка о том, что, если обратиться к коду программы командой, предваренной префиксом DS
, Windows якобы беспрепятственно позволит его изменить. На самом деле это в корне неверно — обратиться‑то она позволит, а вот изменить — нет, каким бы способом ни происходило обращение, так как защита работает на уровне физических страниц, а не логических адресов.
Использование функции WriteProcessMemory
Если требуется изменить некоторое количество байтов своего (или чужого) процесса, самый простой способ сделать это — вызвать функцию WriteProcessMemory
. Она позволяет модифицировать существующие страницы памяти, чей флаг супервизора не взведен, то есть все страницы, доступные из кольца 3, в котором выполняются прикладные приложения. Совершенно бесполезно с помощью WriteProcessMemory
пытаться изменить критические структуры данных операционной системы (например, page
или page
) — они доступны лишь из нулевого кольца. Поэтому указанная функция не представляет никакой угрозы для безопасности системы и успешно вызывается независимо от уровня привилегий пользователя.
Процесс, в память которого происходит запись, должен быть предварительно открыт функцией OpenProcess
с атрибутами доступа PROCESS_VM_OPERATION
и PROCESS_VM_WRITE
. Часто программисты, ленивые от природы, идут более коротким путем, устанавливая все атрибуты — PROCESS_ALL_ACCESS
. И это вполне законно, хотя справедливо считается дурным стилем программирования.
Далее приведен простой пример self-modifying_code
, иллюстрирующий использование функции WriteProcessMemory
для создания самомодифицирующегося кода:
#include <iostream>#include <Windows.h>using namespace std;int WriteMe(void* addr, int wb){ HANDLE h = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, true, GetCurrentProcessId()); return WriteProcessMemory(h, addr, &wb, 1, NULL);}int main(int argc, char* argv[]){ _asm { push 0x74 ; JMP -> JZ push offset Here call WriteMe add esp, 8 Here: JMP short here } cout << "#JMP SHORT $-2 was changed to JZ $-2\n"; return 0;}
Функция WriteProcessMemory
в рассматриваемой программе заменяет инструкцию бесконечного цикла JMP
условным переходом JZ
, который продолжает нормальное выполнение программы. Неплохой способ затруднить взломщику изучение приложения, не правда ли? Особенно если вызов WriteMe
не расположен возле изменяемого кода, а помещен в отдельный поток. Будет еще лучше, если модифицируемый код вполне естественен сам по себе и внешне не вызывает никаких подозрений. В этом случае хакер может долго блуждать в той ветке кода, которая при выполнении программы вообще не получает управления.
Для компиляции этого примера установи 32-битный режим результирующего кода.
Если из ассемблерной вставки убрать вызов функции WriteMe
, которая перезаписывает инструкцию JMP
на JZ
, программа выпадет в бесконечный цикл.
Об устройстве Windows: исторический нюанс
Поскольку Windows для экономии оперативной памяти разделяет код между процессами, возникает вопрос: а что произойдет, если запустить вторую копию самомодифицирующейся программы? Создаст ли операционная система новые страницы или отошлет приложение к уже модифицируемому коду? В документации на Windows NT сказано, что она поддерживает копирование при записи (copy on write), то есть автоматически дублирует страницы кода при попытке их модифицировать. Напротив, Windows 9x не поддерживает такую возможность. Означает ли это, что все копии самомодифицирующегося приложения будут вынуждены работать с одними и теми же страницами кода (а это неизбежно приведет к конфликтам и сбоям)?
Нет, и вот почему: несмотря на то что копирование при записи в Windows 9x не реализовано, эту заботу берет на себя сама функция WriteProcessMemory
, создавая копии всех модифицируемых страниц, распределенных между процессами. Благодаря этому самомодифицирующийся код одинаково хорошо работает как под Windows 9x, так и под Windows NT. Однако следует учитывать, что все копии приложения, модифицируемые любым иным путем (например, командой mov
нулевого кольца), если их запустить под Windows 9x, будут разделять одни и те же страницы кода со всеми вытекающими отсюда последствиями.
Теперь об ограничениях. Во‑первых, использовать WriteProcessMemory
разумно только в компилирующих в память компиляторах или распаковщиках исполняемых файлов, а в защитах — несколько наивно. Мало‑мальски опытный взломщик быстро обнаружит подвох, увидев эту функцию в таблице импорта. Затем он установит точку останова на вызов WriteProcessMemory
и будет контролировать каждую операцию записи в память. А это никак не входит в планы разработчика защиты!
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»