เหตุใดไฟล์ PNG บางส่วนที่แยกจากเกมจึงแสดงไม่ถูกต้อง


14

ฉันสังเกตเห็นการแยก PNG จากไฟล์เกมบางไฟล์ที่ภาพผิดเพี้ยนไปมา ตัวอย่างเช่นต่อไปนี้เป็น PNG สองสามตัวที่แยกจากไฟล์พื้นผิวใน Skyrim:

Illuminated J PNG จาก Skyrim K PNG ส่องสว่างจาก Skyrim

นี่เป็นรูปแบบที่ผิดปกติบางอย่างในรูปแบบ PNG หรือไม่ ฉันต้องแก้ไขอะไรเพื่อดู PNG อย่างถูกต้อง


1
บางทีพวกเขาได้ใส่การเข้ารหัสพิเศษลงในไฟล์เพื่อป้องกันไม่ให้ผู้คนทำสิ่งนี้ หรือบางทีสิ่งที่คุณใช้ในการดึงข้อมูลนั้นทำงานไม่ถูกต้อง
Richard Marskell - Drackir

อาจเป็นการบีบอัดข้อมูลเพื่อทำให้รูปภาพมีขนาดเล็กลง สิ่งนี้สามารถทำได้ในแอพของ iPhone
rightfold

1
หัวข้อปิดเล็กน้อย แต่นั่นเป็นม้าหรือเปล่า
jcora

คำตอบ:


22

นี่คือภาพ "บูรณะ" ขอบคุณการวิจัยเพิ่มเติมของ tillberg:

final1 final2

อย่างที่คาดไว้มีตัวทำเครื่องหมายบล็อกขนาด 5 ไบต์ทุก ๆ ประมาณ 0x4020 ไบต์ รูปแบบที่ปรากฏมีดังต่อไปนี้:

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 == 0xffffmarker.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? */
};

ฉันไม่ได้คิดออกว่าอะไรจะเกิดขึ้นในตอนท้าย แต่เนื่องจาก PNGs ยอมรับการเติมเต็มมันจึงไม่น่าตื่นเต้นนัก อย่างไรก็ตามขนาดไฟล์ที่เข้ารหัสบ่งชี้อย่างชัดเจนว่าควรละเว้น 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 ไบต์

มันจะช่วยถ้าคุณสามารถให้การถ่ายโอนข้อมูลฐานสิบหก (ไม่กี่สิบไบต์) ของสิ่งที่ปรากฏอยู่ตรงหน้าภาพและหลังจากนั้น สิ่งนี้จะให้คำแนะนำเกี่ยวกับประเภทของข้อมูลที่เก็บไว้ที่จุดเริ่มต้นหรือจุดสิ้นสุดของบล็อก

แน่นอนว่ายังมีความเป็นไปได้ที่จะมีข้อผิดพลาดในโค้ดสกัดของคุณ หากคุณใช้บัฟเฟอร์ขนาด 16384 ไบต์สำหรับการดำเนินการกับไฟล์ของคุณฉันจะตรวจสอบที่นั่นก่อน


+1 มีประโยชน์มาก ฉันจะพูดถึงเรื่องนี้ต่อไปโดยมีคนนำทางให้คุณและโพสต์ข้อมูลเพิ่มเติม
James Tauber

"ไฟล์" ฝังตัวเริ่มต้นด้วยสตริงที่มีคำนำหน้ายาวซึ่งมีชื่อไฟล์ ตามด้วย 12 ไบต์ก่อนมายากล 89 50 4e 47 สำหรับไฟล์ PNG 12 ไบต์คือ: 40 25 01 00 78 9c 00 2a 40 d5 bf
James Tauber

