![]() |
|
|
Регистрация Восстановить пароль |
Регистрация | Задать вопрос |
Заплачу за решение |
Новые сообщения |
Сообщения за день |
Расширенный поиск |
Правила |
Всё прочитано |
![]() |
|
|
Опции темы | Поиск в этой теме |
![]() |
#21 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
В арсенале DOS/BIOS имеются некоторое кол-во прерываний, от которых нам 'не холодно, не жарко'. Можно закодировать их в опкоды, и выстроив в таблицу дёргать ими когда угодно. Опкодом инструкции 'INT' является байт со-значением 'CDh'. Кстати, если вам понадобится опкод какой-нибудь инстуркции, то получить его можно в отладчике. Вот пример в дзебуге:
Код:
Код:
Код:
Нашедшего выход - затаптывают первым..
Последний раз редактировалось R71MT; 22.07.2016 в 20:42. |
![]() |
![]() |
![]() |
#22 |
Старожил
Регистрация: 13.07.2012
Сообщений: 6,370
|
![]() |
![]() |
![]() |
![]() |
#23 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
Помимо описанных фишек, неплохой эффект дают и альтернативные способы вызовов самих прерываний. Под этим подразумевается не тупой вызов через классический INT, а через подставные регистры и переходы типа JMP/CALL/RET. Но для этого нужно знать, как работают оригинальные обработчики DOS, и что происходит при вызове INT.
Работа всех 86-ых процессоров основана на том, что на каждом шаге, ЦП считывает инструкцию с регистровой пары CS:IP и выполняет её. На следующем шаге сдвигается указатель(IP) и всё идёт по-кругу. Другими словами, содержимое CS:IP рассматривается как адрес выполняемой инструкции в формате [сегмент:смещение]. Выполнение прерывания(INT) начинается с того, что в стеке сохраняются 6 байт в таком порядке: 1. PUSHF - содержимое регистра флагов; 2. PUSH CS - адрес сегмента кода; 3. PUSH IP - адрес возврата (указатель на сл.инструкцию после INT). Далее, в регистровую пару CS:IP считывается адрес обработчика прерывания из таблицы векторов, и ЦП начинает выполнять уже инструкции обработчика. После того как обработчик выполнит свою задачу, управление возвращается прерванной процедуре командой 'IRET' (внутри обработчика), которая и восстанавливает из стека значения флагов и регистров CS:IP. Проц продолжает выполнять наш код.. Теперь посмотрим, что происходит при вызове 'JMP/CALL/RET'.. Эти переходы бывают трёх типов: короткий, близкий, дальний, ..или Short/Near/Far. Short и Near - это почти одно-и-тоже, поэтому мы рассмотрим только Near/Far-переходы. Эти операторы указывают ассемблеру на формат адресов, которые нужно подставить после инструкции перехода. Если тип JMP/CALL-переходов определён как дальний(Far), то адрес подставляется в формате [сегмент:смещение]. Если-же указан тип 'NEAR', то подставляется только смещение, что означает близкий переход. Безусловный JMP-переход не требует 'адреса возврата', поэтому ничего не сохраняет в стеке. Вызов-же ближней(Near) процедуры через 'CALL' происходит по такому алгоритму: в стеке сохраняется только значение регистра(IP), т.н. адрес возврата, который на выходе из процедуры восстанавливается командой 'RETN' (Return Near) или просто 'RET'. Одним словом, при ближнем(CALL) в стеке сохраняется только смещение следующей инструкции, размером в 2 байта. Соответственно, если наш код вызывает дальнюю процедуру, то мы должны указать 'CALL FAR', который на входе в процедуру сохранит в стеке регистровую пару CS:IP, как адрес возврата (т.е.вместе с сегментом, а это уже 4 байта). От сюда следует, что дальние процедуры должны заканчиваться по 'RETF' (Return Far), который снимет со-стека 4-байтный адрес возврата в пару CS:IP, и указатель вернётся в тот-же сегмент и точку, на которой остановился до вызова дальней процедуры. Если 'call far' сохраняет адрес возврата вместе с сегментным адресом, то ведь это-же делает и вызов INT, о котором мы говорили вначале! Значит через 'call far' можно иммитировать классический вызов прерывания. Только нужно будет предварительно сохранить ещё и флаги. Пробуем.. Код:
В коде выше я получаю вектор нужного прерывания дос-функцией(35h), но можно считать его и напрямую с таблицы векторов, которая лежит по адресу [0:0]. Каждый вектор занимает там по 4 байта, в которых и лежат адреса обработчиков в формате [смещение:сегмент], т.е. расположены наоборот: Код:
Чтобы найти адрес нужного вектора в этой таблице, нужно номер прерывания умножить на 4. Например, вектор прерывания(21h) лежит по адресу: 21h*4=84h; вектор(16h) будет иметь адрес: 16h*4=58h, и т.д: Код:
На следующем шаге можно избавиться и от CALL-переходов, заменив их на простые джумпы. Но тут есть проблема.. Нам нужно будет сохранить регистр(IP), а он отвергает любое-к-себе обращение. Его нельзя не заПУШить, не заМОВить, не заПОПить - поэтому придётся извращатся подручными методами, например так.. Мы знаем, что указатель(IP) всегда указывает на следущую инструкцию кода. Если поставить после вызова какую-нить метку, то она в аккурат будет попадать под указатель(IP). Остаётся заПУШить эту метку, и всё.. Код:
Выше говорилось, что инструкции 'RET/RETN' восстанавливают указатель(IP) со-стека, который поместил туда 'CALL'. В свою очередь 'RETF' восстанавливает(CS:IP), которые поместил в стек 'CALL FAR'. Но если учесть, что этот 'RET' не только снимает со-стека адрес возврата, но ещё и передаёт на него управление, то возможности наши расширяются.
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#24 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
Если положить в стек какой-нибудь адрес, а потом снять его командой 'RET', то мы получим обычный JMP-переход. Посмотрим на такой участок кода:
Код:
- флаги; - сегмент адреса возврата (CS); - смещение адреса возврата (IP) - сегмент обработчика прерывания из таблицы векторов (CS); - смещение обработчика из таблицы векторов (IP) - вызываем прерывание через RETF !; На практике это выглядит так: Код:
Комбинируя эти способы можно придумать ещё парочку. К примеру, стандартные обработчики DOS состоят не из одного обработчика, а из целой цепочки обработчиков, каждый из которых выполняет свою задачу. Большинство из этих задач нам не интересны, и мы можем пропустить их. Нужно сказать, что это не самая/лучшая идея, т.к. если мы и сможем вычислить адрес нужного нам обработчика из цепочки обработчиков, то в другой версии DOS этот адрес будет уже другим, и весь наш план потерпит фиаско. На данный момент мы имеем 4 варианта вызовов прерываний: через INT/CALL/JMP/RETF. Теперь можем каждый вариант выделить в отдельную процедуру и вызывать их рандомом в случайном порядке, как и остальные функции, описанные в этом посте.
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#25 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
В продолжении разговора хотелось-бы затронуть тему шифрования..
Шифрование в простой форме - обычная операция 'XOR'. Она удобна тем, что является обратимой, т.е. повторный 'XOR' с тем-же ключом возвращает оригинальное значение. Такой расклад позволяет нам использовать одну и ту-же функцию и в качестве криптора, и в качестве де/криптора: Код:
Код:
Код:
Определимся с ещё двумя терминами: хэш, и контрольная сумма(CRC). Их ещё называют 'хэш-сумма', и 'избыточный код' (соответственно). Если хэш - это сумма, то мы будем считать её так: Код:
Контрольная сумма (или избыточный код) - это чуть другое. Обычно, CRC вычисляется для защиты программ и данных. В краткой форме определить его можно так.. Возьмём массив, все байты которого сложим, как и в случае с хэш. На следующем этапе, берём какой-нить ключ (полином) и разделим хэш на ключ. Остаток от деления и будет представлять из себя 'избыточный код', или CRC. Нужно сказать, что это их определение на одно-клеточном уровне. На самом деле расчёты имеют намного запутанные алгоритмы, но для начального понимая нам хватит и этих определений. Пусть у нас есть такая процедура, которую мы хотим зашифровать.. Код:
Код:
Посмотрим на результат, как криптор перелопатил опкоды процедуры 'Example': Код:
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#26 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
Первое правило шифрования - не хранить пароли и ключи в явном/открытом виде!
Ну нет, так нет. Пойдём другим путём.. Будем снимать хэши с шифруемых процедур (в данном случае 'Example') и использовать эти хэши в качестве ключей шифрования. Получается выстрел дробью! Во-первых: для каждой процедуры будет генериться свой/индивидуальный ключ, во-вторых: этот ключ будет играть роль CRC, и если кто-либо захочет забить NOP'ами некую инструкцию в этой процедуре, то мы его обломаем, т.к. изменится хэш, ..а с ним и ключ шифрования: Код:
Но и такой алгоритм имеет свои недостатки.. Здесь мы дешифруем процедуры один раз. После запуска приложения, весь код в памяти уже расшифрован. В зашифрованном состоянии находится только файл на диске. Какой от этого толк? Взломщик снимает дамп памяти после запуска нашего приложения, и получает весь исходный код. Нам остаётся лишь с тоскою поглядывать, как он изучает наши алгоритмы. Хорошие результаты даёт динамическое шифрование кода. Суть его в том, что все процедуры кода постоянно находится в зашифрованном состоянии (на диске, в памяти), и дешифруются только при их вызове. Как только вызываемая процедура отработает, она опять шифруется. В этом случае дамп памяти ничего не даст, и будет похож на кучу навоза, разгребать которую мало-кто захочет. Реализация динамического шифрования на программном уровне требует немалых усилий, но зато оправдывает все надежды.
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#27 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
Рассмотрим пример динамического шифрования данных.
Чтоб не забивать пространство, я буду использовать вместо процедур только их имена, без содежимого. Здесь главное понять структурную схему, а хирургические тонкости могут быть у каждого свои. Вариантов много, я например выбрал такой, в котором задействовал 2 дешифратора. Пишу программу обычным способом, помещаю в неё все/нужные процедуры, собираю компилятором. Всё работает, всё ОК! Пришло время шифрования. Здесь нужно понимать, что наши враги могут исследовать как файл на диске, так и (запущенный на исполнение) файл в памяти. Значит файл на диске должен лежать уже в зашифрованном состоянии, и полностью не расшифровываться при запуске. Шифровать мы его будем в самую/последнюю очередь, а пока посмотрим на общую схему с двойным дешифратором.. Код:
У каждого из них свой алгоритм шифрования.. Первый (который не жилец) просто ксорит всё тело программы ключом(A7h). Помимо ксора, в его ДНК должен присутствовать ген самоликвидации, поэтому оформляю его так: Код:
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#28 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
На второй криптор возложена более ответственная задача, поэтому он шифрует/дешифрует чуть-сложней (если можно так выразиться). Беру в качестве ключа начальное значение(0), и ксорю байты блока данных этим ключом, увеличивая ключ на(3) на каждом шаге. Вы можете придумать свой алгоритм, лишь-бы ключи(1.2) были разные:
Код:
На общей схеме видно, что когда код вызывает процедуру, запрос поступает сперва на дешифратор(2), который расшифровывает запрошенную процедуру. Для своей работы декриптор(2) требует только длину, и адрес начала процедуры в памяти. Расшифрованная.., пошевелив своей задницей.., эта процедура возвращает управление обратно криптору, который зашифровав тело отработавшей процедуры, передаёт уже эстафету по адресу возврата в основной код. Роль шифратора и дешифратора играет один и тот-же криптор(2), который я закинул в стек от посторонних глаз, и вызываю его по указателю(BP). Кодируется это просто.. Запускаю отладчик (у меня GRDB), ввожу код криптора в его окне, чтоб получить опкоды инструкций. Получаю такую строку байтов, которые и представляют из-себя криптор(2): Код:
В принципе, 12 байт не так-уж и много. Можно не хранить криптор в теле программы, а генерировать его по-необходимости на лету! В нашем распоряжении есть регистры размером по 4 байта, значит если растасовать опкоды криптора по трём/32-битным регистрам и сохранить эти регистры в стеке, то получим код, который можно будет вызывать прямо из стека по указателю. Получится: спонтом-под-зонтом.. Кстати, такая фишка будет работать и в защищённом режиме, не вызывая никаких 'Exception'. Только нужно помнить, что стек растёт снизу-вверх, а программа выполняется сверху-вниз, поэтому сохранять строку опкодов в стеке нужно в обратном порядке. Посмотрим на такую схему. Пусть наша com'ka занимает в памяти 400h байт: Код:
Далее, движимые этой идеей создаём 'матку', которая будет воспроизводить потомство крипторов(2): Код:
Код:
Остаётся вызвать криптор по указателю 'CALL BP', как он исправно отработает и вернёт управление через последний(RET) в своём теле (опкод С3h). Если идея с 'маткой' вам не приглянулась, или кажется трудно/реализуемой, то можно просто сгенерить один раз криптор в стек, и не затирая его оставить там болтаться. Тогда (за ненадобностью) можно затереть саму матку. Но это уже - на вкус и цвет..
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#29 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
Пришло время разобраться с тонкостями 'Криптора(2)'.
Вызов основным кодом требуемой процедуры происходит по такой схеме: [Запрос--> Декриптор(2)--> Выполнение--> Криптор(2)--> Ответ] Криптор(2) я решил оформить как перемещаемый модуль, чтобы его расположение в памяти было не фиксированным, а менялось с каждым запуском файла. Можно будет разместить в таблице несколько адресов и выбирать их рандомом. На который судьба ляжет, туда и подгружать криптор. Посмотрим на такой код: Код:
Код получился не большим, внутри кода все переходы типа 'Short', поэтому транслятор при ассемблировании не подменяет метки, а вставляет относительные адреса, в результате чего код можно свободно перемещать в любую область памяти, сохраняя при этом работоспособность циклов. Скомпилировав в FASM'е этот исходник, получаю криптор(2) в бинарном виде. Вскармливаю этот бин HIEW'у, и нажав в его окне [F4-->F2] получаю строку опкодов криптора. Эта строка будет сердцем всей программы! Вот как будет выглядеть наше 'Core' внутри основной программы: Код:
Код:
Посмотрим на пример перемещения строки опкодов в стек: Код:
Опкоды криптора хранятся в файле на жёстком диске. Весь файл зашифрован ключом(A7h). После запуска файла на исполнение, криптор(1) вступает в свои права и дешифрует всё тело программы. Вместе с телом обнажается и криптор(2), который сразу-же копируется в рандомную область памяти (в данном случае в стек). На следующем этапе затирается оригинальная строка опкодов из секции данных, и программа переходит в режим готовности и выполнению команд юзверя.
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
#30 |
Участник клуба
Регистрация: 16.06.2011
Сообщений: 1,428
|
![]()
Для продолжения экспериментов понадобится какая-нить оболочка. Попробуем сделать из неё полноценную программу с динамическим шифрованием процедур. Не буду сильно мудрить, а наваяю простенький вызов трёх процедур, которые и покажут всю подноготную замысла.
Из прикладных программ нам понадобится только компилятор 'FASM', редактор 'HIEW', ну и виндовый 'CALC' в инженерном виде. HIEW занимает в этой тройке почётное/первое место, т.к. именно в нём мы и будем шифровать программу. Тонкости шифрования оставим на потом, а пока ознакомимся с самой оболочкой: Код:
Эта программа должна вывести на экран приветствие и запрос на ввод строки, которую просто сохранит в буфере. После ввода строки она должна показать размер своего тела в 16-тиричном формате и выйти в DOS по INT-20h. Помимо декрипторов, в ней 3 процедуры: 'Message1', 'Message2' (с вложеной Hex2Asc) и 'inpBuff' для ввода строки в буфер. Здесь есть некоторые/умышленные ошибки, чтобы потом можно было на них указать, типа: "Так делать нельзя!". При этом программа на-ходу, и выполняет свою функцию. Если сейчас скопировать этот исходник в окно FASM'a и попытаться его скомпилировать, то компиляция пройдёт успешно, но полученный COM-файл не запустится, а досовская консоль просто подмигнёт нам и закроется. Думаю не трудно догадаться почему.. Исполнение кода начинается с того, что декриптор(1) начинает расшифровку всего тела ключом(A7h). Стоп! Так тело-то ещё не зашифровано! Получается что декриптор вместо расшифровки, наоборот шифрует тушку, поэтому программа и рушится ниже плинтуса. Такой-же подарочек нам приподнесёт и криптор(2) - начнёт шифровать ключами(0..3..6..) вместо расшифровки. Значит мы должны предварительно зашифровать программу, чтоб дать правильный старт дешифровщикам! Вот тут-то и начинается самое интересное..
Нашедшего выход - затаптывают первым..
|
![]() |
![]() |
![]() |
|
![]() |
||||
Тема | Автор | Раздел | Ответов | Последнее сообщение |
Полиморфизм | Anubys | Помощь студентам | 1 | 26.12.2011 20:42 |
Полиморфизм | Zorgan | Visual C++ | 22 | 29.08.2011 12:23 |
Полиморфизм | MasterSporta | Общие вопросы C/C++ | 3 | 10.04.2011 23:46 |
полиморфизм | slayerblya | Общие вопросы C/C++ | 1 | 27.02.2011 01:43 |
Полиморфизм | mister2010 | Общие вопросы C/C++ | 30 | 24.05.2010 01:07 |