* fix bugs in the nand extractor that caused it not to be able to find the keys.bin

* add fakesigning stuff to the tmd & ticket classes
* add functions for changing the tmd & ticket classes ( size, hash, tid... )
* allow replacing contents in wads ( untested )
* other little bug fixes i forgot
This commit is contained in:
giantpune@gmail.com 2010-12-09 11:30:25 +00:00
parent 43d202a052
commit 68603b95a9
11 changed files with 392 additions and 70 deletions

View File

@ -25,6 +25,7 @@ MainWindow::MainWindow( QWidget *parent ) : QMainWindow( parent ), ui( new Ui::M
//TODO, really get these paths from settings
ui->lineEdit_cachePath->setText( cachePath );
ui->lineEdit_nandPath->setText( nandPath );
ui->lineEdit_extractPath->setText( "./downloaded" );
//nand.SetPath( nandPath );
nus.SetCachePath( cachePath );
@ -62,7 +63,7 @@ void MainWindow::ShowMessage( const QString& mes )
void MainWindow::NusIsDone()
{
QString str = tr( "NUS ojbect is done working<br>" );
QString str = tr( "NUS object is done working<br>" );
ui->plainTextEdit_log->appendHtml( str );
ui->statusBar->showMessage( tr( "Done" ), 5000 );
if( ui->radioButton_folder->isChecked() )
@ -76,6 +77,7 @@ void MainWindow::NusIsDone()
ui->pushButton_nandPath->setEnabled( true );
//write the uid.sys and content.map to disc
ShowMessage( tr( "Flushing nand..." ) );
nand.Flush();
//make sure there is a setting.txt
@ -106,11 +108,11 @@ void MainWindow::ReceiveTitleFromNus( NusJob job )
ui->plainTextEdit_log->appendHtml( str );
//do something with the data we got
if( ui->radioButton_folder->isChecked() )
if( ui->radioButton_folder->isChecked() )//copy its decrypted contents to a folder
{
SaveJobToFolder( job );
}
else if( ui->radioButton_nand->isChecked() )
else if( ui->radioButton_nand->isChecked() )//install this title to a decrypted nand dump for sneek/dolphin
{
bool ok = nand.InstallNusItem( job );
if( ok )
@ -120,57 +122,8 @@ void MainWindow::ReceiveTitleFromNus( NusJob job )
}
else if( ui->radioButton_wad->isChecked() )
{
Wad wad( job.data );
if( !wad.IsOk() )
{
ShowMessage( "<b>Error making a wad from " + title + "<\b>" );
return;
SaveJobToWad( job );
}
QFileInfo fi( ui->lineEdit_wad->text() );
if( fi.isFile() )
{
ShowMessage( "<b>" + ui->lineEdit_wad->text() + " is a file. I need a folder<\b>" );
return;
}
if( !fi.exists() )
{
ShowMessage( "<b>" + fi.absoluteFilePath() + " is not a folder!\nTrying to create it...<\b>" );
if( !QDir().mkpath( ui->lineEdit_wad->text() ) )
{
ShowMessage( "<b>Failed to make the directory!<\b>" );
return;
}
}
QByteArray w = wad.Data();
if( w.isEmpty() )
{
ShowMessage( "<b>Error creating wad<br>" + wad.LastError() + "<\b>" );
return;
}
QString name = wad.WadName( fi.absoluteFilePath() );
if( name.isEmpty() )
{
name = QFileDialog::getSaveFileName( this, tr( "Filename for %1" ).arg( title ), fi.absoluteFilePath() );
if( name.isEmpty() )
{
ShowMessage( "<b>No save name given, aborting<\b>" );
return;
}
}
QFile file( fi.absoluteFilePath() + "/" + name );
if( !file.open( QIODevice::WriteOnly ) )
{
ShowMessage( "<b>Cant open " + fi.absoluteFilePath() + "/" + name + " for writing<\b>" );
return;
}
file.write( w );
file.close();
ShowMessage( "Saved " + title + " to " + fi.absoluteFilePath() + "/" + name );
}
//bool r = nand.InstallNusItem( job );
//qDebug() << "install:" << r;
}
//clicked the button to get a title
@ -304,8 +257,8 @@ void MainWindow::on_pushButton_nandPath_clicked()
void MainWindow::on_pushButton_decFolder_clicked()
{
QString path = ui->lineEdit_extractPath->text().isEmpty() ? "/media" : ui->lineEdit_extractPath->text();
QString f = QFileDialog::getExistingDirectory( this, tr( "Select folder to decrypt this title to" ), path );
QString path = ui->lineEdit_extractPath->text().isEmpty() ? QDir::currentPath() : ui->lineEdit_extractPath->text();
QString f = QFileDialog::getExistingDirectory( this, tr( "Select folder to save decrypted titles" ), path );
if( f.isEmpty() )
return;
@ -332,7 +285,7 @@ void MainWindow::on_actionSetting_txt_triggered()
return;
}
QByteArray ba = nand.GetSettingTxt(); //read the current setting.txt
ba = SettingTxtDialog::Edit( this, ba ); //call a dialog to edit that existing file and store the result in teh same bytearray
ba = SettingTxtDialog::Edit( this, ba ); //call a dialog to edit that existing file and store the result in the same bytearray
if( !ba.isEmpty() ) //if the dialog returned anything ( cancel wasnt pressed ) write that new setting.txt to the nand dump
nand.SetSettingTxt( ba );
}
@ -343,3 +296,122 @@ void MainWindow::on_actionFlush_triggered()
if( !nand.GetPath().isEmpty() )
nand.Flush();
}
//save a NUS job to a folder
void MainWindow::SaveJobToFolder( NusJob job )
{
QString title = QString( "%1v%2" ).arg( job.tid, 16, 16, QChar( '0' ) ).arg( job.version );
QFileInfo fi( ui->lineEdit_extractPath->text() );
if( fi.isFile() )
{
ShowMessage( "<b>" + ui->lineEdit_extractPath->text() + " is a file. I need a folder<\b>" );
return;
}
if( !fi.exists() )
{
ShowMessage( "<b>" + fi.absoluteFilePath() + " is not a folder!\nTrying to create it...<\b>" );
if( !QDir().mkpath( fi.absoluteFilePath() ) )
{
ShowMessage( "<b>Failed to make the directory!<\b>" );
return;
}
}
QString newFName = title;
int i = 1;
while( QFileInfo( fi.absoluteFilePath() + "/" + newFName ).exists() )//find a folder that doesnt exist and try to create it
{
newFName = QString( "%1 (copy%2)" ).arg( title ).arg( i++ );
}
if( !QDir().mkpath( fi.absoluteFilePath() + "/" + newFName ) )
{
ShowMessage( "<b>Can't create" + fi.absoluteFilePath() + "/" + newFName + " to save this title into!<\b>" );
return;
}
//start writing all this stuff to the HDD
QDir d( fi.absoluteFilePath() + "/" + newFName );
QByteArray tmdDat = job.data.takeFirst(); //remember the tmd and use it for getting the names of the .app files
if( !WriteFile( d.absoluteFilePath( "title.tmd" ), tmdDat ) )
{
ShowMessage( "<b>Error writing " + d.absoluteFilePath( "title.tmd" ) + "!<\b>" );
return;
}
if( !WriteFile( d.absoluteFilePath( "cetk" ), job.data.takeFirst() ) )
{
ShowMessage( "<b>Error writing " + d.absoluteFilePath( "cetk" ) + "!<\b>" );
return;
}
Tmd t( tmdDat );
quint16 cnt = t.Count();
if( job.data.size() != cnt )
{
ShowMessage( "<b>Error! Number of contents in the TMD dont match the number received from NUS!<\b>" );
return;
}
for( quint16 i = 0; i < cnt; i++ )//write all the contents in the new folder. if the job is decrypted, append ".app" to the end of their names
{
QString appName = t.Cid( i );
if( job.decrypt )
appName += ".app";
if( !WriteFile( d.absoluteFilePath( appName ), job.data.takeFirst() ) )
{
ShowMessage( "<b>Error writing " + d.absoluteFilePath( appName ) + "!<\b>" );
return;
}
}
ShowMessage( tr( "Wrote title to %1" ).arg( fi.absoluteFilePath() + "/" + newFName ) );
}
//save a conpleted job to wad
void MainWindow::SaveJobToWad( NusJob job )
{
QString title = QString( "%1v%2" ).arg( job.tid, 16, 16, QChar( '0' ) ).arg( job.version );
Wad wad( job.data );
if( !wad.IsOk() )
{
ShowMessage( "<b>Error making a wad from " + title + "<\b>" );
return;
}
QFileInfo fi( ui->lineEdit_wad->text() );
if( fi.isFile() )
{
ShowMessage( "<b>" + ui->lineEdit_wad->text() + " is a file. I need a folder<\b>" );
return;
}
if( !fi.exists() )
{
ShowMessage( "<b>" + fi.absoluteFilePath() + " is not a folder!\nTrying to create it...<\b>" );
if( !QDir().mkpath( ui->lineEdit_wad->text() ) )
{
ShowMessage( "<b>Failed to make the directory!<\b>" );
return;
}
}
QByteArray w = wad.Data();
if( w.isEmpty() )
{
ShowMessage( "<b>Error creating wad<br>" + wad.LastError() + "<\b>" );
return;
}
QString name = wad.WadName( fi.absoluteFilePath() );
if( name.isEmpty() )
{
name = QFileDialog::getSaveFileName( this, tr( "Filename for %1" ).arg( title ), fi.absoluteFilePath() );
if( name.isEmpty() )
{
ShowMessage( "<b>No save name given, aborting<\b>" );
return;
}
}
QFile file( fi.absoluteFilePath() + "/" + name );
if( !file.open( QIODevice::WriteOnly ) )
{
ShowMessage( "<b>Cant open " + fi.absoluteFilePath() + "/" + name + " for writing<\b>" );
return;
}
file.write( w );
file.close();
ShowMessage( "Saved " + title + " to " + fi.absoluteFilePath() + "/" + name );
}

