 |
|

09.10.2021, 18:00
|
|
Постоянный
Регистрация: 01.09.2019
Сообщений: 378
С нами:
3526561
Репутация:
0
|
|
В программировании принято разделять шифрование на статическое и динамическое. Первый вариант подразумевает крипт всей тушки дискового образа программы, и обратный декрипт его при загрузки в память. Он прост в практической реализации, но как и следует из алгоритма, защищает лишь двоичный образ софта на жёстком диске, а в памяти ОЗУ код уже сбрасывает с себя всю маскировку и становится полностью доступным исследователю. Остаётся тупо снять дамп памяти, и в оффлайне не торопясь разбирать защиту.
Чтобы хоть как-то этому противостоять, программисты придумали "динамическое шифрование" (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 служебная функция, и далее начинается непосредственно сам код с точкой-входа в программу
. На модуль SEH возложена критически-важная часть работы – доступ к процедурам будет осуществляться только через него!
В самом хвосте адресного пространства валяется (в специально выделенной для этих целей секции) временный "криптор". Его задача – собрать информацию о процедурах типа: начало\размер\ключ блока шифрования, и вывести все эти данные на консоль. Это избавит нас от выносящего мозг ручного вычисления паспортов процедур. Модуль обязательно должен находиться в самом конце программы, чтобы после его удаления не съехали со-своих позиций все адреса. После того-как он выполнит свою задачу, нужно будет в HEX-редакторе вписать вместо него
на точку-входа в программу EntryPoint.
В данном случае из кода вызывается процедура(А) и поскольку модулю SEH система передаёт управление только в случае возникновения исключений "Exception", то вызов процедуры нужно будет предварить какой-нибудь исключительной ситуацией типа: деление на нуль, чтение с ядерного или иного недоступного прикладной задаче адреса, или-же специально предназначенной для этих целей инструкцией генерирования ошибок
(UndefineD Instruction opcode). Однако
явно раскрывает все наши карты, т.к. в легальных программах она практически не встречается и служит чисто для отладочных целей, поэтому лучше поступить так..
Создаём переменную со-значением нуль и делаем вид, что она предназначена для какого-нибудь указателя. На самом деле это утка и мы вообще не планируем ложить туда линк, а просто считываем этот нуль в любой регистр и используя его как адрес, пытаемся прочитать с него. В результате получаем нарушение прав доступа "Access Violation" с кодом исключения
. Это хоть какая-то маскировка, чем явный
или деление на нуль.
На момент, когда SEH получит управление, диспетчер исключений мастдая положит в стек адрес глючной инструкции, и если прибавить к нему длину этой инструкции (в случае с UD2 и чтения с нулевого адреса это 2 байта), то в аккурат получим смещение к запрашиваемой процедуре, которая находится ниже аварийной:
C-подобный:
[CODE]
.
data
zero dd
0
;
//Pass-->Info-->mySeh.
Следующий момент – это ключи шифрования, и адрес возврата. Поскольку это просто пример, я храню их в открытом виде, хотя и выделил под них отдельную программную секцию, чтобы они не светились в дизассемблере. Паспорт каждой из процедур храню в своих массивах, которые выбираю табличным методом – вот фрагмент:[/FONT]
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-подобный:
[CODE]
;
//-------------------------------------------------//
;
//-------- Обработчик исключений 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
;
//
,
esi
,
ecx
,
ebx
mov esi
,
Pass
mov ecx
,
[
keyPass
+
4
]
mov ebx
,
[
keyPass
+
8
]
cinvoke printf
,
,
esi
,
ecx
,
ebx
mov esi
,
Info
mov ecx
,
[
keyInfo
+
4
]
mov ebx
,
[
keyInfo
+
8
]
cinvoke printf
,
,
esi
,
ecx
,
ebx
jmp @next
push start
;
//F6[/COLOR] выбираем секцию кода. Далее манипулируя стрелками находим нужный адрес, который мы получили на предыдущем этапе (в данном случае 0x00403000, начало секции), и задаём ключ шифрования последовательностью F3-->F8 (Edit, XOR). По-умолчанию Hiew ксорит байтами и в этом окне нет возможности изменить длину ключа. Но поскольку ключ у нас 4-байтный, то придётся записывать его задом-наперёд, как показано на скрине ниже:[/FONT]
После того-как определились с ключом, подтверждаем свои намерения клавишей Enter и всё той-же F8 приступаем к шифрованию первой процедуры. Как видно из логов, размер её 20 двойных слов DWORD, так-что жмём F8 ровно 20-раз (цвет зашифрованных байт должен меняться на жёлтый). По окончании сохраняем изменения по F9 и выходим из редактора по F10. Выйти надо для того, чтобы сменить значение ключа для шифрования следующей процедуры. Проделываем аналогичные операции с остальными двумя блоками кода.
Посмотрите на предыдущий фрагмент.. В самом его хвосте имеются строки
.
Они вставлены для того, что получить опкоды перехода на EntryPoint в программу. Если посмотреть в отладчике, этим инструкциям (в данном случае) будет соответствовать последовательность байт
. На самом заключительном этапе, эту строку байт нужно будет вписать в самое начало безымянной секции "Крипт", а все временные блоки кода по сбору информации о процедурах можно забить нулями, чтобы они не бросались в глаза в дизассемблере и отладчиках. Так это будет выглядеть в редакторе:
6. Практика – пример программы
Соберём теперь всё вышеизложенное под один капот.
Обратите внимание, что в шапке программы директива
определяет точку-входа, а данная метка прописана в самой последней секции "Крипт". Если не вставить туда
на метку
по руководству выше, то основной код программы вообще не получит управления, со-всеми вытекающими последствиями.
C-подобный:
Код:
format pe console
include
'win32ax.inc'
entry @hideCrypt
;
//
,
buff
ret
endp
;
//-------
;
//-------- Запрашивает и проверяет пароль ------------------
proc Pass
local pKey dd
'e'
+
'n'
+
'c'
+
'r'
+
'y'
+
'p'
+
't'
;
//
xor eax
,
eax
xor ebx
,
ebx
@@
:
cinvoke _getch
;
// символ в AL
cmp al
,
13
;
// это Enter???
je @stop
;
// да - на выход
add ebx
,
eax
;
// иначе: считаем сумму в EBX
push eax ebx
;
//
cinvoke printf
,
;
//[*] вместо введённого символа
pop ebx eax
;
//
jmp @b
;
// уйти на повтор..
@stop
:
cmp ebx
,
[
pKey
]
;
// сравнить хеши паролей!
je @ok
cinvoke printf
,
jmp @retn
@ok
:
cinvoke printf
,
@retn
:
ret
endp
;
//-------
;
//-------- Выводит системную инфу (см.PERF_INFORMATION) -----------//
proc Info
cinvoke printf
,
invoke K32GetPerformanceInfo
,
perfInfo
,
14
*
4
cinvoke printf
,
,
\
[
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
;
//
push mySeh
;
//
@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
,
,
esi
,
ecx
,
ebx
mov esi
,
Pass
mov ecx
,
[
keyPass
+
4
]
mov ebx
,
[
keyPass
+
8
]
cinvoke printf
,
,
esi
,
ecx
,
ebx
mov esi
,
Info
mov ecx
,
[
keyInfo
+
4
]
mov ebx
,
[
keyInfo
+
8
]
cinvoke printf
,
,
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 и прочих. Потренировавшись "на кошках" и освоив эту технику можно смело двигаться дальше и придумать алгоритм, при котором шифруемые процедуры связывались-бы между собой в цепочку так, что одна без другой просто не функционировала-бы. Хорошие результаты даёт и периодический пересчёт контрольной суммы отдельных процедур и всего кода в целом. Это позволит предотвратить всякого рода мод двоичного кода, если вдруг кто-то захочет изменить
на
при проверке паролей.
В скрепку кладу исполняемый файл данного исходника – попробуйте погонять его в отладчике и найти пароль. Для тех-кто захочет попрактиковаться в шифровании и собрать исходник своими руками, в скрепке имеется и HEX-редактор "Hiew 8.66", способный редактировать как 32, так и 64-бит приложения. Всем удачи, пока!
|
|
|

09.10.2021, 20:21
|
|
Новичок
Регистрация: 20.08.2019
Сообщений: 0
С нами:
3544291
Репутация:
0
|
|
Судя по скриншотам с результатами детекта антивирусов - они обиделись на инструкцию XOR. Возможно, её тоже можно замаскировать.
|
|
|

10.10.2021, 10:05
|
|
Новичок
Регистрация: 01.06.2020
Сообщений: 0
С нами:
3132150
Репутация:
0
|
|
Статья, как всегда, на высоте! спасибо!
Marylin сказал(а):
куда мы предварительно подсунули уже новый EIP. Это в очередной раз доказывает, что процессоры х86 не обладают собственным интеллектом и делают лишь то, на что мы им укажем явно.
на x64 это было бы по-другому? имею в виду реализация
|
|
|

10.10.2021, 10:18
|
|
Постоянный
Регистрация: 01.09.2019
Сообщений: 378
С нами:
3526561
Репутация:
0
|
|
Hardreversengineer сказал(а):
они обиделись на инструкцию XOR
..думаю не только
здесь палится.
Антивирусы гоняют код на своих вирт.машинах и по своим шаблонам (штам) смотрят, что он делает. К примеру вот тот-же
, но авер детектит его уже по-иному:
C-подобный:
Код:
include
'win32ax.inc'
;
//----------
.
data
szText db
'Maintained by Stephen J. Gowdy <linux.usb.ids@gmail.com'
db
'If you have any new entries, please submit them via'
db
'http://www.linux-usb.org/usb-ids.html'
txtLen dd $
-
szText
;
//----------
.
code
start
:
mov esi
,
szText
mov ecx
,
[
txtLen
]
shr ecx
,
2
@@
:
xor dword
[
esi
]
,
0x12345678
add esi
,
4
loop @b
invoke ExitProcess
,
0
.
end start
DragonFly сказал(а):
на x64 это было бы по-другому?
Мой пример для х32, и на машинах х64 он запускается из-под оболочки WOW64 (Windows on Windows), поэтому должен работать исправно. Если-же писать полноценное х64 приложение, то размеры регистров и всех полей в структурах нужно расширять до 8-байт
.
|
|
|

12.10.2021, 20:28
|
|
Новичок
Регистрация: 11.10.2021
Сообщений: 0
С нами:
2416198
Репутация:
0
|
|
Разве на x64 это сработает, там же safeseh ?
|
|
|

13.10.2021, 05:00
|
|
Постоянный
Регистрация: 01.09.2019
Сообщений: 378
С нами:
3526561
Репутация:
0
|
|
rulet1337 сказал(а):
Разве на x64 это сработает, там же safeseh ?
На моей Win10 x64 код исправно отрабатывает (через WOW64).

|
|
|

14.10.2021, 00:30
|
|
Новичок
Регистрация: 31.01.2018
Сообщений: 0
С нами:
4359276
Репутация:
0
|
|
Господи, да это лучший раздел. Спасибо, Marylin. Очень приятно тебя читать.
|
|
|

14.10.2021, 12:43
|
|
Новичок
Регистрация: 03.07.2021
Сообщений: 0
С нами:
2560388
Репутация:
0
|
|
Спасибо! Как всегда супер!
|
|
|

14.10.2021, 20:24
|
|
Новичок
Регистрация: 11.10.2021
Сообщений: 0
С нами:
2416198
Репутация:
0
|
|
Marylin сказал(а):
На моей Win10 x64 код исправно отрабатывает (через WOW64).
Я имел ввиду на бинаре 64 разрядном этого не сделать уже, для safeseh заранее прописываются же все обработчики в структуре заголовка, свой обработчик не добавит насколько я понимаю.
ps: Marylin спасибо огромное тебе за твои статьи, тот случай, когда один человек вывозит весь форум (не в обиду остальным авторам)
|
|
|

21.10.2021, 21:35
|
|
Новичок
Регистрация: 26.07.2018
Сообщений: 0
С нами:
4106487
Репутация:
0
|
|
Marylin сказал(а):
..думаю не только
здесь палится.
палится RWX секция кода, аверы такое очень не любят.
Статья шикарная, спасибо!
|
|
|
|
 |
|
|
Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
|
|
|
|