From a9ddb1d3b3bd38e8959a20202e5336c6a092a994 Mon Sep 17 00:00:00 2001 From: Eladash Date: Fri, 5 Mar 2021 18:07:36 +0200 Subject: [PATCH] GUI: Implement full extraction of PUP * Implement full extraction of PS3UPDAT.PUP. * Implement TAR extraction via GUI. * Use VFS to implement missing PS3 filesystem characters escaping. * Use VFS to error on illegal paths. (illegal paths such as malware pointing to "/../../..and so on../C:/Windows") --- rpcs3/Loader/TAR.cpp | 94 ++++++++++++++++++++++++-- rpcs3/Loader/TAR.h | 6 +- rpcs3/rpcs3qt/gui_settings.h | 1 + rpcs3/rpcs3qt/main_window.cpp | 122 +++++++++++++++++++++++++++++++++- rpcs3/rpcs3qt/main_window.h | 4 +- rpcs3/rpcs3qt/main_window.ui | 16 +++++ 6 files changed, 234 insertions(+), 9 deletions(-) diff --git a/rpcs3/Loader/TAR.cpp b/rpcs3/Loader/TAR.cpp index 6b3e9825ec..966e6b9283 100644 --- a/rpcs3/Loader/TAR.cpp +++ b/rpcs3/Loader/TAR.cpp @@ -1,5 +1,10 @@ #include "stdafx.h" +#include "Emu/VFS.h" +#include "Emu/System.h" + +#include "Crypto/unself.h" + #include "TAR.h" #include "util/asm.hpp" @@ -137,7 +142,7 @@ fs::file tar_object::get_file(const std::string& path) } } -bool tar_object::extract(std::string path, std::string ignore) +bool tar_object::extract(std::string vfs_mp) { if (!m_file) return false; @@ -148,11 +153,23 @@ bool tar_object::extract(std::string path, std::string ignore) const TARHeader& header = iter.second.second; const std::string& name = iter.first; - std::string result = path + name; + std::string result = name; - if (result.compare(path.size(), ignore.size(), ignore) == 0) + if (!vfs_mp.empty()) { - result.erase(path.size(), ignore.size()); + result = fmt::format("/%s/%s", vfs_mp, result); + } + else + { + result.insert(result.begin(), '/'); + } + + result = vfs::get(result); + + if (result.empty()) + { + tar_log.error("Path of entry is not mounted: '%s' (vfs_mp='%s')", name, vfs_mp); + return false; } switch (header.filetype) @@ -160,6 +177,13 @@ bool tar_object::extract(std::string path, std::string ignore) case '\0': case '0': { + // Create the directories which should have been mount points if vfs_mp is not empty + if (!vfs_mp.empty() && !fs::create_path(fs::get_parent_dir(result))) + { + tar_log.error("TAR Loader: failed to create directory for file %s (%s)", name, fs::g_tls_error); + return false; + } + auto data = get_file(name).release(); fs::file file(result, fs::rewrite); @@ -194,3 +218,65 @@ bool tar_object::extract(std::string path, std::string ignore) } return true; } + +bool extract_tar(const std::string& file_path, const std::string& dir_path) +{ + tar_log.notice("Extracting '%s' to directory '%s'...", file_path, dir_path); + + fs::file file(file_path); + + if (!file) + { + tar_log.error("Error opening file '%s' (%s)", file_path, fs::g_tls_error); + return false; + } + + std::vector vec; + + if (SCEDecrypter self_dec(file); self_dec.LoadHeaders()) + { + // Encrypted file, decrypt + self_dec.LoadMetadata(SCEPKG_ERK, SCEPKG_RIV); + + if (!self_dec.DecryptData()) + { + tar_log.error("Failed to decrypt TAR."); + return false; + } + + vec = self_dec.MakeFile(); + if (vec.size() < 3) + { + tar_log.error("Failed to decrypt TAR."); + return false; + } + } + else + { + // Not an encrypted file + tar_log.warning("TAR is not encrypted, it may not be valid for this tool. Encrypted TAR are known to be found in PS3 Firmware files only."); + } + + if (!vfs::mount("/tar_extract", dir_path)) + { + tar_log.error("Failed to mount '%s'", dir_path); + return false; + } + + tar_object tar(vec.empty() ? file : vec[2]); + + const bool ok = tar.extract("/tar_extract"); + + if (ok) + { + tar_log.success("Extraction complete!"); + } + else + { + tar_log.error("TAR contents are invalid."); + } + + // Unmount + Emu.Init(); + return ok; +} diff --git a/rpcs3/Loader/TAR.h b/rpcs3/Loader/TAR.h index 120098f1e1..ec9a8dcd5f 100644 --- a/rpcs3/Loader/TAR.h +++ b/rpcs3/Loader/TAR.h @@ -33,5 +33,9 @@ public: fs::file get_file(const std::string& path); - bool extract(std::string path, std::string ignore = ""); // extract all files in archive to path + // Extract all files in archive to destination as VFS + // Allow to optionally specify explicit mount point (which may be directory meant for extraction) + bool extract(std::string vfs_mp = {}); }; + +bool extract_tar(const std::string& file_path, const std::string& dir_path); diff --git a/rpcs3/rpcs3qt/gui_settings.h b/rpcs3/rpcs3qt/gui_settings.h index 4031ef08a1..1632ed4c60 100644 --- a/rpcs3/rpcs3qt/gui_settings.h +++ b/rpcs3/rpcs3qt/gui_settings.h @@ -130,6 +130,7 @@ namespace gui const gui_save fd_cg_disasm = gui_save(main_window, "lastExplorePathCGD", ""); const gui_save fd_log_viewer = gui_save(main_window, "lastExplorePathLOG", ""); const gui_save fd_ext_mself = gui_save(main_window, "lastExplorePathExMSELF", ""); + const gui_save fd_ext_tar = gui_save(main_window, "lastExplorePathExTAR", ""); const gui_save mw_debugger = gui_save(main_window, "debuggerVisible", false); const gui_save mw_logger = gui_save(main_window, "loggerVisible", true); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index f764c08593..1ff7220dd3 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -836,7 +836,87 @@ void main_window::InstallPup(QString file_path) } } -void main_window::HandlePupInstallation(QString file_path) +void main_window::ExtractPup() +{ + const QString path_last_pup = m_gui_settings->GetValue(gui::fd_install_pup).toString(); + QString file_path = QFileDialog::getOpenFileName(this, tr("Select PS3UPDAT.PUP To extract"), path_last_pup, tr("PS3 update file (PS3UPDAT.PUP);;All pup files (*.pup);;All files (*.*)")); + + if (file_path.isEmpty()) + { + return; + } + + QString dir = QFileDialog::getExistingDirectory(this, tr("Extraction Directory"), QString{}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (!dir.isEmpty()) + { + HandlePupInstallation(file_path, dir); + } +} + +void main_window::ExtractTar() +{ + if (!m_gui_settings->GetBootConfirmation(this)) + { + return; + } + + Emu.SetForceBoot(true); + Emu.Stop(); + + const QString path_last_tar = m_gui_settings->GetValue(gui::fd_ext_tar).toString(); + QStringList files = QFileDialog::getOpenFileNames(this, tr("Select TAR To extract"), path_last_tar, tr("All tar files (*.tar *.tar.aa.*);;All files (*.*)")); + + if (files.isEmpty()) + { + return; + } + + QString dir = QFileDialog::getExistingDirectory(this, tr("Extraction Directory"), QString{}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + + if (dir.isEmpty()) + { + return; + } + + m_gui_settings->SetValue(gui::fd_ext_tar, QFileInfo(files[0]).path()); + + progress_dialog pdlg(tr("TAR Extraction"), tr("Extracting encrypted TARs\nPlease wait..."), tr("Cancel"), 0, files.size(), false, this); + pdlg.show(); + + QString error; + + for (const QString& file : files) + { + if (pdlg.wasCanceled()) + { + break; + } + + // Do not abort on failure here, in case the user selected a wrong file in multi-selection while the rest are valid + if (!extract_tar(sstr(file), sstr(dir) + '/')) + { + if (error.isEmpty()) + { + error = tr("The following TAR file(s) could not be extracted:"); + } + + error += "\n"; + error += file; + } + + pdlg.SetValue(pdlg.value() + 1); + QApplication::processEvents(); + } + + if (!error.isEmpty()) + { + pdlg.hide(); + QMessageBox::critical(this, tr("Tar extraction failed"), error); + } +} + +void main_window::HandlePupInstallation(QString file_path, QString dir_path) { if (file_path.isEmpty()) { @@ -852,6 +932,7 @@ void main_window::HandlePupInstallation(QString file_path) Emu.Stop(); m_gui_settings->SetValue(gui::fd_install_pup, QFileInfo(file_path).path()); + const std::string path = sstr(file_path); auto critical = [this](QString str) @@ -926,6 +1007,31 @@ void main_window::HandlePupInstallation(QString file_path) } tar_object update_files(update_files_f); + + if (!dir_path.isEmpty()) + { + // Extract only mode, extract direct TAR entries to a user directory + + if (!vfs::mount("/pup_extract", sstr(dir_path) + '/')) + { + gui_log.error("Error while extracting firmware: Failed to mount '%s'", sstr(dir_path)); + critical(tr("Firmware extraction failed: VFS mounting failed.")); + return; + } + + if (!update_files.extract("/pup_extract")) + { + gui_log.error("Error while installing firmware: TAR contents are invalid."); + critical(tr("Firmware installation failed: Firmware contents could not be extracted.")); + } + + gui_log.success("Extracted PUP file to %s", sstr(dir_path)); + return; + } + + // In regular installation we select specfic entries from the main TAR which are prefixed with "dev_flash_" + // Those entries are TAR as well, we extract their packed files from them and that's what installed in /dev_flash + auto update_filenames = update_files.get_filenames(); update_filenames.erase(std::remove_if( @@ -985,6 +1091,9 @@ void main_window::HandlePupInstallation(QString file_path) progress_dialog pdlg(tr("RPCS3 Firmware Installer"), tr("Installing firmware version %1\nPlease wait...").arg(qstr(version_string)), tr("Cancel"), 0, static_cast(update_filenames.size()), false, this); pdlg.show(); + // Used by tar_object::extract() as destination directory + vfs::mount("/dev_flash", g_cfg.vfs.get_dev_flash()); + // Synchronization variable atomic_t progress(0); { @@ -1010,9 +1119,9 @@ void main_window::HandlePupInstallation(QString file_path) } tar_object dev_flash_tar(dev_flash_tar_f[2]); - if (!dev_flash_tar.extract(g_cfg.vfs.get_dev_flash(), "dev_flash/")) + if (!dev_flash_tar.extract()) { - gui_log.error("Error while installing firmware: TAR contents are invalid."); + gui_log.error("Error while installing firmware: TAR contents are invalid. (package=%s)", update_filename); critical(tr("Firmware installation failed: Firmware contents could not be extracted.")); progress = -1; return; @@ -1055,6 +1164,9 @@ void main_window::HandlePupInstallation(QString file_path) // Update with newly installed PS3 fonts Q_EMIT RequestGlobalStylesheetChange(); + // Unmount + Emu.Init(); + if (progress == update_filenames.size()) { gui_log.success("Successfully installed PS3 firmware version %s.", version_string); @@ -2009,6 +2121,10 @@ void main_window::CreateConnects() connect(ui->toolsExtractMSELFAct, &QAction::triggered, this, &main_window::ExtractMSELF); + connect(ui->toolsExtractPUPAct, &QAction::triggered, this, &main_window::ExtractPup); + + connect(ui->toolsExtractTARAct, &QAction::triggered, this, &main_window::ExtractTar); + connect(ui->showDebuggerAct, &QAction::triggered, this, [this](bool checked) { checked ? m_debugger_frame->show() : m_debugger_frame->hide(); diff --git a/rpcs3/rpcs3qt/main_window.h b/rpcs3/rpcs3qt/main_window.h index e3287dd9a9..c40b636c40 100644 --- a/rpcs3/rpcs3qt/main_window.h +++ b/rpcs3/rpcs3qt/main_window.h @@ -144,8 +144,10 @@ private: void HandlePackageInstallation(QStringList file_paths); void InstallPup(QString filePath = ""); - void HandlePupInstallation(QString file_path = ""); + void ExtractPup(); + void HandlePupInstallation(QString file_path, QString dir_path = ""); + void ExtractTar(); void ExtractMSELF(); drop_type IsValidFile(const QMimeData& md, QStringList* drop_paths = nullptr); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index 84c881e6aa..dae1f3c1a2 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -256,7 +256,10 @@ + + + @@ -634,6 +637,19 @@ Extract MSELF + + + Extract PUP + + + + + Extract Encrypted TAR + + + Extract files from special .tar files inside PS3UPDAT.PUP + + true