Почему некоторые файлы PNG, извлеченные из игр, отображаются неправильно?

14

Я заметил, что из некоторых игровых файлов извлекаются файлы PNG, из-за которых изображение становится частично искаженным. Например, вот несколько PNG, извлеченных из файла Textures в Skyrim:

J PNG с подсветкой от Skyrim Подсветка K PNG от Skyrim

Это какой-то необычный вариант в формате PNG? Какие изменения мне нужно будет сделать, чтобы правильно просматривать такие PNG?

Джеймс Таубер
источник
1
Возможно, они добавили специальную кодировку в свои файлы, чтобы люди не могли делать такие вещи. Или, может быть, то, что вы используете для извлечения, не работает должным образом.
Ричард Марскелл - Дракир
Может быть, это своего рода сжатие, чтобы уменьшить размер изображения. Это также делается в приложениях для iPhone.
вправо
1
Немного не по теме, но это пони?
jcora

Ответы:

22

Вот «восстановленные» изображения, благодаря дальнейшим исследованиям Тильберга:

final1 final2

Как и ожидалось, 5-байтовый маркер блока примерно каждые 0x4020 байт. Формат выглядит следующим образом:

struct marker {
    uint8_t tag;  /* 1 if this is the last marker in the file, 0 otherwise */
    uint16_t len; /* size of the following block (little-endian) */
    uint16_t notlen; /* 0xffff - len */
};

После считывания маркера следующие marker.lenбайты образуют блок, который является частью файла. marker.notlenконтрольная переменная такая, что marker.len + marker.notlen == 0xffff. Последний блок таков, что marker.tag == 1.

Структура, вероятно, следующая. Есть еще неизвестные значения.

struct file {
    uint8_t name_len;    /* number of bytes in the filename */
                         /* (not sure whether it's uint8_t or uint16_t) */
    char name[name_len]; /* filename */
    uint32_t file_len;   /* size of the file (little endian) */
                         /* eg. "40 25 01 00" is 0x12540 bytes */
    uint16_t unknown;    /* maybe a checksum? */

    marker marker1;             /* first block marker (tag == 0) */
    uint8_t data1[marker1.len]; /* data of the first block */
    marker marker2;             /* second block marker (tag == 0) */
    uint8_t data2[marker2.len]; /* data of the second block */
    /* ... */
    marker lastmarker;                /* last block marker (tag == 1) */
    uint8_t lastdata[lastmarker.len]; /* data of the last block */

    uint32_t unknown2; /* end data? another checksum? */
};

Я не понял, что в конце, но, так как PNG допускают заполнение, это не слишком драматично. Однако размер закодированного файла ясно указывает на то, что последние 4 байта следует игнорировать ...

Поскольку у меня не было доступа ко всем маркерам блоков непосредственно перед началом файла, я написал этот декодер, который запускается в конце и пытается найти маркеры блоков. Это совсем не надежно, но хорошо, это работает для ваших тестовых изображений:

#include <stdio.h>
#include <string.h>

#define MAX_SIZE (1024 * 1024)
unsigned char buf[MAX_SIZE];

/* Usage: program infile.png outfile.png */
int main(int argc, char *argv[])
{
    size_t i, len, lastcheck;
    FILE *f = fopen(argv[1], "rb");
    len = fread(buf, 1, MAX_SIZE, f);
    fclose(f);

    /* Start from the end and check validity */
    lastcheck = len;
    for (i = len - 5; i-- > 0; )
    {
        size_t off = buf[i + 2] * 256 + buf[i + 1];
        size_t notoff = buf[i + 4] * 256 + buf[i + 3];
        if (buf[i] >= 2 || off + notoff != 0xffff)
            continue;
        else if (buf[i] == 1 && lastcheck != len)
            continue;
        else if (buf[i] == 0 && i + off + 5 != lastcheck)
            continue;
        lastcheck = i;
        memmove(buf + i, buf + i + 5, len - i - 5);
        len -= 5;
        i -= 5;
    }

    f = fopen(argv[2], "wb+");
    fwrite(buf, 1, len, f);
    fclose(f);

    return 0;
}

Старые исследования

