В программировании принято разделять шифрование на статическое и динамическое. Первый вариант подразумевает крипт всей тушки дискового образа программы, и обратный декрипт его при загрузки в память. Он прост в практической реализации, но как и следует из алгоритма, защищает лишь двоичный образ софта на жёстком диске, а в памяти ОЗУ код уже сбрасывает с себя всю маскировку и становится полностью доступным исследователю. Остаётся тупо снять дамп памяти, и в оффлайне не торопясь разбирать защиту.
Чтобы хоть как-то этому противостоять, программисты придумали "динамическое шифрование" (Dynamic Encryption), когда прожка криптуется не вся целиком, а отдельными блоками, в качестве которых могут выступать, например, процедуры. Теперь, после загрузки образа в память, расшифровывается только исполняемый на текущий момент блок кода, а остальная часть остаётся лежать как и прежде в зашифрованном виде. Когда исполнение доходит до следующего блока он разворачивается, а отработанный опять сворачивается. Таким образом, снятие дампа со-всей программы становится бесполезным, и взломщику приходится дампить каждую процедуру в отдельности. В данной статье рассматривается возможный вариант подобного метода шифрования в динамике.
Оглавление:
1. Плюсы и минусы идеи;
2. Реализация в примитивах;
3. SEH – обработчик исключений;
4. Модуль расшифровки и шифрования процедур;
5. Сбор информации о шифровании;
6. Практика – пример программы;
7. Заключение.
-----------------------------------------------------------
1. Плюсы и минусы динамического шифрования
Когда коду есть-что скрывать от общественности, самый простой вариант – зашифровать его к чертям. В природе встречаются горячие головы, которые применяют даже многослойный крипт, по типу матрёшки. Это конечно-же перебор, но при грамотном проектировании тактика безотказная, ведь в конечном счёте подразделения вражески настроенной публики столкнутся (пусть и с небольшим) препятствием, и мы добьёмся своей цели. Только вот умиротворённый пейзаж портит софт, позволяющий дампить процессы, сводя на нет все наши старания. Тут-то и всплывает буйком динамическое шифрование кода/данных, использованию которого на практике препятствуют в основном следующие две проблемы:
Ну а в остальном, динамическое шифрование превосходит статическое по всем фронтам, а что самое главное – это отличный объект для практики. Разрабатывая его алгоритм можно забрести в такие дебри, куда не каждый взломщик решится залезть. К примеру, ничто не мешает шифровать разные блоки разными ключами, связывать эти блоки (аля процедуры) между собой, как это делает тот-же AES в режиме цепочки CBC (Cipher-Block-Chaining), и т.д.п. В общем направлений для самовыражения здесь предостаточно.
2. Реализация в примитивах
Теперь поговорим о реалиях..
Пусть наша программа имеет с десяток-другой самостоятельных процедур, живописно разбросанных по всему коду. Задача состоит в том, чтобы зашифровать эти процедуры и при их вызове перехватывать управление, для расшифровки и последующего исполнения. Соответственно нужна какая-то служебная функция для этих целей, которую мы оформим и поместим в специальную "промзону" внутри программы чуть позже. Функция будет отслеживать запуск всех рабочих процедур: расшифровывать их на старте, и обратно шифровать на выходе. Здесь притаившись в окопах нас поджидают несколько проблем:
На рисунке ниже представлен алгоритм всей программы из части(6) данной статьи. Это секция-кода, которая начинается с выделенных синим трёх рабочих процедур А,В,С, хотя их может быть сколько угодно. Сразу за процедурами следует обёрнутая в модуль SEH служебная функция, и далее начинается непосредственно сам код с точкой-входа в программу
В самом хвосте адресного пространства валяется (в специально выделенной для этих целей секции) временный "криптор". Его задача – собрать информацию о процедурах типа: начало\размер\ключ блока шифрования, и вывести все эти данные на консоль. Это избавит нас от выносящего мозг ручного вычисления паспортов процедур. Модуль обязательно должен находиться в самом конце программы, чтобы после его удаления не съехали со-своих позиций все адреса. После того-как он выполнит свою задачу, нужно будет в HEX-редакторе вписать вместо него
В данном случае из кода вызывается процедура(А) и поскольку модулю SEH система передаёт управление только в случае возникновения исключений "Exception", то вызов процедуры нужно будет предварить какой-нибудь исключительной ситуацией типа: деление на нуль, чтение с ядерного или иного недоступного прикладной задаче адреса, или-же специально предназначенной для этих целей инструкцией генерирования ошибок
Создаём переменную со-значением нуль и делаем вид, что она предназначена для какого-нибудь указателя. На самом деле это утка и мы вообще не планируем ложить туда линк, а просто считываем этот нуль в любой регистр и используя его как адрес, пытаемся прочитать с него. В результате получаем нарушение прав доступа "Access Violation" с кодом исключения
На момент, когда SEH получит управление, диспетчер исключений мастдая положит в стек адрес глючной инструкции, и если прибавить к нему длину этой инструкции (в случае с UD2 и чтения с нулевого адреса это 2 байта), то в аккурат получим смещение к запрашиваемой процедуре, которая находится ниже аварийной:
Обратите внимание, что вызывать процедуры из кода не нужно, т.е. можно вообще убрать инструкцию
3. SEH – обработчик исключений
Системному механизму SEH уже была посвящена отдельная статья, где рассматривались все его детали. Поэтому повторяться не будем, а вспомним лишь основные моменты, которые пригодятся в динамическом шифровании кода.
Эта мощная фишка позволяет устанавливать (поверх системного) пользовательские обработчики исключений. На языках высокого уровня ЯВУ она имеет эквивалент в виде операторов "try-except". Создать цепочку обработчиков и вклиниться в неё можно всего тремя инструкциями ассемблера так:
С этого момента, все возникшие в системе исключения будут перенаправляться диспетчером на нашу функцию "mySeh", а что мы будем делать внутри неё – это уже зависит от фантазии и настроения. Она представляет собой документированную Callback-функцию обратного вызова с таким прототипом (имя может быть произвольным):
Когда после исключения программа сколлапсирует в чёрную дыру и наш обработчик получит управление, эти 4 указателя окажутся на вершине стека. Суть в том, что в этот момент диспетчер сохраняет значения всех регистров процессора в специальную структуру под названием "Context", и предоставляет нам указатель на неё в третьем аргументе "pContext". Из всей своры этих регистров, нам нужен только регистр-указатель на инструкцию
Чтобы не подтягивать её инклудом в программу, в своём примере я явно указываю смещение на EIP=184, и взяв его значение прибавляю к нему 2:
Значит, чтобы получить адрес процедуры, мы сначала считываем значение EIP на момент исключения, а в конце перезаписываем EIP новым значением, чтобы процессор прыгнул по адресу возврата на нужный нам участок кода, т.е. после процедуры.
Немаловажными являются тут и две последние инструкции. Поскольку всё-что происходит внутри нашего обработчика SEH находится под чутким надзором системного диспетчера исключений, по окончании в регистре EAX он ожидает получить от нас или нуль (что означает "в Багдаде всё спокойно" и мы обработали исключение), или-же единицу, которая говорит об обратном. Здесь я передаю нуль, и диспетчер восстанавливает значения всех регистров из сохранённого контекста, куда мы предварительно подсунули уже новый EIP. Это в очередной раз доказывает, что процессоры х86 не обладают собственным интеллектом и делают лишь то, на что мы им укажем явно.
4. Модуль расшифровки/шифрования процедур
Выше упоминалось, что динамическое шифрование требует от нас депозита мозгов, а если учитывать автоматизацию и скрытность, это на порядок усугубляет и без того напряжённую обстановку. Здесь шаг-влево, шаг-вправо влечёт за собой расстрел, и приложение сразу-же падает навзничь без признаков жизни. Чтобы не получить пепел вместо фейверка, нужно быть предельно осторожным – особенно это касается границ шифрования блоков, так-что имеет смысл заранее оставлять в хвосте резерв как-минимум пару байт. В масштабах всего блока они не сыграют особой роли, зато избавят нас от трудно-вылавливаемых ошибок. Рассмотрим это на фрагменте моего демонстрационного примера.
Значит есть у меня три процедуры: "Mess" (тупо выводит сообщение), "Pass" (запрашивает пароль), и "Info" (выводит некоторую информацию). Теперь я должен предварительно их зашифровать, а когда придёт время, основная функция SEH должна будет: (1)расшифровать их, (2)передать на них управление, и после отработки (3)опять зашифровать. Все процедуры расположены в памяти плечом-к-плечу, поэтому чтобы вычислить размеры каждой из них, достаточно от адреса-начала следующей, отнимать адрес-начала предыдущей, т.е. получать разницу в байтах. В моём случае последовательность такая: Mess-->Pass-->Info-->mySeh.
Следующий момент – это ключи шифрования, и адрес возврата. Поскольку это просто пример, я храню их в открытом виде, хотя и выделил под них отдельную программную секцию, чтобы они не светились в дизассемблере. Паспорт каждой из процедур храню в своих массивах, которые выбираю табличным методом – вот фрагмент:
В первом поле каждого из массивов лежат адреса процедур, а дальше.. для вычисления размера первой процедуры "Mess", я беру её разницу в байтах со-следующей "Pass", и оставив в резерве байт(-1) делю на 4. Это потому, что в третьем поле ключ шифрования у меня 4-байтный, и планирую шифровать процедуры блоками такого-же размера. (для справки: 1-байтный ключ можно подобрать за 256 итераций, 2-байтный уже за 65536, а 4-байтный более чем за 4 млрд).
Когда внутри SEH из контекста регистров мы получим "адрес метки" вызываемой процедуры в лице значения EIP+2, нужно будет пройтись по таблице "procTable" где все они собраны, и получить указатель на соответствующий массив "keyMess" или другой (см.второй dword в procTable). Для расшифровки и последующего шифрования любой из процедур больше ничего и не требуется.
Рассмотрим, что здесь происходит..
Значит в коде мы генерим исключение, после которого стоит одна из трёх (привязанных к процедурам) меток. Обработчик SEH отлавливает исключение и получает контекст всех регистров, среди которых есть и указатель на глючную инструкцию EIP. Прибавив к нему двойку
Таким образом, без каких-либо аргументов, функция SEH сама на автомате вычисляет все необходимые данные, и без знания алгоритма пионерам-взломщикам трудно будет разобраться, что здесь к чему, тем более-что на Win7 и выше отладчики не заходят в пользовательские обработчики исключений (как это было на ХР), пока не поставишь бряк по F2. Такие дела..
5. Сбор информации о шифровании
Ну и на финишной-прямой рассмотрим метод начального шифрования процедур.
Поскольку получив управление SEH расшифровывает процедуры, значит нам нужно предварительно зашифровать их. Радует то, что в массивах "keyMess" и прочих все данные уже для этого есть, и остаётся просто организовать крипт. Для этого в самом хвосте программы я выделил специальную секцию (без имени, чтобы она не подталкивала на мысль), и собрал в ней 3 блока по такой схеме:
Теперь компилируем программу и открываем её дисковый образ в удобном HEX-редакторе – я по старинке предпочитаю HIEW, версия 8.66 которого поддерживает и 64-битный код. Значит загружаем в него образ и комбинацией F8-->F6 выбираем секцию кода. Далее манипулируя стрелками находим нужный адрес, который мы получили на предыдущем этапе (в данном случае 0x00403000, начало секции), и задаём ключ шифрования последовательностью F3-->F8 (Edit, XOR). По-умолчанию Hiew ксорит байтами и в этом окне нет возможности изменить длину ключа. Но поскольку ключ у нас 4-байтный, то придётся записывать его задом-наперёд, как показано на скрине ниже:
После того-как определились с ключом, подтверждаем свои намерения клавишей Enter и всё той-же F8 приступаем к шифрованию первой процедуры. Как видно из логов, размер её 20 двойных слов DWORD, так-что жмём F8 ровно 20-раз (цвет зашифрованных байт должен меняться на жёлтый). По окончании сохраняем изменения по F9 и выходим из редактора по F10. Выйти надо для того, чтобы сменить значение ключа для шифрования следующей процедуры. Проделываем аналогичные операции с остальными двумя блоками кода.
Посмотрите на предыдущий фрагмент.. В самом его хвосте имеются строки
Они вставлены для того, что получить опкоды перехода на EntryPoint в программу. Если посмотреть в отладчике, этим инструкциям (в данном случае) будет соответствовать последовательность байт
6. Практика – пример программы
Соберём теперь всё вышеизложенное под один капот.
Обратите внимание, что в шапке программы директива
Кстати если вскормить этот код аверам, то из 13-ти всего двое посчитали его малварью, хотя на самом деле в нём нет ничего особого, а только крипт. Так-что слепо доверять антивирусам тоже не есть гуд, хотя у каждого своё право.
7. Заключение
Здесь был рассмотрен классический вариант динамического шифрования, ..как говорят фундаментальные основы. В реальных-же программах имеет смысл воспользоваться более продвинутыми методами шифрования, с использованием специальных API-функций из библиотек Advapi32.dll, Bcrypt.dll и прочих. Потренировавшись "на кошках" и освоив эту технику можно смело двигаться дальше и придумать алгоритм, при котором шифруемые процедуры связывались-бы между собой в цепочку так, что одна без другой просто не функционировала-бы. Хорошие результаты даёт и периодический пересчёт контрольной суммы отдельных процедур и всего кода в целом. Это позволит предотвратить всякого рода мод двоичного кода, если вдруг кто-то захочет изменить
В скрепку кладу исполняемый файл данного исходника – попробуйте погонять его в отладчике и найти пароль. Для тех-кто захочет попрактиковаться в шифровании и собрать исходник своими руками, в скрепке имеется и HEX-редактор "Hiew 8.66", способный редактировать как 32, так и 64-бит приложения. Всем удачи, пока!
Чтобы хоть как-то этому противостоять, программисты придумали "динамическое шифрование" (Dynamic Encryption), когда прожка криптуется не вся целиком, а отдельными блоками, в качестве которых могут выступать, например, процедуры. Теперь, после загрузки образа в память, расшифровывается только исполняемый на текущий момент блок кода, а остальная часть остаётся лежать как и прежде в зашифрованном виде. Когда исполнение доходит до следующего блока он разворачивается, а отработанный опять сворачивается. Таким образом, снятие дампа со-всей программы становится бесполезным, и взломщику приходится дампить каждую процедуру в отдельности. В данной статье рассматривается возможный вариант подобного метода шифрования в динамике.
Оглавление:
1. Плюсы и минусы идеи;
2. Реализация в примитивах;
3. SEH – обработчик исключений;
4. Модуль расшифровки и шифрования процедур;
5. Сбор информации о шифровании;
6. Практика – пример программы;
7. Заключение.
-----------------------------------------------------------
1. Плюсы и минусы динамического шифрования
Когда коду есть-что скрывать от общественности, самый простой вариант – зашифровать его к чертям. В природе встречаются горячие головы, которые применяют даже многослойный крипт, по типу матрёшки. Это конечно-же перебор, но при грамотном проектировании тактика безотказная, ведь в конечном счёте подразделения вражески настроенной публики столкнутся (пусть и с небольшим) препятствием, и мы добьёмся своей цели. Только вот умиротворённый пейзаж портит софт, позволяющий дампить процессы, сводя на нет все наши старания. Тут-то и всплывает буйком динамическое шифрование кода/данных, использованию которого на практике препятствуют в основном следующие две проблемы:
• Во-первых оптимизация и производительность, ..а точнее полное отсутствие таковых. В силу того, что вызов каждой из процедур нужно предварять расшифровкой их содержимого, так после отработки требуется время и на обратную шифровку, чтобы восстановить программу в первоначальный вид – иначе в динамическом крипте просто теряется смысл. Это основной недостаток, который неизбежно отнимает у нас фору по-времени.
• Во-вторых – некоторая сложность разработки и трудоёмкость интерактивной отладки, что подтвердит практическая часть. Ведь мало набросать в блокноте правильный план, так нужно ещё заставить его работать в боевых условиях. По сути отлаживать и статический крипт не просто, так-что в отличии первого пункта это ещё можно как-то пережить.
Ну а в остальном, динамическое шифрование превосходит статическое по всем фронтам, а что самое главное – это отличный объект для практики. Разрабатывая его алгоритм можно забрести в такие дебри, куда не каждый взломщик решится залезть. К примеру, ничто не мешает шифровать разные блоки разными ключами, связывать эти блоки (аля процедуры) между собой, как это делает тот-же AES в режиме цепочки CBC (Cipher-Block-Chaining), и т.д.п. В общем направлений для самовыражения здесь предостаточно.
2. Реализация в примитивах
Теперь поговорим о реалиях..
Пусть наша программа имеет с десяток-другой самостоятельных процедур, живописно разбросанных по всему коду. Задача состоит в том, чтобы зашифровать эти процедуры и при их вызове перехватывать управление, для расшифровки и последующего исполнения. Соответственно нужна какая-то служебная функция для этих целей, которую мы оформим и поместим в специальную "промзону" внутри программы чуть позже. Функция будет отслеживать запуск всех рабочих процедур: расшифровывать их на старте, и обратно шифровать на выходе. Здесь притаившись в окопах нас поджидают несколько проблем:
1. Служебная функция НЕ должна принимать никаких аргументов от основного кода, иначе взломщик сможет ухватиться за них и раскурить весь наш тайный план. Функцию нужно наделить как-минимум "байтом" собственного интеллекта, чтобы она на автомате вычисляла, к какой именно процедуре идёт обращение. Вариантов тут у нас всего два – это аппаратные "точки-останова" BreakPoint с использованием отладочных регистров DR0-DR7 (в этом случае мы сможем обслуживать не более четырёх процедур по кол-ву регистров DRx), или-же задействовать свой структурированный обработчик исключений SEH (Structured Exсeption Handler). Остальные варианты производные от них. Дабы не ограничивать себя в кол-ве рабочих процедур, я выбрал второй вариант SEH – ему без разницы, сколько клиентов тащить на себе. В следующей главе мы рассмотрим его детали.
2. Непосредственно крипт/декрипт исполняемых процедур. Это атомный реактор всей программы – именно на нём будет держаться вся конструкция, а потому нужно отнестись к данному модулю с особой внимательностью. Байки про то, как сложно организовать шифрование сильно преувеличены: тут главное чётко представлять себе конечную цель. Основное требование заключается лишь в определении начального адреса блока шифрования, и его длины. Можно было использовать в примере более серьёзный метод шифрования типа AES, позвав на помощь функцию BСryptEncrypt() из либы Bcrypt.dll, но для демонстрационного примера это громоздко, и я ограничился обычным XOR.
На рисунке ниже представлен алгоритм всей программы из части(6) данной статьи. Это секция-кода, которая начинается с выделенных синим трёх рабочих процедур А,В,С, хотя их может быть сколько угодно. Сразу за процедурами следует обёрнутая в модуль SEH служебная функция, и далее начинается непосредственно сам код с точкой-входа в программу
start:
. На модуль SEH возложена критически-важная часть работы – доступ к процедурам будет осуществляться только через него!В самом хвосте адресного пространства валяется (в специально выделенной для этих целей секции) временный "криптор". Его задача – собрать информацию о процедурах типа: начало\размер\ключ блока шифрования, и вывести все эти данные на консоль. Это избавит нас от выносящего мозг ручного вычисления паспортов процедур. Модуль обязательно должен находиться в самом конце программы, чтобы после его удаления не съехали со-своих позиций все адреса. После того-как он выполнит свою задачу, нужно будет в HEX-редакторе вписать вместо него
JMP
на точку-входа в программу EntryPoint.В данном случае из кода вызывается процедура(А) и поскольку модулю SEH система передаёт управление только в случае возникновения исключений "Exception", то вызов процедуры нужно будет предварить какой-нибудь исключительной ситуацией типа: деление на нуль, чтение с ядерного или иного недоступного прикладной задаче адреса, или-же специально предназначенной для этих целей инструкцией генерирования ошибок
UD2
(UndefineD Instruction opcode). Однако UD2
явно раскрывает все наши карты, т.к. в легальных программах она практически не встречается и служит чисто для отладочных целей, поэтому лучше поступить так..Создаём переменную со-значением нуль и делаем вид, что она предназначена для какого-нибудь указателя. На самом деле это утка и мы вообще не планируем ложить туда линк, а просто считываем этот нуль в любой регистр и используя его как адрес, пытаемся прочитать с него. В результате получаем нарушение прав доступа "Access Violation" с кодом исключения
0xC0000005
. Это хоть какая-то маскировка, чем явный UD2
или деление на нуль.На момент, когда SEH получит управление, диспетчер исключений мастдая положит в стек адрес глючной инструкции, и если прибавить к нему длину этой инструкции (в случае с UD2 и чтения с нулевого адреса это 2 байта), то в аккурат получим смещение к запрашиваемой процедуре, которая находится ниже аварийной:
C-подобный:
.data
zero dd 0 ;//<--- переменная-утка
.code
;//----- Здесь решаются какие-нибудь задачи,
mov eax,1234h
sub eax,ebx
nop
;//----- ....и пришло время вызвать процедуру "Mess".
mov ebx,[zero] ;//<--- EBX=0 из переменной
mov eax,[ebx] ;//<--- Внимание! Генерим исключение "Access Violation".
@mess: jmp Mess ;//<--- SEH отловит исключение и вызовет эту процедуру
xor eax,15151515h ;// продолжение кода..
rol eax,28
nop
Обратите внимание, что вызывать процедуры из кода не нужно, т.е. можно вообще убрать инструкцию
JMP Mess
– в этом и вся фишка! Главное оставить метку @mess
, чтобы указать функции SEH именно на процедуру "Mess". Алгоритм программы построен так, что имеется "таблица вызовов" процедур, где собраны все такие метки. Это позволяет запрашивать процедуры в любой последовательности просто перемещая метки в коде. Более того, если нам нужно добавить ещё процедур, мы тупо назначаем им метки и прописываем их в таблице. Позже мы ещё вернёмся к данному вопросу.3. SEH – обработчик исключений
Системному механизму SEH уже была посвящена отдельная статья, где рассматривались все его детали. Поэтому повторяться не будем, а вспомним лишь основные моменты, которые пригодятся в динамическом шифровании кода.
Эта мощная фишка позволяет устанавливать (поверх системного) пользовательские обработчики исключений. На языках высокого уровня ЯВУ она имеет эквивалент в виде операторов "try-except". Создать цепочку обработчиков и вклиниться в неё можно всего тремя инструкциями ассемблера так:
C-подобный:
push mySeh ;// указатель на наш обработчик
push dword[fs:0] ;// сдвигаем системный (на него указывает регистр FS)
mov dword[fs:0],esp ;// вклиниваемся в цепочку Chain!
С этого момента, все возникшие в системе исключения будут перенаправляться диспетчером на нашу функцию "mySeh", а что мы будем делать внутри неё – это уже зависит от фантазии и настроения. Она представляет собой документированную Callback-функцию обратного вызова с таким прототипом (имя может быть произвольным):
C-подобный:
mySeh(
pRecord dd 0 ;// линк на тех.информацию об исключении
pFrame dd 0 ;// линк на данный SEH-фрейм в стеке
pContext dd 0 ;// линк на контекст регистров
pParam dd 0 ;// резерв
);
Когда после исключения программа сколлапсирует в чёрную дыру и наш обработчик получит управление, эти 4 указателя окажутся на вершине стека. Суть в том, что в этот момент диспетчер сохраняет значения всех регистров процессора в специальную структуру под названием "Context", и предоставляет нам указатель на неё в третьем аргументе "pContext". Из всей своры этих регистров, нам нужен только регистр-указатель на инструкцию
EIP
(Instruction Pointer), который расположен по смещению 184 от начала структуры. Именно в нём будет храниться адрес инструкции исключения, и если (как было сказано выше) прибавим к нему 2, то получим адрес метки для вызова процедуры. Полный контекст регистров на который указывает аргумент "pContext" декларируется мелкомягкими так:
C-подобный:
struct CONTEXT ;//<---- Size 204 (регистры CPU)
ContextFlags dd 0 ;// 00 (смещения от начала контекста)
iDr0 dd 0 ;// 04
iDr1 dd 0 ;// 08
iDr2 dd 0 ;// 12
iDr3 dd 0 ;// 16
iDr6 dd 0 ;// 20
iDr7 dd 0 ;// 24
FloatSave FLOATING_SAVE_AREA ;// 28
regGs dd 0 ;// 140
regFs dd 0 ;// 144
regEs dd 0 ;// 148
regDs dd 0 ;// 152
regEdi dd 0 ;// 156
regEsi dd 0 ;// 160
regEbx dd 0 ;// 164
regEdx dd 0 ;// 168
regEcx dd 0 ;// 172
regEax dd 0 ;// 176
regEbp dd 0 ;// 180
regEip dd 0 ;// 184 <---- EIP наш клиент!
regCs dd 0 ;// 188
regFlag dd 0 ;// 192
regEsp dd 0 ;// 196
regSs dd 0 ;// 200
ExtendedRegisters rb 512 ;// 204
ends
struct FLOATING_SAVE_AREA ;//<---- Size 112 (регистры FPU)
ControlWord dd 0
StatusWord dd 0
TagWord dd 0
ErrorOffset dd 0
ErrorSelector dd 0
DataOffset dd 0
DataSelector dd 0
RegisterArea db 80 dup(0)
Cr0NpxState dd 0
ends
Чтобы не подтягивать её инклудом в программу, в своём примере я явно указываю смещение на EIP=184, и взяв его значение прибавляю к нему 2:
C-подобный:
proc mySeh pRecord, pFrame, pContext, pParam
mov ebx,[pContext] ;// линк на контекст
mov ebx,[ebx+184] ;// EBX = значение регистра EIP
add ebx,2 ;// EBX = адрес метки процедуры
call ebx ;// зовём её!
mov ebx,[pContext] ;//
mov eax,[pRet] ;// адрес возврата (предварительно сохранён в переменной pRet)
mov [ebx+184],eax ;// заносим его в EIP
mov eax,0 ;// команда диспетчеру "Перезагрузить контекст и продолжить"
ret ;//<------ возвращаем управление диспетчеру исключений!
Значит, чтобы получить адрес процедуры, мы сначала считываем значение EIP на момент исключения, а в конце перезаписываем EIP новым значением, чтобы процессор прыгнул по адресу возврата на нужный нам участок кода, т.е. после процедуры.
Немаловажными являются тут и две последние инструкции. Поскольку всё-что происходит внутри нашего обработчика SEH находится под чутким надзором системного диспетчера исключений, по окончании в регистре EAX он ожидает получить от нас или нуль (что означает "в Багдаде всё спокойно" и мы обработали исключение), или-же единицу, которая говорит об обратном. Здесь я передаю нуль, и диспетчер восстанавливает значения всех регистров из сохранённого контекста, куда мы предварительно подсунули уже новый EIP. Это в очередной раз доказывает, что процессоры х86 не обладают собственным интеллектом и делают лишь то, на что мы им укажем явно.
4. Модуль расшифровки/шифрования процедур
Выше упоминалось, что динамическое шифрование требует от нас депозита мозгов, а если учитывать автоматизацию и скрытность, это на порядок усугубляет и без того напряжённую обстановку. Здесь шаг-влево, шаг-вправо влечёт за собой расстрел, и приложение сразу-же падает навзничь без признаков жизни. Чтобы не получить пепел вместо фейверка, нужно быть предельно осторожным – особенно это касается границ шифрования блоков, так-что имеет смысл заранее оставлять в хвосте резерв как-минимум пару байт. В масштабах всего блока они не сыграют особой роли, зато избавят нас от трудно-вылавливаемых ошибок. Рассмотрим это на фрагменте моего демонстрационного примера.
Значит есть у меня три процедуры: "Mess" (тупо выводит сообщение), "Pass" (запрашивает пароль), и "Info" (выводит некоторую информацию). Теперь я должен предварительно их зашифровать, а когда придёт время, основная функция SEH должна будет: (1)расшифровать их, (2)передать на них управление, и после отработки (3)опять зашифровать. Все процедуры расположены в памяти плечом-к-плечу, поэтому чтобы вычислить размеры каждой из них, достаточно от адреса-начала следующей, отнимать адрес-начала предыдущей, т.е. получать разницу в байтах. В моём случае последовательность такая: Mess-->Pass-->Info-->mySeh.
Следующий момент – это ключи шифрования, и адрес возврата. Поскольку это просто пример, я храню их в открытом виде, хотя и выделил под них отдельную программную секцию, чтобы они не светились в дизассемблере. Паспорт каждой из процедур храню в своих массивах, которые выбираю табличным методом – вот фрагмент:
C-подобный:
section '.inc' data readable writeable
procTable dd @mess, keyMess
dd @pass, keyPass
dd @info, keyInfo
keyMess dd Mess, (Pass -Mess-1)/4, 0xC9125877
keyPass dd Pass, (Info -Pass-1)/4, 0x65210833
keyInfo dd Info, (mySeh-Info-1)/4, 0xAA27E395
В первом поле каждого из массивов лежат адреса процедур, а дальше.. для вычисления размера первой процедуры "Mess", я беру её разницу в байтах со-следующей "Pass", и оставив в резерве байт(-1) делю на 4. Это потому, что в третьем поле ключ шифрования у меня 4-байтный, и планирую шифровать процедуры блоками такого-же размера. (для справки: 1-байтный ключ можно подобрать за 256 итераций, 2-байтный уже за 65536, а 4-байтный более чем за 4 млрд).
Когда внутри SEH из контекста регистров мы получим "адрес метки" вызываемой процедуры в лице значения EIP+2, нужно будет пройтись по таблице "procTable" где все они собраны, и получить указатель на соответствующий массив "keyMess" или другой (см.второй dword в procTable). Для расшифровки и последующего шифрования любой из процедур больше ничего и не требуется.
C-подобный:
;//-------------------------------------------------//
;//-------- Обработчик исключений SEH --------------//
;//-------------------------------------------------//
proc mySeh pRecord, pFrame, pContext, pParam
local pRet dd 0
mov ebx,[pContext]
mov ebx,[ebx+184] ;// Context.EIP
add ebx,2 ;// EBX = адрес метки процедуры для вызова
mov [pRet],ebx ;// ...(адрес возврата в лок.переменную)
mov esi,procTable ;// ESI = таблица указателей
mov ecx,3 ;// ECX = число записей в ней
@@: lodsd ;// EAX = очередное значение из таблицы
cmp eax,ebx ;// ищем массив записей процедуры
je @procFound ;// если нашли!
add esi,4 ;// иначе: переход к сл.записи
loop @b ;// пройтись по всей таблице
@procFound:
mov ebx,[esi] ;// EBX = указатель на массив данных
mov esi,[ebx] ;// ESI = адрес процедуры (первый DWORD из массива)
mov ecx,[ebx+04] ;// ECX = длина процедуры (второй DWORD)
mov eax,[ebx+08] ;// EAX = ключ шифрования (третий DWORD)
push esi ecx eax esi ;// запомнить все данные для обратного шифрования
;//--- Декрипт
@@: xor [esi],eax
add esi,4
loop @b
pop esi
call esi ;//<---- вызов процедуры!
;//--- обратный Крипт
pop eax ecx esi
@@: xor [esi],eax
add esi,4
loop @b
mov esi,[pContext]
mov eax,[pRet] ;// адрес возврата из процедуры
mov [esi+184],eax ;// загнать его в EIP
xor eax,eax ;// EAX(0)= продолжить, 1 = прервать
ret
endp
Рассмотрим, что здесь происходит..
Значит в коде мы генерим исключение, после которого стоит одна из трёх (привязанных к процедурам) меток. Обработчик SEH отлавливает исключение и получает контекст всех регистров, среди которых есть и указатель на глючную инструкцию EIP. Прибавив к нему двойку
ADD EBX,2
мы получаем адрес метки, и обнаружив её в таблице "procTable" получаем указатель на связанный с процедурой массив данных (адрес процедуры, её размер и ключ). Остаётся расшифровать процедуру, запустить её на исполнение, после чего опять зашифровать.Таким образом, без каких-либо аргументов, функция SEH сама на автомате вычисляет все необходимые данные, и без знания алгоритма пионерам-взломщикам трудно будет разобраться, что здесь к чему, тем более-что на Win7 и выше отладчики не заходят в пользовательские обработчики исключений (как это было на ХР), пока не поставишь бряк по F2. Такие дела..
5. Сбор информации о шифровании
Ну и на финишной-прямой рассмотрим метод начального шифрования процедур.
Поскольку получив управление SEH расшифровывает процедуры, значит нам нужно предварительно зашифровать их. Радует то, что в массивах "keyMess" и прочих все данные уже для этого есть, и остаётся просто организовать крипт. Для этого в самом хвосте программы я выделил специальную секцию (без имени, чтобы она не подталкивала на мысль), и собрал в ней 3 блока по такой схеме:
C-подобный:
section '' code readable executable writeable
@hideCrypt:
mov esi,Mess ;// адрес процедуры для шифрования
mov ecx,[keyMess+4] ;// её длина из массива данных
mov ebx,[keyMess+8] ;// ключ шифрования из массива
cinvoke printf,<10,' Addr: %08x',\
10,' Size: %d',\
10,' Key : %08x',10,0>,esi,ecx,ebx
mov esi,Pass
mov ecx,[keyPass+4]
mov ebx,[keyPass+8]
cinvoke printf,<10,' Addr: %08x',\
10,' Size: %d',\
10,' Key : %08x',10,0>,esi,ecx,ebx
mov esi,Info
mov ecx,[keyInfo+4]
mov ebx,[keyInfo+8]
cinvoke printf,<10,' Addr: %08x',\
10,' Size: %d',\
10,' Key : %08x',10,0>,esi,ecx,ebx
jmp @next
push start ;//<----- опкоды перехода на EntryPoint,
ret ;// которые нужно будет вписать в самое начало секции.
Теперь компилируем программу и открываем её дисковый образ в удобном HEX-редакторе – я по старинке предпочитаю HIEW, версия 8.66 которого поддерживает и 64-битный код. Значит загружаем в него образ и комбинацией F8-->F6 выбираем секцию кода. Далее манипулируя стрелками находим нужный адрес, который мы получили на предыдущем этапе (в данном случае 0x00403000, начало секции), и задаём ключ шифрования последовательностью F3-->F8 (Edit, XOR). По-умолчанию Hiew ксорит байтами и в этом окне нет возможности изменить длину ключа. Но поскольку ключ у нас 4-байтный, то придётся записывать его задом-наперёд, как показано на скрине ниже:
После того-как определились с ключом, подтверждаем свои намерения клавишей Enter и всё той-же F8 приступаем к шифрованию первой процедуры. Как видно из логов, размер её 20 двойных слов DWORD, так-что жмём F8 ровно 20-раз (цвет зашифрованных байт должен меняться на жёлтый). По окончании сохраняем изменения по F9 и выходим из редактора по F10. Выйти надо для того, чтобы сменить значение ключа для шифрования следующей процедуры. Проделываем аналогичные операции с остальными двумя блоками кода.
Посмотрите на предыдущий фрагмент.. В самом его хвосте имеются строки
push start --> ret
.Они вставлены для того, что получить опкоды перехода на EntryPoint в программу. Если посмотреть в отладчике, этим инструкциям (в данном случае) будет соответствовать последовательность байт
0x6846324000c3
. На самом заключительном этапе, эту строку байт нужно будет вписать в самое начало безымянной секции "Крипт", а все временные блоки кода по сбору информации о процедурах можно забить нулями, чтобы они не бросались в глаза в дизассемблере и отладчиках. Так это будет выглядеть в редакторе:6. Практика – пример программы
Соберём теперь всё вышеизложенное под один капот.
Обратите внимание, что в шапке программы директива
entry @hideCrypt
определяет точку-входа, а данная метка прописана в самой последней секции "Крипт". Если не вставить туда JMP
на метку start:
по руководству выше, то основной код программы вообще не получит управления, со-всеми вытекающими последствиями.
C-подобный:
format pe console
include 'win32ax.inc'
entry @hideCrypt ;//<--- точка-входа указывает на безымянную секцию "Крипт"
;//-------
section '.inc' data readable writeable
procTable dd @mess,keyMess
dd @pass,keyPass
dd @info,keyInfo
keyMess dd Mess, (Pass -Mess-1)/4, 0xC9125877
keyPass dd Pass, (Info -Pass-1)/4, 0x65210833
keyInfo dd Info, (mySeh-Info-1)/4, 0xAA27E395
struct PERF_INFORMATION
cb dd sizeof.PERF_INFORMATION
CommitTotal dd 0
CommitLimit dd 0
CommitPeak dd 0
PhyTotal dd 0
PhyAvailable dd 0
SystemCache dd 0
KernelTotal dd 0
KernelPaged dd 0
KernelNonpaged dd 0
PageSize dd 0
HandleCount dd 0
ProcessCount dd 0
ThreadCount dd 0
ends
;//-------
.data
perfInfo PERF_INFORMATION
zero dd 0
buff db 0
;//-------
section '.code' code readable executable writeable
;//-------- Рабочие процедуры = исходный материал -----------//
;//-------- Выводит путь и имя своей тушки ------------------
proc Mess
invoke GetModuleFileName,0,buff,256
cinvoke printf,<10,' Procedure #1 =============',\
10,' File: %s',10,0>,buff
ret
endp
;//-------
;//-------- Запрашивает и проверяет пароль ------------------
proc Pass
local pKey dd 'e'+'n'+'c'+'r'+'y'+'p'+'t' ;//<--- хеш оригин.пароля
cinvoke printf,<10,' Procedure #2 =============',\
10,' Type pass: ',0>
xor eax,eax
xor ebx,ebx
@@: cinvoke _getch ;// символ в AL
cmp al,13 ;// это Enter???
je @stop ;// да - на выход
add ebx,eax ;// иначе: считаем сумму в EBX
push eax ebx ;//
cinvoke printf,<'*',0> ;// [*] вместо введённого символа
pop ebx eax ;//
jmp @b ;// уйти на повтор..
@stop: cmp ebx,[pKey] ;// сравнить хеши паролей!
je @ok
cinvoke printf,<' <<----- Wrong PASS !!!',10,0>
jmp @retn
@ok: cinvoke printf,<' <<----- Pass OK...',10,0>
@retn: ret
endp
;//-------
;//-------- Выводит системную инфу (см.PERF_INFORMATION) -----------//
proc Info
cinvoke printf,<10,' Procedure #3 =============',0>
invoke K32GetPerformanceInfo,perfInfo,14*4
cinvoke printf,<10,' Mem Page size: %d byte',\
10,' Handle count: %d',\
10,' Process count: %d',\
10,' Thread count: %d',10,0>,\
[perfInfo.PageSize],[perfInfo.HandleCount],\
[perfInfo.ProcessCount],[perfInfo.ThreadCount]
ret
endp
;//-------
;//------------------------------------------------------------//
;//-------------- Обработчик исключений SEH -------------------//
;//------------------------------------------------------------//
proc mySeh pRecord, pFrame, pContext, pParam
local pRet dd 0
mov ebx,[pContext]
mov ebx,[ebx+184] ;// Context.EIP
add ebx,2 ;// EBX = адрес процедуры для вызова
mov [pRet],ebx ;// адрес возврата
mov esi,procTable ;// ESI = таблица указателей
mov ecx,3 ;// ECX = число записей в ней
@@: lodsd ;// EAX = очередное значение из таблицы
cmp eax,ebx ;// ищем массив записей процедуры
je @procFound ;// если нашли!
add esi,4 ;// иначе: переход к сл.записи
loop @b ;// пройтись по всей таблице
@procFound:
mov ebx,[esi] ;// EBX = указатель на массив данных
mov esi,[ebx] ;// ESI = адрес процедуры (первый DWORD из массива)
mov ecx,[ebx+04] ;// ECX = длина процедуры (второй DWORD)
mov eax,[ebx+08] ;// EAX = ключ шифрования (третий DWORD)
push esi ecx eax esi ;// запомнить все данные для обратного шифрования
;//--- расшифровка
@@: xor [esi],eax
add esi,4
loop @b
pop esi
call esi ;//<--------- Вызов процедуры!
;//--- обратное шифрование
pop eax ecx esi
@@: xor [esi],eax
add esi,4
loop @b
mov esi,[pContext]
mov eax,[pRet] ;// адрес возврата
mov [esi+184],eax ;// Context.EIP
xor eax,eax ;// EAX(0)= продолжить, 1 = прервать
ret
endp
;//*****************************************************************
;//******************** S T A R T ******************************
;//*****************************************************************
start: invoke SetConsoleTitle,<'*** Dynamic Crypt Example ***',0>
push mySeh ;//<--- Ставим свой SEH
push dword[fs:0]
mov dword[fs:0],esp
;//----- Здесь решаются какие-нибудь задачи,
mov eax,1234h
sub eax,ebx
nop
;//----- ....и пришло время вызвать процедуру!
mov ebx,[zero]
mov eax,[ebx] ;//<--- Внимание! Генерим исключение.
@mess:
xor eax,15151515h ;// продолжение кода..
rol eax,28
nop
;//----- Вызов очередных процедур!
mov ebx,[zero]
mov eax,[ebx] ;//<--- Исключение!
@pass:
mov ebx,[zero]
mov eax,[ebx] ;//<--- Исключение!
@info:
nop
nop
@next: cinvoke printf,<10,' Dynamic crypt OK...',0>
@exit: cinvoke _getch
cinvoke exit,0
;//----- Временная секция для сбора инфы о процедурах -----------------//
;//----- Сейчас это точка-входа в программу, поэтому нужно будет вставить сюда
;//----- инструкцию перехода на фактический EntryPoint в секцию-кода.
section '' code readable executable writeable
@hideCrypt:
mov esi,Mess ;// адрес процедуры для шифрования
mov ecx,[keyMess+4] ;// её длина из массива данных
mov ebx,[keyMess+8] ;// ключ шифрования из массива
cinvoke printf,<10,' Mess Addr: %08x',\
10,' Size: %d',\
10,' Key : %08x',10,0>,esi,ecx,ebx
mov esi,Pass
mov ecx,[keyPass+4]
mov ebx,[keyPass+8]
cinvoke printf,<10,' Pass Addr: %08x',\
10,' Size: %d',\
10,' Key : %08x',10,0>,esi,ecx,ebx
mov esi,Info
mov ecx,[keyInfo+4]
mov ebx,[keyInfo+8]
cinvoke printf,<10,' Info Addr: %08x',\
10,' Size: %d',\
10,' Key : %08x',10,0>,esi,ecx,ebx
jmp @exit
push start ;//<---- опкоды перехода на ОЕР
ret ;//<---- ^^^^^^ вставить в начало данной секции!
;//-------------------
section '.idata' import data readable
library msvcrt,'msvcrt.dll',kernel32,'kernel32.dll',user32,'user32.dll'
include 'api\msvcrt.inc'
include 'api\kernel32.inc'
include 'api\user32.inc'
Кстати если вскормить этот код аверам, то из 13-ти всего двое посчитали его малварью, хотя на самом деле в нём нет ничего особого, а только крипт. Так-что слепо доверять антивирусам тоже не есть гуд, хотя у каждого своё право.
7. Заключение
Здесь был рассмотрен классический вариант динамического шифрования, ..как говорят фундаментальные основы. В реальных-же программах имеет смысл воспользоваться более продвинутыми методами шифрования, с использованием специальных API-функций из библиотек Advapi32.dll, Bcrypt.dll и прочих. Потренировавшись "на кошках" и освоив эту технику можно смело двигаться дальше и придумать алгоритм, при котором шифруемые процедуры связывались-бы между собой в цепочку так, что одна без другой просто не функционировала-бы. Хорошие результаты даёт и периодический пересчёт контрольной суммы отдельных процедур и всего кода в целом. Это позволит предотвратить всякого рода мод двоичного кода, если вдруг кто-то захочет изменить
JZ
на JNZ
при проверке паролей.В скрепку кладу исполняемый файл данного исходника – попробуйте погонять его в отладчике и найти пароль. Для тех-кто захочет попрактиковаться в шифровании и собрать исходник своими руками, в скрепке имеется и HEX-редактор "Hiew 8.66", способный редактировать как 32, так и 64-бит приложения. Всем удачи, пока!