Tại sao một số tệp PNG được trích xuất từ ​​các trò chơi sẽ hiển thị không chính xác?


14

Tôi đã nhận thấy trích xuất PNG từ một số tệp trò chơi mà hình ảnh bị biến dạng một phần xuyên suốt. Ví dụ: đây là một vài PNG được trích xuất từ ​​tệp Hoạ tiết trong Skyrim:

Chiếu sáng J PNG từ Skyrim Chiếu sáng K PNG từ Skyrim

Đây có phải là một số biến thể bất thường trên định dạng PNG? Tôi cần sửa đổi gì để xem các PNG như vậy đúng cách?


1
Có lẽ họ đã đưa một số mã hóa đặc biệt vào các tệp của mình để ngăn mọi người làm những việc như thế này. Hoặc có thể bất cứ điều gì bạn đang sử dụng để giải nén không hoạt động đúng.
Richard Marskell - Drackir

Có lẽ đó là một kiểu nén để làm cho hình ảnh nhỏ hơn trong kích thước tệp. Điều này cũng được thực hiện trong các ứng dụng iPhone.
đúng vào

1
Một chút lạc đề, nhưng đó có phải là một con ngựa?
jcora

Câu trả lời:


22

Dưới đây là những hình ảnh được phục hồi của người Viking, nhờ vào nghiên cứu sâu hơn của Untilberg:

chung kết1 chung kết2

Như mong đợi, có một điểm đánh dấu khối 5 byte mỗi khoảng 0x4020 byte. Các định dạng dường như là như sau:

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

Khi điểm đánh dấu đã được đọc, các marker.lenbyte tiếp theo tạo thành một khối là một phần của tệp. marker.notlenlà một biến điều khiển sao cho marker.len + marker.notlen == 0xffff. Khối cuối cùng là như vậy marker.tag == 1.

Cấu trúc có lẽ là như sau. Vẫn còn những giá trị chưa biết.

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

Tôi đã không tìm ra cuối cùng là gì, nhưng vì PNG chấp nhận phần đệm, nên nó không quá ấn tượng. Tuy nhiên, kích thước tệp được mã hóa chỉ rõ rằng 4 byte cuối cùng sẽ bị bỏ qua ...

Vì tôi không có quyền truy cập vào tất cả các điểm đánh dấu khối ngay trước khi bắt đầu tập tin, tôi đã viết bộ giải mã này bắt đầu ở cuối và cố gắng tìm các điểm đánh dấu khối. Nó không mạnh mẽ chút nào nhưng tốt, nó hoạt động cho hình ảnh thử nghiệm của bạn:

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

Nghiên cứu cũ

Đây là những gì bạn nhận được khi xóa byte 0x4022khỏi hình ảnh thứ hai, sau đó bằng cách xóa byte 0x8092:

nguyên bước đầu tiên bước thứ hai

Nó không thực sự sửa chữa các hình ảnh trên mạng; Tôi đã làm điều này bằng thử nghiệm và lỗi. Tuy nhiên, những gì nó nói là có dữ liệu bất ngờ cứ sau 16384 byte. Tôi đoán là các hình ảnh được đóng gói trong một số loại cấu trúc hệ thống tập tin và dữ liệu không mong muốn chỉ đơn giản là các điểm đánh dấu khối mà bạn nên loại bỏ khi đọc dữ liệu.

Tôi không biết chính xác các điểm đánh dấu khối ở đâu và kích thước của chúng, nhưng kích thước khối chắc chắn là 2 ^ 14 byte.

Sẽ rất hữu ích nếu bạn cũng có thể cung cấp kết xuất hex (vài chục byte) những gì xuất hiện ngay trước ảnh và ngay sau đó. Điều này sẽ đưa ra gợi ý về loại thông tin được lưu trữ ở đầu hoặc cuối khối.

Tất nhiên cũng có khả năng có lỗi trong mã trích xuất của bạn. Nếu bạn đang sử dụng bộ đệm 16384 byte cho các hoạt động tệp của mình, thì trước tiên tôi sẽ kiểm tra ở đó.


