게임에서 추출한 일부 PNG 파일이 잘못 표시되는 이유는 무엇입니까?


14

일부 게임 파일에서 PNG가 추출되어 이미지가 일부 왜곡되어 나타납니다. 예를 들어 다음은 Skyrim의 Textures 파일에서 추출 된 두 개의 PNG입니다.

Skyrim에서 조명 된 J PNG Skyrim에서 조명 된 K PNG

이것이 PNG 형식에있어 특이한 변형입니까? 그러한 PNG를 제대로 보려면 어떤 수정이 필요합니까?


1
아마도 사람들이 이와 같은 일을하지 못하게하기 위해 파일에 특수 인코딩을 넣었을 것입니다. 또는 추출에 사용하는 것이 제대로 작동하지 않을 수 있습니다.
Richard Marskell-Drackir

파일 크기를 줄이면 이미지를 더 작게 압축 할 수 있습니다. 이것은 iPhone 앱에서도 수행됩니다.
rightfold

1
주제를 조금 벗어 났지만 조랑말입니까?
jcora

답변:


22

다음은 tillberg의 추가 연구 덕분에“복원 된”이미지입니다.

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두 번째 이미지에서 바이트 를 제거한 다음 바이트 를 제거하여 얻을 수있는 것입니다 0x8092.

기발한 첫 번째 단계 두번째 단계

이미지를 실제로 "수리"하는 것은 아닙니다. 나는 시행 착오로 이것을했다. 그러나 16384 바이트마다 예기치 않은 데이터가 있다는 것을 알려줍니다. 내 생각에 이미지는 일종의 파일 시스템 구조로 압축되어 있으며 예기치 않은 데이터는 단순히 데이터를 읽을 때 제거해야 할 블록 마커 입니다.

블록 마커의 위치와 크기를 정확히 모르지만 블록 크기 자체는 2 ^ 14 바이트입니다.

이미지 직전과 직후에 나타나는 16 진 덤프 (수십 바이트)를 제공 할 수 있다면 도움이 될 것입니다. 이것은 블록의 시작 또는 끝에 어떤 종류의 정보가 저장되는지에 대한 힌트를 제공합니다.

물론 추출 코드에 버그가있을 가능성도 있습니다. 파일 작업에 16384 바이트의 버퍼를 사용하는 경우 먼저 확인합니다.


+1 매우 도움이 됨; 나는 당신이 나에게 준 리드로 이것을 계속 파고 추가 정보를 게시 할 것입니다
James Tauber

임베드 된 "파일"은 파일 이름을 포함하는 길이가 앞에 붙는 문자열로 시작합니다. PNG 파일에 대한 89 50 4e 47 매직 앞에 12 바이트가옵니다. 12 바이트는 40 25 01 00 78 00 2A 9C 40 D5의 BF
제임스 우베

잘 했어, 샘 실제로 BSA 파일을 직접 읽는 파이썬 코드를 업데이트하여 동일한 작업을 수행했습니다. 결과는 orbza.s3.amazonaws.com/tillberg/pics.html 에서 볼 수 있습니다 (결과를 보여주기에 충분할 정도로 1/3의 이미지 만 표시합니다). 이것은 많은 이미지에서 작동합니다. 다른 이미지들과 함께 진행되는 다른 것들이 있습니다. 그래도 이것이 Fallout 3 또는 Skyrim의 다른 곳에서 해결되었는지 궁금합니다.
tillberg

훌륭합니다, 여러분! 코드도 업데이트하겠습니다
James Tauber

18

Sam의 제안에 따라 https://github.com/tillberg/skyrim 에서 James 코드를 포크 하고 Skyrim Textures BSA 파일에서 n_letter.png를 성공적으로 추출 할 수있었습니다.

편지 N

BSA 헤더가 제공하는 "file_size"는 실제 최종 파일 크기가 아닙니다. 여기에는 약간의 헤더 정보와 쓸데없는 데이터가 무작위로 흩어져 있습니다.

헤더는 다음과 같습니다.

  • 1 바이트 (파일 경로 길이)
  • 파일의 전체 경로 (문자 당 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

파이썬에서 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 바이트를 포함 시켰습니다.

16KB마다 아니지만 0x4030 바이트 간격으로 나타납니다.

위의 지수에서 완벽하게 일치하지는 않는 것을 막기 위해 결과 PNG에서 IDAT 청크의 zlib 압축 풀기를 테스트하여 통과했습니다.


은 "임의의 @ 기호 1 바이트가"파일 이름 문자열의 길이, 저는 믿습니다
제임스 타우 버

각 경우에 5 바이트 세그먼트의 값은 얼마입니까?
제임스 타우 버

제거 된 5 바이트 세그먼트의 16 진수 값으로 답변을 업데이트했습니다. 또한 5 바이트 세그먼트 수를 혼합했습니다 (이전에 신비한 12 바이트 헤더를 7 바이트 헤더 및 5 바이트 반복 분배기로 계산했습니다). 나는 그것을 고쳤다.
tillberg

(little-endian) 0x402A, 0x4030, 0x402B는 5 바이트 세그먼트에 나타납니다. 그들은 실제 간격입니까?
제임스 타우 버

나는 이미 이것이 훌륭한 작품이라고 말했을 것이라고 생각했지만 분명히 그렇지 않았습니다. 잘했습니다! :-)
sam hocevar

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 (압축 데이터 형식 사양 정의), 섹션 3.2.4입니다.


1

바이너리 모드 대신 텍스트 모드 (PNG 데이터에 나타나는 줄 끝이 엉망이 될 수 있음)에서 파일의 데이터를 읽을 수 있습니까?


1
찬성. 그것은 문제와 매우 흡사합니다. 이 그것을 읽는 코드입니다 고려 : github.com/jtauber/skyrim/blob/master/bsa.py --- :-) 확인
아르 민 Ronacher을

아뇨, 차이가 없습니다.
제임스 타우 버

@JamesTauber, Armin의 의견에서 암시하는 것처럼 자신의 PNG 로더를 실제로 코딩하는 경우 (a) 시도한 다른 PNG에서 작동 libpng합니까? (b) Skyrim PNG 를 읽는 등 입증 된 PNG 로더를 사용 합니까? 즉, PNG 로더의 버그입니까?
Nathan Reed

@NathanReed 내가하고있는 일은 바이트 스트림을 추출하고 여기에 업로드하는 것입니다. "로더"가 없습니다
James Tauber

3
-1, 이유가 될 수 없습니다. PNG 파일이 이러한 방식으로 손상된 경우 이미지 디코딩 단계에서 오류가 발생하기 훨씬 전에 팽창 단계에서 CRC 오류가 발생합니다. 또한 헤더에서 예상되는 파일을 제외하고 파일에서 CRLF가 발생하지 않습니다.
sam hocevar
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.