Защита файлов программами pkzip/pkunzip — Архив WASM.RU

Все статьи

Защита файлов программами pkzip/pkunzip — Архив WASM.RU

Как создать защищенный файл, используя pkzip

  Популярная утилита сжатия данных pkzip(pkunzip-распаковщик) позволяет не только уменьшать размер файла(ов), но и зашифровывать данные. Например, для установки защиты на файл my_file.txt вы можете запустить pkzip со следующими ключами:

  pkzip -s protect.zip my_file.txt

  После этого pkzip запросит у Вас пароль(дважды) и после всех подтверждений файл будет упакован с установленной защтой. При последующих попытках открыть файл protect.zip pkunzip будет предлагать ввести пароль, и если тот окажется неверен, то Вы (или кто-то другой) вместо содержимого файла скорее всего получите сообщение "PKUNZIP: (W14) Warning! Incorrect password for file: MY_FILE.TXT".

  Примечание: речь идет о dos-версии 2.06 ("PKUNZIP (R) FAST! Extract Utility Version 2.06 01-24-94"). Однако методы, изложенные в данном документе, применимы с минимальными ограничениями и к файлам, созданным WinZip.

Методика исследования защиты

  Для анализа защиты pkzip-а будем использовать как программные средства (отладчик), так и некий документ APPNOTE.TXT - в нем содержится очень подробное описание (видимо, от авторов) алгоритмов pkzip и pkunzip (на английском языке). Я нашел этот документ по адресу http://www.halyava.ru/document/txt_form.htm. Сразу скажу, что многое о защите pkzip можно почерпнуть из этого файла, но некоторые тонкости и непосредственную реализацию алгоритмов - только путем анализа выполнимого кода. Поэтому сначала приводится исследование pkunzip без помощи APPNOTE.TXT, и только после того, как будут обнаружены все базовые процедуры защиты, будет приведен конспект из этого документа. Наименования процедур, данных будут сопоставлены с наименованиями, данными авторами APPNOTE.TXT.

  Далее на основе полного знания о алгоритме защиты будет проведена попытка оценить его стойкость. Исходя из этой оценки появится возможность "взлома" в случае успешной словарной атаки и будет приведен ее алгоритм.

  Поскольку pkzip является dos-программой, то будем "играть по правилам" и выберем в качестве инструмента исследования dos-отладчик td.exe.

  Разумно также будет привести поверхностный анализ защиты вообще без каких-либо инструментов (отладчиков, дизассемблеров...).

Предварительный анализ (без отладчика)

  Попытаемся получить информацию на основе анализа файлов, упакованных и незащищенных (защищенных) паролем, используя обычный редактор файлов.

  Создадим файл из одного символа. Упакуем его как с паролем, так и без него. Сразу обратим внимание на тот факт, что файлы различаются по размеру ровно на 12 байт (у меня размеры файлов составили 115 и 127 байт). Сравнив побайтно содержимое обоих файлов, обнаруживаем, что они отличаются наличием в защищенном файле блока из 12 байт, находящихся сразу после имени упакованного файла (имя файла находится по смещению 001Eh), тогда как в незащищенном файле следом за именем следует символ из упакованного файла. Естественно, что в защищенном файле нет этого символа, по крайней мере после 12-ти байтного блока (предполагаем, что файлы отличаются только вставкой 12-ти байт).

  Проверим эту версию. Допустим, эти 12 байт (назовем их Hash) относятся к за/расшифровке файла, тогда как следом за ними начинается упакованная информация. Выбор названия достаточно произволен и связан со следующими соображениями: полагаем вначале, что алгоритм проверки пароля pkunzip заключается в получении от введенного пользователем пароля некоторой хэш-суммы и сравнения ее с хэш-суммой от оригинального пароля, сохраненной в файле.

  Действительно, если изменить любой байт из Hash и попробовать получить упакованный файл используя верный пароль, то получим сообщение о неверном пароле. А если изменить первый байт после Hash, то файл "нормально" распакуется, только первый символ в нем окажется отличным от исходного.

  Итак, мы получили некий ориентир при анализе защиты - это 12 байт Hash. Если при разборе pkzip/pkunzip нам встретится код, работающий с ними, то можно с большой вероятностью отнести его к блоку за/расшифровки.

