Wii Savegame Parser

From WiiBrew
Jump to: navigation, search

The contents of this page are superceded by tachtig; see http://git.infradead.org/?p=users/segher/wii.git;a=blob;f=tachtig.c

Overview

The following Perl script can decompile a Wii savedata file which was copied from the Wii to an SD card. These are stored in files called data.bin. The script only decompiles the data.bin file. It does not decrypt the files contained therein to plaintext nor does it encrypt the plaintext again and reassemble the data.bin file. So it cannot use it to create data.bin's that can be copied over to the Wii again.

Structure of data.bin files

All multiple byte numbers (e.g. LONG = 4 bytes) are encoded in big endian format. While some information in the data.bin is plaintext and can therefore be used by the script below to parse the bin-file. The two encrypted blocks of data are the header and the actual savegame files stored in the data bin. LONGs or BYTEs that are fixed and therefore have the same value in each data.bin file are called magics.

Header

The header is one big block of encrypted data at the beginning of the data.bin. It consists of 64 byte blocks and ends in front of the first 64 byte block starting with 0x00000070 followed by 0x426B0001. This header is believed to be the image seen in the save menu, but there is no direct evidence for this. When I look at the save menu there are three distinct pieces of data:

  • The small square (possibly animated) icon (48x48 pixels),
  • The larger rectangular image when you click on the square icon (approx 70x180... it's hard to count pixels on my TV),
  • The name of the game (e.g. "The Legend of Zelda: Twilight Princess")

The header area is approx 60KB, which is large enough to contain these two images at 32bpp without compression. However, I see no redundant words, so the image must be encrypted or obfuscated in some way.

Basic File Information

The ID number of the Wii the data.bin was encoded on follows after that, then a LONG with the number of files contained in the data.bin. This concludes the first 16 bytes after the header. The Wii ID is duplicated later in the Certificate section below.

Data Size

The next 16 bytes contain one LONG with the size of the block of encrypted file data, two zero LONGs and one LONG describing the number of bytes from the end of the header (including 0x00000070) up to the end of file. The sum of this 'Post Header Length' and the Header Length is equal to the size of the file. A 64 byte of zeros follows the data size information.

Program ID

After another fixed magic LONG (0x00010000) the savegame's parent program's ID is stored in another LONG. Then follows the Wii's MAC address in the next two LONGs. The Program ID is a four character string describing the program, for example, Wii Sports (in English) is RSPE. See http://www.wiisave.com/gamecodes/index.php for more details.

First Hash

A 16 byte hash of unknown purpose. My guess is that this is a hash of the Header since it seems to change only when the header changes.

Files

The data.bin contains as many files as indicated by the number of files in the Basic File Information. Each file starts with a file header beginning with a magic LONG 0x03ADF17E and then a LONG describing the individual file size in bytes. Three BYTE magics 0x34, 0x00, 0x01 are followed by a zero-terminated string containing the file's name (e.g."zeldaTp.dat"). A block of of 117 minus the string's length artificially increases each single file header's size to 128 bytes. Then follows the actual encrypted file content followed by a block of random data to force the file data to fit into complete 64 byte blocks.

Note, the Magic byte 0x34 has been observed to be other values including (0x30, 0x34 and 0x3c). This looks like some type of bit mask to me.

Certificate Information

After the file data the certificates that were (probably) used to encrypted and should be used to decrypt the data are given. A 60 byte hash is followed by two LONG magics (0x00000000, 0x00010002) and another 60 byte hash. Then follows a 64 byte block of zeros and a 64 byte zero-terminated string ("Root-CA00000001-MS00000001") patched with zeros. Another magic (0x00000002) is followed by a string containing the Wii's ID in ASCII ("NG0???????"). This is followed by a 64 byte hash and a 60 byte block of zeros. Then follows a magic (0x00010002), a 60 byte hash, a 64 byte block of zeros and another 64 byte string ("Root-CA00000001-MS00000002-NG0???????") followed by a 64 byte block of zeros. A 0x00000002 magic is followed by another string ("AP0000000100000002") followed by 64 zeros. Then follows a 0x00000000 magic and a 60 byte hash which is finally concluded by 60 zeros bytes till the end of file.

Alternative View

Condensed File Format

  • 0x0000 - 0xF0C0: Header - Guessing it's an Image since it's the same for all saves from same game.
  • 0xF0C0 - 0xF0C4: Magic = 0x00000070 (112L)
  • 0xF0C4 - 0xF0C8: Magic = 0x426b0001 (1114308609L)
  • 0xF0C8 - 0xF0CC: Wii ID (same as certificate in later section)
  • 0xF0CC - 0xF0D0: Number of files in this save file.
  • 0xF0D0 - 0xF0D4: Size of all files in this save file.
  • 0xF0D4 - 0xF0D8: Magic = 0x00000000 (0L)
  • 0xF0D8 - 0xF0DC: Magic = 0x00000000 (0L)
  • 0xF0DC - 0xF0E0: Post Header Length (This plus the data at 0xF0D0 equals the file size)
  • 0xF0E0 - 0xF120: 64 (0x40) bytes of Zeros
  • 0xF120 - 0xF124: Magic = 0x00010000 (65536L)
  • 0xF124 - 0xF128: Four character program ID, e.g. "RSPE"
  • 0xf128 - 0xF130: MAC address of Wii. (plus two wasted bytes)
  • 0xF130 - 0xF140: HASH1: 16 byte hash. Probably hashes Header (0x0000-0xF0C0) since it changes as that data changes.


-- Per File Data --

  • 0xF140 - 0xF144: Magic = 0x03adf17e (61731198L)
  • 0xF144 - 0xF148: File Size of this file (For this example, assume size of 0x20000)
  • 0xF148 - 0xF149: Unknown Byte, values are 0x30, 0x34 and 0x3C
  • 0xF149 - 0xF14A: Unknown Byte, values are 0x00
  • 0xF14A - 0xF14B: Unknown Byte, values are 0x01 (TO BE CONFIRMED: 0x01 means file, 0x02 means directory (size is 0x00000000 in that case). I've found that in a Guitar Hero 3 save).
  • 0xF14b : Zero terminated file name, e.g. 'RPSports.dat'

Unclear how things are ordered:

  • Possible string file name filler
  • File Data, length from 0xF144
  • Possible Data Filler to round data out to 64 byte boundary


-- Done with Per File Data -- Addresses assume file size of 0x20000

  • 0x2F1C0 - 0x2F1FC: HASH2: Hash data, 60 (0x3C) bytes long. This is suspect as a hash since it always starts with a zero byte. It changes per save to SD, not as any real data changes.
  • Certificate 1 - Total 0x1FC bytes (508L)
    • 0x2F1FC - 0x2F200: Magic = 0x00000000 (0L) (Observed to also be 0x1b and 0x814823c8)
    • 0x2F200 - 0x2F204: Magic = 0x00010002 (65538L)
    • 0x2F204 - 0x2F240: HASH3: Hash data, 60 (0x3C) bytes long. This doesn't change when any 'real' data doesn't change.
    • 0x2F240 - 0x2F280: 64 (0x40) bytes of zeros
    • 0x2F280 - 0x2F2C0: String "Root-CA00000001-MS00000002" followed by 37 (0x25) zeros. (64 bytes total)
    • 0x2F2C0 - 0x2F2C4: Magic = 0x00000002 (2L)
    • 0x2F2C4 - 0x2F304: String "NG00000000" with 53 (0x35) zeros. This is the same Wii ID as above. (64 bytes total)
    • 0x2F304 - 0x2F344: HASH4: Hash data, 64 (0x40) bytes long. This doesn't change when any 'real' data doesn't change.
  • 0x2F344 - 0x2F37C: 56 (0x38) bytes of zeros
  • Certificate 2 - Total 0x1FC bytes (508L)
    • 0x2F37C - 0x2F380: Magic = 0x00000000 (0x0L)
    • 0x2F380 - 0x2F384: Magic = 0x00010002 (65538L)
    • 0x2F384 - 0x2F3C0: HASH5: Hash data, 60 (0x3C) bytes long. This is suspect as a hash since it always starts with a zero. It changes per save to SD, not as any real data changes.
    • 0x2F3C0 - 0x2F400: 64 (0x40) bytes of zeros
    • 0x2F400 - 0x2F440: String "Root-CA00000001-MS00000002-NG00000000" followed by 26 zeros (64 bytes total)
    • 0x2F440 - 0x2F444: Magic = 0x00000002 (2L)
    • 0x2F444 - 0x2F484: String "AP0000000100000002" followed by 45 zeros (64 bytes total)


  • 0x2F484 - 0x2F488: Magic = 0x00000000 (0L)
  • 0x2F488 - 0x2F48C: HASH6: Hash data, 60 (0x3C) bytes long. This is suspect as a hash since it always starts with a zero. It changes per save to SD, not as any real data changes.
  • Yeah the math is off by one for the ending address.

Script

This Perl script will decompile a data.bin file displaying the values of the LONGs and BYTEs and sizes of hashes, strings and data blocks. The hashes and data blocks will furthermore be saved in single binary files with according suffixes.

 #!/usr/bin/perl
 #-----------------------------------
 # Wii Savegame Parser
 #       written by Lockhool
 #             for #wiidev @ EFnet
 #-----------------------------------
 
 use strict;
 use Fcntl;
 
 sub readLong($;$);
 sub readByte($;$);
 sub readString($);
 sub readUpto($$);
 sub readBlock($$;$);
 sub readEof();
 
 my $file_in=shift;
 die("\n Usage: ./wiiparse.pl <datafile>\n\n")
     unless(sysopen(IN,$file_in,O_RDONLY));
 
 my $add=0;
 my $in;
 
 readUpto('Header',0x00000070);
 readLong('Magic',0x426B0001);
 readLong('WiiID');
 my $numfiles = readLong('NumFiles');
 readLong('FileDataLen');
 readLong('Magic',0x00000000);
 readLong('Magic',0x00000000);
 readLong('PostHeadLen');
 readBlock('Zeros',64);
 readLong('Magic',0x00010000);
 readLong('PrgID');
 readLong('MacAdd');
 readLong('Magic',0xF5550000);
 readBlock('Hash1',16,'hash1');
 for(1..$numfiles){   
    readLong('Magic'.$_,0x03ADF17E);
    my $filesize = readLong('Filesize'.$_);
    readByte('Magic'.$_,0x34);
    readByte('Magic'.$_,0x00);
    readByte('Magic'.$_,0x01);
    my $strlen = readString('Filename'.$_);
    readBlock('StrFiller'.$_,117-$strlen,'f'.$_.'sfill');
    readBlock('Filedata'.$_,$filesize,'f'.$_.'data');
    readBlock('DataFiller'.$_,$filesize % 64,'f'.$_.'dfill');
 }
 readBlock('Hash2',60,'hash2');
 readLong('Magic',0x00000000);
 readLong('Magic',0x00010002);
 readBlock('Hash3',60,'hash3');
 readBlock('Zeros',64);
 my $strlen = readString('RootCA');
 readBlock('Zeros',64-$strlen);
 readLong('Magic',0x00000002);
 my $strlen = readString('NG');
 readBlock('Zeros',64-$strlen);
 readBlock('Hash4',64,'hash4');
 readBlock('Zeros',60);
 readLong('Magic',0x00010002);
 readBlock('Hash5',60,'hash5');
 readBlock('Zeros',64);
 my $strlen = readString('RootCA-MS-NG');
 readBlock('Zeros',64-$strlen);
 readLong('Magic',0x00000002);
 my $strlen = readString('AP');
 readBlock('Zeros',64-$strlen);
 readLong('Magic',0x00000000);
 readBlock('Hash6',60,'hash6');
 readEof;
 
 close(IN);

 sub readLong($;$){
    printf("% 12u : ",$add);
    my $name=shift;
    my $val=shift;
    die("!! '$name' premature EOF !!\n")
    unless(4==sysread(IN,$in,4));
    $in=unpack("N",$in);
    $add+=4;
    printf("  LONG '$name' 0x%08X (%u)\n",$in,$in);
    if(defined $val){print(' 'x17);
        if($val==$in){print('==');}else{print('!=')}
 	printf(" 0x%08X (%u)\n",$val,$val);
    }
    return($in);
 }

 sub readByte($;$){
    printf("% 12u : ",$add);
    my $name=shift;
    my $val=shift;
    die("!! '$name' premature EOF !!\n")
    unless(1==sysread(IN,$in,1));
    $in=ord($in);
    $add+=1;
    printf("  BYTE '$name' 0x%02X (%u)\n",$in,$in);
    if(defined $val){print(' 'x17);
        if($val==$in){print('==');}else{print('!=')}
 	printf(" 0x%02X (%u)\n",$val,$val);
    }    
    return($in);
 }

 sub readBlock($$;$){
    printf("% 12u : ",$add);
    my $name=shift;
    my $cnt=shift;
    my $file=shift;
    if(defined $file){$file=$file_in.'.'.$file}
    my $size=$cnt;
    if(defined $file){
 	die("!! Can't open $file !!\n")
 	unless(sysopen(OUT,$file,O_WRONLY|O_CREAT));}
    while($cnt!=0){
 	die("!! '$name' premature EOF !!\n")
 	unless(1==sysread(IN,$in,1));
 	if(defined $file){syswrite(OUT,$in,1);}
 	$add++;
 	$cnt--;}
    if(defined $file){close(OUT);}
    printf(" BLOCK '$name' ends after %u bytes\n",$size);
    return($size);
 }

 sub readString($){
    printf("% 12u : ",$add);
    my $name=shift;   
    my $upto=shift;
    my $size=0;
    my $string='';
    my $in;
    while(1){
 	die("!! '$name' premature EOF !!\n")
 	unless(1==sysread(IN,$in,1));
 	$add+=1;
 	$size+=1;
 	$string=$string.unpack('a',$in);
 	if(ord($in)==0){last;}
    }
    printf("STRING '$name' ends after %u bytes\n",$size);
    print(' 'x15 ."\"$string\"\n");
    return($size);
 }

 sub readEof(){
    printf("% 12u",$add);
    my $name=shift;   
    my $upto=shift;
    my $size=0;
    my $in;
    while(1){
 	last unless(0!=sysread(IN,$in,1));
 	$add++;
 	$size++;}
    printf(" - %u : EOF reached after %u bytes\n",$add,$size);
    return($size);
 }

 sub readUpto($$){
    printf("% 12u : ",$add);
    my $name=shift;   
    my $upto=shift;
    my $size=0;
    my $in;
    while(1){
 	die("!! '$name' premature EOF !!\n")
 	unless(4==sysread(IN,$in,4));
 	$add+=4;
 	$size+=4;
 	if($upto==unpack("N",$in)){last;}}
    printf("  UPTO '$name' ends after %u bytes\n",$size);
    return($size);
 }

C Code

This is C code translation of the Perl script above. It may contain bugs, it was only tested on one game save file. If you find any errors, please edit the code with the fixes. Thanks, Advant.

Updated Source code with a few minor bug fixes and readability enhancements. -- PaceMaker

 /* 
 	WiiSaveGameParser.cpp : Defines the entry point for the console application.
 */
 
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 unsigned long address = 0;
 FILE * fp = NULL;
 char read_String[256] = {0};
 
 
 unsigned int readString(char *title)
 {
    char data[80] = {0};
    unsigned int bytes_read = 0;
    bytes_read = fread(&data,80,sizeof(unsigned char), fp);
    strcpy(read_String,data);
    printf("0x%.8x | String %s ends after %uL bytes \"%s\"\n", address,title, strlen(read_String),read_String);
    fseek(fp,address+strlen(read_String)+1,SEEK_SET);
    address += strlen(read_String)+1;
 
 	return (0);
 }
 
 /*
 	Assume section after header begins with a specific string.
 	Is there data to show this is a fixed length or terminated with a particular value?
 	All data I can find say this header is 0xF0C0 in length.
 	Could this be a 128x128 image?
 	128 (0x80) * 128 (0x80) = 16384 (0x4000)
 	16384 (0x4000) * 3 (bytes per pixel) = 49152 (0xC000)
 	 Which is not really that close to 0xF0C0. :(
 */
 unsigned long findEndOfHeader()
 {
 unsigned char buffer[64] = {0};
 unsigned long location = 0;
 int bytes_read = -1;
 unsigned char chk_buffer[16] = {0x00,0x00,0x00,0x70,0x42,0x6B,0x00,0x01};
 
 	do
 	{
 		if(bytes_read == 0)
 		{
 			printf("Error:  Unable to locate end of header!\n");
 			exit(0);
 		}
 
 	    bytes_read = fread(buffer,sizeof(unsigned char),64,fp);
 		location += 64;
 	} while(memcmp(buffer,chk_buffer,8) != 0);
 
   location -= 64;
   printf("Header offset: 0x%.8x\n", location);
 
   return (location);
 }
 
 void readByteChk(char * name, unsigned char chk)
 {
    unsigned char data[1] = {0};   
    fread(&data,1,sizeof(unsigned char), fp);
    if(data[0] != chk)
    { printf("0x%.8x |  %s 0x%.2x (%uL)...\t\t\tCheck FAILED!\n",address, name,data[0],data[0]);}
    else{printf("0x%.8x |  %s 0x%.2x (%uL)...\t\t\tCheck passed!\n",address, name,data[0],data[0]);}
    address += 1;
 
 }
 void readBlock(char * title, unsigned long len, char * filename)
 {
    FILE * fp2 = 0;
    unsigned char * data = (unsigned char *) calloc(len,sizeof(unsigned char));
    unsigned long bytes_read = 0;
    bytes_read = fread(data,len,sizeof(unsigned char), fp);
 	if (filename)
 		printf("0x%.8x | Block '%s' length %uL (0x%x) bytes.  File: %s\n", address,title,len, len, filename);
 	else
 		printf("0x%.8x | Block '%s' length %uL (0x%x) bytes.\n", address,title,len, len);
 
    address += len;
    if(filename > 0)
    {
        fp2 = fopen(filename,"w+b");
        fwrite(data,sizeof(unsigned char),len*bytes_read,fp2);
        fclose(fp2);
    }
 }
 unsigned long readLongChk(char * name, unsigned long chk)
 {
    unsigned long dataL = 0;
    unsigned char data[4] = {0};   
    fread(&data,4,sizeof(unsigned char), fp);
    dataL += data[0];
    dataL = dataL << 8;
    dataL += data[1];
    dataL = dataL << 8;
    dataL += data[2];
    dataL = dataL << 8;
    dataL += data[3];
    if(dataL != chk)
    	{printf("0x%.8x |  %s 0x%.8x != 0x%.8x...\t\t\tCheck FAILED!\n",address, name,dataL,chk); }
    else{printf("0x%.8x |  %s 0x%.8x (%uL)...\t\t\tCheck passed!\n",address, name,dataL,dataL);}
    address += 4;
 
    return dataL;
 }
 
 unsigned long readLong(char * name)
 {
    unsigned long dataL = 0;
    unsigned char data[4] = {0};
    fread(&data,4,sizeof(unsigned char), fp);
    dataL += data[0];
    dataL = dataL << 8;
    dataL += data[1];
    dataL = dataL << 8;
    dataL += data[2];
    dataL = dataL << 8;
    dataL += data[3];
    printf("0x%.8x |  %s 0x%.8x (%uL)\n", address,name,dataL,dataL);
    address += 4;
    
    return dataL;
 }
 
 
 int main(int argc, char * argv[])
 {
   char filename[256] = {0};  
   char title[256] = {0};
   unsigned long filesize = 0;
   unsigned long files = 0;
   unsigned long i = 0;
   unsigned long offset;
   unsigned long id;
 
   if(argc < 2)
   {
    printf("Usage: %s filename.bin\n", argv[0]);
    return 0;
   }
   fp = fopen(argv[1], "r+b");
   
 	if(fp == NULL)
 	{
 		printf("Error:  Unable to open %s\n", argv[1]); 
 		return -1;
 	}
 
 
 	/* Find the header. */
 	offset = findEndOfHeader();
 	/* Go back to the beginning of the file */
 	address = 0;
 	rewind (fp);
 	/* And save it to an output file. */
 	sprintf(filename,"out\\%s.header",argv[1]);
 	readBlock("Header",offset,filename);  
 
 	/* Marks end of header or beginning of next stuff. */
 	readLongChk("Magic:", 0x00000070);
 	readLongChk("Magic:", 0x426b0001);
 
 	/* Wii ID.  Not sure if this matches up to any other serial numbers. 
 	   This is duplicated in the Certificate section below. */
 	readLong("WiiID:");
   
 	files = readLong("Number of files");
 	/* Makes for easier debugging of this program when I mess up. */
 	if (files > 1000)
 	{
 		printf ("Probably bad parsing...files = %d\n", files);
 		exit (0);
 	}
 
 	/* Get Length of all file data. */
 	readLong("FileDataLen");
 
 	readLongChk("Magic",0x00000000);
 	readLongChk("Magic",0x00000000);
 
 	/* This size plus the size of the header is the total file size. */
 	readLong("PostHeadLen");
 
 	readBlock("Zeros",64,0);
 	readLongChk("Magic",0x00010000);
 
 	/* This is the four character ID of the program (e.g. RSPE) */
 	id = readLong("PrgID");
 	printf ("               Program ID: \"%c%c%c%c\"\n", ((id & 0xFF000000) >> 24), ((id & 0xFF0000) >> 16), ((id & 0xFF00) >> 8), id & 0xFF);
 
 	/* Read the Mac Address */
 	readLong("MacAdd");
 	readLong("MacAdd2");
 
 	/*
 		First hash, unknown.
 		This is a 128 bit (16 byte) (4 longs) hash.
 		MD5 fits this length, but it seems like a strange choice.
 	*/
 	sprintf(filename,"out\\%s.hash1\0",argv[1],i+1);
 	readBlock("Hash1",16,filename);  
 
 	/* Loop for each file. */
 	for(i = 0; i < files; i++)
 	{
 		printf ("\nStarting file %d\n", i);
 		sprintf(title,"Magic%d",i);
 		readLongChk(title,0x03ADF17E);
 		sprintf(title,"Filesize%d",i);
 		filesize = readLong(title);
 		sprintf(title,"Magic%d",i);
 		readByteChk(title,0x34);
 		readByteChk(title,0x00);
 		readByteChk(title,0x01); 
 		sprintf(title,"Filename%d",i);
 		readString(title);
 
 		/*
 			I see no basis for this title filler.  It's just a zero terminated string.
 		    There appears to be filler somewhere, but why here?
 		*/
 		sprintf(filename,"out\\%s.f%dsfiller\0",argv[1],i+1);
 		readBlock("StrFiller",117-strlen(read_String)-1,filename);
 
 		sprintf(filename,"out\\%s.f%ddata\0",argv[1],i+1);
 		readBlock("Filedata",filesize,filename);
 		sprintf(filename,"out\\%s.f%ddfill\0",argv[1],i+1);
 		readBlock("DataFiller",filesize % 64,filename);
 	}
 
 	printf ("\nDone with all File data\n");
   sprintf(filename,"out\\%s.hash2\0",argv[1]);
   readBlock("Hash2",60,filename);  
   
   readLongChk("Magic",0x00000000);
   readLongChk("Magic",0x00010002);
   
   sprintf(filename,"out\\%s.hash3\0",argv[1]);
   readBlock("Hash3",60,filename);
 
   readBlock("Zeros",64,0);
   readString("RootCA");
   readBlock("Zeros",64-strlen(read_String)-1,0);
   readLongChk("Magic",0x00000002);
   readString("NG");
   readBlock("Zeros",64-strlen(read_String)-1,0);
 
   sprintf(filename,"out\\%s.hash4\0",argv[1]);
   readBlock("Hash4",64,filename);
   
   readBlock("Zeros",60,0);
   readLongChk("Magic",0x00010002);
   
   sprintf(filename,"out\\%s.hash5\0",argv[1]);
   readBlock("Hash5",60,filename);
 
   readBlock("Zeros",64,0);  
   readString("RootCA-MS-NG");
   readBlock("Zeros",64-strlen(read_String) - 1,0);
   readLongChk("Magic",0x00000002);
   readString("AP");
   readBlock("Zeros",64-strlen(read_String) - 1,0);
   readLongChk("Magic",0x00000000);
 
   sprintf(filename,"out\\%s.hash6\0",argv[1]);
   readBlock("Hash6",60,filename);
 
 }