View File

@ -25,6 +25,10 @@ private:
void ShowMessage( const QString& mes );
//do something with a completed download
void SaveJobToFolder( NusJob job );
void SaveJobToWad( NusJob job );
public slots:

View File

@ -207,7 +207,7 @@ bool NusDownloader::SaveDataToCache( const QString &path, const QByteArray &stuf
QDir d( cachePath );
if( !d.exists() || !d.mkpath( parent ) )
{
qWarning() << "NusDownloader::SaveDataToCache -> cant create directory" << d.absolutePath();
qWarning() << "NusDownloader::SaveDataToCache -> cant create directory" << QString( d.absolutePath() + "/" + path );
return false;
}
QFile f( path );

View File

@ -60,10 +60,10 @@ public:
//get a list of titles for a given update
//if a title is not available on NUS, a substitute is given instead ( a later version of the same title )
//to keep people from bulk DLing and installing and messing something up, any boot2 upudate will not be included
//in the list, ask for it specifically
//to keep people from bulk DLing and installing and messing something up, any boot2 upudate will NOT be included
//in the list, ask for it specifically. IOS35 is added in all updates for use in sneek
//lists are created from wiimpersonator logs when available. otherwise they come from examining game update partitions
//for the 2.x updates, IOS35 is added for use in sneek
static QMap< quint64, quint16 > List20u();
static QMap< quint64, quint16 > List30u();
static QMap< quint64, quint16 > List31u();
@ -159,6 +159,7 @@ private:
quint32 totalTitleSize;
quint32 TitleSizeDownloaded();
//remember the ticked key for repeated use
QByteArray decKey;
@ -166,7 +167,7 @@ private:
signals:
void SendError( const QString &message, NusJob job );//send an errer and the title the error is about
//send an errer and the title the error is about, no more jobs will be done, and the SendDone signal will not be emited
void SendFatalErrorError( const QString &message, NusJob job );
void SendFatalErrorError( const QString &message, NusJob job );//currently not used
void SendDone();//message that all jobs are done
//send progress about the currently downloading job

View File

@ -12,6 +12,11 @@ Tmd::Tmd( QByteArray stuff )
return;
SetPointer();
if( (quint32)data.size() != SignedSize() )
{
data.resize( SignedSize() );
SetPointer();
}
//hexdump( stuff );
}
@ -22,6 +27,15 @@ quint64 Tmd::Tid()
return qFromBigEndian( p_tmd->title_id );
}
bool Tmd::SetTid( quint64 tid )
{
if( !p_tmd )
return false;
p_tmd->title_id = qFromBigEndian( tid );
return true;
}
QString Tmd::Cid( quint16 i )
{
if( !p_tmd || i > qFromBigEndian( p_tmd->num_contents ) )
@ -37,6 +51,17 @@ QByteArray Tmd::Hash( quint16 i )
return QByteArray( (const char*)&p_tmd->contents[ i ].hash, 20 );
}
bool Tmd::SetHash( quint16 cid, const QByteArray hash )
{
if( !p_tmd || cid >= qFromBigEndian( p_tmd->num_contents ) || hash.size() != 20 )
return false;
const char* h = hash.data();
for( quint8 i = 0; i < 20; i++ )
p_tmd->contents[ cid ].hash[ i ] = h[ i ];
return true;
}
quint16 Tmd::Count()
{
if( !p_tmd )
@ -53,6 +78,15 @@ quint16 Tmd::Version()
return qFromBigEndian( p_tmd->title_version );
}
bool Tmd::SetVersion( quint16 v )
{
if( !p_tmd )
return false;
p_tmd->title_version = qFromBigEndian( v );
return true;
}
quint64 Tmd::Size( quint16 i )
{
if( !p_tmd || i > qFromBigEndian( p_tmd->num_contents ) )
@ -60,6 +94,15 @@ quint64 Tmd::Size( quint16 i )
return qFromBigEndian( p_tmd->contents[ i ].size );
}
bool Tmd::SetSize( quint16 cid, quint32 size )
{
if( !p_tmd || cid >= qFromBigEndian( p_tmd->num_contents ) )
return false;
p_tmd->contents[ cid ].size = qFromBigEndian( size );
return true;
}
quint16 Tmd::Type( quint16 i )
{
if( !p_tmd || i > qFromBigEndian( p_tmd->num_contents ) )
@ -67,6 +110,15 @@ quint16 Tmd::Type( quint16 i )
return qFromBigEndian( p_tmd->contents[ i ].type );
}
bool Tmd::SetType( quint16 cid, quint16 type )
{
if( !p_tmd || cid >= qFromBigEndian( p_tmd->num_contents ) )
return false;
p_tmd->contents[ cid ].type = qFromBigEndian( type );
return true;
}
quint32 Tmd::SignedSize()
{
if( !p_tmd )
@ -89,6 +141,48 @@ void Tmd::SetPointer()
p_tmd = (tmd*)((quint8*)data.data() + payLoadOffset);
}
void Tmd::Dbg()
{
if( !p_tmd )
return;
QString contents;
quint16 cnt = Count();
for( quint16 i = 0; i < cnt; i++ )
{
contents += QString( "%1 %2 %3 %4 " ).arg( i, 2, 16 ).arg( Type( i ), 4, 16 ).arg( Cid( i ) ).arg( Size( i ), 8, 16 )
+ Hash( i ).toHex() + "\n";
}
QString s = QString( "TMD Dbg:\ntid:: %1\ncnt:: %2\n" )
.arg( Tid(), 16, 16, QChar( '0' ) )
.arg( cnt ) + contents;
qDebug() << s;
}
bool Tmd::FakeSign()
{
if( !p_tmd || payLoadOffset < 5 )
return false;
quint32 size = SignedSize();
memset( (void*)( data.data() + 4 ), 0, payLoadOffset - 4 );//zero the RSA
quint16 i = 0;
bool ret = false;//brute force the sha1
do
{
p_tmd->zero = i;//no need to worry about endian here
if( GetSha1( data.mid( payLoadOffset, size ) ).startsWith( '\0' ) )
{
ret = true;
break;
}
}
while( ++i );
return ret;
}
Ticket::Ticket( QByteArray stuff )
{
data = stuff;
@ -97,6 +191,11 @@ Ticket::Ticket( QByteArray stuff )
return;
SetPointer();
if( (quint32)data.size() != SignedSize() )
{
data.resize( SignedSize() );
SetPointer();
}
}
quint64 Ticket::Tid()
@ -106,6 +205,15 @@ quint64 Ticket::Tid()
return qFromBigEndian( p_tik->titleid );
}
bool Ticket::SetTid( quint64 tid )
{
if( !p_tik )
return false;
p_tik->titleid = qFromBigEndian( tid );
return true;
}
QByteArray Ticket::DecryptedKey()
{
quint8 iv[ 16 ];
@ -145,3 +253,26 @@ void Ticket::SetPointer()
p_tik = (tik*)((quint8*)data.data() + payLoadOffset);
}
bool Ticket::FakeSign()
{
if( !p_tik || payLoadOffset < 5 )
return false;
quint32 size = SignedSize();
memset( (void*)( data.data() + 4 ), 0, payLoadOffset - 4 );//zero the RSA
quint16 i = 0;
bool ret = false;//brute force the sha1
do
{
p_tik->padding = i;//no need to worry about endian here
if( GetSha1( data.mid( payLoadOffset, size ) ).startsWith( '\0' ) )
{
ret = true;
break;
}
}
while( ++i );
return ret;
}

