Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Каждый разработчик программ стремится защитить результат своего труда от взлома. Сегодня нам предстоит исследовать способы противостояния самому популярному виду хакерского инструментария — дизассемблерам. Нашей целью будет запутать хакера, отвадить его от взлома нашей программы, всевозможными способами затруднить ее анализ.
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Продолжаем держать оборону нашего приложения от атак злобных хакеров — от их попыток «за просто так» воспользоваться плодами нашего труда, от их подозрительного интереса к нашим программам и скрываемым в них секретам. Для этого мы продолжим создавать изощренные системы защиты, на сей раз — от дизассемблирования.
Чтобы справиться с задачей, нам необходимо узнать о внутренних механизмах операционной системы, о средствах работы с памятью. Также придется разобраться в работе компиляторов, понять, как они генерируют код, вычислить плюсы и минусы оптимизации. И наконец, погрузиться в шифрование, научиться расшифровывать программный код на лету непосредственно перед выполнением.
В эпоху расцвета MS-DOS программисты широко использовали самомодифицирующийся код, без которого не обходилась практически ни одна мало‑мальски серьезная защита. Да и не только защита — он встречался в компиляторах, компилирующих код непосредственно в память, в распаковщиках исполняемых файлов, в полиморфных генераторах и так далее.
Когда началась массовая миграция пользователей на Windows, разработчикам пришлось задуматься о переносе накопленного опыта и приемов программирования на новую платформу. От бесконтрольного доступа к железу, памяти, компонентам операционной системы и связанных с ними хитроумных трюков программирования пришлось отвыкать. В частности, стала невозможна непосредственная модификация исполняемого кода приложений, поскольку Windows защищает его от непреднамеренных изменений. Это привело к рождению нелепого убеждения, будто под Windows создание самомодифицирующегося кода вообще невозможно, по крайней мере без использования недокументированных возможностей операционной системы.
На самом деле существует как минимум два документированных способа изменить код приложений, хорошо работающих под Windows NT и вполне удовлетворяющихся привилегиями гостевого пользователя.
Во‑первых, kernel32.dll
экспортирует функцию WriteProcessMemory
, предназначенную, как и следует из ее названия, для модификации памяти процесса. Во‑вторых, практически все операционные системы, включая Windows и Linux, разрешают выполнение и модификацию кода, размещенного в стеке. Между тем современные версии указанных операционных систем накладывают на стек ограничения, мы подробно поговорим об этом чуть позднее.
В принципе, задача создания самомодифицирующегося кода может быть решена исключительно средствами языков высокого уровня, таких, например, как C/C++ и Delphi, без применения ассемблера.
Создание самомодифицирующегося кода требует знания некоторых тонкостей архитектуры Windows, не очень‑то хорошо освещенных в документации. Точнее, совсем не освещенных, но от этого отнюдь не приобретающих статус «недокументированных особенностей», поскольку, во‑первых, они одинаково реализованы на всех Windows-платформах, а во‑вторых, их активно использует компилятор Visual C++ от Microsoft. Отсюда следует, что никаких изменений даже в отдаленном будущем компания не планирует; в противном случае код, сгенерированный этим компилятором, откажет в работе, а на это Microsoft не пойдет (вернее, не должна пойти, если верить здравому смыслу).
В режиме обратной совместимости для адресации четырех гигабайт виртуальной памяти, выделенной в распоряжение процесса, Windows использует два селектора, один из которых загружается в сегментный регистр CS
, а другой — в регистры DS
, ES
и SS
. Оба селектора ссылаются на один и тот же базовый адрес памяти, равный нулю, и имеют идентичные лимиты, равные четырем гигабайтам. Помимо перечисленных сегментных регистров, Windows еще использует регистр FS
, в который загружает селектор сегмента, содержащего информационный блок потока — TIB.
Фактически существует всего один сегмент, вмещающий в себя и код, и данные, и стек процесса. Благодаря этому управление коду, расположенному в стеке, передается близким (near) вызовом или переходом, и для доступа к содержимому стека использование префикса SS
совершенно необязательно. Несмотря на то что значение регистра CS
не равно значению регистров DS
, ES
и SS
, команды
• MOV dest,CS:[src]
• MOV dest,DS:[src]
• MOV dest,SS:[src]
в действительности обращаются к одной и той же ячейке памяти.
Это точный прообраз реализованной в процессорах на архитектуре x86-64 RIP-относительной адресации памяти, в которой не используются сегменты.
Отличия между регионами кода, стека и данных заключаются в атрибутах принадлежащих им страниц: страницы кода допускают чтение и исполнение, страницы данных — чтение и запись, а стека — чтение, запись и исполнение одновременно.
Помимо этого, каждая страница имеет специальный флаг, определяющий уровень привилегий, которые необходимы для доступа к этой странице. Некоторые страницы, например те, что принадлежат операционной системе, требуют наличия прав супервизора, которыми обладает только код нулевого кольца. Прикладные программы, исполняющиеся в кольце 3, таких прав не имеют и при попытке обращения к защищенной странице порождают исключение. Манипулировать атрибутами страниц, равно как и ассоциировать страницы с линейными адресами, может только операционная система или код, исполняющийся в нулевом кольце.
Среди начинающих программистов ходит совершенно нелепая байка о том, что, если обратиться к коду программы командой, предваренной префиксом DS
, Windows якобы беспрепятственно позволит его изменить. На самом деле это в корне неверно — обратиться‑то она позволит, а вот изменить — нет, каким бы способом ни происходило обращение, так как защита работает на уровне физических страниц, а не логических адресов.
Если требуется изменить некоторое количество байтов своего (или чужого) процесса, самый простой способ сделать это — вызвать функцию WriteProcessMemory
. Она позволяет модифицировать существующие страницы памяти, чей флаг супервизора не взведен, то есть все страницы, доступные из кольца 3, в котором выполняются прикладные приложения. Совершенно бесполезно с помощью WriteProcessMemory
пытаться изменить критические структуры данных операционной системы (например, page directory
или page table
) — они доступны лишь из нулевого кольца. Поэтому указанная функция не представляет никакой угрозы для безопасности системы и успешно вызывается независимо от уровня привилегий пользователя.
Процесс, в память которого происходит запись, должен быть предварительно открыт функцией 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 $-2n"; return 0;}
Функция WriteProcessMemory
в рассматриваемой программе заменяет инструкцию бесконечного цикла JMP short $-2
условным переходом JZ $-2
, который продолжает нормальное выполнение программы. Неплохой способ затруднить взломщику изучение приложения, не правда ли? Особенно если вызов WriteMe
не расположен возле изменяемого кода, а помещен в отдельный поток. Будет еще лучше, если модифицируемый код вполне естественен сам по себе и внешне не вызывает никаких подозрений. В этом случае хакер может долго блуждать в той ветке кода, которая при выполнении программы вообще не получает управления.
Для компиляции этого примера установи 32-битный режим результирующего кода.
Результат выполнения приложения self-modifying_code
Если из ассемблерной вставки убрать вызов функции WriteMe
, которая перезаписывает инструкцию JMP
на JZ
, программа выпадет в бесконечный цикл.
Вызов функции закомментирован
Программа зациклена
Об устройстве Windows: исторический нюанс
Поскольку Windows для экономии оперативной памяти разделяет код между процессами, возникает вопрос: а что произойдет, если запустить вторую копию самомодифицирующейся программы? Создаст ли операционная система новые страницы или отошлет приложение к уже модифицируемому коду? В документации на Windows NT сказано, что она поддерживает копирование при записи (copy on write), то есть автоматически дублирует страницы кода при попытке их модифицировать. Напротив, Windows 9x не поддерживает такую возможность. Означает ли это, что все копии самомодифицирующегося приложения будут вынуждены работать с одними и теми же страницами кода (а это неизбежно приведет к конфликтам и сбоям)?
Нет, и вот почему: несмотря на то что копирование при записи в Windows 9x не реализовано, эту заботу берет на себя сама функция WriteProcessMemory
, создавая копии всех модифицируемых страниц, распределенных между процессами. Благодаря этому самомодифицирующийся код одинаково хорошо работает как под Windows 9x, так и под Windows NT. Однако следует учитывать, что все копии приложения, модифицируемые любым иным путем (например, командой mov
нулевого кольца), если их запустить под Windows 9x, будут разделять одни и те же страницы кода со всеми вытекающими отсюда последствиями.
Теперь об ограничениях. Во‑первых, использовать WriteProcessMemory
разумно только в компилирующих в память компиляторах или распаковщиках исполняемых файлов, а в защитах — несколько наивно. Мало‑мальски опытный взломщик быстро обнаружит подвох, увидев эту функцию в таблице импорта. Затем он установит точку останова на вызов WriteProcessMemory
и будет контролировать каждую операцию записи в память. А это никак не входит в планы разработчика защиты!
Источник: xakep.ru