#include "svnrev.h" #include "../WiiQt/includes.h" #include "../WiiQt/nandbin.h" #include "../WiiQt/sharedcontentmap.h" #include "../WiiQt/uidmap.h" #include "../WiiQt/tools.h" #include "../WiiQt/tiktmd.h" #include "../WiiQt/settingtxtdialog.h" #include "../WiiQt/u8.h" #include "../WiiQt/keysbin.h" //yippie for global variables QStringList args; NandBin nand; SharedContentMap sharedM; UIDmap uidM; QList< quint64 > tids; QList< quint64 > validIoses;//dont put stubs in this list. QTreeWidgetItem *root; QList fats; quint32 verbose = 0; bool tryToKeepGoing = false; bool color = true; bool calcRsa = false; QByteArray sysMenuResource; QByteArray sysMenuExe; quint64 sysMenuIos; QListBadSharedItems;//remember bad shared items bool CheckTitleIntegrity( quint64 tid ); #ifdef Q_WS_WIN #include #define C_STICKY 31 #define C_CAP 192 int origColor; HANDLE hConsole; int GetColor() { WORD wColor = 0; HANDLE hStdOut = GetStdHandle( STD_OUTPUT_HANDLE ); CONSOLE_SCREEN_BUFFER_INFO csbi; //We use csbi for the wAttributes word. if( GetConsoleScreenBufferInfo( hStdOut, &csbi ) ) { wColor = csbi.wAttributes; } return wColor; } #else #define LEN_STR_PAIR(s) sizeof (s) - 1, s enum indicator_no { C_LEFT, C_RIGHT, C_END, C_RESET, C_NORM, C_FILE, C_DIR, C_LINK, C_FIFO, C_SOCK, C_BLK, C_CHR, C_MISSING, C_ORPHAN, C_EXEC, C_DOOR, C_SETUID, C_SETGID, C_STICKY, C_OTHER_WRITABLE, C_STICKY_OTHER_WRITABLE, C_CAP, C_MULTIHARDLINK, C_CLR_TO_EOL }; struct bin_str { size_t len; /* Number of bytes */ const char *string; /* Pointer to the same */ }; static struct bin_str color_indicator[] = { { LEN_STR_PAIR ("\033[") }, /* lc: Left of color sequence */ { LEN_STR_PAIR ("m") }, /* rc: Right of color sequence */ { 0, NULL }, /* ec: End color (replaces lc+no+rc) */ { LEN_STR_PAIR ("0") }, /* rs: Reset to ordinary colors */ { 0, NULL }, /* no: Normal */ { 0, NULL }, /* fi: File: default */ { LEN_STR_PAIR ("01;34") }, /* di: Directory: bright blue */ { LEN_STR_PAIR ("01;36") }, /* ln: Symlink: bright cyan */ { LEN_STR_PAIR ("33") }, /* pi: Pipe: yellow/brown */ { LEN_STR_PAIR ("01;35") }, /* so: Socket: bright magenta */ { LEN_STR_PAIR ("01;33") }, /* bd: Block device: bright yellow */ { LEN_STR_PAIR ("01;33") }, /* cd: Char device: bright yellow */ { 0, NULL }, /* mi: Missing file: undefined */ { 0, NULL }, /* or: Orphaned symlink: undefined */ { LEN_STR_PAIR ("01;32") }, /* ex: Executable: bright green */ { LEN_STR_PAIR ("01;35") }, /* do: Door: bright magenta */ { LEN_STR_PAIR ("37;41") }, /* su: setuid: white on red */ { LEN_STR_PAIR ("30;43") }, /* sg: setgid: black on yellow */ { LEN_STR_PAIR ("37;44") }, /* st: sticky: black on blue */ { LEN_STR_PAIR ("34;42") }, /* ow: other-writable: blue on green */ { LEN_STR_PAIR ("30;42") }, /* tw: ow w/ sticky: black on green */ { LEN_STR_PAIR ("30;41") }, /* ca: black on red */ { 0, NULL }, /* mh: disabled by default */ { LEN_STR_PAIR ("\033[K") }, /* cl: clear to end of line */ }; static void put_indicator( const struct bin_str *ind ) { fwrite( ind->string, ind->len, 1, stdout ); } #endif void PrintColoredString( const char *msg, int highlite ) { if( !color ) { printf( "%s\n", msg ); } else { QString str( msg ); QStringList list = str.split( "\n", QString::SkipEmptyParts ); foreach( const QString &s, list ) { QString m = s; QString m2 = s.trimmed(); m.resize( m.indexOf( m2 ) ); printf( "%s", m.toLatin1().data() ); //print all leading whitespace #ifdef Q_WS_WIN SetConsoleTextAttribute( hConsole, highlite ); #else put_indicator( &color_indicator[ C_LEFT ] ); put_indicator( &color_indicator[ highlite ] ); //change color put_indicator( &color_indicator[ C_RIGHT ] ); #endif printf( "%s", m2.toLatin1().data() ); //print text #ifdef Q_WS_WIN SetConsoleTextAttribute( hConsole, origColor ); #else put_indicator( &color_indicator[ C_LEFT ] ); put_indicator( &color_indicator[ C_NORM ] ); //reset color put_indicator( &color_indicator[ C_RIGHT ] ); #endif printf( "\n" ); } } fflush( stdout ); } //redirect text output. by default, qDebug() goes to stderr void DebugHandler( QtMsgType type, const char *msg ) { switch( type ) { case QtDebugMsg: printf( "%s\n", msg ); break; case QtWarningMsg: PrintColoredString( msg, C_STICKY ); break; case QtCriticalMsg: PrintColoredString( msg, C_CAP ); break; case QtFatalMsg: fprintf( stderr, "Fatal Error: %s\n", msg ); abort(); break; } } void Usage() { qWarning() << "usage:" << QCoreApplication::arguments().at( 0 ) << "nand.bin" << ""; qDebug() << "\nif no are given, it will default to \"-all -v -v\""; qDebug() << "\nOther options:"; qDebug() << " -boot shows information about boot 1 and 2"; qDebug() << ""; qDebug() << " -fs verify the filesystem is in tact"; qDebug() << " verifies presence of uid & content.map & checks the hashes in the content.map"; qDebug() << " check sha1 hashes for title private contents"; qDebug() << " check all titles with a ticket titles for required IOS, proper uid & gid"; qDebug() << ""; qDebug() << " -settingtxt check setting.txt itself and against system menu resources. this must be combined with \"-fs\""; qDebug() << ""; qDebug() << " -uid Look any titles in the uid.sys, check signatures and whatnot. this must be combined with \"-fs\""; qDebug() << ""; qDebug() << " -rsa Calculate and compare RSA signatures. this must be combined with \"-fs\""; qDebug() << ""; qDebug() << " -clInfo shows free, used, and lost ( marked used, but dont belong to any file ) clusters"; qDebug() << ""; qDebug() << " -spare calculate & compare ecc for all pages in the nand"; qDebug() << " calculate & compare hmac signatures for all files and superblocks"; qDebug() << ""; qDebug() << " -all does all of the above"; qDebug() << ""; qDebug() << " -v increase verbosity ( can be used more than once )"; qDebug() << ""; qDebug() << " -continue try to keep going as fas as possible on errors that should be fatal"; qDebug() << ""; qDebug() << " -nocolor don\'t use terminal color trickery"; qDebug() << ""; qDebug() << " -about info about this program"; exit( 1 ); } void About() { qCritical() << " (c) giantpune 2010, 2011"; qCritical() << " http://code.google.com/p/wiiqt/"; qCritical() << " built:" << __DATE__ << __TIME__; qWarning() << "This software is licensed under GPLv2. It comes with no guarentee that it will work,"; qWarning() << "or that it will work well."; qDebug() << ""; qDebug() << "This program is designed to gather information about a nand dump for a Nintendo Wii"; exit( 1 ); } void Fail( const QString& str ) { qCritical() << str; if( !tryToKeepGoing ) exit( 1 ); } QString TidTxt( quint64 tid ) { return QString( "%1" ).arg( tid, 16, 16, QChar( '0' ) ); } QString AsciiTxt( quint32 lower ) { QString ret; lower = qFromBigEndian( lower ); for( int i = 0; i < 4; i++ ) ret += ascii( (char)( lower >> ( 8 * i ) ) & 0xff ); return ret; } void ShowBootInfo( quint8 boot1, QList boot2stuff ) { switch( boot1 ) { case BOOT_1_A: qDebug() << "Boot1 A (vulnerable)"; break; case BOOT_1_B: qDebug() << "Boot1 B (vulnerable)"; break; case BOOT_1_C: qDebug() << "Boot1 C (fixed)"; break; case BOOT_1_D: qDebug() << "Boot1 D (fixed)"; break; default: qWarning() << "unrecognized boot1 version"; break; } quint16 cnt = boot2stuff.size(); if( !cnt ) Fail( "didnt find any boot2. this nand wont work" ); qDebug() << "found" << cnt << "copies of boot2"; for( quint16 i = 0; i < cnt; i++ ) { Boot2Info bi = boot2stuff.at( i ); QString str = QString( "blocks %1 & %2: " ).arg( bi.firstBlock ).arg( bi.secondBlock ); if( bi.state == BOOT_2_ERROR_PARSING || bi.state == BOOT_2_ERROR ) str += "parsing error"; else if( bi.state == BOOT_2_BAD_SIGNATURE ) str += "Bad RSA Signature"; else if( bi.state == BOOT_2_BAD_CONTENT_HASH ) str += "Content hash doesn't match TMD"; else if( bi.state == BOOT_2_ERROR_PARSING ) str += "Error parsing boot2"; else { if( bi.state & BOOT_2_MARKED_BAD ) str += "Marked as bad blocks; "; else if( bi.state & BOOT_2_USED_TO_BOOT ) { str += "Used for booting; "; if( boot1 != BOOT_1_A && boot1 != BOOT_1_B && (( bi.state & BOOT_2_TIK_FAKESIGNED) || ( bi.state & BOOT_2_TMD_FAKESIGNED ) ) ) Fail( "The version of boot1 installed doesn't have the strncmp bug and the copy of boot2 used for booting is fakesigned" ); } else if( bi.state & BOOT_2_BACKUP_COPY ) str += "Backup copy; "; str += "Content Sha1 matches TMD; "; if( bi.state & BOOT_2_TMD_FAKESIGNED ) str += "TMD is fakesigned; "; else if( bi.state & BOOT_2_TMD_SIG_OK ) str += "TMD officially signed; "; else str += "Error checking TMD; " ; if( bi.state & BOOT_2_TIK_FAKESIGNED ) str += "Ticket is fakesigned; "; else if( bi.state & BOOT_2_TIK_SIG_OK ) str += "Ticket officially signed; "; else str += "Error checking ticket; "; QString ver; switch( bi.version ) { case BOOTMII_11: ver = "BootMii 1.1"; break; case BOOTMII_13: ver = "BootMii 1.3"; break; case BOOTMII_UNK: ver = "BootMii (Unk)"; break; default: ver = QString( "Version %1" ).arg( bi.version ); break; } str += ver; } qDebug() << str; } } QTreeWidgetItem *FindItem( const QString &s, QTreeWidgetItem *parent ) { int cnt = parent->childCount(); for( int i = 0; i child( i ); if( r->text( 0 ) == s ) { return r; } } return NULL; } QTreeWidgetItem *ItemFromPath( const QString &path ) { QTreeWidgetItem *item = root; if( !path.startsWith( "/" ) || path.contains( "//" )) { return NULL; } int slash = 1; while( slash ) { int nextSlash = path.indexOf( "/", slash + 1 ); QString lookingFor = path.mid( slash, nextSlash - slash ); item = FindItem( lookingFor, item ); if( !item ) { //if( verbose ) // qWarning() << "ItemFromPath ->item not found" << path; return NULL; } slash = nextSlash + 1; } return item; } QString PathFromItem( QTreeWidgetItem *item ) { QString ret; while( item ) { ret.prepend( "/" + item->text( 0 ) ); item = item->parent(); if( item->text( 0 ) == "/" )// dont add the root break; } return ret; } //get the installed titles in the nand - first check for tickets and then check for TMDs that match that ticket QList< quint64 > InstalledTitles() { QList< quint64 > ret; QTreeWidgetItem *tikFolder = ItemFromPath( "/ticket" ); if( !tikFolder ) Fail( "Couldnt find a ticket folder in this nand" ); quint16 subfc = tikFolder->childCount(); for( quint16 i = 0; i < subfc; i++ )//check all subfolders of "/ticket" { QTreeWidgetItem *subF = tikFolder->child( i ); //qDebug() << "checking folder" << subF->text( 0 ); bool ok = false; quint32 upper = subF->text( 0 ).toUInt( &ok, 16 );//make sure it can be converted to int if ( !ok ) continue; quint16 subfc2 = subF->childCount();//get all entries in this subfolder for( quint16 j = 0; j < subfc2; j++ ) { QTreeWidgetItem *tikI = subF->child( j ); QString name = tikI->text( 0 ); if( !name.endsWith( ".tik" ) ) { continue; } name.resize( 8 ); quint32 lower = name.toUInt( &ok, 16 ); if( !ok ) { continue; } //now see if there is a tmd QTreeWidgetItem *tmdI = ItemFromPath( "/title/" + subF->text( 0 ) + "/" + name + "/content/title.tmd" ); if( !tmdI ) { continue; } quint64 tid = ( ( (quint64)upper << 32) | lower ); ret << tid; } } qSort( ret.begin(), ret.end() ); return ret; } void CheckShared() { QByteArray ba = nand.GetData( "/shared1/content.map" ); if( ba.isEmpty() ) Fail( "No content map found in the nand" ); sharedM = SharedContentMap( ba ); quint16 cnt = sharedM.Count(); for( quint16 i = 0; i < cnt; i++ ) { QString path = "/shared1/" + sharedM.Cid( i ) + ".app"; qDebug() << "checking" << path << "..."; QByteArray stuff = nand.GetData( path ); if( stuff.isEmpty() ) { BadSharedItems << sharedM.Hash( i ); Fail( "One of the shared contents in this nand is missing" ); } QByteArray realHash = GetSha1( stuff ); if( realHash != sharedM.Hash( i ) ) { BadSharedItems << sharedM.Hash( i ); if( verbose ) { qCritical() << "\texpected: " << sharedM.Hash( i ).toHex(); qCritical() << "\tactual: " << realHash.toHex(); } Fail( "The hash for at least 1 content is bad" ); } } } void BuildGoodIosList() { foreach( quint64 tid, tids ) { quint32 upper = ( tid >> 32 ) & 0xffffffff; if( upper != 1 ) continue; quint32 lower = tid & 0xffffffff; if( lower < 3 || lower > 255 ) continue; QString tmdp = TidTxt( tid ); tmdp.insert( 8, "/" ); tmdp.prepend( "/title/" ); tmdp += "/content/title.tmd"; QByteArray ba = nand.GetData( tmdp ); if( ba.isEmpty() ) continue; Tmd t( ba ); //skip stubbzzzzz if( !( t.Version() % 0x100 ) && //version is a nice pretty round number t.Count() == 3 && //3 contents, 1 private and 2 shared t.Type( 0 ) == 1 && t.Type( 1 ) == 0x8001 && t.Type( 2 ) == 0x8001 ) { continue; } if( !CheckTitleIntegrity( tid ) ) continue; validIoses << tid; //seems good enough. add it to the list } } bool RecurseCheckGidUid( QTreeWidgetItem *item, const QString &uidS, const QString &gidS, const QString &path ) { bool ret = true; quint16 cnt = item->childCount(); for( quint16 i = 0; i < cnt; i++ ) { QTreeWidgetItem *child = item->child( i ); if( child->text( 3 ) != uidS || !child->text( 4 ).startsWith( gidS ) ) { ret = false; qWarning() << "\tincorrect uid/gid for" << QString( path + child->text( 0 ) ); } if( !RecurseCheckGidUid( child, uidS, gidS, path + child->text( 0 ) + "/" ) ) ret = false; } return ret; } void PrintName( const QByteArray &app ) { QString desc; if( app.size() == 0x40 )//tag for IOS, region select, ... { desc = QString( app ).simplified(); QString desc2 = QString( app.right( 0x10 ) ); if( !desc2.isEmpty() ) desc += " " + desc2; } else if( U8::GetU8Offset( app ) >= 0 ) //maybe this is an IMET header. try to get a name from it { U8 u8( app ); if( u8.IsOK() ) { QStringList names = u8.IMETNames(); quint8 cnt = names.size(); if( cnt >= 2 ) //try to use english name first desc = names.at( 1 ); for( quint8 i = 0; i < cnt && desc.isEmpty(); i++ ) { desc = names.at( i ); } } } if( desc.isEmpty() ) { qDebug() << "\tUnable to get title"; return; } qDebug() << "\tname: " << desc; } bool CheckArm003( const QByteArray &stuff ) { qint32 esTag = MAX( stuff.indexOf( "$IOSVersion: ES" ), stuff.indexOf( "$IOSVersion: ES" ) ); if( esTag < 0 ) { qWarning() << "\tFailed to find the ES module in the kernel"; return false; } if( stuff.contains( QByteArray::fromHex( "e2511cb1d7fbbef8d7f97db5a1d81694" ) ) //1337 buffer #1 || stuff.contains( QByteArray::fromHex( "3f5b8cc9ea855a0afa7347d23e8d664e" ) ) //1337 buffer #2 || stuff.contains( QByteArray::fromHex( "b570b08868851c01310c22c0005218ab681b2b00d10248bff001f852680b2b45" ) ) )//blablabla, CMP R3, #0x45 { if( verbose > 1 ) qWarning() << "\tSystem menu IOS supports the Korean-key check"; return true; } if( verbose > 1 ) qDebug() << "\tSystem menu IOS does not appear to support the Korean-key check"; return false; } void Check003() { qDebug() << "Checking for 003 error ..."; if( sysMenuExe.isEmpty() ) { qWarning() << "can\'t check 003 error for empty data"; return; } bool brick = true; //check the PPC half if( !sysMenuExe.contains( QByteArray::fromHex( "3880004538A0000038C00000" ) ) ) //li %r4, 0x45 { //li %r5, 0 brick = false; //li %r6, 0 if( verbose > 1 ) qDebug() << "\tThe system menu doesn\'t appear to perform the Korean-key check"; } else if( verbose > 1 ) qWarning() << "\tThe system menu performs the Korean-key check"; //check the ARM half QString iosStr = TidTxt( sysMenuIos ); iosStr.insert( 8, "/" ); iosStr.prepend( "/title/" ); iosStr += "/content/"; QByteArray tmdD = nand.GetData( iosStr + "title.tmd" ); if( tmdD.isEmpty() ) { qWarning() << "\tcan\'t read systemmenu-ios's TMD"; return; } Tmd t( tmdD ); QByteArray iosKernel; QString kernelPath; quint16 cnt = t.Count(); if( t.BootIndex() >= cnt ) //in case there is some really fucked up TMD { Fail( "\tThe system menu ios bootindex is fucked up pretty bad" ); return; } if( t.Type( t.BootIndex() ) == 0x8001 ) { QString appname = sharedM.GetAppFromHash( t.Hash( t.BootIndex() ) ); if( appname.isEmpty() ) { Fail( "\tError reading the system menu ios" ); return; } kernelPath = "/shared1/" + appname + ".app"; } else { kernelPath = iosStr + t.Cid( t.BootIndex() ) + ".app"; } iosKernel = nand.GetData( kernelPath ); if( iosKernel.isEmpty() ) { Fail( "\tError reading the system menu ios data" ); return; } if( !CheckArm003( iosKernel ) ) brick = false; //look for korean keys in keys.bin QByteArray keys = nand.Keys(); if( keys.size() != 0x400 ) { Fail( "\tError getting nand keys" ); return; } quint8 k_key[ 16 ] = KOREAN_KEY; if( !keys.contains( QByteArray( (const char*)&k_key, 16 ) ) ) { brick = false; if( verbose > 1 ) qDebug() << "\tThe korean key is not present in this wii"; } else if( verbose > 1 ) qWarning() << "\tThis wii contains the korean key"; if( brick ) Fail( "\tThis wii will likely show the 003 error" ); } bool CheckTitleIntegrity( quint64 tid ) { if( validIoses.contains( tid ) )//this one has already been checked return true; quint32 upper = ( ( tid >>32 ) & 0xffffffff ); quint32 lower = ( tid & 0xffffffff ); if( verbose ) qDebug() << ""; qDebug() << "Checking" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ) << "..."; QString p = TidTxt( tid ); p.insert( 8 ,"/" ); QString tikp = p; tikp.prepend( "/ticket/" ); tikp += ".tik"; QString tmdp = p; tmdp.prepend( "/title/" ); tmdp += "/content/title.tmd"; Tmd t; //check the presence of the tmd & ticket. also check their signatures //remember the tmd for checking the actual contents for( quint8 i = 0; i < 2; i++ ) { QString it = ( i ? "tmd" : "ticket" ); QByteArray ba = nand.GetData( i ? tmdp : tikp ); if( ba.isEmpty() ) { qDebug() << "error getting" << it << "data"; return false; } if( i )//tmd { t = Tmd( ba ); if( t.Tid() != tid ) { qWarning() << "\tthe TMD contains the wrong TID"; return false; } if( calcRsa ) { qint32 ch = check_cert_chain( ba ); switch( ch ) { case ERROR_SIG_TYPE: case ERROR_SUB_TYPE: case ERROR_RSA_HASH: case ERROR_RSA_TYPE_UNKNOWN: case ERROR_RSA_TYPE_MISMATCH: case ERROR_CERT_NOT_FOUND: qWarning().nospace() << "\t" << qPrintable( it ) << " RSA signature isn't even close ( " << ch << " )"; //return false; //maye in the future this will be true, but for now, this doesnt mean it wont boot break; case ERROR_RSA_FAKESIGNED: qWarning().nospace() << "\t" << qPrintable( it ) << " fakesigned"; break; default: break; } } } else { Ticket ticket( ba, false ); if( ticket.Tid() != tid ) { qWarning() << "\tthe ticket contains the wrong TID"; return false; } if( calcRsa ) { int tikVersions = ba.size() / 0x2a4; qint32 ch = ERROR_RSA_TYPE_UNKNOWN; bool ok = false; for( int rr = 0; rr < tikVersions && !ok; rr++ ) { ch = check_cert_chain( ba.mid( rr * 0x2a4, 0x2a4 ) ); switch( ch ) { default: break; case ERROR_RSA_FAKESIGNED: case ERROR_SUCCESS: ok = true; break; } } switch( ch ) { case ERROR_SIG_TYPE: case ERROR_SUB_TYPE: case ERROR_RSA_HASH: case ERROR_RSA_TYPE_UNKNOWN: case ERROR_RSA_TYPE_MISMATCH: case ERROR_CERT_NOT_FOUND: qWarning().nospace() << "\t" << qPrintable( it ) << " RSA signature isn't even close ( " << ch << " )"; //return false; //maye in the future this will be true, but for now, this doesnt mean it wont boot break; case ERROR_RSA_FAKESIGNED: qWarning().nospace() << "\t" << qPrintable( it ) << " fakesigned"; break; default: break; } } } } if( upper == 0x10005 || upper == 0x10007 ) //dont try to verify all the contents of DLC, it will just find a bunch of missing contents and bitch about them return true; quint16 cnt = t.Count(); for( quint16 i = 0; i < cnt; i++ ) { if( t.Type( i ) == 0x8001 )//shared { if( BadSharedItems.contains( t.Hash( i ) ) ) { qWarning() << "\tthis title relies on a shared content that is borked (" << i << ")\n\t" << t.Hash( i ).toHex(); return false; } if( sharedM.GetAppFromHash( t.Hash( i ) ).isEmpty() ) { qWarning() << "\tone of the shared contents is missing"; return false; } } else//private { QString cid = t.Cid( i ); QString pA = tmdp; pA.resize( 33 ); pA += cid + ".app"; QByteArray ba = nand.GetData( pA ); if( ba.isEmpty() ) { qWarning() << "\t error reading one of the private contents" << pA; return false; } QByteArray realH = GetSha1( ba ); if( realH != t.Hash( i ) ) { qWarning() << "\tone of the private contents' hash doesnt check out" << i << pA << "\n\texpected" << qPrintable( QString( t.Hash( i ).toHex() ) ) << "\n\tactual " << qPrintable( QString( realH.toHex() ) ); //return false; //dont return false, as this this title may still boot } //if we are going to check the setting.txt stuff, we need to get the system menu resource file to compare ( check for opera bricks ) //so far, i think this file is always boot index 1, type 1 if( tid == 0x100000002ull ) { if( t.Index( i ) == 1 && ( args.contains( "-settingtxt", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) ) { sysMenuResource = ba; } else if( t.Index( i ) == t.BootIndex() ) sysMenuExe = ba; } //print a description of this title if( verbose > 1 && t.Index( i ) == 0 ) { PrintName( ba ); } } } //print version if( verbose ) { quint16 vers = t.Version(); qDebug() << "\tversion:" << qPrintable( QString( "%1.%2" ).arg( ( vers >> 8 ) & 0xff ).arg( vers & 0xff ).leftJustified( 10 ) ) << qPrintable( QString( "%1" ).arg( vers ).leftJustified( 10 ) ) << "hex:" << hex << t.Version(); if( t.AccessFlags() ) qDebug() << "\taccess :" << hex << t.AccessFlags(); } quint64 ios = t.IOS(); if( ios && !validIoses.contains( ios ) ) { qWarning() << "\tthe IOS for this title is not bootable\n\t" << TidTxt( ios ).insert( 8, "-" ); return false; } if( tid == 0x100000002ull ) sysMenuIos = ios; if( verbose > 1 && ( upper != 1 || tid == 0x100000002ull ) ) qDebug() << "\trequires IOS" << ((quint32)( ios & 0xffffffff )); quint32 uid = uidM.GetUid( tid, false ); if( !uid ) { qDebug() << "\tthis title has no UID entry"; return false; } //make sure all the stuff in this title's data directory belongs to it, and all the contents belong to nobody //( im looking at you priibricker & dop-mii ) QString dataP = tmdp; dataP.resize( 25 ); dataP += "data"; QTreeWidgetItem *dataI = ItemFromPath( dataP ); if( dataI ) { quint16 gid = t.Gid(); QString uidS = QString( "%1" ).arg( uid, 8, 16, QChar( '0' ) ); QString gidS = QString( "%1" ).arg( gid, 4, 16, QChar( '0' ) ); if( dataI->text( 3 ) != uidS || !dataI->text( 4 ).startsWith( gidS ) )//dont necessarily fail for this. the title will still be bootable without its data qWarning().nospace() << "\tincorrect uid/gid for data folder-- expected: " << uidS << "/" << gidS << " got: " << dataI->text( 3 ) << "/" << dataI->text( 4 ).left( 4 ); RecurseCheckGidUid( dataI, uidS, gidS, "data/" ); } dataP.resize( 25 ); dataP += "content"; dataI = ItemFromPath( dataP ); if( dataI ) { quint16 cnt = dataI->childCount(); for( quint16 i = 0; i < cnt; i++ ) { QTreeWidgetItem *item = dataI->child( i ); if( item->text( 3 ) != "00000000" || !item->text( 4 ).startsWith( "0000" ) ) qWarning() << "\tincorrect uid/gid for" << QString( "content/" + item->text( 0 ) ); } } return true; } void ListDeletedTitles() { QByteArray uidSys = uidM.Data(); if( uidSys.isEmpty() ) { qDebug() << "Can\'t check deleted titles without a uid.sys"; return; } qDebug() << "Comparing uid.sys against the filesystem..."; //hexdump12( uidSys ); QBuffer buf( &uidSys ); buf.open( QIODevice::ReadWrite ); quint64 tid; quint16 factory = 0; quint32 cnt = uidSys.size() / 12; //skip past items installed at the factory for( quint32 i = 0; i < cnt; i++ ) { buf.seek( i * 12 ); buf.read( (char*)&tid, 8 ); tid = qFromBigEndian( tid ); quint32 upper = ( ( tid >> 32 ) & 0xffffffff ); quint32 lower = ( tid & 0xffffffff ); if( ( upper == 0x10001 && ( ( lower >> 24 ) & 0xff ) != 0x48 ) || //a channel, not starting with 'H' lower == 0x48415858 || //original HBC tid == 0x100000000ull || //bannerbomb -> ATD ( or any other program that uses the SU tid ) ( upper == 0x10000 && ( ( lower & 0xffffff00 ) == 0x555000 ) ) ) //a disc update partition break; if( ( verbose || upper != 0x10000 ) && !tids.contains( tid ) ) qDebug().nospace() << "\t" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ) << " was installed at the factory and is now missing"; factory++; } if( verbose ) qDebug() << factory << "titles were installed before any user intervention"; for( quint32 i = factory; i < cnt; i++ ) { buf.seek( i * 12 ); buf.read( (char*)&tid, 8 ); tid = qFromBigEndian( tid ); if( tids.contains( tid ) ) //this one is already checked continue; quint32 upper = ( ( tid >> 32 ) & 0xffffffff ); quint32 lower = ( tid & 0xffffffff ); bool deleted = false; //load tmd QString path = QString( "/title/%1/%2/content/title.tmd" ).arg( upper, 8, 16, QChar( '0' ) ).arg( lower, 8, 16, QChar( '0' ) ); QTreeWidgetItem *item = ItemFromPath( path ); if( !item ) { if( verbose > 1 ) qWarning() << "\tCan\'t find TMD for" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ); deleted = true; } else { QByteArray ba = nand.GetData( path ); if( ba.isEmpty() ) { if( verbose > 1 ) qWarning() << "\tError reading TMD for" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ); deleted = true; } else if( calcRsa ) { qint32 ch = check_cert_chain( ba ); switch( ch ) { case ERROR_SIG_TYPE: case ERROR_SUB_TYPE: case ERROR_RSA_HASH: case ERROR_RSA_TYPE_UNKNOWN: case ERROR_RSA_TYPE_MISMATCH: case ERROR_CERT_NOT_FOUND: qWarning().nospace() << "\tTMD RSA signature isn't even close for " << qPrintable( TidTxt( tid ).insert( 8, "-" ) ) << " ( " << ch << " )"; break; case ERROR_RSA_FAKESIGNED: qWarning().nospace() << "\tTMD for " << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ) << " is fakesigned"; break; default: break; } } } if( upper != 0x10000 )//dont try to load ticket for disc games { //load ticket path = QString( "/ticket/%1/%2.tik" ).arg( upper, 8, 16, QChar( '0' ) ).arg( lower, 8, 16, QChar( '0' ) ); item = ItemFromPath( path ); if( !item ) { if( verbose > 1 ) qWarning() << "\tCan\'t find ticket for" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ); deleted = true; } else { QByteArray ba = nand.GetData( path ); if( ba.isEmpty() ) { if( verbose > 1 ) qWarning() << "\tError reading ticket for" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ); deleted = true; } else if( calcRsa ) { qint32 ch = check_cert_chain( ba ); switch( ch ) { case ERROR_SIG_TYPE: case ERROR_SUB_TYPE: case ERROR_RSA_HASH: case ERROR_RSA_TYPE_UNKNOWN: case ERROR_RSA_TYPE_MISMATCH: case ERROR_CERT_NOT_FOUND: qWarning().nospace() << "\tticket RSA signature isn't even close for " << qPrintable( TidTxt( tid ).insert( 8, "-" ) ) << " ( " << ch << " )"; break; case ERROR_RSA_FAKESIGNED: qWarning().nospace() << "\tticket for " << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ) << " is fakesigned"; break; default: break; } } } } if( deleted ) qWarning().nospace() << "\t" << qPrintable( TidTxt( tid ).insert( 8, "-" ) + ( upper != 1 ? " (" + AsciiTxt( lower ) + ")" : "" ) ) << " has been deleted"; } buf.close(); } void CheckLostClusters() { QList u = nand.GetFatsForEntry( 0 );//all clusters actually used for a file if( verbose ) qDebug() << "total used clusters" << hex << u.size() << "of 0x8000"; quint16 lost = 0; QList ffs; QList frs; fats = nand.GetFats(); for( quint16 i = 0; i < 0x8000; i++ ) { if( u.contains( fats.at( i ) ) )//this cluster is really used continue; switch( fats.at( i ) ) { case 0xFFFB: case 0xFFFC: case 0xFFFD: break; case 0xFFFE: frs << i; break; case 0xFFFF: ffs << i; break; default: lost++; //qDebug() << hex << i << fats.at( i ); break; } } qDebug() << "found" << lost << "lost clusters\nUNK ( 0xffff )" << hex << ffs.size() << ffs << "\nfree " << frs.size(); } void CheckEcc() { QList< quint32 > bad; QList< quint16 > clusters; fats = nand.GetFats(); quint32 checked = 0; quint16 badClustersNotSpecial = 0; for( quint16 i = 0; i < 0x8000; i++ ) { if( fats.at( i ) == 0xfffd || fats.at( i ) == 0xfffe ) continue; for( quint8 j = 0; j < 8; j++, checked += 8 ) { quint32 page = ( i * 8 ) + j; if( !nand.CheckEcc( page ) ) { bad << page; if( !clusters.contains( i ) ) clusters << i; } } } QList< quint16 > blocks; QList< quint16 > clustersCpy = clusters; while( clustersCpy.size() ) { quint16 p = clustersCpy.takeFirst(); if( fats.at( p ) < 0xfff0 ) badClustersNotSpecial++; quint16 block = p/8; if( !blocks.contains( block ) ) blocks << block; } qDebug() << bad.size() << "out of" << checked << "pages had incorrect ecc.\nthey were spread through" << clusters.size() << "clusters in" << blocks.size() << "blocks:\n" << blocks; qDebug() << badClustersNotSpecial << "of those clusters are non-special (they belong to the fs)"; } void SetUpTree() { if( root ) return; QTreeWidgetItem *r = nand.GetTree(); if( r->childCount() != 1 || r->child( 0 )->text( 0 ) != "/" ) { tryToKeepGoing = false; Fail( "The nand FS is seriously broken. I Couldn't even find a correct root" ); } root = r->takeChild( 0 ); delete r; } int RecurseCountFiles( QTreeWidgetItem *dir ) { int ret = 0; quint16 cnt = dir->childCount(); for( quint16 i = 0; i < cnt; i++ ) { QTreeWidgetItem *item = dir->child( i ); if( item->text( 7 ).startsWith( "02" ) ) ret += RecurseCountFiles( item ); else { ret++; } } return ret; } int RecurseCheckHmac( QTreeWidgetItem *dir ) { int ret = 0; quint16 cnt = dir->childCount(); for( quint16 i = 0; i < cnt; i++ ) { QTreeWidgetItem *item = dir->child( i ); if( item->text( 7 ).startsWith( "02" ) ) ret += RecurseCheckHmac( item ); else { bool ok = false; quint16 entry = item->text( 1 ).toInt( &ok ); if( !ok ) continue; if( !nand.CheckHmacData( entry ) ) { qCritical() << "bad HMAC for" << PathFromItem( item ); ret++; } } } return ret; } void CheckHmac() { quint16 total = RecurseCountFiles( root ); qDebug() << "verifying hmac for" << total << "files"; quint16 bad = RecurseCheckHmac( root ); qDebug() << bad << "files had bad HMAC data"; qDebug() << "checking HMAC for superclusters..."; QList sclBad; for( quint16 i = 0x7f00; i < 0x8000; i += 0x10 ) { if( !nand.CheckHmacMeta( i ) ) sclBad << i; } qDebug() << sclBad.size() << "superClusters had bad HMAC data"; if( sclBad.size() ) qCritical() << sclBad; } void CheckSettingTxt() { qDebug() << "Checking setting.txt stuff..."; QByteArray settingTxt = nand.GetData( "/title/00000001/00000002/data/setting.txt" ); if( settingTxt.isEmpty() ) { Fail( "Error reading setting.txt" ); return; } settingTxt = SettingTxtDialog::LolCrypt( settingTxt ); QString area; bool hArea = false; bool hModel = false; bool hDvd = false; bool hMpch = false; bool hCode = false; bool hSer = false; bool hVideo = false; bool hGame = false; bool shownSetting = false; QString str( settingTxt ); str.replace( "\r\n", "\n" );//maybe not needed to do this in 2 steps, but there may be some reason the file only uses "\n", so do it this way to be safe QStringList parts = str.split( "\n", QString::SkipEmptyParts ); foreach( const QString &part, parts ) { if( part.startsWith( "AREA=" ) ) { if( hArea ) goto error; hArea = true; area = part; area.remove( 0, 5 ); } else if( part.startsWith( "MODEL=" ) ) { if( hModel ) goto error; hModel = true; } else if( part.startsWith( "DVD=" ) ) { if( hDvd ) goto error; hDvd = true; } else if( part.startsWith( "MPCH=" ) ) { if( hMpch ) goto error; hMpch = true; } else if( part.startsWith( "CODE=" ) ) { if( hCode ) goto error; hCode = true; } else if( part.startsWith( "SERNO=" ) ) { if( hSer ) goto error; hSer = true; } else if( part.startsWith( "VIDEO=" ) ) { if( hVideo ) goto error; hVideo = true; } else if( part.startsWith( "GAME=" ) ) { if( hGame ) goto error; hGame = true; } else { qDebug() << "Extra stuff in the setting.txt."; hexdump( settingTxt ); qDebug() << QString( settingTxt ); shownSetting = true; } } //something is missing if( !hArea || !hModel || !hDvd || !hMpch || !hCode || !hSer || !hVideo || !hGame ) goto error; //check for opera brick, //or in certain cases ( such as KOR area setting on the wrong system menu, a full brick presenting as green & purple garbage instead of the "press A" screen ) if( sysMenuResource.isEmpty() ) { qCritical() << "Error getting the resource file for the system menu.\nCan\'t check it against setting.txt"; } else { U8 u8( sysMenuResource ); QStringList entries = u8.Entries(); if( !u8.IsOK() || !entries.size() ) { qCritical() << "Error parsing the resource file for the system menu.\nCan\'t check it against setting.txt"; } else { QString sysMenuPath; //these are all the possibilities i saw for AREA in libogc if( area == "AUS" || area == "EUR" ) //supported by 4.3e sysMenuPath = "html/EU2/iplsetting.ash/EU/EU/ENG/index01.html"; else if( area == "USA" || area == "BRA" || area == "HKG" || area == "ASI" || area == "LTN" || area == "SAF" ) //supported by 4.3u sysMenuPath = "html/US2/iplsetting.ash/FIX/US/ENG/index01.html"; else if( area == "JPN" || area == "TWN" || area == "ROC" ) //supported by 4.3j sysMenuPath = "html/JP2/iplsetting.ash/JP/JP/JPN/index01.html"; else if( area == "KOR" ) //supported by 4.3k sysMenuPath = "html/KR2/iplsetting.ash/KR/KR/KOR/index01.html"; else qDebug() << "unknown AREA setting"; if( !entries.contains( sysMenuPath ) ) { qCritical() << sysMenuPath << "Was not found in the system menu resources, and is needed by the AREA setting" << area; Fail( "This will likely result in a full/opera brick" ); } else { if( verbose ) qDebug() << "system menu resource matches setting.txt AREA setting."; } } } if( verbose ) { shownSetting = true; hexdump( settingTxt ); qDebug() << qPrintable( str ); } return; error: qCritical() << "Something is wrong with this setting.txt"; if( !shownSetting ) { hexdump( settingTxt ); qDebug() << qPrintable( str ); } } int main( int argc, char *argv[] ) { QCoreApplication a( argc, argv ); #ifdef Q_WS_WIN origColor = GetColor(); hConsole = GetStdHandle( STD_OUTPUT_HANDLE ); #endif qInstallMsgHandler( DebugHandler ); args = QCoreApplication::arguments(); if( args.contains( "-nocolor", Qt::CaseInsensitive ) ) color = false; qCritical() << "** nandBinCheck : Wii nand info tool **"; qCritical() << " from giantpune"; qCritical() << " svn r:" << qPrintable( CleanSvnStr( SVN_REV_STR ) ); qCritical() << " built:" << __DATE__ << __TIME__; if( args.contains( "-about", Qt::CaseInsensitive ) ) About(); if( args.size() < 2 ) Usage(); if( !QFile( args.at( 1 ) ).exists() ) Usage(); if( !nand.SetPath( args.at( 1 ) ) || !nand.InitNand() ) Fail( "Error setting path to nand object" ); if( args.size() < 3 ) { args << "-all" << "-v" << "-v"; } root = NULL; verbose = args.count( "-v" ); if( args.contains( "-continue", Qt::CaseInsensitive ) ) tryToKeepGoing = true; if( args.contains( "-rsa", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) calcRsa = true; //these only serve to show info. no action is taken if( args.contains( "-boot", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) { qDebug() << "checking boot1 & 2..."; ShowBootInfo( nand.Boot1Version(), nand.Boot2Infos() ); } if( args.contains( "-fs", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) { qDebug() << "checking uid.sys..."; QByteArray ba = nand.GetData( "/sys/uid.sys" ); if( ba.isEmpty() ) Fail( "No uid map found in the nand" ); uidM = UIDmap( ba ); uidM.Check(); //dont really take action, the check should spit out info if there is an error. not all errors are fatal qDebug() << "checking content.map..."; CheckShared(); //check for the presence of the content.map as well as verify all the hashes SetUpTree(); tids = InstalledTitles(); qDebug() << "found" << tids.size() << "titles installed"; BuildGoodIosList(); qDebug() << "found" << validIoses.size() << "bootable IOS"; foreach( quint64 tid, tids ) { CheckTitleIntegrity( tid ); //if( !CheckTitleIntegrity( tid ) && tid == 0x100000002ull ) //well, this SHOULD be the case. but nintendo doesnt care so much about //Fail( "The System menu isnt valid" ); //checking signatures & hashes as the rest of us. } Check003(); if( args.contains( "-settingtxt", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) { CheckSettingTxt(); } if( args.contains( "-uid", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) { ListDeletedTitles(); } } if( args.contains( "-clInfo", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) { qDebug() << "checking for lost clusters..."; CheckLostClusters(); } if( args.contains( "-spare", Qt::CaseInsensitive ) || args.contains( "-all", Qt::CaseInsensitive ) ) { qDebug() << "verifying ecc..."; CheckEcc(); SetUpTree(); qDebug() << "verifying hmac..."; CheckHmac(); } return 0; }