Dlaczego niektóre pliki PNG wyodrębnione z gier są wyświetlane niepoprawnie?

14

Zauważyłem, że wyodrębniając PNG z niektórych plików gry, obraz ulega częściowemu zniekształceniu. Na przykład oto kilka plików PNG wyodrębnionych z pliku tekstur w Skyrim:

Podświetlany J PNG od Skyrim Podświetlany K PNG od Skyrim

Czy to jakaś niezwykła odmiana formatu PNG? Jakie modyfikacje należy wprowadzić, aby prawidłowo wyświetlać takie pliki PNG?

James Tauber
źródło
1
Być może wprowadzili specjalne kodowanie w swoich plikach, aby uniemożliwić ludziom robienie takich rzeczy. A może to, czego używasz do wyodrębnienia, nie działa poprawnie.
Richard Marskell - Drackir
Może to rodzaj kompresji, aby zmniejszyć obrazy w rozmiarze pliku. Odbywa się to również w aplikacjach na iPhone'a.
prawej
1
Trochę poza tematem, ale czy to kucyk?
jcora

Odpowiedzi:

22

Oto „przywrócone” obrazy, dzięki dalszym badaniom tillberga:

końcowy 1 końcowy 2

Zgodnie z oczekiwaniami 5-bajtowy znacznik bloku co około 0x4020 bajtów. Format wygląda następująco:

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 */
};

Po odczytaniu znacznika następne marker.lenbajty tworzą blok, który jest częścią pliku. marker.notlenjest zmienną kontrolną taką, że marker.len + marker.notlen == 0xffff. Ostatni blok jest taki, że marker.tag == 1.

Struktura jest prawdopodobnie następująca. Nadal istnieją nieznane wartości.

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? */
};

Nie zorientowałem się, co jest na końcu, ale ponieważ PNG akceptują padding, nie jest to zbyt dramatyczne. Jednak zakodowany rozmiar pliku wyraźnie wskazuje, że ostatnie 4 bajty należy zignorować ...

Ponieważ nie miałem dostępu do wszystkich znaczników bloków tuż przed początkiem pliku, napisałem ten dekoder, który zaczyna się na końcu i próbuje znaleźć znaczniki bloków. To wcale nie jest solidne, ale dobrze, zadziałało dla twoich zdjęć testowych:

#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;
}

Starsze badania

Oto, co dostajesz, usuwając bajt 0x4022z drugiego obrazu, a następnie usuwając bajt 0x8092:

oryginalny pierwszy krok drugi krok

Tak naprawdę nie „naprawia” obrazów; Zrobiłem to metodą prób i błędów. Mówi jednak, że co 16384 bajtów występują nieoczekiwane dane. Domyślam się, że obrazy są spakowane w jakąś strukturę systemu plików, a nieoczekiwane dane to po prostu znaczniki bloków , które należy usunąć podczas odczytywania danych.

Nie wiem, gdzie dokładnie są znaczniki bloku i ich rozmiar, ale sam rozmiar bloku z pewnością wynosi 2 ^ 14 bajtów.

Byłoby pomocne, gdybyś mógł również dostarczyć zrzut heksadecymalny (kilkadziesiąt bajtów) tego, co pojawia się tuż przed obrazem i zaraz po nim. Dałoby to wskazówki na temat tego, jakie informacje są przechowywane na początku lub na końcu bloków.

Oczywiście istnieje również możliwość, że w kodzie ekstrakcyjnym występuje błąd. Jeśli używasz bufora 16384 bajtów do operacji na plikach, najpierw bym tam sprawdził.

sam hocevar
źródło
+1 bardzo pomocne; Będę dalej zagłębiał się w to, co mi dałeś, i opublikuję dodatkowe informacje
James Tauber,
Osadzony „plik” zaczyna się ciągiem z przedrostkiem długości zawierającym nazwę pliku; a następnie 12 bajtów przed magią 89 50 4e 47 dla plików PNG. 12 bajtów to: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber
Dobra robota, Sam. Zaktualizowałem kod python, który faktycznie odczytuje pliki BSA bezpośrednio, aby zrobić to samo. Wyniki są widoczne na stronie orbza.s3.amazonaws.com/tillberg/pics.html (pokazuję tam tylko 1/3 zdjęć, co wystarczy, aby pokazać wyniki). Działa to w przypadku wielu obrazów. Istnieją pewne inne rzeczy związane z niektórymi innymi obrazami. Zastanawiam się jednak, czy udało się to rozwiązać gdzie indziej w Fallout 3 lub Skyrim.
tillberg
Świetna robota, chłopaki! Zaktualizuję też mój kod
James Tauber,
18

W oparciu o sugestię Sama rozwidliłem kod Jamesa na https://github.com/tillberg/skyrim i udało mi się wyodrębnić n_letter.png z pliku BSA Skyrim Textures.

Litera n

„Rozmiar_pliku” podany przez nagłówki BSA nie jest rzeczywistym końcowym rozmiarem pliku. Zawiera pewne informacje nagłówka, a także losowe porcje niepotrzebnych danych, które są rozproszone.

Nagłówki wyglądają mniej więcej tak:

  • 1 bajt (długość ścieżki do pliku?)
  • pełna ścieżka do pliku, jeden bajt na znak
  • 12 bajtów niewiadomego pochodzenia, jak napisał James (40 25 01 00 78 9c 00 2a 40 d5 bf).

Aby usunąć bajty nagłówka, zrobiłem to:

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

