Форум программистов
 

Восстановите пароль или Зарегистрируйтесь на форуме, о проблемах и с заказом рекламы пишите сюда - alarforum@yandex.ru, проверяйте папку спам!

Вернуться   Форум программистов > Низкоуровневое программирование > Assembler - Ассемблер (FASM, MASM, WASM, NASM, GoASM, Gas, RosAsm, HLA) и не рекомендуем TASM
Регистрация

Восстановить пароль
Повторная активизация e-mail

Купить рекламу на форуме - 42 тыс руб за месяц

Ответ
 
Опции темы Поиск в этой теме
Старый 22.07.2016, 20:39   #21
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

В арсенале DOS/BIOS имеются некоторое кол-во прерываний, от которых нам 'не холодно, не жарко'. Можно закодировать их в опкоды, и выстроив в таблицу дёргать ими когда угодно. Опкодом инструкции 'INT' является байт со-значением 'CDh'. Кстати, если вам понадобится опкод какой-нибудь инстуркции, то получить его можно в отладчике. Вот пример в дзебуге:
Код:
C:\>debug
-a
1399:0100  mov ax,1234
1399:0103  int 20
1399:0105
-r
AX=0000  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1399  ES=1399  SS=1399  CS=1399  IP=0100   NV UP EI PL NZ NA PO NC
1399:0100    B83412        MOV     AX,1234
-t
AX=1234  BX=0000  CX=0000  DX=0000  SP=FFEE  BP=0000  SI=0000  DI=0000
DS=1399  ES=1399  SS=1399  CS=1399  IP=0103   NV UP EI PL NZ NA PO NC
1399:0103    CD20          INT     20
-
Здесь видно, что 'MOV AX' кодируется опкодом(B8h), после которого идёт значение '1234', записанное (по-соглашению Intel) в обратном порядке. Второй строкой я вызвал 'INT', опкодом которого является байт(CDh). Но вернёмся к безвредным прерываниям..
Код:
;-----------------------------------------------------------
; Вызов пустых прерываний ----------------------------------
;-----------------------------------------------------------
GetHashInterrupt:
   mov	  bx,21		    ; Всего номеров прерываний в таблице
   call  Random 	          ; 
   lea   di,[Opcode+1]     ; Указатель для записи номера прерывания
   mov	  si,IntTable       ; Указатель на таблицу
   add	  si,ax		   ; 
   lodsb                   ; Берём номер из таблицы
   stosb                   ; Подставляем его после INT
Opcode:                    ; 
   db    0CDh,0            ; Вызываем прерывание!
   ret

;Таблица с номерами прерываний
IntTable:
   db	   01h,08h,0Ah,0Bh,0Ch
   db	   0Dh,0Eh,0Fh,1Ch,28h
   db	   2Bh,2Ch,2Dh,70h,71h
   db	   72h,73h,74h,75h,76h,77h
..а здесь описание этих прерываний
Код:
 INT 01h   .  CPU-SINGLE-STEP;  (80386+) - DEBUGGING EXCEPTIONS
 INT 08h   .  IRQ0 - SYSTEM TIMER; CPU-generated (80286+)
 INT 0Ah   .  IRQ2 - LPT2/EGA,VGA/IRQ9; CPU-generated (80286+)
 INT 0Bh   .  IRQ3 - SERIAL COMMUNICATIONS (COM2); CPU-generated (80286+)
 INT 0Ch   .  IRQ4 - SERIAL COMMUNICATIONS (COM1); CPU-generated (80286+)
 INT 0Dh   .  IRQ5 - FIXED DISK/LPT2/reserved; CPU-generated (80286+)
 INT 0Eh   .  IRQ6 - DISKETTE CONTROLLER; CPU-generated (80386+)
 INT 0Fh   .  IRQ7 - PARALLEL PRINTER
 INT 1Ch   .  TIME - SYSTEM TIMER TICK
 INT 28h   .  DOS 2+ - DOS IDLE INTERRUPT
 INT 2Bh   .  DOS 2+ - RESERVED
 INT 2Ch   .  DOS 2+ - RESERVED
 INT 2Dh   .  DOS 2+ - RESERVED
 INT 70h   .  IRQ8 - CMOS REAL-TIME CLOCK
 INT 71h   .  IRQ9 - REDIRECTED TO INT 0A BY BIOS
 INT 72h   .  IRQ10 - RESERVED
 INT 73h   .  IRQ11 - RESERVED
 INT 74h   .  IRQ12 - POINTING DEVICE (PS)
 INT 75h   .  IRQ13 - MATH COPROCESSOR EXCEPTION (AT and up)
 INT 76h   .  IRQ14 - HARD DISK CONTROLLER (AT and later)
 INT 77h   .  IRQ15 - RESERVED (AT,PS); POWER CONSERVATION (Compaq)
