From a5513bd7e68cab1dff78809f424ce450a4d329de Mon Sep 17 00:00:00 2001 From: Billy Laws Date: Mon, 29 Jun 2020 19:23:33 +0100 Subject: [PATCH] Implement an NCA parser and loader Nintendo Content Archives are used to store the assets, executables and updates of applications. They support holding either a PFS0 or a RomFS. An NCA's ExeFS can be loaded by placing each NSO sequentially into memory, starting with rtld which will link them together. Currently only decrypted NCAs are supported, encryption and BKTR handling will be added at a later time. --- app/CMakeLists.txt | 2 + app/src/main/cpp/loader_jni.cpp | 5 +- app/src/main/cpp/skyline/kernel/memory.h | 2 + app/src/main/cpp/skyline/loader/loader.h | 1 + app/src/main/cpp/skyline/loader/nca.cpp | 47 ++++ app/src/main/cpp/skyline/loader/nca.h | 30 +++ app/src/main/cpp/skyline/os.cpp | 3 + app/src/main/cpp/skyline/vfs/nca.cpp | 53 +++++ app/src/main/cpp/skyline/vfs/nca.h | 213 ++++++++++++++++++ app/src/main/java/emu/skyline/MainActivity.kt | 1 + .../main/java/emu/skyline/loader/RomFile.kt | 5 +- 11 files changed, 359 insertions(+), 3 deletions(-) create mode 100644 app/src/main/cpp/skyline/loader/nca.cpp create mode 100644 app/src/main/cpp/skyline/loader/nca.h create mode 100644 app/src/main/cpp/skyline/vfs/nca.cpp create mode 100644 app/src/main/cpp/skyline/vfs/nca.h diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index 2babc86f..15d65623 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(skyline SHARED ${source_DIR}/skyline/loader/loader.cpp ${source_DIR}/skyline/loader/nro.cpp ${source_DIR}/skyline/loader/nso.cpp + ${source_DIR}/skyline/loader/nca.cpp ${source_DIR}/skyline/kernel/memory.cpp ${source_DIR}/skyline/kernel/ipc.cpp ${source_DIR}/skyline/kernel/svc.cpp @@ -104,6 +105,7 @@ add_library(skyline SHARED ${source_DIR}/skyline/vfs/rom_filesystem.cpp ${source_DIR}/skyline/vfs/os_backing.cpp ${source_DIR}/skyline/vfs/nacp.cpp + ${source_DIR}/skyline/vfs/nca.cpp ) target_link_libraries(skyline vulkan android fmt tinyxml2 oboe lz4_static) diff --git a/app/src/main/cpp/loader_jni.cpp b/app/src/main/cpp/loader_jni.cpp index aa83cff0..c1ce0f6a 100644 --- a/app/src/main/cpp/loader_jni.cpp +++ b/app/src/main/cpp/loader_jni.cpp @@ -4,6 +4,7 @@ #include "skyline/vfs/os_backing.h" #include "skyline/loader/nro.h" #include "skyline/loader/nso.h" +#include "skyline/loader/nca.h" #include "skyline/jvm.h" extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JNIEnv *env, jobject thiz, jint jformat, jint fd) { @@ -17,6 +18,8 @@ extern "C" JNIEXPORT jlong JNICALL Java_emu_skyline_loader_RomFile_initialize(JN return reinterpret_cast(new skyline::loader::NroLoader(backing)); case skyline::loader::RomFormat::NSO: return reinterpret_cast(new skyline::loader::NsoLoader(backing)); + case skyline::loader::RomFormat::NCA: + return reinterpret_cast(new skyline::loader::NcaLoader(backing)); default: return 0; } @@ -52,4 +55,4 @@ extern "C" JNIEXPORT jstring JNICALL Java_emu_skyline_loader_RomFile_getApplicat extern "C" JNIEXPORT void JNICALL Java_emu_skyline_loader_RomFile_destroy(JNIEnv *env, jobject thiz, jlong instance) { delete reinterpret_cast(instance); -} \ No newline at end of file +} diff --git a/app/src/main/cpp/skyline/kernel/memory.h b/app/src/main/cpp/skyline/kernel/memory.h index 3fb091cd..1d90955c 100644 --- a/app/src/main/cpp/skyline/kernel/memory.h +++ b/app/src/main/cpp/skyline/kernel/memory.h @@ -213,6 +213,7 @@ namespace skyline { namespace loader { class NroLoader; class NsoLoader; + class NcaLoader; } namespace kernel { @@ -324,6 +325,7 @@ namespace skyline { friend class type::KProcess; friend class loader::NroLoader; friend class loader::NsoLoader; + friend class loader::NcaLoader; friend void svc::SetMemoryAttribute(DeviceState &state); diff --git a/app/src/main/cpp/skyline/loader/loader.h b/app/src/main/cpp/skyline/loader/loader.h index 1fe4baf1..9ed7df7b 100644 --- a/app/src/main/cpp/skyline/loader/loader.h +++ b/app/src/main/cpp/skyline/loader/loader.h @@ -15,6 +15,7 @@ namespace skyline::loader { enum class RomFormat { NRO, //!< The NRO format: https://switchbrew.org/wiki/NRO NSO, //!< The NSO format: https://switchbrew.org/wiki/NSO + NCA, //!< The NCA format: https://switchbrew.org/wiki/NCA XCI, //!< The XCI format: https://switchbrew.org/wiki/XCI NSP, //!< The NSP format from "nspwn" exploit: https://switchbrew.org/wiki/Switch_System_Flaws }; diff --git a/app/src/main/cpp/skyline/loader/nca.cpp b/app/src/main/cpp/skyline/loader/nca.cpp new file mode 100644 index 00000000..d61d11b3 --- /dev/null +++ b/app/src/main/cpp/skyline/loader/nca.cpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include +#include +#include +#include "nso.h" +#include "nca.h" + +namespace skyline::loader { + NcaLoader::NcaLoader(const std::shared_ptr &backing) : nca(backing) { + if (nca.exeFs == nullptr) + throw exception("Only NCAs with an ExeFS can be loaded directly"); + } + + void NcaLoader::LoadExeFs(const std::shared_ptr &exeFs, const std::shared_ptr process, const DeviceState &state) { + if (exeFs == nullptr) + throw exception("Cannot load a null ExeFS"); + + auto nsoFile = exeFs->OpenFile("rtld"); + if (nsoFile == nullptr) + throw exception("Cannot load an ExeFS that doesn't contain rtld"); + + auto loadInfo = NsoLoader::LoadNso(nsoFile, process, state); + u64 offset = loadInfo.size; + u64 base = loadInfo.base; + + state.logger->Info("Loaded nso 'rtld' at 0x{:X}", base); + + for (const auto &nso : {"main", "subsdk0", "subsdk1", "subsdk2", "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "sdk"}) { + nsoFile = exeFs->OpenFile(nso); + + if (nsoFile == nullptr) + continue; + + loadInfo = NsoLoader::LoadNso(nsoFile, process, state, offset); + state.logger->Info("Loaded nso '{}' at 0x{:X}", nso, base + offset); + offset += loadInfo.size; + } + + state.os->memory.InitializeRegions(base, offset, memory::AddressSpaceType::AddressSpace39Bit); + } + + void NcaLoader::LoadProcessData(const std::shared_ptr process, const DeviceState &state) { + LoadExeFs(nca.exeFs, process, state); + } +} diff --git a/app/src/main/cpp/skyline/loader/nca.h b/app/src/main/cpp/skyline/loader/nca.h new file mode 100644 index 00000000..71bcd90c --- /dev/null +++ b/app/src/main/cpp/skyline/loader/nca.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include +#include +#include "loader.h" + +namespace skyline::loader { + /** + * @brief The NcaLoader class allows loading an NCA's ExeFS through the Loader interface (https://switchbrew.org/wiki/NSO) + */ + class NcaLoader : public Loader { + private: + vfs::NCA nca; //!< The backing NCA of the loader + + public: + NcaLoader(const std::shared_ptr &backing); + + /** + * @brief This loads an ExeFS into memory + * @param exefs A filesystem object containing the ExeFS filesystem to load into memory + * @param process The process to load the ExeFS into + */ + static void LoadExeFs(const std::shared_ptr &exefs, const std::shared_ptr process, const DeviceState &state); + + void LoadProcessData(const std::shared_ptr process, const DeviceState &state); + }; +} diff --git a/app/src/main/cpp/skyline/os.cpp b/app/src/main/cpp/skyline/os.cpp index d6916a5c..6cbb662a 100644 --- a/app/src/main/cpp/skyline/os.cpp +++ b/app/src/main/cpp/skyline/os.cpp @@ -4,6 +4,7 @@ #include "vfs/os_backing.h" #include "loader/nro.h" #include "loader/nso.h" +#include "loader/nca.h" #include "nce/guest.h" #include "os.h" @@ -17,6 +18,8 @@ namespace skyline::kernel { state.loader = std::make_shared(romFile); } else if (romType == loader::RomFormat::NSO) { state.loader = std::make_shared(romFile); + } else if (romType == loader::RomFormat::NCA) { + state.loader = std::make_shared(romFile); } else { throw exception("Unsupported ROM extension."); } diff --git a/app/src/main/cpp/skyline/vfs/nca.cpp b/app/src/main/cpp/skyline/vfs/nca.cpp new file mode 100644 index 00000000..fec41842 --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/nca.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#include "region_backing.h" +#include "partition_filesystem.h" +#include "nca.h" +#include "rom_filesystem.h" +#include "directory.h" + +namespace skyline::vfs { + NCA::NCA(const std::shared_ptr &backing) : backing(backing) { + backing->Read(&header); + + if (header.magic != util::MakeMagic("NCA3")) + throw exception("Attempting to load an encrypted or invalid NCA"); + + contentType = header.contentType; + + for (size_t i = 0; i < header.sectionHeaders.size(); i++) { + auto §ionHeader = header.sectionHeaders.at(i); + auto §ionEntry = header.fsEntries.at(i); + + if (sectionHeader.fsType == NcaSectionFsType::PFS0 && sectionHeader.hashType == NcaSectionHashType::HierarchicalSha256) + ReadPfs0(sectionHeader, sectionEntry); + else if (sectionHeader.fsType == NcaSectionFsType::RomFs && sectionHeader.hashType == NcaSectionHashType::HierarchicalIntegrity) + ReadRomFs(sectionHeader, sectionEntry); + } + } + + void NCA::ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry) { + size_t offset = static_cast(entry.startOffset) * constant::MediaUnitSize + header.sha256HashInfo.pfs0Offset; + size_t size = constant::MediaUnitSize * static_cast(entry.endOffset - entry.startOffset); + + auto pfs = std::make_shared(std::make_shared(backing, offset, size)); + + if (contentType == NcaContentType::Program) { + // An ExeFS must always contain an NPDM and a main NSO, whereas the logo section will always contain a logo and a startup movie + if (pfs->FileExists("main") && pfs->FileExists("main.npdm")) + exeFs = std::move(pfs); + else if (pfs->FileExists("NintendoLogo.png") && pfs->FileExists("StartupMovie.gif")) + logo = std::move(pfs); + } else if (contentType == NcaContentType::Meta) { + cnmt = std::move(pfs); + } + } + + void NCA::ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry) { + size_t offset = static_cast(entry.startOffset) * constant::MediaUnitSize + header.integrityHashInfo.levels.back().offset; + size_t size = header.integrityHashInfo.levels.back().size; + + romFs = std::make_shared(backing, offset, size); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/skyline/vfs/nca.h b/app/src/main/cpp/skyline/vfs/nca.h new file mode 100644 index 00000000..4e12b74d --- /dev/null +++ b/app/src/main/cpp/skyline/vfs/nca.h @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + +#pragma once + +#include +#include "filesystem.h" + +namespace skyline { + namespace constant { + constexpr size_t MediaUnitSize = 0x200; //!< The unit size of entries in an NCA + } + + namespace vfs { + /** + * @brief This enumerates the various content types of an NCA + */ + enum class NcaContentType : u8 { + Program = 0x0, //!< This is a program NCA + Meta = 0x1, //!< This is a metadata NCA + Control = 0x2, //!< This is a control NCA + Manual = 0x3, //!< This is a manual NCA + Data = 0x4, //!< This is a data NCA + PublicData = 0x5, //!< This is a public data NCA + }; + + /** + * @brief The NCA class provides an easy way to access the contents of an NCA file (https://switchbrew.org/wiki/NCA_Format) + */ + class NCA { + private: + /** + * @brief This enumerates the distribution types of an NCA + */ + enum class NcaDistributionType : u8 { + System = 0x0, //!< This NCA was distributed on the EShop or is part of the system + GameCard = 0x1, //!< This NCA was distributed on a GameCard + }; + + /** + * @brief This enumerates the key generation version in NCAs before HOS 3.0.1 + */ + enum class NcaLegacyKeyGenerationType : u8 { + Fw100 = 0x0, //!< 1.0.0 + Fw300 = 0x2, //!< 3.0.0 + }; + + /** + * @brief This enumerates the key generation version in NCAs after HOS 3.0.0 + */ + enum class NcaKeyGenerationType : u8 { + Fw301 = 0x3, //!< 3.0.1 + Fw400 = 0x4, //!< 4.0.0 + Fw500 = 0x5, //!< 5.0.0 + Fw600 = 0x6, //!< 6.0.0 + Fw620 = 0x7, //!< 6.2.0 + Fw700 = 0x8, //!< 7.0.0 + Fw810 = 0x9, //!< 8.1.0 + Fw900 = 0xa, //!< 9.0.0 + Fw910 = 0xb, //!< 9.1.0 + Invalid = 0xff, //!< An invalid key generation type + }; + + /** + * @brief This enumerates the key area encryption key types + */ + enum class NcaKeyAreaEncryptionKeyType : u8 { + Application = 0x0, //!< This NCA uses the application key encryption area + Ocean = 0x1, //!< This NCA uses the ocean key encryption area + System = 0x2, //!< This NCA uses the system key encryption area + }; + + /** + * @brief This hold the entry of a single filesystem in an NCA + */ + struct NcaFsEntry { + u32 startOffset; //!< The start offset of the filesystem in units of 0x200 bytes + u32 endOffset; //!< The start offset of the filesystem in units of 0x200 bytes + u64 _pad_; + }; + + /** + * @brief This enumerates the possible filesystem types a section of an NCA can contain + */ + enum class NcaSectionFsType : u8 { + RomFs = 0x0, //!< This section contains a RomFs filesystem + PFS0 = 0x1, //!< This section contains a PFS0 filesystem + }; + + /** + * @brief This enumerates the possible hash header types of an NCA section + */ + enum class NcaSectionHashType : u8 { + HierarchicalSha256 = 0x2, //!< The hash header for this section is that of a PFS0 + HierarchicalIntegrity = 0x3, //!< The hash header for this section is that of a RomFS + }; + + /** + * @brief This enumerates the possible encryption types of an NCA section + */ + enum class NcaSectionEncryptionType : u8 { + None = 0x1, //!< This NCA doesn't use any encryption + XTS = 0x2, //!< This NCA uses AES-XTS encryption + CTR = 0x3, //!< This NCA uses AES-CTR encryption + BKTR = 0x4, //!< This NCA uses BKTR together AES-CTR encryption + }; + + /** + * @brief This holds the data for a single level of the hierarchical integrity scheme + */ + struct HierarchicalIntegrityLevel { + u64 offset; //!< The offset of the level data + u64 size; //!< The size of the level data + u32 blockSize; //!< The block size of the level data + u32 _pad_; + }; + static_assert(sizeof(HierarchicalIntegrityLevel) == 0x18); + + /** + * @brief This holds the hash info header of the hierarchical integrity scheme + */ + struct HierarchicalIntegrityHashInfo { + u32 magic; //!< The hierarchical integrity magic, 'IVFC' + u32 magicNumber; //!< The magic number 0x2000 + u32 masterHashSize; //!< The size of the master hash + u32 numLevels; //!< The number of levels + std::array levels; //!< An array of the hierarchical integrity levels + u8 _pad0_[0x20]; + std::array masterHash; //!< The master hash of the hierarchical integrity system + u8 _pad1_[0x18]; + }; + static_assert(sizeof(HierarchicalIntegrityHashInfo) == 0xf8); + + /** + * @brief This holds the hash info header of the SHA256 hashing scheme for PFS0 + */ + struct HierarchicalSha256HashInfo { + std::array hashTableHash; //!< A SHA256 hash over the hash table + u32 blockSize; //!< The block size of the filesystem + u32 _pad_; + u64 hashTableOffset; //!< The offset from the end of the section header of the hash table + u64 hashTableSize; //!< The size of the hash table + u64 pfs0Offset; //!< The offset from the end of the section header of the PFS0 + u64 pfs0Size; //!< The size of the PFS0 + u8 _pad1_[0xb0]; + }; + static_assert(sizeof(HierarchicalSha256HashInfo) == 0xf8); + + /** + * @brief This holds the header of each specific section in an NCA + */ + struct NcaSectionHeader { + u16 version; //!< The version, always 2 + NcaSectionFsType fsType; //!< The type of the filesystem in the section + NcaSectionHashType hashType; //!< The type of hash header that is used for this section + NcaSectionEncryptionType encryptionType; //!< The type of encryption that is used for this section + u8 _pad0_[0x3]; + union { + HierarchicalIntegrityHashInfo integrityHashInfo; //!< The HashInfo used for RomFS + HierarchicalSha256HashInfo sha256HashInfo; //!< The HashInfo used for PFS0 + }; + u8 _pad1_[0x40]; // PatchInfo + u32 generation; //!< The generation of the NCA section + u32 secureValue; //!< The secure value of the section + u8 _pad2_[0x30]; //!< SparseInfo + u8 _pad3_[0x88]; + }; + static_assert(sizeof(NcaSectionHeader) == 0x200); + + /** + * @brief This struct holds the header of a Nintendo Content Archive + */ + struct NcaHeader { + std::array fixed_key_sig; //!< An RSA-PSS signature over the header with fixed key + std::array npdm_key_sig; //!< An RSA-PSS signature over header with key in NPDM + u32 magic; //!< The magic of the NCA: 'NCA3' + NcaDistributionType distributionType; //!< Whether this NCA is from a gamecard or the E-Shop + NcaContentType contentType; //!< The content type of the NCA + NcaLegacyKeyGenerationType legacyKeyGenerationType; //!< The keyblob to use for decryption + NcaKeyAreaEncryptionKeyType keyAreaEncryptionKeyType; //!< The index of the key area encryption key that is needed + u64 size; //!< The total size of the NCA + u64 programId; //!< The program ID of the NCA + u32 contentIndex; //!< The index of the content + u32 sdkVersion; //!< The version of the SDK the NCA was built with + NcaKeyGenerationType keyGenerationType; //!< The keyblob to use for decryption + u8 fixedKeyGeneration; //!< The fixed key index + u8 _pad0_[0xe]; + std::array rightsId; //!< The NCA's rights ID + std::array fsEntries; //!< The filesystem entries for this NCA + std::array, 4> sectionHashes; //!< This contains SHA-256 hashes for each filesystem header + std::array, 4> encryptedKeyArea; //!< The encrypted key area for each filesystem + u8 _pad1_[0xc0]; + std::array sectionHeaders; + } header{}; + static_assert(sizeof(NcaHeader) == 0xc00); + + std::shared_ptr backing; //!< The backing for the NCA + + void ReadPfs0(const NcaSectionHeader &header, const NcaFsEntry &entry); + + void ReadRomFs(const NcaSectionHeader &header, const NcaFsEntry &entry); + + public: + std::shared_ptr exeFs; //!< The PFS0 filesystem for this NCA's ExeFS section + std::shared_ptr logo; //!< The PFS0 filesystem for this NCA's logo section + std::shared_ptr cnmt; //!< The PFS0 filesystem for this NCA's CNMT section + std::shared_ptr romFs; //!< The backing for this NCA's RomFS section + NcaContentType contentType; //!< The content type of the NCA + + NCA(const std::shared_ptr &backing); + }; + } +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index 0784da0c..239ce437 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -113,6 +113,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) + foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) runOnUiThread { if (!foundRoms) diff --git a/app/src/main/java/emu/skyline/loader/RomFile.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt index 7ad9d920..fd869a55 100644 --- a/app/src/main/java/emu/skyline/loader/RomFile.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -25,8 +25,9 @@ import java.util.* enum class RomFormat(val format: Int){ NRO(0), NSO(1), - XCI(2), - NSP(3), + NCA(2), + XCI(3), + NSP(4), } /**