☭ ВАСМ.РФ

Временно подменяем WASM.RU
  • Вошли как ewoke
  • Последний визит: Сегодня 20:31:16

Объявление

#1 Сегодня 18:26:38

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Основы шифрования данных

ОСНОВЫ ШИФРОВАНИЯ ДАННЫХ
(с) 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)

#2 Сегодня 18:29:22

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Первое правило шифрования - не хранить пароли и ключи в явном/открытом виде!
Ну нет, так нет. Пойдём другим путём.. Будем снимать хэши с шифруемых процедур (в данном случае '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), и т.д. В любом случае - ключ не бросается в глаза, а это уже вери-гуд.

Но и такой алгоритм имеет свои недостатки..
Здесь мы дешифруем процедуры один раз. После запуска приложения, весь код в памяти уже расшифрован. В зашифрованном состоянии находится только файл на диске. Какой от этого толк? Взломщик снимает дамп памяти после запуска нашего приложения, и получает весь исходный код. Нам остаётся лишь с тоскою поглядывать, как он изучает наши алгоритмы.

Хорошие результаты даёт динамическое шифрование кода. Суть его в том, что все процедуры кода постоянно находится в зашифрованном состоянии как на диске, так и в памяти, и дешифруются только при их вызове. Как только вызываемая процедура отработает, она опять шифруется. В этом случае дамп памяти ничего не даст, и будет похож на кучу навоза. Реализация динамического шифрования на программном уровне требует немалых усилий, но зато оправдывает все надежды.

#3 Сегодня 18:30:49

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Рассмотрим пример динамического шифрования данных.
Чтоб не забивать пространство, я буду использовать вместо процедур только их имена, без содержимого. Здесь главное понять структурную схему, а хирургические тонкости могут быть у каждого свои. Вариантов много, я например выбрал такой, в котором задействовал 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' забивается в памяти нулями. Нужно сказать, что файл на диске остаётся неизменным и при следующем запуске - всё повторится. Все манипуляции проделываются только с копией файла в памяти.

#4 Сегодня 18:32:31

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

На второй криптор возложена более ответственная задача, поэтому он шифрует/дешифрует чуть-сложней (если можно так выразиться). Беру в качестве ключа начальное значение(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). Если идея с 'маткой' вам не приглянулась, или кажется трудно/реализуемой, то можно просто сгенерить один раз криптор в стек, и не затирая его оставить там болтаться. Тогда (за ненадобностью) можно затереть саму матку. Но это уже - на вкус и цвет..

#5 Сегодня 18:37:03

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Пришло время разобраться с тонкостями 'Криптора(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), который сразу-же копируется в рандомную область памяти (в данном случае в стек). На следующем этапе затирается оригинальная строка опкодов из секции данных, и программа переходит в режим готовности и выполнению команд юзверя.

#6 Сегодня 18:38:22

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Для продолжения экспериментов понадобится какая-нить оболочка. Попробуем сделать из неё полноценную программу с динамическим шифрованием процедур. Не буду сильно мудрить, а наваяю простенький вызов трёх процедур, которые и покажут всю подноготную замысла.

Из прикладных программ нам понадобится только компилятор '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..) вместо расшифровки. Значит мы должны предварительно зашифровать программу, чтоб дать правильный старт дешифровщикам!
Вот тут-то и начинается самое интересное..

#7 Сегодня 19:55:58

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Значит шифрование готовой программы..
Как говорилось выше, при двойном шифровании порядок следования ключей роли не играет, если ключи одинаковые. Но это не наш случай! У нас разные алгоритмы шифрований. Это означает, что нужно придерживаться правил работы со-стеком: первым пришёл, последним уйдёшь.

В нашей программе первым дешифрует криптор(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

Получив нужную инфу избавляемся от этой функции в исходнике, и раскомментируем голову. Компилируем исходник по-новой, и получаем готовый файл, который будем шифровать.
Проделав всю/эту работу мы встречаем процесс шифрования во-всеоружии!

#8 Сегодня 19:57:31

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Загружаем готовый 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). Запустив на исполнение новоиспечённый/шифрованный файл обнаруживаем, что мы всё сделали правильно, о чём свидетельствует окно консоли.

Но бросать чепчики в воздух рановато! Этот мир дермово устроен, и как только файлик попадёт в руки более-менее опытного взломщика, такое шифрование не спасёт его от взлома. Поиск ошибок при шифровании может послужить темой для следующего разговора.

#9 Сегодня 19:59:02

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Давайте посмотрим, как-бы кодокопатели пытались расшифровать наш файл, какие ошибки мы допустили, чего не нужно было делать. Заметим, что в природе соотношение тех кто пишет программы, и кто их ломает - идёт примерно 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)

#10 Сегодня 20:00:16

R71MT
Воин дзена
Зарегистрирован: 13-11-2016
Сообщений: 45

Re: Основы шифрования данных

Но не будем отчаиваться, а рассмотрим такой вариант...
Если в большой программе мы не можем найти тайник куда спрятать ключ, то может пусть этим тайником будет сам юзверь! Пусть он вводит пароль, а мы заюзаем его ввод вместо ключа, но с некоторыми поправками.

В клинических случаях, парольная защита программы строится по такому алгоритму. Юзер вводит пароль, который проверяется программой на валидность. Если пароль совпадает, то типа: '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

Здесь я попытался определить только основные моменты, большая часть которых осталась за-бортом. Не возможно предскать всех действий двух/враждующих сторон, т.к. нет определённого руководства ни для нас, ни для тех, от кого мы собираемся защищаться. Нужно по-возможности изобретать свой велосипед. И пусть он будет с квадратными колёсами и с седлухой выше руля, зато такого ни у кого не будет, и в этом наш выигрыш!

Можно спорить до хрипоты, какие алгоритмы лучше: готовые или самопальные, но последнее слово всегда остаётся за нами. Баян у нас на руках - как хотим, так и играем! (лишь-бы не забывать тискать пимпы иногда).

#11 Сегодня 22:04:50

Indy
Воин дзена
Зарегистрирован: Вчера
Сообщений: 9

Re: Основы шифрования данных

Что это вообще за стаф такой, какой сейчас дос, удалите это дерьмо мамонта big_smile

Редактировался Indy (Сегодня 22:05:27)


vx

Быстрое сообщение

Введите сообщение и нажмите Отправить

Подвал раздела

Powered by FluxBB