Воробьёвы

(-:

№29.2. Экстрим программированние для дZенствующих. Пароле, пароле, пароле...

############################################################

Serrgio [HI-TECH group]

9. Генерация паролей

В тот год, когда Ю'дзын покинул горы,
умел смеяться он и видел свет [он],
хотел больших серьезных перемен [он].
Теперь не хочет, не умеет и не видит...
скажи учитель, жив Ю'дзын ли?
Учитель сказал: "ЧТОБ ЛОТОС ЦВЕЛ,
ЕМУ НУЖНА ВОДААААА!!"
И ударил его палкой. По голове.
(C) НОМ, portion by Конфуций

#1.В этой главе мы с вами напишем продвинутую программу для генерации паролей. В отличие от тех программ, что мы писали ранее, эта длиннее и более наворочена (а вы чего хотели?), так что будьте бдительны ;)

Не могу не отметить, что DOS'овский прототип программы был взят из книги "Техника и философия хакерских атак" Криса Касперски, а процесс медитирования над win32-версией сопровождался постоянным постукиванием бамбуковой палкой в железных руках Аркадия Белоусова, за что ему большой дзенский сенькс.

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

Например, для алфавита, состоящего из трех символов "abc", программа сгенерирует и напечатает следующие пароли: a, b, c, aa, ba, ca, ab, bb, cb, ac, bc, cc, aaa, baa, caa ... cccbcbbaba, aaaccbbaba, baaccbbaba, caaccbbaba... И будет печатать их, пока не наступит одно из нижеследующих событий:

- вам надоедает это безобразие, и вы жмете на "крестик" закрытия окна;

Тут, однако, встает первый, философский вопрос - не является ли закрытие окна консольной программы столь варварским способом нарушением священных принципов Правильного Программирования? Со всей ответственностью за возможные последствия отвечаю: нет, не является! И тут же, на всякий случай, даю себе пути к отступлению: если ВСЕ-ТАКИ является, то хорошо известная каждому сетевику программа ping-очередное доказательство того, что программисты Microsoft этим правилам не следуют, в чем легко убедиться (как, впрочем, и опровергнуть), выполнив команду ping 127.0.0.1 -t.

- число символов пароля превосходит размер буфера;

А вот это уже вопрос математический. Зная число символов в "алфавите" и скорость генерации их сочетаний (достав с полки толстый талмуд по комбинаторике), нетрудно подсчитать длину пароля, при превышении которой дальнейшую работу программы стоит признать нецелесообразной ;). Без доказательства приведём такую формулу: при длине буфера N и количестве знаков алфавита B количество разных паролей будет B*(B^N-1)/(B-1). Так, при двух знаках (B=2) и длине буфера под 8 знаков (N=8) можно получить 510 паролей. Если же знаков будет 64, то при длине буфера под 4 знака можно получить более 17 миллионов паролей".

- тепловая смерть Вселенной.

Абстрагируясь от многих вещей, астрофизическую часть этого вопроса можно сформулировать следующим образом - какой размер буфера для двухсимвольного алфавита "01" будет достаточным для того, чтобы программа прекратила работу лишь в момент наступления данного события? Часть квантово-физическая - не приближаем ли мы данное событие, проводя дефрагментацию жесткого диска и тем самым уменьшая коэффициент энтропийности нашей Вселенной? И, наконец, часть экологическая - где взять источник энергии для экспериментальной проверки части астрофизической?

Этот алгоритм можно запросто использовать, например, для реверсирования односторонних функций (что, кстати говоря - альфа и омега криптографии) методом брутфорса (сиречь методом грубой силы). Впоследствии мы ещё вернёмся к этому вопросу.

#2.Секция данных нашей программы выглядит следующим образом:

    S_consoleTitle db 'DicMaker+',0
       S_alphabet db 'abcde'
       szAlphabet = $-S_alphabet
       szBuffer = 20
       S_password db szBuffer dup(?)
       S_key db szBuffer dup(?)
       SzKey = $-S_key