+1 rất hữu ích; Tôi sẽ tiếp tục nghiên cứu vấn đề này với sự dẫn dắt mà bạn đã đưa cho tôi và đăng một số thông tin bổ sung
James Tauber

"Tệp" được nhúng bắt đầu bằng một chuỗi có tiền tố dài chứa tên tệp; theo sau là 12 byte trước phép thuật 89 50 4e 47 cho các tệp PNG. 12 byte là: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber

Làm tốt lắm, Sam. Tôi đã cập nhật mã python thực sự đọc các tệp BSA trực tiếp để làm tương tự. Các kết quả có thể nhìn thấy tại orbza.s3.amazonaws.com/tillberg/pics.html (Tôi chỉ hiển thị 1/3 hình ảnh ở đó, chỉ đủ để chứng minh kết quả). Điều này làm việc cho nhiều hình ảnh. Có một số điều khác đang diễn ra với một số hình ảnh khác. Dù vậy, tôi tự hỏi liệu điều này đã được giải quyết ở đâu đó là Fallout 3 hay Skyrim.
Untilberg

Làm tốt lắm, các bạn! Tôi cũng sẽ cập nhật mã của mình
James Tauber

18

Dựa trên đề nghị của Sam, tôi đã rẽ nhánh mã của James tại https://github.com/tillberg/skyrim và có thể trích xuất thành công n_letter.png từ tệp BSA của Skyrim Textures.

Chữ N

"File_size" được cung cấp bởi các tiêu đề BSA không phải là kích thước tệp thực tế cuối cùng. Nó bao gồm một số thông tin tiêu đề cũng như một số khối dữ liệu dường như vô dụng nằm rải rác xung quanh.

Các tiêu đề trông giống như thế này:

  • 1 byte (độ dài của đường dẫn tệp?)
  • đường dẫn đầy đủ của tệp, một byte cho mỗi ký tự
  • 12 byte không rõ nguồn gốc, như James đã đăng (40 25 01 00 78 9c 00 2a 40 d5 bf).

Để loại bỏ các byte tiêu đề, tôi đã làm điều này:

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

Từ đó, tập tin PNG thực sự bắt đầu. Thật dễ dàng để xác minh rằng từ chuỗi bắt đầu 8 byte PNG.

Tôi đã tiến hành cố gắng tìm ra vị trí của các byte bổ sung bằng cách đọc các tiêu đề PNG và so sánh độ dài được truyền trong đoạn IDAT với độ dài dữ liệu ngụ ý được đo từ khi đo số byte cho đến đoạn IEND. (để biết chi tiết về điều đó, hãy xem tệp bsa.py tại github)

Các kích thước được cung cấp bởi các khối trong n_letter.png là:

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

Khi tôi đo khoảng cách thực tế giữa đoạn IDAT và đoạn IEND sau nó (bằng cách đếm byte bằng chuỗi.find () trong Python), tôi thấy rằng độ dài IDAT thực tế ngụ ý là 60640 byte - có thêm 15 byte trong đó .

Nói chung, hầu hết các tệp "thư" có thêm 5 byte cho mỗi 16KB tổng kích thước tệp. Ví dụ: o_letter.png, ở khoảng 73KB, có thêm 20 byte. Các tệp lớn hơn, như các nét vẽ nguệch ngoạc, chủ yếu theo cùng một mẫu, mặc dù một số có số lượng lẻ được thêm vào (52 byte, 12 byte hoặc 32 byte). Không biết chuyện gì đang xảy ra ở đó.

Đối với tệp n_letter.png, tôi có thể tìm thấy các độ lệch chính xác (chủ yếu bằng bản dùng thử và lỗi) để xóa các phân đoạn 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)

Các đoạn năm byte bị loại bỏ là:

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

Để biết giá trị của nó, tôi đã bao gồm năm byte cuối cùng của phân đoạn 12 byte không xác định do có một số điểm tương đồng với các chuỗi khác.

Hóa ra chúng không hoàn toàn sau mỗi 16KB, nhưng ở các khoảng ~ 0x4030 byte.

Để bảo vệ khỏi việc có được các trận đấu gần nhưng không hoàn hảo trong các chỉ số ở trên, tôi cũng đã thử nghiệm giải nén zlib của đoạn IDAT từ PNG kết quả và nó đã vượt qua.


"1 byte cho một dấu @ ngẫu nhiên" là độ dài của chuỗi tên tệp, tôi tin rằng
James Tauber

giá trị của các phân đoạn 5 byte trong mỗi trường hợp là gì?
James Tauber

Tôi đã cập nhật câu trả lời của mình với các giá trị hex của các đoạn 5 byte bị loại bỏ. Ngoài ra, tôi đã trộn lẫn với số lượng phân đoạn 5 byte (trước đây tôi đã đếm tiêu đề 12 byte bí ẩn là tiêu đề 7 byte và bộ chia lặp 5 byte). Tôi cũng sửa nó.
Untilberg

lưu ý rằng (little endian) 0x402A, 0x4030, 0x402B xuất hiện trong các phân đoạn 5 byte đó; chúng có phải là khoảng thời gian thực tế không?
James Tauber

Tôi nghĩ tôi đã nói rằng đây là một công việc tuyệt vời, nhưng rõ ràng là tôi đã không làm vậy. Làm tốt lắm :-)
sam hocevar

3

Trên thực tế, 5 byte không liên tục là một phần của nén zlib.

Như chi tiết trên http://drj11.wordpress.com/2007/11/20/a-use-for-uncompression-pngs/ ,

01 chuỗi bit endian nhỏ 1 00 00000. 1 chỉ ra khối cuối cùng, 00 chỉ ra khối không nén và 00000 là 5 bit đệm để căn chỉnh bắt đầu một khối với octet (được yêu cầu cho các khối không nén , và rất thuận tiện cho tôi). 05 00 fa ff Số octet dữ liệu trong khối không nén (5). Được lưu dưới dạng số nguyên 16 bit nhỏ, theo sau là phần bù 1′s (!) Của nó.

.. vì vậy, một 00 chỉ ra một khối 'tiếp theo' (không phải là một kết thúc) và 4 byte tiếp theo là chiều dài khối và nghịch đảo của nó.

[Chỉnh sửa] Tất nhiên, một nguồn đáng tin cậy hơn là RFC 1951 (Thông số kỹ thuật định dạng dữ liệu nén), phần 3.2.4.


1

Có thể bạn đang đọc dữ liệu từ tệp ở chế độ văn bản (trong đó các kết thúc dòng xuất hiện trong dữ liệu PNG có thể được đọc sai) thay vì ở chế độ nhị phân?


1
Đúng vậy Nghe có vẻ rất giống vấn đề. Xem xét đây là mã đọc nó: github.com/jtauber/skyrim/blob/master/bsa.py --- đã xác nhận :-)
Armin Ronacher

Không, làm cho không có sự khác biệt.
James Tauber

@JamesTauber, nếu bạn thực sự đang mã hóa trình tải PNG của riêng mình vì nhận xét của Armin dường như ngụ ý, thì (a) nó có hoạt động trên các PNG khác mà bạn đã thử không và (b) có trình tải PNG đã được chứng minh như libpngđọc Skyrim PNG không? Nói cách khác, nó chỉ là một lỗi trong trình tải PNG của bạn?
Nathan Reed

@NathanReed tất cả những gì tôi đang làm là trích xuất luồng byte và tải nó lên đây; không có "trình tải" nào liên quan
James Tauber

3
-1, đây không thể là lý do. Nếu các tệp PNG bị hỏng theo cách này, sẽ có lỗi CRC ở giai đoạn lạm phát trước các lỗi ở giai đoạn giải mã hình ảnh. Ngoài ra, không có sự xuất hiện của CRLF trong các tệp ngoài mục đích dự kiến ​​trong tiêu đề.
sam hocevar
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.