Нашедшего выход - затаптывают первым..

Последний раз редактировалось R71MT; 22.07.2016 в 20:42.
R71MT вне форума Ответить с цитированием
Старый 22.07.2016, 20:45   #22
waleri
Старожил
 
Регистрация: 13.07.2012
Сообщений: 6,493
По умолчанию

Цитата:
Сообщение от R71MT Посмотреть сообщение
снимут дамп памяти и кранты шифрованию.
Расшифрована может быть только нужная инструкция, или на худой конец - блок.
waleri на форуме Ответить с цитированием
Старый 24.07.2016, 12:10   #23
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

Помимо описанных фишек, неплохой эффект дают и альтернативные способы вызовов самих прерываний. Под этим подразумевается не тупой вызов через классический INT, а через подставные регистры и переходы типа JMP/CALL/RET. Но для этого нужно знать, как работают оригинальные обработчики DOS, и что происходит при вызове INT.

Работа всех 86-ых процессоров основана на том, что на каждом шаге, ЦП считывает инструкцию с регистровой пары CS:IP и выполняет её. На следующем шаге сдвигается указатель(IP) и всё идёт по-кругу. Другими словами, содержимое CS:IP рассматривается как адрес выполняемой инструкции в формате [сегмент:смещение].

Выполнение прерывания(INT) начинается с того, что в стеке сохраняются 6 байт в таком порядке:
1. PUSHF - содержимое регистра флагов;
2. PUSH CS - адрес сегмента кода;
3. PUSH IP - адрес возврата (указатель на сл.инструкцию после INT).

Далее, в регистровую пару CS:IP считывается адрес обработчика прерывания из таблицы векторов, и ЦП начинает выполнять уже инструкции обработчика. После того как обработчик выполнит свою задачу, управление возвращается прерванной процедуре командой 'IRET' (внутри обработчика), которая и восстанавливает из стека значения флагов и регистров CS:IP. Проц продолжает выполнять наш код..

Теперь посмотрим, что происходит при вызове 'JMP/CALL/RET'..
Эти переходы бывают трёх типов: короткий, близкий, дальний, ..или Short/Near/Far.
Short и Near - это почти одно-и-тоже, поэтому мы рассмотрим только Near/Far-переходы. Эти операторы указывают ассемблеру на формат адресов, которые нужно подставить после инструкции перехода. Если тип JMP/CALL-переходов определён как дальний(Far), то адрес подставляется в формате [сегмент:смещение]. Если-же указан тип 'NEAR', то подставляется только смещение, что означает близкий переход.

Безусловный JMP-переход не требует 'адреса возврата', поэтому ничего не сохраняет в стеке. Вызов-же ближней(Near) процедуры через 'CALL' происходит по такому алгоритму: в стеке сохраняется только значение регистра(IP), т.н. адрес возврата, который на выходе из процедуры восстанавливается командой 'RETN' (Return Near) или просто 'RET'. Одним словом, при ближнем(CALL) в стеке сохраняется только смещение следующей инструкции, размером в 2 байта.