S_alphabet - "алфавит", то есть набор символов, которые будут "участвовать" в генерации пароля. SzAlphabet - его размер (говоря другими словами, число символов). Далее у нас следуют два буфера - S_password для хранения пароля и S_key для хранения "ключа". Учитывая, что ключ и пароль - близнецы-братья, размер буфера под них должен быть одинаковым.

"Что такое пассворд, мы знаем, но что подразумевается под термином ключ?" - спросит читатель. И совершенно правильно сделает, что спросит.

Объясняю. Например, для шестисимвольного алфавита "abcdef" мы можем нарисовать следующую табличку:

Начертание символа a b c d e f
Индекс символа 0 1 2 3 4 5

При генерации паролей в буферы S_password и S_key будут попадать соответственно:

Ключ(байты) Пароль
00 'a'
01 'b'
02 'c'
03 'd'
04 'e'
05 'f'
00 'aa'
01 00 'ba'
02 00 'ca'
03 00 'da'
04 00 'ea'
05 00 'fa'
.......  
00 00 00 'aaa'
01 00 00 'baa'
02 00 00 'caa'
03 00 00 'daa'
04 00 00 'eaa'
....... ......

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

Однако, даже среди грубиянов (к контексте brute force) обязательно найдется особо грубый ;). Специально для таких мужественных, бескомпромиссных, и, что самое главное, собирающихся экспериментально проверить некоторые теории, изложенные параграфом выше, привожу классический алгоритм генерации паролей:
    lea esi,S_password
       @@nextpassword:
       xor ebx,ebx
       @@checkover:
       inc byte ptr [esi+ebx]
       cmp byte ptr [esi+ebx],'z'
       jb @@writepassword
       mov byte ptr [esi+ebx],' '
       inc bx
       jmp @@checkover
       @@writepassword:
       ;очередной пароль готов к употреблению
       jmp @@nextpassword

Медитируем!

#3.А теперь полюбуемся на код программы, и попробуем разобраться, как он работает.

Сначала, как и всегда - локальные переменные под хэндл и под "напечатано символов":

    local houtput :dword
       local nwritten :dword

Хорошо знакомые по предыдущим программам "подготовительные операции":

    invoke SetConsoleTitle,addr S_consoleTitle
       invoke GetStdHandle,STD_OUTPUT_HANDLE
       mov [houtput],eax 

Теперь, собственно, рабочая часть программы (замрите в ужасе!):

    mov esi,offset S_key ;(1)
       mov edi,offset S_password
       mov ecx,esi

    @@keylenloop: ;(2)
       cmp ecx,offset S_key+szKey
       jae @@break
       mov byte ptr [esi],0

    @@passwordloop: ;(3)
       movzx ebx,byte ptr [esi]
       mov al,S_alphabet[ebx]
       mov [edi],al

    ;-|A|--------------------- ;(4)

    push ecx
       mov edi,offset S_password
       sub ecx,offset S_key-3
       mov word ptr [edi+ecx-2],0A0Dh
       invoke WriteConsole,houtput,edi,ecx,
       addr nwritten,NULL
       pop ecx

    ;-|Б|---------------------

    mov esi,offset S_key ;(5)
       ;mov edi,offset S_password 

    @@carryloop: ;(6)
       inc byte ptr [esi]
       cmp byte ptr [esi],szAlphabet
       jb @@passwordloop 

    mov byte ptr [esi],0
       mov al,S_alphabet[0]
       mov [edi],al
       inc esi
       inc edi
       cmp esi,ecx
       jbe @@carryloop 

    inc ecx
       jmp @@keylenloop
       @@break:

    invoke ExitProcess,0 ; ВЫХОД ;)

Уф... Что ж, давайте анализировать, и да пребудет с вами долготерпение.

Бряк 1- загружаем в регистры esi и edi указатели на буферы ключейи паролей соответственно. Регистр ecxиспользуется для подсчета числа разрядов в ключе (оно же - число символов в пароле), с той лишь разницей, что для удобства там лежит смещение от начала буфера ключа. Изначально он инициализируется указателем на первый разряд ключа, что вполне естественно, так как еще не произошло ни одной итерации цикла и первый разряд является одновременно и старшим, и младшим (на выбор), т. е. единственным ;).

