A few days ago while unpacking some
VIS3 archives from the game
"A New Beginning" I noticed that all of its PNG files were broken. And by broken I mean you can't view them nor edit them.
Seems like the authors wanted a little more protection on their huge amount of artwork (I really mean it, they put a lot of effort to draw and paint that game!)
Of course if someone wishes to modify the game, for example - to translate it in some other than the original language, he will fail miserably, so I decided to write this... short article on how to fix these images.
First of all I will pick a target. My choice is the archive
character.vc074 and it's PNG
00000060.png so lets see it in the
HEX editor:
dump from: 00000060.png0000:0000 89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 . P N G . n . n . . . . I H D R
0000:0010 61 64 31 9D 66 61 33 8F 08 06 00 00 00 EB 35 35 a d 1 . f a 3 . . . . . . ë 5 5
0000:0020 0F 00 00 00 09 70 48 59 73 00 00 0B 13 00 00 0B . . . . . p H Y s . . . . . . .
0000:0030 13 01 00 9A 9C 18 00 00 0A 4F 69 43 43 50 50 68 . . . . . . . . n O i C C P P h
0000:0040 6F 74 6F 73 68 6F 70 20 49 43 43 20 70 72 6F 66 o t o s h o p I C C p r o f
You see the PNG header followed by
IHDR and
pHYs chunks.
Now, pHYs seems fine, but the IHDR chunk doesn't look correct.
Let's see the
IHDR specifications from libpng.
IHDR specificationsThe IHDR chunk must appear FIRST. It contains:
Width: 4 bytes
Height: 4 bytes
Bit depth: 1 byte
Color type: 1 byte
Compression method: 1 byte
Filter method: 1 byte
Interlace method: 1 byte
The first two DWORDS are the width and height of the image, and you may have already noticed that these in
00000060.png are way too big to be right - 0x6164319D for width and 0x6661338F for height.
It's obvious that these two were wrong so what can we do about it to fix them?
Well, we can always try to blindly guess its dimensions but that will take years for 1 image, and we have to fix probably more than 1000.
Then lets take a look at the
PNG chunk specifications.
Length | Chunk type | Chunk data | CRC |
---|
4 bytes | 4 bytes | Length bytes | 4 bytes |
See that last DWORD called CRC? That's the chunk's
CRC32 checksum that will help us restore the size.
It's calculated pretty simple by CRC-ing the whole chunk except the Length DWORD and of course the last CRC DWORD.
So, lets take everything between Length and CRC and CRC32.
The easiest way for me is to use
PHP and it's built in function
crc32() on the string
"\x49\x48\x44\x52\x61\x64\x31\x9D\x66\x61\x33\x8F\x08\x06\x00\x00\x00" and the result is 0x5B5EFEF2, quite different than the real which is 0xEB35350F.
Seems like the chunk checksum is actually the original one that is calculated before obfuscating the width and height DWORDs.
Knowing that, I can "exploit" it to get the real width and height and restore the image.
The exploit is actually a simple brute force that will try every possible dimension, CRC32 the result and compare it with the original CRC.
If it gets a match - then that's the right dimension.
Now lets write some code that will brute the checksum.
PNG IHDR brute forcer, written in Cint main() {
char IHDR[] = "\x49\x48\x44\x52\x61\x64\x31\x9D\x66\x61\x33\x8F\x08\x06\x00\x00\x00";
unsigned int w, h, i;
for(w = 0; w < 2000; w++) {
for(h = 0; h < 2000; h++) {
*(DWORD*)&IHDR[4] = w;
__asm__("bswap %0" : "+r" (*(DWORD*)&IHDR[4]));
*(DWORD*)&IHDR[8] = h;
__asm__("bswap %0" : "+r" (*(DWORD*)&IHDR[8]));
if (crc32(IHDR, 17) == 0xEB35350F) {
printf("Correct size: W:%d x H:%d\n", w, h);
}
}
}
return 0;
}
and the output is:
console outputCorrect size: W:164 x H:187
Now lets change the broken width and height with this one, and see if the image will work.
Yes it does. ;)