Соответственно, если наш код вызывает дальнюю процедуру, то мы должны указать 'CALL FAR', который на входе в процедуру сохранит в стеке регистровую пару CS:IP, как адрес возврата (т.е.вместе с сегментом, а это уже 4 байта). От сюда следует, что дальние процедуры должны заканчиваться по 'RETF' (Return Far), который снимет со-стека 4-байтный адрес возврата в пару CS:IP, и указатель вернётся в тот-же сегмент и точку, на которой остановился до вызова дальней процедуры.

Если 'call far' сохраняет адрес возврата вместе с сегментным адресом, то ведь это-же делает и вызов INT, о котором мы говорили вначале! Значит через 'call far' можно иммитировать классический вызов прерывания. Только нужно будет предварительно сохранить ещё и флаги. Пробуем..
Код:
;---------------------------------------------------------
; Иммитация дос fn(9) для вывода мессаги
;---------------------------------------------------------
vec21h    dw  0,0                  ; место под вектор 21h
;-----------------
    mov   ax,3521h                 ; читать вектор 21h 
    int   21h                      ;
    mov   [vec21h],bx              ; запомним смещение,
    mov   [vec21h+2],es            ;   ..и сегмент обработчика 21h

    mov   ah,9                     ; передаём параметры
    mov   dx,message               ;
    pushf                          ; сохраняем флаги в стеке
    call  far dword[vec21h]        ; сохраняем CS:IP в стеке, 
                                   ;     ..и переходим по вектору
    int   20h                      ; (сюда указывал CS:IP)
;...
Здесь видно, что в стек идут флаги и адрес возврата. Это всё правильно.. Но где инструкция, которая снимала-бы их от туда? Дело в том, что мы используем стандартный обработчик DOS. Он и снимет адрес возврата последней командой в своём теле - 'IRET'. Если-бы мы писАли свой обработчик прерывания, тогда вся ответственность за возврат ложилась-бы на нас.

В коде выше я получаю вектор нужного прерывания дос-функцией(35h), но можно считать его и напрямую с таблицы векторов, которая лежит по адресу [0:0]. Каждый вектор занимает там по 4 байта, в которых и лежат адреса обработчиков в формате [смещение:сегмент], т.е. расположены наоборот:
Код:
C:\>debug
-d 0:0
0000:0000  68 10 A7 00 - 8B 01 70 00 - 16 00 96 03 - 8B 01 70 00    h.....p.......p.
0000:0010  8B 01 70 00 - B9 06 0C 02 - 40 07 0C 02 - FF 03 0C 02    ..p.....@.......
0000:0020  46 07 0C 02 - 0A 04 0C 02 - 3A 00 96 03 - 54 00 96 03    F.......:...T...
0000:0030  6E 00 96 03 - 88 00 96 03 - A2 00 96 03 - FF 03 0C 02    n...............
-
Здесь видно, что обработчик прерывания(0) валяется в памяти по адресу: 00A7:1068h.
Чтобы найти адрес нужного вектора в этой таблице, нужно номер прерывания умножить на 4. Например, вектор прерывания(21h) лежит по адресу: 21h*4=84h; вектор(16h) будет иметь адрес: 16h*4=58h, и т.д:
Код:
org 100h
jmp  start

mess      db  'Hello World!$'    ;
old16h    dd  0                  ; место под вектор(16h)
old21h    dd  0                  ; место под вектор(21h)

start:                           ;
    push  ds 0                   ; 
    pop   ds                     ; DS = 0 (сегмент IVT)
    mov   si,84h                 ; DS:SI = 0000:0084h
    mov   di,old21h              ; ES:DI = приёмный буфер
    lodsd                        ; читаем 4 байта с вектора(21h)
    stosd                        ; сохраняем их в буфере
    mov   si,58h                 ;
    mov   di,old16h              ;
    lodsd                        ;
    stosd                        ; сохраняем вектор(16h)
    pop   ds                     ;

    mov   ah,9                   ;
    mov   dx,mess                ;
    pushf                        ;
    call  dword[old21h]          ; INT-21h!