Вот что вы получаете, удаляя байт 0x4022из второго изображения, затем удаляя байт 0x8092:

оригинал первый шаг второй шаг

Это действительно не «восстанавливает» изображения; Я сделал это методом проб и ошибок. Однако, это говорит о том, что каждые 16384 байта появляются непредвиденные данные. Я предполагаю, что изображения упакованы в некую структуру файловой системы, а неожиданные данные - это просто маркеры блоков, которые вы должны удалить при чтении данных.

Я не знаю, где именно находятся маркеры блоков и их размер, но сам размер блока, безусловно, составляет 2 ^ 14 байт.

Было бы полезно, если бы вы также могли предоставить шестнадцатеричный дамп (несколько десятков байт) того, что появляется прямо перед изображением и сразу после него. Это даст подсказки о том, какая информация хранится в начале или конце блоков.

Конечно, есть вероятность того, что в вашем коде извлечения есть ошибка. Если вы используете буфер 16384 байта для файловых операций, я сначала проверил бы там.

Сэм Хоцевар
источник
+1 очень полезно; Я продолжу вникать в это с указанием, которое вы мне дали, и опубликую некоторую дополнительную информацию
Джеймс Таубер
Встроенный «файл» начинается с строки с префиксом длины, содержащей имя файла; затем 12 байтов до 89 50 4e 47 магии для файлов PNG. 12 байтов: 40 25 01 00 78 9c 00 2a 40 d5 bf
Джеймс Таубер
Хорошая работа, Сэм. Я обновил код Python, который фактически читает файлы BSA, чтобы сделать то же самое. Результаты видны на сайте orbza.s3.amazonaws.com/tillberg/pics.html (я показываю только 1/3 изображений, этого достаточно, чтобы продемонстрировать результаты). Это работает для многих изображений. Есть некоторые другие вещи, происходящие с некоторыми другими изображениями. Мне интересно, если это было решено в другом месте, хотя Fallout 3 или Skyrim.
Тильберг
Отличная работа, ребята! Я тоже обновлю свой код
Джеймс Таубер
18

По предложению Сэма я разбудил код Джеймса по адресу https://github.com/tillberg/skyrim и смог успешно извлечь n_letter.png из файла BSA Skyrim Textures.

Буква н

«File_size», заданный заголовками BSA, не является фактическим конечным размером файла. Он включает в себя некоторую информацию заголовка, а также некоторые случайные куски бесполезных данных, разбросанных по всему.

Заголовки выглядят примерно так:

  • 1 байт (длина пути к файлу?)
  • полный путь к файлу, один байт на символ
  • 12 байтов неизвестного происхождения, как писал Джеймс (40 25 01 00 78 9c 00 2a 40 d5 bf).

Чтобы удалить байты заголовка, я сделал это:

f.seek(file_offset)
data = f.read(file_size)
header_size = 1 + len(folder_path) + len(filename) + 12
d = data[header_size:]

Оттуда начинается фактический файл PNG. Это легко проверить по 8-байтовой последовательности запуска PNG.

Я попытался выяснить, где расположены дополнительные байты, прочитав заголовки PNG и сравнив длину, переданную в блоке IDAT, с подразумеваемой длиной данных, полученной в результате измерения количества байтов до блока IEND. (подробности смотрите в файле bsa.py на github)

Размеры, указанные кусками в n_letter.png:

IHDR: 13 bytes
pHYs: 9 bytes
iCCP: 2639 bytes
cHRM: 32 bytes
IDAT: 60625 bytes
IEND: 0 bytes

Когда я измерил фактическое расстояние между блоком IDAT и блоком IEND после него (подсчитав байты с помощью string.find () в Python), я обнаружил, что фактическая подразумеваемая длина IDAT составляла 60640 байтов - там было еще 15 байтов ,

Как правило, в большинстве «буквенных» файлов присутствуют дополнительные 5 байтов на каждые 16 КБ общего размера файла. Например, o_letter.png, размером около 73 КБ, имеет дополнительные 20 байтов. Большие файлы, такие как тайные каракули, в основном следовали той же схеме, хотя в некоторых добавлялись нечетные суммы (52 байта, 12 байтов или 32 байта). Понятия не имею, что там происходит.