Исследования с помощью отладчика

  Будем исследовать как программа pkunzip расшифровывает информацию и на основании чего выдает сообщение о неверном пароле при помощи отладчика td.exe. Однако нам придется выполнить некоторые подготовительные действия, прежде чем мы сможем работать в отладчике обычным способом: ставить точки останова и т.д., поскольку pkunzip обработан утилитой сжатия PKLITE (в этом можно убедиться, если посмотреть на первые байты pkunzip в редакторе) а также содержит некий цикл расшифровки кода распаковщика PKLITE.

  Если загрузить распаковщик в отладчике ("td pkunzip -s protect.zip"), и протрассировать несколько первых его команд, то довольно скоро можно увидеть некий цикл расшифровки блока кода:

; cx=0796h (MagValue?), dx=0138h
; si=di=03E4h, ds=es=cs
0164:
  nop, nop
  dec  dx	; Уменьшить счетчик циклов
  jz   0178     ; Расшифровка закончена ?
  mov  ax,[di]	; Получить слово кода
  sub  di,2     ; Указать на предидущее слово кода
  xchg cx,ax	
  xor  ax,cx    ; Расшифровать слово кода
  mov  [si],ax	; И записать обратно
  sub  si,2
  jmp  0164
0178:

  Итак, этот цикл расшифровывает код, находящийся в интервале начальный сегмент:[0178h..03E4h]. Причем пока его работа не закончена полностью, мы не можем ставить точки останова (F4) в этом интервале, поскольку цикл расшифровки уничтожит эти точки, и мы будем вынуждены трассировать этот довольно значительный цикл пошагово F7(F8). Также мы не сможем ставить точки останова прямо в оригинальный код pkunzip, поскольку он станет доступен только после его распаковки подпрограммой PKLITE. Рассмотрим более детально, как это происходит: дождемся сначала расшифровки кода от адреса 0178:

; Самокопирующая процедура PKLITE
0178:
  pop dx, pop es, push es
  inc  byte ptr [018B] ; Заменить retf 018B -> retf
  mov  cx,012Dh
  xor  di,di, push di, mov si,018Ch, cld
  rep  movsw ; Копировать тело распаковщика PKLITE в последний сегмент
