Mengapa beberapa file PNG yang diekstrak dari game ditampilkan secara tidak benar?

14

Saya perhatikan mengekstraksi PNG dari beberapa file game sehingga gambar menjadi terdistorsi sebagian. Misalnya, berikut adalah beberapa PNG yang diekstrak dari file Tekstur di Skyrim:

Diterangi J PNG dari Skyrim Diterangi K PNG dari Skyrim

Apakah ini beberapa variasi yang tidak biasa pada format PNG? Modifikasi apa yang perlu saya buat untuk melihat PNG dengan benar?

James Tauber
sumber
1
Mungkin mereka telah memasukkan beberapa penyandian khusus ke file mereka untuk mencegah orang melakukan hal-hal seperti ini. Atau mungkin apa pun yang Anda gunakan untuk mengekstrak tidak berfungsi dengan baik.
Richard Marskell - Drackir
Mungkin ini semacam kompresi untuk membuat gambar lebih kecil dalam ukuran file. Ini juga dilakukan di aplikasi iPhone.
rightfold
1
Sedikit keluar dari topik, tetapi apakah itu kuda poni?
jcora

Jawaban:

22

Berikut adalah gambar-gambar yang "dipulihkan", berkat penelitian lebih lanjut tillberg:

final1 final2

Seperti yang diharapkan, ada penanda blok 5 byte setiap sekitar 0x4020 byte. Formatnya tampaknya sebagai berikut:

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

Setelah marker dibaca, marker.lenbyte berikutnya membentuk blok yang merupakan bagian dari file.marker.notlenadalah variabel kontrol sedemikian rupa sehingga marker.len + marker.notlen == 0xffff. Blok terakhir adalah seperti itu marker.tag == 1.

Strukturnya mungkin sebagai berikut. Masih ada nilai yang tidak diketahui.

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

Saya belum tahu apa yang ada di akhir, tetapi karena PNG menerima bantalan, itu tidak terlalu dramatis. Namun, ukuran file yang disandikan dengan jelas menunjukkan bahwa 4 byte terakhir harus diabaikan ...

Karena saya tidak memiliki akses ke semua penanda blok tepat sebelum permulaan file, saya menulis dekoder ini yang dimulai pada bagian akhir dan mencoba untuk menemukan penanda blok. Sama sekali tidak kuat, tetapi berfungsi baik untuk gambar uji Anda:

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

Penelitian yang lebih tua

Inilah yang Anda dapatkan saat menghapus byte 0x4022dari gambar kedua, kemudian dengan menghapus byte 0x8092:

asli Langkah pertama tahap kedua

Itu tidak benar-benar "memperbaiki" gambar; Saya melakukan ini dengan coba-coba. Namun, yang diceritakan adalah bahwa ada data yang tidak terduga setiap 16384 byte. Dugaan saya adalah bahwa gambar-gambar tersebut dikemas dalam semacam struktur sistem file dan data yang tak terduga hanyalah blokir penanda yang harus Anda hapus ketika membaca data.

Saya tidak tahu di mana tepatnya marka blok dan ukurannya, tetapi ukuran blok itu sendiri sudah pasti 2 ^ 14 byte.

Ini akan membantu jika Anda juga bisa memberikan hex dump (beberapa lusin byte) dari apa yang muncul tepat sebelum gambar dan setelahnya. Ini akan memberi petunjuk tentang jenis informasi apa yang disimpan di awal atau di akhir blok.

Tentu saja ada juga kemungkinan ada bug dalam kode ekstraksi Anda. Jika Anda menggunakan buffer 16384 byte untuk operasi file Anda, maka saya akan memeriksa dulu di sana.

sam hocevar
sumber
+1 sangat membantu; Saya akan terus menggali ini dengan petunjuk yang telah Anda berikan kepada saya dan memposting beberapa info tambahan
James Tauber
"File" yang disematkan dimulai dengan string yang diawali dengan panjang yang berisi nama file; diikuti oleh 12 byte sebelum sihir 89 50 4e 47 untuk file PNG. 12 byte adalah: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber
Kerja bagus, Sam. Saya memperbarui kode python yang benar-benar membaca file BSA secara langsung untuk melakukan hal yang sama. Hasilnya terlihat di orbza.s3.amazonaws.com/tillberg/pics.html (saya hanya menampilkan 1/3 gambar di sana, cukup untuk menunjukkan hasilnya). Ini berfungsi untuk banyak gambar. Ada beberapa hal lain yang terjadi dengan beberapa gambar lainnya. Saya bertanya-tanya apakah ini telah diselesaikan di tempat lain Fallout 3 atau Skyrim.
tillberg
Kerja bagus, kawan! Saya juga akan memperbarui kode saya
James Tauber
18

Berdasarkan saran Sam, saya memalsukan kode James di https://github.com/tillberg/skyrim dan berhasil mengekstraksi n_letter.png dari file Skyrim Textures BSA.

Huruf N

"File_size" yang diberikan oleh header BSA bukan ukuran file final yang sebenarnya. Ini mencakup beberapa info tajuk serta beberapa potongan acak dari data yang tampaknya tidak berguna yang tersebar di sekitar.

Header terlihat seperti ini:

  • 1 byte (panjang jalur file?)
  • path lengkap file, satu byte per karakter
  • 12 byte yang tidak diketahui asalnya, seperti yang diposting James (40 25 01 00 78 9c 00 2a 40 d5 bf).

