ゲームから抽出された一部のPNGファイルが正しく表示されないのはなぜですか?


14

一部のゲームファイルからPNGを抽出すると、画像が途中で歪んでいることに気付きました。たとえば、SkyrimのTexturesファイルから抽出されたいくつかのPNGを次に示します。

SkyrimのイルミネーションJ PNG SkyrimのイルミネーションK PNG

これはPNG形式の異常なバリエーションですか?このようなPNGを適切に表示するには、どのような修正が必要ですか?


1
おそらく、彼らはファイルに特別なエンコードを入れて、人々がこのようなことをするのを防ぎます。または、抽出に使用しているものがすべて正しく動作しない可能性があります。
リチャードマースケル-ドラッカー

画像のファイルサイズを小さくするための一種の圧縮かもしれません。これはiPhoneアプリでも行われます。
右折

1
トピックから少し外れていますが、それはポニーですか?
jcora

回答:


22

ティルベルグのさらなる研究のおかげで、ここに「復元された」画像があります。

final1 final2

予想どおり、約0x4020バイトごとに5バイトのブロックマーカーがあります。形式は次のように表示されます。

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、2番目の画像からbyte を削除し、次にbyte を削除したときに得られるものです0x8092

元の 最初の一歩 第二段階

画像を実際に「修復」するわけではありません。これは試行錯誤で行いました。ただし、それは、16384バイトごとに予期しないデータがあることを示しています。私の推測では、画像はある種のファイルシステム構造にパックされており、予期しないデータは単にデータを読み取るときに削除する必要があるブロックマーカーです。

ブロックマーカーの正確な位置とサイズはわかりませんが、ブロックサイズ自体は2 ^ 14バイトです。

画像の直前と直後に表示されるものの16進ダンプ(数十バイト)も提供できれば助かります。これにより、ブロックの最初または最後に保存される情報の種類に関するヒントが得られます。

もちろん、抽出コードにバグがある可能性もあります。ファイル操作に16384バイトのバッファーを使用している場合は、最初にチェックします。


+1非常に役立つ。私はあなたが私に与えたリードでこれを掘り下げ、いくつかの追加情報を投稿します
ジェームズタウバー

埋め込まれた「ファイル」は、ファイル名を含む長さの接頭辞付き文字列で始まります。PNGファイルの89 50 4e 47マジックの前に12バイトが続きます。12バイトは次のとおりです。40 25 01 00 78 9c 00 2a 40 d5 bf
ジェームズタウバー

いいですね、サム。実際にBSAファイルを直接読み取って同じことを行うPythonコードを更新しました。結果はorbza.s3.amazonaws.com/tillberg/pics.htmlで見ることができます(結果を示すのに十分な画像の1/3しか表示していません)。これは多くの画像で機能します。他の画像のいくつかについては、他にもいくつかのことが行われています。ただし、Fallout 3またはSkyrimの別の場所でこれが解決されたかどうかは疑問です。
ティルバーグ

皆さん、すばらしい仕事です!コードも更新します
ジェームズタウバー

18

Samの提案に基づいて、私はJamesのコードをhttps://github.com/tillberg/skyrimで分岐し、Skyrim Textures BSAファイルからn_letter.pngを正常に抽出することができました。

手紙N

BSAヘッダーによって指定された「file_size」は、実際の最終ファイルサイズではありません。これには、いくつかのヘッダー情報だけでなく、無秩序に見えるデータのランダムなチャンクが散らばっています。

ヘッダーは次のようになります。

  • 1バイト(ファイルパスの長さ?)
  • ファイルのフルパス、1文字につき1バイト
  • Jamesが投稿した(40 25 01 00 78 9c 00 2a 40 d5 bf)12バイトの不明な発信​​元。

ヘッダーバイトを取り除くために、私はこれをしました:

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

そこから、実際のP​​NGファイルが始まります。PNGから8バイトの開始シーケンスを確認するのは簡単です。