View File

@ -149,9 +149,14 @@ public:
const tik *payload(){ return p_tik; }
quint64 Tid();
bool SetTid( quint64 tid );
QByteArray DecryptedKey();
quint32 SignedSize();
bool FakeSign();
//get the ticket data
const QByteArray Data(){ return data; }
private:
quint32 payLoadOffset;
@ -193,6 +198,22 @@ public:
//title version
quint16 Version();
//functions to edit the TMD
bool SetTid( quint64 tid );
bool SetVersion( quint16 v );
bool SetType( quint16 cid, quint16 type );
bool SetSize( quint16 cid, quint32 size );
bool SetHash( quint16 cid, const QByteArray hash );
bool FakeSign();
//get the tmd data
const QByteArray Data(){ return data; }
//print the tmd info to qDebug()
void Dbg();
quint32 SignedSize();
private:
quint32 payLoadOffset;

View File

@ -155,6 +155,24 @@ QByteArray ReadFile( const QString &path )
return ret;
}
bool WriteFile( const QString &path, const QByteArray ba )
{
QFile file( path );
if( !file.open( QIODevice::WriteOnly ) )
{
qWarning() << "WriteFile -> can't open" << path;
return false;
}
if( file.write( ba ) != ba.size() )
{
file.close();
qWarning() << "WriteFile -> can't write all the data to" << path;
return false;
}
file.close();
return true;
}
#define CERTS_DAT_SIZE 2560
const quint8 certs_dat[ CERTS_DAT_SIZE ] = {
0x00, 0x01, 0x00, 0x01, 0x7D, 0x9D, 0x5E, 0xBA, 0x52, 0x81, 0xDC, 0xA7, 0x06, 0x5D, 0x2F, 0x08,

View File

@ -26,6 +26,9 @@ QByteArray PaddedByteArray( const QByteArray &orig, quint32 padTo );
//read a file into a bytearray
QByteArray ReadFile( const QString &path );
//save a file to disc
bool WriteFile( const QString &path, const QByteArray ba );
//keep track of the last folder browsed to when looking for files
extern QString currentDir;

View File

@ -180,6 +180,7 @@ void UIDmap::CreateNew( bool addFactorySetupDiscs )
default:
qWarning() << "oops" << hex << i;
return;
break;
}
uid = qFromBigEndian( 0x1000 + i );

