In memory of Ben “bushing” Byer, who passed away on Monday, February 8th, 2016.

Difference between revisions of "Wii Savegame Parser"

From WiiBrew
Jump to navigation Jump to search
Line 14: Line 14:
 
==Basic File Information==
 
==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 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==
 
==Data Size==

Revision as of 21:07, 11 April 2007

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 he 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. Some wild speculation is that this is a 128x128(x3) image. Does anyone know of a link to image formats used on the NDS?

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. 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 another LONG. Another fixed LONG (0xF5550000) completes another 16 byte block.

First Hash

A 16 byte hash of unknown purpose.

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 begining 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 increses each single file header's size to 128 bytes. Then follows the actual encrypted file content followed by a block of randon data to force the file data to fit into complete 64 byte blocks.

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.

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.

#ifdef WIN32
	#define _CRT_SECURE_NO_DEPRECATE
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

unsigned long readLongChk(char * name, unsigned long chk);
void readBlock(char * title, unsigned long len, char * filename);
void readByteChk(char * name, unsigned char chk);
unsigned long findEndOfHeader();

unsigned long readLong(char * name);
unsigned long address = 0;
FILE * fp = NULL;
char read_String[256] = {0};

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;
  if(argc < 2)
  {
   printf("Usage: wii-parse filename.bin\n");
   return 0;
  }
  fp = fopen(argv[1], "r+b");
  
  if(fp == NULL)
 	 { printf("Error:  Unable to open %s\n", argv[1]); return -1;}
  
  findEndOfHeader();
  readLong("WiiID:");
  files = readLong("Number of files");
  readLong("FileDataLen");
  readLongChk("Magic",0x00000000);
  readLongChk("Magic",0x00000000);
  readLong("PostHeadLen");
  readBlock("Zeros",64,0);
  readLongChk("Magic",0x00010000);
  readLong("PrgID");
  readLong("MacAdd");
  readLongChk("Magic",0xF5550000);  
  sprintf(filename,"%s.hash1\0",argv[1],i+1);
  readBlock("Hash1",16,filename);  
  for(i = 0; i < files; i++)
  {
    sprintf(title,"Magic%d",i);
    readLong(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);
    sprintf(filename,"%s.f%dsfille\0",argv[1],i+1);
    readBlock("StrFiller",117-strlen(read_String)-1,filename);
    sprintf(filename,"%s.f%ddata\0",argv[1],i+1);
    readBlock("Filedata",filesize,filename);
    sprintf(filename,"%s.f%ddfill\0",argv[1],i+1);
    readBlock("DataFiller",filesize % 64,filename);
  }
  sprintf(filename,"%s.hash2\0",argv[1]);
  readBlock("Hash2",60,filename);  
  
  readLongChk("Magic",0x00000000);
  readLongChk("Magic",0x00010002);
  
  sprintf(filename,"%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,"%s.hash4\0",argv[1]);
  readBlock("Hash4",64,filename);
  
  readBlock("Zeros",60,0);
  readLongChk("Magic",0x00010002);
  
  sprintf(filename,"%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,"%s.hash6\0",argv[1]);
  readBlock("Hash6",60,filename);

}

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;

}
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);
  fseek(fp,location + 8, SEEK_SET);
  address = location + 8;

}

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)...Check failed!\n",address, name,data[0],data[0]);}
   else{printf("0x%.8x |  %s 0x%.2x (%uL)...Check 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 = calloc(len,sizeof(unsigned char));
   unsigned long bytes_read = 0;
   bytes_read = fread(data,len,sizeof(unsigned char), fp);
   printf("0x%.8x | Block  %s ends after %uL bytes\n", address,title,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...Check failed!\n",address, name,dataL,chk); }
   else{printf("0x%.8x |  %s 0x%.8x (%uL)...Check 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;
}