≡ Передовица » Макулатура » ИиО » Такой же, только без крыльев.
Такой же, только без крыльев. (Полудетективная статья) (N1/1993)
На примере агатовских программ приведён пример "белого" хака - изменения свойств скомпилированной программы при отсутствии исходных текстов.
Начало статьи опубликовано в N1, окончание в N2. Здесь статья целиком. А.Новиков. Cтудент III курса МПИ. Группа сайта просит вас связаться с нами! (ЗАЧЕМ ЭТО?) Ни для кого не секрет, что ПЭВМ "Агат" имеет более скудное программное обеспечение, чем его IBM-совместимые коллеги. Поэтому подбор программного средства для решения конкретной задачи на "Агате" представляет довольно сложную задачу, особенно для начинающего пользователя, "коллекция" программ которого ещё не очень богата. Программист постоянно встречается с программами, которые могут сделать много полезного, но не умеют какую-нибудь мелочь. Но для конкретной задачи эта "мелочь" является решающей, и не позволяет полноценно применять это программное средство. Пользователь оказывается в положении никулинского героя, вопрошающего: "А нет ли у вас такого же, только без крыльев?". Такая ситуация легко объясняется тем, что разработчик программного средства просто не в силах предусмотреть всё и "объять необъятное". Однако это объяснение мало утешает. Можно, конечно, пользоваться несколькими системными средствами для решения одной задачи, однако время, затраченное на переход от одной системы к другой, резко снижает КПД программиста, и кроме того (и это самое главное), какую-нибудь операцию может не позволить произвести ни одна из имеющихся в наличии программ. Думаю, вы уже поняли, к чему этот разговор. Если вы уверены в своих силах, то можете попытаться встроить свой собственный драйвер даже в программу, составленную не вами. Как правило, программные средства написаны на ассемблере, и достать их исходный модуль обычному пользователю не представляется возможным. Однако если вы владеете ассемблером, то сможете вставить "заплату" в готовую программу. Данная статья поможет вам в этом. Подключение новых блоков - довольно сложная, но в то же время увлекательная процедура, особенно для любителей... детективных романов. Программист, выступающий в роли следователя, должен пройти практически все этапы следствия (правда, не всегда в том же порядке), и раскрыть "преступление". Непродуманные действия программиста, как и следователя, могут закончить дело "глухарём". Однако задача практически всегда имеет множество решений, и достаточно найти хотя бы одно из них. Итак, приступим. 1. Определение мотивов преступления Сначала попытаемся чётко сформулировать, что именно нас не устраивает в имеющейся программе. Например, надо изменить клавиши управления, установить другой цвет вывода, обеспечить работу программы с другим типом принтера, и т.д. Понять, что надо сделать - задача, по важности приравниваемая к самому решению. 2. Составление фоторобота преступника Формулировка задачи должна помочь вам понять, в какой именно блок программы необходимо вмешательство. Например, изменение рабочих клавиш лучше всего произвести в блоке опроса клавиатуры и анализа кодов: изменение цвета - в блоке, выводящем эту информацию; переход к другому принтеру - или непосредственно в подпрограмме печати байта, или (если сообщений мало) - в самих сообщениях. Определив блок, подумайте: а) как бы вы написали этот блок? б) Без каких частей алгоритма разработчик программы никак не мог обойтись? Например, блок работы с клавиатурой может выглядеть так: опрос клавиатуры → последовательное сравнение полученного кода с эталонными → условный переход при совпадении; вывод строки на экран или принтер - передача адреса начала сообщения (или смещения относительно начала другой строки) подпрограмме вывода; подпрограмма опроса пультов: включение одновибратора → циклический опрос состояний пультов. 3. Поиск орудий преступления А теперь вам предстоит ответить на вопрос: какие последовательности байтов обязательно встретятся в изменяемом блоке. На первый взгляд, вопрос кажется абсурдным: ведь это зависит от конкретной реализации конкретного алгоритма. Но если вы чётко представляете себе связь между командой ассемблера и её представлением в машинных кодах, то, думаю, сможете ответить на поставленный вопрос. Например, если программа пользуется подпрограммами вывода символа на экран COUT или COUT1 (СМ Бейсика, адреса $FDDC и $FDD7), то в ней встретятся комбинации байтов $DC, $FD или $D7, $FD. Если изменяется цвет или режим вывода (то есть байт атрибута текста), то почти всегда происходит обращение к ячейке $32, и в программе тоже встретится байт $32. И даже если вы не смогли придумать характерных байтов для самого блока, можно попробовать "танцевать от печки", хотя это и дольше. Например, вы "ничего такого" не знаете о блоке программы, знаете только, что в работающей программе он вызывается клавишей "S". В этом случае в программе после опроса клавиатуры произойдёт сравнение с шаблонными значениями, в том числе и с "S". А этот шаблон наиболее вероятно отобразится в памяти байтом $53 или $D3. Аналогично, если вам известен какой-либо числовой параметр (например, число <жизней> в игровой программе), то соответствующий байт с большой вероятностью встретится в программе. Не следует, однако, забывать о том, что существуют индексированные адресации. Например, если программа опрашивает пульты 0 и 1, то комбинации байтов $65, $C0 в программе может и не быть, как нет и прямого обращения в ячейке SC065. Обычно пульты опрашиваются обращением к $C064, X, и в X помещается номер опрашиваемого пульта. Конечно, теоретически можно и клавиатуру опросить через ячейку $BF80, X при X-80, но практической пользы это не даёт, и поэтому маловероятно. А вот числовой параметр вполне может отличаться от своего "отражения" в памяти с гораздо большей вероятностью. Например, если машина N раз выполняет какое-либо действие, то соответствующий цикл может быть запущен от 1 до N, от N до 1, а может и от 0 до N-1. И к тому же конец цикла может быть организован по знакам "больше" и "больше или равно". Поэтому в некоторых случаях число N в памяти не отобразится. 4. Выявление подозреваемых лиц Несложно предугадать следующий этап "следствия": поиск фрагментов программы, содержащих характерные байты, найденные в предыдущем пункте. Лучше всего воспользоваться командами "H" и "Y" "отладчика". Найденные адреса памяти желательно переписать на бумагу. Попутно можно осуществлять визуальный "отсев" наименее вероятных адресов (однако при этом можно "зевнуть" и то место в программе, ради которого устроен поиск). Чем длиннее последовательность искомых байтов, тем меньше количество "попутных" фрагментов, не несущих нужной информации, но содержащих такие же комбинации байтов. Кроме того, если вы выявите несколько независимых комбинаций, присутствующих в исходном фрагменте, то сравнивая карты распределения их по памяти, можно указать расположение фрагмента с большей вероятностью. 5. Следственный эксперимент Теперь нужно выяснить, какой именно из найденных фрагментов входит в искомый блок. Конечно, можно внимательно изучить каждый из них, но лучше поступить иначе. Надо поочерёдно менять содержимое найденных байтов таким образом, чтобы при работе программы можно было понять, когда она проходит изменённый участок. Например, из обращения к ячейке $C000 легко можно сделать обращение к $C030, и теперь машина будет издавать звуковой сигнал при проходе через соответствующее место программы; команду ISR можно перенаправить на подпрограмму BELL и т.д. Однако если вы заменили информацию не в том участке программы, то это приведёт к непредсказуемым последствиям, вплоть до зависания машины. Поэтому перед проведением подобных экспериментов не поленитесь вынуть дискету из дисковода (или хотя бы откройте крышку НГМД) во избежание порчи информации на диске. 6. Досье на преступника Найдя методом проб и ошибок изменяемый блок, не помешает проверить, насколько он соответствует той схеме, которую вы для него предложили. Необходимо выяснить назначение команд хотя бы в непосредственной близости от найденной последовательности байтов. Особенно это важно при косвенном поиске фрагмента. Если назначение команд нельзя понять с первого взгляда (а обычно это именно так), рекомендую распечатать на принтере по крайней мере один экран дизассемблированных команд (или не полениться переписать его от руки, если принтера нет). В бумажном листинге можно поставить комментарии, облегчающие понимание текста программы. 1. Напротив обращений к адресам, назначения которых вам известны (ячейки ввода-вывода, п/п СМ и ДОС, ячейки нулевой страницы) желательно указать, к какому устройству происходит обращение и его цель (опрос клавиатуры, включение фазы F10 НГМД, считывание позиции верха текстового окна, обращение к п/п печати символа и т.д.). 2. Все ветвления программы (особенно близкие условные переходы) желательно указать на листинге стрелками (от команды ветвления до команды с адресом, на который производится переход). Программисты считают, что легче написать свою программу, чем разобраться в чужой. Поэтому данный этап является одним из самых сложных во всей цепи "расследования". 7. Разработка плана захвата Теперь, когда вы собрали всю необходимую информацию о "преступнике", его пора "брать". Для начала ответьте на вопрос: будете ли вы в будущем пользоваться неизменённой программой? (Например, корректное введение нового режима в любой редактор не должно помешать его работе - в крайнем случае новшеством можно не пользоваться. И соответственно, не придётся использовать неизменённый редактор. Если же речь идёт о внесении таких изменений в программы общего назначения, которые в некоторых ситуациях не только не полезны, но и вредны, на вопрос надо ответить утвердительно.) Если вы ответили "нет", то будьте уверены: новый блок можно "вживить" в программу. Если "да", то перед вами два пути: или храните две версии программы (изменённую и неизменённую), или создайте собственный драйвер, который при запуске будет сам вносить изменения в предварительно загруженную основную программу (например, для внесения небольших изменений в крупные программы, типа Бейсика, СМ или ДОС). 8. Захват Чаще всего применяется один из трёх методов "захвата": 1. Изменение параметра. 2. Добавление фрагмента. 3. Подмена фрагмента. Наиболее простой - первый метод. Он основан на подмене в памяти одного или нескольких байтов. Для этого метода характерно сохранение длины файла и отсутствие "довесков" к программе. Однако и возможности его невелики - возможно лишь количественное изменение параметров процесса, его качественная сторона не меняется. (В крайнем случае, часть команд может быть ликвидирована заменой на команды NOP ($ЕА) или изменено используемое устройство ($C030$C020)). Вот, например, подпрограмма BELL в СМ Бейсика, издающая сигнал "БИП" (здесь и далее программы написаны для Бейсик - HELLO к "Агат-7" с семибитным представлением текста): FCB4- A9 40 LDA #$40 ; начальная FCB6- 20 2B FB JSR $FB2B ; задержка FCB9- A0 C0 LDY #$C0 ;количество проходов FCBB- A9 0C +-> LDA #$0C ; Задержка на период FCBD- 20 2B FB ! JSR $FB2B ; колебаний динамика FCC0- AD 30 C0 ! LDA $C030 ;звук FCC3- 88 ! DEY ; цикл до FCC4- D0 F5 +-- BNS $FCBB ; исчерпания Y FCC6- 60 RTS ;выход из п/п А вот программа, изменяющая тон и длительность издаваемого звука: 10 INPUT "ТОН (0-255)";TN 20 INPUT "ДЛИТЕЛЬНОСТЬ (0-255)";DL 30 *$1800: 40 ! STA $C200 50 ! LDA # TN 60 ! STA $FCBC 70 ! LDA # DL 80 ! STA $FCBA 90 ! STA $C220 100 ! RTS 110 !: 120 CALL $1800 Команда в строке 40 открывает банк ПсевдоПЗУ по записи, команды в строках 50-80 изменяют содержимое двух ячеек СМ, а в 90 банк ПсевдоПЗУ снова закрывается. Именно "банковские операции" мешают этой программе воспользоваться оператором POKE и обойтись без Макроассемблера. Если вы захотите проверить свои силы в этом методе - попробуйте заставить курсор Бейсика мигать в 2 раза чаще. Ответ вы найдёте в конце статьи. Метод добавления фрагмента предоставляет пользователю более богатый спектр возможностей, хотя это и создаёт дополнительные неудобства. Принцип подключения таков: вместо одной или нескольких команд изменяемой программы вставляется ссылка на блок пользователя, а закончив свою работу, этот блок "отдаёт долг" (выполняет те команды, вместо которых произведено подключение). Рассмотрим этот метод подробнее. Если в месте программы, выбранном для подключения, есть трёхбайтная команда, то проблем не возникает. Например, команду 3E28- AD 00 C0 LDA $C000 легко преобразовать в команды 3E28- 20 00 80 JSR $8000 ............................. 8000- AD 00 C0 LDA $C000 8003- 30 02 BMI $8007 8005- A9 00 LDA #$00 8007- 60 RTS и оттранслировать блок пользователя с любого адреса памяти (в данном случае, с $8000. Если программа полностью расположена в ОЗУ, то новый блок лучше всего пристыковать к "хвосту" программы.). Приведённый пример полезен при запуске на "Агат-9" программ, написанных для "Агат-7". Как видите, "выполнение долга" здесь происходит в начале блока, так как здесь оно нужно самому встраиваемому драйверу. Перехват производится командами IMP или ISR. ISR позволяет вернуться к прерванной программе через команду RTS, но требует особой внимательности при обращении со стеком. IMP относится к этому более спокойно, но требует выхода из вставленного блока через IMP, а это на 2 байта длиннее, чем через RTS. Зато если трёхбайтовой командой, через которую произведено подключение, тоже была IMP, то выполнение "долга" и возврат в основную программу выполняются единой IMP. Если же в нужном месте программы нет трёхбайтной последовательности, то произвести подключение будет несколько сложнее. Обычно команду IMP или ISR вставляют вместо команд длиной 2+1 или 1+2 байта; команды 65E2- A2 80 LDX #$80 65E4- 88 DEY 65E5- A9 00 LDA #$00 преобразуются в 65E2- 20 00 80 JSR $8000 65E5- A9 00 LDA #$00 ............................. 8000- 38 SEC ............сам блок......... 8123- A2 80 LDX #$80 8125- 88 DEY 8126- 60 RTS Если же в нужном месте программы идут только двухбайтные команды, можно вместо трёх байтов записать ссылку на встраиваемый блок, а четвёртый байт заменить на $EA, т.е. NOP (в случае, если пользуетесь ISR). Однако далеко не к любому месту программы можно подключиться. Рассмотрим основные ограничения: 1) Если ссылка на блок вставлена вместо двух команд, убедитесь, что на вторую из них в исходной программе не было ссылок. 2) Проверьте, не попало ли обращение к новому блоку в тело нежелательного цикла, так как это может привести не только к многократным ненужным вызовам блока, но и к существенному замедлению работы программы. 3) Среди команд, взятых в "долг", нежелательно присутствие команд условной передачи управления, так как из своего нового положения в памяти они, скорее всего, до адреса перехода "не дотянутся", а использование в комплекте с ними IMP займёт несколько лишних байтов памяти. 4) Ещё раз напоминаю об осторожности при использовании, особенно если "в долг" берётся команда, воздействующая на стек. 5) И уж совсем неприятным будет вмешательство в блоки программ, работающие в реальном времени. Если изменения в подпрограмме генерации звука приведут только к искажению издаваемого тона, то "доработка" драйверов магнитофона или НГМД может привести не только к неверному чтению данных с них, но и к непоправимой потере ранее записанных ценных данных. Особо следует поговорить о сохранении содержимого регистров. В общем случае рекомендую сохранять в стеке или в ячейках памяти все регистры, которые в ходе работы вставленного блока могут измениться, и восстанавливать их значения перед выходом из блока (кроме тех, разумеется, ради изменения которых был написан этот блок). Однако в этом не всегда есть необходимость. Например, если вы изменяете содержимое регистра X и сразу после адреса подключения блока стоит команда LDX, то понятно, что сохранять и восстанавливать содержимое X не нужно. Ещё менее "капризным" является регистр P. Если сразу после выхода из нового блока нет команды условного перехода, то P, как правило, можно не сохранять, так как большинство флагов получают свои значения непосредственно перед ветвлением (однако флаг C может при этом "сыграть с вами плохую шутку" - помните об этом). Метод подмены фрагмента принципиально похож на только что рассмотренный метод его добавления. Разница состоит лишь в том, что после выполнения своей функции новый блок не "исполняет долги" и не возвращает управление той точке программы, с которой он был вызван. Понятно, что такой перехват управления удобнее производить через IMP. (Впрочем, если новый блок не длиннее того, вместо которого он вставляется, то вполне разумно будет на адреса памяти старого блока вписать новый (или какую-нибудь его часть. В этом случае в перехвате управления вообще не будет необходимости.). В заключение приведу пример программы, использующей данный метод. Эта программа встраивает в Бейсик "горячие клавиши", и после её запуска многие часто набираемые команды интерпретатора вызываются простым нажатием одной из функциональных клавиш (список команд приведёт в буфере BUFF как последовательность их КОИ, разделённых $00). 100 SVX = 0: SVY = 1 110 PBF = 2: PSB = 3 1000 * $8000: 1010 ! LDA #0 ! STA PSB 1020 ! STA $C200 ! LDA #$4C ! STA $FD0D 1030 ! LDA # > BEG ! STA $FD0E ! LDA # < BEG ! STA $FD0F 1040 ! STA $C220 ! RTS 2000 ! BEG: STY SVY ! LDY PSB ! RD: LDA BUFF,Y 2010 ! BNE EST ! LDA $19 ! JSR $F85E ! JSR $FD12 2020 ! CMP #$A0 ! BPL RT 2030 ! STX SVX ! STA PBF ! LDX #$7F ! LDY #0 2040 ! C1: LDA BUFF,Y ! BNE NX ! INX ! CPX PBF 2050 ! BEQ KN ! NX: INY ! BNE C1 ! KN: LDX SVX 2060 ! INY ! BNE RD ! EST:INY ! STY PSB ! LDY SVY ! RT: RTS 3990 ! BUFF: $008000C5D8 4000 ! $C5C300C3 4010 ! $C1D4C1CCCFC78D00 4020 ! $8300840085008600 4030 ! $8700880089008A00 4040 ! $8B008C008D008E00 4050 ! $8F00CCC9D3D400CC 4060 ! $CFC1C400D3C1D6C5 4070 ! $00D2D54E00D2D54E 4080 ! $8D00950096009700 4090 ! $980099009A009B00 4100 ! $D4C5D8D4BD008D00 4110 ! $8E00AAA400000000 4990 ! : 5000 CALL $8000 А для тех, кто заинтересовался предложенным ранее заданием, сообщаю ответ: в ячейку $ED2B надо занести значение $1F1F, и сделать это можно так: 10 * $1800: 20 ! STA $C200 30 ! LDA #$1F 40 ! STA $FD2B 50 ! STA $0220 60 ! RTS 70 ! : 80 CALL $1800 * * ** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * |