From 7e64a9f6b5acf07eab91282589ce5bf2a84cd0ab Mon Sep 17 00:00:00 2001 From: "giantpune@gmail.com" Date: Sat, 18 Dec 2010 21:07:58 +0000 Subject: [PATCH] * fixed bugs in nandBin dealing with getting the supercluster * adding stuff for writing a supercluster with updates information, using correct rotation * fixed a few more bugs in this class forgot about * move the "fakesign checker" stuff to a more appropriate place * add a couple small functions to TMD, ticket, shardeMap * add a new "demo" program/project nandBinCheck. it does lots of useful integrety checks on a nand.bin that will be needed when starting to actually write these things --- .gitattributes | 3 + WiiQt/blocks0to7.cpp | 257 +------------ WiiQt/includes.h | 2 +- WiiQt/nandbin.cpp | 426 ++++++++++++++++++++-- WiiQt/nandbin.h | 15 + WiiQt/sharedcontentmap.cpp | 20 +- WiiQt/sharedcontentmap.h | 4 + WiiQt/tiktmd.cpp | 262 ++++++++++++++ WiiQt/tiktmd.h | 25 +- nandBinCheck/main.cpp | 662 ++++++++++++++++++++++++++++++++++ nandBinCheck/nandBinCheck.pro | 35 ++ nandBinCheck/readmii.txt | 8 + 12 files changed, 1436 insertions(+), 283 deletions(-) create mode 100644 nandBinCheck/main.cpp create mode 100644 nandBinCheck/nandBinCheck.pro create mode 100644 nandBinCheck/readmii.txt diff --git a/.gitattributes b/.gitattributes index fdeec27..a01cf60 100644 --- a/.gitattributes +++ b/.gitattributes @@ -37,6 +37,9 @@ WiiQt/uidmap.cpp -text WiiQt/uidmap.h -text WiiQt/wad.cpp -text WiiQt/wad.h -text +nandBinCheck/main.cpp -text +nandBinCheck/nandBinCheck.pro -text +nandBinCheck/readmii.txt -text nandExtract/black.png -text nandExtract/blue.png -text nandExtract/boot2infodialog.cpp -text diff --git a/WiiQt/blocks0to7.cpp b/WiiQt/blocks0to7.cpp index 4736a64..46236b2 100644 --- a/WiiQt/blocks0to7.cpp +++ b/WiiQt/blocks0to7.cpp @@ -2,21 +2,6 @@ #include "tools.h" #include "tiktmd.h" -int check_cert_chain( const QByteArray data ); - -enum -{ - ERROR_SUCCESS = 0, - ERROR_SIG_TYPE, - ERROR_SUB_TYPE, - ERROR_RSA_FAKESIGNED, - ERROR_RSA_HASH, - ERROR_RSA_TYPE_UNKNOWN, - ERROR_RSA_TYPE_MISMATCH, - ERROR_CERT_NOT_FOUND, - ERROR_COUNT -}; - Blocks0to7::Blocks0to7( QListblocks ) { _ok = false; @@ -26,6 +11,7 @@ Blocks0to7::Blocks0to7( QListblocks ) bool Blocks0to7::SetBlocks( QListblocks ) { + //qDebug() << "Blocks0to7::SetBlocks" << blocks.size(); _ok = false; this->blocks.clear(); boot2Infos.clear(); @@ -48,7 +34,10 @@ bool Blocks0to7::SetBlocks( QListblocks ) quint8 Blocks0to7::Boot1Version() { if( blocks.size() != 8 ) + { + qWarning() << "Blocks0to7::Boot1Version -> not enough blocks" << blocks.size(); return BOOT_1_UNK; + } QByteArray hash = GetSha1( blocks.at( 0 ) ); if( hash == QByteArray( "\x4a\x7c\x6f\x30\x38\xde\xea\x7a\x07\xd3\x32\x32\x02\x4b\xe9\x5a\xfb\x56\xbf\x65" ) ) @@ -60,6 +49,8 @@ quint8 Blocks0to7::Boot1Version() if( hash == QByteArray( "\x8d\x9e\xcf\x2f\x8f\x98\xa3\xc1\x07\xf1\xe5\xe3\x6f\xf2\x4d\x57\x7e\xac\x36\x08" ) ) return BOOT_1_D; //displayed as "boot1?" in ceilingcat + qWarning() << "Blocks0to7::Boot1Version -> unknown boot1 hash:" << hash.toHex(); + return BOOT_1_UNK; } @@ -324,239 +315,3 @@ Boot2Info Blocks0to7::CheckHashes( Boot2Info info ) return ret; } -//theres probably a better place to put all this stuff. but until then, just put it here -#define BE32(a) (((a)[0] << 24)|((a)[1] << 16)|((a)[2] << 8)|(a)[3]) - -#define bn_zero(a,b) memset(a,0,b) -#define bn_copy(a,b,c) memcpy(a,b,c) -#define bn_compare(a,b,c) memcmp(a,b,c) - -// calc a = a mod N, given n = size of a,N in bytes -static void bn_sub_modulus( quint8 *a, const quint8 *N, const quint32 n ) -{ - quint32 dig; - quint8 c = 0; - - for( quint32 i = n - 1; i < n; i-- ) - { - dig = N[ i ] + c; - c = ( a [ i ] < dig ); - a[ i ] -= dig; - } -} - -// calc d = (a + b) mod N, given n = size of d,a,b,N in bytes -static void bn_add( quint8 *d, const quint8 *a, const quint8 *b, const quint8 *N, const quint32 n ) -{ - quint32 i; - quint32 dig; - quint8 c = 0; - - for( i = n - 1; i < n; i--) - { - dig = a[ i ] + b[ i ] + c; - c = ( dig >= 0x100 ); - d[ i ] = dig; - } - if( c ) - bn_sub_modulus( d, N, n ); - - if( bn_compare( d, N, n ) >= 0 ) - bn_sub_modulus( d, N, n ); -} - -// calc d = (a * b) mod N, given n = size of d,a,b,N in bytes -static void bn_mul( quint8 *d, const quint8 *a, const quint8 *b, const quint8 *N, const quint32 n ) -{ - quint8 mask; - bn_zero( d, n ); - - for( quint32 i = 0; i < n; i++ ) - for( mask = 0x80; mask != 0; mask >>= 1 ) - { - bn_add( d, d, d, N, n ); - if( ( a[ i ] & mask ) != 0 ) - bn_add( d, d, b, N, n ); - } -} - -// calc d = (a ^ e) mod N, given n = size of d,a,N and en = size of e in bytes -static void bn_exp( quint8 *d, const quint8 *a, const quint8 *N, const quint32 n, const quint8 *e, const quint32 en ) -{ - quint8 t[ 512 ]; - quint8 mask; - - bn_zero( d, n ); - d[ n-1 ] = 1; - for( quint32 i = 0; i < en; i++ ) - for( mask = 0x80; mask != 0; mask >>= 1 ) - { - bn_mul( t, d, d, N, n ); - if( ( e [ i ] & mask ) != 0 ) - bn_mul( d, t, a, N, n ); - else - bn_copy( d, t, n ); - } -} - -static int get_sig_len( const quint8 *sig ) -{ - quint32 type; - type = BE32( sig ); - switch( type - 0x10000 ) - { - case 0: - return 0x240; - case 1: - return 0x140; - case 2: - return 0x80; - } - return -ERROR_SIG_TYPE; -} - -static int get_sub_len( const quint8 *sub ) -{ - quint32 type; - type = BE32( sub + 0x40 ); - switch( type ) - { - case 0: return 0x2c0; - case 1: return 0x1c0; - case 2: return 0x100; - } - return -ERROR_SUB_TYPE; -} - -static int check_rsa( QByteArray h, const quint8 *sig, const quint8 *key, const quint32 n ) -{ - quint8 correct[ 0x200 ], x[ 0x200 ]; - static const quint8 ber[ 16 ] = { 0x00,0x30,0x21,0x30,0x09,0x06,0x05,0x2b, - 0x0e,0x03,0x02,0x1a,0x05,0x00,0x04,0x14 }; - correct[ 0 ] = 0; - correct[ 1 ] = 1; - memset( correct + 2, 0xff, n - 38 ); - memcpy( correct + n - 36, ber, 16 ); - memcpy( correct + n - 20, h.constData(), 20 ); - - bn_exp( x, sig, key, n, key + n, 4 ); - - //qDebug() << "Decrypted signature hash:" << QByteArray( (const char*)&x[ n-20 ], 20 ).toHex(); - //qDebug() << " SHA1 hash:" << h.toHex(); - - if( memcmp( correct, x, n ) == 0 ) - return 0; - - if( strncmp( (char*)h.constData(), (char*) x + n - 20, 20 ) == 0 ) - return ERROR_RSA_FAKESIGNED; - - return ERROR_RSA_HASH; -} - -static int check_hash( QByteArray h, const quint8 *sig, const quint8 *key ) -{ - quint32 type; - type = BE32( sig ) - 0x10000; - if( (qint32)type != BE32( key + 0x40 ) ) - return ERROR_RSA_TYPE_MISMATCH; - - if( type == 1 ) - return check_rsa( h, sig + 4, key + 0x88, 0x100 ); - return ERROR_RSA_TYPE_UNKNOWN; -} - -static const quint8* find_cert_in_chain( const quint8 *sub, const quint8 *cert, const quint32 cert_len, int *err ) -{ - char parent[ 64 ], *child; - int sig_len, sub_len; - const quint8 *p, *issuer; - - strncpy( parent, (char*)sub, sizeof parent ); - parent[ sizeof parent - 1 ] = 0; - child = strrchr( parent, '-' ); - if( child ) - *child++ = 0; - else - { - *parent = 0; - child = (char*)sub; - } - - *err = -ERROR_CERT_NOT_FOUND; - - for( p = cert; p < cert + cert_len; p += sig_len + sub_len ) - { - sig_len = get_sig_len( p ); - if( sig_len < 0 ) - { - *err = sig_len; - break; - } - issuer = p + sig_len; - sub_len = get_sub_len( issuer ); - if( sub_len < 0 ) - { - *err = sub_len; - break; - } - if( strcmp( parent, (char*)issuer ) == 0 && strcmp( child, (char*)issuer + 0x44 ) == 0 ) - return p; - } - return NULL; -} - -int check_cert_chain( const QByteArray data ) -{ - int cert_err; - const quint8* key; - const quint8 *sig, *sub, *key_cert; - int sig_len, sub_len; - QByteArray h; - int ret; - const quint8 *certificates = certs_dat; - - sig = (const quint8*)data.constData(); - sig_len = get_sig_len( sig ); - if( sig_len < 0 ) - return -sig_len; - - sub = (const quint8*)( data.data() + sig_len ); - sub_len = data.size() - sig_len; - if( sub_len <= 0 ) - return ERROR_SUB_TYPE; - - for( ; ; ) - { - //qDebug() << "Verifying using" << QString( (const char*) sub ); - if( strcmp((char*)sub, "Root" ) == 0 ) - { - key = root_dat; - h = GetSha1( QByteArray( (const char*)sub, sub_len ) ); - if( BE32( sig ) != 0x10000 ) - return ERROR_SIG_TYPE; - return check_rsa( h, sig + 4, key, 0x200 ); - } - - key_cert = find_cert_in_chain( sub, certificates, CERTS_DAT_SIZE, &cert_err ); - if( key_cert ) - cert_err = get_sig_len( key_cert ); - - if( cert_err < 0 ) - return -cert_err; - key = key_cert + cert_err; - - h = GetSha1( QByteArray( (const char*)sub, sub_len ) ); - ret = check_hash( h, sig, key ); - // remove this if statement if you don't want to check the whole chain - if( ret != ERROR_SUCCESS ) - return ret; - sig = key_cert; - sig_len = get_sig_len( sig ); - if( sig_len < 0 ) - return -sig_len; - sub = sig + sig_len; - sub_len = get_sub_len( sub ); - if( sub_len < 0 ) - return -sub_len; - } -} diff --git a/WiiQt/includes.h b/WiiQt/includes.h index 96f7a8b..29b611d 100644 --- a/WiiQt/includes.h +++ b/WiiQt/includes.h @@ -1,7 +1,7 @@ #ifndef INCLUDES_H #define INCLUDES_H - +#include #include #include #include diff --git a/WiiQt/nandbin.cpp b/WiiQt/nandbin.cpp index 2e98e18..6835a17 100755 --- a/WiiQt/nandbin.cpp +++ b/WiiQt/nandbin.cpp @@ -1,6 +1,8 @@ #include "nandbin.h" #include "tools.h" +//#define NAND_BIN_CAN_WRITE + NandBin::NandBin( QObject * parent, const QString &path ) : QObject( parent ) { type = -1; @@ -30,7 +32,12 @@ bool NandBin::SetPath( const QString &path ) f.close(); f.setFileName( path ); - bool ret = !f.exists() || !f.open( QIODevice::ReadOnly ); + bool ret = !f.exists() || +#ifdef NAND_BIN_CAN_WRITE + !f.open( QIODevice::ReadWrite ); +#else + !f.open( QIODevice::ReadOnly ); +#endif if( ret ) { emit SendError( tr( "Cant open %1" ).arg( path ) ); @@ -224,6 +231,7 @@ bool NandBin::ExtractFile( fst_t fst, QString parent ) bool NandBin::InitNand( QIcon dirs, QIcon files ) { fstInited = false; + memset( (void*)&fsts, 0, sizeof( fst_t ) * 0x17ff ); fats.clear(); type = GetDumpType( f.size() ); if( type < 0 || type > 3 ) @@ -324,18 +332,41 @@ bool NandBin::InitNand( QIcon dirs, QIcon files ) }*/ //GetFile( 369 ); + /*QByteArray scl; + for( quint16 i = 0; i < 16; i++ ) + { + scl += GetCluster( currentSuperCluster + i, false ); + } + WriteFile( "./aaaa_superCluster.bin", scl ); + qDebug() << "starting scl" << hex << currentSuperCluster;*/ + //testing creating new items /*quint16 n = CreateEntry( "/test1", 0x4444, 0x5555, 2, 1, 2, 0 ); qDebug() << "created item" << n; - n = CreateEntry( "/test1/test2", 0x4444, 0x5555, 1, 1, 2, 3 ); + n = CreateEntry( "/test1/test4", 0x4444, 0x5555, 1, 1, 2, 3 ); qDebug() << "created item" << n; if( n ) { - bool dd = SetData( n, QByteArray( 0x3000000, '\x69') ); + bool dd = SetData( n, QByteArray( 0x4000000, '\x69') ); qDebug() << "added data" << dd; + + dd = WriteMetaData(); + qDebug() << "write meta" << dd; }*/ + /*scl.clear(); + for( quint16 i = 0; i < 16; i++ ) + { + scl += GetCluster( currentSuperCluster + i, false ); + } + qDebug() << "tests" << scl.count( "test" ) << "in scl" << hex << currentSuperCluster; + WriteFile( "./aaaa_superCluster2.bin", scl );*/ + + //QByteArray fu = GetData( "/test1/test2" ); + //if( !fu.isEmpty() ) + //hexdump( fu, 0, 0x30 ); + /*ShowLostClusters(); bool x= Delete( "/ticket" ); qDebug() << "delete" << x; @@ -345,6 +376,9 @@ bool NandBin::InitNand( QIcon dirs, QIcon files ) //root = new QTreeWidgetItem( QStringList() << nandPath ); //AddChildren( root, 0 ); + //bool l = TestMetaData(); + //qDebug() << "test meta" << l; + if( !bootBlocks.SetBlocks( blocks ) ) return false; @@ -382,7 +416,8 @@ void NandBin::ShowLostClusters() break; } } - qDebug() << "found" << lost << "lost clusters\n0xffffs" << hex << ffs.size() << ffs << "\nfree" << frs.size(); + qDebug() << "found" << lost << "lost clusters\nUNK ( 0xffff )" << hex << ffs.size() << ffs << + "\nfree " << frs.size(); } int NandBin::GetDumpType( quint64 fileSize ) @@ -507,30 +542,46 @@ qint32 NandBin::FindSuperblock() emit SendError( tr( "Tried to get superblock of unopened dump" ) ); return -1; } - quint32 loc = 0, current = 0, last = 0; + quint32 loc = 0, current = 0, magic = 0; + superClusterVersion = 0; quint32 n_start[] = { 0x1FC00000, 0x20BE0000, 0x20BE0000 }, n_end[] = { 0x20000000, 0x21000000, 0x21000000 }, n_len[] = { 0x40000, 0x42000, 0x42000 }; - - for( loc = n_start[ type ]; loc < n_end[ type ]; loc += n_len[ type ] ) + quint8 rewind = 1; + for( loc = n_start[ type ], currentSuperCluster = 0x7f00; loc < n_end[ type ]; loc += n_len[ type ], currentSuperCluster += 0x10 ) { + f.seek( loc ); + //QByteArray sss = f.peek( 0x50 ); + f.read( (char*)&magic, 4 );//no need to switch endian here + if( magic != 0x53464653 ) + { + qWarning() << "oops, this isnt a supercluster" << hex << loc << magic << currentSuperCluster; + rewind++; + //hexdump( sss ); + continue; + } + f.read( (char*)¤t, 4 ); current = qFromBigEndian( current ); - //qDebug() << "superblock" << hex << current; + //qDebug() << "superblock" << hex << current << currentSuperCluster << loc; - if( current > last ) - last = current; + if( current > superClusterVersion ) + superClusterVersion = current; else { - //qDebug() << "superblock loc" << hex << loc - n_len[ type ]; - return loc - n_len[ type ]; + //qDebug() << "using superblock" << hex << superClusterVersion << currentSuperCluster - 0x10 << f.pos() - n_len[ type ]; + currentSuperCluster -= ( 0x10 * rewind ); + loc -= ( n_len[ type ] * rewind ); + break; } - - f.seek( n_len[ type ] - 4 ); } - return -1;//hmmmm what happens if the last supercluster is the latest one? seems like a bug to fix at a later date... + if( !superClusterVersion ) + return -1; + + //qDebug() << "using superblock" << hex << superClusterVersion << currentSuperCluster << "page:" << ( loc / 0x840 ); + return loc; } fst_t NandBin::GetFST( quint16 entry ) @@ -627,6 +678,7 @@ QByteArray NandBin::GetPage( quint32 pageNo, bool withEcc ) return QByteArray(); } f.seek( pageNo * n_pagelen[ type ] ); //seek to the beginning of the page to read + //qDebug() << "reading page from" << hex << (quint32)f.pos(); QByteArray page = f.read( ( type && withEcc ) ? n_pagelen[ type ] : 0x800 ); return page; } @@ -647,7 +699,8 @@ QByteArray NandBin::GetCluster( quint16 cluster_entry, bool decrypt ) for( int i = 0; i < 8; i++ ) { - f.seek( ( cluster_entry * n_clusterlen[ type ] ) + ( i * n_pagelen[ type ] ) ); //seek to the beginning of the page to read + f.seek( ( cluster_entry * n_clusterlen[ type ] ) + ( i * n_pagelen[ type ] ) ); //seek to the beginning of the page to read + //qDebug() << "reading page from" << hex << (quint32)f.pos(); //QByteArray page = f.read( n_pagelen[ type ] ); //read the page, with ecc QByteArray page = f.read( 0x800 ); //read the page, skip the ecc //hexdump( page.mid( 0x800, 0x40 ) );//just here for debugging purposes @@ -682,7 +735,7 @@ QByteArray NandBin::GetFile( quint16 entry ) QByteArray NandBin::GetFile( fst_t fst_ ) { - qDebug() << "NandBin::GetFile" << (const char*)fst_.filename; + //qDebug() << "NandBin::GetFile" << QByteArray( (const char*)fst_.filename, 12 ); if( !fst_.size ) return QByteArray(); @@ -691,7 +744,8 @@ QByteArray NandBin::GetFile( fst_t fst_ ) QByteArray data; - //int idx = 0; + //QList readFats; + //int idx = 0;a for (int i = 0; fat < 0xFFF0; i++) { QByteArray cluster = GetCluster( fat ); @@ -702,8 +756,11 @@ QByteArray NandBin::GetFile( fst_t fst_ ) //WriteDecryptedCluster( 0, cluster, fst_, idx++ ); data += cluster; + //readFats << fat; fat = GetFAT( fat ); } + //qDebug() << "actually read data from fats\n" << hex << readFats; + //qDebug() << "stopped reading because of" << hex << fat; //this check doesnt really seem to matter, it always appears to be 1 extra cluster added to the end //of the file and that extra bit is dropped in this function before the data is returned. @@ -967,11 +1024,11 @@ bool NandBin::WriteCluster( quint32 pageNo, const QByteArray data, const QByteAr bool NandBin::WriteDecryptedCluster( quint32 pageNo, const QByteArray data, fst_t fst, quint16 idx ) { - qDebug() << "NandBin::WriteDecryptedCluster"; + //qDebug() << "NandBin::WriteDecryptedCluster"; QByteArray hmac = spare.Get_hmac_data( data, fst.uid, (const unsigned char *)&fst.filename, fst.fst_pos, fst.x3, idx ); - hexdump( hmac ); - return true; + //hexdump( hmac ); + //return true; AesSetKey( key ); QByteArray encData = AesEncrypt( 0, data ); @@ -980,8 +1037,13 @@ bool NandBin::WriteDecryptedCluster( quint32 pageNo, const QByteArray data, fst_ bool NandBin::WritePage( quint32 pageNo, const QByteArray data ) { + //return true; +#ifndef NAND_BIN_CAN_WRITE + qWarning() << __FILE__ << "was built without write support"; + return false; +#endif //qDebug() << "NandBin::WritePage(" << hex << pageNo << ")"; - return true; + //return true; quint32 n_pagelen[] = { 0x800, 0x840, 0x840 }; if( (quint32)data.size() != n_pagelen[ type ] ) { @@ -994,7 +1056,9 @@ bool NandBin::WritePage( quint32 pageNo, const QByteArray data ) emit SendError( tr( "Tried to write page past size of nand.bin" ) ); return false; } - f.seek( pageNo * n_pagelen[ type ] ); //seek to the beginning of the page to write + f.seek( (quint64)pageNo * (quint64)n_pagelen[ type ] ); //seek to the beginning of the page to write + //qDebug() << "writing page at:" << f.pos() << hex << (quint32)f.pos(); + //hexdump( data, 0, 0x20 ); return f.write( data ); } @@ -1002,7 +1066,7 @@ quint16 NandBin::CreateNode( const QString &name, quint32 uid, quint16 gid, quin { attr = attr | ( ( user_perm & 3 ) << 6 ) | ( ( group_perm & 3 ) << 4 ) | ( ( other_perm & 3 ) << 2 ); - quint32 i;//TODO: maybe add in some sort of wear-leveling emulation so all new entries arent created in sequential order + quint32 i; for( i = 1; i < 0x17ff; i++ )//cant be entry 0 because that is the root { fst_t fst = fsts[ i ]; @@ -1013,8 +1077,10 @@ quint16 NandBin::CreateNode( const QString &name, quint32 uid, quint16 gid, quin { return 0; } + QByteArray n = name.toLatin1().data(); n.resize( 12 ); + //qDebug() << "will add entry for" << n << "at" << hex << i; memcpy( &fsts[ i ].filename, n, 12 ); fsts[ i ].attr = attr; fsts[ i ].wtf = 0; @@ -1031,6 +1097,7 @@ quint16 NandBin::CreateNode( const QString &name, quint32 uid, quint16 gid, quin fsts[ i ].uid = uid; fsts[ i ].gid = gid; fsts[ i ].x3 = 0; + //hexdump( (const void*)&fsts[ i ], sizeof( fst_t ) ); return i; } @@ -1284,6 +1351,11 @@ bool NandBin::SetData( quint16 idx, const QByteArray data ) //grab a random cluster from the list quint16 idx = qrand() % freeClusters.size(); quint16 cl = freeClusters.takeAt( idx ); //remove this number from the list + /*if( freeClusters.contains( cl ) ) + { + qDebug() << "wtf4"; + return false; + }*/ fts << cl; //add this one to the clusters that will be used to hold the data quint16 block = cl / 8; //try to find other clusters in the same block for( quint16 i = block * 8; i < ( ( block + 1 ) * 8 ) && fts.size() < clCnt; i++ ) @@ -1294,7 +1366,12 @@ bool NandBin::SetData( quint16 idx, const QByteArray data ) if( fats.at( i ) == 0xfffe ) //theres more free clusters in this same block, grab them { fts << i; - freeClusters.removeAt( freeClusters.indexOf( i, MAX( cl - 8, 0 ) ) ); + freeClusters.removeAt( freeClusters.indexOf( i, 0 ) ); + /*if( freeClusters.contains( i ) ) + { + qDebug() << "wtf5"; + return false; + }*/ } } //read the spare data to see that the cluster is good - removed for now. but its probably not a bad idea to do this @@ -1315,7 +1392,7 @@ bool NandBin::SetData( quint16 idx, const QByteArray data ) } } //qDebug() << "about to writing shit" << clCnt << fts.size(); - //qDebug() << "file will be on clusters" << hex << fts; + //qDebug() << "file will be on clusters\n" << hex << fts; for( quint32 i = 0; i < clCnt; i++ ) { QByteArray cluster = pData.mid( i * 0x4000, 0x4000 ); @@ -1326,17 +1403,52 @@ bool NandBin::SetData( quint16 idx, const QByteArray data ) //all the data has been written, now make sure the fats are correct fsts[ idx ].sub = fts.at( 0 ); + /*QListbugFix = fts; + for( quint16 i = 0; i < fts.size(); i++ ) + { + if( bugFix.at( i ) != fts.at( i ) ) + { + qDebug() << "wwwwtttf?" << i << hex << bugFix.at( i ) << fts.at( i ); + return false; + } + } + quint16 te = fsts[ idx ].sub;*/ + for( quint16 i = 0; i < clCnt - 1; i++ ) { - //qDebug() << "replacing fat" << hex << fts.at( 0 ); fats.replace( fts.at( 0 ), fts.at( 1 ) ); + /*qDebug() << "replacing fat" << hex << fts.at( 0 ) << "to point to" << fts.at( 1 ) << "actual:" << fats.at( fts.at( 0 ) ); + if( te != fts.at( 0 ) || te != bugFix.at( i ) ) + { + qDebug() << "failed" << i << hex << te << fts.at( 0 ) << bugFix.at( i ); + return false; + }*/ fts.takeFirst(); + //te = GetFAT( te ); } + //follow the fat chain and make sure it is as expected + /*quint16 num = 0; + te = fsts[ idx ].sub; + while( te < 0xfff0 ) + { + if( te != bugFix.at( num ) ) + { + qDebug() << "mismatch" << num << hex << te << bugFix.at( num ); + break; + } + te = GetFAT( te ); + num++; + }*/ + + //qDebug() << "1 followed the chain to" << num << "items. expected" << clCnt; //qDebug() << "loop is done"; fats.replace( fts.at( 0 ), 0xfffb );//last cluster in chain fts.takeFirst(); //qDebug() << "fixed the last one" << hex << fts; // if the new data uses less clusters than the previous data, mark the extra ones as free + //if( !fts.isEmpty() ) + //qDebug() << "need to mark" << fts.size() << "clusters free"; + while( !fts.isEmpty() ) { fats.replace( fts.at( 0 ), 0xfffe ); @@ -1344,6 +1456,22 @@ bool NandBin::SetData( quint16 idx, const QByteArray data ) } //qDebug() << "2nd loop is done"; + //follow the fat chain and make sure it is as expected + /*num = 0; + te = fsts[ idx ].sub; + while( te < 0xfff0 ) + { + if( te != bugFix.at( num ) ) + { + qDebug() << "mismatch" << num << hex << te << bugFix.at( num ); + break; + } + te = GetFAT( te ); + num++; + } + + qDebug() << "2 followed the chain to" << num << "items. expected" << clCnt;*/ + fsts[ idx ].size = data.size(); QTreeWidgetItem *i = ItemFromEntry( idx, root ); @@ -1353,3 +1481,247 @@ bool NandBin::SetData( quint16 idx, const QByteArray data ) i->setText( 2, QString( "%1" ).arg( data.size(), 0, 16 ) ); return true; } + +bool NandBin::WriteMetaData() +{ + //make sure the currect cluster is sane + if( currentSuperCluster < 0x7f00 || currentSuperCluster > 0x7ff0 || currentSuperCluster % 16 || fats.size() != 0x8000 ) + return false; + quint16 nextSuperCluster = currentSuperCluster + 0x10; + if( nextSuperCluster > 0x7ff0 ) + nextSuperCluster = 0x7f00; + + quint32 nextClusterVersion = superClusterVersion + 1; + QByteArray scl( 0x4000 * 16, '\0' ); //this will hold all the data + //qDebug() << "created the meta block buffer" << hex << scl.size(); + QBuffer b( &scl ); + b.open( QIODevice::WriteOnly ); + quint32 tmp; + quint16 t; + + b.write( "SFFS" ); //magic word + tmp = qFromBigEndian( nextClusterVersion ); + b.write( (const char*)&tmp, 4 ); //version + tmp = qFromBigEndian( (quint32)0x10 ); + b.write( (const char*)&tmp, 4 ); //wiibrew says its always 0x10. but mine is 0 + qDebug() << "writing the fats at" << hex << (quint32)b.pos(); + + //write all the fats + for( quint16 i = 0; i < 0x8000; i++ ) + { + t = qFromBigEndian( fats.at( i ) ); + b.write( (const char*)&t, 2 ); + } + + qDebug() << "writing the fsts at" << hex << (quint32)b.pos(); + //write all the fst entries + for( quint16 i = 0; i < 0x17ff; i++ ) + { + fst_t fst = fsts[ i ]; + + b.write( (const char*)&fst.filename, 0xc ); + b.write( (const char*)&fst.attr, 1 ); + b.write( (const char*)&fst.wtf, 1 ); + + t = qFromBigEndian( fst.sub ); + b.write( (const char*)&t, 2 ); + + t = qFromBigEndian( fst.sib ); + b.write( (const char*)&t, 2 ); + + tmp = qFromBigEndian( fst.size ); + b.write( (const char*)&tmp, 4 ); + + tmp = qFromBigEndian( fst.uid ); + b.write( (const char*)&tmp, 4 ); + + t = qFromBigEndian( fst.gid ); + b.write( (const char*)&t, 2 ); + + tmp = qFromBigEndian( fst.x3 ); + b.write( (const char*)&tmp, 4 ); + } + + + //qDebug() << "done adding shit" << hex << (quint32)b.pos(); + b.close(); + //qDebug() << "tests" << scl.count( "test" ); + //get hmac data + QByteArray hmR = spare.Get_hmac_meta( scl, nextSuperCluster ); + qDebug() << "about to write the meta block" << hex << nextSuperCluster << nextClusterVersion << "to page" << (quint32)( nextSuperCluster * 8 ); + + for( quint8 i = 0; i < 0x10; i++ ) + { + bool ret = WriteCluster( (quint32)( ( nextSuperCluster + i ) * 8 ), scl.mid( 0x4000 * i, 0x4000 ), ( i == 15 ? hmR : QByteArray() ) ); + if( !ret ) + { + qWarning() << "failed to write the metadata. this nand may be broken now :(" << i; + return false; + } + } + currentSuperCluster = nextSuperCluster; + superClusterVersion = nextClusterVersion; //probably need to put some magic here in case the version wraps around back to 0 + + + //testing 1,2 + /*QByteArray tt; + for( quint16 i = 0; i < 16; i++ ) + { + tt += GetCluster( currentSuperCluster + i, false ); + } + if( tt != scl ) + qDebug() << "shit doesnt match :("; + else + qDebug() << "wrote what was expected :)";*/ + + return true; +} + +bool NandBin::CheckEcc( quint32 pageNo ) +{ + if( !type ) + return false; + + QByteArray whole = GetPage( pageNo, true ); + if( whole.size() != 0x840 ) + return false; + + QByteArray data = whole.left( 0x800 ); + QByteArray ecc = whole.right( 0x10 ); + QByteArray calc = spare.CalcEcc( data ); + return ( ecc == calc ); +} + +bool NandBin::CheckHmacData( quint16 entry ) +{ + if( entry > 0x17fe ) + { + qDebug() << "bad entry #" << hex << entry; + return false; + } + + fst_t fst = fsts[ entry ]; + if( ( fst.attr & 3 ) != 1 ) + { + qDebug() << "bad attributes" << ( fst.attr & 3 ); + return false; + } + + if( !fst.size ) + return true; + + quint16 clCnt = ( RU( 0x4000, fst.size ) / 0x4000 ); + //qDebug() << FstName( fst ) << "is" << hex << fst.size << "bytes (" << clCnt << ") clusters"; + + quint16 fat = fst.sub; + QByteArray sp1; + QByteArray sp2; + QByteArray hmac; + //qDebug() << "fat" << hex << fat; + for( quint32 i = 0; i < clCnt; i++ ) + { + if( fat > 0x7fff ) + { + qDebug() << "fat is out of range" << hex << fat; + return false; + } + QByteArray cluster = GetCluster( fat ); //hmac is calculated with the decrypted cluster data + if( cluster.size() != 0x4000 ) + { + qDebug() << "error reading cluster"; + return false; + } + sp1 = GetPage( ( fat * 8 ) + 6, true ); //the spare data of these 2 pages hold the hmac data for the cluster + sp2 = GetPage( ( fat * 8 ) + 7, true ); + if( sp1.isEmpty() || sp2.isEmpty() ) + { + qDebug() << "error getting spare data"; + return false; + } + + sp1 = sp1.right( 0x40 ); //only keep the spare data and drop the data + sp2 = sp2.right( 0x40 ); + + hmac = spare.Get_hmac_data( cluster, fst.uid, (const unsigned char*)&fst.filename, entry, fst.x3, i ); + + //this part is kinda ugly, but this is how it is layed out by big N + if( sp1.mid( 1, 0x14 ) != hmac ) + { + qDebug() << "hmac bad (1)"; + goto error; + } + if( sp1.mid( 0x15, 0xc ) != hmac.left( 0xc ) ) + { + qDebug() << "hmac bad (2)"; + goto error; + } + if( sp2.mid( 1, 8 ) != hmac.right( 8 ) ) + { + qDebug() << "hmac bad (3)"; + goto error; + } + //qDebug() << "hmac ok for cluster" << i; + //data += cluster; + fat = GetFAT( fat ); + } + return true; + +error: + qDebug() << FstName( fst ) << "is" << hex << fst.size << "bytes (" << clCnt << ") clusters"; + hexdump( sp1 ); + hexdump( sp2 ); + hexdump( hmac ); + return false; + +} + +bool NandBin::CheckHmacMeta( quint16 clNo ) +{ + if( clNo < 0x7f00 || clNo > 0x7ff0 || clNo % 0x10 ) + return false; + + QByteArray data; + for( quint8 i = 0; i < 0x10; i++ ) + { + data += GetCluster( ( clNo + i ), false ); + } + QByteArray hmac = spare.Get_hmac_meta( data, clNo ); + quint32 baseP = ( clNo + 15 ) * 8; + //qDebug() << "baseP" << hex << baseP << ( baseP + 6 ) << ( baseP + 7 ); + QByteArray sp1 = GetPage( baseP + 6, true ); //the spare data of these 2 pages hold the hmac data for the supercluster + QByteArray sp2 = GetPage( baseP + 7, true ); + if( sp1.isEmpty() || sp2.isEmpty() ) + { + qDebug() << "error getting spare data"; + return false; + } + + sp1 = sp1.right( 0x40 ); //only keep the spare data and drop the data + sp2 = sp2.right( 0x40 ); + + //this part is kinda ugly, but this is how it is layed out by big N + if( sp1.mid( 1, 0x14 ) != hmac ) + { + qDebug() << "hmac bad (1)"; + goto error; + } + if( sp1.mid( 0x15, 0xc ) != hmac.left( 0xc ) ) + { + qDebug() << "hmac bad (2)"; + goto error; + } + if( sp2.mid( 1, 8 ) != hmac.right( 8 ) ) + { + qDebug() << "hmac bad (3)"; + goto error; + } + return true; + +error: + qDebug() << "supercluster" << hex << clNo; + hexdump( sp1 ); + hexdump( sp2 ); + hexdump( hmac ); + return false; + +} diff --git a/WiiQt/nandbin.h b/WiiQt/nandbin.h index 20a9b35..0916fb7 100755 --- a/WiiQt/nandbin.h +++ b/WiiQt/nandbin.h @@ -116,12 +116,25 @@ public: //overloads the above function bool SetData( const QString &path, const QByteArray data ); + //write the current changes to the metadata( if you dont do this, then none of the other stuff youve done wont be saved ) + bool WriteMetaData(); + + //functions to verify spare data + bool CheckEcc( quint32 pageNo ); + bool CheckHmacData( quint16 entry ); + + //verify hmac stuff for a given supercluster + //expects 0x7f00 - 0x7ff0 + bool CheckHmacMeta( quint16 clNo ); + private: QByteArray key; qint32 loc_super; qint32 loc_fat; qint32 loc_fst; + quint16 currentSuperCluster; + quint32 superClusterVersion; QString extractPath; QString nandPath; QFile f; @@ -181,6 +194,8 @@ private: QTreeWidgetItem *ItemFromEntry( quint16 i, QTreeWidgetItem *parent = NULL ); QTreeWidgetItem *ItemFromEntry( const QString &i, QTreeWidgetItem *parent = NULL ); + + signals: //connect to these to receive messages from this object void SendError( QString ); diff --git a/WiiQt/sharedcontentmap.cpp b/WiiQt/sharedcontentmap.cpp index b6f3a4e..e2d85ea 100644 --- a/WiiQt/sharedcontentmap.cpp +++ b/WiiQt/sharedcontentmap.cpp @@ -117,6 +117,22 @@ QString SharedContentMap::GetNextEmptyCid() void SharedContentMap::AddEntry( const QString &app, const QByteArray &hash ) { data += app.toAscii() + hash; - //qDebug() << "SharedContentMap::AddEntry -> added entry, rechecking this beast"; - //Check(); +} + +const QByteArray SharedContentMap::Hash( quint16 i ) +{ + if( Count() < i ) + return QByteArray(); + return data.mid( ( i * 28 ) + 8, 20 ); +} + +const QString SharedContentMap::Cid( quint16 i ) +{ + if( Count() < i ) + return QByteArray(); + return QString( data.mid( ( i * 28 ), 8 ) ); +} +quint16 SharedContentMap::Count() +{ + return ( data.size() / 28 ); } diff --git a/WiiQt/sharedcontentmap.h b/WiiQt/sharedcontentmap.h index 4c825fc..b026338 100644 --- a/WiiQt/sharedcontentmap.h +++ b/WiiQt/sharedcontentmap.h @@ -28,6 +28,10 @@ public: //get the entire data ready for writing to a wii nand const QByteArray Data(){ return data; } + const QByteArray Hash( quint16 i ); + const QString Cid( quint16 i ); + quint16 Count(); + private: QByteArray data; }; diff --git a/WiiQt/tiktmd.cpp b/WiiQt/tiktmd.cpp index abdcbf3..189be1c 100644 --- a/WiiQt/tiktmd.cpp +++ b/WiiQt/tiktmd.cpp @@ -36,6 +36,29 @@ bool Tmd::SetTid( quint64 tid ) return true; } +quint64 Tmd::IOS() +{ + if( !p_tmd ) + return 0; + return qFromBigEndian( p_tmd->sys_version ); +} + +bool Tmd::SetIOS( quint64 ios ) +{ + if( !p_tmd ) + return false; + + p_tmd->sys_version = qFromBigEndian( ios ); + return true; +} + +quint16 Tmd::Gid() +{ + if( !p_tmd ) + return 0; + return qFromBigEndian( p_tmd->group_id ); +} + QString Tmd::Cid( quint16 i ) { if( !p_tmd || i > qFromBigEndian( p_tmd->num_contents ) ) @@ -276,3 +299,242 @@ bool Ticket::FakeSign() while( ++i ); return ret; } + + +//theres probably a better place to put all this stuff. but until then, just put it here +//and this BE() probably isnt too good for cross-platformyitutiveness +#define BE32(a) (((a)[0] << 24)|((a)[1] << 16)|((a)[2] << 8)|(a)[3]) + +#define bn_zero(a,b) memset(a,0,b) +#define bn_copy(a,b,c) memcpy(a,b,c) +#define bn_compare(a,b,c) memcmp(a,b,c) + +// calc a = a mod N, given n = size of a,N in bytes +static void bn_sub_modulus( quint8 *a, const quint8 *N, const quint32 n ) +{ + quint32 dig; + quint8 c = 0; + + for( quint32 i = n - 1; i < n; i-- ) + { + dig = N[ i ] + c; + c = ( a [ i ] < dig ); + a[ i ] -= dig; + } +} + +// calc d = (a + b) mod N, given n = size of d,a,b,N in bytes +static void bn_add( quint8 *d, const quint8 *a, const quint8 *b, const quint8 *N, const quint32 n ) +{ + quint32 i; + quint32 dig; + quint8 c = 0; + + for( i = n - 1; i < n; i--) + { + dig = a[ i ] + b[ i ] + c; + c = ( dig >= 0x100 ); + d[ i ] = dig; + } + if( c ) + bn_sub_modulus( d, N, n ); + + if( bn_compare( d, N, n ) >= 0 ) + bn_sub_modulus( d, N, n ); +} + +// calc d = (a * b) mod N, given n = size of d,a,b,N in bytes +static void bn_mul( quint8 *d, const quint8 *a, const quint8 *b, const quint8 *N, const quint32 n ) +{ + quint8 mask; + bn_zero( d, n ); + + for( quint32 i = 0; i < n; i++ ) + for( mask = 0x80; mask != 0; mask >>= 1 ) + { + bn_add( d, d, d, N, n ); + if( ( a[ i ] & mask ) != 0 ) + bn_add( d, d, b, N, n ); + } +} + +// calc d = (a ^ e) mod N, given n = size of d,a,N and en = size of e in bytes +static void bn_exp( quint8 *d, const quint8 *a, const quint8 *N, const quint32 n, const quint8 *e, const quint32 en ) +{ + quint8 t[ 512 ]; + quint8 mask; + + bn_zero( d, n ); + d[ n-1 ] = 1; + for( quint32 i = 0; i < en; i++ ) + for( mask = 0x80; mask != 0; mask >>= 1 ) + { + bn_mul( t, d, d, N, n ); + if( ( e [ i ] & mask ) != 0 ) + bn_mul( d, t, a, N, n ); + else + bn_copy( d, t, n ); + } +} + +static int get_sig_len( const quint8 *sig ) +{ + quint32 type; + type = BE32( sig ); + switch( type - 0x10000 ) + { + case 0: + return 0x240; + case 1: + return 0x140; + case 2: + return 0x80; + } + return -ERROR_SIG_TYPE; +} + +static int get_sub_len( const quint8 *sub ) +{ + quint32 type; + type = BE32( sub + 0x40 ); + switch( type ) + { + case 0: return 0x2c0; + case 1: return 0x1c0; + case 2: return 0x100; + } + return -ERROR_SUB_TYPE; +} + +static int check_rsa( QByteArray h, const quint8 *sig, const quint8 *key, const quint32 n ) +{ + quint8 correct[ 0x200 ], x[ 0x200 ]; + static const quint8 ber[ 16 ] = { 0x00,0x30,0x21,0x30,0x09,0x06,0x05,0x2b, + 0x0e,0x03,0x02,0x1a,0x05,0x00,0x04,0x14 }; + correct[ 0 ] = 0; + correct[ 1 ] = 1; + memset( correct + 2, 0xff, n - 38 ); + memcpy( correct + n - 36, ber, 16 ); + memcpy( correct + n - 20, h.constData(), 20 ); + + bn_exp( x, sig, key, n, key + n, 4 ); + + //qDebug() << "Decrypted signature hash:" << QByteArray( (const char*)&x[ n-20 ], 20 ).toHex(); + //qDebug() << " SHA1 hash:" << h.toHex(); + + if( memcmp( correct, x, n ) == 0 ) + return 0; + + if( strncmp( (char*)h.constData(), (char*) x + n - 20, 20 ) == 0 ) + return ERROR_RSA_FAKESIGNED; + + return ERROR_RSA_HASH; +} + +static int check_hash( QByteArray h, const quint8 *sig, const quint8 *key ) +{ + quint32 type; + type = BE32( sig ) - 0x10000; + if( (qint32)type != BE32( key + 0x40 ) ) + return ERROR_RSA_TYPE_MISMATCH; + + if( type == 1 ) + return check_rsa( h, sig + 4, key + 0x88, 0x100 ); + return ERROR_RSA_TYPE_UNKNOWN; +} + +static const quint8* find_cert_in_chain( const quint8 *sub, const quint8 *cert, const quint32 cert_len, int *err ) +{ + char parent[ 64 ], *child; + int sig_len, sub_len; + const quint8 *p, *issuer; + + strncpy( parent, (char*)sub, sizeof parent ); + parent[ sizeof parent - 1 ] = 0; + child = strrchr( parent, '-' ); + if( child ) + *child++ = 0; + else + { + *parent = 0; + child = (char*)sub; + } + + *err = -ERROR_CERT_NOT_FOUND; + + for( p = cert; p < cert + cert_len; p += sig_len + sub_len ) + { + sig_len = get_sig_len( p ); + if( sig_len < 0 ) + { + *err = sig_len; + break; + } + issuer = p + sig_len; + sub_len = get_sub_len( issuer ); + if( sub_len < 0 ) + { + *err = sub_len; + break; + } + if( strcmp( parent, (char*)issuer ) == 0 && strcmp( child, (char*)issuer + 0x44 ) == 0 ) + return p; + } + return NULL; +} + +int check_cert_chain( const QByteArray data ) +{ + int cert_err; + const quint8* key; + const quint8 *sig, *sub, *key_cert; + int sig_len, sub_len; + QByteArray h; + int ret; + const quint8 *certificates = certs_dat; + + sig = (const quint8*)data.constData(); + sig_len = get_sig_len( sig ); + if( sig_len < 0 ) + return -sig_len; + + sub = (const quint8*)( data.data() + sig_len ); + sub_len = data.size() - sig_len; + if( sub_len <= 0 ) + return ERROR_SUB_TYPE; + + for( ; ; ) + { + //qDebug() << "Verifying using" << QString( (const char*) sub ); + if( strcmp((char*)sub, "Root" ) == 0 ) + { + key = root_dat; + h = GetSha1( QByteArray( (const char*)sub, sub_len ) ); + if( BE32( sig ) != 0x10000 ) + return ERROR_SIG_TYPE; + return check_rsa( h, sig + 4, key, 0x200 ); + } + + key_cert = find_cert_in_chain( sub, certificates, CERTS_DAT_SIZE, &cert_err ); + if( key_cert ) + cert_err = get_sig_len( key_cert ); + + if( cert_err < 0 ) + return -cert_err; + key = key_cert + cert_err; + + h = GetSha1( QByteArray( (const char*)sub, sub_len ) ); + ret = check_hash( h, sig, key ); + // remove this if statement if you don't want to check the whole chain + if( ret != ERROR_SUCCESS ) + return ret; + sig = key_cert; + sig_len = get_sig_len( sig ); + if( sig_len < 0 ) + return -sig_len; + sub = sig + sig_len; + sub_len = get_sub_len( sub ); + if( sub_len < 0 ) + return -sub_len; + } +} diff --git a/WiiQt/tiktmd.h b/WiiQt/tiktmd.h index 6a4ae0e..f171a6d 100644 --- a/WiiQt/tiktmd.h +++ b/WiiQt/tiktmd.h @@ -139,7 +139,7 @@ typedef struct _cert_ecdsa { class Ticket { public: - Ticket( QByteArray stuff ); + Ticket( QByteArray stuff = QByteArray() ); //expose the tik data to the rest of the code so it cas read directly from the p_tmd instead of having to add a function to access all the data //the pointer should be good until "data" is changed @@ -172,7 +172,7 @@ private: class Tmd { public: - Tmd( QByteArray stuff ); + Tmd( QByteArray stuff = QByteArray() ); //expose the tmd data to the rest of the code so it cas read directly from the p_tmd instead of having to add a function to access all the data //the pointer should be good until "data" is changed @@ -188,6 +188,9 @@ public: quint64 Size( quint16 i ); quint16 Type( quint16 i ); quint64 Tid(); + quint64 IOS(); + quint16 Gid(); + //gets the number of contents quint16 Count(); @@ -202,6 +205,7 @@ public: bool SetType( quint16 i, quint16 type ); bool SetSize( quint16 i, quint32 size ); bool SetHash( quint16 i, const QByteArray hash ); + bool SetIOS( quint64 ios ); bool FakeSign(); @@ -227,4 +231,21 @@ private: tmd *p_tmd; }; +enum +{ + ERROR_SUCCESS = 0, + ERROR_SIG_TYPE, + ERROR_SUB_TYPE, + ERROR_RSA_FAKESIGNED, + ERROR_RSA_HASH, + ERROR_RSA_TYPE_UNKNOWN, + ERROR_RSA_TYPE_MISMATCH, + ERROR_CERT_NOT_FOUND, + ERROR_COUNT +}; + +//checks the signatures in a tmd/ticket +//returns 1 of the above enums +int check_cert_chain( const QByteArray data ); + #endif // TIKTMD_H diff --git a/nandBinCheck/main.cpp b/nandBinCheck/main.cpp new file mode 100644 index 0000000..cc375a3 --- /dev/null +++ b/nandBinCheck/main.cpp @@ -0,0 +1,662 @@ +#include "../WiiQt/includes.h" +#include "../WiiQt/nandbin.h" +#include "../WiiQt/sharedcontentmap.h" +#include "../WiiQt/uidmap.h" +#include "../WiiQt/tools.h" +#include "../WiiQt/tiktmd.h" + + +//yippie for global variables +NandBin nand; +SharedContentMap sharedM; +UIDmap uidM; +QList< quint64 > tids; +QList< quint64 > validIoses;//dont put stubs in this list. +QTreeWidgetItem *root; +QList fats; + + +bool CheckTitleIntegrity( quint64 tid ); + +void Usage() +{ + qDebug() << "usage" << QCoreApplication::arguments().at( 0 ) << "nand.bin" << ""; + qDebug() << "\nOther options:"; + qDebug() << " -boot shows information about boot 1 and 2"; + qDebug() << " -fs verify the filesystem is in tact"; + qDebug() << " verifies presence of uid & content.map & checks the hashes in the content.map"; + qDebug() << " check installed titles for RSA & sha1 validity"; + qDebug() << " check installed titles for required IOS, proper uid & gid"; + qDebug() << " -clInfo shows free, used, and lost ( marked used, but dont belong to any file ) clusters"; + qDebug() << " -spare calculate & compare ecc for all pages in the nand"; + qDebug() << " calculate & compare hmac signatures for all files and superblocks"; + exit( 1 ); +} + +void Fail( const QString& str ) +{ + qDebug() << str; + exit( 1 ); +} + +QString TidTxt( quint64 tid ) +{ + return QString( "%1" ).arg( tid, 16, 16, QChar( '0' ) ); +} + +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: + qDebug() << "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 ) + { + 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 ).toInt( &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 ); + //qDebug() << "checking item" << subF->text( 0 ) + "/" + name; + if( !name.endsWith( ".tik" ) ) + { + //qDebug() << "!tik"; + continue; + } + + name.resize( 8 ); + quint32 lower = name.toInt( &ok, 16 ); + if( !ok ) + { + //qDebug() << "!ok"; + continue; + } + + //now see if there is a tmd + QTreeWidgetItem *tmdI = ItemFromPath( "/title/" + subF->text( 0 ) + "/" + name + "/content/title.tmd" ); + if( !tmdI ) + { + //qDebug() << "no tmd"; + continue; + } + + quint64 tid = (( (quint64)upper << 32) | lower ); + //qDebug() << "adding item to list" << TidTxt( tid ); + ret << tid; + } + } + 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() ) + Fail( "One of the shared contents in this nand is missing" ); + + QByteArray realHash = GetSha1( stuff ); + if( realHash != sharedM.Hash( i ) ) + 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 CheckTitleIntegrity( quint64 tid ) +{ + if( validIoses.contains( tid ) )//this one has already been checked + return true; + qDebug() << "Checking" << TidTxt( tid ).insert( 8, "-" ) << "..."; + 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++ ) + { + QByteArray ba = nand.GetData( i ? tmdp : tikp ); + if( ba.isEmpty() ) + { + if( i ) + qDebug() << "error getting tmd data"; + else + qDebug() << "error getting ticket data"; + return false; + } + switch( check_cert_chain( ba ) ) + { + case ERROR_SIG_TYPE: + case ERROR_SUB_TYPE: + case ERROR_RSA_TYPE_UNKNOWN: + case ERROR_RSA_TYPE_MISMATCH: + case ERROR_CERT_NOT_FOUND: + qDebug() << "the RSA signature isn't even close"; + return false; + break; + default: + break; + } + if( i ) + { + t = Tmd( ba ); + if( t.Tid() != tid ) + { + qDebug() << "the TMD contains the wrong TID"; + return false; + } + } + else + { + Ticket ticket( ba ); + if( ticket.Tid() != tid ) + { + qDebug() << "the ticket contains the wrong TID"; + return false; + } + } + } + + quint16 cnt = t.Count(); + for( quint16 i = 0; i < cnt; i++ ) + { + if( t.Type( i ) == 0x8001 )//shared + { + if( sharedM.GetAppFromHash( t.Hash( i ) ).isEmpty() ) + { + qDebug() << "one 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() ) + { + qDebug() << "one of the private contents is missing" << pA; + return false; + } + QByteArray realH = GetSha1( ba ); + if( realH != t.Hash( i ) ) + { + qDebug() << "one of the private contents' hash doesnt check out" << i << pA << + "\nexpected" << t.Hash( i ).toHex() << + "\nactual " << realH.toHex(); + return false; + } + } + + } + + quint64 ios = t.IOS(); + if( ios && !validIoses.contains( ios ) ) + { + qDebug() << "the IOS for this title is not bootable"; + return false; + } + quint32 uid = uidM.GetUid( tid, false ); + if( !uid ) + { + qDebug() << "this title has no UID entry"; + return false; + } + + //make sure all the stuff in this title's data directory belongs to it ( 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 + qDebug() << "incorrect uid/gid for data folder"; + quint16 cnt = dataI->childCount(); + for( quint16 i = 0; i < cnt; i++ ) + { + QTreeWidgetItem *item = dataI->child( i ); + if( item->text( 3 ) != uidS || !item->text( 4 ).startsWith( gidS ) ) + qDebug() << "incorrect uid/gid for" << QString( "data/" + item->text( 0 ) ); + } + } + return true; +} + +void CheckLostClusters() +{ + QList u = nand.GetFatsForEntry( 0 );//all clusters actually used for a file + 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; + //qDebug() << hex << i; + + 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 ++; + //qDebug() << p << hex << fats.at( p ); + quint16 block = p/8; + if( !blocks.contains( block ) ) + blocks << block; + } + /*QList< quint32 > badCpy = bad; + while( badCpy.size() ) + { + quint16 p = badCpy.takeFirst(); + quint16 block = p/64; + 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)"; + //qDebug() << bad; +} + +void SetUpTree() +{ + if( root ) + return; + QTreeWidgetItem *r = nand.GetTree(); + if( r->childCount() != 1 || r->child( 0 )->text( 0 ) != "/" ) + 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 ) ) + { + qDebug() << "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() ) + qDebug() << sclBad; +} + +int main( int argc, char *argv[] ) +{ + QCoreApplication a( argc, argv ); + QStringList args = QCoreApplication::arguments(); + 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" ); + + root = NULL; + + //these only serve to show info. no action is taken + if( args.contains( "-boot", Qt::CaseInsensitive ) ) + { + qDebug() << "checking boot1 & 2..."; + ShowBootInfo( nand.Boot1Version(), nand.Boot2Infos() ); + } + + if( args.contains( "-fs", 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() << "good 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. + } + } + + if( args.contains( "-clInfo", Qt::CaseInsensitive ) ) + { + qDebug() << "checking for lost clusters..."; + CheckLostClusters(); + } + + if( args.contains( "-spare", Qt::CaseInsensitive ) ) + { + qDebug() << "verifying ecc..."; + CheckEcc(); + + SetUpTree(); + qDebug() << "verifying hmac..."; + CheckHmac(); + } + + + return 0; +} + diff --git a/nandBinCheck/nandBinCheck.pro b/nandBinCheck/nandBinCheck.pro new file mode 100644 index 0000000..241d423 --- /dev/null +++ b/nandBinCheck/nandBinCheck.pro @@ -0,0 +1,35 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2010-12-17T16:22:53 +# +#------------------------------------------------- + +#QT += core\ +# gui + +TARGET = nandBinCheck +#CONFIG += console +#CONFIG -= app_bundle + +TEMPLATE = app + + +SOURCES += main.cpp \ + ../WiiQt/blocks0to7.cpp \ + ../WiiQt/tiktmd.cpp \ + ../WiiQt/nandbin.cpp \ + ../WiiQt/tools.cpp \ + ../WiiQt/savebanner.cpp \ + ../WiiQt/aes.c \ + ../WiiQt/sha1.c \ + ../WiiQt/uidmap.cpp \ + ../WiiQt/sharedcontentmap.cpp \ + ../WiiQt/nandspare.cpp + +HEADERS += ../WiiQt/tiktmd.h \ + ../WiiQt/nandbin.h \ + ../WiiQt/tools.h \ + ../WiiQt/blocks0to7.h \ + ../WiiQt/uidmap.h \ + ../WiiQt/sharedcontentmap.h \ + ../WiiQt/nandspare.h diff --git a/nandBinCheck/readmii.txt b/nandBinCheck/readmii.txt new file mode 100644 index 0000000..4a73717 --- /dev/null +++ b/nandBinCheck/readmii.txt @@ -0,0 +1,8 @@ +this is a simple cli program to check a nand.bin for various things... +boot1/2 info, fs integrity, signatures, hashes, ecc, hmac, lost clusters, and title sanity. + +i have written this as an aide to help debugging the write functions in the nand.bin class. but it likely will serve other purposes. + +run it with no args to see the usage/options + +giantpune