Comments 8
Для использования нужно запускать в консоли команды вида:
mutator in.nes out.nes keyword 100
, где keyword используется как randseed (то есть для одного и того же слова будет один и тот же набор случайных мутаций), а число задаёт количество мутаций (конкретно сколько байт будет испорчено).using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
class NesFile
{
public string FileName;
public byte[] Header;
public byte[] Trainer;
public List<byte[]> PRG;
public List<byte[]> CHR;
public byte[] Attach;
public NesFile(string filename)
{
FileName = filename;
var ROM = File.ReadAllBytes(FileName);
if (ROM[0] != 'N' || ROM[1] != 'E' || ROM[2] != 'S' || ROM[3] != 0x1A)
{
throw new Exception("Invalid iNES header");
}
Header = new byte[16];
Array.Copy(ROM, 0, Header, 0, 16);
var offset = 16;
if ((Header[6] & 0x04) != 0)
{
Trainer = new byte[512];
Array.Copy(ROM, offset, Trainer, 0, Trainer.Length);
offset += Trainer.Length;
}
PRG = new List<byte[]>();
for (var i = 0; i < Header[4]; i++)
{
PRG.Add(new byte[16384]);
Array.Copy(ROM, offset, PRG[i], 0, PRG[i].Length);
offset += PRG[i].Length;
}
CHR = new List<byte[]>();
for (var i = 0; i < Header[5]; i++)
{
CHR.Add(new byte[8192]);
Array.Copy(ROM, offset, CHR[i], 0, CHR[i].Length);
offset += CHR[i].Length;
}
if (ROM.Length > offset)
{
Attach = new byte[ROM.Length - offset];
Array.Copy(ROM, offset, Attach, 0, Attach.Length);
}
}
public void PrepareHeader()
{
Header[4] = (byte)PRG.Count;
Header[5] = (byte)CHR.Count;
}
public void Save(string filename = null)
{
PrepareHeader();
if (filename != null)
{
FileName = filename;
}
using (var fs = File.OpenWrite(FileName))
{
fs.Write(Header, 0, Header.Length);
if (Trainer != null)
{
fs.Write(Trainer, 0, Trainer.Length);
}
for (var i = 0; i < PRG.Count; i++)
{
fs.Write(PRG[i], 0, PRG[0].Length);
}
for (var i = 0; i < CHR.Count; i++)
{
fs.Write(CHR[i], 0, CHR[0].Length);
}
if (Attach != null)
{
fs.Write(Attach, 0, Attach.Length);
}
}
}
}
class Mutagen
{
static void Main(string[] args)
{
if (args.Length < 4) return;
var nes = new NesFile(args[0]);
var rnd = new Random(args[2].GetHashCode());
var crb = Int32.Parse(args[3]);
for (int i = 0; i < crb; i++)
{
var prg = rnd.Next(nes.PRG.Count);
var pos = rnd.Next(nes.PRG[prg].Length);
var shift = rnd.Next(-2, +2);
int value = nes.PRG[prg][pos] + shift;
nes.PRG[prg][pos] = (byte)(value & 0xFF);
}
nes.Save(args[1]);
}
}
Ежели у Вас селекция, то у нас — ампутация.Не понял, почему вы говорите, что у меня селекция. Я сам занимался именно реверс-инжинирингом оригинальной менюшки, и в статье об этом сказано. Поскольку кода было немного (всего 16 килобайт), получилось разобраться в её работе на 100%, и на её основе сделать демку.
Есть какие-то предположения, сколько потребовалось проб и ошибок для создания такого хака? Это все, полагаю, вручную проверялось?
Как делал? Просто, но долго. Корруптором.Потом в процессе разговора он уточнил суть используемого им метода:
Но не всегда случайные и не всегда случайным образом. Можно определённые на определённые. Но если ты не знаешь АСМ, дебаггеры и т.д., то только так.Видимо, «случайные» правки всё же как-то были приправлены интуицией. Как видно, использованная программа позволяет задавать некоторые правила для «мутаций»:
Я сам в детстве (15 лет назад) просто исправляя в HEX-редакторе байты, которые мне просто «не нравились», умудрился снять требование регистрации в одной из вариаций игры «О, счастливчик!» (я писал свой вариант этой игры, и взламывал «конкурента», который тоже был написан школьником). Просто очень сильно повезло. Плюс много терпения и сотни перезапусков с разными исправлениями :)
Сделал подобную программу для игр Sega Mega Drive, инвертирующую инструкции условного перехода (равно <---> не равно). Из-за того, что процессор Сеги обращается к памяти только по чётным адресам, портится меньше областей, не относящихся к инструкциям.
Нужная инструкция находится посекторной порчей: портим половину рома, восстанавливаем и портим вторую половину. Потом первую половину первой половины, вторую половину первой половины и так до конца.
При каждой порче пользователь проверяет наличие нужного ему изменения в эмуляторе и выбирает один из трёх пунктов:
1 баги мешают увидеть есть ли нужное изменение - область остаётся на будущее изучение
2 баги не мешают увидеть, что нужного изменения нет - эта область рома больше не трогается
3 баги не мешают увидеть, что нужное изменение есть. В этой области начинается двоичный поиск инструкции
На это могло уходить два часа, поэтому искал способы уменьшить поломки игры.
Оказалось, что достаточно портить инструкции в случайном порядке. В итоге теперь получается найти нужное за 20-30 минут.
Примеры
(в скобках - количество шагов)
Ultimate Mortal Kombat 3 (USA)
Чтобы секретные меню были изначально, без ввода комбинации. Оказалось, что инструкций понадобится две - одна отображает эти меню (56), другая даёт по ним ходить (34). Потом обнаружилась ещё одна альтернативная, активирующая эти меню при нажатии "вниз" (127), будто была набрана полная комбинация
Streets of Rage (W) (REV00) [!]
Секретное меню выбора уровня (19)
Выбор одинаковых персонажей (48)
Streets of Rage 2 (USA)
Секретное меню выбора уровня, на этот раз понадобились три инструкции: отображение пункта меню (83), доступ к нему сверху (156) и доступ снизу (45). Можно было ограничиться только одной стороной или вообще без отображения, но хотелось сделать красиво
Streets of Rage 3 (USA)
На этот раз аж 5 инструкций: доступ сверху (60), доступ снизу (43), а отображение - уже комбинация инструкций: нет номера уровня и пункта Exit (18) + есть номер и Exit (46) + ещё одна инструкция. Последнюю нашёл только предварительно изменив первые 2 инструкции в роме, т.к. она сама по себе, без сочетания с остальными, не находима - визуально ничего не меняет в игре
Contra - Hard Corps (USA, Korea) (En)
Выбор одинаковых персонажей - 2 инструкции: для нажатия "вверх" или "влево" (114) и "вниз" или "вправо" (217)
Shining Force II (U)
Секретное меню (26)
Режим управление врагами (185)
Методы модификации машинного кода: «селекция» vs. «генная инженерия»