exit:                            ;
    xor   ax,ax                  ;
    pushf                        ;
    call  dword[old16h]          ; INT-16h!
    ret                          ; выход из программы..
Теперь наш код приобрёл вид, в котором напрочь отсутствуют вызовы прерываний INT.
На следующем шаге можно избавиться и от CALL-переходов, заменив их на простые джумпы. Но тут есть проблема.. Нам нужно будет сохранить регистр(IP), а он отвергает любое-к-себе обращение. Его нельзя не заПУШить, не заМОВить, не заПОПить - поэтому придётся извращатся подручными методами, например так..

Мы знаем, что указатель(IP) всегда указывает на следущую инструкцию кода. Если поставить после вызова какую-нить метку, то она в аккурат будет попадать под указатель(IP). Остаётся заПУШить эту метку, и всё..
Код:
[....]
    pushf                        ; сохраняем флаги
    push  cs                     ; сохраняем CS
    push  pause                  ; сохраняем IP
    jmp   dword[old21h]          ; зовём обработчик INT-21h!
pause:                           ;
    xor   ax,ax                  ;
    pushf                        ; это-же и для INT-16h!
    push  cs                     ; 
    push  exit                   ;
    jmp   dword[old16h]          ; (..ждём клавишу)
exit:                            ;
    ret                          ; выход из программы..
Ладно.. Мы избавились от 'INT/CALL', так давайте избавимся и от 'JMP'!
Выше говорилось, что инструкции 'RET/RETN' восстанавливают указатель(IP) со-стека, который поместил туда 'CALL'. В свою очередь 'RETF' восстанавливает(CS:IP), которые поместил в стек 'CALL FAR'. Но если учесть, что этот 'RET' не только снимает со-стека адрес возврата, но ещё и передаёт на него управление, то возможности наши расширяются.
Нашедшего выход - затаптывают первым..
R71MT вне форума Ответить с цитированием
Старый 24.07.2016, 12:11   #24
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

Если положить в стек какой-нибудь адрес, а потом снять его командой 'RET', то мы получим обычный JMP-переход. Посмотрим на такой участок кода:
Код:
[....]
    push  next            ; заносим адрес возврата
    inc   di              ; мусор..
    ret                   ; jmp next
    xor   dx,dx           ; эта ветка никогда не получит управление
    dec   bp              ; ^^^
next:                     ;
    push  100h            ; 
    ret                   ; уходим на начало com-файла
[....]
Это пример с близким(Near) переходом, алгоритм которого не подходит для вызова прерываний. Давайте изменим его на FAR-переход, для которого нужен 'RETF'. Значит сохраняем в стеке такую конструкцию:
- флаги;
- сегмент адреса возврата (CS);
- смещение адреса возврата (IP)
- сегмент обработчика прерывания из таблицы векторов (CS);
- смещение обработчика из таблицы векторов (IP)
- вызываем прерывание через RETF !;

На практике это выглядит так:
Код:
;fasm code...
org 100h
push  start
ret

mess      db  'Hello World!$'

start:                       ;
    mov   ah,9               ;
    mov   dx,mess            ;

    push  0                  ;
    pop   es                 ; сегмент таблицы векторов

    pushf                    ; флаги
    push  cs                 ; адрес возврата
    push  next               ; ^^^
    push  word[es:86h]       ; сегмент,
    push  word[es:84h]       ;   ..и смещение обработчика 21h
    retf                     ; зовём INT-21h!
next:                        ;
    xor   ax,ax              ; то-же для INT-16h...
    pushf                    ;
    push  cs                 ;
    push  exit               ;
    push  word[es:5ah]       ;
    push  word[es:58h]       ;
    retf                     ;
exit:                        ;
    ret                      ; выход из COM-программы.
