mirror of
https://wiilab.wiimart.org/wiimart/WiiMart-Patcher
synced 2025-09-04 04:21:19 +02:00
Add IOS syscall patch
This commit is contained in:
parent
f905d22563
commit
bd625c0779
@ -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.
|
||||
|
@ -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
153
docs/patch_overwrite_ios.md
Normal 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;
|
||||
}
|
||||
```
|
8
main.go
8
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)
|
||||
|
82
modify_dol.go
Normal file
82
modify_dol.go
Normal 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
142
patches.go
Normal 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
26
powerpc.go
Normal 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}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user