Join the discord

"A New Beginning" - PNG fixing

16 Aug, 2011 16:10
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.
LengthChunk typeChunk dataCRC
4 bytes4 bytesLength bytes4 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!

Guest 24 Dec, 2018 10:01
@Guest
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
Guest 08 Jan, 2017 11:30
The link doesn't works. Please reup. Thaks.
Guest 21 May, 2016 06:54
Full source code for decrypt and encrypt any png, i tested in Linux and works at 100% (7kb's)

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?
XpoZed 21 Jan, 2014 00:58
It's just a source snippet written in C. If you want to build yourself a fully working solution, you will have to write it yourself. My snippet will just give you idea about the attack vector i used.
Guest 20 Jan, 2014 23:18
Noob question, but how do you run that on windows?
Guest 11 Nov, 2013 18:06
#include <windows.h>
#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;
}
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts