Ассемблер под Windows для чайников. Часть 8
Доброго вам здравия, о покорители оконного ассемблера! Надеюсь, вы уже разобрались с пройденным материалом и готовы к очередному занятию. В шестой части я обещал вам подробнее рассказать о применении диалоговых окон. О них сегодня и пойдет речь. Напишем простейший шифровальщик данных на основе диалогового окна. Прошу заметить, что "простейший" не всегда означает "быстрейший". Ввиду этого наша сегодняшняя программа вряд ли претендует на роль в криптографии огромных массивов или видеофайлов, однако, думаю, вполне справится с шифрованием небольших текстовых документов с паролями и секретными данными на ожидаемом уровне.
include 'win32ax.inc'
include 'encoding\WIN1251.INC'
MAXSIZE = 260
PASSIZE = 8
.data
ernopass db 'Введите пароль!',0
erdifpass db 'Пароль и подтверждение не совпадают!',0
erfsize db 'Файл слишком большой!',0
erfname db 'Файл не выбран!',0
bigfile db 'Файл довольно большой!',13,10
db 'Шифрование может занять много времени.',0
success db 'Успешно завершено!',0
extname db '.crpt',0
errtxt db 'Код ошибки: %u',0
errbuf rb $-errtxt+10
fname1 rb MAXSIZE
fname2 rb MAXSIZE
pass1 rb PASSIZE+1
pass2 rb PASSIZE+1
buf db ?
passlen dd ?
hfile1 dd ?
hfile2 dd ?
temp dd ?
ofn OPENFILENAME sizeof.OPENFILENAME,,,,,,,fname1,MAXSIZE,,,,, OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST + OFN_EXPLORER
.code
start:
invoke GetModuleHandle,0
mov [ofn.hInstance],eax
invoke DialogBoxParam,eax,IDD_MAIN,HWND_DESKTOP,DialogProc,0
invoke ExitProcess,0
proc DialogProc hwnddlg,msg,wparam,lparam
push ebx esi edi
cmp [msg],WM_INITDIALOG
je .wminitdialog
cmp [msg],WM_COMMAND
je .wmcommand
cmp [msg],WM_DROPFILES
je .drop
cmp [msg],WM_CLOSE
je .wmclose
xor eax,eax
jmp .finish
.wminitdialog:
mov eax,[hwnddlg]
mov [ofn.hwndOwner],eax
invoke SendDlgItemMessage,[hwnddlg],ID_PASS1,EM_LIMITTEXT,PASSIZE,0
invoke SendDlgItemMessage,[hwnddlg],ID_PASS2,EM_LIMITTEXT,PASSIZE,0
jmp .processed
.wmcommand:
cmp [wparam],ID_OPEN
je .open
cmp [wparam],ID_EXIT
je .wmclose
cmp [wparam],ID_CRYPT
jne .finish
;=crypt=
cmp [fname1],0
je .erfname
invoke GetDlgItemText,[hwnddlg],ID_PASS1,pass1,PASSIZE+1
cmp [pass1],0
je .ernopass
invoke GetDlgItemText,[hwnddlg],ID_PASS2,pass2,PASSIZE+1
invoke lstrcmp,pass1,pass2
test eax,eax
jne .erdifpass
invoke lstrlen,pass1
mov [passlen],eax
invoke CreateFile,fname1,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0
cmp eax,-1
je .erfname
mov [hfile1],eax
invoke GetFileSize,eax,0
cmp eax,-1
je .erfilesize
test eax,eax
je .erfilesize
mov ebx,eax
cmp eax,500000h ;5MB
jb .size_ok
invoke MessageBox,[hwnddlg],bigfile,0,MB_OKCANCEL
cmp eax,IDOK
jne .hclose
.size_ok:
invoke lstrcpy,fname2,fname1
invoke lstrcat,fname2,extname
test eax,eax
je .erfname
invoke CreateFile,fname2,GENERIC_WRITE,FILE_SHARE_READ,0,CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,0
cmp eax,-1
je .erfname
mov [hfile2],eax
.crypt_cycle:
invoke ReadFile,[hfile1],buf,1,temp,0
test eax,eax
je .error
xor edx,edx
mov eax,ebx
div [passlen]
add edx,pass1
mov al,[edx]
xor [buf],al
invoke WriteFile,[hfile2],buf,1,temp,0
test eax,eax
je .error
dec ebx
test ebx,ebx
jne .crypt_cycle
invoke CloseHandle,[hfile1]
invoke CloseHandle,[hfile2]
invoke SetFileAttributes,fname1,FILE_ATTRIBUTE_NORMAL
invoke DeleteFile,fname1
test eax,eax
je .error
invoke MoveFile,fname2,fname1
test eax,eax
je .error
invoke MessageBox,[hwnddlg],success,0,0
jmp .processed
.ernopass:
invoke MessageBox,[hwnddlg],ernopass,0,0
jmp .processed
.erdifpass:
invoke MessageBox,[hwnddlg],erdifpass,0,0
jmp .processed
.erfname:
invoke MessageBox,[hwnddlg],erfname,0,0
jmp .processed
.erfilesize:
invoke MessageBox,[hwnddlg],erfsize,0,0
jmp .hclose
.error:
invoke GetLastError
invoke wsprintf,errbuf,errtxt,eax
invoke MessageBox,[hwnddlg],errbuf,0,0
.hclose:
invoke CloseHandle,[hfile1]
invoke CloseHandle,[hfile2]
jmp .processed
.drop:
invoke DragQueryFile,[wparam],0,fname1,MAXSIZE
jmp .new_file
.open:
invoke GetOpenFileName,ofn
test eax,eax
je .processed
.new_file:
invoke SetDlgItemText,[hwnddlg],ID_NAMESTRING,fname1
jmp .processed
.wmclose:
invoke EndDialog,[hwnddlg],0
.processed:
mov eax,1
.finish:
pop edi esi ebx
ret
endp
.end start
section '.rsrc' resource data readable
IDD_MAIN = 100
ID_NAMESTRING = 101
ID_PASS1 = 102
ID_PASS2 = 103
ID_OPEN = 104
ID_CRYPT = 105
ID_EXIT = 109
directory RT_DIALOG,dialogs
resource dialogs,\
IDD_MAIN,LANG_RUSSIAN+SUBLANG_DEFAULT,main_dialog
dialog main_dialog,'Простейший Шифровальщик',240,120,190,175,WS_CAPTION+WS_POPUP+WS_SYSMENU+ DS_MODALFRAME,WS_EX_ACCEPTFILES
dialogitem 'STATIC','&Файл:',-1,10,10,70,8,WS_VISIBLE
dialogitem 'EDIT','',ID_NAMESTRING,10,20,170,13,WS_VISIBLE+WS_BORDER+ ES_AUTOHSCROLL+ES_READONLY
dialogitem 'STATIC','&Пароль:',-1,10,50,70,8,WS_VISIBLE
dialogitem 'EDIT','',ID_PASS1,10,60,60,13,WS_VISIBLE+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL
dialogitem 'STATIC','&Подтверждение:',-1,10,80,70,8,WS_VISIBLE
dialogitem 'EDIT','',ID_PASS2,10,90,60,13,WS_VISIBLE+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL
dialogitem 'BUTTON','&Открыть',ID_OPEN,10,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON
dialogitem 'BUTTON','&Шифровать',ID_CRYPT,85,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON
dialogitem 'BUTTON','В&ыход',ID_EXIT,135,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_PUSHBUTTON
enddialog
Пробежимся взглядом по секции данных. MAXSIZE — максимальное количество символов в полном имени файла вместе с путем. PASSIZE — максимальная длина пароля без учета нуль-терминатора. Содержимое extname добавляется к полному имени для создания временного файла. В описании структуры ofn опущенные параметры отсутствуют — это допустимо, необходимо лишь обозначить их запятыми, если за ними следуют инициализируемые параметры. Диалоговое окно, как можно понять из кода, является упрощенным вариантом создания окна. Напомню параметры функции DialogBoxParam:
1) идентификатор исполняемого модуля программы;
2) идентификатор ресурса, содержащего шаблон диалогового окна;
3) дескриптор окна-владельца;
4) указатель на процедуру диалогового окна;
5) значение, передаваемое диалоговому окну в параметре lParam сообщения WM_INITDIALOG.
Для вызова DialogBoxParam нам достаточно указать в первом параметре значение, возвращаемое функцией GetModuleHandle, во втором — шаблон окна, в четвертом — процедуру обработки сообщений. Третий параметр у нас — константа HWND_DESKTOP, которая равна нулю и указана лишь для наглядности. Шаблон окна содержится в секции ресурсов и описывается аналогично прочим ресурсам. Тип ресурса — RT_DIALOG. Объявляется макроинструкцией dialog, за которой следует некоторое количество элементов, обозначаемых макросом dialogitem, завершаемое инструкцией enddialog. Макроинструкция dialog может иметь до 11 параметров, первые 7 из которых обязательные:
1. Имя ресурса.
2. Заголовок окна в кавычках.
3, 4, 5, 6 — соответственно X, Y — ширина, высота.
7. Стиль окна.
8. Расширенный стиль (необязательный параметр).
9. Идентификатор меню окна (необязательный параметр).
10. Имя шрифта в кавычках (необязательный параметр).
11. Размер шрифта (необязательный параметр).
Инструкция dialogitem имеет 8 обязательных параметров и один допустимый:
1. Имя класса элемента в кавычках.
2. Текст элемента в кавычках, или идентификатор ресурса (например, ID картинки для элемента класса STATIC со стилем SS_BITMAP).
3. Идентификатор элемента (используется в функциях API).
4, 5, 6, 7 — соответственно X, Y, ширина и высота элемента.
8. Стиль.
9. Расширенный стиль (необязательный параметр).
Шаблон диалога может содержать любое количество элементов или вообще ни одного, но обязательно должен завершаться макросом enddialog. Вызов функции DialogBoxParam надолго передает управление процедуре DialogProc. В данном случае нам не надо организовывать цикл получения сообщений от окна. Операционная система будет крутить этот цикл самостоятельно, а нашей процедуре останется лишь обеспечить обработку входящих сообщений. Если на каком-то этапе нам понадобится закрыть диалоговое окно и продолжить выполнение команд, следующих за вызовом функции DialogBoxParam, то мы прямо из процедуры обработки сообщений вызовем функцию EndDialog для завершения функции и передачи управления следующей команде. Она имеет 2 параметра: дескриптор приговоренного диалогового окна; значение, которое вернет функция DialogBoxParam при успешном завершении (в случае неудачи DialogBoxParam вернет -1, поэтому во избежание двусмысленности сами не отправляйте -1 в этом параметре). После обработки сообщения наша процедура должна поместить в eax единицу и выполнить команду возврата из процедуры (ret). Если сообщение не было обработано процедурой, то в eax должен быть помещен ноль — тогда операционная система сама выполнит требуемые действия по обработке сообщения. Функция DialogBoxParam создает окно по указанному шаблону и перед самой публикацией его на экране отправляет процедуре диалога сообщение WM_INITDIALOG, содержащее в lParam значение, переданное нами в пятом параметре DialogBoxParam. Это значение может использоваться программистом для передачи каких-либо данных диалоговому окну в момент его инициализации. В нашем примере мы ничего не передаем, но по получении сообщения WM_INITDIALOG копируем дескриптор окна в элемент hwndOwner структуры ofn (сразу оба операнда команды MOV не могут являться указателями на память, поэтому копируем через привилегированный регистр EAX за две команды). Также отправляем обоим полям для ввода пароля сообщения EM_LIMITTEXT.
Для отправки сообщений элементам диалогового окна служит функция SendDlgItemMessage. В первых двух параметрах этой функции указываются дескриптор диалогового окна и идентификатор элемента-получателя. В остальном функция аналогична функции SendMessage. Сообщение EM_LIMITTEXT устанавливает максимально допустимое число вводимых символов для элемента класса EDIT. Первый параметр сообщения — максимальное число символов, второй не используется. Сообщение WM_DROPFILES было рассмотрено в четвертой статье. По приходу сообщения от элемента ID_OPEN (кнопка "Открыть"), вызывается диалог открытия файла. Если после него возвращается ноль — значит, файл не был выбран. В этом случае — прыжок на метку .processed и возврат. Если же пользователь выбрал файл — сползаем на метку .new_file. Метка здесь для того, чтобы функция SetDlgItemText выполнялась и при открытии файла через диалог открытия, и при дропе файла на окно. Данная функция похожа на сообщение WM_SETTEXT. Она предназначена для установки нового текста элемента диалогового окна. Параметры: дескриптор диалога; идентификатор элемента; указатель на текстовую строку с нулем на конце. На метку .wmclose мы попадаем при получении сообщения WM_CLOSE или при получении сообщения WM_COMMAND от элемента ID_EXIT (кнопка "Выход"). Тут все предельно ясно: получили приказ на закрытие — вызываем упоминавшуюся выше функцию EndDialog.
Теперь переходим, так сказать, к ядру нашей программы. Тут нас и поджидает масса новых неизвестностей. Когда мы проверяем последнее из возможных сообщений от элементов диалога — ID_CRYPT, — то помещаем ноль в EAX и прыгаем на .finish, если это не оно, чтобы функция операционной системы самостоятельно обрабатывала не предусмотренные нашей программой сообщения. Иначе — делаем вывод, что сообщение пришло от кнопки "Шифровать", и начинаем процедуру шифрования. Сразу мы сравниваем первый байт содержимого fname1 с нулем. Если файл был выбран, то там должен находиться первый символ его полного имени, ну, а если нет — там будет ноль, и мы переходим на сообщение об ошибке "Файл не выбран!" Функция GetDlgItemText аналогична сообщению WM_SETTEXT и является противоположностью функции SetDlgItemText. Она возвращает текст заданного элемента диалога в указанный буфер. Ее параметры: дескриптор диалога; идентификатор элемента; указатель на буфер для размещения текста; максимальное количество символов включая завершающий ноль. Максимальная длина пароля у нас равна константе PASSIZE, поэтому в качестве последнего параметра мы указываем PASSIZE+1 с учетом завершающего строку нуля. Сравнивая первый байт содержимого pass1 с нулем, мы проверяем, был ли введен хотя бы один символ пароля. Если пароль был введен — получаем содержимое поля для подтверждения пароля в переменную pass2. При помощи функции lstrcmp (ее мы проходили еще на втором занятии) сравниваем строки pass1 и pass2. Если возвращается не ноль — значит, пароль и подтверждение не совпадают, и мы прыгаем на соответствующую ошибку. В противном случае, используя функцию lstrlen, мы помещаем в переменную passlen количество введенных символов пароля: нам необходимо будет знать, из скольких символов состоит введенный пользователем пароль. Как же будет происходить шифрование выбранного файла? Самый простой способ шифрования — использование команды xor. Эта команда изначально предназначена для других вещей, но как нельзя лучше подходит для нашей сегодняшней задачи. Для начала давайте познакомимся поближе с основными арифметическими и логическими командами ассемблера:
add — складывает два операнда и помещает результат в первый операнд. Синтаксис: add op1,op2
inc — прибавляет к операнду единицу. Синтаксис: inc op1
sub — вычитает второй операнд из первого. Результат сохраняется в первом операнде. Синтаксис: sub op1,op2
dec — вычитает из операнда единицу. Синтаксис: dec op1
mul — умножает операнд на регистр AL/AX/EAX в зависимости от размерности операнда (байт/слово/двойное слово). Результат помещается соответственно в AX/DX:AX/EDX:EAX. То есть, если вы хотите умножить 3 на 5, то задвигаете пятерку в AL и умножаете на тройку:
mov al,5
mul 3
div — делит AX/DX:AX/EDX:EAX (в зависимости от размерности операнда) на операнд. Частное помещается соответственно в AL/AX/EAX, а остаток — в AH/DX/EDX. Пример деления мы разберем ниже по тексту.
Думаю, вы знаете, что каждый байт данных в ЭВМ представлен в виде блока из восьми бит — восьми элементов, каждый из которых имеет лишь два состояния: выключен и включен (0 и 1). Логические команды выполняют логические операции над битами операндов.
and — логическое "И": соответствующие биты первого операнда устанавливаются в единицу только в том случае, если в обоих операндах они равны единице. Иначе — в ноль.
or — логическое "ИЛИ": соответствующие биты первого операнда устанавливаются в ноль только в том случае, если в обоих операндах они равны нолю. Иначе — в единицу.
xor — логическое "ЛИБО" (логическое исключающее ИЛИ): соответствующие биты первого операнда устанавливаются в единицу только в том случае, если бит одного из операндов равен единице, а другого — нолю. Если соответствующие биты операндов равны, то бит первого операнда устанавливается в ноль.
not — логическое "НЕ": биты операнда инвертируются. Ноль меняется на единицу, единица — на ноль. Второй операнд не используется.
test — логическое "И" без изменения операндов: выполняется операция and, но результат не записывается в операнд, а выставляет флаг нуля (ZF) в ноль, если хотя бы одна пара соответствующих битов обоих операндов совпала в значении единицы. Команду cmp eax,0 удобно и выгодно заменять командой test eax,eax: результат тот же, зато выполняется за три такта процессора (против четырех в случае использования cmp) и занимает на один байт меньше памяти.
С флагом нуля есть небольшая путаница: если ZF=0 — значит, флаг нуля не установлен, значит, результат не ноль. Если же результат ноль — флаг ZF установлен, то есть ZF=1. Все становится более понятным при использовании команд перехода JZ (100% синоним JE) — перейти, если ноль (флаг нуля установлен); JNZ (100% синоним JNE) — перейти, если не ноль (флаг нуля не установлен).
Команда cmp после сравнения двух операндов методом вычитания выставляет соответствующие флаги. По флагу нуля мы и определяем, равны операнды или нет. Если операнды равны — флаг нуля устанавливается (ZF=1), и мы можем осуществить условный переход — например, je или jz — переход, если флаг нуля установлен, то есть если операнды равны. По состоянию флага переноса (CF) после выполнения команды cmp можно определить, меньше ли первый операнд, чем второй. Если флаг переноса установлен (CF=1) — значит, первый операнд меньше второго. Если не установлен (CF=0) — значит, первый операнд больше или равен второму.
JB (синоним JC JNAE) осуществляет переход, если первый операнд команды cmp меньше второго, то есть если установлен флаг переноса CF. JAE (JNB, JNC) делает прыг, когда первый операнд больше или равен второму, то есть не меньше второго.
JBE (JNA) — переход, если меньше или равно, то есть когда CF=1 или ZF=1.
JA (JNBE) — переход, если больше, — CF=0 и ZF=0.
Много информации? Перечитайте и отдохните. Сейчас будет еще веселей. Я говорил, что шифровать мы будем командой xor. Как же это работает? Команду xor можно представлять как переключатель выбранных битов в противоположное значение. То есть те биты, которые установлены в единицы во втором операнде (второй операнд в таком случае называют маской), будут инвертированы в первом. Те, которые во втором операнде установлены в ноль, в первом останутся нетронутыми. xor очень удобно использовать, к примеру, для быстрого обнуления регистра: xor eax,eax. Получается, что все установленные в единицу биты содержимого eax будут инвертированы, а нули останутся как есть. В итоге имеем в регистре набор исключительно нулевых битов, он же ноль. И экономим память и процессорное время. Когда мы применяем команду xor к какому-либо значению, второй операнд как бы является ключом шифра. Он указывает, значение каких битов изменить на противоположное, а какие не трогать. Если зашифровать какие-либо данные командой xor с определенным ключом, то повторное шифрование тем же ключом произведет расшифровку данных. Поэтому для расшифровки нам даже не придется менять порядок действий. Возвращаемся к коду. Функцией CreateFile открываем исходный файл на чтение. Проверяем на ошибку и сохраняем дескриптор в hfile1. Функция GetFileSize возвращает размер файла, дескриптор которого передается в функцию в первом параметре. Второй параметр — указатель на 32-битную переменную, в которую вернется старшее двойное слово размера файла. То есть, если число, обозначающее размер файла в байтах, будет настолько велико, что не вместится в 32 бита регистра для ответа, то функция запихнет его в 64 бита по частям: левые 32 бита — во второй параметр, правую часть — в привычный eax. Такое хитрое оформление ответа сделано из соображений совместимости со старыми программами, которые не предполагают, что размер файла может равняться 4 гигабайтам и выше. На самом деле я тоже совершенно не рассчитываю, что этим простейшим шифратором кто-то станет шифровать образы DVD. Программа может работать некорректно, если размер файла окажется более четырех гиг, так что будьте осторожны. Просто -1 (он же 0xFFFFFFFF в 32-битном варианте), который функция возвращает в случае ошибки, может трактоваться и как ошибка, и как размер в 4 гигабайта, а, к примеру, ноль в ответе функции может оказаться и нулевым размером файла, и 4 гигабайта + один байт. В любом случае этот пример не оптимизирован для работы с большими файлами, о чем выводится предупреждающее сообщение в случае, если размер файла равен или больше 0x500.000 (5 мегабайт).
Если размер файла в норме (или программа думает, что в норме, а на самом деле размер превышает 4 гигабайта), мы имеем в ebx размер файла и начинаем подготовку к циклу шифрования на метке .size_ok. Копируем строку с именем файла из fname1 в fname2. Добавляем к строке fname2 строку extname, чтобы получилось имя временного файла, в который мы будем записывать шифрованные данные, отличное от имени оригинала. Создаем файл с именем fname2 и правами на запись. Помещаем его дескриптор в hfile2. Цикл шифрования .crypt_cycle. Функцией ReadFile читаем 1 байт в переменную buf. Указатель на текущую позицию для чтения/записи в файле автоматически сдвигается каждый раз на число прочитанных/записанных байт. Поэтому каждый следующий раз мы будем автоматически читать следующий байт. Далее нам надо поместить куда-нибудь очередной символ пароля, чтобы шифровать каждый следующий байт следующим символом пароля. Проще всего вычислить очередной символ, поделив номер текущего шифруемого байта нацело на количество символов указанного пароля. Остаток от деления каждый раз будет необходимым смещением относительно начала пароля. То есть, если длина введенного пароля три символа, а текущий шифруемый байт — седьмой, то 6/3=2 и 0 в остатке, 7 — 1 в остатке, 8 — 2 в остатке, 9 — снова 0 в остатке и т.д. В ebx мы сохранили размер файла в байтах и на каждом витке цикла отнимаем единичку (dec ebx). Значит, ebx у нас хранит число байт, оставшихся для шифрования, а когда становится нулем, мы понимаем, что все байты обработаны, и выходим из цикла. Значит, отсчет байтов в ebx у нас идет как бы обратный, и символы пароля тоже будут выбираться в обратном порядке, что немного повышает устойчивость шифра к взлому. Для деления нам необходимо поместить делимое в EDX:EAX. Старшую часть — EDX — мы просто обнуляем (xor edx,edx), а в младшую копируем наш обратный счетчик (mov eax,ebx). Делим на количество введенных символов пароля (passlen). Имеем остаток от деления в EDX. Теперь, чтобы обратиться именно к выбранному таким методом символу пароля, необходимо обратиться к байту по адресу [pass1 + полученное смещение]. От перемены мест слагаемых сумма не меняется, поэтому к полученному смещению прибавляем pass1 и имеем в EDX адрес искомого символа пароля. Оба операнда команды xor не могут одновременно являться указателями на память, поэтому одним из операндов у нас будет самый младшенький байт регистра EAX — AL. В него мы и скопируем необходимый символ (mov al,[edx]). Вас, наверное, уже давно мучает вопрос, что означают квадратные скобки в FASM? Почему al мы записали просто так, а edx заключили в них? Все дело в том, что, если операнд записывается без скобок, то это значит, что процессор оперирует непосредственно со значением операнда. Взятие операнда в квадратные скобки дает команду работать с данными, находящимися по адресу, указанному в операнде. В данном случае EDX содержит лишь адрес в виртуальной памяти нашей программы, а не само значение необходимого символа пароля. А вот в регистр AL будет помещено именно значение — конкретный символ пароля. Шифруем содержимое buf этим символом. Записываем зашифрованное значение в hfile2. Уменьшаем EBX и повторяем цикл, если EBX еще не равен нулю. По завершению цикла шифрования закрываем дескрипторы открытых файлов. Устанавливаем атрибуты файла-оригинала в значение FILE_ATTRIBUTE_NORMAL. Просто нам надо его теперь удалить — а вдруг он имеет атрибут "только для чтения", и удаление не удастся? Поэтому удаляем оригинал. Переименовываем зашифрованный дубликат в имя оригинала. Вызываем окошко с сообщением об успешном завершении.
Стоит сказать, что чтение и запись большого файла по одному байту за раз является не самым лучшим вариантом с точки зрения скорости. Каждый вызов функции занимает достаточно много процессорного времени, что будет заметно невооруженным глазом при шифровании файлов размером в несколько мегабайт. Для ускорения процесса шифрования можно было бы шифровать данные сразу по 4 байта за проход, но тогда придется немного усложнить процедуру выбора текущих символов пароля для шифрования. Также можно заметно ускорить процесс шифрования, если помещать зашифрованные данные во временный буфер в оперативной памяти и только по заполнению буфера скидывать их на жесткий диск. Попробуйте самостоятельно осуществить эти задачи. На самом деле, имеющихся у вас знаний уже вполне достаточно для этого. Если хотите, чтобы вводимые символы пароля отображались в виде звездочек — добавьте к стилю полей для ввода паролей ES_PASSWORD.
Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере.
Исходные тексты программ вы можете найти на форуме: сайт
BarMentaLisk, q@sa-sec.org
include 'win32ax.inc'
include 'encoding\WIN1251.INC'
MAXSIZE = 260
PASSIZE = 8
.data
ernopass db 'Введите пароль!',0
erdifpass db 'Пароль и подтверждение не совпадают!',0
erfsize db 'Файл слишком большой!',0
erfname db 'Файл не выбран!',0
bigfile db 'Файл довольно большой!',13,10
db 'Шифрование может занять много времени.',0
success db 'Успешно завершено!',0
extname db '.crpt',0
errtxt db 'Код ошибки: %u',0
errbuf rb $-errtxt+10
fname1 rb MAXSIZE
fname2 rb MAXSIZE
pass1 rb PASSIZE+1
pass2 rb PASSIZE+1
buf db ?
passlen dd ?
hfile1 dd ?
hfile2 dd ?
temp dd ?
ofn OPENFILENAME sizeof.OPENFILENAME,,,,,,,fname1,MAXSIZE,,,,, OFN_FILEMUSTEXIST + OFN_PATHMUSTEXIST + OFN_EXPLORER
.code
start:
invoke GetModuleHandle,0
mov [ofn.hInstance],eax
invoke DialogBoxParam,eax,IDD_MAIN,HWND_DESKTOP,DialogProc,0
invoke ExitProcess,0
proc DialogProc hwnddlg,msg,wparam,lparam
push ebx esi edi
cmp [msg],WM_INITDIALOG
je .wminitdialog
cmp [msg],WM_COMMAND
je .wmcommand
cmp [msg],WM_DROPFILES
je .drop
cmp [msg],WM_CLOSE
je .wmclose
xor eax,eax
jmp .finish
.wminitdialog:
mov eax,[hwnddlg]
mov [ofn.hwndOwner],eax
invoke SendDlgItemMessage,[hwnddlg],ID_PASS1,EM_LIMITTEXT,PASSIZE,0
invoke SendDlgItemMessage,[hwnddlg],ID_PASS2,EM_LIMITTEXT,PASSIZE,0
jmp .processed
.wmcommand:
cmp [wparam],ID_OPEN
je .open
cmp [wparam],ID_EXIT
je .wmclose
cmp [wparam],ID_CRYPT
jne .finish
;=crypt=
cmp [fname1],0
je .erfname
invoke GetDlgItemText,[hwnddlg],ID_PASS1,pass1,PASSIZE+1
cmp [pass1],0
je .ernopass
invoke GetDlgItemText,[hwnddlg],ID_PASS2,pass2,PASSIZE+1
invoke lstrcmp,pass1,pass2
test eax,eax
jne .erdifpass
invoke lstrlen,pass1
mov [passlen],eax
invoke CreateFile,fname1,GENERIC_READ,FILE_SHARE_READ,0,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,0
cmp eax,-1
je .erfname
mov [hfile1],eax
invoke GetFileSize,eax,0
cmp eax,-1
je .erfilesize
test eax,eax
je .erfilesize
mov ebx,eax
cmp eax,500000h ;5MB
jb .size_ok
invoke MessageBox,[hwnddlg],bigfile,0,MB_OKCANCEL
cmp eax,IDOK
jne .hclose
.size_ok:
invoke lstrcpy,fname2,fname1
invoke lstrcat,fname2,extname
test eax,eax
je .erfname
invoke CreateFile,fname2,GENERIC_WRITE,FILE_SHARE_READ,0,CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,0
cmp eax,-1
je .erfname
mov [hfile2],eax
.crypt_cycle:
invoke ReadFile,[hfile1],buf,1,temp,0
test eax,eax
je .error
xor edx,edx
mov eax,ebx
div [passlen]
add edx,pass1
mov al,[edx]
xor [buf],al
invoke WriteFile,[hfile2],buf,1,temp,0
test eax,eax
je .error
dec ebx
test ebx,ebx
jne .crypt_cycle
invoke CloseHandle,[hfile1]
invoke CloseHandle,[hfile2]
invoke SetFileAttributes,fname1,FILE_ATTRIBUTE_NORMAL
invoke DeleteFile,fname1
test eax,eax
je .error
invoke MoveFile,fname2,fname1
test eax,eax
je .error
invoke MessageBox,[hwnddlg],success,0,0
jmp .processed
.ernopass:
invoke MessageBox,[hwnddlg],ernopass,0,0
jmp .processed
.erdifpass:
invoke MessageBox,[hwnddlg],erdifpass,0,0
jmp .processed
.erfname:
invoke MessageBox,[hwnddlg],erfname,0,0
jmp .processed
.erfilesize:
invoke MessageBox,[hwnddlg],erfsize,0,0
jmp .hclose
.error:
invoke GetLastError
invoke wsprintf,errbuf,errtxt,eax
invoke MessageBox,[hwnddlg],errbuf,0,0
.hclose:
invoke CloseHandle,[hfile1]
invoke CloseHandle,[hfile2]
jmp .processed
.drop:
invoke DragQueryFile,[wparam],0,fname1,MAXSIZE
jmp .new_file
.open:
invoke GetOpenFileName,ofn
test eax,eax
je .processed
.new_file:
invoke SetDlgItemText,[hwnddlg],ID_NAMESTRING,fname1
jmp .processed
.wmclose:
invoke EndDialog,[hwnddlg],0
.processed:
mov eax,1
.finish:
pop edi esi ebx
ret
endp
.end start
section '.rsrc' resource data readable
IDD_MAIN = 100
ID_NAMESTRING = 101
ID_PASS1 = 102
ID_PASS2 = 103
ID_OPEN = 104
ID_CRYPT = 105
ID_EXIT = 109
directory RT_DIALOG,dialogs
resource dialogs,\
IDD_MAIN,LANG_RUSSIAN+SUBLANG_DEFAULT,main_dialog
dialog main_dialog,'Простейший Шифровальщик',240,120,190,175,WS_CAPTION+WS_POPUP+WS_SYSMENU+ DS_MODALFRAME,WS_EX_ACCEPTFILES
dialogitem 'STATIC','&Файл:',-1,10,10,70,8,WS_VISIBLE
dialogitem 'EDIT','',ID_NAMESTRING,10,20,170,13,WS_VISIBLE+WS_BORDER+ ES_AUTOHSCROLL+ES_READONLY
dialogitem 'STATIC','&Пароль:',-1,10,50,70,8,WS_VISIBLE
dialogitem 'EDIT','',ID_PASS1,10,60,60,13,WS_VISIBLE+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL
dialogitem 'STATIC','&Подтверждение:',-1,10,80,70,8,WS_VISIBLE
dialogitem 'EDIT','',ID_PASS2,10,90,60,13,WS_VISIBLE+WS_BORDER+ WS_TABSTOP+ES_AUTOHSCROLL
dialogitem 'BUTTON','&Открыть',ID_OPEN,10,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON
dialogitem 'BUTTON','&Шифровать',ID_CRYPT,85,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_DEFPUSHBUTTON
dialogitem 'BUTTON','В&ыход',ID_EXIT,135,150,45,15,WS_VISIBLE+WS_TABSTOP+ BS_PUSHBUTTON
enddialog
Пробежимся взглядом по секции данных. MAXSIZE — максимальное количество символов в полном имени файла вместе с путем. PASSIZE — максимальная длина пароля без учета нуль-терминатора. Содержимое extname добавляется к полному имени для создания временного файла. В описании структуры ofn опущенные параметры отсутствуют — это допустимо, необходимо лишь обозначить их запятыми, если за ними следуют инициализируемые параметры. Диалоговое окно, как можно понять из кода, является упрощенным вариантом создания окна. Напомню параметры функции DialogBoxParam:
1) идентификатор исполняемого модуля программы;
2) идентификатор ресурса, содержащего шаблон диалогового окна;
3) дескриптор окна-владельца;
4) указатель на процедуру диалогового окна;
5) значение, передаваемое диалоговому окну в параметре lParam сообщения WM_INITDIALOG.
Для вызова DialogBoxParam нам достаточно указать в первом параметре значение, возвращаемое функцией GetModuleHandle, во втором — шаблон окна, в четвертом — процедуру обработки сообщений. Третий параметр у нас — константа HWND_DESKTOP, которая равна нулю и указана лишь для наглядности. Шаблон окна содержится в секции ресурсов и описывается аналогично прочим ресурсам. Тип ресурса — RT_DIALOG. Объявляется макроинструкцией dialog, за которой следует некоторое количество элементов, обозначаемых макросом dialogitem, завершаемое инструкцией enddialog. Макроинструкция dialog может иметь до 11 параметров, первые 7 из которых обязательные:
1. Имя ресурса.
2. Заголовок окна в кавычках.
3, 4, 5, 6 — соответственно X, Y — ширина, высота.
7. Стиль окна.
8. Расширенный стиль (необязательный параметр).
9. Идентификатор меню окна (необязательный параметр).
10. Имя шрифта в кавычках (необязательный параметр).
11. Размер шрифта (необязательный параметр).
Инструкция dialogitem имеет 8 обязательных параметров и один допустимый:
1. Имя класса элемента в кавычках.
2. Текст элемента в кавычках, или идентификатор ресурса (например, ID картинки для элемента класса STATIC со стилем SS_BITMAP).
3. Идентификатор элемента (используется в функциях API).
4, 5, 6, 7 — соответственно X, Y, ширина и высота элемента.
8. Стиль.
9. Расширенный стиль (необязательный параметр).
Шаблон диалога может содержать любое количество элементов или вообще ни одного, но обязательно должен завершаться макросом enddialog. Вызов функции DialogBoxParam надолго передает управление процедуре DialogProc. В данном случае нам не надо организовывать цикл получения сообщений от окна. Операционная система будет крутить этот цикл самостоятельно, а нашей процедуре останется лишь обеспечить обработку входящих сообщений. Если на каком-то этапе нам понадобится закрыть диалоговое окно и продолжить выполнение команд, следующих за вызовом функции DialogBoxParam, то мы прямо из процедуры обработки сообщений вызовем функцию EndDialog для завершения функции и передачи управления следующей команде. Она имеет 2 параметра: дескриптор приговоренного диалогового окна; значение, которое вернет функция DialogBoxParam при успешном завершении (в случае неудачи DialogBoxParam вернет -1, поэтому во избежание двусмысленности сами не отправляйте -1 в этом параметре). После обработки сообщения наша процедура должна поместить в eax единицу и выполнить команду возврата из процедуры (ret). Если сообщение не было обработано процедурой, то в eax должен быть помещен ноль — тогда операционная система сама выполнит требуемые действия по обработке сообщения. Функция DialogBoxParam создает окно по указанному шаблону и перед самой публикацией его на экране отправляет процедуре диалога сообщение WM_INITDIALOG, содержащее в lParam значение, переданное нами в пятом параметре DialogBoxParam. Это значение может использоваться программистом для передачи каких-либо данных диалоговому окну в момент его инициализации. В нашем примере мы ничего не передаем, но по получении сообщения WM_INITDIALOG копируем дескриптор окна в элемент hwndOwner структуры ofn (сразу оба операнда команды MOV не могут являться указателями на память, поэтому копируем через привилегированный регистр EAX за две команды). Также отправляем обоим полям для ввода пароля сообщения EM_LIMITTEXT.
Для отправки сообщений элементам диалогового окна служит функция SendDlgItemMessage. В первых двух параметрах этой функции указываются дескриптор диалогового окна и идентификатор элемента-получателя. В остальном функция аналогична функции SendMessage. Сообщение EM_LIMITTEXT устанавливает максимально допустимое число вводимых символов для элемента класса EDIT. Первый параметр сообщения — максимальное число символов, второй не используется. Сообщение WM_DROPFILES было рассмотрено в четвертой статье. По приходу сообщения от элемента ID_OPEN (кнопка "Открыть"), вызывается диалог открытия файла. Если после него возвращается ноль — значит, файл не был выбран. В этом случае — прыжок на метку .processed и возврат. Если же пользователь выбрал файл — сползаем на метку .new_file. Метка здесь для того, чтобы функция SetDlgItemText выполнялась и при открытии файла через диалог открытия, и при дропе файла на окно. Данная функция похожа на сообщение WM_SETTEXT. Она предназначена для установки нового текста элемента диалогового окна. Параметры: дескриптор диалога; идентификатор элемента; указатель на текстовую строку с нулем на конце. На метку .wmclose мы попадаем при получении сообщения WM_CLOSE или при получении сообщения WM_COMMAND от элемента ID_EXIT (кнопка "Выход"). Тут все предельно ясно: получили приказ на закрытие — вызываем упоминавшуюся выше функцию EndDialog.
Теперь переходим, так сказать, к ядру нашей программы. Тут нас и поджидает масса новых неизвестностей. Когда мы проверяем последнее из возможных сообщений от элементов диалога — ID_CRYPT, — то помещаем ноль в EAX и прыгаем на .finish, если это не оно, чтобы функция операционной системы самостоятельно обрабатывала не предусмотренные нашей программой сообщения. Иначе — делаем вывод, что сообщение пришло от кнопки "Шифровать", и начинаем процедуру шифрования. Сразу мы сравниваем первый байт содержимого fname1 с нулем. Если файл был выбран, то там должен находиться первый символ его полного имени, ну, а если нет — там будет ноль, и мы переходим на сообщение об ошибке "Файл не выбран!" Функция GetDlgItemText аналогична сообщению WM_SETTEXT и является противоположностью функции SetDlgItemText. Она возвращает текст заданного элемента диалога в указанный буфер. Ее параметры: дескриптор диалога; идентификатор элемента; указатель на буфер для размещения текста; максимальное количество символов включая завершающий ноль. Максимальная длина пароля у нас равна константе PASSIZE, поэтому в качестве последнего параметра мы указываем PASSIZE+1 с учетом завершающего строку нуля. Сравнивая первый байт содержимого pass1 с нулем, мы проверяем, был ли введен хотя бы один символ пароля. Если пароль был введен — получаем содержимое поля для подтверждения пароля в переменную pass2. При помощи функции lstrcmp (ее мы проходили еще на втором занятии) сравниваем строки pass1 и pass2. Если возвращается не ноль — значит, пароль и подтверждение не совпадают, и мы прыгаем на соответствующую ошибку. В противном случае, используя функцию lstrlen, мы помещаем в переменную passlen количество введенных символов пароля: нам необходимо будет знать, из скольких символов состоит введенный пользователем пароль. Как же будет происходить шифрование выбранного файла? Самый простой способ шифрования — использование команды xor. Эта команда изначально предназначена для других вещей, но как нельзя лучше подходит для нашей сегодняшней задачи. Для начала давайте познакомимся поближе с основными арифметическими и логическими командами ассемблера:
add — складывает два операнда и помещает результат в первый операнд. Синтаксис: add op1,op2
inc — прибавляет к операнду единицу. Синтаксис: inc op1
sub — вычитает второй операнд из первого. Результат сохраняется в первом операнде. Синтаксис: sub op1,op2
dec — вычитает из операнда единицу. Синтаксис: dec op1
mul — умножает операнд на регистр AL/AX/EAX в зависимости от размерности операнда (байт/слово/двойное слово). Результат помещается соответственно в AX/DX:AX/EDX:EAX. То есть, если вы хотите умножить 3 на 5, то задвигаете пятерку в AL и умножаете на тройку:
mov al,5
mul 3
div — делит AX/DX:AX/EDX:EAX (в зависимости от размерности операнда) на операнд. Частное помещается соответственно в AL/AX/EAX, а остаток — в AH/DX/EDX. Пример деления мы разберем ниже по тексту.
Думаю, вы знаете, что каждый байт данных в ЭВМ представлен в виде блока из восьми бит — восьми элементов, каждый из которых имеет лишь два состояния: выключен и включен (0 и 1). Логические команды выполняют логические операции над битами операндов.
and — логическое "И": соответствующие биты первого операнда устанавливаются в единицу только в том случае, если в обоих операндах они равны единице. Иначе — в ноль.
or — логическое "ИЛИ": соответствующие биты первого операнда устанавливаются в ноль только в том случае, если в обоих операндах они равны нолю. Иначе — в единицу.
xor — логическое "ЛИБО" (логическое исключающее ИЛИ): соответствующие биты первого операнда устанавливаются в единицу только в том случае, если бит одного из операндов равен единице, а другого — нолю. Если соответствующие биты операндов равны, то бит первого операнда устанавливается в ноль.
not — логическое "НЕ": биты операнда инвертируются. Ноль меняется на единицу, единица — на ноль. Второй операнд не используется.
test — логическое "И" без изменения операндов: выполняется операция and, но результат не записывается в операнд, а выставляет флаг нуля (ZF) в ноль, если хотя бы одна пара соответствующих битов обоих операндов совпала в значении единицы. Команду cmp eax,0 удобно и выгодно заменять командой test eax,eax: результат тот же, зато выполняется за три такта процессора (против четырех в случае использования cmp) и занимает на один байт меньше памяти.
op1 | op2 | and | or | xor | not |
0 | 0 | 0 | 0 | 0 | 1 |
0 | 1 | 0 | 1 | 1 | 1 |
1 | 0 | 0 | 1 | 1 | 0 |
1 | 1 | 1 | 1 | 0 | 0 |
С флагом нуля есть небольшая путаница: если ZF=0 — значит, флаг нуля не установлен, значит, результат не ноль. Если же результат ноль — флаг ZF установлен, то есть ZF=1. Все становится более понятным при использовании команд перехода JZ (100% синоним JE) — перейти, если ноль (флаг нуля установлен); JNZ (100% синоним JNE) — перейти, если не ноль (флаг нуля не установлен).
Команда cmp после сравнения двух операндов методом вычитания выставляет соответствующие флаги. По флагу нуля мы и определяем, равны операнды или нет. Если операнды равны — флаг нуля устанавливается (ZF=1), и мы можем осуществить условный переход — например, je или jz — переход, если флаг нуля установлен, то есть если операнды равны. По состоянию флага переноса (CF) после выполнения команды cmp можно определить, меньше ли первый операнд, чем второй. Если флаг переноса установлен (CF=1) — значит, первый операнд меньше второго. Если не установлен (CF=0) — значит, первый операнд больше или равен второму.
JB (синоним JC JNAE) осуществляет переход, если первый операнд команды cmp меньше второго, то есть если установлен флаг переноса CF. JAE (JNB, JNC) делает прыг, когда первый операнд больше или равен второму, то есть не меньше второго.
JBE (JNA) — переход, если меньше или равно, то есть когда CF=1 или ZF=1.
JA (JNBE) — переход, если больше, — CF=0 и ZF=0.
Много информации? Перечитайте и отдохните. Сейчас будет еще веселей. Я говорил, что шифровать мы будем командой xor. Как же это работает? Команду xor можно представлять как переключатель выбранных битов в противоположное значение. То есть те биты, которые установлены в единицы во втором операнде (второй операнд в таком случае называют маской), будут инвертированы в первом. Те, которые во втором операнде установлены в ноль, в первом останутся нетронутыми. xor очень удобно использовать, к примеру, для быстрого обнуления регистра: xor eax,eax. Получается, что все установленные в единицу биты содержимого eax будут инвертированы, а нули останутся как есть. В итоге имеем в регистре набор исключительно нулевых битов, он же ноль. И экономим память и процессорное время. Когда мы применяем команду xor к какому-либо значению, второй операнд как бы является ключом шифра. Он указывает, значение каких битов изменить на противоположное, а какие не трогать. Если зашифровать какие-либо данные командой xor с определенным ключом, то повторное шифрование тем же ключом произведет расшифровку данных. Поэтому для расшифровки нам даже не придется менять порядок действий. Возвращаемся к коду. Функцией CreateFile открываем исходный файл на чтение. Проверяем на ошибку и сохраняем дескриптор в hfile1. Функция GetFileSize возвращает размер файла, дескриптор которого передается в функцию в первом параметре. Второй параметр — указатель на 32-битную переменную, в которую вернется старшее двойное слово размера файла. То есть, если число, обозначающее размер файла в байтах, будет настолько велико, что не вместится в 32 бита регистра для ответа, то функция запихнет его в 64 бита по частям: левые 32 бита — во второй параметр, правую часть — в привычный eax. Такое хитрое оформление ответа сделано из соображений совместимости со старыми программами, которые не предполагают, что размер файла может равняться 4 гигабайтам и выше. На самом деле я тоже совершенно не рассчитываю, что этим простейшим шифратором кто-то станет шифровать образы DVD. Программа может работать некорректно, если размер файла окажется более четырех гиг, так что будьте осторожны. Просто -1 (он же 0xFFFFFFFF в 32-битном варианте), который функция возвращает в случае ошибки, может трактоваться и как ошибка, и как размер в 4 гигабайта, а, к примеру, ноль в ответе функции может оказаться и нулевым размером файла, и 4 гигабайта + один байт. В любом случае этот пример не оптимизирован для работы с большими файлами, о чем выводится предупреждающее сообщение в случае, если размер файла равен или больше 0x500.000 (5 мегабайт).
Если размер файла в норме (или программа думает, что в норме, а на самом деле размер превышает 4 гигабайта), мы имеем в ebx размер файла и начинаем подготовку к циклу шифрования на метке .size_ok. Копируем строку с именем файла из fname1 в fname2. Добавляем к строке fname2 строку extname, чтобы получилось имя временного файла, в который мы будем записывать шифрованные данные, отличное от имени оригинала. Создаем файл с именем fname2 и правами на запись. Помещаем его дескриптор в hfile2. Цикл шифрования .crypt_cycle. Функцией ReadFile читаем 1 байт в переменную buf. Указатель на текущую позицию для чтения/записи в файле автоматически сдвигается каждый раз на число прочитанных/записанных байт. Поэтому каждый следующий раз мы будем автоматически читать следующий байт. Далее нам надо поместить куда-нибудь очередной символ пароля, чтобы шифровать каждый следующий байт следующим символом пароля. Проще всего вычислить очередной символ, поделив номер текущего шифруемого байта нацело на количество символов указанного пароля. Остаток от деления каждый раз будет необходимым смещением относительно начала пароля. То есть, если длина введенного пароля три символа, а текущий шифруемый байт — седьмой, то 6/3=2 и 0 в остатке, 7 — 1 в остатке, 8 — 2 в остатке, 9 — снова 0 в остатке и т.д. В ebx мы сохранили размер файла в байтах и на каждом витке цикла отнимаем единичку (dec ebx). Значит, ebx у нас хранит число байт, оставшихся для шифрования, а когда становится нулем, мы понимаем, что все байты обработаны, и выходим из цикла. Значит, отсчет байтов в ebx у нас идет как бы обратный, и символы пароля тоже будут выбираться в обратном порядке, что немного повышает устойчивость шифра к взлому. Для деления нам необходимо поместить делимое в EDX:EAX. Старшую часть — EDX — мы просто обнуляем (xor edx,edx), а в младшую копируем наш обратный счетчик (mov eax,ebx). Делим на количество введенных символов пароля (passlen). Имеем остаток от деления в EDX. Теперь, чтобы обратиться именно к выбранному таким методом символу пароля, необходимо обратиться к байту по адресу [pass1 + полученное смещение]. От перемены мест слагаемых сумма не меняется, поэтому к полученному смещению прибавляем pass1 и имеем в EDX адрес искомого символа пароля. Оба операнда команды xor не могут одновременно являться указателями на память, поэтому одним из операндов у нас будет самый младшенький байт регистра EAX — AL. В него мы и скопируем необходимый символ (mov al,[edx]). Вас, наверное, уже давно мучает вопрос, что означают квадратные скобки в FASM? Почему al мы записали просто так, а edx заключили в них? Все дело в том, что, если операнд записывается без скобок, то это значит, что процессор оперирует непосредственно со значением операнда. Взятие операнда в квадратные скобки дает команду работать с данными, находящимися по адресу, указанному в операнде. В данном случае EDX содержит лишь адрес в виртуальной памяти нашей программы, а не само значение необходимого символа пароля. А вот в регистр AL будет помещено именно значение — конкретный символ пароля. Шифруем содержимое buf этим символом. Записываем зашифрованное значение в hfile2. Уменьшаем EBX и повторяем цикл, если EBX еще не равен нулю. По завершению цикла шифрования закрываем дескрипторы открытых файлов. Устанавливаем атрибуты файла-оригинала в значение FILE_ATTRIBUTE_NORMAL. Просто нам надо его теперь удалить — а вдруг он имеет атрибут "только для чтения", и удаление не удастся? Поэтому удаляем оригинал. Переименовываем зашифрованный дубликат в имя оригинала. Вызываем окошко с сообщением об успешном завершении.
Стоит сказать, что чтение и запись большого файла по одному байту за раз является не самым лучшим вариантом с точки зрения скорости. Каждый вызов функции занимает достаточно много процессорного времени, что будет заметно невооруженным глазом при шифровании файлов размером в несколько мегабайт. Для ускорения процесса шифрования можно было бы шифровать данные сразу по 4 байта за проход, но тогда придется немного усложнить процедуру выбора текущих символов пароля для шифрования. Также можно заметно ускорить процесс шифрования, если помещать зашифрованные данные во временный буфер в оперативной памяти и только по заполнению буфера скидывать их на жесткий диск. Попробуйте самостоятельно осуществить эти задачи. На самом деле, имеющихся у вас знаний уже вполне достаточно для этого. Если хотите, чтобы вводимые символы пароля отображались в виде звездочек — добавьте к стилю полей для ввода паролей ES_PASSWORD.
Все приводимые примеры были протестированы на правильность работы под Windows XP и, скорее всего, будут работать под другими версиями Windows, однако я не даю никаких гарантий их правильной работы на вашем компьютере.
Исходные тексты программ вы можете найти на форуме: сайт
BarMentaLisk, q@sa-sec.org
Компьютерная газета. Статья была опубликована в номере 29 за 2008 год в рубрике программирование