Страницы 1
ОСНОВЫ ШИФРОВАНИЯ ДАННЫХ
(с) R71MT //2016г
-------------------------------
Шифрование в простой форме - обычная операция 'XOR'. Она удобна тем, что является обратимой, т.е. повторный 'XOR' с тем-же ключом возвращает оригинальное значение. Такой расклад позволяет нам использовать одну и ту-же функцию и в качестве криптора, и в качестве де/криптора:
mov al,9Ah ;AL = 9Ah (шифруемое значение)
mov bl,37h ;BL = 37h (ключ шифрования)
xor al,bl ;AL = ADh (шифруем AL)
xor al,bl ;AL = 9Ah (дешифруем AL)
Ключ шифрования нужно выбирать с таким значением, в котором единичные и нулевые биты активно перемешаны., т.к. XOR'ятся только те биты шифруемого значения, которые в маске ключа взведены. В однобайтном ключе должно быть, как минимум 3..5 единичных бита. Если проксорить значение ключом(0), то значение останется прежним:
; Плохие ключи ------------------
10h = 00010000b
88h = 10001000b
20h = 00100000b
0Ch = 00001100b
; Хорошие ключи ------------------
AAh = 10101010b
CDh = 11001101b
E6h = 11100110b
39h = 00111001b
Выбрав два ключа можно зашифровать значение повторно.
Тогда получим двойное шифрование, только и расшифровывать придётся 2 раза:
mov bx,0CD37h ;BX = два ключа шифрования
mov al,9Ah ;AL = 9Ah (шифруемое значение)
xor al,bl ;AL = ADh (шифруем AL первый раз)
xor al,bh ;AL = 60h (шифруем AL второй раз)
[....]
xor al,bl ;AL = 57h (дешифруем AL первый раз)
xor al,bh ;AL = 9Ah (дешифруем AL второй раз)
Здесь я намеренно поменял порядок дешифровки, хотя на выходе опять получил исходное значение(9Аh). Делаем вывод, что порядок следования ключей при двойной дешифровки не имеет значение: лишь-бы ключи были те-же. В сети можно найти много готовых алгоритмов шифрования, но все/они хорошо изучены. Интереснее придумывать свои/оригинальные пути, на изучение которых взломщик потеряет время.
Определимся с ещё двумя терминами: хэш, и контрольная сумма(CRC). Их ещё называют 'хэш-сумма', и 'избыточный код' (соответственно). Если хэш - это сумма, то мы будем считать её так:
C + r + y + p + t + o + r
-------------------------------------
43 72 79 70 74 6F 72 = 02F3h
Здесь, просто складываем все ASCII-коды символов текстовой строки. В случае с инструкциями - складываются все значения опкодов. Если нам нужен однобайтный хэш, то мы отбрасываем старшую часть суммы и получаем хэш(F3h).
Контрольная сумма (или избыточный код) - это чуть другое. Обычно, CRC вычисляется для защиты программ и данных. В краткой форме определить его можно так.. Возьмём массив, все байты которого сложим, как и в случае с хэш. На следующем этапе, берём какой-нить ключ (полином) и разделим хэш на ключ. Остаток от деления и будет представлять из себя 'избыточный код', или CRC. Нужно сказать, что это их определение на одно-клеточном уровне. На самом деле расчёты имеют намного запутанные алгоритмы, но для начального понимая нам хватит и этих определений.
Пусть у нас есть такая процедура, которую мы хотим зашифровать..
Example:
mov ax,1234
dec bp
les di,[bp+8]
stosw
xor dx,dx
div cx
shld bx,dx,16
ret
end_Example:
Для этого нам понадобятся: ключ шифрования, длина процедуры, и адрес начала этой процедуры в памяти. С ключом мы вроде определились, а остальное у нас предусмотрено в виде меток. Осталось набросать код криптора, который оформим в виде универсальной функции:
[....]
mov ah,77h ; Ключ
mov si,Example ; Адрес начала процедуры
mov cx,end_Example ; Адрес её конца
call Cryptor ; Шифруем процедуру!
[....]
;== Криптор ======[ver 0.1]==
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Cryptor:
mov di,si ; DI - для записи зашифрованного байта
sub cx,di ; СХ - длина процедуры 'Example'
@@: lodsb ; Читаем байт из SI
xor al,ah ; Шифруем его ключом
stosb ; Запись AL в DI
loop @b ; Циклимся СХ-раз
ret ; Выход..
Этот криптор получился слишком-уж ламерским, но он работает!
Посмотрим на результат, как криптор перелопатил опкоды процедуры 'Example':
; до шифрования...........................................
00000000: B8D204 mov ax,004D2
00000003: 4D dec bp
00000004: C47E08 les di,[bp][00008]
00000007: AB stosw
00000008: 31D2 xor dx,dx
0000000A: F7F1 div cx
0000000C: 0FA4D310 shld bx,dx,010
00000010: C3 retn
; и после.................................................
00000000: CF iret
00000001: A5 movsw
00000002: 733A jae 00000003E
00000004: B309 mov bl,009
00000006: 7FDC jg 0FFFFFFE4
00000008: 46 inc si
00000009: A5 movsw
0000000A: 808678D3A4 add b,[bp][0D378],0A4
0000000F: 67B4 mov ah,000
Ясно, что после таких изменений код теряет свою функциональность и взломщику придётся по-возиться, чтоб востановить его работоспособность. Всё хорошо, -да ничего хорошего. Что здесь искать, если мы сами передаём взломщику ключ на ладони, тьфу, в регистре(AH)?! Он имеет ключ, начало и длину шифруемого участка кода, а этого достаточно, чтобы расшифровать процедуру, например в Hiew'e. Прокол!..
Редактировался R71MT (Сегодня 20:12:37)
Первое правило шифрования - не хранить пароли и ключи в явном/открытом виде!
Ну нет, так нет. Пойдём другим путём.. Будем снимать хэши с шифруемых процедур (в данном случае 'Example') и использовать эти хэши в качестве ключей шифрования. Получается выстрел дробью! Во-первых: для каждой процедуры будет генериться свой/индивидуальный ключ, во-вторых: этот ключ будет играть роль CRC, и если кто-либо захочет забить NOP'ами некую инструкцию в этой процедуре, то мы его обломаем, т.к. изменится хэш, ..а с ним и ключ шифрования:
[....]
mov si,Example
mov cx,end_Example
call Cryptor
[....]
;== Криптор ======[ver 0.2]==
;^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Cryptor:
xor ax,ax
sub cx,si ; Длина процедуры
push si si cx ; Запомним для шифрования..
@@: lodsb ; Читаем байтики из SI
add ah,al ; Считаем хэш-сумму в AH
loop @b
pop cx si di ; СХ = длина, SI = чтение, DI = запись
@@: lodsb
xor al,ah ; Шифруем процедуру!
stosb ; Пере/запись байта(DI)
loop @b
ret
Здесь уже нет ключа в явном виде, хотя его можно увидеть в отладчике. Но отладчику можно противостоять тупым вырубанием клавы, перехватом INT-1h, инверсией регистра(SP), и т.д. В любом случае - ключ не бросается в глаза, а это уже вери-гуд.
Но и такой алгоритм имеет свои недостатки..
Здесь мы дешифруем процедуры один раз. После запуска приложения, весь код в памяти уже расшифрован. В зашифрованном состоянии находится только файл на диске. Какой от этого толк? Взломщик снимает дамп памяти после запуска нашего приложения, и получает весь исходный код. Нам остаётся лишь с тоскою поглядывать, как он изучает наши алгоритмы.
Хорошие результаты даёт динамическое шифрование кода. Суть его в том, что все процедуры кода постоянно находится в зашифрованном состоянии как на диске, так и в памяти, и дешифруются только при их вызове. Как только вызываемая процедура отработает, она опять шифруется. В этом случае дамп памяти ничего не даст, и будет похож на кучу навоза. Реализация динамического шифрования на программном уровне требует немалых усилий, но зато оправдывает все надежды.
Рассмотрим пример динамического шифрования данных.
Чтоб не забивать пространство, я буду использовать вместо процедур только их имена, без содержимого. Здесь главное понять структурную схему, а хирургические тонкости могут быть у каждого свои. Вариантов много, я например выбрал такой, в котором задействовал 2 дешифратора.
Пишу программу обычным способом, помещаю в неё все/нужные процедуры, собираю компилятором. Всё работает, всё ОК! Пришло время шифрования. Здесь нужно понимать, что наши враги могут исследовать как файл на диске, так и запущенный на исполнение, файл в памяти. Значит файл на диске должен лежать уже в зашифрованном состоянии, и полностью не расшифровываться при запуске. Шифровать мы его будем в самую/последнюю очередь, а пока посмотрим на общую схему с двойным дешифратором:
org 0h ------------------------8<-------------------8<-----+
Место под PSP |
... |
org 100h |
JMP на декриптор(1) ---->>----------->>------+ |
| |
Начало программы <<-------------<<---------+ | V
Тело программы... | | |
Вызов процедуры --->>----------->>------+ | | |
Адрес возврата <<----------<<--------+ | | | |
Конец программы ! | | | | |
;------------------------------------|--|--|-|----- |
Процедура(0) <------<<-----<<-----+ | | | V |
Процедура(1) | | | | | |
Процедура(N) ---->>----->>---+ | | V | | |
;----------------------------|----|--|--|--|-|----->8------+
| | | | | |
Начало декриптора(1) <<------|----|--|--|--|-+
Конец декриптора(1) ---->>--|----|--|--|--+
| | | |
Свободная память ----+ | | | V
| V | | |
| | | | |
Указатель стека =====+=======|====|==|==|===> Текущее значение(SP) ==+
Начало дектиптора(2) <<------|----|--|--+ |
Конец дектиптора(2) ---->>--|----+ | ==== C Т Е К ==== V
Начало криптора(2) <<------+ | ================= |
Конец криптора(2) ----->>---------+ |
Дно стека ========== Конец сегментной памяти ==========<<============+
.
После запуска дискового файла, его содержимое помещается в ОЗУ и дешифратор(1) приступает к расшифровке всего тела программы. Выполнив на начальном этапе свою задачу, он делает себе хара-кири. Дешифратор(1) нам больше не нужен. В игру вступает основной криптор/декриптор(2), который играет в этом спектакле главную роль.
У каждого из них свой алгоритм шифрования..
Первый (который не жилец) просто ксорит всё тело программы ключом(A7h).
Помимо ксора, в его ДНК должен присутствовать ген самоликвидации, поэтому оформляю его так:
Cryptor_1:
mov al,0A7h
mov bx,start
@@: xor byte[bx],al
inc bx
cmp bx,end_start
jb @b
end_Cryptor_1:
mov di,Cryptor_1
lea cx,[end_Cryptor_1 - Cryptor_1]
xor al,al
rep stosb
ret
Здесь ничего сверх/естесственного: дешифруется всё тело ключом(А7) от метки 'Start' до метки 'endStart', после чего сам криптор через 'stosb' забивается в памяти нулями. Нужно сказать, что файл на диске остаётся неизменным и при следующем запуске - всё повторится. Все манипуляции проделываются только с копией файла в памяти.
На второй криптор возложена более ответственная задача, поэтому он шифрует/дешифрует чуть-сложней (если можно так выразиться). Беру в качестве ключа начальное значение(0), и ксорю байты блока данных этим ключом, увеличивая ключ на(3) на каждом шаге. Вы можете придумать свой алгоритм, лишь-бы ключи(1.2) были разные:
Cryptor_2:
xor ax,ax
@@: lodsb
xor al,ah
add ah,3
loop @b
ret
Позже мы добавим в него ещё кучу всего, пока это не важно. Главное представить, как это хозяйство будет функционировать: где будет валяться, у кого будет принимать и кому будет возвращать управление, какие параметры её нужны. Вот об этом и поговорим..
На общей схеме видно, что когда код вызывает процедуру, запрос поступает сперва на дешифратор(2), который расшифровывает запрошенную процедуру. Для своей работы декриптор(2) требует только длину, и адрес начала процедуры в памяти. Расшифрованная, ..пошевелив своей задницей, эта процедура возвращает управление обратно криптору, который зашифровав тело отработавшей процедуры, передаёт уже эстафету по адресу возврата в основной код.
Роль шифратора и дешифратора играет один и тот-же криптор(2), который я закинул в стек от посторонних глаз, и вызываю его по указателю(BP). Кодируется это просто. Запускаю отладчик (у меня GRDB), ввожу код криптора в его окне, чтоб получить опкоды инструкций. Получаю такую строку байтов, которые и представляют из-себя криптор(2):
Cryptor_2:
db 031h,0C0h,0ACh,030h,0E0h,080h,0C4h,003h,0E2h,0F8h,0C3h,0
[....]
Повторюсь, что это тестовый вариант и показан чисто для визуального восприятия происходящего. Законченный вариант требует некоторых объяснений, которые обсудим позже. Значит строка опкодов криптора..
В принципе, 12 байт не так-уж и много. Можно не хранить криптор в теле программы, а генерировать его по-необходимости на лету! В нашем распоряжении есть регистры размером по 4 байта, значит если растасовать опкоды криптора по трём/32-битным регистрам и сохранить эти регистры в стеке, то получим код, который можно будет вызывать прямо из стека по указателю. Получится: спонтом-под-зонтом. Кстати, такая фишка будет работать и в защищённом режиме, не вызывая никаких 'Exception'. Только нужно помнить, что стек растёт снизу-вверх, а программа выполняется сверху-вниз, поэтому сохранять строку опкодов в стеке нужно в обратном порядке.
Посмотрим на такую схему.
Пусть наша com'ka занимает в памяти 400h байт:
;...........................;
org 100h ; 0100h = адрес первой инструкции кода
push start ;
ret ;
start: ;
[.....] ;
int 20h ; 0500h = адрес последней инструкции кода
;...........................;
; Свободное пространство ;
; в нашем сегменте памяти ;
;...........................; MOV BP,SP (BP = адрес криптора в памяти)
; Фрейм в стеке ;
; для нашего криптора ;
; 31 C0 AC 30 ;
; E0 80 C4 03 ;
; E2 F8 C3 00 ;
;...........................; FFFEh = дно стека (значение SP)
Здесь наглядно продемонстрировано, где будет валяться криптор(2). В доках от Intel сказано, что ЦП отвечает за сохранность всех данных, которые расположены выше текущего значения регистра(SP). Естессно, что это относится только к стандартным/стековым операциям, типа PUSH/POP. Мы, при выделении фрейма в стеке, смещаем указатель командой [SUB SP,12], и теперь за свои данные можем не беспокоиться.
Далее, движимые этой идеей создаём 'матку', которая будет воспроизводить потомство крипторов(2):
CreateCryptor:
pop si ; Очищаем стек от адреса возврата
mov eax,30ACC031h ; Растасовываем
mov ebx,03C480E0h ; ..опкоды криптора
mov ecx,00C3F8E2h ; ..по-трём регистрам
push ecx ebx eax ; Помещаем их в стек, в нужном порядке
mov [pointer],sp ; Сохраняем в BP (или переменной), указатель на начало криптора
jmp si
Проверим, в правильном-ли порядке после таких ПУШ'ей расположились опкоды криптора в стеке:
; Содержимое стека после пушей 32-битных регистров...
GRDB version 1.7 Copyright (c) LADsoft
History enabled
->a
16FA:0100 mov eax,30ACC031
16FA:0106 mov ebx,03C480E0
16FA:010D mov ecx,00C3F8E2
16FA:0114 push ecx
16FA:0116 push ebx
16FA:0118 push eax
16FA:011A mov bp,sp
16FA:011C
.........
->t
eax:30ACC031 ebx:03C480E0 ecx:00C3F8E2 edx:00000000 esi:00000000 edi:00000000
ebp:00000000 esp:0000FFE2 eip:0000011A eflags:000B3202 NV UP EI PL NZ NA PO NC
ds: 16FA es:16FA fs:16FA gs:16FA ss:16FA cs:16FA
16FA:011A 8B EC mov bp,sp
->d bp
16FA:FFE0 31 C0 AC 30 E0 80 C4 03 E2 F8 C3 00 00 00
16FA:FFF0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
->q
;-----------------------------------------------------------------------------
; Код криптора(2) в опкодах.....
Cryptor_2:
db 031h,0C0h,0ACh,030h,0E0h,080h,0C4h,003h,0E2h,0F8h,0C3h,0
[....]
Как видим, данные идут в правильном порядке и это радует.
Остаётся вызвать криптор по указателю 'CALL BP', как он исправно отработает и вернёт управление через последний(RET) в своём теле (опкод С3h). Если идея с 'маткой' вам не приглянулась, или кажется трудно/реализуемой, то можно просто сгенерить один раз криптор в стек, и не затирая его оставить там болтаться. Тогда (за ненадобностью) можно затереть саму матку. Но это уже - на вкус и цвет..
Пришло время разобраться с тонкостями 'Криптора(2)'.
Вызов основным кодом требуемой процедуры происходит по такой схеме:
[Запрос--> Декриптор(2)--> Выполнение--> Криптор(2)--> Ответ]
Криптор(2) я решил оформить как перемещаемый модуль, чтобы его расположение в памяти было не фиксированным, а менялось с каждым запуском файла. Можно будет разместить в таблице несколько адресов и выбирать их рандомом. На который судьба ляжет, туда и подгружать криптор.
Посмотрим на такой код:
; fasm code.......
; Код собирается как бинарник, чтобы получить строку опкодов
;------------------------------------------------------------
format binary
push ax bx bx ; АХ конец, ВХ начало процедуры
mov dx,2 ; DX кол-во вызовов (декрипт/крипт)
@1: xor cx,cx ; ключ
@2: xor byte[bx],cl ; ксорим байт(ВХ) ключом
inc bx ; сл.байт
add cl,3 ; сл.ключ
cmp bx,ax ; всю процедуру проксорили?
jb @2 ; нет: продолжаем..
dec dx ; иначе: мы сейчас дешифровали или шифровали?
jz @3 ; пропустить, если шифровали!
pop bx ; иначе: берём адрес процедуры
call bx ; выполнить ВХ! (ниже - адрес возврата)
pop bx ax ; подготовка к шифрованию..
jmp @1 ; зашифровываем процедуру обратно!
@3: ret ; возвращаем управление в основной код
;......
Думаю комментов предостаточно.. На входе - сохраняется адрес начала/конца процедуры для последующей шифровки. Т.к. один криптор и-дешифрует-и-шифрует, то применяем в нём рекурсивный алгоритм (вызов самого себя). Рекурсию привязываем к счётчику(DX).
Код получился не большим, внутри кода все переходы типа 'Short', поэтому транслятор при ассемблировании не подменяет метки, а вставляет относительные адреса, в результате чего код можно свободно перемещать в любую область памяти, сохраняя при этом работоспособность циклов.
Скомпилировав в FASM'е этот исходник, получаю криптор(2) в бинарном виде.
Вскармливаю этот бин HIEW'у, и нажав в его окне [F4-->F2] получаю строку опкодов криптора. Эта строка будет сердцем всей программы! Вот как будет выглядеть наше 'Core' внутри основной программы:
; Криптор(2) в виде опкодов. Размер: 1Ch байт
; .....размер с нулями выравнивания: 1Fh байт (32d)
;----------------------------------------------------------------
Cryptor2 db 050h,053h,053h,0BAh,002h,000h,031h,0C9h,030h,00Fh
db 043h,080h,0C1h,003h,039h,0C3h,072h,0F6h,04Ah,074h
db 007h,05Bh,0FFh,0D3h,05Bh,058h,0EBh,0EAh,0C3h,000h,000h,000h
length = $ - Cryptor2
В исходнике видно, что адрес процедуры передаётся в регистрах(BX/AX), поэтому вызов процедуры из программы оформляем так:
mov bx,funcName
mov ax,end_funcName
call bp
Я специально перепутал все назначения регистров, порядок которых никак не влияет на ход событий. Например вместо счётчика используется(DX), ключ шифрования лежит в(СХ), на адрес конца указывает(АХ) и т.д. Нужно заметить, что голова криптора(2) должна находиться по адресу(BP), т.к. вызов у нас идёт через [CALL BP]. Значит перед копированием криптора в случайную область памяти, нужно запомнить точку входа в регистре(BP).
Посмотрим на пример перемещения строки опкодов в стек:
sub sp,length ; Выделяем фрейм в стеке для криптора
mov bp,sp ; Запомним точку входа в него
mov si,Cryptor2 ; SI = адрес источника
mov di,bp ; DI = адрес приёмника
mov cx,length ; СХ = длина строки опкодов
push cx si ; ..запомним
cld ; DF = 0 (прямой шаг слева-направо)
rep movsb ; Копируем строку из SI в DI
xor al,al ; Затираем нулями опкоды
pop di cx ; ..внутри тела программы,
rep stosb ; ..оставив только копию в стеке
Что мы имеем на данный момент?
Опкоды криптора хранятся в файле на жёстком диске. Весь файл зашифрован ключом(A7h). После запуска файла на исполнение, криптор(1) вступает в свои права и дешифрует всё тело программы. Вместе с телом обнажается и криптор(2), который сразу-же копируется в рандомную область памяти (в данном случае в стек). На следующем этапе затирается оригинальная строка опкодов из секции данных, и программа переходит в режим готовности и выполнению команд юзверя.
Для продолжения экспериментов понадобится какая-нить оболочка. Попробуем сделать из неё полноценную программу с динамическим шифрованием процедур. Не буду сильно мудрить, а наваяю простенький вызов трёх процедур, которые и покажут всю подноготную замысла.
Из прикладных программ нам понадобится только компилятор 'FASM', редактор 'HIEW', ну и виндовый 'CALC' в инженерном виде. HIEW занимает в этой тройке почётное/первое место, т.к. именно в нём мы и будем шифровать программу. Тонкости шифрования оставим на потом, а пока ознакомимся с самой оболочкой:
; fasm code....
org 100h
push Cryptor1 ; Расшифровываем всё тело
ret ; ..от метки 'start' и до подвала!
start:
;----------
about db 13,10,'Example of the dynamic encryption (c)R71MT'
db 13,10,'------------------------------------------'
db 13,10,'Type string..: $'
sizeMess db 13,10,'Programm size: $'
buff db 80,0,80 dup(0)
Cryptor2 db 050h,053h,053h,0BAh,002h,000h,031h,0C9h,030h,00Fh
db 043h,080h,0C1h,003h,039h,0C3h,072h,0F6h,04Ah,074h
db 007h,05Bh,0FFh,0D3h,05Bh,058h,0EBh,0EAh,0C3h,000h,000h,000h
length = $ - Cryptor2
align 16
Begin: ; Точка входа в программу
;-----------
mov bx,Message1
mov ax,end_Message1
call bp ; Вызов процедуры! (зовём декриптор(2))
mov bx,inpBuff
mov ax,end_inpBuff
call bp ;
mov bx,Message2
mov ax,end_Message2
call bp ;
xor ax,ax ; Ждём клавишу..
int 16h
int 20h ; Выход из com-программы!
dw 9090h
;-----------------------------------------------------------
;--------- П Р О Ц Е Д У Р Ы -------------------------------
;-----------------------------------------------------------
Message1: ;<---------; Вывести шапку на экран
mov ah,9
mov dx,about
int 21h
ret
end_Message1:
dw 1234h,8520h,0f0fh,9090h ; Мусор...
Message2: ;<---------; Вывести размер программы в байтах
mov ah,9
mov dx,sizeMess
int 21h
lea ax,[end_Start - start] ; АХ = размер программы
Hex2Asc:
xchg dx,ax ; Функция выводит на экран(АХ) в HEX-виде
mov cx,4
@@: shld ax,dx,4
rol dx,4
and al,0Fh
cmp al,0Ah
sbb al,69h
das
int 29h
loop @b
ret
end_Message2:
dw 0f0fh,9090h,1234h,8520h ; Мусор...
inpBuff: ;<---------; Ввод строки в буфер средствами DOS
mov ah,0Ah
mov dx,buff
int 21h
ret
end_inpBuff:
end_Start:
;--------------------------------------------------------------------------
;================== К О Н Е Ц П Р О Г Р А М М Ы =========================
;--------------------------------------------------------------------------
Cryptor1: ;<--------; Декриптор(1)
mov si,start ;
mov al,0A7h ; Ключ дешифратора
Decrypt:
xor byte[si],al ;
inc si ;
cmp si,end_Start ; Всё расшифровали?
jb Decrypt ;
end_Cryptor1: ;
sub al,al ; Самоликвидация! (деструкция)
mov di,Cryptor1 ;
lea cx,[end_Cryptor1 - Cryptor1]
rep stosb ;
copyCryptor2: ;<--------; Сразу копируем криптор(2) в стек!
sub sp,length ; Выделяем фрейм
mov bp,sp ; Запомним точку входа в него
mov si,Cryptor2 ;
mov di,bp ;
mov cx,length ;
push cx si ;
rep movsb ; Копируем строку из SI в DI
xor al,al ;
pop di cx ;
rep stosb ; Затираем строку опкодов нулями
jmp Begin ;<-------; На точку входа в основное тело..
;[...]
Вот пример готовой тушки, над которой можно по-эксперементировать.
Эта программа должна вывести на экран приветствие и запрос на ввод строки, которую просто сохранит в буфере. После ввода строки она должна показать размер своего тела в 16-тиричном формате и выйти в DOS по INT-20h. Помимо декрипторов, в ней 3 процедуры: 'Message1', 'Message2' (с вложеной Hex2Asc) и 'inpBuff' для ввода строки в буфер.
Здесь есть некоторые/умышленные ошибки, чтобы потом можно было на них указать, типа: "Так делать нельзя!". При этом программа на-ходу, и выполняет свою функцию. Если сейчас скопировать этот исходник в окно FASM'a и попытаться его скомпилировать, то компиляция пройдёт успешно, но полученный COM-файл не запустится, а досовская консоль просто подмигнёт нам и закроется. Думаю не трудно догадаться почему..
Исполнение кода начинается с того, что декриптор(1) начинает расшифровку всего тела ключом(A7h). Стоп! Так тело-то ещё не зашифровано! Получается что декриптор вместо расшифровки, наоборот шифрует тушку, поэтому программа и рушится ниже плинтуса. Такой-же подарочек нам приподнесёт и криптор(2) - начнёт шифровать ключами(0..3..6..) вместо расшифровки. Значит мы должны предварительно зашифровать программу, чтоб дать правильный старт дешифровщикам!
Вот тут-то и начинается самое интересное..
Значит шифрование готовой программы..
Как говорилось выше, при двойном шифровании порядок следования ключей роли не играет, если ключи одинаковые. Но это не наш случай! У нас разные алгоритмы шифрований. Это означает, что нужно придерживаться правил работы со-стеком: первым пришёл, последним уйдёшь.
В нашей программе первым дешифрует криптор(1) ключом(A7h), поэтому шифровать этим ключом мы должны в последнюю очередь. Сначала нужно зашифровать все процедуры, причём каждую из них отдельно, с начальным значением ключа(0). Уже потом зашифруем всё тело программы. Для процедур получится двойное шифрование, да ещё и с разными алгоритмами.
При шифровании нужно быть предельно осторожными, чтобы не зашифровать лишний байтик. Это может обломать нас по-полной. Нам нужно вычислить точный размер шифруемых процедур, и адрес их начала. Адрес конца не обязателен, т.к. есть длина. На всякий/пожарный нужно иметь ввиду, что все процедуры заканчиваются командой(RET/RETN), опкод которой имеет значение(С3h).
Тут произошёл небольшой конфуз с процедурами! Статья сырая и пишу её на-ходу, опираясь только на теорию не проверив на практике. Одним словом, нужно на входе в процедуры сохранять/восстанавливать содержимое регистра(DX), т.к. криптор(2) использует его в качестве счётчика. В идеале, нужно сохранять/восстанавливать вообще все регистры командами(Pusha/Popa), но здесь нам хватит и одного(DX). Причём внутри процедуры 'Message2' есть 'Hex2Asc', которая является отдельной процедурой. Её нужно обработать отдельно. Законченный вариант будет такой:
Message2:
push dx ; запомнить
mov ah,9
mov dx,sizeMess
int 21h
pop dx ; восстановить
mov si,start
lea ax,[end_Start - start]
Hex2Asc:
push dx ; запомнить
;[......]
pop dx ; восстановить
ret
end_Message2:
dw 0f0fh,9090h,1234h,8520h
Исправив мои косяки в трёх процедурах, двигаемся по-тропе шифрования дальше..
На подготовительном этапе удобно вставить в хвост исходника одну полезную/отладочную функцию, которая вычислит начальные адреса процедур и их длинну. Вставлять эту функцию нужно обязательно в хвост, иначе адреса изменятся и мы получим неверный результат:
;.......
copyCryptor2: ;<--------; Сразу копируем криптор(2) в стек!
sub sp,length ; Выделяем фрейм
mov bp,sp ; Запомним точку входа в него
;.......
rep stosb ; Затираем строку опкодов нулями
jmp Begin ;<-------; На точку входа в основное тело..
; ------------------------------------------------------------------
; C этого места начинается вспомогательная функция вычисления адресов
; Нужно закомментировать 'PUSH CRYPTOR1' в голове (после ORG 100h),
; изменив его на 'PUSH testFunc'.
; После отладки - изменения нужно будет восстановить.
testFunc:
call @f
db 13,10,'Proc count: 3'
db 13,10,'--------------------'
db 13,10,' Addr Size'
db 13,10,' Message1: $'
@@: pop dx
call outString ; Вывод мессаги
mov ax,Message1 ; Адрес функции
call Print ; Выводим АХ с тире
lea ax,[end_Message1 - Message1] ; Длина функции
call Hex2Asc ; Выводим АХ
call @f
db 13,10,' Message2: $'
@@: pop dx
call outString
mov ax,Message2
call Print
lea ax,[end_Message2 - Message2]
call Hex2Asc
call @f
db 13,10,' inpBuff: $'
@@: pop dx
call outString
mov ax,inpBuff
call Print
lea ax,[end_inpBuff - inpBuff]
call Hex2Asc
call @f
db 13,10,'--------------------'
db 13,10,'Full size: $'
@@: pop dx
call outString
mov ax,start
call Print
lea ax,[end_Start - start]
call Hex2Asc
xor ax,ax
int 16h
int 20h
outString:
mov ah,9
int 21h
ret
Print:
call Hex2Asc
mov al,'-'
int 29h
ret
Небольшие телодвижения избавили нас от ковыряния кода в поисках адресов. Обычно процедур в коде намного больше трёх и вычислять их адреса вручную явное самоубийство. Замечу, что текстовые строки хранятся здесь в потоке кода. Адрес их ПУШится, а потом ПОПится в регистр(DX) для fn.9h INT-21h. Таким образом сохраняются оригинальные смещения внутри программы и её размер. Вот результат работы этой/отладочной функции:
Proc count: 3
--------------------
Addr Size
Message1: 0220-000A
Message2: 0232-0029
inpBuff: 0263-000A
--------------------
Full size: 0104-0169
Получив нужную инфу избавляемся от этой функции в исходнике, и раскомментируем голову. Компилируем исходник по-новой, и получаем готовый файл, который будем шифровать.
Проделав всю/эту работу мы встречаем процесс шифрования во-всеоружии!
Загружаем готовый COM-файл в HIEW. Шифрование начинаем с любой из понравившихся процедур, ..пусть будет 'Message1'. Как видно из отчёта выше, эта процедура начинается с адреса(220h) и заканчивается адресом(22Аh). Сам адрес(22Аh) не входит в процедуру, т.к. отсчёт с нуля, поэтому адресом конца будет(229h) с опкодом RET(C3h).
Юзая HIEW нужно учитывать, что он считает адреса не с ORG-100h (как у нас в программе), а с ORG-0h, поэтому адрес(220h) он будет отображать как 120h. Значит загрузив программу в HIEW жмём в его окне такую последовательность клавиш: [F4->F3->F5->120->Enter] и попадаем сюда:
00000120: 52 push dx
00000121: B409 mov ah,009
00000123: BA0401 mov dx,00104
00000126: CD21 int 021
00000128: 5A pop dx
00000129: C3 retn
0000012A: 3412 xor al,012
0000012C: 20850F0F and [di][00F0F],al
00000130: 90 nop
00000131: 90 nop
У-гу.. Это как-раз процедура 'Message1' которая выводит шапку. Смотрим, что по адресу(129h) действительно находится(RET), значит мы на правильном пути. Ставим курсор на адрес(120h) и жмём последовательность: [F3->F7] или Edit->Crypt. В ответ, HIEW просит нас указать алгоритм шифрования в окне 'Assembler'. Отвечаем ему так:
xor cx,cx
xor al,cl
add cl,3
loop 2
..после этих команд, выходим по(Esc) из режима ввода алгоритма, и подтверждаем серьёзность наших намерений нажав(F7). HIEW перешёл в режим шифрования, для которого так-же юзается клавиша[F7].
Значит шифруем по(F7) все значения с адреса(120h), до адреса(129h) включительно. Зашифрованные значения меняют свой цвет на жёлтый, что означает ОК! Зашифровав последнее значение(С3h) в процедуре, сохраняем изменения клавишей(F9h). Всё.. Процедуру 'Message1' зашифровали! Зашифрованный код должен выглядеть так:
00000120: 52 push dx
00000121: B70F mov bh,00F
00000123: B308 mov bl,008
00000125: 0E push cs
00000126: DF34 fbstp t,[si]
00000128: 42 inc dx
00000129: D834 fdiv d,[si]
0000012B: 1220 adc ah,[bx][si]
0000012D: 850F test [bx],cx
0000012F: 0F909052B4 seto [bx][si][0B452]
Чтобы приступить к шифрованию следующей процедуры 'Message2', нужно полностью выйти из HIEW'а, и по-новой загрузить программу, чтобы задать шифровальщику опять начальное значение(0). Шифрование остальных процедур происходит по такой-же схеме. Адреса процедур в окне HIEW'а будут следующими:
- начало 'Message1': 220h - 100h = 0120h
- конец 'Message1': 220h + 00Ah = 022Ah - 0101h = 0129h
- начало 'Message2': 232h - 100h = 0132h
- конец 'Message2': 232h + 029h = 025Bh - 0101h = 015Ah
- начало ' inpBuff': 263h - 100h = 0163h
- конец ' inpBuff': 263h + 00Ah = 026Dh - 0101h = 016Ch
- начало 'FullSize': 104h - 100h = 0004h
- конец 'FullSize': 104h + 169h = 026Dh - 0101h = 016Ch
Зашифровав все/три процедуры одинаковым алгоритмом, приступаем к шифровке всего тела, чтобы программа на диске лежала в зашифрованном виде. Тут ничего нового, только вместо 'Edit->Crypt', нужно будет заюзать 'Edit->Xor' с ключом(А7h), т.к. криптор(1) внутри программы расшифровывает тело именно этим ключом. Значит перезапустив HIEW открываем нашу программу с зашифрованными процедурами и жмём последовательность клавиш:
[F4->F2->F5->4->Enter->F3->F8->A7->Enter]
Эта комбинация перевела HIEW в режим шифрования 'XOR' с ключом(А7). Процесс шифрования привязан к клавише(F8). Шифруем все значения от макушки с адресом(0004) и до хвоста с адресом(016Ch) включительно. Шифруемые значения меняют свой цвет с голубого на жёлтый. Достигнув адреса(016Dh) сохраняем изменения по(F9), и с чувством выполненного долга выходим из HIEW'a по(F10). Запустив на исполнение новоиспечённый/шифрованный файл обнаруживаем, что мы всё сделали правильно, о чём свидетельствует окно консоли.
Но бросать чепчики в воздух рановато! Этот мир дермово устроен, и как только файлик попадёт в руки более-менее опытного взломщика, такое шифрование не спасёт его от взлома. Поиск ошибок при шифровании может послужить темой для следующего разговора.
Давайте посмотрим, как-бы кодокопатели пытались расшифровать наш файл, какие ошибки мы допустили, чего не нужно было делать. Заметим, что в природе соотношение тех кто пишет программы, и кто их ломает - идёт примерно 50:1. Интересным фактом является и то, что взломщики в этой войне выигрывают.
В общем случае, бесполезно защищать программу, т.к. её всё-равно сломают. Для этого даже ломать ничего не нужно, а достаточно просто найти координаты защитного механизма в коде и забить его NOP'ами (no operation). Но такой подход не везде срабатывает. Например зашифрованный/основной код не забьёшь пустой операцией(NOP), т.к. программа потеряет свою функциональность, и взломщику приходиться подбирать ключ, чтобы расшифровать это тело.
Однобайтный ключ (как в нашем случае) может иметь максимум FFh = 256 значений. Чтобы найти из 256-ти именно наш ключ, взломщику потребуется всего-лишь перебрать все ключи в диапазоне 0..256. Учитывая скорость выполнения операций современными ЦП это не займёт много времени. Следуя ходу его мыслей, приведём возможный алгоритм вычисления им, нашего ключа шифрования.
Для DOS-программ, в качестве самой/юзерской скан-строки может выступить обычный вызов прерывания INT-21h, как наиболее/часто встречаемый. Опкодом этой команды является значение(CD21h), которое и будет служить сигнатурой для поиска. Читаем зашифрованный файл в свой буфер и начинаем последовательно ксорить его слова ключами 0..256. После каждого ксора проверяем результ на 'CD21h'. Вот и весь алгоритм:
mov si, addr ; Адрес буфера с зашифрованным файлом
mov cx, 0FFFFh ; Макс.значение ключа (CH,CL = FFh)
lodsw ; Берём в АХ слово из буфера
findKey:
mov bx,ax
xor bx, сx ; Ксорим его
cmp bx, 21CDh ; Это int-21h ?!
je okey ; Да - наш клиент!
dec ch ; Иначе: сл.ключ
dec cl ; ^^^^
jnz findKey ; Все ключи перепробовали? Нет - продолжить..
ret ; Да - Облом.
okey: ;<---------; Нашли ключ! (CL = ключ)
[.....]
Как видим - подобрать однобайтный ключ очень просто. Чем такое шифрование, лучше-уж никакого: пустая трата времени. Но посмотрим на вариант с 2-х и-более байтными ключами:
- 1 байт = 000000FFh = 256 вариантов ключей;
- 2 байта = 0000FFFFh = 65.535 вариантов ключей;
- 4 байта = FFFFFFFFh = 4.294.967.295 вариантов ключей;
- 8 байт = FFFFFFFFFFFFFFFFh = 18.446.744.073.709.551.615 ключей!
Разрядность регистров современных ЦП составляет 64-бит, а это целых 8 байт, которые можно обрабатывать за-раз! Да сюда можно поместить целую строку символов! В таблице выше видно, сколько вариантов ключей придётся перебирать взломщику, если мы заюзаем 8-байтный ключ для шифрования всего тела нашей программы криптором(1). Ясно, что наш/однобайтный ключ(A7h) здесь отдыхает..
От сюда вывод: что ключ шифрования должен быть как-минимум 4-байтным (32-бит). Всё-что нам нужно было сделать, так это брать в регистр(EAX) сразу по 4 байта из шифруемой программы, и ксорить их 32-битным ключом например так (благо HIEW позволяет это делать по F6):
lea ecx,[end_Start - start] ; Длина тушки в байтах
shr ecx,2 ; Разделим её на 4 (длина в DW'ордах)
mov esi,start ;
mov edi,esi ;
@@: lodsd ; Берём 4 байта из ESI
xor eax,77E3A853h ; Ксорим их 32-битным ключом!
stosd ; Сохраняем изменения
loop @b ; Мотаем цикл СХ-раз..
Здесь, (как любят говорить рук.государств) нашим партёрам придётся малость по-потеть, чтобы во-первых: выделить скан-строку, а во-вторых: поймать ключ шифрования. Мотаем это на ус...
Помимо разрядности ключа, атмосферу подряжают ещё множество заряжённых частиц, которые могут прервать наш полёт. Одной из них является хранение самого ключа(1) в открытом виде. Это огромная брешь в системе безопасности, которая не даёт нам спокойно спать. Открытый ключ просто прикрывает защиту фиговым листиком, обнажая при этом весь алгоритм шифрования. Нужно срочно куда-то спрятать этот/чёртов ключ, ..но куда? В любом случае нам нужно будет воспользоваться им при дешифровке, а наши партнёры смогут отловить этот момент в отладчике.
Редактировался R71MT (Сегодня 20:17:08)
Но не будем отчаиваться, а рассмотрим такой вариант...
Если в большой программе мы не можем найти тайник куда спрятать ключ, то может пусть этим тайником будет сам юзверь! Пусть он вводит пароль, а мы заюзаем его ввод вместо ключа, но с некоторыми поправками.
В клинических случаях, парольная защита программы строится по такому алгоритму. Юзер вводит пароль, который проверяется программой на валидность. Если пароль совпадает, то типа: 'Thank-you!', иначе: 'Fack-you!'. Этот пример в аккурат показывает, как нельзя делать! Толку от пароля, который опять нужно сравнивать с оригиналом? Можно пойти по-более достойному пути, который заключается в следующем..
1. Снимаем контрольную сумму всей тушки;
2. Сохраняем её в открытом виде;
3. Шифруем всю программу 32-битным ключом: DEAD6666h;
4. Читаем юзерский пассворд, и делаем из него ключ;
5. Дешифруем юзверским ключом тушку;
6. Считаем по-новой контрольную сумму получившегося тела;
7. Сравниваем две контрольные суммы;
8. Здесь (как-правило) Fack-you!
Таким образом мы избавляемся от хранения ключа в открытом виде.
До шифрования снимается CRC-программы, которая не несёт в себе ни йоты полезной инфы. Её можно хранить обнажённой. После шифрования секретным ключом контрольная сумма меняется, и её можно восстановить только дешифровав тело таким-же ключом, которым мы шифровали. Юзерский пасс от фонаря преобразуется в ключ, которым дефишруется тело. Если после этого ни один тушканчик не постадал, то контрольная сумма совпадёт, что будет означать ОК!
Простой алгоритм может вставить такие палки в колёса наших партнёров, что мало не покажется. Ключ шифрования мы использовали только один раз закрывшись в тёмной комнате от всех глаз. Его никто не знает кроме нас и он нигде не хранится. Не знает его и сама программа, которая тупо дешифрует тем, что мы ей подсунем, ..а подсунуть ей мы можем сколько? - точно: 4 млрд. вариантов ключей.
Реализуется это просто..
Допустим мы уже посчитали CRC не зашифрованного тела, который равен 8AE5h. На следующем шаге шифруем его секретным ключом, а внутри программы помещаем такую функцию:
;[....]
len equ (end_Start - start)/4 ; Длина в dword
len2 equ len*4 ; Длина в байтах
hash equ 8AE5h ; Хэш-сумма всей программы
Doberman: ;<------; Читаем юзерский пасс
xor eax,eax
sub ebx,ebx ; Здесь будет ключ из пасса
mov ecx,8 ; Длина ввода
pass: mov ah,1 ; Ввод с эхом средствами DOS
int 21h
and ax,0Fh ; Оставляем только мл.тетраду
shrd ebx,eax,4 ; Двигаем ключ в EBX
loop pass ; Читаем остальные символы юзверя
Decrypt: ;<------; Ключ шифрования в ЕВХ. Дешифруем тушку!
mov esi,start
mov edi,esi
mov ecx,len
@@: lodsd
xor eax,ebx ; Ксор ключом..
stosd
loop @b
GetCRC: ;<------; Перерсчитаем хэш-сумму в ВХ (2 байта)
push 0 0
pop eax ebx ; Сбрасываем регистры в нуль
mov ecx,len2
mov esi,start
@@: lodsb
add bx,ax ; Считаем сумму в ВХ
loop @b
Welcome: ;<------; Таможня..
cmp bx,hash ; Сверяем два хэша!
jne fack ; Если не совпали
call @f
db 'Thank-you!$' ; Иначе
@@: pop dx
call @prn
jmp Begin ;<------; На точку входа в программу!!!
fack: call @f ; Не верный пасс юзверя...
db 'Fack-you!$'
@@: pop dx
call @prn
ret
;-----------------------------------------------------------------
@prn: mov ah,9 ; Функция вывода сообщений
int 21h
ret
Здесь я попытался определить только основные моменты, большая часть которых осталась за-бортом. Не возможно предскать всех действий двух/враждующих сторон, т.к. нет определённого руководства ни для нас, ни для тех, от кого мы собираемся защищаться. Нужно по-возможности изобретать свой велосипед. И пусть он будет с квадратными колёсами и с седлухой выше руля, зато такого ни у кого не будет, и в этом наш выигрыш!
Можно спорить до хрипоты, какие алгоритмы лучше: готовые или самопальные, но последнее слово всегда остаётся за нами. Баян у нас на руках - как хотим, так и играем! (лишь-бы не забывать тискать пимпы иногда).