Для файла n_letter.png мне удалось найти правильные смещения (в основном методом проб и ошибок), при которых удаляются 5-байтовые сегменты.

index = 0x403b
index2 = 0x8070
index3 = 0xc0a0
pngdata = (
  d[0      : (index - 5)] + 
  d[index  : (index2 - 5)] + 
  d[index2 : (index3 - 5)] + 
  d[index3 : ] )
pngfile.write(pngdata)

Удалены пять байтовых сегментов:

at 000000: 00 2A 40 D5 BF (<-- included at end of 12 bytes above)
at 00403B: 00 30 40 CF BF
at 008070: 00 2B 40 D4 BF
at 00C0A0: 01 15 37 EA C8

Что бы это ни стоило, я включил последние пять байтов неизвестного 12-байтового сегмента из-за некоторого сходства с другими последовательностями.

Оказывается, они не совсем каждые 16KB, но с интервалом ~ 0x4030 байтов.

Чтобы не допустить получения близких, но не идеальных совпадений в приведенных выше индексах, я также проверил декомпрессию zlib фрагмента IDAT из полученного PNG, и он прошел.

tillberg
источник
«1 байт для случайного знака @» - это длина строки имени файла, я полагаю
Джеймс Таубер
Каково значение 5-байтовых сегментов в каждом случае?
Джеймс Таубер
Я обновил свой ответ шестнадцатеричными значениями удаленных 5-байтовых сегментов. Кроме того, я перепутал количество 5-байтовых сегментов (ранее я считал загадочный 12-байтовый заголовок как 7-байтовый заголовок и 5-байтовый повторяющийся разделитель). Я тоже это исправил.
Тильберг
обратите внимание, что (little-endian) 0x402A, 0x4030, 0x402B появляются в этих 5-байтовых сегментах; они фактические интервалы?
Джеймс Таубер
Я думал, что уже сказал, что это отличная работа, но, видимо, я этого не сделал. Превосходная работа! :-)
Сам Хочевар
3

На самом деле прерывистые 5 байтов являются частью сжатия zlib.

Как подробно описано на http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 - битовая строка с прямым порядком байтов: 1 00 00000. 1 обозначает последний блок, 00 обозначает несжатый блок, а 00000 обозначает 5 битов заполнения, чтобы выровнять начало блока по октету (что требуется для несжатых блоков и очень удобно для меня). 05 00 fa ff Количество октетов данных в несжатом блоке (5). Хранится как 16-разрядное целое число с прямым порядком байтов, за которым следует его дополнение 1 (!).

.. таким образом, 00 указывает «следующий» блок (не конечный), а 4 следующих байта - это длина блока и его инверсия.

[Править] Более надежным источником является, конечно, RFC 1951 (Deflate Compressed Data Format Specification), раздел 3.2.4.

jongware
источник
1

Возможно ли, что вы читаете данные из файла в текстовом режиме (где окончания строк, которые появляются в данных PNG, возможно, искажены), а не в двоичном режиме?

Грег Хьюгилл
источник
1
Айя. Это очень похоже на проблему. Учитывая, что это код, который читает его: github.com/jtauber/skyrim/blob/master/bsa.py --- подтверждено :-)
Армин Ронахер
Нет, не имеет значения.
Джеймс Таубер
@JamesTauber, если вы действительно кодируете свой собственный загрузчик PNG, как, по-видимому, подразумевает комментарий Армина, то (а) он работает на других PNG, которые вы пробовали, и (б) проверенный загрузчик PNG, например, libpngчитает PNG Skyrim? Другими словами, это просто ошибка в вашем PNG-загрузчике?
Натан Рид
@NathanReed все, что я делаю - это извлекаю поток байтов и загружаю его сюда; «погрузчик» не задействован
Джеймс Таубер
3
-1, это не может быть причиной. Если файлы PNG были повреждены таким образом, на этапе надувания будут ошибки CRC задолго до ошибок на этапе декодирования изображения. Кроме того, CRLF не встречается в файлах, кроме ожидаемого в заголовке.
Сам Хочевар