ฉันสังเกตเห็นการแยก PNG จากไฟล์เกมบางไฟล์ที่ภาพผิดเพี้ยนไปมา ตัวอย่างเช่นต่อไปนี้เป็น PNG สองสามตัวที่แยกจากไฟล์พื้นผิวใน Skyrim:
นี่เป็นรูปแบบที่ผิดปกติบางอย่างในรูปแบบ PNG หรือไม่ ฉันต้องแก้ไขอะไรเพื่อดู PNG อย่างถูกต้อง
ฉันสังเกตเห็นการแยก PNG จากไฟล์เกมบางไฟล์ที่ภาพผิดเพี้ยนไปมา ตัวอย่างเช่นต่อไปนี้เป็น PNG สองสามตัวที่แยกจากไฟล์พื้นผิวใน Skyrim:
นี่เป็นรูปแบบที่ผิดปกติบางอย่างในรูปแบบ PNG หรือไม่ ฉันต้องแก้ไขอะไรเพื่อดู PNG อย่างถูกต้อง
คำตอบ:
นี่คือภาพ "บูรณะ" ขอบคุณการวิจัยเพิ่มเติมของ tillberg:
อย่างที่คาดไว้มีตัวทำเครื่องหมายบล็อกขนาด 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 == 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? */
};
ฉันไม่ได้คิดออกว่าอะไรจะเกิดขึ้นในตอนท้าย แต่เนื่องจาก 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 ไบต์สำหรับการดำเนินการกับไฟล์ของคุณฉันจะตรวจสอบที่นั่นก่อน
ตามข้อเสนอแนะของแซมฉันได้แยกรหัสของเจมส์ที่https://github.com/tillberg/skyrimและสามารถแยก n_letter.png จากไฟล์ BSA ของ Skyrim Textures ได้สำเร็จ
"file_size" ที่กำหนดโดยส่วนหัว BSA ไม่ใช่ขนาดไฟล์สุดท้ายจริง มันมีข้อมูลส่วนหัวบางส่วนรวมทั้งข้อมูลสุ่มที่ดูเหมือนไร้ประโยชน์ที่กระจัดกระจายอยู่รอบ ๆ
ส่วนหัวมีลักษณะดังนี้:
ในการตัดส่วนหัวไบต์ฉันทำสิ่งนี้:
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 ที่ได้และมันก็ผ่านไป
ที่จริงแล้ว 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
เป็นไปได้ไหมว่าคุณกำลังอ่านข้อมูลจากไฟล์ในโหมดข้อความ (ที่จุดสิ้นสุดของบรรทัดที่ปรากฏในข้อมูล PNG อาจมีการพันกัน) แทนที่จะเป็นในโหมดไบนารี
libpng
อ่าน Skyrim PNGs หรือไม่ กล่าวอีกนัยหนึ่งมันเป็นข้อผิดพลาดในตัวโหลด PNG ของคุณหรือไม่