Stamtąd zaczyna się rzeczywisty plik PNG. Łatwo jest sprawdzić, czy z 8-bajtowej sekwencji startowej PNG.

Próbowałem dowiedzieć się, gdzie znajdują się dodatkowe bajty, czytając nagłówki PNG i porównując długość przekazaną w porcji IDAT z implikowaną długością danych wyprowadzoną z pomiaru liczby bajtów do porcji IEND. (szczegółowe informacje na ten temat można znaleźć w pliku bsa.py na github)

Rozmiary podane przez porcje w n_letter.png to:

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

Kiedy zmierzyłem rzeczywistą odległość między porcją IDAT a porcją IEND po niej (zliczając bajty za pomocą string.find () w Pythonie), stwierdziłem, że rzeczywista sugerowana długość IDAT wynosiła 60640 bajtów - było tam dodatkowych 15 bajtów .

Ogólnie rzecz biorąc, większość plików „listowych” zawiera dodatkowe 5 bajtów na każde 16 KB całkowitego rozmiaru pliku. Na przykład o_letter.png, o wielkości około 73 KB, miał dodatkowe 20 bajtów. Większe pliki, takie jak tajemne bazgroły, przeważnie miały ten sam wzór, chociaż niektóre miały dodane nieparzyste ilości (52 bajty, 12 bajtów lub 32 bajty). Nie mam pojęcia, co się tam dzieje.

W przypadku pliku n_letter.png udało mi się znaleźć prawidłowe przesunięcia (głównie metodą prób i błędów), w których można usunąć segmenty 5-bajtowe.

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

Pięć usuniętych segmentów bajtowych to:

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

Dla tego, co jest warte, dołączyłem ostatnie pięć bajtów nieznanego 12-bajtowego segmentu ze względu na pewne podobieństwo z innymi sekwencjami.

Okazuje się, że nie są one co 16 KB, ale w odstępach ~ 0x4030 bajtów.

Aby uchronić się przed uzyskiwaniem bliskich, ale nie idealnych dopasowań w powyższych wskaźnikach, przetestowałem również dekompresję zlib fragmentu IDAT z wynikowego PNG i przechodzi.

tillberg
źródło
„1 bajt dla losowego znaku @” to długość ciągu nazwy pliku, jak sądzę
James Tauber,
jaka jest wartość segmentów 5-bajtowych w każdym przypadku?
James Tauber
Zaktualizowałem odpowiedź o wartości szesnastkowe usuniętych segmentów 5-bajtowych. Zmieszałem też liczbę 5-bajtowych segmentów (wcześniej liczyłem tajemniczy 12-bajtowy nagłówek jako 7-bajtowy nagłówek i 5-bajtowy powtarzający się dzielnik). Też to naprawiłem.
tillberg
zauważ, że (little-endian) 0x402A, 0x4030, 0x402B pojawiają się w tych 5-bajtowych segmentach; czy to są rzeczywiste interwały?
James Tauber
Myślałem, że już powiedziałem, że to doskonała robota, ale najwyraźniej nie. Wspaniała robota! :-)
sam hocevar
3

W rzeczywistości przerywane 5 bajtów jest częścią kompresji zlib.

Jak szczegółowo opisano na http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 mały łańcuch bitów endianowych 1 00 00000. 1 wskazujący ostatni blok, 00 wskazujący nieskompresowany blok, a 00000 to 5 bitów wypełnienia w celu wyrównania początku bloku do oktetu (który jest wymagany w przypadku nieskompresowanych bloków i dla mnie bardzo wygodne). 05 00 fa ff Liczba oktetów danych w nieskompresowanym bloku (5). Przechowywana jako 16-bitowa liczba całkowita typu endian, po której następuje uzupełnienie 1 (!).

.. więc 00 oznacza „następny” blok (nie końcowy), a 4 kolejne bajty to długość bloku i jego odwrotność.

[Edytuj] Bardziej niezawodnym źródłem jest oczywiście RFC 1951 (Deflate Compressed Data Format Specification), sekcja 3.2.4.

jongware
źródło
1

Czy to możliwe, że czytasz dane z pliku w trybie tekstowym (gdzie zakończenia linii, które pojawiają się w danych PNG, są prawdopodobnie zniekształcone) zamiast w trybie binarnym?

Greg Hewgill
źródło
1
Zawsze. To brzmi jak problem. Biorąc pod uwagę, że jest to kod, który go czyta: github.com/jtauber/skyrim/blob/master/bsa.py --- potwierdzony :-)
Armin Ronacher
Nie, nie ma znaczenia.
James Tauber
@JamesTauber, jeśli naprawdę kodujesz swój własny program ładujący PNG, jak wydaje się sugerować komentarz Armina, to (a) czy działa on na innych próbnych plikach PNG, które wypróbowałeś, i (b) czy sprawdzony program ładujący PNG, taki jak libpngczytanie plików PNG Skyrim? Innymi słowy, czy to tylko błąd w twoim programie ładującym PNG?
Nathan Reed
@NathanReed wszystko, co robię, to wyodrębnienie strumienia bajtów i przesłanie go tutaj; nie jest zaangażowany „moduł ładujący”
James Tauber
3
-1, to nie może być powód. Gdyby pliki PNG zostały w ten sposób uszkodzone, wystąpiłyby błędy CRC na etapie nadmuchiwania na długo przed błędami na etapie dekodowania obrazu. Ponadto w plikach nie ma żadnych wystąpień CRLF poza oczekiwanym w nagłówku.
sam hocevar