018B:
  retf 8CFD ; Перейти на код распаковки

  Таким образом, мы видим, как стартовый цикл расшифровки декриптует код PKLITE, который в свою очередь перебрасывает свое тело в сегмент после упакованного оригинального кода pkunzip и переходит на него. Поскольку при исследовании pkunzip предполагается многократная его загрузка в отладчике, то проще не выполнять каждый раз процедуру декриптования, а сразу (небольшой программкой, повторяющей действия декриптора) расшифровать код, не забыв удалить код декриптора (он исказит нам расшифрованный код). Алгоритм примерно следующий: найти в pkunzip начало зашифрованного кода (байты E6h,59h,BCh,.. по смещению в файле 00D8), декриптовать их аналогично оригинальному декриптору, дезактивировать код декриптора (например, заменить команду декриптора mov [si],ax на два nop'а: 89h,04h->90h,90h).

  Если после такого изменения pkunzip'а загрузить его в отладчик, то можно сразу ставить breakpoint на любую команду PKLITE с адреса 0178 кроме модифицирцуемой команды retf 8CFD - например, на rep movsw.

  Трассируя далее уже код PKLITE переходим вслед за ним в последний сегмент и видим, как он начинает распаковывать код pkunzip:

; Начало кода PKLITE
0000: 
  std
  mov  bx,ds ; <- seg PSP
  push bx
  ...

  Трассировать код PKLITE также довольно утомительное занятие. Хотелось бы сразу установить прерывание в то место, когда его код полностью отработал и осталось только перейти на распакованный код pkunzip. Как правило, межсегментный переход реализуется с помощью команд retf, jmp far, причем он должен располагаться сразу после всех циклов распаковки PKLITE. Действительно, пролистав вниз код, обнаруживаем такой переход:

; Завершение кода PKLITE
020E: 
  xor  bx,bx
  mov  cx,bx, mov dx,bx, mov bp,bx, mov si,bx, mov di,bx
021A:
  retf ; Переход на оригинальный код pkunzip
  ...

  Теперь достаточно устанавливать breakpoint на этот retf, чтобы разом пропускать всю работу PKLITE.

  Получив таким образом возможность "обычным образом" исследовать в отладчике pkunzip, приступим к определению механизма проверки пароля и расшифровки данных. Задача сводится к поиску в значительном потоке выполняющихся команд тех, которые непосредственно относятся к проверке пароля и (желательно для полноты картины) расшифровке данных. Выбор варианта решения зависит в некоторой степени от вкусов исследователя, я же остановлюсь на последовательной локализации интересующего кода ориентируясь на характерные для него проявления.

  Так, например, нас интересует код проверки пароля. Делаем предположение, что он находится между кодом, получающим пароль от пользователя и выдачей сообщения о неверном пароле (если специально ввести таковой). Так, к примеру, после перехода на распакованный PKLITE'ом код pkunzip мы видим следующий порядок вызовов процедур:

; В основном сегменте(cs) pkunzip после его распаковки
cs:4BE2: 
  mov  ah,30h
  int  21h
  ...
4C83:
  call 5718
  call 558E
  xor  bp,bp
  ...
  call 20B6
  ...
  call 4DB0
  ret

  Как видно, перед нами серия call-ов, видимо отвечающих стартовому коду программы. Пройдем все вызовы командой F8 (Step over - не заходя в код подпрограммы). Оказывается, подпрограммы 5718, 558E и 4DB0 "ничего" не делают, в то время как 20B6 запрашивает у нас пароль, выдает сообщение о неверном пароле и проч. Поэтому мы можем на данном этапе локализовать область исследований подпрограммой 20B6 и в следующей итерации исследовать только ее код. Далее аналогично исследуя набор вызовов этой подпрограммы:

; Внутри процедуры 20B6 pkunzip
20B6:
  ...
  call 1EEC
  ...
  call 70D4:0004 ; Дальний вызов из вспомогательного модуля
  ...
  call 071E
  ...

  Обнаруживаем, что процедура 071E запрашивает пароль - в первом приближении интересующий нас код находится внутри 071E или далее. Установить способ получения пароля можно следующим способом: простая резидентная программа, перехватившая некий набор функций прерывания int 21h или int 16h должна "всплыть", если pkunzip будет использовать одну или несколько функций из этого набора. В данном случае pkunzip'ом используется функция 08h int 21h. Сжимая далее круг поиска, обнаруживаем следующий интересный кусок кода:

; Проверка правильности пароля в pkunzip
1880:
  call 7566
  les  bx,[bp+4], test byte ptr es:[bx+0A],8
  jz   1893
  mov  al,es:[bx+0F]
  jmp  189B
1893:
  mov  ax,es, mov si,bx, mov al,es:[si+15h]
189B:
  sub  ah,ah, mov [bp-6],ax, mov al,[BCA9], cmp ax,[bp-6]
  jz   18B2
  lea  bx,[04A9] ; bx->"Incorrect password for file..."
  ...
  call 0136

  Процедура 0136 выдает сообщение в случае неверного пароля, в то время как в случае верного пароля выполняется переход на метку 18B2. Анализируя условный переход, делаем вывод, что после подпрограммы 7566 инициализируется дальний указатель на некоторую структуру (es:bx) и выполняется сравнение между элементом этой структуры (либо +0F, либо +15h) и некоторой глобальной переменной [BCA9]. Что же представляют собой используемые в данном месте программы элементы структуры ? Простейшим действием является проверка на наличие в zip-файле элементов этой структуры. Действительно, байты структуры со смещением 0F и 15h совпадают с байтами файла по смещению 11h и 17h соответственно ! А в элементе со смещением 0Ah, похоже, находится какой-то битовый флаг, управляющий выбором тестового байта.

  Итак, небольшая часть кода проверки пароля известна. Однако стоило ли исследовать машинный код, если, как уже было сказано в начале, многое о защите можно было почерпнуть из документа APPNOTE.TXT ? Приведем цитату, относящуюся к описанию защитного механизма и сравним ее непосредственно с обнаруженным алгоритмом:

; Описание структуры заголовка файлов *.zip и алгоритма проверки пароля
; из APPNOTE.TXT
...
A. Local file header:

   local file header signature     4 bytes  (0x04034b50)
   version needed to extract       2 bytes
   general purpose bit flag        2 bytes
   compression method              2 bytes
   last mod file time              2 bytes
   last mod file date              2 bytes
   crc-32                          4 bytes
   compressed size                 4 bytes
   uncompressed size               4 bytes
   filename length                 2 bytes
   extra field length              2 bytes

...After the header is decrypted,  the last 1 or 2 bytes in Buffer
should be the high-order word/byte of the CRC for the file being
decrypted, stored in Intel low-byte/high-byte order.  Versions of
PKZIP prior to 2.0 used a 2 byte CRC check; a 1 byte CRC check is
used on versions after 2.0.  This can be used to test if the password
supplied is correct or not...

  Итак, из документа следует, что для проверки пароля необходимо сравнить последний байт(слово) некоего расшифрованного буфера с старшим байтом(словом) crc, содержащимся в заголовке файла. Однако мы видим, что в зависимости от некоторго бита (по всей видимости, находящимся в элементе "general purpose bit flag" заголовка) сравнение может быть проведено совсем не с crc, а с старшим байтом времени ("last mod file time")...

  Остается найти код, формирующий "второй параметр" сравнения - байт в ячейке [BCA9]. В отладчике td нет возможности установить "breakpoint at memory access" (точку прерывания при обращении к памяти), но можно применить все тот же метод "последовательной локализации", только индикатором будет служить изменение (визуальное) ячейки [BCA9]. Для этого необходимо указать отладчику в окне "data" отображать указанную ячейку и, сделав это в момент начала выполнения кода pkunzip'а, следить за ней во время трассировки. Разумным предположением в смысле экономии усилий будет начать поиск с "близлежащего" от места проверки правильности пароля - а именно, подпрограммы 7566. Именно в ней мы и находим сначала следующий код, использующий пароль:

; Подпрограмма использования пароля для изменения им 12 байтного буфера (buff12)
7554:
  mov  al,[si] ; ds:si-> введенный пароль (си-строка)
  call add_seg:07CA
  inc  si
  cmp  byte ptr [si],0
  jnz  7754
; А также подпрограмма модификации байтом(в al) 12 байтного буфера (buff12)
add_seg:082A:
  cli, xor ecx,ecx
  movzx edx,al ; al= очередной символ пароля
  push di, push ds, pop es, cld, mov di,0BDF4h
  mov   eax,[di]
  xor   dl,al
  shr   eax,8
  xor   eax,cs:[edx*4+ecx]
  stosd
  mov   dl,al
  mov   eax,[di]
  add   eax,edx
  imul  eax,eax,08088405h
  inc   eax
  stosd
  shr   eax,18h
  mov   edx,[di]
  xor   al,dl
  shr   edx,8
  xor   edx,cs:[eax*4+ecx]
  mov   [di],edx
  sti, pop di, retf

  Грубо говоря, каждый символ пароля последовательно передается на вход процедуре add_seg:082A, которая использует его для последовательного изменения трех двойных слов в буфере [0BDF4]. К моменту начала добавления пароля буфер инициализируется значениями 12345678h, 23456789h и 34567890h (оригинальный код не приводится). Мы назовем эти три двойные слова "buff12". При этом используется добавочная таблица размером в 100h двойных слов (см. команды типа xor eax,cs:[edx*4+ecx], размер следует из того, что индекс (edx) всегда меньше 256). Небольшой промах компилятора (xor ecx,ecx, use ecx как базу) не помешает нам понять расположение этой таблицы в памяти - а именно, в сегменте кода дополнительного модуля. Видимо, к основной программе был подключен модуль с функциями криптования, выполненными в far call стиле и активно использующими инструкции 386 процессора. Действуя так же, как и при поиске вышеприведенного кода, пытаемся обнаружить процедуру инициализации добавочной таблицы (назовем ее XOR-таблицей) - хотя бы ориентируясь на факт ее заполнения. Если проявить чрезмерную осторожность и начать следить за ее изменением с самого начала работы программы, то можно обнаружить любопытный факт: таблица заполняется дважды и оба раза ... неодинаково ! Первый раз (оригинальный код пропущен) это делается на базе инструкций 8086 процессора, далее следует проверка текущего типа CPU и таблица переинициализируется с использованием инструкций 386 процессора:

; Подпрограмма инициализации XOR-таблицы.
Build_XorTable proc near
  std
  mov  di,offset XorTable+400h-4h
  mov  dx,0FFh
  mov  ebx,0EDB88320h
@@FillTable:
  mov   cx,8
  movzx eax,dx
@@Loop8:
  shr   eax,1
  sbb   esi,esi
  and   esi,ebx
  xor   eax,esi
  dec   cx
  jnz   @@Loop8
  stosd
  dec   dx
  jns   @@FillTable
  retn
Build_XorTable endp

  Можно предположить гипотетическую ситуацию: файл был заархивирован с паролем на машине 8086, а расшифровывается на 386-ой. Тогда он ... не может быть расшифрован !

  Чтож, любопытно, однако перейдем к финальной части проверки пароля. Нам осталось выяснить, как формируется байт в [BCA9] и как при этом используется вышеприведенный набор из трех двойных слов, измененных паролем. Прежде всего замечаем, что немного ранее кода изменения двойных слов buff12 (метка 7554) в буфер [BCA9-(0Ch-1)] заносятся те самые двенадцать байт из zip-файла (Hash), которые после изменения двойных слов паролем также модифицируются подпрограммой 2938:

; Подпрограмма использования файлового Hash после использования пароля.
2938: push bp, mov bp,sp...
2948:
  mov  ax,[BDFC] ; Фактически, младший байт третьего двойного слова
  or   al,2
  mov  bx,ax
  xor  bl,1
  mul  bx
  mov  al,ah
  xor  ah,ah
  mov  es,bp
  xor  al,es:[di] ; es:di->[BCA9-(0Ch-1)]=Hash
  stosb
  call add_seg:07CA ; Опять изменить buff12 байтом из al
  dec  si
  jnz  2948

  Таким образом Hash преобразуется (дешифруется) и последний байт его сравнивается либо с старшим байтом crc, либо со старшим байтом временем файла.

  Последнее, что осталось рассмотреть - это способ расшифровки потока данных. При его поиске можно, например, опираться на следующие предположения: вероятно, код расшифровки начинает работать после проверки правильности пароля (условный переход на метку 18B2), вероятно, что расшифровкой занимаются подпрограммы дополнительного модуля, аналогичные или совпадающие с приведенными выше - как только в программе встретится дальний вызов подпрограммы из этого модуля, это должно вызвать подозрения. Собственно говоря, процедура расшифровки Hash сама по себе уже является кандидатом на "расшифровщик" потока данных. Действительно, если "на всякий случай" установить breakpoint в ее начало , то он сработает ! Дампируя содержимое памяти по адресу в es:di (адрес дешифруемых байт), идентифицируем их с байтами файла, лежащими следом за Hash файла: процедура дешифровки данных - это тот код, который ранее расшифровал Hash.

Итоговое описание алгоритма проверки пароля с выдержками из авторского описания (APPNOTE.TXT)

  Итак, приводим описание алгоритма проверки пароля и дешифровки данных.

  Сначала программа запрашивает пароль. Это осуществляется при помощи функции 08h прерывания 21h. Для нас важно то, что эта функция не позволяет вводить символы с следующими кодами: 0,3,8,13,16,19,27.

  Инициализируются три двойных слова следующими значениями: 12345678h, 23456789h и 34567890h:

...
Step 1 - Initializing the encryption keys
-----------------------------------------
Key(0) <- 305419896
Key(1) <- 591751049
Key(2) <- 878082192

  При рассмотрении защиты под отладчиком Keys мы назвали buff12.

  Используя пароль, изменяем эти Keys (оригинальный код приведен выше и носит название "Подпрограмма использования пароля для изменения им 12 байтного буфера (buff12)" и "Подпрограмма модификации байтом 12 байтного буфера (buff12)"):

loop for i <- 0 to length(password)-1
    update_keys(password(i))
end loop

Where update_keys() is defined as:

update_keys(char):
  Key(0) <- crc32(key(0),char)
  Key(1) <- Key(1) + (Key(0) & 000000ffH)
  Key(1) <- Key(1) * 134775813 + 1
  Key(2) <- crc32(key(2),key(1) >> 24)
end update_keys

Where crc32(old_crc,char) is a routine that given a CRC value and a
character, returns an updated CRC value after applying the CRC-32
algorithm described elsewhere in this document.

  Способ получения crc32(XOR-таблицы) приведен выше и носит название "Подпрограмма инициализации XOR-таблицы".

  Дешифруется буфер из 12 байт, расположенных в zip-файле сразу за именем упакованного файла (ранее мы дали ему название Hash):

Step 2 - Decrypting the encryption header
----------------------------------------
The purpose of this step is to further initialize the encryption
keys, based on random data, to render a plaintext attack on the
data ineffective.

Read the 12-byte encryption header into Buffer, in locations
Buffer(0) thru Buffer(11).

loop for i <- 0 to 11
    C <- buffer(i) ^ decrypt_byte()
    update_keys(C)
    buffer(i) <- C
end loop

Where decrypt_byte() is defined as:

unsigned char decrypt_byte()
    local unsigned short temp
    temp <- Key(2) | 2
    decrypt_byte <- (temp * (temp ^ 1)) >> 8
end decrypt_byte

  Далее осуществляется проверка правильности пароля. Для этого осуществляется сравнение последнего, 12-го расшифрованного байта и старшего байта crc32 (лежит по смещению 14 в файле) упакованного файла, если третий бит байта ("general purpose bit flag", лежит по смещению 6 в файле) сброшен, иначе берется страший байт времени (смещение 10).

  Дешифрация данных производится аналогично расшифровке 12-ти байт. Поток защищенных данных (сначала упакованных, затем шифрованых) подается на вход процедуре дешифровки аналогично потоку 12-ти байт безо всяких дополнительных действий.

Словарная атака

  Обсудим возможность нахождения неизвестного пароля к произвольному защищеному zip-файлу.

  Итак, перед нами следующая последовательность действий pkunzip'а при расшифровке файла:

  - Получение пароля от пользователя;

  - Инициализация Key, изменение Key паролем;

  - Использование Key для расшифровки "12-byte encryption header";

  - Сравнение последнего расшифрованного байта и crc(времени) файла. Если они совпадают, пароль признается pkunzip'ом верным.

  - Использование Key для расшифровки данных в файле.

  Вначале исследуем последнию стадию - расшифровку данных. Ведь конечной целью является получение не пароля, а самих данных. Входными данными для алгоритма дешифрации является поток данных из файла и Key в некотором произвольном состоянии.

  Здесь возможны два варианта, зависящие от нашего знания о шифруемой информации. Если мы не обладаем никакими данными о информации, то тут мы зашли в тупик. Допустим, однако, мы имеем некоторое представление о том, что зашифровано в файле. Откуда мы его можем получить ? Сначала необходимо понять, что она представляет собой. Вспомним, что основная функция pkunzip - это упаковка файлов и информация может быть сначала упакована и затем закриптована, либо наоборот. Оказывается, pkunzip криптует упакованные данные ! В случае значительного по объему входного файла первыми байтами упакованных данных будет что-то вроде начала таблицы символов для распаковки. В общем случае pkunzip может использовать до восьми методов сжатия ! Интерес представляет метод stored - сохранение информации без сжатия. Этот метод может быть применен в случае небольшого объема информации либо принудительно по желанию пользователя. Метод упаковки можно получить из поля "compression method" заголовка файла (0 - The file is stored). Не будем подробно рассматривать алгоритм атаки в этом маловероятном случае и перейдем к рассмотрению возможности получения пароля.

  Прежде всего замечаем, что все множество различных шифровок любой информации совпадает количественно с множеством состояний 12 байт Key, которые можно получить, используя все множество доступных для ввода паролей. Максимальная длина пароля составляет примерно 64 символа, что, несмотря на некоторое количество запрещенных для ввода символов, дает огромное в сравнении с числом всех возможных вообще состояний Key (256^12). Интуитивно ясно, что если алгоритм изменения Key паролем не содержит явных ошибок, то множество возможных состояний Key по порядку величины совпадает с числом комбинаций 12 байт (256^12). Даже если число возможных состояний Key, полученных от всего множества возможных паролей составит 256^8 ~ 10^19, то, даже имея однозначное правило проверки пароля (или соответствующего ему состояния Key), невозможно за приемлимое время перебором всех вариантов обнаружить верный пароль. Если же число возможных состояний состовляет, например, 256^5 ~ 10^12 вариантов, то при скорости проверки паролей в 10^5 паролей в секунду имеет смысл выполнять такой подбор. Необходимо только знать критерий проверки пароля, оставляющий конечное число кандидатов в пароли (чтобы можно было проверить их вручную за конечное время) - например, 10000 или немногим более.

  Вряд ли, однако, число состояний Key состовляет менее 256^6, что делает бессмысленным полный перебор всех возможных паролей (Key) даже при наличии критерия проверки пароля. Косвенным доказательством служит следующий опыт. Будем подавать на вход процедуре update_keys различные пароли и следить за выпадением первых четырех байт (двойное слово) в интервале от 0 до 255 (или любом другом). Например, в моем опыте подавались пароли из символов от "A" и до конца таблицы длиной от единицы и выше. В результате после подачи ~7*10^8 таких паролей выпало не менее ~40 Key со значением первого двойного слова в интервале 0..255, из чего можно сделать оценку (очень грубо), что существует не менее 40/256 ~1/8 состояний Key от максимально возможного (256^12).

  Альтернативой алгоритму перебора является получение пароля по следам его использования. В нашем случае пароль используется единожды при изменении начального состояния Key и единственное место, если не считать расшифровку данных, где он проявляет свои свойства - это заданное значение последнего байта расшифрованного "encryption header".

  Поток состояний Key, сформированных множеством возможных паролей, способен образовать практически псведосучайные последовательности в виде расшифровки encryption header, следовательно, примерно на каждые 256 входящих паролей один окажется верным с точки зрения алгоритма проверки. Следовательно, даже если бы число возможных состояний Key было конечным, порядка 256^5, мы бы смогли уменьшить число возможных Key не более чем в 256 раз. Вот если бы мы знали исходное состояние encryption header, то мы бы могли практически однозначно проверить любой пароль, либо попытаться получить пароль по результату шифрования исходного состояния. Авторами подчеркивается, что исходное состояние encryption header - это псевдосучайный набор байт.

  Так ли это на самом деле ? Проверим это утверждение и найдем отладчиком код, формирующий случайные байты encryption header. Для этого нам придется трассировать уже pkzip (модуль упаковки), правда, по сходной схеме с pkunzip. Вот этот код:

; Процедура формирования случайных байт header
; es=0, ds:bx=header(в начале все байты равны 0)
; в si - псевдослучайное число
  and  si,07FEh
  mov  cx,56h
  mov  dx,07FEh-8
  jmp  short @@L6034
@@L6032:
  sub  si,dx
@@L6034:
  cmp  si,dx
  jnb  @@L6032
  lods word ptr es:[si]
  sbb  [bx],ax
  lods word ptr es:[si]
  adc  [bx+2],ax
  lods word ptr es:[si]
  sbb  [bx+4],ax
  lods word ptr es:[si]
  adc  [bx+6],ax
  lods word ptr es:[si]
  sbb  [bx+8],ax
  lods word ptr es:[si]
  adc  [bx+0Ah],ax
  loop @@L6034

  В чем смысл этого кода ? header представляется в виде шести слов, изначально равных нулю, каждое из которых командами sbb или adc модифицируется 86 раз. Команды sbb(adc) вычитают(добавляют) с учетом флага переноса содержимое памяти в регионе 0000h...0800h. В этом регионе памяти находится таблица векторов прерываний (первые 400h байт), затем идут области данных BIOS и DOS). Значения слов региона зависят от установленных драйверов, версии OS. Но самое главное, что в этой области находится счетчик тиков системного таймера и буфер клавиатуры, которые используются примерно в 50% случаев. Правда, существует выделенный случай, когда в точности известна конфигурация машины, на которой была осуществлена упаковка.

  Таким образом, в общем случае невозможно обращение пароля по следам его использования и невозможен полный перебор всех вариантов паролей (Key). Разумеется, необходимо проверить выделенные случаи: например, первый зашифрованный файл в архиве - распространенный read.me и т.д. Следовательно, следует вывод о том, что единственным возможным путем атаки является перебор ограниченного числа паролей с расчетом на то, что искомый пароль окажется среди рассматриваемых - атака по словарю.

