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. ;)
Comments
* You have an opinion? Let us all hear it!Here you have the source code:
https://mon-partage.fr/f/WkYqI6uE/
https://megaupload.nz/9232Abo5b0
https://megaup.net/nh1R/pngfixer_src.7z
https://www.mirrored.to/files/CYI9TWEE/pngfixer_src.7z_links
And the binary compiled for windows (used cgywin32 in windows 7)
https://mon-partage.fr/f/WkYqI6uE/
https://megaupload.nz/0f4fA6o3b1
https://megaup.net/nh1X/pngfixer.7z
https://www.mirrored.to/files/Q7XNWLDH/pngfixer.7z_links
http://www111.zippyshare.com/v/HwUsqHNI/file.html
tha bad news is..the engine checks if the image it's manipulated, i mean, i can modifiy a bit an image, then encrypt again, but is not working, looks we have some crc file check there?
#include <stdio.h>
#include <direct.h>
#include <vector>
unsigned int crc32(char *buf, size_t len);
int readint32(FILE *fp)
{
int ch, value = 0;
for(int i = 3; i >= 0; i--)
{
if( (ch = getc(fp)) == EOF )
return 0;
value |= (ch << (i<<3));
}
return value;
}
unsigned int swap_endian(unsigned int value)
{
unsigned char *bytes = (unsigned char*)&value;
std::swap(bytes[0], bytes[3]);
std::swap(bytes[1], bytes[2]);
return value;
}
int getfiles(const char *search, std::vector<std::string> &files)
{
WIN32_FIND_DATA ffd;
HANDLE hFind;
hFind = FindFirstFile(search, &ffd);
files.erase(files.begin(), files.end());
if (INVALID_HANDLE_VALUE == hFind)
{
return -1;
}
do
{
if (!(ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY))
{
files.push_back(ffd.cFileName);
}
}
while (FindNextFile(hFind, &ffd) != 0);
return files.size();
}
int main(int argc, char **argv)
{
std::vector<std::string> files;
unsigned int w, h, i;
char filename[256];
std::vector<char> data;
getfiles("*.png", files);
for(i = 0; i < files.size(); i++)
{
FILE *fp;
int crc, offset, length;
strcpy_s(filename, files[i].c_str());
if( fopen_s(&fp, filename, "rb+") )
break;
int header = readint32(fp);
// skip unused bytes
readint32(fp);
if( header != 0x89504e47 )
{
printf("%s is not a PNG...\n", filename);
fclose(fp);
continue;
}
offset = 0;
while((length = readint32(fp)) > 0)
{
data.resize(length + 4, 0);
offset = ftell(fp);
fread(&data[0], 1, data.size(), fp);
crc = readint32(fp);
if( memcmp(&data[0], "IHDR", 4) == 0 )
{
break;
}
}
if( length == 0 )
{
printf("failed to find IHDR in %s...\n", filename);
fclose(fp);
continue;
}
bool doneProcessing = false;
for(w = 1; !doneProcessing && w < 2048; w++)
{
for(h = 1; !doneProcessing && h < 2048; h++)
{
*(unsigned int*)&data[4] = swap_endian(w);
*(unsigned int*)&data[8] = swap_endian(h);
if (crc32(&data[0], data.size()) == crc)
{
printf("%s correct size: W:%d x H:%d\n", filename, w, h);
fseek(fp, offset, SEEK_SET);
fwrite(&data[0], 1, data.size(), fp);
doneProcessing = true;
}
}
}
fclose(fp);
if( doneProcessing == false )
{
printf("Failed to find a correct size for %s...\n", filename);
}
}
return 0;
}