Add IOS syscall patch

This commit is contained in:
Spotlight 2021-12-30 16:31:05 -06:00
parent f905d22563
commit bd625c0779
No known key found for this signature in database
GPG Key ID: 874AA355B3209BDC
7 changed files with 415 additions and 3 deletions

View File

@ -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, `*.<basedomain>` 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_<name>.md` for more information on what these contain.
- The patched WAD is written to disk.

View File

@ -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.
- [[`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.

153
docs/patch_overwrite_ios.md Normal file
View File

@ -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 <RETURN>
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;
}
```

View File

@ -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)

82
modify_dol.go Normal file
View File

@ -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)
}

142
patches.go Normal file
View File

@ -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
},
},
}

26
powerpc.go Normal file
View File

@ -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}
}