Бряк 2- сюда переходит управление в том случае если предполагается увеличение разрядности ключа (она же - длина пароля). Здесь сравнивается указатель на старший разряд ключа с адресом, следующим за буфером под ключ. И если "потребности" программы больше, чем "возможности" буфера, перепрыгиваем на invoke ExitProcess и благополучно завершаем её выполнение. А если же подобное безобразие ещё не случилось, то инициализируем старший разряд ключа нулем. Собственно, алгоритм начинается отсюда, с "увеличения" ключа до единичной длины.

Бряк 3- старший разряд ключа конвертируется в соответствующий символ пароля. То есть берётся значение разряда ключа, достаётся соответствующий символ алфавита и записывается в соответствующий разряд пароля. Обратите внимание - меняется только старший разряд пароля, поскольку остальные разряды пароля (которых в начале алгоритма ещё нет) к этому моменту должны быть уже инициализированы правильным значением.

Бряк 4- между A и Б можно вставить любое действо, которое должно произойти со сгенерированным паролем. В данном примере пароль печатается, но ничто не мешает вставить сюда, например, код вычисления хэш-функции пароля и сравнения его с реверсируемым ;). Также обратите внимание, каким "хитрым" способом проводится добавление CRLF в буфер пароля для вывода на печать (с вычислением длины пароля вместе с CRLF). ;)

Бряк 5- снова берутся указатели. Вследствие особенностей кода между A и Б нам нет необходимости повторно брать указатель на буфер пароля, поэтому эта строчка закомментирована. Но, заметьте, не убрана - на тот случай если мы захотим написать между A и Б нечто иное. ;) Это разумный компромисс между оптимальностью (когда нет лишних инструкций) и читабельностью/модифицируемостью.

Бряк 6- увеличивается разряд (разряд, не разрядность!) ключа на единицу. Если не превышено максимальное значение разряда (соответствующее последнему символу "алфавита"), то ключ считается готовым и значение его разряда конвертируется в символ пароля, то есть идёт прыжок на бряк 3. Если же максимальное значение разряда превышено, то разряд обнуляется, также "обнуляется" соответствующий разряд пароля и, если не было выхода за пределы позиции старшего разряда (указатель в ecx), увеличивается следующий разряд (вам это ещё не напомнило длинную многоразрядную арифметику? :). А если выход был - добавляется очередной разряд к ключу (увеличивается его разрядность), то есть идёт прыжок на бряк 1.

Вот, в общем-то, и вся программа ;).

############################################################

FatMoon [HI-TECH group]

Сам себе компилятор

(или руководство по мазохизму для дзенствующих)

Предисловие

FatMoon, ты извращенец! Знаешь об этом?
Serrgio

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

Когда я сел за компьютер, подозрения перешли в твердую уверенность. Да, похоже, что я извращенец. Хотя все относительно - видал я таких извращенцев, по сравнению с которыми я просто … пуританин! А писать на ассемблере под WINDOWS не извращение? Молчите? И в этом молчании я слышу глас рассудка. © А. Сапковский.
О чем эта статья? О программировании на ассемблере без компилятора, об отладчике DEBUG из стандартного пакета DOS/WINDOWS, о машинном коде и прочим смежным темам.
Alex FatMoon

Как обычно пишутся программы на ассемблере? В редакторе - не суть важно, каком - набирается исходный текст, содержащий директивы ассемблера, мнемокоды инструкций, определения данных и метки. Затем вызывается компилятор - не суть важно, какой - который транслирует исходный файл в объектный. Затем линкер собирает исполняемый файл из одного или нескольких объектных модулей. Знакомая картина, не правда ли? Однако необходимо ли все это для создания работоспособной программы? Нет, конечно же. Для создания .com - файлов вполне достаточно отладчика DEBUG. Вот об этом и будет речь.