View File

@ -441,7 +441,7 @@ QString Wad::WadName( quint64 tid, quint16 version, QString path )
QByteArray Wad::FromDirectory( QDir dir )
{
QFileInfoList tmds = dir.entryInfoList( QStringList() << "*.tmd", QDir::Files );
QFileInfoList tmds = dir.entryInfoList( QStringList() << "*.tmd" << "tmd.*", QDir::Files );
if( tmds.isEmpty() )
{
qWarning() << "Wad::FromDirectory -> no tmd found in" << dir.absolutePath();
@ -450,7 +450,7 @@ QByteArray Wad::FromDirectory( QDir dir )
QByteArray tmdD = ReadFile( tmds.at( 0 ).absoluteFilePath() );
if( tmdD.isEmpty() )
return QByteArray();
QFileInfoList tiks = dir.entryInfoList( QStringList() << "*.tik", QDir::Files );
QFileInfoList tiks = dir.entryInfoList( QStringList() << "*.tik" << "cetk", QDir::Files );
if( tiks.isEmpty() )
{
qWarning() << "Wad::FromDirectory -> no tik found in" << dir.absolutePath();
@ -459,8 +459,17 @@ QByteArray Wad::FromDirectory( QDir dir )
QByteArray tikD = ReadFile( tiks.at( 0 ).absoluteFilePath() );
if( tikD.isEmpty() )
return QByteArray();
Tmd t(tmdD );
QList<QByteArray> datas = QList<QByteArray>()<< tmdD << tikD;
Tmd t( tmdD );
Ticket ticket( tikD );
//make sure to only add the tmd & ticket without all the cert mumbo jumbo
QByteArray tmdP = tmdD;
tmdP.resize( t.SignedSize() );
QByteArray tikP = tikD;
tikP.resize( ticket.SignedSize() );
QList<QByteArray> datas = QList<QByteArray>()<< tmdP << tikP;
quint16 cnt = t.Count();
for( quint16 i = 0; i < cnt; i++ )
@ -486,3 +495,61 @@ QByteArray Wad::FromDirectory( QDir dir )
QByteArray ret = wad.Data();
return ret;
}
bool Wad::SetTid( quint64 tid )
{
if( !tmdData.size() || !tikData.size() )
{
Err( "Mising parts of the wad" );
return false;
}
Tmd t( tmdData );
Ticket ti( tikData );
t.SetTid( tid );
ti.SetTid( tid );
if( !t.FakeSign() )
{
Err( "Error signing TMD" );
return false;
}
if( !ti.FakeSign() )
{
Err( "Error signing ticket" );
return false;
}
tmdData = t.Data();
tikData = ti.Data();
return true;
}
bool Wad::ReplaceContent( quint16 idx, const QByteArray ba )
{
if( idx >= partsEnc.size() || !tmdData.size() || !tikData.size() )
{
Err( "Mising parts of the wad" );
return false;
}
QByteArray hash = GetSha1( ba );
quint32 size = ba.size();
Tmd t( tmdData );
t.SetHash( idx, hash );
t.SetSize( idx, size );
if( !t.FakeSign() )
{
Err( "Error signing the tmd" );
return false;
}
tmdData = t.Data();
Ticket ti( tikData );
AesSetKey( ti.DecryptedKey() );
QByteArray decDataPadded = PaddedByteArray( ba, 0x40 );
QByteArray encData = AesEncrypt( idx, decDataPadded );
partsEnc.replace( idx, encData );
return true;
}

View File

@ -23,15 +23,19 @@ public:
quint64 Tid();
//set the tid in the ticket&tmd and fakesign the wad
void SetTid( quint64 tid );
bool SetTid( quint64 tid );
//replace a content of this wad, update the size & hash in the tmd and sign it
//ba should be decrypted
bool ReplaceContent( quint16 idx, const QByteArray ba );
//add a new content to this wad and fakesign
//if the data is encrypted, set that arguement to true
//index is the index used for the new entry, default is after all the others
void AddContent( const QByteArray &stuff, quint16 type, bool isEncrypted = false, quint16 index = 0xffff );
//void AddContent( const QByteArray &stuff, quint16 type, bool isEncrypted = false, quint16 index = 0xffff );
//remove a content from this wad
void RemoveContent( quint16 index );
//void RemoveContent( quint16 index );
//set the global cert that will be used for all created
static void SetGlobalCert( const QByteArray &stuff );
@ -58,9 +62,9 @@ public:
//returns an empty string if it cant guess the title based on TID
static QString WadName( quint64 tid, quint16 version, QString path = QString() );
//get this Wad's name as it would appear on a disc update partition
QString WadName( QString path = QString() );
private:
bool ok;
QString errStr;