การทำงานที่ดีแซม ฉันอัพเดทรหัสหลามที่อ่านไฟล์ BSA โดยตรงเพื่อทำสิ่งเดียวกัน ผลลัพธ์สามารถมองเห็นได้ที่orbza.s3.amazonaws.com/tillberg/pics.html (ฉันแสดงเพียง 1/3 ของภาพที่นั่นเพียงพอที่จะแสดงผลลัพธ์) สิ่งนี้ใช้ได้กับหลาย ๆ ภาพ มีบางสิ่งที่เกิดขึ้นกับภาพอื่น ๆ ฉันสงสัยว่าสิ่งนี้ได้รับการแก้ไขที่อื่นใน Fallout 3 หรือ Skyrim หรือไม่
tillberg

ทำงานได้ดีเยี่ยม ฉันจะอัปเดตโค้ดของฉันด้วย
James Tauber

18

ตามข้อเสนอแนะของแซมฉันได้แยกรหัสของเจมส์ที่https://github.com/tillberg/skyrimและสามารถแยก n_letter.png จากไฟล์ BSA ของ Skyrim Textures ได้สำเร็จ

ตัวอักษร N

"file_size" ที่กำหนดโดยส่วนหัว BSA ไม่ใช่ขนาดไฟล์สุดท้ายจริง มันมีข้อมูลส่วนหัวบางส่วนรวมทั้งข้อมูลสุ่มที่ดูเหมือนไร้ประโยชน์ที่กระจัดกระจายอยู่รอบ ๆ

ส่วนหัวมีลักษณะดังนี้:

  • 1 ไบต์ (ความยาวของเส้นทางไฟล์?)
  • เส้นทางแบบเต็มของไฟล์หนึ่งไบต์ต่ออักขระ
  • 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 (สำหรับรายละเอียดเกี่ยวกับสิ่งนี้ให้ตรวจสอบไฟล์ bsa.py ที่ github)

ขนาดที่กำหนดโดย chunks ใน n_letter.png คือ:

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

เมื่อฉันวัดระยะทางที่แท้จริงระหว่าง IDAT chunk และ IEND chunk หลังจากนั้น (โดยการนับไบต์โดยใช้ string.find () ใน Python) ฉันพบว่าความยาว IDAT จริงโดยนัยคือ 60640 ไบต์ - มี 15 ไบต์พิเศษอยู่ที่นั่น .

โดยทั่วไปแล้วไฟล์ "ตัวอักษร" ส่วนใหญ่จะมีขนาดเพิ่มขึ้น 5 ไบต์ต่อขนาดไฟล์ทั้งหมด 16KB ตัวอย่างเช่น o_letter.png ที่ประมาณ 73KB มีขนาดเกิน 20 ไบต์ ไฟล์ที่มีขนาดใหญ่กว่าเช่นอาร์เคน scribblings ส่วนใหญ่เป็นไปตามรูปแบบเดียวกันแม้ว่าบางไฟล์จะมีจำนวนแปลก ๆ เพิ่ม (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 ไบต์

เพื่อป้องกันการจับคู่แบบใกล้ชิด แต่ไม่สมบูรณ์แบบในดัชนีด้านบนฉันได้ทดสอบการคลายการบีบอัดของชิ้นส่วน IDAT ของ zlib จาก PNG ที่ได้และมันก็ผ่านไป


"1 byte สำหรับ random @ sign" คือความยาวของสตริงชื่อไฟล์ที่ฉันเชื่อ
James Tauber

มูลค่าของเซ็กเมนต์ 5 ไบต์ในแต่ละกรณีคืออะไร
James Tauber

ฉันอัปเดตคำตอบของฉันด้วยค่าฐานสิบหกของ 5 ไบต์ที่ลบออก นอกจากนี้ฉันผสมตัวเองเกินจำนวนเซ็กเมนต์ 5 ไบต์ (ก่อนหน้านี้ฉันนับส่วนหัวลึกลับขนาด 12 ไบต์เป็นส่วนหัว 7 ไบต์และตัวแบ่งซ้ำ 5 ไบต์) ฉันคงที่เช่นกัน
tillberg

โปรดทราบว่า (เล็ก ๆ น้อย ๆ endian) 0x402A, 0x4030, 0x402B ปรากฏในเซ็กเมนต์ขนาด 5 ไบต์ พวกเขาเป็นช่วงเวลาที่เกิดขึ้นจริง?
James Tauber

ฉันคิดว่าฉันพูดไปแล้วว่านี่เป็นงานที่ยอดเยี่ยม แต่ฉันไม่ได้ทำ เยี่ยมมาก! :-)
sam hocevar

