diff --git a/fusee/fusee-secondary/src/ips.c b/fusee/fusee-secondary/src/ips.c new file mode 100644 index 000000000..46747e5a0 --- /dev/null +++ b/fusee/fusee-secondary/src/ips.c @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2018 Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include "utils.h" +#include "se.h" +#include "ips.h" + +/* IPS Patching adapted from Luma3DS (https://github.com/AuroraWright/Luma3DS/blob/master/sysmodules/loader/source/patcher.c) */ + +#define IPS_MAGIC "PATCH" +#define IPS_TAIL "EOF" + +#define IPS32_MAGIC "IPS32" +#define IPS32_TAIL "EEOF" + +/* Applies an IPS/IPS32 patch to memory, disregarding writes to the first prot_size bytes. */ +static void apply_ips_patch(uint8_t *mem, size_t mem_size, size_t prot_size, bool is_ips32, FILE *f_ips) { + uint8_t buffer[4]; + while (fread(buffer, is_ips32 ? 4 : 3, 1, f_ips) == 1) { + if (is_ips32 && memcmp(buffer, IPS32_TAIL, 4) == 0) { + break; + } else if (!is_ips32 && memcmp(buffer, IPS_TAIL, 3) == 0) { + break; + } + + /* Offset of patch. */ + uint32_t patch_offset; + if (is_ips32) { + patch_offset = (buffer[0] << 24) | (buffer[1] << 16) | (buffer[2] << 8) | buffer[3]; + } else { + patch_offset = (buffer[0] << 16) | (buffer[1] << 8) | (buffer[2]); + } + + /* Size of patch. */ + if (fread(buffer, 2, 1, f_ips) != 1) { + break; + } + uint32_t patch_size = (buffer[0] << 8) | (buffer[1]); + + /* Check for RLE encoding. */ + if (patch_size == 0) { + /* Size of RLE. */ + if (fread(buffer, 2, 1, f_ips) != 1) { + break; + } + + uint32_t rle_size = (buffer[0] << 8) | (buffer[1]); + + /* Value for RLE. */ + if (fread(buffer, 1, 1, f_ips) != 1) { + break; + } + + if (patch_offset < prot_size) { + if (patch_offset + rle_size > prot_size) { + uint32_t diff = prot_size - patch_offset; + patch_offset += diff; + rle_size -= diff; + goto IPS_RLE_PATCH_OFFSET_WITHIN_BOUNDS; + } + } else { + IPS_RLE_PATCH_OFFSET_WITHIN_BOUNDS: + patch_offset -= prot_size; + if (patch_offset + rle_size > mem_size) { + rle_size = mem_size - patch_offset; + } + memset(mem + patch_offset, buffer[0], rle_size); + } + } else { + if (patch_offset < prot_size) { + if (patch_offset + patch_size > prot_size) { + uint32_t diff = prot_size - patch_offset; + patch_offset += diff; + patch_size -= diff; + fseek(f_ips, diff, SEEK_CUR); + goto IPS_DATA_PATCH_OFFSET_WITHIN_BOUNDS; + } else { + fseek(f_ips, patch_size, SEEK_CUR); + } + } else { + IPS_DATA_PATCH_OFFSET_WITHIN_BOUNDS: + patch_offset -= prot_size; + uint32_t read_size = patch_size; + if (patch_offset + read_size > mem_size) { + read_size = mem_size - patch_offset; + } + if (fread(mem + patch_offset, read_size, 1, f_ips) != 1) { + break; + } + if (patch_size > read_size) { + fseek(f_ips, patch_size - read_size, SEEK_CUR); + } + } + } + } +} + + +static inline uint8_t hex_nybble_to_u8(const char nybble) { + if ('0' <= nybble && nybble <= '9') { + return nybble - '0'; + } else if ('a' <= nybble && nybble <= 'f') { + return nybble - 'a' + 0xa; + } else { + return nybble - 'A' + 0xA; + } +} + +static bool name_matches_hash(const char *name, size_t name_len, const void *hash, size_t hash_size) { + /* Validate name is hex build id. */ + for (unsigned int i = 0; i < name_len - 4; i++) { + if (isxdigit(name[i]) == 0) { + return false; + } + } + + /* Read hash from name. */ + uint8_t hash_from_name[0x20] = {0}; + for (unsigned int name_ofs = 0, id_ofs = 0; name_ofs < name_len - 4; id_ofs++) { + hash_from_name[id_ofs] |= hex_nybble_to_u8(name[name_ofs++]) << 4; + hash_from_name[id_ofs] |= hex_nybble_to_u8(name[name_ofs++]); + } + + return memcmp(hash, hash_from_name, hash_size) == 0; + +} + +static bool apply_ips_patches(const char *dir, void *mem, size_t mem_size, size_t prot_size, const void *hash, size_t hash_size) { + bool any_patches = false; + char path[0x301] = {0}; + snprintf(path, sizeof(path) - 1, "%s", dir); + DIR *patches_dir = opendir(path); + struct dirent *pdir_ent; + if (patches_dir != NULL) { + /* Iterate over the patches directory to find patch subdirectories. */ + while ((pdir_ent = readdir(patches_dir)) != NULL) { + if (strcmp(pdir_ent->d_name, ".") == 0 || strcmp(pdir_ent->d_name, "..") == 0) { + continue; + } + snprintf(path, sizeof(path) - 1, "%s/%s", dir, pdir_ent->d_name); + DIR *patch_dir = opendir(path); + struct dirent *ent; + if (patch_dir != NULL) { + /* Iterate over the patch subdirectory to find .ips patches. */ + while ((ent = readdir(patch_dir)) != NULL) { + if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { + continue; + } + size_t name_len = strlen(ent->d_name); + if ((4 < name_len && name_len <= 0x44) && ((name_len & 1) == 0) && strcmp(ent->d_name + name_len - 4, ".ips") == 0 && name_matches_hash(ent->d_name, name_len, hash, hash_size)) { + snprintf(path, sizeof(path) - 1, "%s/%s/%s", dir, pdir_ent->d_name, ent->d_name); + FILE *f_ips = fopen(path, "rb"); + if (f_ips != NULL) { + uint8_t header[5]; + if (fread(header, 5, 1, f_ips) == 1) { + if (memcmp(header, IPS_MAGIC, 5) == 0) { + any_patches = true; + apply_ips_patch(mem, mem_size, prot_size, false, f_ips); + } else if (memcmp(header, IPS32_MAGIC, 5) == 0) { + any_patches = true; + apply_ips_patch(mem, mem_size, prot_size, true, f_ips); + } + } + fclose(f_ips); + } + } + } + closedir(patch_dir); + } + } + closedir(patches_dir); + } + + return any_patches; +} + +void apply_kernel_ips_patches(void *kernel, size_t kernel_size) { + uint8_t hash[0x20]; + se_calculate_sha256(hash, kernel, kernel_size); + apply_ips_patches("atmosphere/kernel_patches", kernel, kernel_size, 0, hash, sizeof(hash)); +} + +static void kip1_blz_uncompress(void *hdr_end) { + uint32_t addl_size = ((uint32_t *)hdr_end)[-1]; + uint32_t header_size = ((uint32_t *)hdr_end)[-2]; + uint32_t cmp_and_hdr_size = ((uint32_t *)hdr_end)[-3]; + + unsigned char *cmp_start = (unsigned char *)(((uintptr_t)hdr_end) - cmp_and_hdr_size); + uint32_t cmp_ofs = cmp_and_hdr_size - header_size; + uint32_t out_ofs = cmp_and_hdr_size + addl_size; + + while (out_ofs) { + unsigned char control = cmp_start[--cmp_ofs]; + for (unsigned int i = 0; i < 8; i++) { + if (control & 0x80) { + if (cmp_ofs < 2) { + fatal_error("KIP1 decompression out of bounds!\n"); + } + cmp_ofs -= 2; + uint16_t seg_val = ((unsigned int)cmp_start[cmp_ofs+1] << 8) | cmp_start[cmp_ofs]; + uint32_t seg_size = ((seg_val >> 12) & 0xF) + 3; + uint32_t seg_ofs = (seg_val & 0x0FFF) + 3; + if (out_ofs < seg_size) { + /* Kernel restricts segment copy to stay in bounds. */ + seg_size = out_ofs; + } + out_ofs -= seg_size; + + for (unsigned int j = 0; j < seg_size; j++) { + cmp_start[out_ofs + j] = cmp_start[out_ofs + j + seg_ofs]; + } + } else { + /* Copy directly. */ + if (cmp_ofs < 1) { + fatal_error("KIP1 decompression out of bounds!\n"); + } + cmp_start[--out_ofs] = cmp_start[--cmp_ofs]; + } + control <<= 1; + if (out_ofs == 0) { + return; + } + } + } +} + +static kip1_header_t *kip1_uncompress(kip1_header_t *kip, size_t *size) { + kip1_header_t new_header = *kip; + for (unsigned int i = 0; i < 3; i++) { + new_header.section_headers[i].compressed_size = new_header.section_headers[i].out_size; + } + new_header.flags &= 0xF8; + + *size = kip1_get_size_from_header(&new_header); + + unsigned char *new_kip = calloc(1, *size); + if (new_kip == NULL) { + return NULL; + } + *((kip1_header_t *)new_kip) = new_header; + + size_t new_offset = 0x100; + size_t old_offset = 0x100; + for (unsigned int i = 0; i < 3; i++) { + memcpy(new_kip + new_offset, (unsigned char *)kip + old_offset, kip->section_headers[i].compressed_size); + if (kip->flags & (1 << i)) { + kip1_blz_uncompress(new_kip + new_offset + kip->section_headers[i].compressed_size); + } + new_offset += kip->section_headers[i].out_size; + old_offset += kip->section_headers[i].compressed_size; + } + + return (kip1_header_t *)new_kip; +} + +kip1_header_t *apply_kip_ips_patches(kip1_header_t *kip, size_t kip_size) { + uint8_t hash[0x20]; + se_calculate_sha256(hash, kip, kip_size); + + size_t uncompressed_kip_size; + kip1_header_t *uncompressed_kip = kip1_uncompress(kip, &uncompressed_kip_size); + if (uncompressed_kip == NULL) { + return NULL; + } + + if (apply_ips_patches("atmosphere/kip_patches", uncompressed_kip, uncompressed_kip_size, 0x100, hash, sizeof(hash))) { + return uncompressed_kip; + } else { + free(uncompressed_kip); + return NULL; + } +} diff --git a/fusee/fusee-secondary/src/ips.h b/fusee/fusee-secondary/src/ips.h new file mode 100644 index 000000000..6db00a1f1 --- /dev/null +++ b/fusee/fusee-secondary/src/ips.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2018 Atmosphère-NX + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU General Public License, + * version 2, as published by the Free Software Foundation. + * + * This program is distributed in the hope it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FUSEE_IPS_H +#define FUSEE_IPS_H + +#include "utils.h" +#include "kip.h" +#include + +void apply_kernel_ips_patches(void *kernel, size_t kernel_size); +kip1_header_t *apply_kip_ips_patches(kip1_header_t *kip, size_t kip_size); + +#endif