Таким образом, мы одним махом избавились сразу от трёх инструкций: 'INT/CALL/JMP', и готовы-бы избавится и от чёртова-'RET', но ведь кто-то должен передавать управление на другие ветки. Поэтому на этом и остановимся.

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

На данный момент мы имеем 4 варианта вызовов прерываний: через INT/CALL/JMP/RETF.
Теперь можем каждый вариант выделить в отдельную процедуру и вызывать их рандомом в случайном порядке, как и остальные функции, описанные в этом посте.
Нашедшего выход - затаптывают первым..
R71MT вне форума Ответить с цитированием
Старый 26.07.2016, 11:20   #25
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

В продолжении разговора хотелось-бы затронуть тему шифрования..
Шифрование в простой форме - обычная операция '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 вне форума Ответить с цитированием
Старый 26.07.2016, 11:21   #26
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

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

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

Хорошие результаты даёт динамическое шифрование кода. Суть его в том, что все процедуры кода постоянно находится в зашифрованном состоянии (на диске, в памяти), и дешифруются только при их вызове. Как только вызываемая процедура отработает, она опять шифруется. В этом случае дамп памяти ничего не даст, и будет похож на кучу навоза, разгребать которую мало-кто захочет. Реализация динамического шифрования на программном уровне требует немалых усилий, но зато оправдывает все надежды.
Нашедшего выход - затаптывают первым..
R71MT вне форума Ответить с цитированием
Старый 28.07.2016, 19:55   #27
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

Рассмотрим пример динамического шифрования данных.
Чтоб не забивать пространство, я буду использовать вместо процедур только их имена, без содежимого. Здесь главное понять структурную схему, а хирургические тонкости могут быть у каждого свои. Вариантов много, я например выбрал такой, в котором задействовал 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' забивается в памяти нулями. Нужно сказать, что файл на диске остаётся неизменным и при следующем запуске - всё повторится. Все манипуляции проделываются только с копией файла в памяти.
Нашедшего выход - затаптывают первым..
R71MT вне форума Ответить с цитированием
Старый 28.07.2016, 19:56   #28
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

На второй криптор возложена более ответственная задача, поэтому он шифрует/дешифрует чуть-сложней (если можно так выразиться). Беру в качестве ключа начальное значение(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). Если идея с 'маткой' вам не приглянулась, или кажется трудно/реализуемой, то можно просто сгенерить один раз криптор в стек, и не затирая его оставить там болтаться. Тогда (за ненадобностью) можно затереть саму матку. Но это уже - на вкус и цвет..
Нашедшего выход - затаптывают первым..
R71MT вне форума Ответить с цитированием
Старый 30.07.2016, 17:29   #29
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

Пришло время разобраться с тонкостями 'Криптора(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), который сразу-же копируется в рандомную область памяти (в данном случае в стек). На следующем этапе затирается оригинальная строка опкодов из секции данных, и программа переходит в режим готовности и выполнению команд юзверя.
Нашедшего выход - затаптывают первым..
R71MT вне форума Ответить с цитированием
Старый 30.07.2016, 17:30   #30
R71MT
Участник клуба
 
Аватар для R71MT
 
Регистрация: 16.06.2011
Сообщений: 1,428
По умолчанию

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

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


Купить рекламу на форуме - 42 тыс руб за месяц

Опции темы Поиск в этой теме
Поиск в этой теме:

Расширенный поиск


Похожие темы
Тема Автор Раздел Ответов Последнее сообщение
Полиморфизм Anubys Помощь студентам 1 26.12.2011 20:42
Полиморфизм Zorgan Visual C++ 22 29.08.2011 12:23
Полиморфизм MasterSporta Общие вопросы C/C++ 3 10.04.2011 23:46
полиморфизм slayerblya Общие вопросы C/C++ 1 27.02.2011 01:43
Полиморфизм mister2010 Общие вопросы C/C++ 30 24.05.2010 01:07