Untuk menghapus byte header, saya melakukan ini:

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

Dari sana, file PNG yang sebenarnya dimulai. Sangat mudah untuk memverifikasi itu dari urutan mulai PNG 8-byte.

Saya melanjutkan untuk mencoba mencari tahu di mana byte tambahan berada dengan membaca header PNG dan membandingkan panjang yang dilewatkan dalam potongan IDAT dengan panjang data tersirat yang disimpulkan dari mengukur jumlah byte sampai potongan IEND. (untuk detailnya, lihat file bsa.py di github)

Ukuran yang diberikan oleh chunks di n_letter.png adalah:

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

Ketika saya mengukur jarak aktual antara potongan IDAT dan potongan IEND setelahnya (dengan menghitung byte menggunakan string.find () dengan Python), saya menemukan bahwa panjang IDAT aktual yang tersirat adalah 60640 byte - ada tambahan 15 byte di sana .

Secara umum, sebagian besar file "letter" memiliki tambahan 5 byte untuk setiap 16KB dari total ukuran file. Misalnya, o_letter.png, sekitar 73KB, memiliki tambahan 20 byte. File yang lebih besar, seperti scribbling misterius, sebagian besar mengikuti pola yang sama, meskipun beberapa memiliki jumlah ganjil yang ditambahkan (52 byte, 12 byte, atau 32 byte). Tidak tahu apa yang terjadi di sana.

Untuk file n_letter.png, saya dapat menemukan offset yang benar (kebanyakan dengan coba-coba) untuk menghapus segmen 5-byte.

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

Segmen lima byte yang dihapus adalah:

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

Untuk apa nilainya, saya telah memasukkan lima byte terakhir dari segmen 12-byte yang tidak diketahui karena beberapa kesamaan dengan urutan lainnya.

Ternyata mereka tidak cukup setiap 16KB, tetapi pada interval ~ 0x4030 byte.

Untuk menjaga agar tidak mendapatkan kecocokan yang dekat tetapi tidak sempurna dalam indeks di atas, saya juga menguji dekompresi zlib dari potongan IDAT dari PNG yang dihasilkan, dan itu lolos.

tillberg
sumber
"1 byte untuk tanda @ acak" adalah panjang dari string nama file, saya percaya
James Tauber
berapakah nilai segmen 5-byte dalam setiap kasus?
James Tauber
Saya memperbarui jawaban saya dengan nilai hex segmen 5-byte yang dihapus. Juga, saya telah mencampuradukkan diri dengan jumlah segmen 5-byte (saya sebelumnya menghitung header 12-byte yang misterius sebagai header 7 byte dan pembagi berulang 5 byte). Saya memperbaikinya juga.
tillberg
perhatikan bahwa (little-endian) 0x402A, 0x4030, 0x402B muncul di segmen 5-byte tersebut; apakah itu interval sebenarnya?
James Tauber
Saya pikir saya sudah mengatakan ini adalah pekerjaan yang sangat baik, tetapi ternyata saya tidak melakukannya. Kerja bagus! :-)
sam hocevar
3

Sebenarnya, 5 byte intermiten adalah bagian dari kompresi zlib.

Seperti yang dijelaskan pada http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 string bit endian kecil 1 00 00000. 1 menunjukkan blok terakhir, 00 menunjukkan blok non-terkompresi, dan 00000 adalah 5 bit bantalan untuk menyelaraskan awal blok ke oktet (yang diperlukan untuk blok tanpa kompresi , dan sangat nyaman bagi saya). 05 00 fa ff Jumlah oktet data dalam blok terkompresi (5). Disimpan sebagai integer 16-bit little-endian diikuti oleh komplemen 1-nya (!).

.. jadi 00 menunjukkan blok 'berikutnya' (bukan yang berakhir), dan 4 byte berikutnya adalah panjang blok dan kebalikannya.

[Sunting] Sumber yang lebih andal tentu saja adalah RFC 1951 (Spesifikasi Format Data yang Dikosongkan), bagian 3.2.4.

jongware
sumber
1

Apakah mungkin Anda membaca data dari file dalam mode teks (di mana ujung baris yang muncul dalam data PNG mungkin hancur) daripada dalam mode biner?

Greg Hewgill
sumber
1
Iya. Kedengarannya sangat mirip dengan masalahnya. Mempertimbangkan ini adalah kode yang membacanya: github.com/jtauber/skyrim/blob/master/bsa.py --- dikonfirmasi :-)
Armin Ronacher
Tidak, tidak ada bedanya.
James Tauber
@JamesTauber, jika Anda benar-benar mengkodekan loader PNG Anda sendiri sebagai komentar Armin tampaknya menyiratkan, maka (a) apakah itu berfungsi pada PNG lain yang telah Anda coba, dan (b) apakah loader PNG terbukti seperti libpngmembaca PNG Skyrim? Dengan kata lain, apakah itu hanya bug di PNG loader Anda?
Nathan Reed
@NathanReed yang saya lakukan hanyalah mengekstrak aliran byte dan mengunggahnya di sini; tidak ada "loader" yang terlibat
James Tauber
3
-1, ini tidak bisa menjadi alasan. Jika file PNG rusak dengan cara ini, akan ada kesalahan CRC pada tahap mengembang jauh sebelum kesalahan pada tahap decoding gambar. Juga, tidak ada kemunculan CRLF dalam file selain yang diharapkan di header.
sam hocevar