From 6edcde25c3f3c5f26afccd96525b9166bc348499 Mon Sep 17 00:00:00 2001 From: Edoardo Lolletti Date: Tue, 19 Aug 2025 18:18:24 +0200 Subject: [PATCH] Add sanitization to loaded GIF images Parse the provided loaded background images and reconstruct a new GIF file on the fly to ensure they don't contain any extension that might not be supported by unalunch. The generated GIF file will have a global color table, and single section, being the image descriptor. It also won't have any looping block. --- arm9/src/bgMenu.cpp | 25 +++-- arm9/src/bgMenu.h | 11 +- arm9/src/gifConverter.cpp | 225 ++++++++++++++++++++++++++++++++++++++ arm9/src/gifConverter.h | 12 ++ arm9/src/main.cpp | 18 +-- arm9/src/unlaunch.cpp | 54 +++------ arm9/src/unlaunch.h | 5 +- 7 files changed, 282 insertions(+), 68 deletions(-) create mode 100644 arm9/src/gifConverter.cpp create mode 100644 arm9/src/gifConverter.h diff --git a/arm9/src/bgMenu.cpp b/arm9/src/bgMenu.cpp index fbf7cfc..dc916a7 100644 --- a/arm9/src/bgMenu.cpp +++ b/arm9/src/bgMenu.cpp @@ -1,13 +1,19 @@ #include "bgMenu.h" #include "main.h" #include "menu.h" +#include "gifConverter.h" +#include "message.h" #include +#include +#include #include #include #include #include +static std::array currentlyLoadedGif; + static const auto& getBackgroundList() { static auto bgs = []{ @@ -39,7 +45,7 @@ static const auto& getBackgroundList() return bgs; } -const char* backgroundMenu() +std::span backgroundMenu() { //top screen clearScreen(&topScreen); @@ -79,12 +85,17 @@ const char* backgroundMenu() } } - const char* result = nullptr; - if(static_cast(m->cursor) < bgs.size()) - result = bgs[m->cursor].second.data(); - else if(static_cast(m->cursor) == bgs.size()) - result = "default"; + auto selection = static_cast(m->cursor); freeMenu(m); - return result; + if(selection < bgs.size()) { + try { + return parseGif(bgs[selection].second.data(), currentlyLoadedGif); + } catch(const std::exception& e) { + messageBox(std::format("\x1B[31mError:\x1B[33m The image could not\n" + "be loaded: {}", e.what()).data()); + } + } + + return {}; } \ No newline at end of file diff --git a/arm9/src/bgMenu.h b/arm9/src/bgMenu.h index 1ad40d9..96bb4d4 100644 --- a/arm9/src/bgMenu.h +++ b/arm9/src/bgMenu.h @@ -1,14 +1,9 @@ #ifndef BGMENU_H #define BGMENU_H -#ifdef __cplusplus -extern "C" { -#endif +#include +#include -const char* backgroundMenu(); - -#ifdef __cplusplus -} -#endif +std::span backgroundMenu(); #endif \ No newline at end of file diff --git a/arm9/src/gifConverter.cpp b/arm9/src/gifConverter.cpp new file mode 100644 index 0000000..850ab90 --- /dev/null +++ b/arm9/src/gifConverter.cpp @@ -0,0 +1,225 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "storage.h" +#include "unlaunch.h" + +struct Gif { + struct Header { + char signature[6]; + uint16_t width; + uint16_t height; + uint8_t gctSize: 3; + uint8_t sortFlag: 1; + uint8_t colorResolution: 3; + uint8_t gctFlag: 1; + uint8_t bgColor; + uint8_t pixelAspectRatio; + } __attribute__ ((__packed__)) header; + static_assert(sizeof(Header) == 13); + + std::array colorTable; + size_t numColors; + + struct Frame { + struct Descriptor { + uint16_t x; + uint16_t y; + uint16_t w; + uint16_t h; + uint8_t lctSize: 3; + uint8_t reserved: 2; + uint8_t sortFlag: 1; + uint8_t interlaceFlag: 1; + uint8_t lctFlag: 1; + } __attribute__ ((__packed__)) descriptor; + static_assert(sizeof(Descriptor) == 9); + + struct Image { + uint8_t lzwMinimumCodeSize; + std::vector> imageDataChunks; + } image; + } frame; +}; + +static constexpr auto WIDTH = 256; +static constexpr auto HEIGHT = 192; + +Gif getGif(std::string_view path) { + Gif gif; + auto file = fopen(path.data(), "rb"); + if (!file) + throw std::runtime_error("Failed to open file"); + + auto fileSptr = std::shared_ptr(file, fclose); + + if(auto size = getFileSize(file); size < 7 || size > MAX_GIF_SIZE) + { + throw std::runtime_error("Gif file too big.\n"); + } + + auto& header = gif.header; + + // Read header + fread(&header, 1, sizeof(header), file); + + // Check that this is a GIF + if (memcmp(header.signature, "GIF87a", sizeof(header.signature)) != 0 && memcmp(header.signature, "GIF89a", sizeof(header.signature)) != 0) { + throw std::runtime_error("File not a gif"); + } + + if(header.width != WIDTH || header.height != HEIGHT) { + throw std::runtime_error("Invalid gif size"); + } + + auto& numColors = gif.numColors; + auto& color_table = gif.colorTable; + // Load global color table + if (header.gctFlag) { + numColors = (2 << header.gctSize); + fread(color_table.data(), 1, numColors * 3, file); + } + + auto& frame = gif.frame; + bool gotImage = false; + while (1) { + switch (fgetc(file)) { + case 0x21: { // Extension + switch (fgetc(file)) { + case 0xF9: { // Graphics Control + uint8_t toRead = fgetc(file); + fseek(file, toRead, SEEK_CUR); + fgetc(file); // Terminator + break; + } + case 0x01: { // Plain text + throw std::runtime_error("Plain text found"); +#if 0 + fseek(file, 12, SEEK_CUR); + while (uint8_t size = fgetc(file)) { + fseek(file, size, SEEK_CUR); + } + break; +#endif + } + case 0xFF: { // Application extension + throw std::runtime_error("Application extension found"); +#if 0 + if (fgetc(file) == 0xB) { + char buffer[0xC] = {0}; + fread(buffer, 1, 0xB, file); + if (strcmp(buffer, "NETSCAPE2.0") == 0) { // Check for Netscape loop count + fseek(file, 2, SEEK_CUR); + fseek(file, 2, SEEK_CUR); + fgetc(file); //terminator + break; + } + } + [[fallthrough]]; +#endif + } + case 0xFE: { // Comment + // Skip comments and unsupported application extionsions + while (uint8_t size = fgetc(file)) { + fseek(file, size, SEEK_CUR); + } + break; + } + default: { + throw std::runtime_error("Unknown GIF extension found"); + } + } + break; + } case 0x2C: { // Image descriptor + gotImage = true; + fread(&frame.descriptor, 1, sizeof(frame.descriptor), file); + if (frame.descriptor.lctFlag) { + header.gctFlag = 1; + header.gctSize = frame.descriptor.lctSize; + header.sortFlag = frame.descriptor.sortFlag; + numColors = 2 << header.gctSize; + fread(color_table.data(), 1, numColors * 3, file); + frame.descriptor.lctFlag = 0; + frame.descriptor.lctSize = 0; + frame.descriptor.sortFlag = 0; + } + + if(frame.descriptor.w != WIDTH || frame.descriptor.h != HEIGHT) { + throw std::runtime_error("Wrong frame size"); + } + + if(frame.descriptor.x != 0 || frame.descriptor.y != 0) { + throw std::runtime_error("Wrong frame coordinates"); + } + + frame.image.lzwMinimumCodeSize = fgetc(file); + while (uint8_t size = fgetc(file)) { + std::vector dataChunk; + dataChunk.resize(size); + fread(dataChunk.data(), 1, size, file); + frame.image.imageDataChunks.push_back(std::move(dataChunk)); + } + + goto breakWhile; + } case 0x3B: { // Trailer + goto breakWhile; + } + } + if(feof(file)){ + throw std::runtime_error("Unexpected file termination"); + } + } +breakWhile: + if(!gotImage){ + throw std::runtime_error("Image data not found in gif"); + } + if(!header.gctFlag) { + throw std::runtime_error("Invalid gif (missing color table)"); + } + return gif; +} + +std::span writeGif(const Gif& gif, std::array& outArr) { + size_t totalWritten = 0; + auto writeArr = [&, ptr = outArr.data()](const void* data, size_t len) mutable { + if((totalWritten + len) > MAX_GIF_SIZE) + throw std::runtime_error("Gif too big"); + memcpy(ptr, data, len); + ptr += len; + totalWritten += len; + }; + auto writeCh = [&](char ch) mutable { + writeArr(&ch, 1); + }; + + writeArr(&gif.header, sizeof(gif.header)); + writeArr(gif.colorTable.data(), gif.numColors * 3); + + // write single image descriptor + writeCh(0x2C); + writeArr(&gif.frame.descriptor, sizeof(gif.frame.descriptor)); + + // write image data + writeCh(gif.frame.image.lzwMinimumCodeSize); + for(const auto& chunk : gif.frame.image.imageDataChunks){ + writeCh(chunk.size()); + writeArr(chunk.data(), chunk.size()); + } + writeCh('\0'); + + // write trailer + writeCh(0x3B); + return std::span{outArr.data(), totalWritten}; +} + + +std::span parseGif(const char* path, std::array& outArr) { + const auto gif = getGif(path); + return writeGif(gif, outArr); +} diff --git a/arm9/src/gifConverter.h b/arm9/src/gifConverter.h new file mode 100644 index 0000000..eb6eaf9 --- /dev/null +++ b/arm9/src/gifConverter.h @@ -0,0 +1,12 @@ +#ifndef GIF_CONVERTER_H +#define GIF_CONVERTER_H + +#include +#include +#include + +#include "unlaunch.h" + +std::span parseGif(const char* path, std::array& outArr); + +#endif //GIF_CONVERTER_H \ No newline at end of file diff --git a/arm9/src/main.cpp b/arm9/src/main.cpp index 848ecc0..4969062 100644 --- a/arm9/src/main.cpp +++ b/arm9/src/main.cpp @@ -26,7 +26,7 @@ static UNLAUNCH_VERSION foundUnlaunchInstallerVersion = INVALID; static bool disableAllPatches = false; static bool enableSoundAndSplash = false; static const char* splashSoundBinaryPatchPath = NULL; -static const char* customBgPath = NULL; +static std::span customBgSpan{}; volatile bool charging = false; volatile u8 batteryLevel = 0; static bool advancedOptionsUnlocked = false; @@ -590,7 +590,7 @@ void install(consoleInfo& info) { } if(installUnlaunch(info, disableAllPatches, enableSoundAndSplash ? splashSoundBinaryPatchPath : NULL, - customBgPath)) + customBgSpan)) { messageBox("Install successful!\n"); info.tmdGood = false; @@ -615,19 +615,7 @@ void customBg() { { return; } - const char* customBg = backgroundMenu(); - if(!customBg) - { - return; - } - if(strcmp(customBg, "default") == 0) - { - customBgPath = NULL; - } - else - { - customBgPath = customBg; - } + customBgSpan = backgroundMenu(); } void doMainMenu(consoleInfo& info) { diff --git a/arm9/src/unlaunch.cpp b/arm9/src/unlaunch.cpp index f2137d5..456fcce 100644 --- a/arm9/src/unlaunch.cpp +++ b/arm9/src/unlaunch.cpp @@ -3,10 +3,12 @@ #include "storage.h" #include "tonccpy.h" #include "unlaunch.h" -#include + #include -#include -#include + +#include +#include +#include #include #include #include @@ -425,38 +427,19 @@ static bool verifyUnlaunchInstaller(void) return true; } -static bool patchCustomBackground(const char* customBackgroundPath) +static bool patchCustomBackground(std::span customBackground) { - if(!customBackgroundPath) + auto size = customBackground.size(); + if(size == 0) { return true; } - auto bgGif = fopen(customBackgroundPath, "rb"); - if(!bgGif) + if(size < 7 || size > MAX_GIF_SIZE) { - messageBox("\x1B[31mError:\x1B[33m Failed to open custom bg gif.\n"); - return false; - } - auto size = getFileSize(bgGif); - if(size < 7 || size > 0x3C70) - { - messageBox("\x1B[31mError:\x1B[33m Invalid gif file.\n"); - fclose(bgGif); - return false; - } - u16 gifWidth; - u16 gifHeight; - if((fseek(bgGif, 6, SEEK_SET) != 0) || (fread(&gifWidth, 1, sizeof(u16), bgGif) != sizeof(u16)) || (fread(&gifHeight, 1, sizeof(u16), bgGif) != sizeof(u16))) - { - messageBox("\x1B[31mError:\x1B[33m Failed to parse gif file.\n"); - fclose(bgGif); - return false; - } - if (gifWidth != 256 || gifHeight != 192) { - messageBox("\x1B[31mError:\x1B[33m Gif file has invalid dimensions.\n"); - fclose(bgGif); + messageBox("\x1B[31mError:\x1B[33m Gif file too big.\n"); return false; } + const u32 gifSignatureStart = 0x38464947; const u32 gifSignatureEnd = 0x3B000044; @@ -468,14 +451,11 @@ static bool patchCustomBackground(const char* customBackgroundPath) if(*gifStart != gifSignatureStart || *gifEnd != gifSignatureEnd) { messageBox("\x1B[31mError:\x1B[33m Gif offsets not matching.\n"); - fclose(bgGif); return false; } - fseek(bgGif, 0, SEEK_SET); - - //read the whole file, could be less than 0x3C70, but unlaunch should then just ignore the leftover data - fread(gifStart, 1, 0x3C70, bgGif); + + std::memcpy(gifStart, customBackground.data(), size); return true; } @@ -507,7 +487,7 @@ static bool applyBinaryPatch(const char* path) return true; } -static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath) +static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span customBackground) { tonccpy(unlaunchInstallerBuffer, ogUnlaunchInstallerBuffer, sizeof(unlaunchInstallerBuffer)); if (splashSoundBinaryPatchPath) @@ -530,7 +510,7 @@ static bool patchUnlaunchInstaller(bool disableAllPatches, const char* splashSou return false; } } - if(!patchCustomBackground(customBackgroundPath)) + if(!patchCustomBackground(customBackground)) { return false; } @@ -559,9 +539,9 @@ const char* getUnlaunchVersionString(UNLAUNCH_VERSION version) return unlaunchVersionStrings[version]; } -bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath) +bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span customBackground) { - if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackgroundPath)) + if (installerVersion == INVALID || !patchUnlaunchInstaller(disableAllPatches, splashSoundBinaryPatchPath, customBackground)) return false; // Treat protos differently diff --git a/arm9/src/unlaunch.h b/arm9/src/unlaunch.h index f2443a1..bc88cc6 100644 --- a/arm9/src/unlaunch.h +++ b/arm9/src/unlaunch.h @@ -1,6 +1,7 @@ #ifndef UNLAUNCH_H #define UNLAUNCH_H #include +#include #include "consoleInfo.h" @@ -9,10 +10,12 @@ typedef enum UNLAUNCH_VERSION { INVALID, } UNLAUNCH_VERSION; +static constexpr auto MAX_GIF_SIZE = 0x3C70; + const char* getUnlaunchVersionString(UNLAUNCH_VERSION); bool uninstallUnlaunch(const consoleInfo& info, bool removeHNAABackup); -bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, const char* customBackgroundPath); +bool installUnlaunch(const consoleInfo& info, bool disableAllPatches, const char* splashSoundBinaryPatchPath, std::span customBackground); UNLAUNCH_VERSION loadUnlaunchInstaller(std::string_view path);