Как организовать словарную атаку ?

  Ключевым моментом является определение критерия проверки правильности пароля. Как было сказано выше, критерий, применяемый программой для выдачи сообщения о неверном пароле (сверка последнего байта расшифрованного encryption header с crc или временем файла) уменьшит число проверяемых паролей не более чем в 256 раз, что явно мало для более или менее солидной подборки паролей.

  Но что же произойдет, если мы перебором возможных паролей найдем такой "верный" пароль ? Достаточно перебрать пароли числом >> 256, чтобы найти пароль, удовлетворяющий критерию программы. Попытаемся расшифровать им защищенный файл. Сообщения о неверном пароле не появилось, но зато pkunzip выдаст сообщение о неверном crc распакованного файла ! В ряде случаев может появиться сообщение о том, что разрушена таблица символов или еще что-то в этом роде. Оказывается, распаковав файл, pkunzip проверит его crc. Причем используется crc32. Вероятность совпадения для такого crc составляет 1/2^32, что практически однозначно при проверке паролей числом 256^4 определит нужный. Плюс стандартный критерий уменьшит число паролей в 256 раз, но и это еще не все ! Что будет, если pkunzip начнет распаковывать неверно расшифрованные данные ? Наверняка в их структуре будет что-то не так, и это тоже является критерием проверки пароля ! Так, например, при методе упаковки "deflating" первый бит упакованных данных - признак последнего блока, а если следующие два бита равны единице, то это является признаком разрушенного блока.

  Итак, алгоритм словарной атаки примерно ясен: получаем очередной пароль, изменяем им key, расшифровываем encryption header, сверяем последний байт. Если сравнение успешно, то распаковываем файл так же, как это делает pkunzip, при ошибках отбрасываем пароль. Считаем crc файла и сверяем его с эталонным. Этим методом мы из каждых 256^5 или более паролей оставим примерно один "верный".

  Осталась реализация. Очевидно, что универсальный алгоритм подбора должен включить в себя все методы распаковки, которые может использовать pkunzip. Это довольно утомитльно, в результате мы напишем еще один распаковщик. Единственным его достоинством будет относительно большая скорость распаковки, достигаемая за счет буферизации данных. А если шифрованный файл более 64K, то будет необходимо держать несколько буферов (речь идет о DOS) и на этом пути pkunzip постепенно догоняет нас. Так не лучше ли задачу распаковки и проверки crc возложить на него, а пароли подбирать только с точки зрения стандартнго критерия ?

  Действительно, pkunzip поддерживает возможность передать пароль в командной строке ! Например, текущий проверяемый пароль - "TEST". Для его проверки необходимо запустить pkunzip следующим образом:

  pkunzip -sTEST -o test.zip

  "-o" означает записывать распаковываемый файл поверх старого, если таковой уже имеется, символы после "-s" трактуются как пароль. Правда, нельзя передавть принципиально возможные пароли с символами " " (пробел) или "-". Следовательно, необходимо выполнять запуск pkunzip'а из подборщика, например с помощью функции ax=4B00h прерывания int 21h. Ну а как же получить результат проверки от работающего pkunzip'а ? Понятно, что он выдаст предупреждение на дисплей, но ведь речь идет о массовых запусках. Оказывается, в случае ошибочной распаковки pkunzip устанавливает значение errorlevel (код возврата) в отличное от нуля значение. Получить код возврата после выполнения программы можно через функцию ah=4Dh прерывания int 21h.

  Быстродействие ? На компьютере P-120 составляет около 3000 вариантов в секунду, что позволит просмотреть словарь объемом 3 миллиона слов всего за 1000 секунд или 20 минут.

  Небольшие тонкости. В ряде случаев в архиве лежит более одного файла. В этом случае можно применять ключ "-x", позволяющий отбирать нужные. Также в случае работы под dos архивы, содержащие файлы с длинными именами, вызывают проблемы с оценкой кода возврата. В этом случае достаточно сократить имена вручную прямо в файле (в двух местах), не сдвигая остальные данные. Увеличить скорость перебора более чем в два раза можно перенаправлением вывода pkunzip'а с экрана на фиктивное устройство ("> nul").

Исходник здесь.

  (C) Chingachguk /HI-TECH

2002-2013 (c) wasm.ru