PNGヘッダーを読み取り、IDATチャンクで渡された長さを、IENDチャンクまでのバイト数の測定から推測された暗黙のデータ長と比較することにより、余分なバイトがどこにあるかを把握しようとしました。(その詳細については、githubのbsa.pyファイルをご覧ください)

n_letter.pngのチャンクによって指定されるサイズは次のとおりです。

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

(Pythonでstring.find()を使用してバイトをカウントすることにより)IDATチャンクとIENDチャンク間の実際の距離を測定すると、暗黙の実際のIDAT長は60640バイトであることがわかりました-そこに余分な15バイトがありました。

一般に、ほとんどの「文字」ファイルには、合計16KBのファイルサイズごとに5バイトが追加されていました。たとえば、約73KBのo_letter.pngには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)

削除される5バイトセグメントは次のとおりです。

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バイトセグメントの最後の5バイトを含めました。

16 KBごとではなく、〜0x4030バイト間隔であることがわかります。

上記のインデックスで完全ではないが完全に一致する一致を防ぐために、結果のPNGからIDATチャンクのzlib圧縮解除もテストしました。


ファイル名の文字列の長さで、私は信じている「ランダム@記号のための1バイト」
ジェームズ・タウバー

それぞれの場合の5バイトセグメントの値は何ですか?
ジェームズタウバー

削除した5バイトセグメントの16進値で回答を更新しました。また、私は5バイトのセグメントの数をめぐって混乱していました(以前は、謎の12バイトのヘッダーを7バイトのヘッダーと5バイトの繰り返しディバイダーとしてカウントしていました)。私もそれを修正しました。
ティルバーグ

(リトルエンディアン)0x402A、0x4030、0x402Bはこれらの5バイトセグメントに表示されることに注意してください。実際の間隔ですか?
ジェームズタウバー

私はすでにこれが素晴らしい仕事だと言ったと思っていましたが、明らかにそうではなかったようです。すばらしい仕事です!:-)
サムホセバー

3

実際、断続的な5バイトはzlib圧縮の一部です。

http://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/で詳しく説明されているように、

01リトルエンディアンビット文字列1 0000000。1は最終ブロックを示し、00は非圧縮ブロックを示し、00000はブロックの開始をオクテットに合わせるための5ビットのパディング(非圧縮ブロックに必要) 、そして私にとって非常に便利です)。05 00 fa ff非圧縮ブロック内のデータのオクテット数(5)。リトルエンディアンの16ビット整数とそれに続く1の補数(!)として格納されます。

..したがって、00は「次の」ブロック(終了ブロックではない)を示し、次の4バイトはブロック長とその逆です。

[編集]より信頼できるソースは、もちろんRFC 1951(Deflate Compressed Data Format Specification)、セクション3.2.4です。


1

バイナリモードではなく、テキストモードでファイルからデータを読み込んでいる可能性はありますか?


1
そう。それは問題によく似ています。これを考慮するコードは、それを読み取るコードです:github.com/jtauber/skyrim/blob/master/bsa.py ---確認済み:
アーミン

いいえ、違いはありません。
ジェームズタウバー

@JamesTauber、アーミンのコメントが暗示しているように実際に独自のPNGローダーをコーディングしている場合、(a)あなたが試した他のPNGで動作し、(b)libpngSkyrim PNGを読むなどの実績のあるPNGローダーを実行しますか?つまり、PNGローダーのバグですか?
ネイサンリード

@NathanReedは、バイトストリームを抽出して、ここにアップロードするだけです。「ローダー」は関係ありません
ジェームズタウバー

3
-1、これは理由にはなりません。この方法でPNGファイルが破損した場合、イメージのデコード段階でエラーが発生する前に、インフレ段階でCRCエラーが発生します。また、ヘッダーに予期されているものを除き、ファイルにはCRLFの発生はありません。
サムホセバー
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.