为什么从游戏中提取的某些PNG文件显示不正确?


14

我注意到从某些游戏文件中提取PNG时,图像会在途中失真。例如,以下是从Skyrim中的Textures文件中提取的几个PNG:

Skyrim的照明J PNG Skyrim的照明K PNG

这是PNG格式的一些异常变化吗?我需要进行哪些修改才能正确查看此类PNG?


1
也许他们在文件中加入了一些特殊的编码,以防止人们做这样的事情。也许您正在提取的任何内容均无法正常工作。
理查德·马斯克

也许是一种压缩,以减小文件大小中的图像。这也可以在iPhone应用程序中完成。
2011年

1
一点点话题,但这是小马吗?
jcora 2011年

Answers:


22

这是经过得力伯格进一步研究后的“还原”图像:

决赛1 决赛2

如预期的那样,每大约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从第二个图像中删除字节,然后通过删除byte时得到的0x8092

原版的 第一步 第二步

它并没有真正“修复”图像;我通过反复试验做到了这一点。但是,它告诉我们每16384个字节有意外数据。我的猜测是,图像打包在某种文件系统结构中,意外数据只是块标记读取数据时应删除的。

我不知道块标记的确切位置和大小,但是块大小本身肯定是2 ^ 14字节。

如果您还可以提供十六进制转储(几十个字节),该十六进制转储显示在映像之前和之后的内容。这将提示有关在块的开头或结尾存储了哪种信息。

当然,提取代码中也有可能存在错误。如果您使用16384字节的缓冲区进行文件操作,那么我将首先在此处进行检查。


+1非常有帮助;我将继续深入探讨这个与你给我的铅和发布一些额外的信息
詹姆斯堡

嵌入的“文件”以包含文件名的带前缀的字符串开头;在PNG文件的89 50 4e 47魔术之前是12个字节。这12个字节是:40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber

干得好,山姆。我更新了实际上直接读取BSA文件的python代码来执行相同的操作。结果在orbza.s3.amazonaws.com/tillberg/pics.html上可见(我只在其中显示1/3的图像,足以证明结果了)。这适用于许多图像。其他一些图像还在处理其他事情。我想知道这是否在《辐射3》或《天际》中的其他地方得到了解决。
tillberg

各位,出色的工作!我会更新我的代码太
詹姆斯堡

18

根据Sam的建议,我在https://github.com/tillberg/skyrim上分叉了James的代码并能够从Skyrim Textures BSA文件中成功提取n_letter.png。

字母N

BSA标头提供的“ file_size”不是实际的最终文件大小。它包括一些标头信息以及一些散乱的无用的看起来数据的随机块。

标头看起来像这样:

  • 1个字节(文件路径的长度?)
  • 文件的完整路径,每个字符一个字节
  • 詹姆斯(James)发布了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文件开始。很容易从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

当我测量IDAT块和IEND块之间的实际距离时(通过在Python中使用string.find()计数字节),我发现暗示的实际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)

删除的五个字节段为:

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字节间隔。

为了防止在上面的索引中获得接近但不完美的匹配,我还测试了从生成的PNG中对IDAT块进行zlib解压缩,然后将其通过。


我相信“ 1个随机@符号的字节”是文件名字符串的长度
James Tauber

5字节段的值分别是多少?
詹姆斯·陶伯

我用删除的5字节段的十六进制值更新了答案。另外,我在5字节段的数量上混淆了自己(我之前将神秘的12字节标头算作7字节标头和5字节重复除法器)。我也解决了。
tillberg

请注意,(小尾数)0x402A,0x4030、0x402B出现在这5个字节的段中;它们是实际间隔吗?
詹姆斯·陶伯

我以为我已经说过这是出色的工作,但显然我没有。优秀作品!:-)
sam hocevar

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压缩数据格式规范)的3.2.4节。


1

您是否有可能以文本模式(其中碰巧出现在PNG数据中的行尾可能会被扭曲)而不是二进制模式从文件中读取数据?


1
嗯 听起来很像问题。考虑到这是读取它的代码:github.com/jtauber/skyrim/blob/master/bsa.py ---已确认:-)
Armin Ronacher,

不,没有区别。
詹姆斯·陶伯

@JamesTauber,如果您确实是在编码自己的PNG加载器,就像Armin的注释似乎暗示的那样,那么(a)它是否可以在您尝试过的其他PNG上运行,并且(b)是否有可靠的PNG加载器,例如libpng读取Skyrim PNG?换句话说,这只是您的PNG加载器中的错误吗?
内森·里德

@NathanReed我正在做的是提取字节流并将其上传到此处;没有涉及“装载者”
James Tauber

3
-1,这不是原因。如果以这种方式破坏了PNG文件,则在膨胀阶段将出现CRC错误,而在图像解码阶段将出现CRC错误。此外,除了标头中的预期值以外,文件中也没有出现CRLF。
sam hocevar,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.