diff --git a/README.md b/README.md index 321fd9d..aa1d2aa 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,6 @@ Throughout its operation, the patcher will perform the following: - If `output/root.cer` is not present, a 1024-bit (RSA), SHA-1 CA certificate will be generated. - At the same time, `*.` will be issued for ease of use. See `output/server.pem` and `output/server.key` for usage with nginx or similar servers. - Modifications are made to the application's main `.arc` (within content index 2) to permit Opera loading the base domain, and the customized certificates. - - Patches to the application's main dol are also performed. Please see `patches.go` for more information on what these contain. + - Patches to the application's main dol are also performed. Please see `docs/patch_.md` for more information on what these contain. - The patched WAD is written to disk. \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 0ffae14..f382503 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,5 +2,6 @@ This directory contains documentation about given patches applied. Contents: - - `opcacrt6.yml`: A [Kaitai](https://kaitai.io) structure describing a very basic `opcacrt6.dat`. -It does not attempt to handle things such as client certificates or user passwords. \ No newline at end of file + - [[`opcacrt6.yml`]]: A [Kaitai](https://kaitai.io) structure describing a very basic `opcacrt6.dat`. +It does not attempt to handle things such as client certificates or user passwords. + - [[`patch_overwrite_ec.md`]]: An explanation as to why IOS needs to be patched for our operations. \ No newline at end of file diff --git a/docs/patch_overwrite_ios.md b/docs/patch_overwrite_ios.md new file mode 100644 index 0000000..06f597d --- /dev/null +++ b/docs/patch_overwrite_ios.md @@ -0,0 +1,153 @@ +# Patch: Overwrite IOS Syscall for ES + +## Motivation +When installing a custom title via the Wii Shop Channel, the signature expects a valid signature. +Most homebrew-based WAD installers apply IOS patches or manually insert contents to their proper place. +However, as we use Nintendo's official code, we lack this luxury. + +When EC finishes downloading a title, it calls [ES](https://wiibrew.org/wiki//dev/es) within IOS to handle title installation. +Along the process, the WAD's certificate is verified, and content signatures are verified. +This is done via `IOSC_VerifyPublicKeySign`, an [IOS syscall](https://wiibrew.org/wiki/IOS/Syscalls) available for ES. + +As we are not Nintendo and lack their private key, we cannot sign officially, and fail all checks. +We need to bypass this. + +## Explanation +It was determined that the best solution was to overwrite `IOSC_VerifyPublicKeySign` itself. + +The Wii Shop Channel uses IOS 56. Across a Wii and a vWii, analysis of the IOSP module (roughly its primary kernel) +shows that the `IOSC_VerifyPublicKeySign` syscall has its function present at `0x13a73ad4` within MEM2. (Dolphin silently ignores this write.) + +The function - in ARM THUMB mode - looks similar to the following (as copied from Ghidra): +``` + ************************************************************** + * * + * FUNCTION * + ************************************************************** + int __stdcall IOSC_VerifyPublicKeySign(uint8_t * inputDa + assume LRset = 0x0 + assume TMode = 0x1 + int r0:4 + uint8_t * r0:4 inputData + u32 r1:4 inputSize + int r2:4 publicHandle + uint8_t * r3:4 signData + undefined4 Stack[-0x24]:4 local_24 XREF[1]: 13a73b54(R) + undefined4 Stack[-0x28]:4 local_28 XREF[2]: 13a73ae4(W), + 13a73b8a(R) + undefined4 Stack[-0x2c]:4 local_2c XREF[2]: 13a73b68(W), + 13a73b8c(W) + IOSC_VerifyPublicKeySign+1 XREF[0,1]: ffff9580(*) + IOSC_VerifyPublicKeySign +13a73ad4 b5 f0 push { r4, r5, r6, r7, lr } +13a73ad6 46 57 mov r7, r10 +13a73ad8 46 4e mov r6, r9 +13a73ada 46 45 mov r5, r8 +13a73adc b4 e0 push { r5, r6, r7 } +``` + +Thankfully, IOS utilizes [TPCS](https://developer.arm.com/documentation/espc0002/1-0), the Thumb Procedure Call Standard. +This states that `r0` is utilized as the first return value, and Ghidra accurately identifies this. + +Therefore, we are able to simply return `IOS_SUCCESS` (or 0) to negate the entire check: +``` + IOSC_VerifyPublicKeySign +13a73ad4 20 00 mov r0, #0x0 +13a73ad6 47 70 bx lr +``` + + +This leaves us with the challenge of writing 4 bytes - `20 00 47 70` - to `0xd3a73ad4` as identified previously. + +## Execution +Ideally, we would not rely on a hardcoded address for this patch, and would instead choose to iterate through memory. +However, space constraints for our patch made this difficult. We chose to hardcode the address. +(Further testing may result in this patch having a different method of application.) + +By default, the PowerPC core (Broadway) has memory protections enabled, preventing from us editing IOS's memory in MEM2. +We need to apply several patches to achieve our goal. + +1. e need to obtain access to overwriting IOS memory. +We set the Access Rights field in the WAD's Title Metadata (or `.tmd`) to 0x00000003, permitting memory access. We will use this with `MEM2_PROT` later. + +This is thankfully a very quick fix. + +2. We need to find space to put our own custom function within the binary. + +Via symbols within the main ARC, we find a C++ class named `textinput::EventObserver` with 4 functions in a row that +immediately `blr` - returning with no other logic: + - At `0x80014420`, `textinput::EventObserver::onSE` + - At `0x80014430`, `textinput::EventObserver::onEvent` + - At `0x80014440`, `textinput::EventObserver::onCommand` + - At `0x80014450`, `textinput::EventObserver::onInput` + +An unrelated function resides at `0x80014460`, so we cannot continue. However, we do not need to. + +We consolidate these four functions into a single function at `0x80014420` - our own `textinput::EventObserver::doNothing`, if you will. +We must additionally update references to this single at two separate virtual tables: + - `textinput::EventObserver` at `0x802f7a9` + - `ipl::keyboard::EventObserver` at `0x802f8418` + +3. We need to devise a way to have the channel overwrite IOS memory. + +We have carved out our own space at `0x80014428` to put a function. +Thankfully, the operation is fairly simple: + - Write to [`MEM_PROT`](https://wiibrew.org/wiki/Hardware/Hollywood_Registers) and disable it. It is on by default. + - We use `0xcd8b420a` instead of `0x0d8b420a` as that appears to be where it is mapped for us. + - Dolphin appears to silently ignore this MMIO access. One day, we may want to not apply the patch should we be able to open `/dev/dolphin`. + - Write `20 00 47 70` as described above to `0x13a73ad4` to negate `IOSC_VerifyPublicKeySign`. + - Again, we must actually use `0xd3a73ad4` due to mapping. + - Dolphin once again appears to ignore this, thankfully. + - Clear cache + - TODO: Is this actually functional? + +We write and apply the following PowerPC assembly to achieve this task: +```asm +overwriteIOSPatch: + ; Load 0x0d8b420a, location of MEM_PROT, to r9. + lis r9, 0xcd8b + ori r9, r9, 0x420a + ; We wish to write 0x2 in order to disable. + li r10, 0x2 + + ; And... write! + sth r10, 0x0(r9) + eieio + + ; Load 0xd3a73ad4, location of of IOSC_VerifyPublicKeySig, to r9. + lis r9, 0xd3a7 + ori r9, r9, 0x73ad4 + ; 0x20004770 represents our actual patch. + lis r10, 0x2000 + ori r10, r10, 0x4770 + + ; And... write. + stw r10, 0x0(r9) + + ; Clear cache + dcbi 0, r10 + blr +``` + + +4. We need to determine the best way to call our custom patching function. +Using the aforementioned symbols we find `ES_InitLib`, called once during initialization to open a handle with `/dev/es`. + +We insert a call to our function in its epilog, immediately before loading the previous LR from stack and branching back. +This makes its flow roughly the following pseudocode: +```c +int ES_InitLib() { + int fd = 0; + if (ES_HANDLE < 0) { + ES_HANDLE = IOS_Open("/dev/es", 0); + if (ES_HANDLE < 0) { + fd = ES_HANDLE; + } + } + + // Our custom code + overwriteIOSMemory(); + + return fd; +} +``` diff --git a/main.go b/main.go index 38aa4a2..f729452 100644 --- a/main.go +++ b/main.go @@ -84,6 +84,14 @@ func main() { mainDol, err = originalWad.GetContent(1) check(err) + // Permit r/w access to MEM2_PROT via the TMD. + // See docs/patch_overwrite_ios.md for more information! + originalWad.TMD.AccessRightsFlags = 0x3 + + // Apply all DOL patches + log.Println("Applying DOL patches...") + applyDefaultPatches() + // Load main ARC arcData, err := originalWad.GetContent(2) check(err) diff --git a/modify_dol.go b/modify_dol.go new file mode 100644 index 0000000..4e1766d --- /dev/null +++ b/modify_dol.go @@ -0,0 +1,82 @@ +package main + +import ( + "bytes" + "errors" + "log" +) + +var ( + ErrInconsistentPatch = errors.New("before and after data present within file are not the same size") + ErrPatchOutOfRange = errors.New("patch cannot be applied past binary size") + ErrInvalidPatch = errors.New("before data present within patch did not exist in file") +) + +// Patch represents a patch applied to the main binary. +type Patch struct { + // Name is an optional name for this patch. + // If present, its name will be logged upon application. + Name string + + // AtOffset is the offset within the file this patch should be applied at. + AtOffset int + + // Before is an array of the bytes to find for, i.e. present within the original file. + Before []byte + + // After is an array of the bytes to replace with. + After []byte +} + +// PatchSet represents multiple patches available to be applied. +type PatchSet []Patch + +// applyPatch applies the given patch to the main DOL. +func applyPatch(patch Patch) error { + // Print name if present + if patch.Name != "" { + log.Println("Applying patch " + patch.Name) + } + + // Ensure consistency + if len(patch.Before) != len(patch.After) { + return ErrInconsistentPatch + } + if patch.AtOffset > len(mainDol) { + return ErrPatchOutOfRange + } + + // Either Before or After should return the same length. + patchLen := len(patch.Before) + + // Ensure original bytes are present + originalBytes := mainDol[patch.AtOffset : patch.AtOffset+patchLen] + if !bytes.Equal(originalBytes, patch.Before) { + return ErrInvalidPatch + } + + // Apply patch + copy(mainDol[patch.AtOffset:], patch.After) + + return nil +} + +// applyPatchSet iterates through all possible patches, noting their name. +func applyPatchSet(setName string, set PatchSet) { + log.Printf("Handling patch set \"%s\":", setName) + + for _, patch := range set { + err := applyPatch(patch) + check(err) + } +} + +// applyDefaultPatches iterates through a list of default patches. +func applyDefaultPatches() { + applyPatchSet("Overwrite IOS Syscall for ES", OverwriteIOSPatch) +} + +// emptyBytes returns an empty byte array of the given length. +func emptyBytes(length int) []byte { + return bytes.Repeat([]byte{0x00}, length) +} diff --git a/patches.go b/patches.go new file mode 100644 index 0000000..2e98647 --- /dev/null +++ b/patches.go @@ -0,0 +1,142 @@ +package main + +// OverwriteIOSPatch effectively nullifies IOSC_VerifyPublicKeySign. +// See docs/patch_overwrite_ios.md for more information. +var OverwriteIOSPatch = PatchSet{ + Patch{ + Name: "Clear textinput::EventObserver functions", + AtOffset: 20320, + + Before: Instructions{ + // Function: textinput::EventObserver::onSE + BLR(), + padding, + padding, + padding, + // Function: textinput::EventObserver::onEvent + BLR(), + padding, + padding, + padding, + // Function: textinput::EventObserver::onCommand + BLR(), + padding, + padding, + padding, + // Function: textinput::EventObserver::onInput + BLR(), + padding, + padding, + padding, + }.toBytes(), + + // We wish to clear these so that our custom overwriteIOSMemory + // function does not somehow conflict. + After: emptyBytes(64), + }, + Patch{ + Name: "Repair textinput::EventObserver vtable", + AtOffset: 3095452, + + Before: []byte{ + 0x80, 0x01, 0x44, 0x50, // onSE + 0x80, 0x01, 0x44, 0x40, // onEvent + 0x80, 0x01, 0x44, 0x30, // onCommand + 0x80, 0x01, 0x44, 0x20, // onInput + }, + After: []byte{ + // These are all pointers to our so-called doNothing. + 0x80, 0x01, 0x44, 0x20, + 0x80, 0x01, 0x44, 0x20, + 0x80, 0x01, 0x44, 0x20, + 0x80, 0x01, 0x44, 0x20, + }, + }, + Patch{ + Name: "Repair ipl::keyboard::EventObserver vtable", + AtOffset: 3097888, + + Before: []byte{ + 0x80, 0x01, 0x44, 0x50, // onSE + 0x80, 0x01, 0x84, 0xE0, // ipl::keyboard::EventObserver::onCommand - not patched + 0x80, 0x01, 0x44, 0x30, // onCommand + }, + After: []byte{ + 0x80, 0x01, 0x44, 0x20, // doNothing + 0x80, 0x01, 0x84, 0xE0, // ipl::keyboard::EventObserver::onCommand - not patched + 0x80, 0x01, 0x44, 0x20, // doNothing + }, + }, + Patch{ + Name: "Insert overwriteIOSMemory", + AtOffset: 20328, + + // This area should be cleared. + Before: emptyBytes(48), + After: []byte{ + // We want r9 to store the location of MEM_PROT at 0x0d8b420a. + // For us, this is mapped to 0xcd8b420a. + // lis r9, 0xcd8b + 0x3D, 0x20, 0xCD, 0x8B, + // ori r9, r9, 0x420a + 0x61, 0x29, 0x42, 0x0A, + + // We want to write 0x2 and unlock everything. + // li r10, 0x02 + 0x39, 0x40, 0x00, 0x02, + + // Write! + // sth r10, 0x0(r9) + 0xB1, 0x49, 0x00, 0x00, + // Flush memory + // eieio + 0x7C, 0x00, 0x06, 0xAC, + + // Location of IOSC_VerifyPublicKeySign + // lis r9, 0xd3a7 + 0x3D, 0x20, 0xD3, 0xA7, + // ori r9, r9, 0x3ad4 + 0x61, 0x29, 0x3A, 0xD4, + + // Write our custom THUMB. + // 0x20004770 is equivalent to: + // mov r0, #0x0 + // bx lr + // lis r10, 0x2000 + 0x3D, 0x40, 0x20, 0x00, + // ori r10, r10, 0x4770 + 0x61, 0x4A, 0x47, 0x70, + + // Write! + // stw r10, 0x0(r9) + 0x91, 0x49, 0x00, 0x00, + // Possibly clear cache + // TODO(spotlightishere): Is this needed? + // dcbi 0, r10 + 0x7C, 0x00, 0x53, 0xAC, + // And finish. + // blr + 0x4E, 0x80, 0x00, 0x20, + }, + }, + Patch{ + Name: "Modify ES_InitLib", + AtOffset: 2399844, + + // We inject in the epilog of the function. + Before: []byte{ + 0x80, 0x01, 0x00, 0x14, // lwz r0, local_res4(r1) + 0x7C, 0x08, 0x03, 0xA6, // mtspr LR, r0 + 0x38, 0x21, 0x00, 0x10, // addi r1, r1, 0x10 + 0x4E, 0x80, 0x00, 0x20, // blr + 0x00, 0x00, 0x00, 0x00, // ; empty space following function + }, + After: []byte{ + 0x80, 0x01, 0x00, 0x14, // lwz r0, local_res4(r1) + 0x4B, 0xDB, 0xB1, 0x01, // bl overwriteIOSMemory @ 0x80014428 + 0x7C, 0x08, 0x03, 0xA6, // mtspr LR, r0 + 0x38, 0x21, 0x00, 0x10, // addi r1, r1, 0x10 + 0x4E, 0x80, 0x00, 0x20, // blr + }, + }, +} diff --git a/powerpc.go b/powerpc.go new file mode 100644 index 0000000..ec7271e --- /dev/null +++ b/powerpc.go @@ -0,0 +1,26 @@ +package main + +// Instruction represents a 4-byte PowerPC instruction. +type Instruction [4]byte + +// Instructions represents a group of PowerPC instructions. +type Instructions []Instruction + +// toBytes returns the bytes necessary to represent these instructions. +func (i Instructions) toBytes() []byte { + var contents []byte + + for _, instruction := range i { + contents = append(contents, instruction[:]...) + } + + return contents +} + +// padding is not an actual instruction - it represents 4 zeros. +var padding Instruction = [4]byte{0x00, 0x00, 0x00, 0x00} + +// BLR represents the blr PowerPC instruction. +func BLR() Instruction { + return [4]byte{0x4E, 0x80, 0x00, 0x20} +}