..--------------------------------------------------------..
.. Пример использования частично шифрованного кода ..
.. на ассемблере (x86) ..
..________________________________________________________..
Про кого в древности говорили, что он хорошо
сражается, тот побеждал, когда было легко победить.
Поэтому, когда хорошо сражавшийся побеждал, у него
не оказывалось ни славы ума, ни подвигов мужества.
(с) Сунь-цзы, Искусство войны
Иногда в процессе программирования встает вопрос о защите своего
кода от сторонних "наблюдателей": реверсеров, антивирусных программ, да
и просто от любопытствующих. Данная задача может быть решена
применением обычных пакеров или протекторов. Пакеры, сжимая код
программы, делают его, соответсвенно, невозможным для изучения (если
перед этим программу не распаковать конечно :). Протекторы же
проектируются как раз для защиты. Поэтому, если в первом случае дело
обстоит просто: стоит только снять дамп с работающего образа файла, то
в случае с протекторами приходит нежданный облом. Код может быть до
того запутан, что разобраться в нем - очень непростое дело.
Другой вариант - шифрование программы. Что, конечно, хорошо, но тоже не
спасает от обычного дампа.
Это если зашифрован весь код. А если попробовать защитить только
определенные функции? Те, которые легко обнаружить антивирусом, или
те, которые станут доступны только в определенном случае (пример -
шароварные программы)? Вот этим мы сегодня и займемся =)
Итак, все примеры будут написаны на ассемблере, т.к. этот язык
однозначно рулит во всех направлениях. А для начала разберемся в том,
что, собственно, хотим получить.
Чтобы зашифровать какую-либо функцию, ее стоит сначала написать. Чем в
данный момент и займемся. Накатаем на скорую руку парочку процедур,
вводный код, который их вызывает и... пока остановимся :)
.code
;----------------------------------------------
Test1 proc
pusha
mov eax, 1
mov ebx, 2
xor eax, ebx
popa
ret
Test1_endp:
endp
;----------------------------------------------
Test2 proc
pusha
mov eax, 2
mov ebx, 3
xor eax, ebx
popa
ret
Test2_endp:
endp
;----------------------------------------------
Test3 proc
pusha
mov eax, 3
mov ebx, 4
xor eax, ebx
popa
ret
Test3_endp:
endp
Думаем дальше. Чтобы знать КАКУЮ функцию нужно шифровать - нужно знать
ее точный адрес в памяти и размер. И вот тут начинают проявляться
многие прелести ассемблера. Все решается банальным offset, что не
скажешь, например, о Си или дельфях, где гемороя больше на порядок. В
Си получить смещение определенной функции в файле не проблема. Проблемы
начнутся тогда, когда вы захотите узнать ее размер. Но об этом не
сегодня =)
Чтобы в любое время иметь прямой доступ к полученным нами значениям -
хорошей идеей будет где-то их хранить. Я выбрал следующий вариант (вы
же можете его улучшить/изменить по своему усмотрению): в начале секции
данных я размещал структуру следующего вида:
Смещение Размер Описание
0 2 Количество нужных нам функций
2 4 Адрес первой функции
6 4 Размер первой функции
... ...
n 4 Адреc n-ой функции
n+4 4 Размер n-ой функции
О предназначении каждого поля я думаю догадаетесь :) Заполняется эта
структура в процессе компиляции. В итоге - имеем доступ ко всей нужной
нам информации.
.data
Proc_Entries_Num dw 3 ; кол-во функций
Proc_Entries:
Test1_addr dd offset Test1 ; адрес первой функции
Test1_len dd Test1_endp - offset Test1 ; её размер
Test2_addr dd offset Test2 ; адрес второй функции
Test2_len dd Test2_endp - offset Test2 ; её размер
Test3_addr dd offset Test3 ; адрес третьей функции
Test3_len dd Test3_endp - offset Test3 ; её размер
curr_proc dd ? ; здесь будем хранить адрес текущей функции
curr_proc_size dd ? ; здесь - её размер
; данные параметры необходимы для
; корректного выполненния...
Дальше - лучше. Т.к. в только что созданном исполняемом файле все
функции прописаны открытым текстом, следует их каким-то образом
зашифровать. Лично я сделал для этого отдельную утилитку: она просто
ищет в файле секцию данных, разбирает созданную нами структуру и
шифрует нужные адреса в соответствии с выбранным алгоритмом. Все
просто как никогда.
Что касается самого криптования - вот это и есть самое интересное :)
Здесь открывается неограниченное поле для наших возможностей. Все дело
в том, что нам (т.е. вам) ничего не стоит на место процедуры CryptProc
вставить свой алгоритм. Будь то Blowfish, MD5 или же какой-нибудь
DES =) Главное - это позаботиться о соответствующей корректной
расшифровке. Я же в качестве примера использовал обычный xor.
DecryptProc:
mov esi, eax
mov edi, offset Proc_Entries
movsx ecx, word ptr Proc_Entries_Num
search:
mov edx, dword ptr [edi]
cmp edx, esi
jz found
add edi, 8
loop search
ret
found:
mov ecx, dword ptr [edi+4] ;ecx - size of func
mov dword ptr [curr_proc], eax
mov dword ptr [curr_proc_size], ecx
decrypt: ; crypto algorithm..
xor byte ptr [esi], 66h
inc esi
loop decrypt
ret
EncryptProc:
mov esi, dword ptr [curr_proc] ; offset of our function
mov ecx, dword ptr [curr_proc_size] ; its size
crypt: ; crypto algorithm
xor byte ptr [esi], 066h
inc esi
loop crypt
ret
Недостатки данного метода:
а) Структуру нужно составлять руками
б) Если данная структура испортится каким-либо непонятным образом,
то восстановить первоначальный код будет проблематично. Чтобы
решить данную проблему, можно использовать, например, некий
CRC-алгоритм. Применяемый или к структуре как таковой, или ко всему
образу в целом.
в) Проблематичность использования в больших проектах. Хотя, конечно,
адаптации и доводке данный спопоб и подлежит, но вот чтобы
реализовать на его основе что-то большое - в это мне с трудом
верится :)
В общем, можно предположить, что данный код будет служить неким
базисом, от которого вы будете отталкиваться в дальнейших своих
исследованиях. Принцип на самом деле очень прост. Поэтому - дерзайте =)
В include/crypt_func вы найдете:
crypt.asm - разбирает структуру в файле и шифрует заданные функции.
test.asm - тестовая программа.