Передовица » Макулатура » ИиО » Такой же, только без крыльев.

Такой же, только без крыльев. (Полудетективная статья) (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

* * *

Использование материалов проекта agatcomp без получения предварительного письменного разрешения agatcomp запрещено.


Почта для обратной связи: mail@agatcomp.ru


Живое общение по теме Агата: Telegram группа Agatcomp.


Накопленные знания и проекты: тематический ФОРУМ.


© 2004-2024 agatcomp.su / agatcomp.ru

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *