Методи модифікації машинного коду: «селекція» vs. «генна інженерія»

Цей пост — 5-в-1! Він зачіпає такі теми, як: генна інженерія, реверс-інжиніринг, ненормальне програмування, ностальгія за Dendy і емуляція NES. Як же такі різні теми могли зустрітися разом? Ласкаво просимо під кат!



Про міфи щодо ГМО тут вже писали. В додаток до цього я хотів би поділитися відмінною аналогією з розбором реальних прикладів з області реверс-інжинірингу та модифікації машинного коду, що буде зрозуміло і близько саме програмістам.

«Мутації» машинного коду
В якості прикладу візьмемо приставку NES (відому у нас як Dendy), в якій використовується процесор 6502. Система команд у нього дуже проста — опкод представлений завжди одним байтом, і кожен з 256 хоч щось, так робить. Ніяких «захист» від дурня не передбачено, і майже будь-який випадковий набір байт буде виконуватися без опору з боку процесора. Таким чином, ми можемо взяти ROM якоїсь гри, виправити в ньому випадкові біти (будемо називати це «мутаціями») — і після запуску спостерігати кумедні глюки в різних несподіваних місцях, але при цьому в цілому гра швидше за все буде працездатною. Схоже, що на YouTube є цілий жанр подібного відео. Отриманий таким чином машинний код напевно не дуже коректний, але в більшості випадків процесор зможе його виконати і щось зробити.
<habracut/>
Як виявилося, таку методику використовують не тільки для веселощів (а грати в знайомі ігри з несподіваними глюками досить кумедно), але і для одержання цілком собі конкретних модифікацій: роблять велику кількість «мутантів» і шукають той, в якому проявився потрібний ефект. Точь-в-точь як у сучасних методів селекції, коли зародки організмів піддаються впливу мутагенів (що призводить до випадкових змін у генетичному коді), а потім з того що зміг вирости відбираються ті, у яких є потрібний ознака. Отримані таким чином організми отримують в доважок масу інших небажаних мутацій. Позбавляються від них шляхом поступового схрещування c нормальним виглядом, домагаючись отримання більш-менш нормальної організму з потрібною ознакою і мінімумом інших мутацій, які виявилися помітні. Те ж саме можна зробити і з машинним кодом.

«Генна інженерія» на машинному коді
В якості прямої аналогії тут напрошується реверс-інжиніринг. Ми вивчаємо машинний (двійковий) код, розбираємося як він працює (повністю або лише деякі його частини), після чого вносимо саме ті зміни, які напевно приведуть нас до цільового ефекту.

У програмістів існує величезна кількість інструментів для аналізу машинного коду (всілякі дізассемблер, отладчики тощо). Не дивлячись на те, що сам по собі машинний код не призначений для читання людиною, він був придуманий людьми і задокументований, так що створення інструментів для перетворення його в більш зрозумілий вигляд (асемблерний лістинг) не було проблемою. Також практично будь-яка досліджувана програма скоріше всього теж була написана людиною, тому те, що робить її код зазвичай виглядає логічним і зрозумілим.

Ясна річ, що генетикам набагато складніше. Немає ніякої офіційної документації, яка могла б пролити світло на всі виникаючі питання. Генетичний код був написаний» волею випадку відібрано за принципом «змогло воно вижити, пристосуватися і дати потомство», тому, напевно, він далекий від логічного й оптимального, скоріше навіть будь-яка придумана нами обфускація програмного коду здасться просто дитячим лепетом. Проте це не означає, що генетичний код неможливо вивчати і пробувати його змінювати.

Передісторія
Для мене Dendy завжди була чимось більшим, ніж просто приставкою. Я не тільки грав в неї, але і значний час провів всередині неї з паяльником в руках для деяких простих модифікацій. По дорозі куди-небудь я часто розмірковував про те, як створюються ці ігри і як це працює всередині. Напевно, багато хто з вас теж колись задавалися подібними питаннями, така вже натура майбутніх IT-шників.

Пройшли роки. З деякою періодичністю занурювався в ему-тему, вивчаючи все нове на тематичних сайтах, але я не наважувався зануритися у вивчення асемблера 6502 і архітектури NES. Внутрішній конфлікт раціонального та ірраціонального. Я довго переконував себе, що мені не потрібно витрачати на це час, але… зірвався. Дивлячись на те, які цікаві речі роблять ентузіасти ему-сцени, я взявся за свою давню ідею зі світлою думкою: «Я теж зможу!».

Напевно багато хто з вас пам'ятають картриджі типу «9999-in-1», які були примітні красивим меню з романтичним сюжетом біля моря, літаючими чайками і приємною музикою (Unchained Melody). Я дизассемблировал це меню і зробив на його основі триб'ют-демку Unchained Nostalgia.



Основна ідея — ніякого списку ігор, а слайди перемикаються під музику. У звільненому небі я намалював осмислені хмари і зірки; реалізував автоматичне перемикання слайдів синхронно з музикою і можливість ручного перемикання; додав автоматичне виправлення швидкості відтворення і висоти звуку для систем, відмінних від Dendy (адже це меню робилося саме для китайських фамиклонов, а вони відрізнялися від оригінальних NES, з-за чого оригінальне меню працює дуже швидко на NES NTSC, а на NES PAL виходить занадто низький звук); не встояв перед спокусою додавання цілого ряду цікавих сюрпризів. Тобто використовуваний підхід нічим не обмежує фантазію і дозволяє вносити будь-які зміни за якесь кінцеве час.

Проте, схожі ідеї приходять в голову різним людям
Вже після релізу своєї демки я виявив, що хтось теж пробував реалізувати цю ідею. Автор вказав, що він використовував «corruptor», який зазвичай використовується для створення зіпсованих версій ігор з різними глюками.

Якби мені раніше хтось сказав, що подібний «випадковий» метод реально використовується для модифікації машинного коду з потрібними ефектами — я б, напевно, не повірив. Адже для цього потрібно багато часу і удача. Хоча з іншого боку, немає необхідності вивчати і розуміти машинний код. Оскільки у мене в руках уже був «живий» приклад подібного хака, нічого не залишалося, крім як дослідити його роботу, щоб зрозуміти, як воно взагалі працює.



Слайди перемикаються автоматично, приблизно кожні 2 секунди (що занадто швидко). Ручного управління немає. При зміні слайдів екран чомусь мерехтить двічі. Але воно працює! Але яким чином?

Сліди хірургічного втручання
У картриджі програмний код і тайли для графіки зберігаються окремо. Код — PRG ROM, тайли — CHR ROM. Другий може бути легко змінений з використанням стандартних інструментів, з цієї причини для Dendy існує величезна кількість графічних хаків відомих ігор, де код залишено без змін.

Порівняємо CHR ROM оригінального меню і досліджуваного хака:

На обличчя сліди прямого «хірургічного» втручання. З якоїсь причини були вирізані тайли цифр, крапки та маленької чайки. Спробуємо повернути оригінальний CHR ROM і подивимося, що саме було приховано таким чином.



Тепер стало трохи зрозуміліше. У програмному коді був зламаний enter, з-за чого менюшка думає, що кнопка «вниз» завжди натиснута. Також був зламаний висновок назв ігор. Мабуть, зламати висновок номерів ігор і маленькою чайки (це курсор, що вказує на обрану гру) не вийшло, тому їх довелося ховати вирізанням відповідних тайлів в CHR ROM.

… і трохи магії!
Копаємо глибше. За допомогою дизассемблера подивимося, що саме роблять виправлені в PRG ROM байти.
Код оригіналу Код хака
NMI:
 
PHA
 
TXA
 
PHA
 
TYA
 
PHA
 
LDA $1E
 
BEQ loc_C181
 
JSR sub_C592
 
loc_C181:
 
LDA #0
 
JSR <font color="red">sub_C428</font>
NMI:
 
PHA
 
TXA
 
PHA
 
TYA
 
PHA
 
LDA $1E
 
BEQ loc_C181
 
JSR sub_C592
 
loc_C181:
 
LDA #0
 
JSR <font color="red">sub_C445</font>
Це обробник переривання NMI, який викликається при початку показу чергового кадру. Як видно, було виправлено адресу виклику функції. Оригінальна функція займалася зчитуванням стану всіх кнопок і записуванням їх значень в один байт у вигляді бітової маски. Новий виклик виконує тільки останню інструкцію до цієї функції, яка зберігає результат з регістра Y в змінну з маскою натиснутих в даний момент кнопок. Таким чином, використовується відро значення регістра Y, яке залишилося після виконання попереднього коду. У більшості випадків тут виявляється відро значення 0x07, яке в двійковому вигляді виглядає як 00000111 і відповідає натиснутим одночасно кнопки «Вниз», «Вліво» і «Вправо» (код основний ігнорує «Вліво» і «Вправо», обробляючи кнопку «Вниз»). Однак, відразу після перемикання слайдів в Y залишається відро значення 0xFF, що означає, що всі кнопки були натиснуті одночасно, з-за чого в підсумку викликається налагоджувальний код, який в оригінальному меню (за Left+Start+B) виводить номер ревізії на екран, що супроводжується відключенням PPU на один кадр. Це і викликає подвійне мерехтіння екрана.
Код оригіналу Код хака
LDA #0
 
STA $2000
 
STA $2001
 
<font color="red">LDA</font> #$22
 
STA $2006
 
LDA #$AC
 
STA $2006
LDA #0
 
STA $2000
 
STA $2001
 
<font color="red">SBC</font> #$22
 
STA $2006
 
LDA #$AC
 
STA $2006
Тут зламаний код виведення номера ревізії за Left+Start+B, тому хоч цей налагоджувальний код і виконується кожен раз з-за описаної вище проблеми, номери ревізії ми все одно не бачимо на екрані. Вийшло це забавним чином: інструкція, що встановлювала в регістр A значення 0x22, була замінена на інструкцію, яка віднімає з регістра A число 0x22. Оскільки регістр A до цього завжди 0, виходить число 0xDE. Оригінальний код встановлював для PPU початок виведення за адресою 0x22AC у відеопам'яті, що знаходиться в межах першої екранної сторінки. Новий код, виходить, встановлює адресу 0xDEAC, що знаходиться далеко за межами доступної відеопам'яті (максимальний дозволений адреса $3FFF), і в результаті виводиться напис «в нікуди».
Код оригіналу Код хака
ASL A
 
TAX
 
LDA $EAB3,X
 
STA <font color="red">$0A</font>
 
LDA $EAB4,X
 
STA $0B
 
LDY #0
 
loc_C380:
 
LDA (<font color="red">$0A</font>),Y
 
BEQ loc_C38B
 
STA $2007
 
INY
 
JMP loc_C380
ASL A
 
TAX
 
LDA $EAB3,X
 
STA <font color="red">$FA</font>
 
LDA $EAB4,X
 
STA $0B
 
LDY #0
 
loc_C380:
 
LDA (<font color="red">$FA</font>),Y
 
BEQ loc_C38B
 
STA $2007
 
INY
 
JMP loc_C380
Тут у нас був зламаний код, який виводить назви ігор. При запису вказівника на виводиться рядок молодший байт 16-розрядного покажчика записується за адресою 0xFA замість 0x0A, а старший байт пишеться як і повинен — за адресою 0x0B. Пощастило, що 0xFA не використовувалося, та це нічого не зламало. Далі програма читає рядок, на яку вказує пара байт 0xFA і 0xFB (0xFB завжди 0). За щасливим збігом обставин, всі отримувані таким чином покажчики вказують на заповнену нулями пам'ять, тому рядка з назвою ігор не виводяться.

Висновки
Ви змогли відчути, як усі «хвацько закручена» в модифікаціях, створених випадковим чином? Одна поломка підпирає іншу поломку, і воно якось в цілому дає результат, близький до потрібного. При цьому з'являються просто неймовірні залежності між випадковими і на перший погляд ніяк не зв'язаними ділянками коду, і при щонайменших подальших модифікаціях (навіть самих по собі коректних) все може завалитися. Повноцінний реверс-інжиніринг набагато надійніше. Те ж саме можна сказати і про генної інженерії, у порівнянні з традиційною селекцією.

Посилання на файли
  • unchained_nostalgia.zip — демка Unchained Nostalgia, створена методами реверс-інжинірингу.
  • guyver_hack.zip — досліджуваний хак, створений «випадковим» чином, і його вихідний ROM.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.