Почти любой отладчик имеет функцию транслирования мнемокодов в машинный код с возможностью записи на диск набранного блока. Поэтому в принципе эти же приемы могут быть использованы и с другими утилитами. Почему все-таки я описываю DEBUG? Он есть практически на любой машине, где установлена любая из версий MS-DOS или WINDOWS. Он прост в использовании, не требователен к ресурсам, знает почти все машинные коды и даже поддерживает инструкции сопроцессора. Он - первый друг взломщика сейвов к играм и просто хакера. И вообще - одно из лучших творений МикроСофт. Лежит он, родимый, в директории C:\DOS или C:\WINDOWS\COMMAND - даже если ваши папки называются по-другому и расположены на других дисках, найти его нетрудно. Переходим к непосредственному использованию.

Программирование без компилятора

Русские сначала придумывают себе препятствия, а потом их преодолевают.
(известный факт)
Входите узкими вратами, ибо широки врата и просторен путь, ведущий в геену огненную.
(Евангелие от Матфея)

Да, если у вас есть компилятор, использовать что-то еще нет нужды. Но представим себе, что компилятора нет. Или мы только начали изучать ассемблер и в принципе знаем мнемокоды по богатому опыту программирования на БК, Спектруме или Микроше, но о директивах имеем только общее представление. И напряженно думаем, с какими ключами запускать этот tasm? И почему tasm выдает ошибки при компиляции, хотя все написано правильно (кто бы мог подумать, что ss: mov ax, [si] на самом деле должно быть записано как mov ax, ss:[si]?). Тогда это для вас. А также если есть желание понять, как же работает компилятор и зачем он все-таки нужен. Подумав (от "DOOM") и перекрестясь (наведя перекрестье на последнего из монстров, нажав "fire primary" и выйдя из игры), приступим!

Пример программы.

Главная команда отладчика, которую будем использовать, "А"ssembly. Итак, входим в отладчик, набираем

    a 100

и видим адрес ххх:0100. От нас ждут ввода инструкций. Собственно, можно набирать программу. Пример:

    a 100
       mov ah,09
       mov dx,0100
       int 21
       ret
       db "Hello, world!$"

и готово. Однако это еще не конец. Надо скорректировать dx - 0100 туда засылается просто для того, чтобы обозначить команду. Ведь когда мы пишем:

    mov dx,xxx

мы еще не знаем адреса, с которого начнется строка "Hello, world!$", не так ли? Этот адрес мы узнаем только после того, как введем "ret". Итак, запоминаем в cx длину нашей программы - она равна адресу инструкции после строки, где мы нажали "Enter", чтобы выйти из режима компиляции, за вычетом 0100 - адрес, с которого начинается программа типа "COM". У меня для вышеприведенной программы получилось 016h. Значит, набираем:

    rcx
       16

А теперь корректируем адрес - строка начинается с 0108. Набираем:

    a 102
       mov dx,108

Осталось сохранить программу на диске:

    n hello.com
       w

Вуаля! Теперь в директории, где мы находились перед вызовом отладчика, есть файл hello.com длиной в 22 байта, исправно печатающий при запуске строку "Hello, world!" и возвращающий управление ОС. Просто, не так ли? И никаких компиляторов. На самом деле в роли такового выступили мы сами - назначили строке фиктивный адрес (0100), затем заменили его на точный (0108). Вся помощь отладчика заключалась в переводе мнемокодов в машинные. Не можем же мы помнить все машинные коды!

Это кажется элементарным, не спорю. Попробуем сделать что-нибудь более сложное, и затем попытаемся оптимизировать это. То есть, обычная процедура для любой программы - сначала "рыхлый" код, от которого добиваются работоспособности, затем оптимизация. Лично я придерживаюсь именно такой тактики, и нахожу ее достаточно удобной. Для примера выведем бегущую строку на обычный текстовый экран 80*25 - разрешение, к которому все привыкли (надеюсь) в Norton Commander. Не претендуя на оригинальность, скажу, что нам потребуется две строки в качестве данных/переменных. Одна из строк будет оригиналом, а вторая - служить для хранения подстроки. Алгоритм весьма прост:

  • Цикл: вырезать подстроку выбранной длины;
  • переместить курсор на выбранное место на экране;
  • вывести подстроку;
  • сделать задержку;
  • сдвинуться в строке на символ;
  • если не конец строки, то продолжать цикл.

Но перед тем, как набивать в отладчике код, я несколько облегчу вам работу.

Перенаправление ввода-вывода в ДОС.

Каждый раз набивать программу в отладчике было бы очень долго и может навсегда отвратить от программирования. К счастью, в ДОС (и, в частности,("в частности" убрать) в коммандной строке Windows (согласен, переставить слово надо)) можно использовать перенаправление стандартного ввода-вывода. Как это делается? Пишем текстовый файл, например, содержащий предыдущую программу "Hello, world!", в виде:

    ----8<---
       a 100
       mov ah,09
       mov dx,108
       int 21
       ret
       db "Hello, world!$"

    rcx
       16
       n hello.com
       w
       q

    --->8---

Не забудьте пустую строку после DB (она выводит отладчик из режима ассемблирования), и не забываем поставить в конце команду выхода! Сохраняем его под именем, например, hello.dbg и пишем в командной строке:

    debug < hello.dbg

В результате отладчик берет команды не с клавиатуры, а из файла. Вводит программу на языке ассемблера начиная с адреса 0100, и сохраняет на диск под именем hello.com. И выходит в ДОС. А что, почти обычная программа - только вместо

    ORG 0100h

имеем

    a 100

и еще команды в конце.

Точно также, чтобы получить красивый листинг при дизассемблировании, не обязательно делать PrintScreen после команды "u". Поскольку это наша программа и мы знаем, где находится код, а где данные, создаем файл d_hello.dbg, содержащий следующие строки:

    u 100,107
       d 108,115
       q

И набираем в командной строке

    debug hello.com < d_hello.dbg > hello.diz

Здесь мы перенаправляем как ввод (из файла d_hello.dbg), так и вывод (в файл hello.diz).

В результате получаем еще и файл hello.diz, содержащий дизассемблированную программу.

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

[прим.: Но это ещё не всё! В отличие от "простого" компилятора DEBUG может также сразу и выполнять (почти "интерпретировать") нашу "программу" (по аналогии с бейсиком), поэтому сохранять результат трансляции в отдельный файл нет необходимости - достаточно вместо команд RCX и N поставить G=100, не забыв завершить нашу программу инструкцией INT 3, если мы желаем увидеть содержимое регистров, с которым наша программа завершается. Итак, делаем следующий файл:
        a
             xor ax,ax
             mov es,ax ; сегмент 0
             es:
             mov ax,[46c] ; адрес таймера в области данных BIOS
             es:
             mov dx,[46e]
             int 3 ; DX:AX содержат значение системного таймера

        g=100
             q

сохраняем это под именем ticks, даём команду DEBUG<TICKS>RESULT, и наблюдаем в файле RESULT в регистрах DX и AX значение системного таймера. Впрочем, конкретно для данного случая наша программа может быть "короче":

        d 0:46c l 4
             q

Использование внешнего файла и комментариев в нём также облегчает "настройку" адресов. Например, в исходном файле на месте меток можно вставить соответствующий комментарий, а в инструкциях перехода вместо имён меток будет фиктивное значение. Разумеется, в исходном файле не должно быть команды G, ведь программа ещё не завершена. Например:

        a
             mov ax,100
             mov cx,100
             ;top:
             dec ax
             loop 100 ; top
             int 3

        ;g=100
             q

После первого "прохода" (выполнения DEBUG с перенаправлением ввода-вывода) следует проанализировать вывод на наличие сообщений об ошибках. Потом следует проверить значения закомментированных меток и скопировать эти значения в соответствующие места (в данном случае значение метки top нужно вставить после инструкции loop). Теперь можно расскоментарить команду G и программа готова. :)]

Вывод "бегущей строки" - создание программы с помощью отладчика.

