Содержание статьи
«Фундаментальные основы хакерства»
Мы публикуем эту статью в честь начала предзаказов обновленной версии книги Криса Касперски «Фундаментальные основы хакерства», получившей подзаголовок «Анализ программ в среде Win64».
В первой части статьи ты вкратце познакомишься с основными инструментами отладки, а вторая — это отрывок из книги. Он уже публиковался в «Хакере», но теперь доступен без платной подписки.
Купить книгу ты можешь на сайте издательства «Солон‑пресс».
Примечание: Текст статьи обновлен с учетом работы с x86-64.
Этап 1
Если работа ведется в Linux, среди инструментов поверхностного анализа можно отметить такие приложения:
- file определяет тип файла, анализируя его поля;
- readelf отображает информацию о секциях файла;
- ldd выводит список динамических библиотек, от которых зависит данный исполняемый файл;
- nm выводит список декорированных имен функций, которые создаются компиляторами языков, поддерживающих перегрузку функций. Затем утилита может декодировать эти имена;
- c++filt преобразует декорированные имена функций в первоначальные с учетом передаваемых и получаемых аргументов;
- strings выводит строки, содержащиеся в файле, с учетом заданного шаблона.
В Windows со многими из этих задач справляется утилита dumpbin, входящая в поставку Visual Studio.
Если мы получили общее представление о файле и не нашли ничего полезного для взлома, есть смысл переходить к следующему этапу.
Этап 2
Второй этап — это дизассемблирование. Его цель — обнаружить защитный механизм. Существует два вида дизассемблирования: статическое и динамическое (более известное как отладка или трассировка). В первом случае изучается дизассемблерный код, полученный с помощью автоматического анализа дизассемблером исполняемого файла без его запуска.
Динамическое дизассемблирование предполагает запуск программы и ее пошаговое исполнение. Обычно отладка проходит в специальной и обособленной среде. Это нужно на случай, если программа представляет какую‑то угрозу для системы.
В качестве отладчика в Linux можно воспользоваться старым добрым GDB либо средствами трассировки в Radare2. В Windows выбор тоже невелик: OllyDbg постепенно устаревает и не обновляется. В нем отлаживать приложения можно только в режиме пользователя. После смерти SoftICE единственным нормальным отладчиком в Windows стал WinDbg. В нем можно отлаживать драйверы на уровне ядра.
Статические дизассемблеры тоже делятся на две группы: линейные и рекурсивные. Первые перебирают все сегменты кода в двоичном файле, декодируют и преобразуют их в мнемоники языка ассемблера. Так работает большинство простых дизассемблеров, включая objdump и dumpbin. Может показаться, что так и должно быть. Однако, когда среди исполняемого кода встречаются данные, возникает проблема: их не надо преобразовывать в команды, но линейный дизассемблер не в состоянии отличить их от кода! Мало того, после того как сегмент данных завершится, дизассемблер будет рассинхронизирован с текущей позиции кода.
С другой стороны, рекурсивные дизассемблеры, такие как IDA Pro, Radare2 и Ghidra, ведут себя иначе. Они дизассемблируют код в той же последовательности, в которой его будет исполнять процессор. Поэтому они аккуратно обходят данные, и в результате большинство команд благополучно распознается.
Но и здесь есть ложка дегтя. Не все косвенные переходы легко отследить. Следовательно, какие‑то пути выполнения могут остаться нераспознанными. Помогает то, что современные дизассемблеры применяют разные эвристические приемы с учетом конкретных компиляторов.
Этап 3
Когда адрес защитного механизма найден, можно приступать к третьему этапу — хирургическому. Существует несколько видов непосредственного взлома. Самый простой вариант — воспользоваться шестнадцатеричным редактором.
Существует множество HEX-редакторов на любой вкус и цвет, например 010 Editor, HexEdit, HIEW. Вооружайся одним из них, и тебе останется только перезаписать команду по найденному адресу. Но в двоичном файле сильно не разбежишься! Существующий код не дает простора для манипуляций, поэтому нам приходится умещаться в имеющемся пространстве.
Противопоказано раздвигать или сдвигать команды, даже выкидывать имеющиеся конструкции можно только с большой осторожностью. Ведь подобные манипуляции с кодом приведут к сдвигу смещений остальных команд, а указатели в таком случае будут указывать в космос!
Этим способом, однако, можно хакнуть большое количество защитных механизмов. А как быть, если надо не просто взломать программу, а добавить или заменить в ней какие‑то функции? Понятно, что HEX-редактор тут тебе не друг.
Здесь на помощь приходит техника оснащения двоичного файла. Оно позволяет вставить почти неограниченный блок кода почти в любое место файла. Понятно, что без посторонних средств этого не добиться. Оснащение реализуют специальные библиотеки, предоставляющие гибкие API.
С помощью статического оснащения производится модификация файла непосредственно на диске. То есть бинарный файл сначала надо дизассемблировать, найти подходящее место, обладающее достаточным пространством для включения полезной нагрузки, и, внедряя потусторонний код, уследить, чтобы не поломались указатели на код и данные. В этом деле призваны помочь такие инструменты, как DynInst и PEBIL. После модификации файла он записывается в измененном виде обратно на диск.
Динамическое оснащение работает по другому принципу. Подобно отладчикам движки динамического оснащения следят за выполняемыми процессами и вносят код прямо в память. Поэтому при динамическом оснащении не нужно ни дизассемблирование, ни изменение бинарника, от чего одним махом исчезают все негативные последствия.
Однако во время динамического оснащения, так как программа выполняется «под наблюдением», ее производительность заметно падает. Для динамического оснащения используются системы DynamoRIO (совместный проект HP и MIT) и Pin (Intel).
Может показаться, что динамическое оснащение лучше во всех отношениях. А вот и нет! Такую программу нельзя будет исполнить на другом компьютере, где нет никаких специальных средств. Тогда как в процессе статического оснащения мы заранее готовим бинарник и передаем его пользователю. Теми же средствами могут пользоваться и злоумышленники, желающие заразить программу вирусом или снабдить какой‑то подлой функцией.
Практический взлом
Чтобы поупражняться на практике, проведем анализ и раскусим элементарную защиту.
Шаг первый. Разминочный
Алгоритм простейшего механизма аутентификации заключается в посимвольном сравнении введенного пользователем пароля с эталонным значением, хранящимся либо в самой программе (как часто и бывает), либо вне ее, например в конфигурационном файле или реестре (что встречается реже).
Достоинство такой защиты — крайне простая программная реализация. Ее ядро состоит фактически из нескольких строк, которые на языке С/C++ можно записать так:
if (strcmp(введенный пароль, эталонный пароль)){/* Пароль неверен */}else{/* Пароль ОК*/}
Давай дополним этот код процедурами запроса пароля и вывода результатов сравнения, а затем испытаем полученную программу на прочность, т. е. на стойкость к взлому. В Visual Studio создай консольный проект и скомпилируй следующий код, установив целевую платформу x64.
Пример простейшей системы аутентификации:
#include "stdafx.h"// Простейшая система аутентификации// Посимвольное сравнение пароля#include <stdio.h>#include <string.h>#define PASSWORD_SIZE 100#define PASSWORD "myGOODpassword\n"// Этот перенос нужен затем, чтобы// не выкусывать перенос из строки,// введенной пользователемint main(){// Счетчик неудачных попыток аутентификацииint count=0;// Буфер для пароля, введенного пользователемchar buff[PASSWORD_SIZE];// Главный цикл аутентификацииfor(;;){// Запрашиваем и считываем пользовательский парольprintf("Enter password:");fgets(&buff[0],PASSWORD_SIZE,stdin);// Сравниваем оригинальный и введенный парольif (strcmp(&buff[0],PASSWORD))// Если пароли не совпадают — «ругаемся»printf("Wrong password\n");// Иначе (если пароли идентичны)// выходим из цикла аутентификацииelse break;// Увеличиваем счетчик неудачных попыток// аутентификации и, если все попытки// исчерпаны, завершаем программуif (++count>3) return -1;}// Раз мы здесь, то пользователь ввел правильный парольprintf("Password OK\n");}
В популярных кинофильмах крутые хакеры легко проникают в любые жутко защищенные системы, каким‑то непостижимым образом угадывая искомый пароль с нескольких попыток. Почему бы не попробовать пойти их путем?
Не так уж редко пароли представляют собой осмысленные слова наподобие Ferrari, QWERTY, имена любимых хомячков, названия географических пунктов и т. д. Угадывание пароля сродни гаданию на кофейной гуще — никаких гарантий на успех нет, остается рассчитывать на одно лишь везение. А удача, как известно, птица гордая — палец в рот ей не клади. Нет ли более надежного способа взлома?
Раз эталонный пароль хранится в теле программы, то, если он не зашифрован каким‑нибудь хитрым образом, его можно обнаружить тривиальным просмотром двоичного кода программы. Перебирая все встретившиеся в ней текстовые строки, начиная с тех, что больше всего смахивают на пароль, мы очень быстро подберем нужный ключ и откроем им программу! Причем область просмотра можно существенно сузить — в подавляющем большинстве случаев компиляторы размещают все инициализированные переменные в сегменте данных (в PE-файлах он размещается в секции .
или .
). Исключение составляют, пожалуй, ранние компиляторы Borland с их маниакальной любовью всовывать текстовые строки в сегмент кода — непосредственно по месту их вызова. Это упрощает сам компилятор, но порождает множество проблем. Современные операционные системы, в отличие от старушки MS-DOS, запрещают модификацию кодового сегмента, и все размещенные в нем переменные доступны лишь для чтения. К тому же на процессорах с раздельной системой кеширования они «засоряют» кодовый кеш, попадая туда при упреждающем чтении, но при первом же обращении к ним вновь загружаются из медленной оперативной памяти в кеш данных. В результате — тормоза и падение производительности.
Что ж, пусть это будет секция данных! Остается только найти удобный инструмент для просмотра двоичного файла. Можно, конечно, нажать клавишу F3 в своей любимой оболочке (FAR, DOS Navigator) и, придавив кирпичом Page Down, любоваться бегущими циферками до тех пор, пока не надоест.
Можно воспользоваться любым HEX-редактором (qView, HIEW... — кому какой по вкусу), но в данном случае, по соображениям наглядности, приведен результат работы утилиты dumpbin из штатной поставки Microsoft Visual Studio. Запустить dumpbin можно из Developer Command Prompt или другой консоли.
Натравим утилиту на исполняемый файл нашей программы, содержащей пароль, и попросим ее распечатать содержащую только для чтения инициализированные данные секцию — rdata
(ключ /
) в «сыром» виде (ключ /
), указав значок > для перенаправления вывода в файл (ответ программы занимает много места, и на экране помещается один лишь «хвост»).
dumpbin /RAWDATA:BYTES /SECTION:.rdata passCompare1.exe > rdata.txt
0000000140002240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000140002250: 40 40 00 40 01 00 00 00 E0 40 00 40 01 00 00 00 @@.@....□@.@....
0000000140002260: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF □□□□□□□□□□□□□□□□
0000000140002270: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00 Enter password:.
0000000140002280: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00 myGOODpassword..
0000000140002290: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00 Wrong password..
00000001400022A0: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00 Password OK.....
00000001400022B0: 40 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 @...............
00000001400022C0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Среди всего прочего тут есть одна строка, до боли похожая на эталонный пароль. Испытаем ее? Впрочем, какой смысл — судя по исходному тексту программы, это действительно искомый пароль, открывающий защиту, словно золотой ключик. Слишком уж видное место выбрал компилятор для его хранения — пароль не мешало бы запрятать и получше.
Один из способов сделать это — насильно поместить эталонный пароль в собственноручно выбранную нами секцию. Такая возможность не предусмотрена стандартом, и потому каждый разработчик компилятора (строго говоря, не компилятора, а линкера, но это не суть важно) волен реализовывать ее по‑своему или не реализовывать вообще. В Microsoft Visual C++ для этой цели предусмотрена специальная прагма data_seg
, указывающая, в какую секцию помещать следующие за ней инициализированные переменные. Неинициализированные переменные по умолчанию располагаются в секции .
и управляются прагмой bss_seg
соответственно.
В примере аутентификации выше перед функцией main
добавим новую секцию, в которой будем хранить наш пароль (для удобства я создал отдельный проект — passCompare2
):
// С этого момента все инициализированные// переменные будут размещаться в секции .kpnc#pragma data_seg(".kpnc")#define PASSWORD_SIZE 100#define PASSWORD "myGOODpassword\n"char passwd[] = PASSWORD;#pragma data_seg()
Внутри функции main
проинициализируем массив:
// Теперь все инициализированные переменные// вновь будут размещаться в секции по умолчанию,// т. е. .rdatachar buff[PASSWORD_SIZE]="";
Немного изменилось условие сравнения строк в цикле:
if (strcmp(&buff[0],&passwd[0]))
Натравим утилиту dumpbin на новый исполняемый файл:
dumpbin /RAWDATA:BYTES /SECTION:.rdata passCompare2.exe > rdata.txt
0000000140002230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0000000140002240: 40 30 00 40 01 00 00 00 E0 30 00 40 01 00 00 00 @0.@....□0.@....
0000000140002250: FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF □□□□□□□□□□□□□□□□
0000000140002260: 45 6E 74 65 72 20 70 61 73 73 77 6F 72 64 3A 00 Enter password:.
0000000140002270: 57 72 6F 6E 67 20 70 61 73 73 77 6F 72 64 0A 00 Wrong password..
0000000140002280: 50 61 73 73 77 6F 72 64 20 4F 4B 0A 00 00 00 00 Password OK.....
0000000140002290: 40 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 @...............
00000001400022A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
Ага, теперь в секции данных пароля нет и хакеры «отдыхают»! Но не спеши с выводами. Давай сначала выведем на экран список всех секций, имеющихся в файле:
dumpbin passCompare2.exe
Summary
1000 .data
1000 .kpnc
1000 .pdata
1000 .reloc
1000 .rsrc
1000 .text
Нестандартная секция .
сразу же приковывает к себе внимание. А ну‑ка посмотрим, что там в ней.
dumpbin /SECTION:.kpnc /RAWDATA passCompare2.exe
RAW DATA #5
0000000140005000: 6D 79 47 4F 4F 44 70 61 73 73 77 6F 72 64 0A 00 myGOODpassword..
Вот он, пароль! Спрятали, называется... Можно, конечно, извратиться и засунуть секретные данные в секцию неинициализированных данных (.
) или даже секцию кода (.
) — не все там догадаются поискать, а работоспособность программы такое размещение не нарушит. Но не стоит забывать о возможности автоматизированного поиска текстовых строк в двоичном файле. В какой бы секции ни содержался эталонный пароль, фильтр без труда его найдет (единственная проблема — определить, какая из множества текстовых строк представляет собой искомый ключ; возможно, потребуется перебрать с десяток‑другой потенциальных кандидатов).
Шаг второй. Знакомство с дизассемблером
Пароль мы узнали. Но как же утомительно вводить его каждый раз с клавиатуры перед запуском программы! Хорошо бы ее хакнуть так, чтобы никакой пароль вообще не запрашивался или любой введенный пароль программа воспринимала как правильный.
Хакнуть, говорите?! Что ж, это несложно! Куда проблематичнее определиться, чем именно ее хакать. Инструментарий хакеров чрезвычайно разнообразен, чего тут только нет: и дизассемблеры, и отладчики, и API-, и message-шпионы, и мониторы обращений к файлам (портам, реестру), и распаковщики исполняемых файлов, и... Сложновато начинающему кодокопателю со всем этим хозяйством разобраться!
Впрочем, шпионы, мониторы, распаковщики — второстепенные утилиты заднего плана, а основное оружие взломщика — отладчик (динамический дизассемблер) и дизассемблер (статический).
Итак, дизассемблер применим для исследования откомпилированных программ и частично пригоден для анализа псевдокомпилированного кода. Раз так, он должен подойти для вскрытия парольной защиты passCompare1.exe. Весь вопрос в том, какой дизассемблер выбрать.
Не все дизассемблеры одинаковы. Есть среди них и «интеллектуалы», автоматически распознающие многие конструкции, как то: прологи и эпилоги функций, локальные переменные, перекрестные ссылки и т. д., а есть и «простаки», чьи способности ограничены одним лишь переводом машинных команд в ассемблерные инструкции.
Логичнее всего воспользоваться услугами дизассемблера‑интеллектуала (если он есть), но... давай не будем спешить, а попробуем выполнить весь анализ вручную. Техника, понятное дело, штука хорошая, да вот не всегда она оказывается под рукой, и неплохо бы заранее научиться работе в полевых условиях. К тому же общение с плохим дизассемблером как нельзя лучше подчеркивает «вкусности» хорошего.
Воспользуемся уже знакомой нам утилитой dumpbin, настоящим «швейцарским ножиком» со множеством полезных функций, среди которых притаился и дизассемблер. Дизассемблируем секцию кода (как мы помним, носящую имя .
), перенаправив вывод в файл, так как на экран он, очевидно, не поместится:
dumpbin /SECTION:.text /DISASM passCompare1.exe > code-text.txt
Заглянем еще раз в секцию данных (или в другую — в зависимости от того, где хранится пароль).
Запомним найденный пароль: myGOODpassword
. В зависимости от версии и настроек дизассемблера dumpbin инициализированные переменные, к которым обращается код, могут быть представлены по‑разному: на их месте могут быть или символьные константы, или непосредственно шестнадцатеричное смещение. Попробуем найти выявленный ранее пароль в дизассемблированном листинге тривиальным контекстным поиском с помощью любого текстового редактора.
00000001400010B2: 48 8D 15 C7 11 00 lea rdx, [??_C@_0BA@PCMCJPMK@myGOODpassword?6@]
00
00000001400010B9: 48 8D 4C 24 20 lea rcx,[rsp+20h]
00000001400010BE: E8 8C 0D 00 00 call strcmp
00000001400010C3: 85 C0 test eax,eax
00000001400010C5: 74 58 je 000000014000111F
Есть совпадение! Центральная часть этого листинга — вызов функции strcmp
. В качестве параметров ей передаются две строки, размещенные в регистрах RCX
и RDX
. Как мы видим, в первой строке листинга в RDX
помещается эталонный пароль, а во второй строке в регистр RCX
копируется значение из локальной переменной — очевидно, строка, введенная пользователем.
В результате выполнения, если строки равны, функция strcmp
возвращает 0 в регистре RAX
или EAX
, как в данном случае. Если же строки различаются, возвращается значение, отличающееся от нуля.
В следующей строке листинга инструкция test
проверяет значение в регистре EAX
на равенство нулю. Если ответ положительный, что может произойти, только когда строки одинаковые, флаг ZF
устанавливается в единицу.
Следующая инструкция JE
осуществляет переход по указанному адресу только в том случае, когда флаг ZF
равен единице. Посмотрим, на какой код указывает этот адрес:
000000014000111F: 48 8D 0D 7A 11 00 lea rcx, [??_C@_0N@MBEFNJID@Password?5OK?6@]
00
0000000140001126: E8 E5 FE FF FF call printf
Это код вывода строки Password
. В регистр RCX
помещается указанная строка, затем значение этого регистра передается функции printf
, которая выводит переданную строку на экран. Из этого следует, что перед нами валидная ветвь программы.
Рассмотрим обратный исход, когда строки различаются, strcmp
возвращает ненулевое значение, флаг ZF
остается сброшенным (равным нулю) и переход не осуществляется. Тогда мы проваливаемся в такой код:
00000001400010C7: 66 0F 1F 84 00 00 nop word ptr [rax+rax]
00 00 00
00000001400010D0: 48 8D 0D B9 11 00 lea rcx, [??_C@_0BA@EHHIHKNJ@Wrong?5password?6@]
00
00000001400010D7: E8 34 FF FF FF call printf
Он выводит строку о неправильном пароле — тоже с помощью функции printf
, в параметре принимающей строку для отображения.
Оперативные соображения следующие: если команду JE
заменить JNE
, то программа отвергнет истинный пароль как неправильный, а любой неправильный пароль воспримет как истинный. А если TEST
заменить XOR
, то после исполнения этой команды регистр EAX
будет всегда равен нулю, какой бы пароль ни вводился.
Дело за малым — найти эти самые байтики в исполняемом файле и малость подправить их.
Хирургическое вмешательство
Как мы обсуждали выше, внесение изменений непосредственно в исполняемый файл — дело серьезное. Стиснутым существующим кодом, нам приходится довольствоваться только тем, что есть, и ни раздвинуть команды, ни даже сдвинуть их, выкинув из защиты «лишние запчасти», не получится. Ведь это привело бы к сдвигу смещений всех остальных команд, тогда как значения указателей и адресов переходов остались бы без изменений и стали бы указывать совсем не туда, куда нужно!
Ну, с «выкидыванием запчастей» справиться как раз таки просто — достаточно забить код командами NOP (опкод которой 0x90, а вовсе не 0x0, как почему‑то думают многие начинающие кодокопатели), т. е. пустой операцией (вообще‑то NOP — это просто другая форма записи инструкции XCHG
, если интересно). С «раздвижкой» куда сложнее! К счастью, в PE-файлах всегда присутствует множество «дыр», оставшихся от выравнивания, в них‑то и можно разместить свой код или свои данные.
Но не проще ли просто откомпилировать ассемблированный файл, предварительно внеся в него требуемые изменения? Нет, не проще, и вот почему: если ассемблер не распознает указатели, передаваемые функции (а как мы видели, наш дизассемблер не смог отличить их от констант), он, соответственно, не позаботится должным образом их скорректировать, и, естественно, программа работать не будет.
Приходится резать программу вживую. Легче всего это делать с помощью утилиты HIEW, которая «переваривает» PE-формат файлов и упрощает тем самым поиск нужного фрагмента. Обрати внимание, так как мы работаем в 64-битной среде, подойдет только одна из новых версий программы с поддержкой файлов PE32+. Например, я использую версию 8.67, прекрасно уживающуюся с Windows 10. Запустим ее, указав имя файла в командной строке (hiew32
), двойным нажатием клавиши Enter, переключимся в режим ассемблера и при помощи клавиши F5 перейдем к требуемому адресу. Как мы помним, команда TEST, проверяющая результат на равенство нулю, располагалась по адресу 0x1400010C3.
Чтобы HIEW мог отличить адрес от смещения в самом файле, предварим его символом точки: .
.
00000001400010C3: 85 C0 test eax,eax
00000001400010C5: 74 58 je 000000014000111F
Ага, как раз то, что нам надо! Нажмем клавишу F3 для перевода HIEW в режим правки, подведем курсор к команде TEST EAX, EAX и, нажав клавишу Enter, заменим ее командой XOR
.
00000001400010C3: 31 C0 xor eax,eax
00000001400010C5: 74 58 je 000000014000111F
С удовлетворением заметив, что новая команда в аккурат вписалась в предыдущую, нажмем клавишу F9 для сохранения изменений на диске, а затем выйдем из HIEW и попробуем запустить программу, вводя первый пришедший на ум пароль:
>passCompare1
Enter password:Привет, шляпа!
Password OK
Получилось! Защита пала! Хорошо, а как бы мы действовали, если бы у нас не было HIEW? Тогда, вооружившись каким‑нибудь шестнадцатеричным редактором (например, HxD), пришлось бы прибегнуть к контекстному поиску.
Загрузим подопытный файл в редактор. Конечно, если пытаться найти последовательность 85
— код команды TEST
, ничего хорошего из этого не выйдет — этих самых «тестов» в программе может быть несколько сотен, а то и больше. А вот адрес перехода, скорее всего, во всех ветках программы различен, и подстрока TEST
имеет хорошие шансы на уникальность. Попробуем найти в файле соответствующий ей код: 85
. В HxD для этого надо нажать Ctrl-F, чтобы открыть окно поиска, затем перейти на вкладку «Hex-значения» и искать уже отсюда.
Оп‑с! Найдено только одно вхождение, что нам, собственно, и нужно. Давай теперь попробуем модифицировать файл непосредственно в HEX-режиме, не переходя в ассемблер. Возьмем себе на заметку: инверсия младшего бита кода команды приводит к изменению условия перехода на противоположное, т. е. 74
→ 75
. Подведи курсор к значению 74
и введи 75
. Значение будет выделено красным. Далее надо сохранить результат работы. Готово!
Работает? В смысле защита свихнулась окончательно — не признает истинные пароли, зато радостно приветствует остальные. Замечательно!