3

ที่จริงแล้ว 5 ไบต์ต่อเนื่องเป็นส่วนหนึ่งของการบีบอัด zlib

ตามรายละเอียดในhttp://drj11.wordpress.com/2007/11/20/a-use-for-uncompressed-pngs/ ,

01 สตริงบิต endian เล็กน้อย 1 00 00000 1 ระบุบล็อกสุดท้าย 00 ระบุบล็อกที่ไม่บีบอัดและ 00000 เป็นช่องว่าง 5 บิตเพื่อจัดตำแหน่งจุดเริ่มต้นของบล็อกไปยังออคเต็ต (ซึ่งจำเป็นสำหรับบล็อกที่ไม่บีบอัด และสะดวกมากสำหรับฉัน) 05 00 fa ff จำนวนของ octet ของข้อมูลในบล็อกที่ไม่บีบอัด (5) จัดเก็บเป็นจำนวนเต็ม 16 บิตแบบ endian เล็ก ๆ น้อย ๆ ตามด้วยส่วนเติมเต็ม 1 (!)

.. ดังนั้น 00 หมายถึงบล็อก 'ถัดไป' (ไม่ใช่ตอนจบ) และ 4 ไบต์ถัดไปคือความยาวบล็อกและผกผัน

[แก้ไข] แหล่งที่เชื่อถือได้มากขึ้นแน่นอนว่า RFC 1951 (ข้อมูลจำเพาะรูปแบบการบีบอัดข้อมูลแบบย่อ) ส่วน 3.2.4


1

เป็นไปได้ไหมว่าคุณกำลังอ่านข้อมูลจากไฟล์ในโหมดข้อความ (ที่จุดสิ้นสุดของบรรทัดที่ปรากฏในข้อมูล PNG อาจมีการพันกัน) แทนที่จะเป็นในโหมดไบนารี


1
ใช่ ฟังดูเหมือนปัญหามาก พิจารณาว่านี่เป็นรหัสที่อ่านได้: github.com/jtauber/skyrim/blob/master/bsa.py --- ยืนยัน :
Armin Ronacher

ไม่ทำให้ไม่มีความแตกต่าง
James Tauber

@ JamesTauber หากคุณเข้ารหัสรหัสตัวโหลด PNG ของคุณเองเนื่องจากความคิดเห็นของ Armin ดูเหมือนจะบอกเป็นนัย ๆ แล้ว (ก) มันทำงานกับ PNG ตัวอื่นที่คุณได้ลองแล้วหรือ (b) ตัวโหลด PNG ที่พิสูจน์แล้วเช่นlibpngอ่าน Skyrim PNGs หรือไม่ กล่าวอีกนัยหนึ่งมันเป็นข้อผิดพลาดในตัวโหลด PNG ของคุณหรือไม่
นาธานรีด

@NathanReed ทั้งหมดที่ฉันทำคือการแยกสตรีมไบต์และอัปโหลดที่นี่ ไม่มีตัวโหลด "ที่เกี่ยวข้อง"
James Tauber

3
-1 นี่ไม่สามารถเป็นเหตุผลได้ หากไฟล์ PNG เสียหายด้วยวิธีนี้จะมีข้อผิดพลาด CRC ในระยะขยายเกินก่อนที่จะเกิดข้อผิดพลาดในขั้นตอนการถอดรหัสภาพ นอกจากนี้ยังไม่มีการเกิด CRLF ในไฟล์นอกเหนือจากที่คาดไว้ในส่วนหัว
sam hocevar
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.