Итак, набираем сразу в виде файла, чтобы потом "скомпилировать" его отладчиком.

    ---------Start of file------------
       a 100
       mov cx,555
       push cx
       mov si,200
       sub cx,555
       sub si,cx
       mov di,400
       mov cx,14
       rep movsb
       mov byte ptr [di],24
       mov bh,0
       mov dx,0c1f
       mov ah,02
       int 10
       mov dx,400
       mov ah,09
       int 21
       mov ah, 86
       inc cx
       inc cx
       xor dx,dx
       int 15
       mov ah,01
       int 16
       pop cx
       jnz 0140
       loop 0103
       jmp 0100
       ret
       db " ******** This is a long long string."
       db " It can be up to 64K long! You never see such long string"
       db " before! Enjoy it. Here may be some of your text and "
       db "advertizing. Just type what you want together"
       db " with WISPA chocolate! This program is debug handwork."
       db " All rights reserved. Decompilation and dizassembly "
       db " prohibited! The length of this string now 477 bytes"
       db " but length of code that print them is only 59 bytes."
       db " Sorry for my english, I write as I can. Alex FatMoon. $"

    rcx
       400
       n r_string.com
       w
       q
       ------------End of file----------

Не забудьте поставить пробел перед долларом - он нужен, что бы конец строки затирал символы, остающиеся от предыдущего вывода. Сделаю некоторые пояснения:

  • 555 - обозначил длину строки. На самом деле тут должно быть число, равное "адрес конца строки" - "адрес начала строки".
  • 0200 - обозначим адрес начала строки. Мы его пока не знаем, но потом в листинге легко найдем.
  • 0400 - адрес начала подстроки. Подстрока начнется сразу после конца строки. Этот адрес мы тоже пока не знаем.
  • 0140 - адрес инструкции "ret". Он идет непосредственно перед строкой. И его тоже пока не знаем.

Сохраняем на диске 1К, поскольку точной длины программы опять же не знаем. Для тех, кто ввел очень длинную строку - можете сохранять не 0400h, а ff00h. Наверняка где-то в начале этого блока все-таки уместилась наша "гигантская" программа.

Теперь загружаем в отладчик почти готовый r_string.com, и дизассемблируем, отмечая исправления, которые надо внести: строка начинается с 013ch, длина всей программы 0219h, длина строки = 0319h-013ch=01ddh, адрес "ret" - 013bh. Вносим изменения - можно прямо в отладчике, можно в текстовом файле. Все должно работать. Оптимизация, очистка экрана в начале и прочее - на ваше усмотрение.

[прим.: как упоминалось выше, для этого ничего дизассемблировать не нужно, достаточно выполнить один проход "компиляции" с перенаправлением вывода, после чего значения всех меток станут известны]

Послесловие

Блаженны больные, ибо исцелятся.
(почти из Библии)
И тебя вылечат… И меня вылечат!
("Иван Васильевич меняет профессию")

Вот собственно для этого и нужен компилятор - назначить адреса меткам и переменным и правильно вставить их в машинный код. Чем больше переменных и переходов, тем сложнее написать программу в отладчике. И слава богу, что это делать не обязательно, поскольку есть компиляторы. Если же читатель достаточно безумен, чтобы попробовать самостоятельно написать довольно объемистый проект, флаг в руки. Моего терпения хватило однажды на двух-килобайтную видеодемку, и я думаю, что писать программы под ДОС, используя только debug, вполне реально. Хотя и неудобно. А вот для написания небольших скриптов и тестовых фрагментов использование DEBUG в качестве компилятора - самое оно. И, кстати, при этом автоматом решается проблема лицензионности компилятора. J

При этом надо помнить несколько вещей:

  • смена сегментного регистра делается в две команды, поскольку это префикс, как и REP, LOCK и другие. То есть mov si, es:[di]
    записывается в виде
              es:
    
                  mov si, [di]
    
  • переходы типа short возможны лишь в пределах 128 байт выше или ниже. Из-за этого могут быть проблемы при вводе программ вышеописанным способом.
  • отладчик не выдаст ошибки, если какая-то инструкция неправильна. То есть, выдаст, но мы ее можем не увидеть, если используем перенаправленный ввод.
  • некоторые машинные коды 186-го, 286-го, 386-го и выше процов к сожалению остаются для debug'а неизвестными, а именно:
              pusha
    
                  popa
    
                  push <immediate>
    
                  bound
    
                  arpl
    
    • все команды, появившиеся в 486 и позднее
    • все команды, использующие 32-битную адресацию и операнды
    • insb / insw
    • и некоторые другие.

Проблему можно решить, занося непосредственно код через

    db <opcode>

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

© Alex Antipoff aka FatMoon, 2002.