Работа с файлами неизвестных форматов. Анализ структуры, написание редактора.
Так получилось, что основы программирования, равно, как и вся теория, прошли мимо меня стороной. И вот столкнулся я с необходимостью разобрать неизвестный формат файла. Понятно, что под расширением DAT может скрываться что угодно. И, вероятно, это архив. Заглянул WinHex'ом (я его впервые вижу, пользоваться не умею) - и впрямь, вначале идут имена файлов "лесенкой" (в текстовом виде).
Код:
28 04 00 00 AC 46 00 00 41 62 79 73 73 2E 64 6C 72
Вроде, понятно:
28 04 - идетификатор заголовка файла.
00 00 - разделитель (или часть следующего)
AC 46 - вероятно смещение на начало файла и\или длина
00 00 - очередной разделитель
41 62 79 73 73 2E 64 6C 72 - собственно имя файла, что видно в текстовом виде "Abyss.dlr"
Дальше идет следующий файл:
00 00 00 00 9C 5C 22 00 41 62 79 73 73 32 2E 64 6C 72
Потом имена файлов заканчиваются и идут запятые, точки и прочая кракозябра. На этом мои скромные аналитические способности тихо загибаются.
Что делать? Куда думать? Как думать? Как понять - кто оно, что оно, как работает и как с этим работать?..
Абалдеть! Я только что понял, что байты записываются в обратном порядке!.. Тоесть отображаются! И AC 46 - это на самом деле 46 AC и там действительно начинается запись чего-то нового. Вот так начнешь писать на форуме и оно приходит! :)
Но, не суть. Это частный случай. Да и текстовый вид помог. Но все-таки я вовсе не чувствую себя увернным и способным написать анпакер, а тем более пакер подобного архива.
Может быть, посоветуете какой-нибудь материал на тему? Почитать, поизучать. Какие-нибудь советы? Тонкости работы в HEX... Что-нибудь, чтобы научиться читать HEX, аки книгу...
Буду благодарен любой помощи и советам!
---
Да, и буду очень благодарен, если подскажите как с этим работать в реалиях C#. К примеру банально считать список файлов. Имеется ввиду правильно и удобно. А то я сейчас начну заводить List<byte>, искать первый 00, потом последний 00, выставлять позицию в -= 1, записывать циклом по байту в этот список, потом конвертировать в long... Думаю, это малость неправильно... Если можно - крошечный примерчик.
типы int или unsigned int это 4 байта, записанные в обратном порядке т.е. число 1 хранится как 01 00 00 00. Выложи побольше какойнить участок файла. Чтоб определить сжат или нет - поищи в dat'е кусок из распаковыного "архива".
http://www.mediafire.com/?b0ygu8s3g6ug89r
Честно говоря, не думаю, что он сжат... А вот этого не понял: 'поищи в dat'е кусок из распаковыного "архива".' Если смотреть на содержимое предполагаемых файлов, то точно не сжат - там даже виден внятный текст. Да и zip ужал его в 6 раз. Сомнительно.
Но, в любом случае, мне главное научиться с этим работать самостоятельно. Так что выслушаю все советы, прочитаю предложенные книги, с удовольствием поковыряюсь в небольших примерчиках. Главное, чтобы потом я мог разобраться в незнакомом формате самостоятельно. Собственно этот вот dat - только начало. Дальше идут жуткие файлы с расширением dlr, которые тоже придется разобрать и вот там уже придется как-то учиться это анализировать... Там может быть графика, а может и какие-то тех.данные. В общем, очень хочу научиться анализировать непонятно что и писать редакторы для работы с этим. Буду рад любой помощи.
P.S. За разъяснение по поводу int - спасибо! Понял! Запомню!.. А short, long (int16\64)?
---
Вот еще интересно: после имени файла идут \0. Но не 1, а произвольное количество. Почему? То 4, то 3, то 1... Или это для выравнивания для кратности 4?..
---
Все, осознал, там просто длина файла фиксированная - 12 байт... И почему умные мысли приходят только после того, как задашь вопрос на форуме?..
---
А первые 4 байта это, часом не количество ли файлов?.. Мда... вот тебе и неизвестный формат...
Может быть проще, если знать форматы других файлов - dli, dlr, dlv. Но я понятие не имею что это такое :) Погугли.
Дельный совет, но, боюсь, гугл промолчит. Вероятнеее всего это эксплюзивный формат одной ныне покойной game-студии. Впрочем, попытка не пытка. Я уже осознал, как там все устроено. Оказывается, все не так страшно, как казалось на первый взгляд. Сейчас нарисую анпакер и возьмусь за эти самый dli, dir, div. Так что I'll be back в самое ближайшее время. :)
Взгляните, пожалуйста, на софтинку, и скажите - в чем я не прав. Наверняка, что-нибудь можно было реализовать поизящнее!
Код:
class Program
{
static void Main(string[] args)
{
FileInfo file = new FileInfo(args[0]);
byte[] buff = new byte[17];
using (FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read))
{
stream.Read(buff, 0, 4);
UInt32 count = BitConverter.ToUInt32(buff, 0) - 1;
Console.WriteLine(String.Format("Распаковка файла: {0}", file.Name));
Console.WriteLine(String.Format("Количество файлов: {0}", count));
Console.WriteLine("-------------------");
FileHeader[] files = new FileHeader[count];
for (UInt32 index = 0; index <= count; index++)
{
stream.Read(buff, 0, 17);
FileHeader fs = new FileHeader(BitConverter.ToUInt32(buff, 0), Encoding.GetEncoding(1251).GetString(buff, 4, 12).TrimEnd(new char[] { '\0' }));
if (index != count)
{
files[index] = fs;
}
if (index != 0)
{
files[index - 1].lenght = fs.shift - files[index - 1].shift;
}
}
DirectoryInfo dir = new DirectoryInfo(file.FullName + "_");
if (!dir.Exists) dir.Create();
foreach (FileHeader s in files)
{
Console.WriteLine(String.Format("|Файл: {0} | Смещение: {1:x}| Длина: {2}", s.name, s.shift, s.lenght));
stream.Position = s.shift;
byte[] b = new byte[s.lenght];
stream.Read(b, 0, (int)s.lenght);
using (FileStream output = new FileStream(String.Format(@"{0}\{1}", dir.FullName, s.name), FileMode.Create, FileAccess.Write))
{
output.Write(b, 0, (int)s.lenght);
}
}
}
}
}
public class FileHeader
{
public UInt32 shift = 0;
public UInt32 lenght = 0;
public string name = "";
public FileHeader(UInt32 shift, string name)
{
this.shift = shift;
this.name = name;
}
}
{
static void Main(string[] args)
{
FileInfo file = new FileInfo(args[0]);
byte[] buff = new byte[17];
using (FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read))
{
stream.Read(buff, 0, 4);
UInt32 count = BitConverter.ToUInt32(buff, 0) - 1;
Console.WriteLine(String.Format("Распаковка файла: {0}", file.Name));
Console.WriteLine(String.Format("Количество файлов: {0}", count));
Console.WriteLine("-------------------");
FileHeader[] files = new FileHeader[count];
for (UInt32 index = 0; index <= count; index++)
{
stream.Read(buff, 0, 17);
FileHeader fs = new FileHeader(BitConverter.ToUInt32(buff, 0), Encoding.GetEncoding(1251).GetString(buff, 4, 12).TrimEnd(new char[] { '\0' }));
if (index != count)
{
files[index] = fs;
}
if (index != 0)
{
files[index - 1].lenght = fs.shift - files[index - 1].shift;
}
}
DirectoryInfo dir = new DirectoryInfo(file.FullName + "_");
if (!dir.Exists) dir.Create();
foreach (FileHeader s in files)
{
Console.WriteLine(String.Format("|Файл: {0} | Смещение: {1:x}| Длина: {2}", s.name, s.shift, s.lenght));
stream.Position = s.shift;
byte[] b = new byte[s.lenght];
stream.Read(b, 0, (int)s.lenght);
using (FileStream output = new FileStream(String.Format(@"{0}\{1}", dir.FullName, s.name), FileMode.Create, FileAccess.Write))
{
output.Write(b, 0, (int)s.lenght);
}
}
}
}
}
public class FileHeader
{
public UInt32 shift = 0;
public UInt32 lenght = 0;
public string name = "";
public FileHeader(UInt32 shift, string name)
{
this.shift = shift;
this.name = name;
}
}
Не увидь я прогресса в работе,я бы написал,что самому разбираться в неизвестных форматах–задача,как минимум,крайне сложная…Однако,увидя за такое короткое время написанный парсер,я взмолчу:)
Но тут все было до примитивизма просто. :) Вот дальше будет весело! :)
P.S. А если есть подобный опыт - поделись! Пригодится любая информация. :)
---
О! А заголовок то я могу считать единым массивом, ибо его длина = 4(количество записей) + 17*количество записей!
Цитата: Vitamant
Взгляните, пожалуйста, на софтинку, и скажите - в чем я не прав. Наверняка, что-нибудь можно было реализовать поизящнее!
Код:
Console.WriteLine(String.Format("Распаковка файла: {0}", file.Name));
Методы Write и WriteLine в качестве первого параметра могут принимать строку формата, поэтому можно записать чуть короче, без string.Format:
Код:
Console.WriteLine("Распаковка файла: {0}", file.Name);
Но это несущественно.
Цитата: Vitamant
А если есть подобный опыт - поделись! Пригодится любая информация. :)
Вот здесь показан разбор базы данных. Может пригодится.
http://forum.codenet.ru/threads/61199-%D1%84%D0%BE%D1%80%D0%BC%D0%B0%D1%82-%D0%91%D0%94
В целом - весьма похвально! Так держать!
З.Ы. Че это за игруха? Музон какой-то бешенный :)
Может не надо столько героических усилий прикладывать, лучше приберечь силы на что-то другое :)Попробуйте распаковать с помощью DragonUnpacker (он заточен для раздербанивания игровых потрохов).
1. Дабы у таких же новичков, как я, был пример работы с простенькими форматами файлов.
(Сам архив: http://www.mediafire.com/?bf7vyw3bzz4vcmz [40мб])
2. Дабы гуру подсказали - что еще можно улучшить. В частности очень не нравится сшивка файлов в конце упаковки. Наверняка ведь есть способ и по-лучше и по-быстрее (впрочем, и этот довольно быстрый).
Код:
namespace BoYRepacker
{
class Program
{
static void Main(string[] args)
{
Mode mode = Mode.Unpack; // режим работы - упаковка или распаковка
FileInfo file = new FileInfo("Z:\\z.tmp"); // DAT-архив
DirectoryInfo dir = new DirectoryInfo("Z:\\"); // папка с файлами
// Проверка количества аргументов
if (args.Length < 3) { onDone(2, args.Length); }
// Парсинг аргументов
switch (args[0])
{
case "p":
mode = Mode.Pack;
file = new FileInfo(args[args.Length - 1]);
dir = new DirectoryInfo(args[args.Length - 2]);
break;
case "u":
mode = Mode.Unpack;
file = new FileInfo(args[args.Length - 2]);
dir = new DirectoryInfo(args[args.Length - 1]);
break;
default: onDone(3, args[0]); break;
}
// Проверка существования входного объекта
if ((mode == Mode.Pack) && !dir.Exists)
onDone(4, dir.FullName);
if ((mode == Mode.Unpack) && !file.Exists)
onDone(4, file.FullName);
// Проверка существования выходного объекта
if (!(args.Contains<string>("/y") || args.Contains<string>("/Y")))
{
bool outputExist = false;
if ((mode == Mode.Pack) && file.Exists)
outputExist = true;
if ((mode == Mode.Unpack) && dir.Exists)
outputExist = true;
if (outputExist)
{
Console.WriteLine("Выходной объект уже существует! Вы уверены, что хотите перезаписать его?");
Console.WriteLine("Перезаписать {0}? [Y - да, N - нет]", (mode == Mode.Pack ? file.FullName : dir.FullName));
while (true)
{
int c = Console.Read();
if ((c == -1) || ((char)c == 'n') || ((char)c == 'N')) { onDone(1); }
else if (((char)c == 'y') || ((char)c == 'Y')) { break; }
}
}
}
// Создаем несуществующие папки
if (!file.Directory.Exists)
file.Directory.Create();
if (!dir.Exists)
dir.Create();
// Запуск операции
if (mode == Mode.Pack) { pack(file, dir); } else { unpack(file, dir); }
}
/// <summary>
/// Упаковка папки
/// </summary>
/// <param name="file">Выходной DAT-файл</param>
/// <param name="dir">Входная папка</param>
static void pack(FileInfo file, DirectoryInfo dir)
{
// Пишем-с
using (FileStream stream = new FileStream(file.FullName, FileMode.Create, FileAccess.Write))
{
FileInfo[] files = dir.GetFiles("*", SearchOption.TopDirectoryOnly);
byte[] header = new byte[4 + 17 * (files.Length + 1)];
Console.WriteLine("Упаковка файла: {0}", file.Name);
Console.WriteLine("Количество файлов: {0}", files.Length);
Console.WriteLine("-----------------------------------");
// Записываем количество файлов (+1 пустышка)
stream.Write(BitConverter.GetBytes(files.Length + 1), 0, 4);
// Пишем заголовок
UInt32 shift = (UInt32)(4 + 17 * (files.Length + 1));
for (int i = 0; i < files.Length; i++)
{
shift += (UInt32)(i > 0 ? files[i - 1].Length : 0);
byte[] name = new byte[13];
Encoding.GetEncoding(1251).GetBytes(files.Name).CopyTo(name, 0);
stream.Write(BitConverter.GetBytes(shift), 0, 4);
stream.Write(name, 0, 13);
Console.WriteLine("| Файл: {0} | Смещение: {1:x} |", files.Name, shift);
}
// Не забываем несуществующий объект, для вычисления длины последнего файла
shift += (UInt32)(files[files.Length - 1].Length);
stream.Write(BitConverter.GetBytes(shift), 0, 4);
stream.Write(new byte[13], 0, 13);
Console.WriteLine("-----------------------------------");
// Последовательно записываем файлы один за другим
for (int i = 0; i < files.Length; i++)
{
using (FileStream f = new FileStream(files.FullName, FileMode.Open, FileAccess.Read))
{
Console.WriteLine("| Файл: {0} | Смещение: {1:x} | Размер: {2} |", files.Name, stream.Position, (int)files.Length);
byte[] buff = new byte[files.Length];
f.Read(buff, 0, (int)files.Length);
stream.Write(buff, 0, (int)files.Length);
}
}
}
}
/// <summary>
/// Распаковка архива
/// </summary>
/// <param name="file">DAT-файл</param>
/// <param name="dir">Выходная папка</param>
static void unpack(FileInfo file, DirectoryInfo dir)
{
// Читаем-с
using (FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read))
{
// Считываем количество файлов
byte[] header = new byte[4];
stream.Read(header, 0, 4);
UInt32 count = BitConverter.ToUInt32(header, 0);
Console.WriteLine("Распаковка файла: {0}", file.Name);
Console.WriteLine("Количество файлов: {0}", count - 1);
Console.WriteLine("-----------------------------------");
// Считываем заголовок
header = new byte[count * 17];
stream.Read(header, 0, (int)(count * 17));
byte[] buff;
for (UInt32 index = 0; index < count - 1; index++)
{
UInt32 shift = BitConverter.ToUInt32(header, (int)(index * 17));
UInt32 length = BitConverter.ToUInt32(header, (int)((index + 1) * 17)) - shift;
string name = Encoding.GetEncoding(1251).GetString(header, (int)(4 + index * 17), 12);
Console.WriteLine("| Файл: {0} | Смещение: {1:x} | Длина: {2} |", name, shift, length);
buff = new byte[length];
stream.Position = shift;
stream.Read(buff, 0, (int)length);
using (FileStream output = new FileStream(String.Format(@"{0}\{1}", dir.FullName, name.TrimEnd(new char[] { '\0' })), FileMode.Create, FileAccess.Write))
{
output.Write(buff, 0, (int)length);
}
}
}
}
// Вывод сообщения о завершении работы и выход из программы
static void onDone(int exitCode, object param = null)
{
switch (exitCode)
{
case 0:
Console.WriteLine("Операция успешно завершена.");
break;
case 1:
Console.WriteLine("Операция прервана пользователем.");
break;
case 2:
Console.WriteLine("Неверное число параметров: {0}" + Environment.NewLine, param);
goto default;
case 3:
Console.WriteLine("Неизвестный ключ запуска: {0}" + Environment.NewLine, param);
goto default;
case 4:
Console.WriteLine("Входной объект не существует: {0}" + Environment.NewLine, param);
goto default;
default:
Console.WriteLine("Распаковка или упаковка игровых архивов Battle of the Youstrass.");
Console.WriteLine("2011 (c) Vitamant [vitamant@gmail.com]" + Environment.NewLine);
Console.WriteLine("{0} [p | u] [/Y] источник результат" + Environment.NewLine, Process.GetCurrentProcess().ProcessName);
Console.WriteLine(" p Упаковка входной папки в выходной DAT-архив.");
Console.WriteLine(" u Распаковка входного DAT-архива в выходную папку.");
Console.WriteLine(" /Y Подтверждение перезаписи выходного объекта.");
Console.WriteLine(" источник Распаковываемый DAT-архив или упаковываемая папка.");
Console.WriteLine(" результат Распакованная папка или упакованный DAT-архив." + Environment.NewLine);
break;
}
Environment.Exit(exitCode);
}
}
/// <summary>
/// Режим работы - распаковка или упаковка
/// </summary>
public enum Mode
{
Pack, Unpack
}
}
{
class Program
{
static void Main(string[] args)
{
Mode mode = Mode.Unpack; // режим работы - упаковка или распаковка
FileInfo file = new FileInfo("Z:\\z.tmp"); // DAT-архив
DirectoryInfo dir = new DirectoryInfo("Z:\\"); // папка с файлами
// Проверка количества аргументов
if (args.Length < 3) { onDone(2, args.Length); }
// Парсинг аргументов
switch (args[0])
{
case "p":
mode = Mode.Pack;
file = new FileInfo(args[args.Length - 1]);
dir = new DirectoryInfo(args[args.Length - 2]);
break;
case "u":
mode = Mode.Unpack;
file = new FileInfo(args[args.Length - 2]);
dir = new DirectoryInfo(args[args.Length - 1]);
break;
default: onDone(3, args[0]); break;
}
// Проверка существования входного объекта
if ((mode == Mode.Pack) && !dir.Exists)
onDone(4, dir.FullName);
if ((mode == Mode.Unpack) && !file.Exists)
onDone(4, file.FullName);
// Проверка существования выходного объекта
if (!(args.Contains<string>("/y") || args.Contains<string>("/Y")))
{
bool outputExist = false;
if ((mode == Mode.Pack) && file.Exists)
outputExist = true;
if ((mode == Mode.Unpack) && dir.Exists)
outputExist = true;
if (outputExist)
{
Console.WriteLine("Выходной объект уже существует! Вы уверены, что хотите перезаписать его?");
Console.WriteLine("Перезаписать {0}? [Y - да, N - нет]", (mode == Mode.Pack ? file.FullName : dir.FullName));
while (true)
{
int c = Console.Read();
if ((c == -1) || ((char)c == 'n') || ((char)c == 'N')) { onDone(1); }
else if (((char)c == 'y') || ((char)c == 'Y')) { break; }
}
}
}
// Создаем несуществующие папки
if (!file.Directory.Exists)
file.Directory.Create();
if (!dir.Exists)
dir.Create();
// Запуск операции
if (mode == Mode.Pack) { pack(file, dir); } else { unpack(file, dir); }
}
/// <summary>
/// Упаковка папки
/// </summary>
/// <param name="file">Выходной DAT-файл</param>
/// <param name="dir">Входная папка</param>
static void pack(FileInfo file, DirectoryInfo dir)
{
// Пишем-с
using (FileStream stream = new FileStream(file.FullName, FileMode.Create, FileAccess.Write))
{
FileInfo[] files = dir.GetFiles("*", SearchOption.TopDirectoryOnly);
byte[] header = new byte[4 + 17 * (files.Length + 1)];
Console.WriteLine("Упаковка файла: {0}", file.Name);
Console.WriteLine("Количество файлов: {0}", files.Length);
Console.WriteLine("-----------------------------------");
// Записываем количество файлов (+1 пустышка)
stream.Write(BitConverter.GetBytes(files.Length + 1), 0, 4);
// Пишем заголовок
UInt32 shift = (UInt32)(4 + 17 * (files.Length + 1));
for (int i = 0; i < files.Length; i++)
{
shift += (UInt32)(i > 0 ? files[i - 1].Length : 0);
byte[] name = new byte[13];
Encoding.GetEncoding(1251).GetBytes(files.Name).CopyTo(name, 0);
stream.Write(BitConverter.GetBytes(shift), 0, 4);
stream.Write(name, 0, 13);
Console.WriteLine("| Файл: {0} | Смещение: {1:x} |", files.Name, shift);
}
// Не забываем несуществующий объект, для вычисления длины последнего файла
shift += (UInt32)(files[files.Length - 1].Length);
stream.Write(BitConverter.GetBytes(shift), 0, 4);
stream.Write(new byte[13], 0, 13);
Console.WriteLine("-----------------------------------");
// Последовательно записываем файлы один за другим
for (int i = 0; i < files.Length; i++)
{
using (FileStream f = new FileStream(files.FullName, FileMode.Open, FileAccess.Read))
{
Console.WriteLine("| Файл: {0} | Смещение: {1:x} | Размер: {2} |", files.Name, stream.Position, (int)files.Length);
byte[] buff = new byte[files.Length];
f.Read(buff, 0, (int)files.Length);
stream.Write(buff, 0, (int)files.Length);
}
}
}
}
/// <summary>
/// Распаковка архива
/// </summary>
/// <param name="file">DAT-файл</param>
/// <param name="dir">Выходная папка</param>
static void unpack(FileInfo file, DirectoryInfo dir)
{
// Читаем-с
using (FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read))
{
// Считываем количество файлов
byte[] header = new byte[4];
stream.Read(header, 0, 4);
UInt32 count = BitConverter.ToUInt32(header, 0);
Console.WriteLine("Распаковка файла: {0}", file.Name);
Console.WriteLine("Количество файлов: {0}", count - 1);
Console.WriteLine("-----------------------------------");
// Считываем заголовок
header = new byte[count * 17];
stream.Read(header, 0, (int)(count * 17));
byte[] buff;
for (UInt32 index = 0; index < count - 1; index++)
{
UInt32 shift = BitConverter.ToUInt32(header, (int)(index * 17));
UInt32 length = BitConverter.ToUInt32(header, (int)((index + 1) * 17)) - shift;
string name = Encoding.GetEncoding(1251).GetString(header, (int)(4 + index * 17), 12);
Console.WriteLine("| Файл: {0} | Смещение: {1:x} | Длина: {2} |", name, shift, length);
buff = new byte[length];
stream.Position = shift;
stream.Read(buff, 0, (int)length);
using (FileStream output = new FileStream(String.Format(@"{0}\{1}", dir.FullName, name.TrimEnd(new char[] { '\0' })), FileMode.Create, FileAccess.Write))
{
output.Write(buff, 0, (int)length);
}
}
}
}
// Вывод сообщения о завершении работы и выход из программы
static void onDone(int exitCode, object param = null)
{
switch (exitCode)
{
case 0:
Console.WriteLine("Операция успешно завершена.");
break;
case 1:
Console.WriteLine("Операция прервана пользователем.");
break;
case 2:
Console.WriteLine("Неверное число параметров: {0}" + Environment.NewLine, param);
goto default;
case 3:
Console.WriteLine("Неизвестный ключ запуска: {0}" + Environment.NewLine, param);
goto default;
case 4:
Console.WriteLine("Входной объект не существует: {0}" + Environment.NewLine, param);
goto default;
default:
Console.WriteLine("Распаковка или упаковка игровых архивов Battle of the Youstrass.");
Console.WriteLine("2011 (c) Vitamant [vitamant@gmail.com]" + Environment.NewLine);
Console.WriteLine("{0} [p | u] [/Y] источник результат" + Environment.NewLine, Process.GetCurrentProcess().ProcessName);
Console.WriteLine(" p Упаковка входной папки в выходной DAT-архив.");
Console.WriteLine(" u Распаковка входного DAT-архива в выходную папку.");
Console.WriteLine(" /Y Подтверждение перезаписи выходного объекта.");
Console.WriteLine(" источник Распаковываемый DAT-архив или упаковываемая папка.");
Console.WriteLine(" результат Распакованная папка или упакованный DAT-архив." + Environment.NewLine);
break;
}
Environment.Exit(exitCode);
}
}
/// <summary>
/// Режим работы - распаковка или упаковка
/// </summary>
public enum Mode
{
Pack, Unpack
}
}
А я приступаю к разбору того, что находится внутри этого образа-архива. :)
Battle of the Youstrass :)
kooder
Спасибо большое! Наверняка поможет!
Alegun
Познание ради познания - мое любимое развлечение. :) А DU тоже кто-то писал. И DU позволяет распаковать архив (что я и сам уже сделал), но не редактировать внутренние ресурсы. Так что впереди меня ждет неравный бой: "чайник" против "dlv, dli, dlr, sn" :)
http://www.mediafire.com/?pdxbrl0y6mnj2m2
Файл с именем Stage1.fdb просто не мог не относиться к одной из игровых локаций. Первой, если точнее. Заглянув внутрь, обнаружил чудную картинку:
Ровные колонки "00" не давали и шанса взглянуть это, как на какую-нибудь разумную управляющую последовательность, равно, как и большое количество повторяющихся ненулевых байт. Не долго думая, забил все первым попавшимся на глаза (D1 00 D1 00 D1 00 D1 00...), упаковал, запустил игру. И довольно быстро заметил изменения. Ими оказались условия проходимости клеток игровой карты. Осталось составить список байт и сопоставить с ними те или иные условия проходимости. Все просто до отвращения. Правда, отдельно эту штуку использовать не получилось, так как размеры карт меняются, а внутри файла нет ну абсолютно никаких намеков на количество клеток. Двумерный массив представлен одномерной последовательностью и совершенно неюзабилен в отсутствии самих клеток.
К сожалению, здесь опять помог визуальный анализ, поэтому я прошу людей знающих иные методы анализа файлов помочь советом или умной книжкой, ибо об другой я зубы уже обломал. Им оказался сопутствующий файл *.sn:
http://www.mediafire.com/?l1l15176fxxkr2c
Никаких признаков константных структур навели на неприятную мысль - файл пожат. Также косвенным признаком оказалось то, что файл ужатый в zip стал весить всего на 18% меньше. Банальной подменой Stage1 на Stage51, выяснил, что это аж целая карта притом, по всей видимости, еще и с текстовыми диалогами, помимо самих ID тайлов. И вот тут я зашел в тупик. С чего начать? За что хватиться? Можно ли визуально определить алгоритм сжатия (если он там есть, хотя должен быть - ведь текстов невооруженным глазом не видно)? Или же поочереди перебирать наиболее популярные, особенно в 2000 (дата выхода игры) алгоритмы, ища нужный? LZ77/78, LZW, Deflate?.. Тогда существуют ли готовые решения для их распаковки? (чтобы в программе пришлось реализовывать только один, а не все, ибо нужный без сомнения окажется последним). Или сразу хватать в зубы ассемблер и смотреть - как там все устроено и как игра с этим взаимодействует? Тогда буду признателен за любую информацию о нем, ибо прошел он мимо меня и я лишь знаю, что это язык с очень простым синтаксисом, и очень сложный "для чайника" во всем остальном. :)
---
Следующим отонсительно адекватным форматом оказался dli.
http://www.mediafire.com/?jujeduucrwhi9cq
Еще не разобран, но не думаю, что окажется чем-то сверх сложным. Коротенький заголовок и большое количество повторяющихся байт, плавно перетекающих одних в другие, навели на мысли о графике. Путем подмены одного файлика другим, выяснил, что это действительно так. Просто картинки и есть мнение, что каждый пиксель кодируется отдельным байтом. Игрушка работает в режиме 256 цветов, так что едва ли здесь скрываются что-то более сложное. Попытаюсь нарисовать конвертет 2BMP. В заголовке, видимо, пишется палитра и\или размеры изображения. Еще есть небольшой хвост. Возможно палитра указана как раз в нем.
...sn по-прежнему держится крепким орешком. Нужна помощь...
Цитата: Vitamant
поочереди перебирать наиболее популярные, особенно в 2000 (дата выхода игры) алгоритмы, ища нужный? LZ77/78, LZW, Deflate?.. Тогда существуют ли готовые решения для их распаковки?
Большинство (если не все) форматы Z-семейства умеет распаковывать 7-Zip -- как из командной строки, так и на уровне библиотеки (предполагаю). Нужно только убедиться, что 7z.exe полный, не 7za.exe или 7zm.exe (забыл) -- суть урезанная версия. Полная версия требует 7z.dll для работы.
---------------------------
7-zip
---------------------------
Не удалось открыть файл 'Stage49.sn' как архив
---------------------------
ОК
---------------------------
Ну и из командной строки соответственно тоже самое.
pal, как не сложно догадаться, оказалась палитрой в 256 цветов. Достаточно было только взглянуть на ровные колонке цифр, разделенных 00, чтобы опознать записанные в RGB цвета + неиспользуемый альфа-канал. Размер файла 1056 байт. 1056 - 256*4 = 32 => длина заголовка. В начале стоит более чем понятное "DLPalette". За ним нули, а вот начиная с 16го байта - странное. Признаться, над двумя идущими подряд integer'ами я думал долго - просто не мог никуда приладить. А вот когда у моей картинки, всунутой в игру, поплыли цвета, пришло озарение. Посчитал количество идущих вначале "пустых цветов" (00 00 00 00) и вуаля - оно сошлось с первым int'ом. Вычил из 256 - получил второе. Почему-то все используемые цвета "прижаты" к концу файла, а неиспользуемые из отведенных 256 наоборот - лежат вначале. Возможно, это следствие работы оптимизиратора палитры (об этом ниже).
Собственно, с палитрой на этом и все. Ничего сложного. Едем дальше...
dli, наличие файла палитры рядом с говорящими названиями Title и WorldMap, являлось едва ли не 90% гарантией, что это изображения. Для теста поменял одно на другое, игру запустил - ага! Все верно.
Далее приступил к разбору. Помня опыт работы с DAT-файлом, первым делом обратил внимание на заголовок, в котором явственно читались два целых числа: 80 02 00 00 и E0 01 00 00. Перевел - все просто. 640х480. Размеры изображения.
Дальше был ступор: 00 00 00 00 01 01 A0 00
Непонятно что, непонятно зачем. Не имея альтернатив - плюнул и пока забыл.
Смотрю дальше.
Тут, признаться, на помощь пришли криворукие русские пираты, которые поленились сделать человеческую запаковку файлов и записали их всех без сжатия, так что в отличии от непонятной кучи байт в английских версиях файлов, в русских мне предстала чудная картинка:
Код:
C1A0C1A0C1A0C1A0C1A0C1A0C1A0C1A0
C1ACC1ACC1A5C1A0C1A0C1A0C1A0C1A5
C1A0C1A2C1A2C1B3C1BEC1ACC1A5C1A5
C1ACC1ACC1ACC1ACC1A0C1A2C1A2C1A2
C1ACC1ACC1A5C1A0C1A0C1A0C1A0C1A5
C1A0C1A2C1A2C1B3C1BEC1ACC1A5C1A5
C1ACC1ACC1ACC1ACC1A0C1A2C1A2C1A2
И так весь файл. C1 - неизменно. За ней - переменная. В конце файла одинокий 0C.
Поскольку для формирования изображения, как минимум, нужно знать цвет каждого его пикселя (или их группы), картинка стала предельно ясной. Что-то, за ним номер цвета из палитры. Что-то, еще один цвет. Никаких управляющих конструкций - полоска пикселей проходит до границы изображения - переходит на новый ряд. Все предельно просто. Забил всю "рабочую" область файла последователньостью C1A0 - в игре получил однородную заливку. Все просто.
Взглянул в английскую версию, а там все выглядело совсем по-другому:
Код:
FFA0FFA0C2A0C2ACA5C4A0A5A0C2A2B3
Сейчас это кажется очевидным, но тогда повергло в некоторый шок. Впрочем, уже через час экспериментов с изменением этих байт, и припоминанием вычитанного в этой замечательной статье:
http://shedevr.org.ru/forum/viewtopic.php?t=3590
Было вычислено, что C1 - это 1 пиксель, C2 - 2 пикселя... и т.д. до FF. А за ними идет цвет, которым указанное количество пикселей будет заполнено. Все предельно просто. Также эксперименты указали, что C0 заливает всю строку выбранным цветом до конца. А ниже нее - делает с картинкой странное.
Впрочем, главное я для себя определил. Впереди идущий байт определяет длину последователньости пикселей со сдвигом на 192 (С0). И может соответственно быть не больше FF (63 подряд идущих пикселя).
Написал конвертер и долго созерцал кашу непонятно чего.
Стал более подробно изучать имеющиеся данные. Обнаружил, что в английской версии почти не встерчается C1, в то время, как одиночные пиксели игра как-то закрашивает. К тому же, были найдены управляющие байты, значение которых меньше 192. Поэкспериментировав с ними, я обнаружил, что от этого меняется цвет! Притом всегда - одного пикселя. Эксперименты подтвердили смутные догадки - обрезав файл и забив его произвольной последовательностью байт, которые видел в файле, я получил строчку из разноцветных пикселей. Взглянул в палитру по соответствующим номерам, перевел цвета из HEX в десятичную RGB и вбил в Paint'е. Цвета сошлись. Из этого был сделан вывод - байты ниже 192 обозначают пиксели отдельного цвета.
Поправил конвертер - сдохло. Встретились пиксели, которых нет в палитре! И тут же взгляд уткнулся в заголовок, где маячил байт с тем же номером. Опять же эксперименты и вуаля - предпоследний байт заголовка определяет прозрачный цвет, который может даже не существовать в палитре - это пиксель не отрисовывается, а просто пропускается.
Наконец, все было готово. Две картинки с pal-файлами успешно переведены в gif'ы (хотя сейчас я знаю, что родным для разработчиков был формат pcx, но уже поздно). Но были еще сотни изображений, в которых никакими палитрами и не пахло!
Рыл я долго. Один добрый человек меткими пинками направлял меня в нужную сторону (хотя я старательно норовил свернуть и искал Единую Палитру для всех изображений, пробовал даже подцепить стандартную Win256; после ковырял exe-файлы игры...). Все без толку.
Уже отчаявшись, решился на страшное: взвел виртуальную машину с WinXP. Запустил на ней игру. Сделал скриншот экрана, на котором маячило интересующее изображение. В любимом Paint'е взял пипеткой образец цвета, перевел из десятичного rgb в HEX, добавил в конце 00 и... запустил поиск по игровому архиву... и к своему удивлению нашел, что искал!
Посмотрел по смещению и ощутил, как у меня вспотели зубы - судя по названию это был тот самый файл изображения.
Открыл, пролистал в конец... и обнаружил, что после 0C идет еще коротенький блок в 256 байт, содержащий 64 цвета. :)
Файлы не имеющие прикладной палитры, я даже не удосужился пролистать до конца.
Поправил конвертер, проверил - все сдохло. В файле цвета указаны с номерами аля 144, которых в принципе не может быть, при условии, что у меня их всего 64.
Было потрачено еще полдня на эксперименты. Палитра в конце файла забивалась красным, синим, зеленым и изменения проходили. Принялся изучать заголовок и вот тут страшное и раскрылось - последний байт заголовка отнимается от номера цвета!
Зачем? Вероятно, по той же причине, по которой палитра пишется от конца к началу, а не наоборот. Оптимизация для тех самых одиночных цветов ниже 192.
На этом разбор формата был закончен.
sn
Самое страшное, и одновременно самое скучное. Просто потому, что решение было найдено добрым человеком за меня. За что я ему, естетсвенно, благодарен, но никакого морального удовлетворения от написания софтинки к этому формату, испытать не удалось.
Это текстовые файлы, зашифрованные по алгоритму DEC с 8ми байтными независимыми блоками и 8ми байтным же единым ключом. Ключ он мне сказал, дальше оставалось лишь копнуть MSDN и еще немного постучаться головой об стену, в поисках свойства Padding. Файлы были успешно дешифрованы и зашифрованы обратно.
От игры остались dlv, dlr, dlx, dlb файлы.
dlr - анимация
dlx - эффекты
dlb - вероятно бинарные базы
dlv - пока неизвестно
За них тоже примусь но позже, сейчас приступлю к ковырянию шрифтов, используемых игрой и ее переводу на человеческий русский язык.
По сути, тема решена. Впрочем, она уже давно перестала быть вопросом и состоит, по большей части, из моих рассуждений. В будущем обязательно составлю подробный отчет о взломе игры для таких же новичков в этом деле. А пока будет черновичком. Впрочем, быть может, я еще вернусь за советом или просто похвастаться достижениями.
Ну и наконец, озвученный еще в первом посте вопрос насчет ассёмблера и изучения памяти процессов остается открытым. Кто что может подсказать из серии "для чайников на русском" по ним - буду премного благодарен.:)
Ширина расчитывается просто:
32 + (255 - value) * 4, где value - значение байта
А вот зависимость высоты изображения от значения второго байта никак вывести не удается. Составил таблицу соответствий:
Но пока никакой логики не нашел. Для первых столбцов, вроде бы, подходит ([value:5-8] - 0x0C) * 32 + k, где value:5-8 - байт, образованные 4мя старшими битами, k - неизвестное слагаемое.
Но для столбца 8 она уже неверна. Да и k найти не удалось. Помогите добрым советом!
---
Для FF:CA подходит формула ([BYTE:5-8] - 0x0C) * 32 + ([BYTE:1-4] - 0x0A) * 2, но в остальных случаях даёт сбой.
---
Учитывая то, что формула кажется очень зыбкой и вероятно является совпадением, а в вычислениях могут участвовать и соседние биты, креплю архив с собственно файлами. Сам же продолжу копать...
[ATTACH]5185[/ATTACH]
Очень полезно!!! Спасибо!!!
Высота определяется следующим образом:
Байт делится на две тетрады. Каждая из них представляется отдельным байтом и являет значение от 0 до 15. Этот коэффициент умножается на 32 в случае с первой тетрадой и на 2 - со второй. Получившиеся цифры складываются, в итоге получается высота.
Проблема в том, что данные уж больно хитро зашифрованы. И на данный момент легче нарисовать хэш-таблицу соответствий тетрады и коэффициента, однако это не очень красивое решение. так как данные в таблице все-таки имеют определенную структуру. В связи с этим вопрос: как инвертировать операцию, проведенную над байтами, так что бы значению 0x0 соответствовал коэффициент 0, а 0xF - 15? На словах все просто - поменять местами 1 и 2, 3 и 4 четверти, после чего также поменять местами 1 и 2 половины таблиц. Но как это реализовать на практике?
Код:
C 1100 - 0 A 1010 - 0
D 1101 - 1 B 1011 - 1
E 1110 - 2 8 1000 - 2
F 1111 - 3 9 1001 - 3
8 1000 - 4 E 1110 - 4
9 1001 - 5 F 1111 - 5
A 1010 - 6 C 1100 - 6
B 1011 - 7 D 1101 - 7
4 0100 - 8 2 0011 - 8
5 0101 - 9 3 0011 - 9
6 0110 - 10 0 0000 - 10
7 0111 - 11 1 0001 - 11
0 0000 - 12 6 0110 - 12
1 0001 - 13 7 0111 - 13
2 0010 - 14 4 0100 - 14
3 0011 - 15 5 0101 - 15
D 1101 - 1 B 1011 - 1
E 1110 - 2 8 1000 - 2
F 1111 - 3 9 1001 - 3
8 1000 - 4 E 1110 - 4
9 1001 - 5 F 1111 - 5
A 1010 - 6 C 1100 - 6
B 1011 - 7 D 1101 - 7
4 0100 - 8 2 0011 - 8
5 0101 - 9 3 0011 - 9
6 0110 - 10 0 0000 - 10
7 0111 - 11 1 0001 - 11
0 0000 - 12 6 0110 - 12
1 0001 - 13 7 0111 - 13
2 0010 - 14 4 0100 - 14
3 0011 - 15 5 0101 - 15
P.S. Как видно из таблице, представленные выше данные были ложными. В связи с этим, я сделал главный вывод и предостерегаю новичков: НИКОГДА не следует полностью верить данным, тем более графическим, которые выводит программа, над которой до этого бессовестно измывались! Она может отомстить (и обязательно это сделает)!
Valiant
Да, не за что!